From cce120abe51e152baaa2d40a002348b0a252f9e7 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 10 Mar 2019 13:13:47 -0700 Subject: [PATCH 0001/1221] Not much was changed in that new update, core features still work, but some code could be rewritten --- cogs/modmail.py | 2 +- cogs/plugins.py | 2 +- cogs/utility.py | 5 +++-- requirements.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 660aff14fb..247b0dd2e3 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -18,7 +18,7 @@ from core.utils import format_preview, User -class Modmail: +class Modmail(commands.Cog): """Commands directly related to Modmail functionality.""" def __init__(self, bot: Bot): diff --git a/cogs/plugins.py b/cogs/plugins.py index 54ed742f53..a226307984 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -19,7 +19,7 @@ class DownloadError(Exception): pass -class Plugins: +class Plugins(commands.Cog): """Plugins expand Modmail functionality by allowing third-party addons. These addons could have a range of features from moderation to simply diff --git a/cogs/utility.py b/cogs/utility.py index 2f8b1a625b..e3c4161ae6 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -26,7 +26,7 @@ logger = logging.getLogger('Modmail') -class Utility: +class Utility(commands.Cog): """General commands that provide utility""" def __init__(self, bot: Bot): @@ -52,7 +52,7 @@ async def format_cog_help(self, ctx, cog): fmts = [''] for cmd in sorted(self.bot.commands, key=lambda cmd: cmd.qualified_name): - if cmd.instance is cog and not cmd.hidden and \ + if cmd.cog is cog and not cmd.hidden and \ await self.verify_checks(ctx, cmd): new_fmt = f'`{prefix + cmd.qualified_name}` - ' new_fmt += f'{cmd.short_doc}\n' @@ -580,6 +580,7 @@ async def set_presence(self, *, presence['status'] = (status, msg) return presence + @commands.Cog.listener() async def on_ready(self): # Wait until config cache is populated with stuff from db await self.bot.config.wait_until_ready() diff --git a/requirements.txt b/requirements.txt index 13c53cf338..e97008a573 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -git+https://github.com/Rapptz/discord.py@7f4c57dd5ad20b7fa10aea485f674a4bc24b9547 +git+https://github.com/Rapptz/discord.py colorama>=0.4.0 python-dateutil>=2.7.0 emoji>=0.2 From 2c15166a9bb764ade997ed5c877ea31e4262b068 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Apr 2019 20:45:56 -0700 Subject: [PATCH 0002/1221] Remove modmail.tk link in changelog --- core/changelog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/changelog.py b/core/changelog.py index 60e4e0186a..32ad8f9b01 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -68,7 +68,6 @@ def embed(self) -> Embed: embed.set_author( name=f'{self.version} - Changelog', icon_url=self.bot.user.avatar_url, - url='https://modmail.tk/changelog' ) for name, value in self.fields.items(): From 2a7d126c902163b98551904fa81b91697b9899bf Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Apr 2019 21:37:51 -0700 Subject: [PATCH 0003/1221] Removed api --- bot.py | 97 +++++----------------------------- cogs/modmail.py | 22 +------- cogs/utility.py | 66 +++++++++++------------ core/changelog.py | 11 ++-- core/clients.py | 129 --------------------------------------------- core/decorators.py | 15 ++---- core/models.py | 17 ------ core/thread.py | 8 +-- 8 files changed, 54 insertions(+), 311 deletions(-) diff --git a/bot.py b/bot.py index 628402a7f2..84d95e3f40 100644 --- a/bot.py +++ b/bot.py @@ -31,6 +31,7 @@ import sys from datetime import datetime +from pkg_resources import parse_version from types import SimpleNamespace import discord @@ -45,7 +46,7 @@ from motor.motor_asyncio import AsyncIOMotorClient from core.changelog import Changelog -from core.clients import ModmailApiClient, SelfHostedClient, PluginDatabaseClient +from core.clients import SelfHostedClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import info, error from core.models import Bot @@ -104,14 +105,10 @@ def __init__(self): self._configure_logging() - if self.self_hosted: - self._db = AsyncIOMotorClient(self.config.mongo_uri).modmail_bot - self._api = SelfHostedClient(self) - else: - self._api = ModmailApiClient(self) + self._db = AsyncIOMotorClient(self.config.mongo_uri).modmail_bot + self._api = SelfHostedClient(self) self.plugin_db = PluginDatabaseClient(self) - self.data_task = self.loop.create_task(self.data_loop()) self.autoupdate_task = self.loop.create_task(self.autoupdate_loop()) self._load_extensions() self.owner = None @@ -144,10 +141,6 @@ def version(self): def db(self): return self._db - @property - def self_hosted(self): - return bool(self.config.get('mongo_uri', '')) - @property def api(self): return self._api @@ -210,12 +203,6 @@ def run(self, *args, **kwargs): except Exception: logger.critical(error('Fatal exception'), exc_info=True) finally: - try: - self.data_task.cancel() - self.loop.run_until_complete(self.data_task) - except asyncio.CancelledError: - logger.debug('data_task has been cancelled') - try: self.autoupdate_task.cancel() self.loop.run_until_complete(self.autoupdate_task) @@ -345,15 +332,8 @@ def main_color(self): async def on_connect(self): logger.info(LINE) - if not self.self_hosted: - logger.info(info('MODE: Using the Modmail API')) - logger.info(LINE) - await self.validate_api_token() - logger.info(LINE) - else: - logger.info(info('Mode: Self-hosting logs.')) - await self.validate_database_connection() - logger.info(LINE) + await self.validate_database_connection() + logger.info(LINE) logger.info(info('Connected to gateway.')) await self.config.refresh() @@ -784,32 +764,6 @@ def overwrites(ctx): ) return overwrites - async def validate_api_token(self): - try: - self.config.modmail_api_token - except KeyError: - logger.critical(error(f'MODMAIL_API_TOKEN not found.')) - logger.critical(error('Set a config variable called ' - 'MODMAIL_API_TOKEN with a token from ' - 'https://dashboard.modmail.tk.')) - logger.critical(error('If you want to self-host logs, ' - 'input a MONGO_URI config variable.')) - logger.critical(error('A Modmail API token is not needed ' - 'if you are self-hosting logs.')) - - return await self.logout() - else: - valid = await self.api.validate_token() - if not valid: - logger.critical(error('Invalid MODMAIL_API_TOKEN - get one ' - 'from https://dashboard.modmail.tk')) - return await self.logout() - - user = await self.api.get_user_info() - username = user['user']['username'] - logger.info(info('Validated token.')) - logger.info(info('GitHub user: ' + username)) - async def validate_database_connection(self): try: await self.db.command('buildinfo') @@ -821,32 +775,6 @@ async def validate_database_connection(self): else: logger.info(info('Successfully connected to the database.')) - async def data_loop(self): - await self.wait_until_ready() - self.owner = (await self.application_info()).owner - - while not self.is_closed(): - data = { - "owner_name": str(self.owner), - "owner_id": self.owner.id, - "bot_id": self.user.id, - "bot_name": str(self.user), - "avatar_url": self.user.avatar_url, - "guild_id": self.guild_id, - "guild_name": self.guild.name, - "member_count": len(self.guild.members), - "uptime": (datetime.utcnow() - - self.start_time).total_seconds(), - "latency": f'{self.ws.latency * 1000:.4f}', - "version": self.version, - # TODO: change to `self_hosted` - "selfhosted": self.self_hosted, - "last_updated": str(datetime.utcnow()) - } - - await self.api.post_metadata(data) - await asyncio.sleep(3600) - async def autoupdate_loop(self): await self.wait_until_ready() @@ -855,16 +783,19 @@ async def autoupdate_loop(self): logger.info(LINE) return - if self.self_hosted and not self.config.get('github_access_token'): + if not self.config.get('github_access_token'): logger.warning(info('GitHub access token not found.')) logger.warning(info('Autoupdates disabled.')) logger.info(LINE) return + logger.info(info('Autoupdate loop started.')) + while not self.is_closed(): - metadata = await self.api.get_metadata() + changelog = await Changelog.from_url(self) + latest = changelog.latest_version - if metadata['latest_version'] != self.version: + if parse_version(self.version) < parse_version(latest.version): data = await self.api.update_repository() embed = discord.Embed(color=discord.Color.green()) @@ -875,10 +806,8 @@ async def autoupdate_loop(self): icon_url=user['avatar_url'], url=user['url']) embed.set_footer(text=f"Updating Modmail v{self.version} " - f"-> v{metadata['latest_version']}") + f"-> v{latest.version}") - changelog = await Changelog.from_url(self) - latest = changelog.latest_version embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=value) diff --git a/cogs/modmail.py b/cogs/modmail.py index f17e97cade..1fce69fa0d 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -372,11 +372,7 @@ def format_log_embeds(self, logs, avatar_url): created_at = parser.parse(entry['created_at']) - log_url = ( - f"https://logs.modmail.tk/{key}" - if not self.bot.self_hosted else - self.bot.config.log_url.strip('/') + f'/logs/{key}' - ) + log_url = self.bot.config.log_url.strip('/') + f'/logs/{key}' username = entry['recipient']['name'] + '#' username += entry['recipient']['discriminator'] @@ -443,14 +439,6 @@ async def logs(self, ctx, *, member: User = None): @checks.has_permissions(manage_messages=True) async def closed_by(self, ctx, *, user: User = None): """Returns all logs closed by a user.""" - if not self.bot.self_hosted: - embed = discord.Embed( - color=discord.Color.red(), - description='This command only works if ' - 'you are self-hosting your logs.' - ) - return await ctx.send(embed=embed) - user = user or ctx.author query = { @@ -485,14 +473,6 @@ async def search(self, ctx, limit: Optional[int] = None, *, query): await ctx.trigger_typing() - if not self.bot.self_hosted: - embed = discord.Embed( - color=discord.Color.red(), - description='This command only works if you ' - 'are self-hosting your logs.' - ) - return await ctx.send(embed=embed) - query = { 'guild_id': str(self.bot.guild_id), 'open': False, diff --git a/cogs/utility.py b/cogs/utility.py index bc24a8cbf7..0ab77a34d1 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -7,6 +7,7 @@ from difflib import get_close_matches from io import StringIO from json import JSONDecodeError +from pkg_resources import parse_version from textwrap import indent import discord @@ -203,19 +204,8 @@ async def about(self, ctx): desc += 'an organised manner.' embed.description = desc - url = 'https://api.modmail.tk/metadata' - async with self.bot.session.get(url) as resp: - try: - meta = await resp.json() - except (JSONDecodeError, ClientResponseError): - meta = None - embed.add_field(name='Uptime', value=self.bot.uptime) - if meta: - embed.add_field(name='Instances', value=meta['instances']) - else: - embed.add_field(name='Latency', - value=f'{self.bot.latency * 1000:.2f} ms') + embed.add_field(name='Latency', value=f'{self.bot.latency * 1000:.2f} ms') embed.add_field(name='Version', value=f'[`{self.bot.version}`]' @@ -225,12 +215,13 @@ async def about(self, ctx): footer = f'Bot ID: {self.bot.user.id}' - if meta: - if self.bot.version != meta['latest_version']: - footer = ("A newer version is available " - f"v{meta['latest_version']}") - else: - footer = 'You are up to date with the latest version.' + changelog = await Changelog.from_url(self.bot) + latest = changelog.latest_version + + if parse_version(self.bot.version) < parse_version(latest.version): + footer = f"A newer version is available v{latest.version}" + else: + footer = 'You are up to date with the latest version.' embed.add_field(name='GitHub', value='https://github.com/kyb3r/modmail', @@ -364,18 +355,19 @@ async def github(self, ctx): @trigger_typing async def update(self, ctx): """Updates the bot, this only works with heroku users.""" - metadata = await self.bot.api.get_metadata() + changelog = await Changelog.from_url(self.bot) + latest = changelog.latest_version desc = (f'The latest version is [`{self.bot.version}`]' '(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)') - embed = Embed( - title='Already up to date', - description=desc, - color=self.bot.main_color - ) + if parse_version(self.bot.version) >= parse_version(latest.version): + embed = Embed( + title='Already up to date', + description=desc, + color=self.bot.main_color + ) - if metadata['latest_version'] == self.bot.version: data = await self.bot.api.get_user_info() if not data.get('error'): user = data['user'] @@ -387,19 +379,17 @@ async def update(self, ctx): commit_data = data['data'] user = data['user'] - embed.title = None - embed.set_author(name=user['username'], - icon_url=user['avatar_url'], - url=user['url']) - embed.set_footer(text=f'Updating Modmail v{self.bot.version} ' - f"-> v{metadata['latest_version']}") if commit_data: + embed = Embed(color=self.bot.main_color) + + embed.set_footer(text=f'Updating Modmail v{self.bot.version} ' + f'-> v{latest.version}') + embed.set_author(name=user['username'] + ' - Updating bot', icon_url=user['avatar_url'], url=user['url']) - changelog = await Changelog.from_url(self.bot) - latest = changelog.latest_version + embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=value) @@ -409,8 +399,14 @@ async def update(self, ctx): embed.add_field(name='Merge Commit', value=f'[`{short_sha}`]({html_url})') else: - embed.description = ('Already up to date ' - 'with master repository.') + embed = Embed( + title='Already up to date with master repository.', + description='No further updates required', + color=self.bot.main_color + ) + embed.set_author(name=user['username'], + icon_url=user['avatar_url'], + url=user['url']) return await ctx.send(embed=embed) diff --git a/core/changelog.py b/core/changelog.py index 32ad8f9b01..519040b975 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -36,14 +36,14 @@ class Version: def __init__(self, bot: Bot, version: str, lines: str): self.bot = bot - self.version = version + self.version = version.lstrip('vV') self.lines = [x for x in lines.splitlines() if x] self.fields = defaultdict(str) self.description = '' self.parse() def __repr__(self) -> str: - return f'Version({self.version}, description="{self.description}")' + return f'Version(v{self.version}, description="{self.description}")' def parse(self) -> None: """ @@ -66,7 +66,7 @@ def embed(self) -> Embed: """ embed = Embed(color=Color.green(), description=self.description) embed.set_author( - name=f'{self.version} - Changelog', + name=f'v{self.version} - Changelog', icon_url=self.bot.user.avatar_url, ) @@ -150,8 +150,3 @@ async def from_url(cls, bot: Bot, url: str = '') -> 'Changelog': url = url or cls.CHANGELOG_URL resp = await bot.session.get(url) return cls(bot, await resp.text()) - - -if __name__ == '__main__': - with open('../CHANGELOG.md') as f: - print(Changelog(..., f.read()).latest_version) diff --git a/core/clients.py b/core/clients.py index 4142df39af..6bee795eee 100644 --- a/core/clients.py +++ b/core/clients.py @@ -251,125 +251,7 @@ async def login(cls, bot: Bot) -> 'GitHub': return self -class ModmailApiClient(UserClient, ApiClient): - BASE = 'https://api.modmail.tk' - METADATA = BASE + '/metadata' - GITHUB = BASE + '/github' - LOGS = BASE + '/logs' - CONFIG = BASE + '/config' - - def __init__(self, bot: Bot): - super().__init__(bot) - if self.token: - self.headers = { - 'Authorization': 'Bearer ' + self.token - } - - @property - def token(self): - return self.bot.config.get('modmail_api_token') - - async def validate_token(self): - resp = await self.request(self.BASE + '/token/verify', - return_response=True) - return resp.status == 200 - - async def post_metadata(self, data): - return await self.request(self.METADATA, method='POST', payload=data) - - async def get_user_info(self): - return await self.request(self.GITHUB + '/userinfo') - - async def update_repository(self): - return await self.request(self.GITHUB + '/update') - - async def get_metadata(self): - return await self.request(self.METADATA) - - async def get_user_logs(self, user_id): - return await self.request(self.LOGS + '/user/' + str(user_id)) - - async def get_log(self, channel_id): - return await self.request(self.LOGS + '/' + str(channel_id)) - - async def get_log_link(self, channel_id): - doc = await self.get_log(channel_id) - return f'https://logs.modmail.tk/{doc["key"]}' - - async def get_config(self): - return await self.request(self.CONFIG) - - async def update_config(self, data): - data = self.filter_valid(data) - return await self.request(self.CONFIG, method='PATCH', payload=data) - - async def create_log_entry(self, recipient, channel, creator): - return await self.request(self.LOGS + '/key', payload={ - 'channel_id': str(channel.id), - 'guild_id': str(self.bot.guild_id), - 'recipient': { - 'id': str(recipient.id), - 'name': recipient.name, - 'discriminator': recipient.discriminator, - 'avatar_url': recipient.avatar_url, - 'mod': False - }, - 'creator': { - 'id': str(creator.id), - 'name': creator.name, - 'discriminator': creator.discriminator, - 'avatar_url': creator.avatar_url, - 'mod': isinstance(creator, Member) - } - }) - - async def edit_message(self, message_id, new_content): - await self.request(self.LOGS + '/edit', method='PATCH', - payload={ - 'message_id': str(message_id), - 'new_content': new_content - }) - - async def append_log(self, message, channel_id='', type_='thread_message'): - channel_id = str(channel_id) or str(message.channel.id) - data = { - 'payload': { - 'timestamp': str(message.created_at), - 'message_id': str(message.id), - # author - 'author': { - 'id': str(message.author.id), - 'name': message.author.name, - 'discriminator': message.author.discriminator, - 'avatar_url': message.author.avatar_url, - 'mod': not isinstance(message.channel, DMChannel), - }, - # message properties - 'content': message.content, - 'type': type_, - 'attachments': [ - { - 'id': a.id, - 'filename': a.filename, - 'is_image': a.width is not None, - 'size': a.size, - 'url': a.url - } for a in message.attachments] - } - } - return await self.request(self.LOGS + f'/{channel_id}', - method='PATCH', - payload=data) - - async def post_log(self, channel_id, data): - return await self.request(self.LOGS + f'/{channel_id}', - method='POST', - payload=data) - - class SelfHostedClient(UserClient, ApiClient): - BASE = 'https://api.modmail.tk' - METADATA = BASE + '/metadata' def __init__(self, bot: Bot): super().__init__(bot) @@ -390,17 +272,6 @@ def db(self): def logs(self): return self.db.logs - async def validate_token(self): - resp = await self.request(self.BASE + '/token/verify', - return_response=True) - return resp.status == 200 - - async def get_metadata(self): - return await self.request(self.METADATA) - - async def post_metadata(self, data): - return await self.request(self.METADATA, method='POST', payload=data) - async def get_user_logs(self, user_id): query = { 'recipient.id': str(user_id), diff --git a/core/decorators.py b/core/decorators.py index 9540597c3d..4d9ef729e9 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -16,19 +16,12 @@ async def wrapper(self, ctx: commands.Context, *args, **kwargs): def github_access_token_required(func): @functools.wraps(func) async def wrapper(self, ctx: commands.Context, *args, **kwargs): - if (self.bot.self_hosted and - self.bot.config.get('github_access_token')) or \ - self.bot.config.get('modmail_api_token'): + if self.bot.config.get('github_access_token'): return await func(self, ctx, *args, **kwargs) - if not self.bot.self_hosted: - desc = ('You can only use this command if you have a ' - 'configured `MODMAIL_API_TOKEN`. Get your ' - 'token from https://dashboard.modmail.tk') - else: - desc = ('You can only use this command if you have a ' - 'configured `GITHUB_ACCESS_TOKEN`. Get a ' - 'personal access token from developer settings.') + desc = ('You can only use this command if you have a ' + 'configured `GITHUB_ACCESS_TOKEN`. Get a ' + 'personal access token from developer settings.') embed = Embed(color=Color.red(), title='Unauthorized', description=desc) diff --git a/core/models.py b/core/models.py index 96043267e4..6af8802200 100644 --- a/core/models.py +++ b/core/models.py @@ -41,11 +41,6 @@ def version(self) -> str: def db(self) -> typing.Optional[AsyncIOMotorClient]: raise NotImplementedError - @property - @abc.abstractmethod - def self_hosted(self) -> bool: - raise NotImplementedError - @property @abc.abstractmethod def config(self) -> 'ConfigManagerABC': @@ -168,10 +163,6 @@ async def update_repository(self) -> dict: async def get_user_logs(self, user_id: typing.Union[str, int]) -> list: raise NotImplementedError - @abc.abstractmethod - async def validate_token(self) -> bool: - raise NotImplementedError - @abc.abstractmethod async def get_log(self, channel_id: typing.Union[str, int]) -> dict: raise NotImplementedError @@ -208,14 +199,6 @@ async def post_log(self, data: dict) -> dict: raise NotImplementedError - @abc.abstractmethod - async def get_metadata(self) -> dict: - raise NotImplementedError - - @abc.abstractmethod - async def post_metadata(self, data: dict) -> dict: - raise NotImplementedError - @abc.abstractmethod async def edit_message(self, message_id: typing.Union[int, str], new_content: str) -> None: diff --git a/core/thread.py b/core/thread.py index 737da82ceb..d8891a05e8 100644 --- a/core/thread.py +++ b/core/thread.py @@ -104,7 +104,7 @@ async def setup(self, *, creator=None, category=None): try: log_url, log_data = await asyncio.gather( self.bot.api.create_log_entry(recipient, channel, - creator or recipient), + creator or recipient), self.bot.api.get_user_logs(recipient.id) ) @@ -221,11 +221,7 @@ async def _close(self, closer, silent=False, delete_channel=True, }) if log_data is not None and isinstance(log_data, dict): - if self.bot.self_hosted: - log_url = f"{self.bot.config.log_url.strip('/')}/" \ - f"logs/{log_data['key']}" - else: - log_url = f"https://logs.modmail.tk/{log_data['key']}" + log_url = f"{self.bot.config.log_url.strip('/')}/logs/{log_data['key']}" if log_data['messages']: content = str(log_data['messages'][0]['content']) From 3b620a49efb92577f38977bf69c8a4352e72913f Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Apr 2019 21:50:08 -0700 Subject: [PATCH 0004/1221] Remove instance from readme, and other references --- CHANGELOG.md | 2 +- README.md | 7 +------ cogs/utility.py | 3 +-- core/config.py | 2 +- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd9bf5300..375431315c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -191,7 +191,7 @@ Huge thanks to Sasiko for reporting these issues. # v2.12.0 -### Important +### Important **In the future, the Modmail API (https://modmail.tk) will be deprecated. This is due to the fact that we are providing a free service without getting anything in return, and thus we do not have the resources to scale to accommodate for more users. We recommend using your own database for logs. In the future you will soon get a `backup` command so you can download all your pre-existing data and migrate to your own database.** diff --git a/README.md b/README.md index f8e6b07e98..d8bcc7273c 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,6 @@ Support - - Bot instances - - Python 3.7 @@ -56,8 +52,7 @@ Have you sent something with the `?reply` command by accident? Don't fret, you c ## Thread Logs -Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://logs.modmail.tk/02032d65a6f3 - +Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://modmail-logs.herokuapp.com/logs/02032d65a6f3. # Contributing diff --git a/cogs/utility.py b/cogs/utility.py index 0ab77a34d1..cd10310845 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -208,8 +208,7 @@ async def about(self, ctx): embed.add_field(name='Latency', value=f'{self.bot.latency * 1000:.2f} ms') embed.add_field(name='Version', - value=f'[`{self.bot.version}`]' - '(https://modmail.tk/changelog)') + value=f'`{self.bot.version}`') embed.add_field(name='Author', value='[`kyb3r`](https://github.com/kyb3r)') diff --git a/core/config.py b/core/config.py index 548795de03..0abb9dcd80 100644 --- a/core/config.py +++ b/core/config.py @@ -53,7 +53,7 @@ class ConfigManager(ConfigManagerABC): protected_keys = { # Modmail - 'modmail_api_token', 'modmail_guild_id', 'guild_id', 'owners', + 'modmail_guild_id', 'guild_id', 'owners', 'log_url', 'mongo_uri', # bot From b73d3cea3976fe344a5e0c777f4d91adeeb44f4d Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Apr 2019 22:00:45 -0700 Subject: [PATCH 0005/1221] Bump version 2.16.0 --- CHANGELOG.md | 15 +++++++++++++++ bot.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 375431315c..bc88cdcaee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.16.0 + +### What's changed? + +All support for Modmail API (api.modmail.tk) has terminated. +If you're still using api.modmail.tk, you will need to migrate to the self-hosted database +option ASAP. Your bot will not work unless you switch to the self-hosted option. Refer to the +installation tutorial for information regarding self-hosted Modmail. + +# v2.15.1 + +### Fixed + +Emergency patch of a SyntaxError. + # v2.15.0 ### What's new? diff --git a/bot.py b/bot.py index 84d95e3f40..a5d1793049 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.15.1' +__version__ = '2.16.0' import asyncio import logging From 7e6b973be0776bd401d6ee29cd9127d901ca367a Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Apr 2019 21:37:49 -0700 Subject: [PATCH 0006/1221] v2.18.0 - new permission system --- CHANGELOG.md | 33 +++++ bot.py | 30 +++-- cogs/modmail.py | 55 +++++---- cogs/plugins.py | 16 ++- cogs/utility.py | 317 +++++++++++++++++++++++++++++++++++++++++------- core/checks.py | 78 +++++++----- core/config.py | 6 +- core/models.py | 17 ++- 8 files changed, 439 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c80b399f99..c37318275d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.18.0 + +### What's new? + +- A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desire permission level specific to a command or a group of commands for a role or user. +- There are five permission groups/levels: + - Owner + - Administrator + - Moderator + - Supporter + - Regular + +You may add a role or user to a permission group through any of the following methods: +- `?permissions add level owner @role` +- `?permissions add level supporter member-name` +- `?permissions add level moderator everyone` +- `?permissions add level moderator @member#1234` +- `?permissions add level administrator 78912384930291853` + +The same applies for individual commands permissions: +- `?permissions add command-name @member#1234` +- ... and the other methods listed above. + +To revoke a permission, use `remove` instead of `add`. + +To view all roles and users with permission for a permission group or command do: +- `?permissions get command command-name` +- `?permissions get level owner` + +### Note + +When updating to this version, all prior permission settings with guild-based permissions will be invalidated. You will need to convert to the above system. + # v2.17.1 ### Changed diff --git a/bot.py b/bot.py index 3dc4b00a63..08b6363af1 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.17.1' +__version__ = '2.18.0' import asyncio import logging @@ -48,7 +48,7 @@ from core.clients import ModmailApiClient, SelfHostedClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import info, error -from core.models import Bot +from core.models import Bot, PermissionLevel from core.thread import ThreadManager from core.time import human_timedelta @@ -195,11 +195,6 @@ def _load_extensions(self): except Exception: logger.exception(error(f'Failed to load {cog}')) - async def is_owner(self, user): - allowed = {int(x) for x in - str(self.config.get('owners', '0')).split(',')} - return user.id in allowed - def run(self, *args, **kwargs): try: self.loop.run_until_complete(self.start(self.token)) @@ -599,6 +594,25 @@ async def get_context(self, message, *, cls=commands.Context): return ctx + async def update_perms(self, name, value, add=True): + if isinstance(name, PermissionLevel): + permissions = self.config.level_permissions + name = name.name + else: + permissions = self.config.command_permissions + if name not in permissions: + if add: + permissions[name] = [value] + else: + if add: + if value not in permissions[name]: + permissions[name].append(value) + else: + if value in permissions[name]: + permissions[name].remove(value) + logger.info(info(f'Updating permissions for {name}, {value} (add={add}).')) + await self.config.update() + async def on_message(self, message): if message.type == discord.MessageType.pins_add and \ message.author == self.user: @@ -799,7 +813,7 @@ def overwrites(ctx): } for role in ctx.guild.roles: - if role.permissions.manage_guild: + if role.permissions.administrator: overwrites[role] = discord.PermissionOverwrite( read_messages=True ) diff --git a/cogs/modmail.py b/cogs/modmail.py index b676e51708..4f04f8f494 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -12,7 +12,7 @@ from core import checks from core.decorators import trigger_typing -from core.models import Bot +from core.models import Bot, PermissionLevel from core.paginator import PaginatorSession from core.time import UserFriendlyTime, human_timedelta from core.utils import format_preview, User @@ -26,7 +26,7 @@ def __init__(self, bot: Bot): @commands.command() @trigger_typing - @checks.has_permissions(administrator=True) + @checks.has_permissions(PermissionLevel.OWNER) async def setup(self, ctx): """Sets up a server for Modmail""" if self.bot.main_category: @@ -62,15 +62,18 @@ async def setup(self, ctx): self.bot.config['log_channel_id'] = log_channel.id await self.bot.config.update() - await ctx.send('Successfully set up server.') + await ctx.send('Successfully set up server.\n' + 'Consider setting permission groups to give access ' + 'to roles or users the ability to use Modmail.\n' + f'Type `{self.bot.prefix}permissions` for more info.') + if not self.bot.config.permissions: + await self.bot.update_perms(PermissionLevel.REGULAR, -1) + await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id) - @commands.group() - @checks.has_permissions(manage_messages=True) + @commands.group(invoke_without_command=True) + @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippets(self, ctx): """Returns a list of snippets that are currently set.""" - if ctx.invoked_subcommand is not None: - return - embeds = [] if self.bot.snippets: @@ -101,7 +104,7 @@ async def snippets(self, ctx): await session.run() @snippets.command(name='add') - @checks.has_permissions(manage_messages=True) + @checks.has_permissions(PermissionLevel.SUPPORTER) async def add_(self, ctx, name: str.lower, *, value): """Add a snippet to the bot config.""" if 'snippets' not in self.bot.config.cache: @@ -118,9 +121,9 @@ async def add_(self, ctx, name: str.lower, *, value): await ctx.send(embed=embed) - @snippets.command(name='del') - @checks.has_permissions(manage_messages=True) - async def del_(self, ctx, *, name: str.lower): + @snippets.command(name='remove', aliases=['del', 'delete', 'rm']) + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def remove_(self, ctx, *, name: str.lower): """Removes a snippet from bot config.""" if self.bot.config.snippets.get(name): @@ -142,7 +145,7 @@ async def del_(self, ctx, *, name: str.lower): await ctx.send(embed=embed) @commands.command() - @checks.has_permissions(manage_messages=True) + @checks.has_permissions(PermissionLevel.MODERATOR) async def move(self, ctx, *, category: discord.CategoryChannel): """Moves a thread to a specified category.""" thread = ctx.thread @@ -179,6 +182,7 @@ async def send_scheduled_close_message(ctx, after, silent=False): await ctx.send(embed=embed) @commands.command(usage='[after] [close message]') + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def close(self, ctx, *, after: UserFriendlyTime = None): """ @@ -236,6 +240,7 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): ) @commands.command(aliases=['alert']) + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def notify(self, ctx, *, role=None): """ @@ -272,6 +277,7 @@ async def notify(self, ctx, *, role=None): return await ctx.send(embed=embed) @commands.command(aliases=['sub']) + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def subscribe(self, ctx, *, role=None): """ @@ -311,6 +317,7 @@ async def subscribe(self, ctx, *, role=None): return await ctx.send(embed=embed) @commands.command(aliases=['unsub']) + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def unsubscribe(self, ctx, *, role=None): """Unsubscribe yourself or a given role from a thread.""" @@ -343,6 +350,7 @@ async def unsubscribe(self, ctx, *, role=None): return await ctx.send(embed=embed) @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def nsfw(self, ctx): """Flags a Modmail thread as nsfw.""" @@ -350,6 +358,7 @@ async def nsfw(self, ctx): await ctx.message.add_reaction('✅') @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def loglink(self, ctx): """Return the link to the current thread's logs.""" @@ -407,7 +416,7 @@ def format_log_embeds(self, logs, avatar_url): return embeds @commands.group(invoke_without_command=True) - @checks.has_permissions(manage_messages=True) + @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs(self, ctx, *, member: User = None): """Shows a list of previous Modmail thread logs of a member.""" @@ -440,7 +449,7 @@ async def logs(self, ctx, *, member: User = None): await session.run() @logs.command(name='closed-by') - @checks.has_permissions(manage_messages=True) + @checks.has_permissions(PermissionLevel.SUPPORTER) async def closed_by(self, ctx, *, user: User = None): """Returns all logs closed by a user.""" if not self.bot.self_hosted: @@ -478,8 +487,8 @@ async def closed_by(self, ctx, *, user: User = None): session = PaginatorSession(ctx, *embeds) await session.run() - @logs.command(name='search') - @checks.has_permissions(manage_messages=True) + @logs.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) async def search(self, ctx, limit: Optional[int] = None, *, query): """Searches all logs for a message that contains your query.""" @@ -521,6 +530,7 @@ async def search(self, ctx, limit: Optional[int] = None, *, query): await session.run() @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def reply(self, ctx, *, msg=''): """Reply to users using this command. @@ -533,6 +543,7 @@ async def reply(self, ctx, *, msg=''): await ctx.thread.reply(ctx.message) @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def anonreply(self, ctx, *, msg=''): """Reply to a thread anonymously. @@ -548,6 +559,7 @@ async def anonreply(self, ctx, *, msg=''): await ctx.thread.reply(ctx.message, anonymous=True) @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def note(self, ctx, *, msg=''): """Take a note about the current thread, useful for noting context.""" @@ -556,6 +568,7 @@ async def note(self, ctx, *, msg=''): await ctx.thread.note(ctx.message) @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def edit(self, ctx, message_id: Optional[int] = None, *, new_message): @@ -599,8 +612,8 @@ async def edit(self, ctx, message_id: Optional[int] = None, await ctx.message.add_reaction('✅') @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) @trigger_typing - @checks.has_permissions(manage_messages=True) async def contact(self, ctx, category: Optional[discord.CategoryChannel] = None, *, user: Union[discord.Member, discord.User]): @@ -639,8 +652,8 @@ async def contact(self, ctx, await ctx.send(embed=embed) @commands.command() + @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing - @checks.has_permissions(manage_channels=True) async def blocked(self, ctx): """Returns a list of blocked users""" embed = discord.Embed(title='Blocked Users', @@ -672,8 +685,8 @@ async def blocked(self, ctx): await ctx.send(embed=embed) @commands.command() + @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing - @checks.has_permissions(manage_channels=True) async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTime = None): """ @@ -738,8 +751,8 @@ async def block(self, ctx, user: Optional[User] = None, *, return await ctx.send(embed=embed) @commands.command() + @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing - @checks.has_permissions(manage_channels=True) async def unblock(self, ctx, *, user: User = None): """ Unblocks a user from using Modmail. diff --git a/cogs/plugins.py b/cogs/plugins.py index 54ed742f53..45ba1a9b23 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -9,7 +9,8 @@ from discord.ext import commands -from core.models import Bot +from core import checks +from core.models import Bot, PermissionLevel from core.utils import info, error logger = logging.getLogger('Modmail') @@ -113,15 +114,15 @@ async def load_plugin(self, username, repo, plugin_name): msg = f'Loaded plugins.{username}-{repo}.{plugin_name}' logger.info(info(msg)) - @commands.group(aliases=['plugins']) - @commands.is_owner() + @commands.group(aliases=['plugins'], invoke_without_command=True) + @checks.has_permissions(PermissionLevel.OWNER) async def plugin(self, ctx): """Plugin handler. Controls the plugins in the bot.""" - if ctx.invoked_subcommand is None: - cmd = self.bot.get_command('help') - await ctx.invoke(cmd, command='plugin') + cmd = self.bot.get_command('help') + await ctx.invoke(cmd, command='plugin') @plugin.command() + @checks.has_permissions(PermissionLevel.OWNER) async def add(self, ctx, *, plugin_name): """Adds a plugin""" if plugin_name in self.bot.config.plugins: @@ -161,6 +162,7 @@ async def add(self, ctx, *, plugin_name): 'Use username/repo/plugin.') @plugin.command() + @checks.has_permissions(PermissionLevel.OWNER) async def remove(self, ctx, *, plugin_name): """Removes a certain plugin""" if plugin_name in self.bot.config.plugins: @@ -195,6 +197,7 @@ def onerror(func, path, exc_info): await ctx.send('Plugin not installed.') @plugin.command() + @checks.has_permissions(PermissionLevel.OWNER) async def update(self, ctx, *, plugin_name): """Updates a certain plugin""" if plugin_name not in self.bot.config.plugins: @@ -227,6 +230,7 @@ async def update(self, ctx, *, plugin_name): await ctx.send(f'Unable to start plugin: `{exc}`') @plugin.command(name='list') + @checks.has_permissions(PermissionLevel.OWNER) async def list_(self, ctx): """Shows a list of currently enabled plugins""" if self.bot.config.plugins: diff --git a/cogs/utility.py b/cogs/utility.py index bc24a8cbf7..3f164bbffe 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -6,6 +6,7 @@ from datetime import datetime from difflib import get_close_matches from io import StringIO +from typing import Union from json import JSONDecodeError from textwrap import indent @@ -19,9 +20,9 @@ from core import checks from core.changelog import Changelog from core.decorators import github_access_token_required, trigger_typing -from core.models import Bot, InvalidConfigError +from core.models import Bot, InvalidConfigError, PermissionLevel from core.paginator import PaginatorSession, MessagePaginatorSession -from core.utils import cleanup_code, info, error +from core.utils import cleanup_code, info, error, User logger = logging.getLogger('Modmail') @@ -37,9 +38,6 @@ async def verify_checks(ctx, cmd): predicates = cmd.checks if not predicates: return True - - if await ctx.bot.is_owner(ctx.author): - return True try: return await discord.utils.async_all(predicate(ctx) @@ -100,14 +98,14 @@ async def format_command_help(self, ctx, cmd): return embed fmt = '' - length = len(cmd.all_commands) - for i, (name, c) in enumerate(sorted(cmd.all_commands.items(), - key=lambda c: c[0])): + length = len(cmd.commands) + for i, c in enumerate(sorted(cmd.commands, key=lambda c: c.name)): + # Bug: fmt may run over the embed limit if length == i + 1: # last branch = '└─' else: branch = '├─' - fmt += f"`{branch} {name}` - {c.short_doc}\n" + fmt += f'`{branch} {c.name}` - {c.short_doc}\n' embed.add_field(name='Sub Commands', value=fmt) embed.set_footer( @@ -148,6 +146,7 @@ async def format_not_found(self, ctx, command): return embed @commands.command(name='help') + @checks.has_permissions(PermissionLevel.REGULAR) @trigger_typing async def help_(self, ctx, *, command: str = None): """Shows the help message.""" @@ -181,6 +180,7 @@ async def help_(self, ctx, *, command: str = None): return await p_session.run() @commands.command() + @checks.has_permissions(PermissionLevel.REGULAR) @trigger_typing async def changelog(self, ctx): """Show a paginated changelog of the bot.""" @@ -189,6 +189,7 @@ async def changelog(self, ctx): await paginator.run() @commands.command(aliases=['bot', 'info']) + @checks.has_permissions(PermissionLevel.REGULAR) @trigger_typing async def about(self, ctx): """Shows information about the bot.""" @@ -239,15 +240,12 @@ async def about(self, ctx): embed.set_footer(text=footer) await ctx.send(embed=embed) - @commands.group() - @commands.is_owner() + @commands.group(invoke_without_command=True) + @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing async def debug(self, ctx): """Shows the recent logs of the bot.""" - if ctx.invoked_subcommand is not None: - return - with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../temp/logs.log'), 'r+') as f: logs = f.read().strip() @@ -292,7 +290,7 @@ async def debug(self, ctx): return await session.run() @debug.command() - @commands.is_owner() + @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing async def hastebin(self, ctx): """Upload logs to hastebin.""" @@ -323,7 +321,7 @@ async def hastebin(self, ctx): await ctx.send(embed=embed) @debug.command() - @commands.is_owner() + @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing async def clear(self, ctx): """Clears the locally cached logs.""" @@ -336,14 +334,11 @@ async def clear(self, ctx): )) @commands.command() - @commands.is_owner() + @checks.has_permissions(PermissionLevel.OWNER) @github_access_token_required @trigger_typing async def github(self, ctx): """Shows the GitHub user your access token is linked to.""" - if ctx.invoked_subcommand: - return - data = await self.bot.api.get_user_info() embed = Embed( @@ -359,7 +354,7 @@ async def github(self, ctx): await ctx.send(embed=embed) @commands.command() - @commands.is_owner() + @checks.has_permissions(PermissionLevel.OWNER) @github_access_token_required @trigger_typing async def update(self, ctx): @@ -415,7 +410,7 @@ async def update(self, ctx): return await ctx.send(embed=embed) @commands.command(aliases=['presence']) - @checks.has_permissions(administrator=True) + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def activity(self, ctx, activity_type: str.lower, *, message: str = ''): """ Set a custom activity for the bot. @@ -466,7 +461,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ''): return await ctx.send(embed=embed) @commands.command() - @checks.has_permissions(administrator=True) + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def status(self, ctx, *, status_type: str.lower): """ Set a custom status for the bot. @@ -594,8 +589,8 @@ async def on_ready(self): logger.info(info(presence['status'][1])) @commands.command() + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) @trigger_typing - @checks.has_permissions(administrator=True) async def ping(self, ctx): """Pong! Returns your websocket latency.""" embed = Embed( @@ -606,7 +601,7 @@ async def ping(self, ctx): return await ctx.send(embed=embed) @commands.command() - @checks.has_permissions(administrator=True) + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def mention(self, ctx, *, mention=None): """Changes what the bot mentions at the start of each thread.""" current = self.bot.config.get('mention', '@here') @@ -630,7 +625,7 @@ async def mention(self, ctx, *, mention=None): return await ctx.send(embed=embed) @commands.command() - @checks.has_permissions(administrator=True) + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def prefix(self, ctx, *, prefix=None): """Changes the prefix for the bot.""" @@ -650,17 +645,15 @@ async def prefix(self, ctx, *, prefix=None): await self.bot.config.update() await ctx.send(embed=embed) - @commands.group() - @commands.is_owner() + @commands.group(invoke_without_command=True) + @checks.has_permissions(PermissionLevel.OWNER) async def config(self, ctx): """Change config vars for the bot.""" - - if ctx.invoked_subcommand is None: - cmd = self.bot.get_command('help') - await ctx.invoke(cmd, command='config') + cmd = self.bot.get_command('help') + await ctx.invoke(cmd, command='config') @config.command() - @commands.is_owner() + @checks.has_permissions(PermissionLevel.OWNER) async def options(self, ctx): """Return a list of valid config keys you can change.""" allowed = self.bot.config.allowed_to_change_in_command @@ -671,7 +664,7 @@ async def options(self, ctx): return await ctx.send(embed=embed) @config.command() - @commands.is_owner() + @checks.has_permissions(PermissionLevel.OWNER) async def set(self, ctx, key: str.lower, *, value): """ Sets a configuration variable and its value @@ -702,9 +695,9 @@ async def set(self, ctx, key: str.lower, *, value): return await ctx.send(embed=embed) - @config.command(name='del') - @commands.is_owner() - async def del_config(self, ctx, key: str.lower): + @config.command(name='remove', aliases=['del', 'delete', 'rm']) + @checks.has_permissions(PermissionLevel.OWNER) + async def remove_config(self, ctx, key: str.lower): """Deletes a key from the config.""" keys = self.bot.config.allowed_to_change_in_command if key in keys: @@ -731,7 +724,7 @@ async def del_config(self, ctx, key: str.lower): return await ctx.send(embed=embed) @config.command() - @commands.is_owner() + @checks.has_permissions(PermissionLevel.OWNER) async def get(self, ctx, key=None): """Shows the config variables that are currently set.""" keys = self.bot.config.allowed_to_change_in_command @@ -774,12 +767,10 @@ async def get(self, ctx, key=None): return await ctx.send(embed=embed) - @commands.group(aliases=['aliases']) - @checks.has_permissions(manage_messages=True) + @commands.group(aliases=['aliases'], invoke_without_command=True) + @checks.has_permissions(PermissionLevel.MODERATOR) async def alias(self, ctx): """Returns a list of aliases that are currently set.""" - if ctx.invoked_subcommand is not None: - return embeds = [] desc = 'Here is a list of aliases that are currently configured.' @@ -814,7 +805,7 @@ async def alias(self, ctx): return await session.run() @alias.command(name='add') - @checks.has_permissions(manage_messages=True) + @checks.has_permissions(PermissionLevel.MODERATOR) async def add_(self, ctx, name: str.lower, *, value): """Add an alias to the bot config.""" if 'aliases' not in self.bot.config.cache: @@ -849,9 +840,9 @@ async def add_(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) - @alias.command(name='del') - @checks.has_permissions(manage_messages=True) - async def del_alias(self, ctx, *, name: str.lower): + @alias.command(name='remove', aliases=['del', 'delete', 'rm']) + @checks.has_permissions(PermissionLevel.MODERATOR) + async def remove_alias(self, ctx, *, name: str.lower): """Removes a alias from bot config.""" if 'aliases' not in self.bot.config.cache: @@ -876,8 +867,240 @@ async def del_alias(self, ctx, *, name: str.lower): return await ctx.send(embed=embed) + @commands.group(aliases=['perms'], invoke_without_command=True) + @checks.has_permissions(PermissionLevel.OWNER) + async def permissions(self, ctx): + """Sets the permissions for Modmail commands. + + You may set permissions based on individual command names, or permission + levels. + + Acceptable permission levels are: + - **Owner** (absolute control over the bot) + - **Administrator** (administrative powers such as setting activities) + - **Moderator** (ability to block) + - **Supporter** (access to core Modmail supporting functions) + - **Regular** (most basic interactions such as help and about) + + By default, owner is set to the bot owner and regular is @everyone. + + Note: You will still have to manually give/take permission to the Modmail + category to users/roles. + """ + cmd = self.bot.get_command('help') + await ctx.invoke(cmd, command='perms') + + @permissions.group(name='add', invoke_without_command=True) + @checks.has_permissions(PermissionLevel.OWNER) + async def add_perms(self, ctx): + """Add a permission to a command or a permission level.""" + cmd = self.bot.get_command('help') + await ctx.invoke(cmd, command='perms add') + + @add_perms.command(name='command') + @checks.has_permissions(PermissionLevel.OWNER) + async def add_perms_command(self, ctx, command: str, user_or_role: Union[User, str]): + """Add a user, role, or everyone permission to use a command.""" + if command not in self.bot.all_commands: + embed = Embed( + title='Error', + color=Color.red(), + description='The command you are attempting to point ' + f'to does not exist: `{command}`.' + ) + return await ctx.send(embed=embed) + + if hasattr(user_or_role, 'id'): + value = user_or_role.id + elif user_or_role in {'everyone', 'all'}: + value = -1 + else: + converter = commands.RoleConverter() + role = await converter.convert(ctx, user_or_role) + value = role.id + await self.bot.update_perms(self.bot.all_commands[command].name, value) + embed = Embed( + title='Success', + color=self.bot.main_color, + description=f'Permission for {command} was successfully updated.' + ) + return await ctx.send(embed=embed) + + @add_perms.command(name='level', aliases=['group']) + @checks.has_permissions(PermissionLevel.OWNER) + async def add_perms_level(self, ctx, level: str, user_or_role: Union[User, str]): + """Add a user, role, or everyone permission to use commands of a permission level.""" + if level.upper() not in PermissionLevel._member_names_: + embed = Embed( + title='Error', + color=Color.red(), + description='The permission level you are attempting to point ' + f'to does not exist: `{level}`.' + ) + return await ctx.send(embed=embed) + + if hasattr(user_or_role, 'id'): + value = user_or_role.id + elif user_or_role in {'everyone', 'all'}: + value = -1 + else: + converter = commands.RoleConverter() + role = await converter.convert(ctx, user_or_role) + value = role.id + await self.bot.update_perms(PermissionLevel[level.upper()], value) + embed = Embed( + title='Success', + color=self.bot.main_color, + description=f'Permission for {level} was successfully updated.' + ) + return await ctx.send(embed=embed) + + @permissions.group(name='remove', aliases=['del', 'delete', 'rm', 'revoke']) + @checks.has_permissions(PermissionLevel.OWNER) + async def remove_perms(self, ctx): + """Remove a permission to use a command or permission level.""" + cmd = self.bot.get_command('help') + await ctx.invoke(cmd, command='perms remove') + + @remove_perms.command(name='command') + @checks.has_permissions(PermissionLevel.OWNER) + async def remove_perms_command(self, ctx, command: str, user_or_role: Union[User, str]): + """Remove a user, role, or everyone permission to use a command.""" + if command not in self.bot.all_commands: + embed = Embed( + title='Error', + color=Color.red(), + description='The command you are attempting to point ' + f'to does not exist: `{command}`.' + ) + return await ctx.send(embed=embed) + + if hasattr(user_or_role, 'id'): + value = user_or_role.id + elif user_or_role in {'everyone', 'all'}: + value = -1 + else: + converter = commands.RoleConverter() + role = await converter.convert(ctx, user_or_role) + value = role.id + await self.bot.update_perms(self.bot.all_commands[command].name, value, add=False) + embed = Embed( + title='Success', + color=self.bot.main_color, + description=f'Permission for {command} was successfully updated.' + ) + return await ctx.send(embed=embed) + + @remove_perms.command(name='level', aliases=['group']) + @checks.has_permissions(PermissionLevel.OWNER) + async def remove_perms_level(self, ctx, level: str, user_or_role: Union[User, str]): + """Remove a user, role, or everyone permission to use commands of a permission level.""" + if level.upper() not in PermissionLevel._member_names_: + embed = Embed( + title='Error', + color=Color.red(), + description='The permission level you are attempting to point ' + f'to does not exist: `{level}`.' + ) + return await ctx.send(embed=embed) + + if hasattr(user_or_role, 'id'): + value = user_or_role.id + elif user_or_role in {'everyone', 'all'}: + value = -1 + else: + converter = commands.RoleConverter() + role = await converter.convert(ctx, user_or_role) + value = role.id + await self.bot.update_perms(PermissionLevel[level.upper()], value, add=False) + embed = Embed( + title='Success', + color=self.bot.main_color, + description=f'Permission for {level} was successfully updated.' + ) + return await ctx.send(embed=embed) + + @permissions.group(name='get', invoke_without_command=True) + @checks.has_permissions(PermissionLevel.OWNER) + async def get_perms(self, ctx): + """View the currently-set permissions.""" + cmd = self.bot.get_command('help') + await ctx.invoke(cmd, command='perms get') + + @get_perms.command(name='command') + @checks.has_permissions(PermissionLevel.OWNER) + async def get_perms_command(self, ctx, command: str): + """View the currently-set permissions for a command.""" + if command not in self.bot.all_commands: + embed = Embed( + title='Error', + color=Color.red(), + description='The command you are attempting to point ' + f'to does not exist: `{command}`.' + ) + return await ctx.send(embed=embed) + permissions = self.bot.config.command_permissions.get(self.bot.all_commands[command].name) + if permissions is None or not permissions: + embed = Embed( + title=f'Permission entries for command `{command}`:', + description='No permission entries found.' + ) + else: + values = [] + for perm in permissions: + if perm == -1: + values.insert(0, '**everyone**') + else: + user = self.bot.get_user(perm) + if user is not None: + values.append(user.name) + else: + values.append(str(perm)) + + embed = Embed( + title=f'Permission entries for command `{command}`:', + description=', '.join(values) + ) + return await ctx.send(embed=embed) + + @get_perms.command(name='level', aliases=['group']) + @checks.has_permissions(PermissionLevel.OWNER) + async def get_perms_level(self, ctx, level: str): + """View the currently-set permissions for commands of a permission level.""" + if level.upper() not in PermissionLevel._member_names_: + embed = Embed( + title='Error', + color=Color.red(), + description='The permission level you are attempting to point ' + f'to does not exist: `{level}`.' + ) + return await ctx.send(embed=embed) + permissions = self.bot.config.command_permissions.get(PermissionLevel[level.upper()].name) + if permissions is None or not permissions: + embed = Embed( + title=f'Permission entries for permission level `{level}`:', + description='No permission entries found.' + ) + else: + values = [] + for perm in permissions: + if perm == -1: + values.insert(0, '**everyone**') + else: + user = self.bot.get_user(perm) + if user is not None: + values.append(user.name) + else: + values.append(str(perm)) + + embed = Embed( + title=f'Permission entries for permission level `{level}`:', + description=', '.join(values) + ) + return await ctx.send(embed=embed) + @commands.command(hidden=True, name='eval') - @commands.is_owner() + @checks.has_permissions(PermissionLevel.OWNER) async def eval_(self, ctx, *, body): """Evaluates Python code""" diff --git a/core/checks.py b/core/checks.py index dadc7e7608..d8b611a271 100644 --- a/core/checks.py +++ b/core/checks.py @@ -1,54 +1,76 @@ import logging + from discord.ext import commands + +from core.models import PermissionLevel from core.utils import error logger = logging.getLogger('Modmail') -def has_permissions(**perms): +def has_permissions(permission_level: PermissionLevel = PermissionLevel.REGULAR): """ A decorator that checks if the author has the required permissions. + Parameters + ---------- + + permission_level : PermissionLevel + The lowest level of permission needed to use this command. + Defaults to REGULAR. + Examples -------- :: - @has_permissions(administrator=True) + @has_permissions(PermissionLevel.OWNER) async def setup(ctx): print("Success") """ async def predicate(ctx): - """ - Parameters - ---------- - ctx : Context - The current discord.py `Context`. + has_perm = await check_permissions(ctx, ctx.command.qualified_name, permission_level) - Returns - ------- - bool - `True` if the author is a bot owner, or - has the ``administrator`` permission. - Or if the author has all of the required permissions. - Otherwise, `False`. - """ - if await ctx.bot.is_owner(ctx.author): + if not has_perm and ctx.command.qualified_name != 'help': + logger.error(error(f'You does not have permission to use this command: ' + f'`{ctx.command.qualified_name}` ({permission_level.name}).')) + return has_perm + return commands.check(predicate) + + +async def check_permissions(ctx: commands.Context, + command_name: str, + permission_level: PermissionLevel) -> bool: + if await ctx.bot.is_owner(ctx.author): + # Direct bot owner (creator) has absolute power over the bot + return True + + if permission_level != PermissionLevel.OWNER and ctx.channel.permissions_for(ctx.author).administrator: + # Administrators have permission to all non-owner commands + return True + + command_permissions = ctx.bot.config.command_permissions + author_roles = ctx.author.roles + + if command_name in command_permissions: + # -1 is for @everyone + if -1 in command_permissions[command_name]: return True + has_perm_role = any(role.id in command_permissions[command_name] for role in author_roles) + has_perm_id = ctx.author.id in command_permissions[command_name] + return has_perm_role or has_perm_id - resolved = ctx.channel.permissions_for(ctx.author) + level_permissions = ctx.bot.config.level_permissions - has_perm = resolved.administrator or all( - getattr(resolved, name, None) == value - for name, value in perms.items() - ) + for level in PermissionLevel: + if level >= permission_level and level.name in level_permissions: + if -1 in level_permissions[level.name]: + return True + has_perm_role = any(role.id in level_permissions[level.name] for role in author_roles) + has_perm_id = ctx.author.id in level_permissions[level.name] + if has_perm_role or has_perm_id: + return True - if not has_perm: - restrictions = ', '.join(f'{name.replace("_", " ").title()} ({"yes" if value else "no"})' - for name, value in perms.items()) - logger.error(error(f'Command `{ctx.command.qualified_name}` processes ' - f'the following restrictions(s): {restrictions}.')) - return has_perm - return commands.check(predicate) + return False def thread_only(): diff --git a/core/config.py b/core/config.py index 3474fbb5d4..88d30b0392 100644 --- a/core/config.py +++ b/core/config.py @@ -42,7 +42,7 @@ class ConfigManager(ConfigManagerABC): 'activity_message', 'activity_type', 'status', # moderation - 'blocked', + 'blocked', 'command_permissions', 'level_permissions', # threads 'snippets', 'notification_squad', 'subscriptions', 'closures', @@ -53,7 +53,7 @@ class ConfigManager(ConfigManagerABC): protected_keys = { # Modmail - 'modmail_api_token', 'modmail_guild_id', 'guild_id', 'owners', + 'modmail_api_token', 'modmail_guild_id', 'guild_id', 'log_url', 'mongo_uri', # bot @@ -107,6 +107,8 @@ def populate_cache(self): 'plugins': [], 'aliases': {}, 'blocked': {}, + 'command_permissions': {}, + 'level_permissions': {}, 'notification_squad': {}, 'subscriptions': {}, 'closures': {}, diff --git a/core/models.py b/core/models.py index 96043267e4..528c40beca 100644 --- a/core/models.py +++ b/core/models.py @@ -2,6 +2,7 @@ import asyncio import typing from datetime import datetime +from enum import IntEnum from discord import Color, Member, User, CategoryChannel, DMChannel, Embed from discord import Message, TextChannel, Guild @@ -11,6 +12,14 @@ from motor.motor_asyncio import AsyncIOMotorClient +class PermissionLevel(IntEnum): + OWNER = 5 + ADMINISTRATOR = 4 + MODERATOR = 3 + SUPPORTER = 2 + REGULAR = 1 + + class Bot(abc.ABC, commands.Bot): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -144,6 +153,11 @@ async def process_modmail(self, message: Message) -> None: async def convert_emoji(self, name: str) -> str: raise NotImplementedError + @abc.abstractmethod + async def update_perms(self, name: typing.Union[PermissionLevel, str], + value: int, add: bool = True) -> None: + raise NotImplementedError + @staticmethod @abc.abstractmethod def overwrites(ctx: commands.Context) -> dict: @@ -380,7 +394,8 @@ def __getitem__(self, item: str) -> 'ThreadABC': @abc.abstractmethod async def find(self, *, recipient: typing.Union[Member, User] = None, - channel: TextChannel = None) -> \ + channel: TextChannel = None, + recipient_id: int = None) -> \ typing.Optional['ThreadABC']: raise NotImplementedError From d19a51056eb6ab6ca08d760b40d853bd85f6127e Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Apr 2019 21:46:44 -0700 Subject: [PATCH 0007/1221] Spacing --- bot.py | 31 ++++++++++++++++++------------- cogs/utility.py | 10 +++++----- core/checks.py | 2 +- core/thread.py | 42 ++++++++++++++++++++++-------------------- 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/bot.py b/bot.py index 08b6363af1..7133e9e406 100644 --- a/bot.py +++ b/bot.py @@ -52,7 +52,6 @@ from core.thread import ThreadManager from core.time import human_timedelta - init() logger = logging.getLogger('Modmail') @@ -486,7 +485,8 @@ async def process_modmail(self, message): except isodate.ISO8601Error: logger.warning('The account age limit needs to be a ' 'ISO-8601 duration formatted duration string ' - f'greater than 0 days, not "%s".', str(account_age)) + f'greater than 0 days, not "%s".', + str(account_age)) del self.config.cache['account_age'] await self.config.update() account_age = isodate.duration.Duration() @@ -541,7 +541,7 @@ async def process_modmail(self, message): await self.config.update() else: reaction = sent_emoji - + if reaction != 'disable': try: await message.add_reaction(reaction) @@ -610,7 +610,9 @@ async def update_perms(self, name, value, add=True): else: if value in permissions[name]: permissions[name].remove(value) - logger.info(info(f'Updating permissions for {name}, {value} (add={add}).')) + logger.info( + info(f'Updating permissions for {name}, {value} (add={add}).') + ) await self.config.update() async def on_message(self, message): @@ -649,7 +651,7 @@ async def on_message(self, message): async def on_typing(self, channel, user, _): if user.bot: - return + return if isinstance(channel, discord.DMChannel): if not self.config.get('user_typing'): return @@ -662,7 +664,7 @@ async def on_typing(self, channel, user, _): thread = await self.threads.find(channel=channel) if thread and thread.recipient: await thread.recipient.trigger_typing() - + async def on_raw_reaction_add(self, payload): user = self.get_user(payload.user_id) @@ -682,12 +684,14 @@ async def on_raw_reaction_add(self, payload): message = await channel.get_message(payload.message_id) reaction = payload.emoji - close_emoji = await self.convert_emoji(self.config.get('close_emoji', '🔒')) + close_emoji = await self.convert_emoji( + self.config.get('close_emoji', '🔒') + ) if isinstance(channel, discord.DMChannel) and str(reaction) == str(close_emoji): # closing thread thread = await self.threads.find(recipient=user) ts = message.embeds[0].timestamp if message.embeds else None - if thread and ts == thread.channel.created_at: + if thread and ts == thread.channel.created_at: # the reacted message is the corresponding thread creation embed if not self.config.get('disable_recipient_thread_close'): await thread.close(closer=user) @@ -733,23 +737,23 @@ async def on_guild_channel_delete(self, channel): return await thread.close(closer=mod, silent=True, delete_channel=False) - + async def on_member_remove(self, member): thread = await self.threads.find(recipient=member) if thread: em = discord.Embed( description='The recipient has left the server.', color=discord.Color.red() - ) + ) await thread.channel.send(embed=em) - + async def on_member_join(self, member): thread = await self.threads.find(recipient=member) if thread: em = discord.Embed( description='The recipient has joined the server.', color=self.mod_color - ) + ) await thread.channel.send(embed=em) async def on_message_delete(self, message): @@ -909,7 +913,7 @@ async def autoupdate_loop(self): embed.set_author(name=user['username'] + ' - Updating Bot', icon_url=user['avatar_url'], url=user['url']) - embed.set_footer(text=f"Updating Modmail v{self.version} " + embed.set_footer(text=f'Updating Modmail v{self.version} ' f"-> v{metadata['latest_version']}") changelog = await Changelog.from_url(self) @@ -935,6 +939,7 @@ async def autoupdate_loop(self): if __name__ == '__main__': if os.name != 'nt': import uvloop + uvloop.install() bot = ModmailBot() bot.run() diff --git a/cogs/utility.py b/cogs/utility.py index 3f164bbffe..fe7be63cf3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -442,9 +442,9 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ''): raise commands.UserInputError activity, msg = (await self.set_presence( - activity_identifier=activity_type, - activity_by_key=True, - activity_message=message + activity_identifier=activity_type, + activity_by_key=True, + activity_message=message ))['activity'] if activity is None: raise commands.UserInputError @@ -488,8 +488,8 @@ async def status(self, ctx, *, status_type: str.lower): status_type = status_type.replace(' ', '_') status, msg = (await self.set_presence( - status_identifier=status_type, - status_by_key=True + status_identifier=status_type, + status_by_key=True ))['status'] if status is None: raise commands.UserInputError diff --git a/core/checks.py b/core/checks.py index d8b611a271..5fa91e8092 100644 --- a/core/checks.py +++ b/core/checks.py @@ -37,7 +37,7 @@ async def predicate(ctx): return commands.check(predicate) -async def check_permissions(ctx: commands.Context, +async def check_permissions(ctx: commands.Context, command_name: str, permission_level: PermissionLevel) -> bool: if await ctx.bot.is_owner(ctx.author): diff --git a/core/thread.py b/core/thread.py index 1fa1614858..c6962475d4 100644 --- a/core/thread.py +++ b/core/thread.py @@ -12,7 +12,6 @@ from core.utils import is_image_url, days, match_user_id from core.utils import truncate, ignore, error - logger = logging.getLogger('Modmail') @@ -104,12 +103,12 @@ async def setup(self, *, creator=None, category=None): try: log_url, log_data = await asyncio.gather( self.bot.api.create_log_entry(recipient, channel, - creator or recipient), + creator or recipient), self.bot.api.get_user_logs(recipient.id) ) log_count = sum(1 for log in log_data if not log['open']) - except: # Something went wrong with database? + except: # Something went wrong with database? log_url = log_count = None # ensure core functionality still works @@ -153,14 +152,15 @@ async def send_info_embed(): footer = self.bot.config.get('thread_creation_footer', footer) embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) - embed.title = self.bot.config.get('thread_creation_title', 'Thread Created') + embed.title = self.bot.config.get('thread_creation_title', + 'Thread Created') if creator is None: msg = await recipient.send(embed=embed) if not self.bot.config.get('disable_recipient_thread_close'): close_emoji = self.bot.config.get('close_emoji', '🔒') close_emoji = await self.bot.convert_emoji(close_emoji) - await msg.add_reaction(close_emoji) + await msg.add_reaction(close_emoji) def _close_after(self, closer, silent, delete_channel, message): return self.bot.loop.create_task( @@ -245,7 +245,7 @@ async def _close(self, closer, silent=False, delete_channel=True, user = f"{self.recipient} (`{self.id}`)" else: user = f'`{self.id}`' - + if self.id == closer.id: _closer = 'the Recipient' else: @@ -261,7 +261,7 @@ async def _close(self, closer, silent=False, delete_channel=True, tasks = [ self.bot.config.update() ] - + try: tasks.append(self.bot.log_channel.send(embed=embed)) except (ValueError, AttributeError): @@ -269,26 +269,29 @@ async def _close(self, closer, silent=False, delete_channel=True, # Thread closed message - embed = discord.Embed(title=self.bot.config.get('thread_close_title', 'Thread Closed'), - color=discord.Color.red(), - timestamp=datetime.utcnow()) + embed = discord.Embed( + title=self.bot.config.get('thread_close_title', 'Thread Closed'), + color=discord.Color.red(), + timestamp=datetime.utcnow()) if not message: if self.id == closer.id: message = self.bot.config.get( - 'thread_self_close_response', + 'thread_self_close_response', 'You have closed this Modmail thread.' - ) + ) else: message = self.bot.config.get( 'thread_close_response', '{closer.mention} has closed this Modmail thread.' - ) - - message = message.format(closer=closer, loglink=log_url, logkey=log_data['key']) + ) + + message = message.format(closer=closer, loglink=log_url, + logkey=log_data['key']) embed.description = message - footer = self.bot.config.get('thread_close_footer', 'Replying will create a new thread') + footer = self.bot.config.get('thread_close_footer', + 'Replying will create a new thread') embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) @@ -476,13 +479,12 @@ async def send(self, message, destination=None, for att in images: if not prioritize_uploads or ( - is_image_url(*att) and not - embedded_image and - att[1] + is_image_url(*att) and not embedded_image and att[1] ): embed.set_image(url=att[0]) if att[1]: - embed.add_field(name='Image', value=f'[{att[1]}]({att[0]})') + embed.add_field(name='Image', + value=f'[{att[1]}]({att[0]})') embedded_image = True elif att[1] is not None: if note: From 656b34ab36e69536502f5634dbd684086ff72b23 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Apr 2019 21:52:30 -0700 Subject: [PATCH 0008/1221] pylint --- bot.py | 8 ++++---- cogs/modmail.py | 7 +++---- cogs/utility.py | 2 +- core/config.py | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/bot.py b/bot.py index 7133e9e406..7cbd6cac45 100644 --- a/bot.py +++ b/bot.py @@ -741,20 +741,20 @@ async def on_guild_channel_delete(self, channel): async def on_member_remove(self, member): thread = await self.threads.find(recipient=member) if thread: - em = discord.Embed( + embed = discord.Embed( description='The recipient has left the server.', color=discord.Color.red() ) - await thread.channel.send(embed=em) + await thread.channel.send(embed=embed) async def on_member_join(self, member): thread = await self.threads.find(recipient=member) if thread: - em = discord.Embed( + embed = discord.Embed( description='The recipient has joined the server.', color=self.mod_color ) - await thread.channel.send(embed=em) + await thread.channel.send(embed=embed) async def on_message_delete(self, message): """Support for deleting linked messages""" diff --git a/cogs/modmail.py b/cogs/modmail.py index 4f04f8f494..abaa6f7e6e 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,12 +1,11 @@ import asyncio +import re from datetime import datetime from typing import Optional, Union import discord from discord.ext import commands -import re - from dateutil import parser from natural.date import duration @@ -708,9 +707,9 @@ async def block(self, ctx, user: Optional[User] = None, *, reason = after.arg if reason.startswith('System Message: '): raise commands.UserInputError - elif re.search(r'%(.+?)%$', reason) is not None: + if re.search(r'%(.+?)%$', reason) is not None: raise commands.UserInputError - elif after.dt > after.now: + if after.dt > after.now: reason = f'{reason} %{after.dt.isoformat()}%' if not reason: diff --git a/cogs/utility.py b/cogs/utility.py index fe7be63cf3..c2280b2353 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -445,7 +445,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ''): activity_identifier=activity_type, activity_by_key=True, activity_message=message - ))['activity'] + ))['activity'] if activity is None: raise commands.UserInputError diff --git a/core/config.py b/core/config.py index 88d30b0392..bcd940d5fb 100644 --- a/core/config.py +++ b/core/config.py @@ -29,7 +29,7 @@ class ConfigManager(ConfigManagerABC): 'thread_creation_response', 'thread_creation_footer', 'thread_creation_title', 'thread_close_footer', 'thread_close_title', 'thread_close_response', 'thread_self_close_response', - + # moderation 'recipient_color', 'mod_tag', 'mod_color', From a56feec0aed160b90e8aeb90521dc696e0ad183d Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Apr 2019 21:55:44 -0700 Subject: [PATCH 0009/1221] Updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c37318275d..b155bbced3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,9 +34,12 @@ To view all roles and users with permission for a permission group or command do - `?permissions get command command-name` - `?permissions get level owner` +By default, all newly set up Modmail will have `OWNER` set to the owner of the bot, and `EGULAR` set to @everyone. + ### Note When updating to this version, all prior permission settings with guild-based permissions will be invalidated. You will need to convert to the above system. +`OWNERS` will also get removed, you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. # v2.17.1 From 2ea2246c7dbc684ba40761098cf5f50919366b2a Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 29 Apr 2019 18:15:17 -0700 Subject: [PATCH 0010/1221] Fixed a typo --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index c2280b2353..558db1ae27 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1075,7 +1075,7 @@ async def get_perms_level(self, ctx, level: str): f'to does not exist: `{level}`.' ) return await ctx.send(embed=embed) - permissions = self.bot.config.command_permissions.get(PermissionLevel[level.upper()].name) + permissions = self.bot.config.level_permissions.get(PermissionLevel[level.upper()].name) if permissions is None or not permissions: embed = Embed( title=f'Permission entries for permission level `{level}`:', From cd94027ab81e195ed332fdf2e1b3869441b3e8c2 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 29 Apr 2019 19:14:01 -0700 Subject: [PATCH 0011/1221] Fixed a typo, changed help message to include all commands --- CHANGELOG.md | 12 +++++---- cogs/utility.py | 71 +++++++++++++++++++------------------------------ core/checks.py | 2 ++ 3 files changed, 37 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b155bbced3..cdd7d18ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desire permission level specific to a command or a group of commands for a role or user. - There are five permission groups/levels: - - Owner - - Administrator - - Moderator - - Supporter - - Regular + - Owner [5] + - Administrator [4] + - Moderator [3] + - Supporter [2] + - Regular [1] You may add a role or user to a permission group through any of the following methods: - `?permissions add level owner @role` @@ -36,6 +36,8 @@ To view all roles and users with permission for a permission group or command do By default, all newly set up Modmail will have `OWNER` set to the owner of the bot, and `EGULAR` set to @everyone. +The help message no longer conceal inaccessible commands due to check failures. + ### Note When updating to this version, all prior permission settings with guild-based permissions will be invalidated. You will need to convert to the above system. diff --git a/cogs/utility.py b/cogs/utility.py index 558db1ae27..d15bb32b65 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -10,7 +10,6 @@ from json import JSONDecodeError from textwrap import indent -import discord from discord import Embed, Color, Activity from discord.enums import ActivityType, Status from discord.ext import commands @@ -28,23 +27,11 @@ class Utility: - """General commands that provide utility""" + """General commands that provide utility.""" def __init__(self, bot: Bot): self.bot = bot - @staticmethod - async def verify_checks(ctx, cmd): - predicates = cmd.checks - if not predicates: - return True - - try: - return await discord.utils.async_all(predicate(ctx) - for predicate in predicates) - except commands.CheckFailure: - return False - async def format_cog_help(self, ctx, cog): """Formats the text for a cog help""" @@ -53,10 +40,13 @@ async def format_cog_help(self, ctx, cog): fmts = [''] for cmd in sorted(self.bot.commands, key=lambda cmd: cmd.qualified_name): - if cmd.instance is cog and not cmd.hidden and \ - await self.verify_checks(ctx, cmd): - new_fmt = f'`{prefix + cmd.qualified_name}` - ' - new_fmt += f'{cmd.short_doc}\n' + if cmd.instance is cog and not cmd.hidden: + new_fmt = f'`{prefix + cmd.qualified_name}` ' + perm_level = next(getattr(c, 'permission_level', None) for c in cmd.checks) + if perm_level is not None: + new_fmt += f'[{perm_level}] ' + + new_fmt += f'- {cmd.short_doc}\n' if len(new_fmt) + len(fmts[-1]) >= 1024: fmts.append(new_fmt) else: @@ -81,19 +71,21 @@ async def format_cog_help(self, ctx, cog): embeds.append(embed) return embeds - async def format_command_help(self, ctx, cmd): + async def format_command_help(self, cmd): """Formats command help.""" - if cmd.hidden or not await self.verify_checks(ctx, cmd): + if cmd.hidden: return None prefix = self.bot.prefix + + perm_level = next(getattr(c, 'permission_level', None) for c in cmd.checks) + perm_level = f' [{perm_level}]' if perm_level is not None else '' embed = Embed( + title=f'`{prefix}{cmd.signature}`{perm_level}', color=self.bot.main_color, description=cmd.help ) - embed.title = f'`{prefix}{cmd.signature}`' - if not isinstance(cmd, commands.Group): return embed @@ -124,20 +116,12 @@ async def format_not_found(self, ctx, command): 'a full list of commands.') choices = set() - # filter out hidden commands & blank cogs - for i in self.bot.cogs: - for cmd in self.bot.commands: - if cmd.cog_name == i and not cmd.hidden and \ - await self.verify_checks(ctx, cmd): - # as long as there's one valid cmd, add cog - choices.add(i) - break - - for i in self.bot.commands: - if not i.hidden and await self.verify_checks(ctx, i): - choices.add(i.name) - - closest = get_close_matches(command, choices, n=1, cutoff=0.45) + + for name, c in self.bot.all_commands: + if not c.hidden: + choices.add(name) + + closest = get_close_matches(command, choices, n=1, cutoff=0.75) if closest: # Perhaps you meant: # - `item` @@ -157,7 +141,7 @@ async def help_(self, ctx, *, command: str = None): embeds = [] if cmd: - help_msg = await self.format_command_help(ctx, cmd) + help_msg = await self.format_command_help(cmd) if help_msg: embeds = [help_msg] @@ -876,11 +860,11 @@ async def permissions(self, ctx): levels. Acceptable permission levels are: - - **Owner** (absolute control over the bot) - - **Administrator** (administrative powers such as setting activities) - - **Moderator** (ability to block) - - **Supporter** (access to core Modmail supporting functions) - - **Regular** (most basic interactions such as help and about) + - **Owner** [5] (absolute control over the bot) + - **Administrator** [4] (administrative powers such as setting activities) + - **Moderator** [3] (ability to block) + - **Supporter** [2] (access to core Modmail supporting functions) + - **Regular** [1] (most basic interactions such as help and about) By default, owner is set to the bot owner and regular is @everyone. @@ -955,7 +939,8 @@ async def add_perms_level(self, ctx, level: str, user_or_role: Union[User, str]) ) return await ctx.send(embed=embed) - @permissions.group(name='remove', aliases=['del', 'delete', 'rm', 'revoke']) + @permissions.group(name='remove', aliases=['del', 'delete', 'rm', 'revoke'], + invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def remove_perms(self, ctx): """Remove a permission to use a command or permission level.""" diff --git a/core/checks.py b/core/checks.py index 5fa91e8092..cfdbf33276 100644 --- a/core/checks.py +++ b/core/checks.py @@ -34,6 +34,8 @@ async def predicate(ctx): logger.error(error(f'You does not have permission to use this command: ' f'`{ctx.command.qualified_name}` ({permission_level.name}).')) return has_perm + + predicate.permission_level = permission_level return commands.check(predicate) From 41ce2eaaa408777e08da2c9e683be2043f25a927 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 29 Apr 2019 19:19:40 -0700 Subject: [PATCH 0012/1221] Use * in command parameter --- cogs/utility.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index d15bb32b65..acf36a64bc 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -883,7 +883,7 @@ async def add_perms(self, ctx): @add_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms_command(self, ctx, command: str, user_or_role: Union[User, str]): + async def add_perms_command(self, ctx, *, command: str, user_or_role: Union[User, str]): """Add a user, role, or everyone permission to use a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -912,7 +912,7 @@ async def add_perms_command(self, ctx, command: str, user_or_role: Union[User, s @add_perms.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms_level(self, ctx, level: str, user_or_role: Union[User, str]): + async def add_perms_level(self, ctx, *, level: str, user_or_role: Union[User, str]): """Add a user, role, or everyone permission to use commands of a permission level.""" if level.upper() not in PermissionLevel._member_names_: embed = Embed( @@ -949,7 +949,7 @@ async def remove_perms(self, ctx): @remove_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms_command(self, ctx, command: str, user_or_role: Union[User, str]): + async def remove_perms_command(self, ctx, *, command: str, user_or_role: Union[User, str]): """Remove a user, role, or everyone permission to use a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -978,7 +978,7 @@ async def remove_perms_command(self, ctx, command: str, user_or_role: Union[User @remove_perms.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms_level(self, ctx, level: str, user_or_role: Union[User, str]): + async def remove_perms_level(self, ctx, *, level: str, user_or_role: Union[User, str]): """Remove a user, role, or everyone permission to use commands of a permission level.""" if level.upper() not in PermissionLevel._member_names_: embed = Embed( @@ -1014,7 +1014,7 @@ async def get_perms(self, ctx): @get_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def get_perms_command(self, ctx, command: str): + async def get_perms_command(self, ctx, *, command: str): """View the currently-set permissions for a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -1050,7 +1050,7 @@ async def get_perms_command(self, ctx, command: str): @get_perms.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def get_perms_level(self, ctx, level: str): + async def get_perms_level(self, ctx, *, level: str): """View the currently-set permissions for commands of a permission level.""" if level.upper() not in PermissionLevel._member_names_: embed = Embed( @@ -1086,7 +1086,7 @@ async def get_perms_level(self, ctx, level: str): @commands.command(hidden=True, name='eval') @checks.has_permissions(PermissionLevel.OWNER) - async def eval_(self, ctx, *, body): + async def eval_(self, ctx, *, body: str): """Evaluates Python code""" env = { From 82df63b472f8164f40c1f879f7ed4ecdd66e1f35 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 30 Apr 2019 19:25:15 -0700 Subject: [PATCH 0013/1221] Fixed * problem, added get command and get level for permissions --- cogs/utility.py | 153 ++++++++++++++++++++++++++++-------------------- 1 file changed, 89 insertions(+), 64 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index acf36a64bc..8712396813 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -883,7 +883,7 @@ async def add_perms(self, ctx): @add_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms_command(self, ctx, *, command: str, user_or_role: Union[User, str]): + async def add_perms_command(self, ctx, command: str, user_or_role: Union[User, str]): """Add a user, role, or everyone permission to use a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -912,7 +912,7 @@ async def add_perms_command(self, ctx, *, command: str, user_or_role: Union[User @add_perms.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms_level(self, ctx, *, level: str, user_or_role: Union[User, str]): + async def add_perms_level(self, ctx, level: str, user_or_role: Union[User, str]): """Add a user, role, or everyone permission to use commands of a permission level.""" if level.upper() not in PermissionLevel._member_names_: embed = Embed( @@ -949,7 +949,7 @@ async def remove_perms(self, ctx): @remove_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms_command(self, ctx, *, command: str, user_or_role: Union[User, str]): + async def remove_perms_command(self, ctx, command: str, user_or_role: Union[User, str]): """Remove a user, role, or everyone permission to use a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -978,7 +978,7 @@ async def remove_perms_command(self, ctx, *, command: str, user_or_role: Union[U @remove_perms.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms_level(self, ctx, *, level: str, user_or_role: Union[User, str]): + async def remove_perms_level(self, ctx, level: str, user_or_role: Union[User, str]): """Remove a user, role, or everyone permission to use commands of a permission level.""" if level.upper() not in PermissionLevel._member_names_: embed = Embed( @@ -1014,75 +1014,100 @@ async def get_perms(self, ctx): @get_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def get_perms_command(self, ctx, *, command: str): + async def get_perms_command(self, ctx, *, command: str = None): """View the currently-set permissions for a command.""" - if command not in self.bot.all_commands: - embed = Embed( - title='Error', - color=Color.red(), - description='The command you are attempting to point ' - f'to does not exist: `{command}`.' - ) - return await ctx.send(embed=embed) - permissions = self.bot.config.command_permissions.get(self.bot.all_commands[command].name) - if permissions is None or not permissions: - embed = Embed( - title=f'Permission entries for command `{command}`:', - description='No permission entries found.' - ) - else: - values = [] - for perm in permissions: - if perm == -1: - values.insert(0, '**everyone**') - else: - user = self.bot.get_user(perm) - if user is not None: - values.append(user.name) + + def get_command(cmd): + permissions = self.bot.config.command_permissions.get(cmd.name) + if permissions is None or not permissions: + embed = Embed( + title=f'Permission entries for command `{cmd.name}`:', + description='No permission entries found.' + ) + else: + values = [] + for perm in permissions: + if perm == -1: + values.insert(0, '**everyone**') else: - values.append(str(perm)) + user = self.bot.get_user(perm) + if user is not None: + values.append(user.name) + else: + values.append(str(perm)) - embed = Embed( - title=f'Permission entries for command `{command}`:', - description=', '.join(values) - ) - return await ctx.send(embed=embed) + embed = Embed( + title=f'Permission entries for command `{cmd.name}`:', + description=', '.join(values) + ) + return embed + + embeds = [] + if command is not None: + if command not in self.bot.all_commands: + embed = Embed( + title='Error', + color=Color.red(), + description='The command you are attempting to point ' + f'to does not exist: `{command}`.' + ) + return await ctx.send(embed=embed) + embeds.append(get_command(self.bot.all_commands[command])) + else: + for cmd in self.bot.commands: + embeds.append(get_command(cmd)) + + p_session = PaginatorSession(ctx, *embeds) + return await p_session.run() @get_perms.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def get_perms_level(self, ctx, *, level: str): + async def get_perms_level(self, ctx, *, level: str = None): """View the currently-set permissions for commands of a permission level.""" - if level.upper() not in PermissionLevel._member_names_: - embed = Embed( - title='Error', - color=Color.red(), - description='The permission level you are attempting to point ' - f'to does not exist: `{level}`.' - ) - return await ctx.send(embed=embed) - permissions = self.bot.config.level_permissions.get(PermissionLevel[level.upper()].name) - if permissions is None or not permissions: - embed = Embed( - title=f'Permission entries for permission level `{level}`:', - description='No permission entries found.' - ) - else: - values = [] - for perm in permissions: - if perm == -1: - values.insert(0, '**everyone**') - else: - user = self.bot.get_user(perm) - if user is not None: - values.append(user.name) + + def get_level(perm_level): + permissions = self.bot.config.level_permissions.get(perm_level.name) + if permissions is None or not permissions: + embed = Embed( + title='Permission entries for permission ' + f'level `{perm_level.name}`:', + description='No permission entries found.' + ) + else: + values = [] + for perm in permissions: + if perm == -1: + values.insert(0, '**everyone**') else: - values.append(str(perm)) + user = self.bot.get_user(perm) + if user is not None: + values.append(user.name) + else: + values.append(str(perm)) - embed = Embed( - title=f'Permission entries for permission level `{level}`:', - description=', '.join(values) - ) - return await ctx.send(embed=embed) + embed = Embed( + title=f'Permission entries for permission level `{perm_level.name}`:', + description=', '.join(values) + ) + return embed + + embeds = [] + if level is not None: + if level.upper() not in PermissionLevel._member_names_: + embed = Embed( + title='Error', + color=Color.red(), + description='The permission level you are attempting to point ' + f'to does not exist: `{level}`.' + ) + return await ctx.send(embed=embed) + embeds.append(get_level(PermissionLevel[level.upper()].name)) + else: + for perm_level in PermissionLevel: + embeds.append(get_level(perm_level)) + + p_session = PaginatorSession(ctx, *embeds) + return await p_session.run() @commands.command(hidden=True, name='eval') @checks.has_permissions(PermissionLevel.OWNER) From ac5d5848e8b03cf0047b69d97c6a66770b4bcbd6 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 30 Apr 2019 19:36:44 -0700 Subject: [PATCH 0014/1221] Use * again --- cogs/utility.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 8712396813..1e07fe9dcf 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -883,7 +883,7 @@ async def add_perms(self, ctx): @add_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms_command(self, ctx, command: str, user_or_role: Union[User, str]): + async def add_perms_command(self, ctx, command: str, *, user_or_role: Union[User, str]): """Add a user, role, or everyone permission to use a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -912,7 +912,7 @@ async def add_perms_command(self, ctx, command: str, user_or_role: Union[User, s @add_perms.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms_level(self, ctx, level: str, user_or_role: Union[User, str]): + async def add_perms_level(self, ctx, level: str, *, user_or_role: Union[User, str]): """Add a user, role, or everyone permission to use commands of a permission level.""" if level.upper() not in PermissionLevel._member_names_: embed = Embed( @@ -949,7 +949,7 @@ async def remove_perms(self, ctx): @remove_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms_command(self, ctx, command: str, user_or_role: Union[User, str]): + async def remove_perms_command(self, ctx, command: str, *, user_or_role: Union[User, str]): """Remove a user, role, or everyone permission to use a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -978,7 +978,7 @@ async def remove_perms_command(self, ctx, command: str, user_or_role: Union[User @remove_perms.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms_level(self, ctx, level: str, user_or_role: Union[User, str]): + async def remove_perms_level(self, ctx, level: str, *, user_or_role: Union[User, str]): """Remove a user, role, or everyone permission to use commands of a permission level.""" if level.upper() not in PermissionLevel._member_names_: embed = Embed( From 55223ecbab1f4c37d95f2913bc05dbf9cb6018c2 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 30 Apr 2019 20:42:19 -0700 Subject: [PATCH 0015/1221] Added perm search by role/user/id --- cogs/utility.py | 122 +++++++++++++++++++++++++++++++++--------------- core/models.py | 2 + 2 files changed, 86 insertions(+), 38 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 1e07fe9dcf..0786c2c12b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -10,7 +10,7 @@ from json import JSONDecodeError from textwrap import indent -from discord import Embed, Color, Activity +from discord import Embed, Color, Activity, Role from discord.enums import ActivityType, Status from discord.ext import commands @@ -883,7 +883,7 @@ async def add_perms(self, ctx): @add_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms_command(self, ctx, command: str, *, user_or_role: Union[User, str]): + async def add_perms_command(self, ctx, command: str, *, user_or_role: Union[User, Role, str]): """Add a user, role, or everyone permission to use a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -899,9 +899,8 @@ async def add_perms_command(self, ctx, command: str, *, user_or_role: Union[User elif user_or_role in {'everyone', 'all'}: value = -1 else: - converter = commands.RoleConverter() - role = await converter.convert(ctx, user_or_role) - value = role.id + raise commands.CommandInvokeError('Invalid user or role.') + await self.bot.update_perms(self.bot.all_commands[command].name, value) embed = Embed( title='Success', @@ -912,9 +911,9 @@ async def add_perms_command(self, ctx, command: str, *, user_or_role: Union[User @add_perms.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms_level(self, ctx, level: str, *, user_or_role: Union[User, str]): + async def add_perms_level(self, ctx, level: str, *, user_or_role: Union[User, Role, str]): """Add a user, role, or everyone permission to use commands of a permission level.""" - if level.upper() not in PermissionLevel._member_names_: + if level.upper() not in PermissionLevel.__members__: embed = Embed( title='Error', color=Color.red(), @@ -928,9 +927,8 @@ async def add_perms_level(self, ctx, level: str, *, user_or_role: Union[User, st elif user_or_role in {'everyone', 'all'}: value = -1 else: - converter = commands.RoleConverter() - role = await converter.convert(ctx, user_or_role) - value = role.id + raise commands.CommandInvokeError('Invalid user or role.') + await self.bot.update_perms(PermissionLevel[level.upper()], value) embed = Embed( title='Success', @@ -949,7 +947,7 @@ async def remove_perms(self, ctx): @remove_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms_command(self, ctx, command: str, *, user_or_role: Union[User, str]): + async def remove_perms_command(self, ctx, command: str, *, user_or_role: Union[User, Role, str]): """Remove a user, role, or everyone permission to use a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -965,9 +963,8 @@ async def remove_perms_command(self, ctx, command: str, *, user_or_role: Union[U elif user_or_role in {'everyone', 'all'}: value = -1 else: - converter = commands.RoleConverter() - role = await converter.convert(ctx, user_or_role) - value = role.id + raise commands.CommandInvokeError('Invalid user or role.') + await self.bot.update_perms(self.bot.all_commands[command].name, value, add=False) embed = Embed( title='Success', @@ -978,9 +975,9 @@ async def remove_perms_command(self, ctx, command: str, *, user_or_role: Union[U @remove_perms.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms_level(self, ctx, level: str, *, user_or_role: Union[User, str]): + async def remove_perms_level(self, ctx, level: str, *, user_or_role: Union[User, Role, str]): """Remove a user, role, or everyone permission to use commands of a permission level.""" - if level.upper() not in PermissionLevel._member_names_: + if level.upper() not in PermissionLevel.__members__: embed = Embed( title='Error', color=Color.red(), @@ -994,9 +991,8 @@ async def remove_perms_level(self, ctx, level: str, *, user_or_role: Union[User, elif user_or_role in {'everyone', 'all'}: value = -1 else: - converter = commands.RoleConverter() - role = await converter.convert(ctx, user_or_role) - value = role.id + raise commands.CommandInvokeError('Invalid user or role.') + await self.bot.update_perms(PermissionLevel[level.upper()], value, add=False) embed = Embed( title='Success', @@ -1007,10 +1003,44 @@ async def remove_perms_level(self, ctx, level: str, *, user_or_role: Union[User, @permissions.group(name='get', invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) - async def get_perms(self, ctx): + async def get_perms(self, ctx, *, user_or_role: Union[User, Role, str]): """View the currently-set permissions.""" - cmd = self.bot.get_command('help') - await ctx.invoke(cmd, command='perms get') + + if hasattr(user_or_role, 'id'): + value = user_or_role.id + elif user_or_role in {'everyone', 'all'}: + value = -1 + else: + raise commands.CommandInvokeError('Invalid user or role.') + + cmds = [] + levels = [] + for cmd in self.bot.commands: + permissions = self.bot.config.command_permissions.get(cmd.name, []) + if value in permissions: + cmds.append(cmd.name) + for level in PermissionLevel: + permissions = self.bot.config.level_permissions.get(level.name, []) + if value in permissions: + levels.append(level.name) + mention = user_or_role.name if hasattr(user_or_role, 'name') else user_or_role + desc_cmd = ', '.join(map(lambda x: f'`{x}`', cmds)) if cmds else 'No permission entries found.' + desc_level = ', '.join(map(lambda x: f'`{x}`', levels)) if levels else 'No permission entries found.' + + embeds = [ + Embed( + title=f'{mention} has permission with the following commands:', + description=desc_cmd, + color=self.bot.main_color + ), + Embed( + title=f'{mention} has permission with the following permission groups:', + description=desc_level, + color=self.bot.main_color + ) + ] + p_session = PaginatorSession(ctx, *embeds) + return await p_session.run() @get_perms.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) @@ -1018,8 +1048,8 @@ async def get_perms_command(self, ctx, *, command: str = None): """View the currently-set permissions for a command.""" def get_command(cmd): - permissions = self.bot.config.command_permissions.get(cmd.name) - if permissions is None or not permissions: + permissions = self.bot.config.command_permissions.get(cmd.name, []) + if not permissions: embed = Embed( title=f'Permission entries for command `{cmd.name}`:', description='No permission entries found.' @@ -1029,12 +1059,20 @@ def get_command(cmd): for perm in permissions: if perm == -1: values.insert(0, '**everyone**') + continue + member = ctx.guild.get_member(perm) + if member is not None: + values.append(member.mention) + continue + user = self.bot.get_user(perm) + if user is not None: + values.append(user.mention) + continue + role = ctx.guild.get_role(perm) + if role is not None: + values.append(role.mention) else: - user = self.bot.get_user(perm) - if user is not None: - values.append(user.name) - else: - values.append(str(perm)) + values.append(str(perm)) embed = Embed( title=f'Permission entries for command `{cmd.name}`:', @@ -1066,8 +1104,8 @@ async def get_perms_level(self, ctx, *, level: str = None): """View the currently-set permissions for commands of a permission level.""" def get_level(perm_level): - permissions = self.bot.config.level_permissions.get(perm_level.name) - if permissions is None or not permissions: + permissions = self.bot.config.level_permissions.get(perm_level.name, []) + if not permissions: embed = Embed( title='Permission entries for permission ' f'level `{perm_level.name}`:', @@ -1078,12 +1116,20 @@ def get_level(perm_level): for perm in permissions: if perm == -1: values.insert(0, '**everyone**') + continue + member = ctx.guild.get_member(perm) + if member is not None: + values.append(member.mention) + continue + user = self.bot.get_user(perm) + if user is not None: + values.append(user.mention) + continue + role = ctx.guild.get_role(perm) + if role is not None: + values.append(role.mention) else: - user = self.bot.get_user(perm) - if user is not None: - values.append(user.name) - else: - values.append(str(perm)) + values.append(str(perm)) embed = Embed( title=f'Permission entries for permission level `{perm_level.name}`:', @@ -1093,7 +1139,7 @@ def get_level(perm_level): embeds = [] if level is not None: - if level.upper() not in PermissionLevel._member_names_: + if level.upper() not in PermissionLevel.__members__: embed = Embed( title='Error', color=Color.red(), @@ -1101,7 +1147,7 @@ def get_level(perm_level): f'to does not exist: `{level}`.' ) return await ctx.send(embed=embed) - embeds.append(get_level(PermissionLevel[level.upper()].name)) + embeds.append(get_level(PermissionLevel[level.upper()])) else: for perm_level in PermissionLevel: embeds.append(get_level(perm_level)) diff --git a/core/models.py b/core/models.py index 528c40beca..839d23a42c 100644 --- a/core/models.py +++ b/core/models.py @@ -15,7 +15,9 @@ class PermissionLevel(IntEnum): OWNER = 5 ADMINISTRATOR = 4 + ADMIN = 4 MODERATOR = 3 + MOD = 3 SUPPORTER = 2 REGULAR = 1 From 92c706ef4b4d074286586bfbe4a6ab03794a70a1 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 30 Apr 2019 21:12:27 -0700 Subject: [PATCH 0016/1221] Use UserInputError instead of CommandInvokeError --- cogs/utility.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 0786c2c12b..c23a29ac2d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -899,7 +899,7 @@ async def add_perms_command(self, ctx, command: str, *, user_or_role: Union[User elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.CommandInvokeError('Invalid user or role.') + raise commands.UserInputError('Invalid user or role.') await self.bot.update_perms(self.bot.all_commands[command].name, value) embed = Embed( @@ -927,7 +927,7 @@ async def add_perms_level(self, ctx, level: str, *, user_or_role: Union[User, Ro elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.CommandInvokeError('Invalid user or role.') + raise commands.UserInputError('Invalid user or role.') await self.bot.update_perms(PermissionLevel[level.upper()], value) embed = Embed( @@ -963,7 +963,7 @@ async def remove_perms_command(self, ctx, command: str, *, user_or_role: Union[U elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.CommandInvokeError('Invalid user or role.') + raise commands.UserInputError('Invalid user or role.') await self.bot.update_perms(self.bot.all_commands[command].name, value, add=False) embed = Embed( @@ -991,7 +991,7 @@ async def remove_perms_level(self, ctx, level: str, *, user_or_role: Union[User, elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.CommandInvokeError('Invalid user or role.') + raise commands.UserInputError('Invalid user or role.') await self.bot.update_perms(PermissionLevel[level.upper()], value, add=False) embed = Embed( @@ -1011,7 +1011,7 @@ async def get_perms(self, ctx, *, user_or_role: Union[User, Role, str]): elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.CommandInvokeError('Invalid user or role.') + raise commands.UserInputError('Invalid user or role.') cmds = [] levels = [] From ed6047c53b53b6da18d776e2128dc1e4f6cf15c7 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 30 Apr 2019 21:45:44 -0700 Subject: [PATCH 0017/1221] Added a delete command --- cogs/modmail.py | 64 ++++++++++++++++++++++++++++++++++++------------- core/thread.py | 29 +++++++++++++++++----- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index abaa6f7e6e..d3699d348c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -566,21 +566,7 @@ async def note(self, ctx, *, msg=''): async with ctx.typing(): await ctx.thread.note(ctx.message) - @commands.command() - @checks.has_permissions(PermissionLevel.SUPPORTER) - @checks.thread_only() - async def edit(self, ctx, message_id: Optional[int] = None, - *, new_message): - """Edit a message that was sent using the reply command. - - If no `message_id` is provided, the - last message sent by a mod will be edited. - - `[message_id]` the id of the message that you want to edit. - `new_message` is the new message that will be edited in. - """ - thread = ctx.thread - + async def find_linked_message(self, ctx, message_id): linked_message_id = None async for msg in ctx.channel.history(): @@ -600,8 +586,31 @@ async def edit(self, ctx, message_id: Optional[int] = None, linked_message_id = str(url).split('/')[-1] break - if not linked_message_id: - raise commands.UserInputError + return linked_message_id + + @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def edit(self, ctx, message_id: Optional[int] = None, + *, new_message): + """Edit a message that was sent using the reply command. + + If no `message_id` is provided, the + last message sent by a mod will be edited. + + `[message_id]` the id of the message that you want to edit. + `new_message` is the new message that will be edited in. + """ + thread = ctx.thread + + linked_message_id = await self.find_linked_message(ctx, message_id) + + if linked_message_id is None: + return await ctx.send(embed=discord.Embed( + title='Failed', + description='Cannot find a message to edit.', + color=discord.Color.red() + )) await asyncio.gather( thread.edit_message(linked_message_id, new_message), @@ -801,6 +810,27 @@ async def unblock(self, ctx, *, user: User = None): return await ctx.send(embed=embed) + @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def delete(self, ctx, message_id: int = None): + """ + Deletes the previous message, unless a message ID is provided, which in that case, + delete the message with that message ID. + """ + thread = ctx.thread + + linked_message_id = await self.find_linked_message(ctx, message_id) + + if linked_message_id is None: + return await ctx.send(embed=discord.Embed( + title='Failed', + description='Cannot find a message to delete.', + color=discord.Color.red() + )) + + await thread.delete_message(linked_message_id) + await ctx.message.add_reaction('✅') + def setup(bot): bot.add_cog(Modmail(bot)) diff --git a/core/thread.py b/core/thread.py index c6962475d4..9b6040db70 100644 --- a/core/thread.py +++ b/core/thread.py @@ -313,21 +313,38 @@ async def cancel_closure(self): await self.bot.config.update() @staticmethod - async def _edit_thread_message(channel, message_id, message): + 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]: - embed.description = message - await msg.edit(embed=embed) - break + return msg async def edit_message(self, message_id, message): + msg_recipient, msg_channel = await asyncio.gather( + self._find_thread_message(self.recipient, message_id), + self._find_thread_message(self.channel, message_id) + ) + + embed_recipient = msg_recipient.embeds[0] + embed_channel = msg_recipient.embeds[0] + embed_recipient.description = message + embed_channel.description = message + await asyncio.gather( + msg_recipient.edit(embed=embed_recipient), + msg_channel.edit(embed=embed_channel) + ) + + 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) + ) await asyncio.gather( - self._edit_thread_message(self.recipient, message_id, message), - self._edit_thread_message(self.channel, message_id, message) + msg_recipient.delete(), + msg_channel.delete() ) async def note(self, message): From adbbb85b074c2664782b535cad3c5b27b207df1f Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 30 Apr 2019 21:48:06 -0700 Subject: [PATCH 0018/1221] Updated CHANGELOG.md --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd7d18ea0..22522e62ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### What's new? -- A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desire permission level specific to a command or a group of commands for a role or user. +- A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. - There are five permission groups/levels: - Owner [5] - Administrator [4] @@ -24,11 +24,11 @@ You may add a role or user to a permission group through any of the following me - `?permissions add level moderator @member#1234` - `?permissions add level administrator 78912384930291853` -The same applies for individual commands permissions: +The same applies to individual commands permissions: - `?permissions add command-name @member#1234` - ... and the other methods listed above. -To revoke a permission, use `remove` instead of `add`. +To revoke permission, use `remove` instead of `add`. To view all roles and users with permission for a permission group or command do: - `?permissions get command command-name` @@ -38,6 +38,8 @@ By default, all newly set up Modmail will have `OWNER` set to the owner of the b The help message no longer conceal inaccessible commands due to check failures. +A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer require manage messages permission to recall thread messages. + ### Note When updating to this version, all prior permission settings with guild-based permissions will be invalidated. You will need to convert to the above system. From 6dce77b7a27a5a3442f7f04bb41a4e315d748531 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Fri, 10 May 2019 09:16:50 +0800 Subject: [PATCH 0019/1221] Remove reliability on Modmail API (#219) * Remove modmail.tk link in changelog * Removed api * Remove instance from readme, and other references * Bump version 2.16.0 --- CHANGELOG.md | 25 ++++++--- README.md | 7 +-- bot.py | 97 +++++----------------------------- cogs/modmail.py | 22 +------- cogs/utility.py | 69 +++++++++++------------- core/changelog.py | 12 ++--- core/clients.py | 129 --------------------------------------------- core/config.py | 2 +- core/decorators.py | 15 ++---- core/models.py | 20 +------ core/thread.py | 8 +-- 11 files changed, 76 insertions(+), 330 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80e3c71d25..3f211d5862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,16 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Logs search command will search through log keys as well now. - -e.g. `?logs search e7499e82f8ff` +- For example, `?logs search e7499e82f8ff`. # v2.17.1 +### What's new? -### Changed - -- Stricter fallback genesis embed search. This update shouldn't affect anyone. +Stricter fallback genesis embed search. -### Info +### Changed How modmail checks if a channel is a thread: 1. First the bot checks if the channel topic is in the format `User ID: xxxx`, this means it is a thread. @@ -46,10 +44,21 @@ An issue where a scheduled close would not execute over a long period of time if # v2.16.0 -### What's new? +### Changed + +All support for Modmail API (api.modmail.tk) has terminated. +If you're still using api.modmail.tk, you will need to migrate to the self-hosted database +option ASAP. Your bot will not work unless you switch to the self-hosted option. Refer to the +installation tutorial for information regarding self-hosted Modmail. If a member leaves/joins (again) while they are a recipient of a thread, a message will be sent to notify you that this has occured. +# v2.15.1 + +### Fixed + +Emergency patch of a SyntaxError. + # v2.15.0 ### What's new? @@ -237,7 +246,7 @@ Huge thanks to Sasiko for reporting these issues. # v2.12.0 -### Important +### Important **In the future, the Modmail API (https://modmail.tk) will be deprecated. This is due to the fact that we are providing a free service without getting anything in return, and thus we do not have the resources to scale to accommodate for more users. We recommend using your own database for logs. In the future you will soon get a `backup` command so you can download all your pre-existing data and migrate to your own database.** diff --git a/README.md b/README.md index f8e6b07e98..d8bcc7273c 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,6 @@ Support - - Bot instances - - Python 3.7 @@ -56,8 +52,7 @@ Have you sent something with the `?reply` command by accident? Don't fret, you c ## Thread Logs -Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://logs.modmail.tk/02032d65a6f3 - +Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://modmail-logs.herokuapp.com/logs/02032d65a6f3. # Contributing diff --git a/bot.py b/bot.py index 56502badac..5dd69fef6d 100644 --- a/bot.py +++ b/bot.py @@ -31,6 +31,7 @@ import sys from datetime import datetime +from pkg_resources import parse_version from types import SimpleNamespace import discord @@ -45,7 +46,7 @@ from motor.motor_asyncio import AsyncIOMotorClient from core.changelog import Changelog -from core.clients import ModmailApiClient, SelfHostedClient, PluginDatabaseClient +from core.clients import SelfHostedClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import info, error from core.models import Bot @@ -104,14 +105,10 @@ def __init__(self): self._configure_logging() - if self.self_hosted: - self._db = AsyncIOMotorClient(self.config.mongo_uri).modmail_bot - self._api = SelfHostedClient(self) - else: - self._api = ModmailApiClient(self) + self._db = AsyncIOMotorClient(self.config.mongo_uri).modmail_bot + self._api = SelfHostedClient(self) self.plugin_db = PluginDatabaseClient(self) - self.data_task = self.loop.create_task(self.data_loop()) self.autoupdate_task = self.loop.create_task(self.autoupdate_loop()) self._load_extensions() self.owner = None @@ -144,10 +141,6 @@ def version(self): def db(self): return self._db - @property - def self_hosted(self): - return bool(self.config.get('mongo_uri', '')) - @property def api(self): return self._api @@ -210,12 +203,6 @@ def run(self, *args, **kwargs): except Exception: logger.critical(error('Fatal exception'), exc_info=True) finally: - try: - self.data_task.cancel() - self.loop.run_until_complete(self.data_task) - except asyncio.CancelledError: - logger.debug('data_task has been cancelled') - try: self.autoupdate_task.cancel() self.loop.run_until_complete(self.autoupdate_task) @@ -345,15 +332,8 @@ def main_color(self): async def on_connect(self): logger.info(LINE) - if not self.self_hosted: - logger.info(info('MODE: Using the Modmail API')) - logger.info(LINE) - await self.validate_api_token() - logger.info(LINE) - else: - logger.info(info('Mode: Self-hosting logs.')) - await self.validate_database_connection() - logger.info(LINE) + await self.validate_database_connection() + logger.info(LINE) logger.info(info('Connected to gateway.')) await self.config.refresh() @@ -806,32 +786,6 @@ def overwrites(ctx): ) return overwrites - async def validate_api_token(self): - try: - self.config.modmail_api_token - except KeyError: - logger.critical(error(f'MODMAIL_API_TOKEN not found.')) - logger.critical(error('Set a config variable called ' - 'MODMAIL_API_TOKEN with a token from ' - 'https://dashboard.modmail.tk.')) - logger.critical(error('If you want to self-host logs, ' - 'input a MONGO_URI config variable.')) - logger.critical(error('A Modmail API token is not needed ' - 'if you are self-hosting logs.')) - - return await self.logout() - else: - valid = await self.api.validate_token() - if not valid: - logger.critical(error('Invalid MODMAIL_API_TOKEN - get one ' - 'from https://dashboard.modmail.tk')) - return await self.logout() - - user = await self.api.get_user_info() - username = user['user']['username'] - logger.info(info('Validated token.')) - logger.info(info('GitHub user: ' + username)) - async def validate_database_connection(self): try: await self.db.command('buildinfo') @@ -843,32 +797,6 @@ async def validate_database_connection(self): else: logger.info(info('Successfully connected to the database.')) - async def data_loop(self): - await self.wait_until_ready() - self.owner = (await self.application_info()).owner - - while not self.is_closed(): - data = { - "owner_name": str(self.owner), - "owner_id": self.owner.id, - "bot_id": self.user.id, - "bot_name": str(self.user), - "avatar_url": self.user.avatar_url, - "guild_id": self.guild_id, - "guild_name": self.guild.name, - "member_count": len(self.guild.members), - "uptime": (datetime.utcnow() - - self.start_time).total_seconds(), - "latency": f'{self.ws.latency * 1000:.4f}', - "version": self.version, - # TODO: change to `self_hosted` - "selfhosted": self.self_hosted, - "last_updated": str(datetime.utcnow()) - } - - await self.api.post_metadata(data) - await asyncio.sleep(3600) - async def autoupdate_loop(self): await self.wait_until_ready() @@ -877,16 +805,19 @@ async def autoupdate_loop(self): logger.info(LINE) return - if self.self_hosted and not self.config.get('github_access_token'): + if not self.config.get('github_access_token'): logger.warning(info('GitHub access token not found.')) logger.warning(info('Autoupdates disabled.')) logger.info(LINE) return + logger.info(info('Autoupdate loop started.')) + while not self.is_closed(): - metadata = await self.api.get_metadata() + changelog = await Changelog.from_url(self) + latest = changelog.latest_version - if metadata['latest_version'] != self.version: + if parse_version(self.version) < parse_version(latest.version): data = await self.api.update_repository() embed = discord.Embed(color=discord.Color.green()) @@ -897,10 +828,8 @@ async def autoupdate_loop(self): icon_url=user['avatar_url'], url=user['url']) embed.set_footer(text=f"Updating Modmail v{self.version} " - f"-> v{metadata['latest_version']}") + f"-> v{latest.version}") - changelog = await Changelog.from_url(self) - latest = changelog.latest_version embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=value) diff --git a/cogs/modmail.py b/cogs/modmail.py index 443ade3c3d..66e7de242f 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -372,11 +372,7 @@ def format_log_embeds(self, logs, avatar_url): created_at = parser.parse(entry['created_at']) - log_url = ( - f"https://logs.modmail.tk/{key}" - if not self.bot.self_hosted else - self.bot.config.log_url.strip('/') + f'/logs/{key}' - ) + log_url = self.bot.config.log_url.strip('/') + f'/logs/{key}' username = entry['recipient']['name'] + '#' username += entry['recipient']['discriminator'] @@ -443,14 +439,6 @@ async def logs(self, ctx, *, member: User = None): @checks.has_permissions(manage_messages=True) async def closed_by(self, ctx, *, user: User = None): """Returns all logs closed by a user.""" - if not self.bot.self_hosted: - embed = discord.Embed( - color=discord.Color.red(), - description='This command only works if ' - 'you are self-hosting your logs.' - ) - return await ctx.send(embed=embed) - user = user or ctx.author query = { @@ -485,14 +473,6 @@ async def search(self, ctx, limit: Optional[int] = None, *, query): await ctx.trigger_typing() - if not self.bot.self_hosted: - embed = discord.Embed( - color=discord.Color.red(), - description='This command only works if you ' - 'are self-hosting your logs.' - ) - return await ctx.send(embed=embed) - query = { 'guild_id': str(self.bot.guild_id), 'open': False, diff --git a/cogs/utility.py b/cogs/utility.py index bc24a8cbf7..cd10310845 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -7,6 +7,7 @@ from difflib import get_close_matches from io import StringIO from json import JSONDecodeError +from pkg_resources import parse_version from textwrap import indent import discord @@ -203,34 +204,23 @@ async def about(self, ctx): desc += 'an organised manner.' embed.description = desc - url = 'https://api.modmail.tk/metadata' - async with self.bot.session.get(url) as resp: - try: - meta = await resp.json() - except (JSONDecodeError, ClientResponseError): - meta = None - embed.add_field(name='Uptime', value=self.bot.uptime) - if meta: - embed.add_field(name='Instances', value=meta['instances']) - else: - embed.add_field(name='Latency', - value=f'{self.bot.latency * 1000:.2f} ms') + embed.add_field(name='Latency', value=f'{self.bot.latency * 1000:.2f} ms') embed.add_field(name='Version', - value=f'[`{self.bot.version}`]' - '(https://modmail.tk/changelog)') + value=f'`{self.bot.version}`') embed.add_field(name='Author', value='[`kyb3r`](https://github.com/kyb3r)') footer = f'Bot ID: {self.bot.user.id}' - if meta: - if self.bot.version != meta['latest_version']: - footer = ("A newer version is available " - f"v{meta['latest_version']}") - else: - footer = 'You are up to date with the latest version.' + changelog = await Changelog.from_url(self.bot) + latest = changelog.latest_version + + if parse_version(self.bot.version) < parse_version(latest.version): + footer = f"A newer version is available v{latest.version}" + else: + footer = 'You are up to date with the latest version.' embed.add_field(name='GitHub', value='https://github.com/kyb3r/modmail', @@ -364,18 +354,19 @@ async def github(self, ctx): @trigger_typing async def update(self, ctx): """Updates the bot, this only works with heroku users.""" - metadata = await self.bot.api.get_metadata() + changelog = await Changelog.from_url(self.bot) + latest = changelog.latest_version desc = (f'The latest version is [`{self.bot.version}`]' '(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)') - embed = Embed( - title='Already up to date', - description=desc, - color=self.bot.main_color - ) + if parse_version(self.bot.version) >= parse_version(latest.version): + embed = Embed( + title='Already up to date', + description=desc, + color=self.bot.main_color + ) - if metadata['latest_version'] == self.bot.version: data = await self.bot.api.get_user_info() if not data.get('error'): user = data['user'] @@ -387,19 +378,17 @@ async def update(self, ctx): commit_data = data['data'] user = data['user'] - embed.title = None - embed.set_author(name=user['username'], - icon_url=user['avatar_url'], - url=user['url']) - embed.set_footer(text=f'Updating Modmail v{self.bot.version} ' - f"-> v{metadata['latest_version']}") if commit_data: + embed = Embed(color=self.bot.main_color) + + embed.set_footer(text=f'Updating Modmail v{self.bot.version} ' + f'-> v{latest.version}') + embed.set_author(name=user['username'] + ' - Updating bot', icon_url=user['avatar_url'], url=user['url']) - changelog = await Changelog.from_url(self.bot) - latest = changelog.latest_version + embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=value) @@ -409,8 +398,14 @@ async def update(self, ctx): embed.add_field(name='Merge Commit', value=f'[`{short_sha}`]({html_url})') else: - embed.description = ('Already up to date ' - 'with master repository.') + embed = Embed( + title='Already up to date with master repository.', + description='No further updates required', + color=self.bot.main_color + ) + embed.set_author(name=user['username'], + icon_url=user['avatar_url'], + url=user['url']) return await ctx.send(embed=embed) diff --git a/core/changelog.py b/core/changelog.py index 60e4e0186a..519040b975 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -36,14 +36,14 @@ class Version: def __init__(self, bot: Bot, version: str, lines: str): self.bot = bot - self.version = version + self.version = version.lstrip('vV') self.lines = [x for x in lines.splitlines() if x] self.fields = defaultdict(str) self.description = '' self.parse() def __repr__(self) -> str: - return f'Version({self.version}, description="{self.description}")' + return f'Version(v{self.version}, description="{self.description}")' def parse(self) -> None: """ @@ -66,9 +66,8 @@ def embed(self) -> Embed: """ embed = Embed(color=Color.green(), description=self.description) embed.set_author( - name=f'{self.version} - Changelog', + name=f'v{self.version} - Changelog', icon_url=self.bot.user.avatar_url, - url='https://modmail.tk/changelog' ) for name, value in self.fields.items(): @@ -151,8 +150,3 @@ async def from_url(cls, bot: Bot, url: str = '') -> 'Changelog': url = url or cls.CHANGELOG_URL resp = await bot.session.get(url) return cls(bot, await resp.text()) - - -if __name__ == '__main__': - with open('../CHANGELOG.md') as f: - print(Changelog(..., f.read()).latest_version) diff --git a/core/clients.py b/core/clients.py index 4142df39af..6bee795eee 100644 --- a/core/clients.py +++ b/core/clients.py @@ -251,125 +251,7 @@ async def login(cls, bot: Bot) -> 'GitHub': return self -class ModmailApiClient(UserClient, ApiClient): - BASE = 'https://api.modmail.tk' - METADATA = BASE + '/metadata' - GITHUB = BASE + '/github' - LOGS = BASE + '/logs' - CONFIG = BASE + '/config' - - def __init__(self, bot: Bot): - super().__init__(bot) - if self.token: - self.headers = { - 'Authorization': 'Bearer ' + self.token - } - - @property - def token(self): - return self.bot.config.get('modmail_api_token') - - async def validate_token(self): - resp = await self.request(self.BASE + '/token/verify', - return_response=True) - return resp.status == 200 - - async def post_metadata(self, data): - return await self.request(self.METADATA, method='POST', payload=data) - - async def get_user_info(self): - return await self.request(self.GITHUB + '/userinfo') - - async def update_repository(self): - return await self.request(self.GITHUB + '/update') - - async def get_metadata(self): - return await self.request(self.METADATA) - - async def get_user_logs(self, user_id): - return await self.request(self.LOGS + '/user/' + str(user_id)) - - async def get_log(self, channel_id): - return await self.request(self.LOGS + '/' + str(channel_id)) - - async def get_log_link(self, channel_id): - doc = await self.get_log(channel_id) - return f'https://logs.modmail.tk/{doc["key"]}' - - async def get_config(self): - return await self.request(self.CONFIG) - - async def update_config(self, data): - data = self.filter_valid(data) - return await self.request(self.CONFIG, method='PATCH', payload=data) - - async def create_log_entry(self, recipient, channel, creator): - return await self.request(self.LOGS + '/key', payload={ - 'channel_id': str(channel.id), - 'guild_id': str(self.bot.guild_id), - 'recipient': { - 'id': str(recipient.id), - 'name': recipient.name, - 'discriminator': recipient.discriminator, - 'avatar_url': recipient.avatar_url, - 'mod': False - }, - 'creator': { - 'id': str(creator.id), - 'name': creator.name, - 'discriminator': creator.discriminator, - 'avatar_url': creator.avatar_url, - 'mod': isinstance(creator, Member) - } - }) - - async def edit_message(self, message_id, new_content): - await self.request(self.LOGS + '/edit', method='PATCH', - payload={ - 'message_id': str(message_id), - 'new_content': new_content - }) - - async def append_log(self, message, channel_id='', type_='thread_message'): - channel_id = str(channel_id) or str(message.channel.id) - data = { - 'payload': { - 'timestamp': str(message.created_at), - 'message_id': str(message.id), - # author - 'author': { - 'id': str(message.author.id), - 'name': message.author.name, - 'discriminator': message.author.discriminator, - 'avatar_url': message.author.avatar_url, - 'mod': not isinstance(message.channel, DMChannel), - }, - # message properties - 'content': message.content, - 'type': type_, - 'attachments': [ - { - 'id': a.id, - 'filename': a.filename, - 'is_image': a.width is not None, - 'size': a.size, - 'url': a.url - } for a in message.attachments] - } - } - return await self.request(self.LOGS + f'/{channel_id}', - method='PATCH', - payload=data) - - async def post_log(self, channel_id, data): - return await self.request(self.LOGS + f'/{channel_id}', - method='POST', - payload=data) - - class SelfHostedClient(UserClient, ApiClient): - BASE = 'https://api.modmail.tk' - METADATA = BASE + '/metadata' def __init__(self, bot: Bot): super().__init__(bot) @@ -390,17 +272,6 @@ def db(self): def logs(self): return self.db.logs - async def validate_token(self): - resp = await self.request(self.BASE + '/token/verify', - return_response=True) - return resp.status == 200 - - async def get_metadata(self): - return await self.request(self.METADATA) - - async def post_metadata(self, data): - return await self.request(self.METADATA, method='POST', payload=data) - async def get_user_logs(self, user_id): query = { 'recipient.id': str(user_id), diff --git a/core/config.py b/core/config.py index 3474fbb5d4..eb738c8aa2 100644 --- a/core/config.py +++ b/core/config.py @@ -53,7 +53,7 @@ class ConfigManager(ConfigManagerABC): protected_keys = { # Modmail - 'modmail_api_token', 'modmail_guild_id', 'guild_id', 'owners', + 'modmail_guild_id', 'guild_id', 'owners', 'log_url', 'mongo_uri', # bot diff --git a/core/decorators.py b/core/decorators.py index 9540597c3d..4d9ef729e9 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -16,19 +16,12 @@ async def wrapper(self, ctx: commands.Context, *args, **kwargs): def github_access_token_required(func): @functools.wraps(func) async def wrapper(self, ctx: commands.Context, *args, **kwargs): - if (self.bot.self_hosted and - self.bot.config.get('github_access_token')) or \ - self.bot.config.get('modmail_api_token'): + if self.bot.config.get('github_access_token'): return await func(self, ctx, *args, **kwargs) - if not self.bot.self_hosted: - desc = ('You can only use this command if you have a ' - 'configured `MODMAIL_API_TOKEN`. Get your ' - 'token from https://dashboard.modmail.tk') - else: - desc = ('You can only use this command if you have a ' - 'configured `GITHUB_ACCESS_TOKEN`. Get a ' - 'personal access token from developer settings.') + desc = ('You can only use this command if you have a ' + 'configured `GITHUB_ACCESS_TOKEN`. Get a ' + 'personal access token from developer settings.') embed = Embed(color=Color.red(), title='Unauthorized', description=desc) diff --git a/core/models.py b/core/models.py index 96043267e4..05d3ef595a 100644 --- a/core/models.py +++ b/core/models.py @@ -41,11 +41,6 @@ def version(self) -> str: def db(self) -> typing.Optional[AsyncIOMotorClient]: raise NotImplementedError - @property - @abc.abstractmethod - def self_hosted(self) -> bool: - raise NotImplementedError - @property @abc.abstractmethod def config(self) -> 'ConfigManagerABC': @@ -168,10 +163,6 @@ async def update_repository(self) -> dict: async def get_user_logs(self, user_id: typing.Union[str, int]) -> list: raise NotImplementedError - @abc.abstractmethod - async def validate_token(self) -> bool: - raise NotImplementedError - @abc.abstractmethod async def get_log(self, channel_id: typing.Union[str, int]) -> dict: raise NotImplementedError @@ -208,14 +199,6 @@ async def post_log(self, data: dict) -> dict: raise NotImplementedError - @abc.abstractmethod - async def get_metadata(self) -> dict: - raise NotImplementedError - - @abc.abstractmethod - async def post_metadata(self, data: dict) -> dict: - raise NotImplementedError - @abc.abstractmethod async def edit_message(self, message_id: typing.Union[int, str], new_content: str) -> None: @@ -380,7 +363,8 @@ def __getitem__(self, item: str) -> 'ThreadABC': @abc.abstractmethod async def find(self, *, recipient: typing.Union[Member, User] = None, - channel: TextChannel = None) -> \ + channel: TextChannel = None, + recipient_id: int = None) -> \ typing.Optional['ThreadABC']: raise NotImplementedError diff --git a/core/thread.py b/core/thread.py index 1fa1614858..d68bd8a786 100644 --- a/core/thread.py +++ b/core/thread.py @@ -104,7 +104,7 @@ async def setup(self, *, creator=None, category=None): try: log_url, log_data = await asyncio.gather( self.bot.api.create_log_entry(recipient, channel, - creator or recipient), + creator or recipient), self.bot.api.get_user_logs(recipient.id) ) @@ -221,11 +221,7 @@ async def _close(self, closer, silent=False, delete_channel=True, }) if log_data is not None and isinstance(log_data, dict): - if self.bot.self_hosted: - log_url = f"{self.bot.config.log_url.strip('/')}/" \ - f"logs/{log_data['key']}" - else: - log_url = f"https://logs.modmail.tk/{log_data['key']}" + log_url = f"{self.bot.config.log_url.strip('/')}/logs/{log_data['key']}" if log_data['messages']: content = str(log_data['messages'][0]['content']) From 0dbc913de76daab96feaf150e08b9027d18296c0 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 10 May 2019 19:37:31 -0700 Subject: [PATCH 0020/1221] Update CHANGELOG.md Co-Authored-By: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc0edc729..8997fdde55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ The help message no longer conceal inaccessible commands due to check failures. A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer require manage messages permission to recall thread messages. -### Note +### Breaking When updating to this version, all prior permission settings with guild-based permissions will be invalidated. You will need to convert to the above system. `OWNERS` will also get removed, you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. From 2125dd2d0227bb42119ad236117e5d0ad71ceff6 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 14:29:13 +1000 Subject: [PATCH 0021/1221] Add metadata posting loop back in --- bot.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/bot.py b/bot.py index 5dd69fef6d..cc75c88598 100644 --- a/bot.py +++ b/bot.py @@ -109,6 +109,7 @@ def __init__(self): self._api = SelfHostedClient(self) self.plugin_db = PluginDatabaseClient(self) + self.metadata_task = self.loop.create_task(self.metadata_loop()) self.autoupdate_task = self.loop.create_task(self.autoupdate_loop()) self._load_extensions() self.owner = None @@ -203,6 +204,11 @@ def run(self, *args, **kwargs): except Exception: logger.critical(error('Fatal exception'), exc_info=True) finally: + try: + self.metadata_task.cancel() + self.loop.run_until_complete(self.metadata_task) + except asyncio.CancelledError: + logger.debug('data_task has been cancelled') try: self.autoupdate_task.cancel() self.loop.run_until_complete(self.autoupdate_task) @@ -847,6 +853,37 @@ async def autoupdate_loop(self): await asyncio.sleep(3600) + async def metadata_loop(self): + await self.wait_until_ready() + self.owner = (await self.application_info()).owner + + while not self.is_closed(): + data = { + "owner_name": str(self.owner), + "owner_id": self.owner.id, + "bot_id": self.user.id, + "bot_name": str(self.user), + "avatar_url": self.user.avatar_url, + "guild_id": self.guild_id, + "guild_name": self.guild.name, + "member_count": len(self.guild.members), + "uptime": (datetime.utcnow() - + self.start_time).total_seconds(), + "latency": f'{self.ws.latency * 1000:.4f}', + "version": self.version, + "selfhosted": True, + "last_updated": str(datetime.utcnow()) + } + + + try: + await self.session.post('https://api.modmail.tk/metadata', json=data) + logger.debug('Posted metadata') + except: + pass + + await asyncio.sleep(3600) + if __name__ == '__main__': if os.name != 'nt': From 1898bcbe2510ab6d9f7be3375de83c384b93c14b Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 15:02:44 +1000 Subject: [PATCH 0022/1221] Shorten docstring --- cogs/modmail.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 22c669eeb9..ee5735c1e6 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -793,9 +793,10 @@ async def unblock(self, ctx, *, user: User = None): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) async def delete(self, ctx, message_id: int = None): - """ + """Delete a message that was sent using the reply command. + Deletes the previous message, unless a message ID is provided, which in that case, - delete the message with that message ID. + deletes the message with that message ID. """ thread = ctx.thread From da9d16b2c382424991f87ee01a5c3105c78e36c8 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 15:03:03 +1000 Subject: [PATCH 0023/1221] Better formatting for perms in help cmd --- cogs/utility.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index c02ebdad90..ffa30f7ba3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -45,7 +45,7 @@ async def format_cog_help(self, ctx, cog): new_fmt = f'`{prefix + cmd.qualified_name}` ' perm_level = next(getattr(c, 'permission_level', None) for c in cmd.checks) if perm_level is not None: - new_fmt += f'[{perm_level}] ' + new_fmt = f'`[{perm_level}] {prefix + cmd.qualified_name}` ' new_fmt += f'- {cmd.short_doc}\n' if len(new_fmt) + len(fmts[-1]) >= 1024: @@ -80,12 +80,15 @@ async def format_command_help(self, cmd): prefix = self.bot.prefix perm_level = next(getattr(c, 'permission_level', None) for c in cmd.checks) - perm_level = f' [{perm_level}]' if perm_level is not None else '' + perm_level = f'**{perm_level.name}** [{perm_level}]' if perm_level is not None else '' + embed = Embed( - title=f'`{prefix}{cmd.signature}`{perm_level}', + title=f'`{prefix}{cmd.signature}`', color=self.bot.main_color, description=cmd.help ) + + embed.add_field(name='Permission level', value=perm_level) if not isinstance(cmd, commands.Group): return embed From 8e8287525bf84303611d44746c396e473c79bc6d Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 15:09:02 +1000 Subject: [PATCH 0024/1221] Improve formatting --- cogs/utility.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index ffa30f7ba3..e72201c97b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -80,7 +80,7 @@ async def format_command_help(self, cmd): prefix = self.bot.prefix perm_level = next(getattr(c, 'permission_level', None) for c in cmd.checks) - perm_level = f'**{perm_level.name}** [{perm_level}]' if perm_level is not None else '' + perm_level = f'{perm_level.name} [{perm_level}]' if perm_level is not None else '' embed = Embed( title=f'`{prefix}{cmd.signature}`', @@ -88,10 +88,13 @@ async def format_command_help(self, cmd): description=cmd.help ) - embed.add_field(name='Permission level', value=perm_level) + if not isinstance(cmd, commands.Group): + embed.set_footer(text=f'Permission level: {perm_level}') return embed + + embed.add_field(name='Permission level', value=perm_level) fmt = '' length = len(cmd.commands) From fba9662fee8fc6e168b8b291d08dd3fcf1e5ad34 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 15:21:09 +1000 Subject: [PATCH 0025/1221] Use main color for embeds --- cogs/utility.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index e72201c97b..9b207c0a43 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -87,8 +87,7 @@ async def format_command_help(self, cmd): color=self.bot.main_color, description=cmd.help ) - - + if not isinstance(cmd, commands.Group): embed.set_footer(text=f'Permission level: {perm_level}') @@ -1053,7 +1052,8 @@ def get_command(cmd): if not permissions: embed = Embed( title=f'Permission entries for command `{cmd.name}`:', - description='No permission entries found.' + description='No permission entries found.', + color=self.bot.main_color, ) else: values = [] @@ -1077,7 +1077,8 @@ def get_command(cmd): embed = Embed( title=f'Permission entries for command `{cmd.name}`:', - description=', '.join(values) + description=', '.join(values), + color=self.bot.main_color ) return embed @@ -1110,7 +1111,8 @@ def get_level(perm_level): embed = Embed( title='Permission entries for permission ' f'level `{perm_level.name}`:', - description='No permission entries found.' + description='No permission entries found.', + color=self.bot.main_color, ) else: values = [] @@ -1134,7 +1136,8 @@ def get_level(perm_level): embed = Embed( title=f'Permission entries for permission level `{perm_level.name}`:', - description=', '.join(values) + description=', '.join(values), + color=self.bot.main_color, ) return embed From 48571b70b624a829b1ef0cda2556a3654f709bb0 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 15:28:16 +1000 Subject: [PATCH 0026/1221] Update CHANGELOG.md --- CHANGELOG.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8997fdde55..6a0802064c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # v2.18.0 -### What's new? +### New Permissions System - A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. - There are five permission groups/levels: @@ -36,15 +36,19 @@ To view all roles and users with permission for a permission group or command do By default, all newly set up Modmail will have `OWNER` set to the owner of the bot, and `EGULAR` set to @everyone. -The help message no longer conceal inaccessible commands due to check failures. - -A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer require manage messages permission to recall thread messages. - ### Breaking When updating to this version, all prior permission settings with guild-based permissions will be invalidated. You will need to convert to the above system. `OWNERS` will also get removed, you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. +### New Command + +- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer require manage messages permission to recall thread messages. + +### Changed + +- The help message no longer conceals inaccessible commands due to check failures. + # v2.17.2 From 4d81de7499e64de061e07a7e3615c28cc7746541 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 16:00:53 +1000 Subject: [PATCH 0027/1221] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0802064c..d8bf95057f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Supporter [2] - Regular [1] +### Usage + You may add a role or user to a permission group through any of the following methods: - `?permissions add level owner @role` - `?permissions add level supporter member-name` From fd6dde56fe50ea46efa40410952c2313efeeb323 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 16:15:20 +1000 Subject: [PATCH 0028/1221] Add urls that point to the specified version in the changelog --- cogs/utility.py | 7 +++++-- core/changelog.py | 17 ++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 9b207c0a43..3e1fbd923a 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -175,8 +175,11 @@ async def help_(self, ctx, *, command: str = None): async def changelog(self, ctx): """Show a paginated changelog of the bot.""" changelog = await Changelog.from_url(self.bot) - paginator = PaginatorSession(ctx, *changelog.embeds) - await paginator.run() + try: + paginator = PaginatorSession(ctx, *changelog.embeds) + await paginator.run() + except: + await ctx.send(changelog.CHANGELOG_URL) @commands.command(aliases=['bot', 'info']) @checks.has_permissions(PermissionLevel.REGULAR) diff --git a/core/changelog.py b/core/changelog.py index 519040b975..1a884f3eaf 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -59,6 +59,10 @@ def parse(self) -> None: else: self.fields[curr_action] += line + '\n' + @property + def url(self) -> str: + return Changelog.CHANGELOG_URL + '#v' + self.version.replace('.', '') + @property def embed(self) -> Embed: """ @@ -68,6 +72,7 @@ def embed(self) -> Embed: embed.set_author( name=f'v{self.version} - Changelog', icon_url=self.bot.user.avatar_url, + url=self.url ) for name, value in self.fields.items(): @@ -99,14 +104,16 @@ class Changelog: Class Attributes ---------------- - CHANGELOG_URL : str + RAW_CHANGELOG_URL : str The URL to Modmail changelog. VERSION_REGEX : re.Pattern The regex used to parse the versions. """ + + - CHANGELOG_URL = ('https://raw.githubusercontent.com/' - 'kyb3r/modmail/master/CHANGELOG.md') + RAW_CHANGELOG_URL = 'https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md' + CHANGELOG_URL = 'https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md' VERSION_REGEX = re.compile(r'# (v\d+\.\d+\.\d+)([\S\s]*?(?=# v|$))') def __init__(self, bot: Bot, text: str): @@ -139,7 +146,7 @@ async def from_url(cls, bot: Bot, url: str = '') -> 'Changelog': bot : Bot The Modmail bot. url : str, optional - Defaults to `CHANGELOG_URL`. + Defaults to `RAW_CHANGELOG_URL`. The URL to the changelog. Returns @@ -147,6 +154,6 @@ async def from_url(cls, bot: Bot, url: str = '') -> 'Changelog': Changelog The newly created `Changelog` parsed from the `url`. """ - url = url or cls.CHANGELOG_URL + url = url or cls.RAW_CHANGELOG_URL resp = await bot.session.get(url) return cls(bot, await resp.text()) From 6ebfffece7462c0f66ceda6aee8e03fb1488ba39 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 16:58:46 +1000 Subject: [PATCH 0029/1221] Support team accounts by adding OWNERS config var --- CHANGELOG.md | 3 +++ bot.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8bf95057f..ef3a3fcd95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.18.1 + +Un-deprecated the `OWNERS` config variable to support discord developer team accounts. # v2.18.0 diff --git a/bot.py b/bot.py index 5c5535c9f7..726cf2adaa 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.18.0' +__version__ = '2.18.1' import asyncio import logging @@ -223,6 +223,11 @@ def run(self, *args, **kwargs): self.loop.close() logger.info(error(' - Shutting down bot - ')) + async def is_owner(self, user): + raw = str(self.config.get('owners', '0')).split(',') + allowed = {int(x) for x in raw} + return await super().is_owner(user) or user.id in allowed + @property def log_channel(self): channel_id = self.config.get('log_channel_id') From 99379d072275180202c371ed29d73bcdc263d46f Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 17:13:17 +1000 Subject: [PATCH 0030/1221] Sort commands by permission level in help. --- cogs/utility.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 3e1fbd923a..1276e6abc7 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -38,12 +38,15 @@ async def format_cog_help(self, ctx, cog): prefix = self.bot.prefix + def perms_required(cmd): + return next(getattr(c, 'permission_level', None) for c in cmd.checks) + fmts = [''] for cmd in sorted(self.bot.commands, - key=lambda cmd: cmd.qualified_name): + key=lambda cmd: perms_required(cmd)): if cmd.instance is cog and not cmd.hidden: new_fmt = f'`{prefix + cmd.qualified_name}` ' - perm_level = next(getattr(c, 'permission_level', None) for c in cmd.checks) + perm_level = perms_required(cmd) if perm_level is not None: new_fmt = f'`[{perm_level}] {prefix + cmd.qualified_name}` ' @@ -64,12 +67,16 @@ async def format_cog_help(self, ctx, cog): ) embed.add_field(name='Commands', value=fmt) - embed.set_author(name=cog.__class__.__name__ + ' - Help', + + continued = ' (Continued)' if len(embeds) > 0 else '' + + embed.set_author(name=cog.__class__.__name__ + ' - Help' + continued, icon_url=ctx.bot.user.avatar_url) embed.set_footer(text=f'Type "{prefix}help command" ' 'for more info on a command.') embeds.append(embed) + return embeds async def format_command_help(self, cmd): From 1995349a27fc08785e638342f8ccb7fa13e66647 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 17:17:26 +1000 Subject: [PATCH 0031/1221] Add perm level 0 instead of NoneType --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 1276e6abc7..bbbc3d5247 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -39,7 +39,7 @@ async def format_cog_help(self, ctx, cog): prefix = self.bot.prefix def perms_required(cmd): - return next(getattr(c, 'permission_level', None) for c in cmd.checks) + return next(getattr(c, 'permission_level', 0) for c in cmd.checks) fmts = [''] for cmd in sorted(self.bot.commands, From ce91cb93f678fbf8fc5bad686b3a3a29e951532d Mon Sep 17 00:00:00 2001 From: Kyber Date: Sat, 11 May 2019 18:04:55 +1000 Subject: [PATCH 0032/1221] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3a3fcd95..b0ba3259ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ You may add a role or user to a permission group through any of the following me - `?permissions add level administrator 78912384930291853` The same applies to individual commands permissions: -- `?permissions add command-name @member#1234` +- `?permissions add command command-name @member#1234` - ... and the other methods listed above. To revoke permission, use `remove` instead of `add`. From 4b7b1f917531221eb111da0f4e21b2d2abf527fb Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 19:05:45 +1000 Subject: [PATCH 0033/1221] Better error messages --- bot.py | 30 +++++++++++++++++++++++++----- cogs/modmail.py | 11 ++++++----- cogs/utility.py | 12 ++++++------ 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/bot.py b/bot.py index 726cf2adaa..ad092d3742 100644 --- a/bot.py +++ b/bot.py @@ -784,11 +784,31 @@ async def on_error(self, event_method, *args, **kwargs): logger.error(error('Ignoring exception in {}'.format(event_method))) logger.error(error('Unexpected exception:'), exc_info=sys.exc_info()) - async def on_command_error(self, context, exception): - if isinstance(exception, (commands.MissingRequiredArgument, - commands.UserInputError)): - await context.invoke(self.get_command('help'), - command=str(context.command)) + async def on_command_error(self, ctx, exception): + + def human_join(strings): + if len(strings) <= 2: + return ' or '.join(strings) + else: + return ', '.join(strings[:len(strings)-1]) + ' or ' + strings[-1] + + if isinstance(exception, commands.BadUnionArgument): + msg = 'Could not find the specified ' + human_join([c.__name__ for c in exception.converters]) + await ctx.trigger_typing() + await ctx.send(embed=discord.Embed( + color=discord.Color.red(), + description=msg + )) + + elif isinstance(exception, commands.BadArgument): + await ctx.trigger_typing() + await ctx.send(embed=discord.Embed( + color=discord.Color.red(), + description=str(exception) + )) + elif isinstance(exception, commands.MissingRequiredArgument): + await ctx.invoke(self.get_command('help'), + command=str(ctx.command)) elif isinstance(exception, commands.CommandNotFound): logger.warning(error('CommandNotFound: ' + str(exception))) elif isinstance(exception, commands.CheckFailure): diff --git a/cogs/modmail.py b/cogs/modmail.py index ee5735c1e6..7add32cdab 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -2,6 +2,7 @@ import re from datetime import datetime from typing import Optional, Union +from types import SimpleNamespace as p import discord from discord.ext import commands @@ -420,7 +421,7 @@ async def logs(self, ctx, *, member: User = None): if not member: thread = ctx.thread if not thread: - raise commands.UserInputError + raise commands.MissingRequiredArgument(p(name=member)) user = thread.recipient else: user = member @@ -690,14 +691,14 @@ async def block(self, ctx, user: Optional[User] = None, *, if thread: user = thread.recipient else: - raise commands.UserInputError + raise commands.MissingRequiredArgument(p(name='user')) if after is not None: reason = after.arg if reason.startswith('System Message: '): - raise commands.UserInputError + raise commands.BadArgument('The reason cannot start with `System Message:`.') if re.search(r'%(.+?)%$', reason) is not None: - raise commands.UserInputError + raise commands.MissingRequiredArgument(p(name='reason')) if after.dt > after.now: reason = f'{reason} %{after.dt.isoformat()}%' @@ -754,7 +755,7 @@ async def unblock(self, ctx, *, user: User = None): if thread: user = thread.recipient else: - raise commands.UserInputError + raise commands.MissingRequiredArgument(p(name='user')) mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`' diff --git a/cogs/utility.py b/cogs/utility.py index bbbc3d5247..78cf2eb6fa 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -130,7 +130,7 @@ async def format_not_found(self, ctx, command): choices = set() - for name, c in self.bot.all_commands: + for name, c in self.bot.all_commands.items(): if not c.hidden: choices.add(name) @@ -909,7 +909,7 @@ async def add_perms_command(self, ctx, command: str, *, user_or_role: Union[User elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.UserInputError('Invalid user or role.') + raise commands.BadArgument('Could not find the specified User or Role.') await self.bot.update_perms(self.bot.all_commands[command].name, value) embed = Embed( @@ -937,7 +937,7 @@ async def add_perms_level(self, ctx, level: str, *, user_or_role: Union[User, Ro elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.UserInputError('Invalid user or role.') + raise commands.BadArgument('Could not find the specified User or Role.') await self.bot.update_perms(PermissionLevel[level.upper()], value) embed = Embed( @@ -973,7 +973,7 @@ async def remove_perms_command(self, ctx, command: str, *, user_or_role: Union[U elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.UserInputError('Invalid user or role.') + raise commands.BadArgument('Could not find the specified User or Role.') await self.bot.update_perms(self.bot.all_commands[command].name, value, add=False) embed = Embed( @@ -1001,7 +1001,7 @@ async def remove_perms_level(self, ctx, level: str, *, user_or_role: Union[User, elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.UserInputError('Invalid user or role.') + raise commands.BadArgument('Could not find the specified User or Role.') await self.bot.update_perms(PermissionLevel[level.upper()], value, add=False) embed = Embed( @@ -1021,7 +1021,7 @@ async def get_perms(self, ctx, *, user_or_role: Union[User, Role, str]): elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.UserInputError('Invalid user or role.') + raise commands.BadArgument('Could not find the specified User or Role.') cmds = [] levels = [] From 9131af482d1c3b34878a8e143b8124607050efee Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 19:38:43 +1000 Subject: [PATCH 0034/1221] More explicit error responses --- cogs/utility.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 78cf2eb6fa..f844b80b0d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -909,7 +909,7 @@ async def add_perms_command(self, ctx, command: str, *, user_or_role: Union[User elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.BadArgument('Could not find the specified User or Role.') + raise commands.BadArgument(f'User or Role "{user_or_role}" not found') await self.bot.update_perms(self.bot.all_commands[command].name, value) embed = Embed( @@ -937,7 +937,7 @@ async def add_perms_level(self, ctx, level: str, *, user_or_role: Union[User, Ro elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.BadArgument('Could not find the specified User or Role.') + raise commands.BadArgument(f'User or Role "{user_or_role}" not found') await self.bot.update_perms(PermissionLevel[level.upper()], value) embed = Embed( @@ -973,7 +973,7 @@ async def remove_perms_command(self, ctx, command: str, *, user_or_role: Union[U elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.BadArgument('Could not find the specified User or Role.') + raise commands.BadArgument(f'User or Role "{user_or_role}" not found') await self.bot.update_perms(self.bot.all_commands[command].name, value, add=False) embed = Embed( @@ -1001,7 +1001,7 @@ async def remove_perms_level(self, ctx, level: str, *, user_or_role: Union[User, elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.BadArgument('Could not find the specified User or Role.') + raise commands.BadArgument(f'User or Role "{user_or_role}" not found') await self.bot.update_perms(PermissionLevel[level.upper()], value, add=False) embed = Embed( @@ -1021,7 +1021,7 @@ async def get_perms(self, ctx, *, user_or_role: Union[User, Role, str]): elif user_or_role in {'everyone', 'all'}: value = -1 else: - raise commands.BadArgument('Could not find the specified User or Role.') + raise commands.BadArgument(f'User or Role "{user_or_role}" not found') cmds = [] levels = [] From 9d82f88461b25ab2d1e77f440773b49c23503a0c Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 19:39:05 +1000 Subject: [PATCH 0035/1221] Better response messages --- cogs/modmail.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7add32cdab..f5357b5f5d 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -2,7 +2,7 @@ import re from datetime import datetime from typing import Optional, Union -from types import SimpleNamespace as p +from types import SimpleNamespace as param import discord from discord.ext import commands @@ -421,7 +421,7 @@ async def logs(self, ctx, *, member: User = None): if not member: thread = ctx.thread if not thread: - raise commands.MissingRequiredArgument(p(name=member)) + raise commands.MissingRequiredArgument(param(name='member')) user = thread.recipient else: user = member @@ -690,15 +690,17 @@ async def block(self, ctx, user: Optional[User] = None, *, thread = ctx.thread if thread: user = thread.recipient + elif after is None: + raise commands.MissingRequiredArgument(param(name='user')) else: - raise commands.MissingRequiredArgument(p(name='user')) + raise commands.BadArgument(f'User "{after}" not found') if after is not None: reason = after.arg if reason.startswith('System Message: '): raise commands.BadArgument('The reason cannot start with `System Message:`.') if re.search(r'%(.+?)%$', reason) is not None: - raise commands.MissingRequiredArgument(p(name='reason')) + raise commands.MissingRequiredArgument(param(name='reason')) if after.dt > after.now: reason = f'{reason} %{after.dt.isoformat()}%' @@ -750,12 +752,13 @@ async def unblock(self, ctx, *, user: User = None): use only. """ + if user is None: thread = ctx.thread if thread: user = thread.recipient else: - raise commands.MissingRequiredArgument(p(name='user')) + raise commands.MissingRequiredArgument(param(name='user')) mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`' @@ -793,7 +796,8 @@ async def unblock(self, ctx, *, user: User = None): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) - async def delete(self, ctx, message_id: int = None): + @checks.thread_only() + async def delete(self, ctx, message_id = None): """Delete a message that was sent using the reply command. Deletes the previous message, unless a message ID is provided, which in that case, @@ -801,6 +805,11 @@ async def delete(self, ctx, message_id: int = None): """ thread = ctx.thread + try: + message_id = int(message_id) + except ValueError: + raise commands.BadArgument('An integer message ID needs to be specified.') + linked_message_id = await self.find_linked_message(ctx, message_id) if linked_message_id is None: From 3730bdb3dfb4ac273603405940fa29f5495430cc Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 19:40:47 +1000 Subject: [PATCH 0036/1221] After.arg instead of userfriendly time --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index f5357b5f5d..55a87d31a5 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -693,7 +693,7 @@ async def block(self, ctx, user: Optional[User] = None, *, elif after is None: raise commands.MissingRequiredArgument(param(name='user')) else: - raise commands.BadArgument(f'User "{after}" not found') + raise commands.BadArgument(f'User "{after.arg}" not found') if after is not None: reason = after.arg From 0643c98b5b6198087187ce4bb713b5debefed649 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 11 May 2019 19:44:31 +1000 Subject: [PATCH 0037/1221] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ba3259ef..9382850ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.18.2 + +### Changed + +Commands now have better error messages, instead of just sending the help message for a command when an argument fails to be converted to its specified object, the bot now says things like "User 'bob' not found" instead. + # v2.18.1 Un-deprecated the `OWNERS` config variable to support discord developer team accounts. From edfe6bc5eec26cc3f7c591d874e125a2499b75a5 Mon Sep 17 00:00:00 2001 From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com> Date: Sat, 11 May 2019 17:35:51 +0600 Subject: [PATCH 0038/1221] Bump version to 2.18.2 (#228) --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index ad092d3742..0caedb5cd2 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.18.1' +__version__ = '2.18.2' import asyncio import logging From c982106ddd120fcd063c5d28503303c069b36d8b Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 00:01:47 +1000 Subject: [PATCH 0039/1221] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9382850ec8..ad4427924e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ To view all roles and users with permission for a permission group or command do - `?permissions get command command-name` - `?permissions get level owner` -By default, all newly set up Modmail will have `OWNER` set to the owner of the bot, and `EGULAR` set to @everyone. +By default, all newly set up Modmail will have `OWNER` set to the owner of the bot, and `REGULAR` set to @everyone. ### Breaking From c948beb2b7e5223339502efe0a9078b52f955ceb Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 12 May 2019 02:11:56 +1000 Subject: [PATCH 0040/1221] Add support to change log url prefix --- cogs/modmail.py | 7 ++++++- core/clients.py | 8 ++++++-- core/thread.py | 5 ++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 55a87d31a5..b4addea777 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,5 +1,6 @@ import asyncio import re +import os from datetime import datetime from typing import Optional, Union from types import SimpleNamespace as param @@ -381,7 +382,11 @@ def format_log_embeds(self, logs, avatar_url): created_at = parser.parse(entry['created_at']) - log_url = self.bot.config.log_url.strip('/') + f'/logs/{key}' + prefix = os.getenv('LOG_URL_PREFIX', '/logs') + prefix = '' + + + log_url = self.bot.config.log_url.strip('/') + f'{prefix}/{key}' username = entry['recipient']['name'] + '#' username += entry['recipient']['discriminator'] diff --git a/core/clients.py b/core/clients.py index 6bee795eee..f1290e541f 100644 --- a/core/clients.py +++ b/core/clients.py @@ -1,3 +1,4 @@ +import os import logging import secrets from datetime import datetime @@ -14,6 +15,9 @@ logger = logging.getLogger('Modmail') +prefix = os.getenv('LOG_URL_PREFIX', '/logs') +if prefix == 'NONE': + prefix = '' class ApiClient: """ @@ -288,7 +292,7 @@ async def get_log(self, channel_id): async def get_log_link(self, channel_id): doc = await self.get_log(channel_id) - return f"{self.bot.config.log_url.strip('/')}/logs/{doc['key']}" + return f"{self.bot.config.log_url.strip('/')}{prefix}/{doc['key']}" async def create_log_entry(self, recipient, channel, creator): key = secrets.token_hex(6) @@ -318,7 +322,7 @@ async def create_log_entry(self, recipient, channel, creator): 'messages': [] }) - return f"{self.bot.config.log_url.strip('/')}/logs/{key}" + return f"{self.bot.config.log_url.strip('/')}{prefix}/{key}" async def get_config(self): conf = await self.db.config.find_one({'bot_id': self.bot.user.id}) diff --git a/core/thread.py b/core/thread.py index 974b698a1e..e42b163cc2 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1,6 +1,7 @@ import asyncio import logging import re +import os import string import typing from datetime import datetime, timedelta @@ -221,7 +222,9 @@ async def _close(self, closer, silent=False, delete_channel=True, }) if log_data is not None and isinstance(log_data, dict): - log_url = f"{self.bot.config.log_url.strip('/')}/logs/{log_data['key']}" + prefix = os.getenv('LOG_URL_PREFIX', '/logs') + if prefix == 'NONE': prefix = '' + log_url = f"{self.bot.config.log_url.strip('/')}{prefix}/{log_data['key']}" if log_data['messages']: content = str(log_data['messages'][0]['content']) From 7483d1403f3da2b1525fa1ccb8e72441fbde345a Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 12 May 2019 02:12:53 +1000 Subject: [PATCH 0041/1221] Update modmail.py --- cogs/modmail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/modmail.py b/cogs/modmail.py index b4addea777..11361c571c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -383,6 +383,7 @@ def format_log_embeds(self, logs, avatar_url): created_at = parser.parse(entry['created_at']) prefix = os.getenv('LOG_URL_PREFIX', '/logs') + if prefix == 'NONE': prefix = '' From 406e859d713085af951c884e1da145cc3fbe6273 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 12 May 2019 02:17:50 +1000 Subject: [PATCH 0042/1221] Attempt to fix teams issue --- bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 0caedb5cd2..4a85cd1efa 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.18.2' +__version__ = '2.18.3' import asyncio import logging @@ -226,7 +226,7 @@ def run(self, *args, **kwargs): async def is_owner(self, user): raw = str(self.config.get('owners', '0')).split(',') allowed = {int(x) for x in raw} - return await super().is_owner(user) or user.id in allowed + return (user.id in allowed) or await super().is_owner(user) @property def log_channel(self): From a4cd2c571de6edbf220ef925180b85f3b6965f7f Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 12 May 2019 02:25:32 +1000 Subject: [PATCH 0043/1221] Attempt to fix teams bug --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad4427924e..00b4bed2f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v.18.3 + +Attempt to fix teams bug. + # v2.18.2 ### Changed From 2c8d49ba4af072a2198ff905255c605511c0677f Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 12 May 2019 02:32:19 +1000 Subject: [PATCH 0044/1221] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00b4bed2f9..9b5ad7ae16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# v.18.3 +# v2.18.3 Attempt to fix teams bug. From e8a2d8965cb0b35c1ed1eac4b7da136741dcbebf Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 12 May 2019 02:57:30 +1000 Subject: [PATCH 0045/1221] Add owners config key again --- core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.py b/core/config.py index 7c6f8a8e76..509771a751 100644 --- a/core/config.py +++ b/core/config.py @@ -54,7 +54,7 @@ class ConfigManager(ConfigManagerABC): protected_keys = { # Modmail 'modmail_guild_id', 'guild_id', - 'log_url', 'mongo_uri', + 'log_url', 'mongo_uri', 'owners' # bot 'token', From c4c2fb0acd0d4a2943ddf7cc9e83bb1f9b2fcb22 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 12 May 2019 02:58:15 +1000 Subject: [PATCH 0046/1221] Increment version --- bot.py | 2 +- core/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 4a85cd1efa..8463fd646a 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.18.3' +__version__ = '2.18.4' import asyncio import logging diff --git a/core/config.py b/core/config.py index 509771a751..8e1224a76c 100644 --- a/core/config.py +++ b/core/config.py @@ -54,7 +54,7 @@ class ConfigManager(ConfigManagerABC): protected_keys = { # Modmail 'modmail_guild_id', 'guild_id', - 'log_url', 'mongo_uri', 'owners' + 'log_url', 'mongo_uri', 'owners', # bot 'token', From d6eb0e7eb862d0ed4f44f8833e75f89d876e856d Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 12 May 2019 02:59:32 +1000 Subject: [PATCH 0047/1221] Fix team permission bug --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b5ad7ae16..62e2a2e3cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# v2.18.3 +# v2.18.4 -Attempt to fix teams bug. +Fix the teams permission bug. # v2.18.2 From 20cbc0863db1591c8d2be5d91cfb27f8af8c1b83 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 12 May 2019 03:18:13 +1000 Subject: [PATCH 0048/1221] Fix external plugin crashing help cmd bug --- cogs/utility.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index f844b80b0d..97e9770689 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -39,7 +39,9 @@ async def format_cog_help(self, ctx, cog): prefix = self.bot.prefix def perms_required(cmd): - return next(getattr(c, 'permission_level', 0) for c in cmd.checks) + for c in cmd.checks: + return getattr(c, 'permission_level', 0) + return 0 fmts = [''] for cmd in sorted(self.bot.commands, From 8a6ab07024554925cfd08ba662862b93a9e4590f Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 12 May 2019 17:45:31 +1000 Subject: [PATCH 0049/1221] Fix help command for external plugins --- CHANGELOG.md | 4 ++++ bot.py | 2 +- cogs/utility.py | 13 ++++--------- core/models.py | 1 + core/utils.py | 10 ++++++++++ 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62e2a2e3cf..7d3eafac75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.18.5 + +Fix help command bug when using external plugins. + # v2.18.4 Fix the teams permission bug. diff --git a/bot.py b/bot.py index 8463fd646a..70f4106165 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.18.4' +__version__ = '2.18.5' import asyncio import logging diff --git a/cogs/utility.py b/cogs/utility.py index 97e9770689..b775579e14 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -22,7 +22,7 @@ from core.decorators import github_access_token_required, trigger_typing from core.models import Bot, InvalidConfigError, PermissionLevel from core.paginator import PaginatorSession, MessagePaginatorSession -from core.utils import cleanup_code, info, error, User +from core.utils import cleanup_code, info, error, User, perms_level logger = logging.getLogger('Modmail') @@ -38,17 +38,12 @@ async def format_cog_help(self, ctx, cog): prefix = self.bot.prefix - def perms_required(cmd): - for c in cmd.checks: - return getattr(c, 'permission_level', 0) - return 0 - fmts = [''] for cmd in sorted(self.bot.commands, - key=lambda cmd: perms_required(cmd)): + key=lambda cmd: perms_level(cmd)): if cmd.instance is cog and not cmd.hidden: new_fmt = f'`{prefix + cmd.qualified_name}` ' - perm_level = perms_required(cmd) + perm_level = perms_level(cmd) if perm_level is not None: new_fmt = f'`[{perm_level}] {prefix + cmd.qualified_name}` ' @@ -88,7 +83,7 @@ async def format_command_help(self, cmd): prefix = self.bot.prefix - perm_level = next(getattr(c, 'permission_level', None) for c in cmd.checks) + perm_level = perms_level(cmd) perm_level = f'{perm_level.name} [{perm_level}]' if perm_level is not None else '' embed = Embed( diff --git a/core/models.py b/core/models.py index 4d0a7c905b..6dca430d89 100644 --- a/core/models.py +++ b/core/models.py @@ -20,6 +20,7 @@ class PermissionLevel(IntEnum): MOD = 3 SUPPORTER = 2 REGULAR = 1 + NONE = 0 class Bot(abc.ABC, commands.Bot): diff --git a/core/utils.py b/core/utils.py index 1b6438145a..ede86db6d1 100644 --- a/core/utils.py +++ b/core/utils.py @@ -6,6 +6,7 @@ from discord.ext import commands from colorama import Fore, Style +from core.models import PermissionLevel def info(*msgs): @@ -186,6 +187,15 @@ def match_user_id(text: str) -> int: return int(match.group(1)) return -1 +def perms_level(cmd): + for c in cmd.checks: + perm = getattr(c, 'permission_level', None) + if perm is not None: + return perm + for c in cmd.checks: + if 'is_owner' in str(c): + return PermissionLevel.OWNER + return PermissionLevel.NONE async def ignore(coro): try: From 6cac86a9983a02e9021d4cbabba8a24b9564a59b Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 19:24:08 +1000 Subject: [PATCH 0050/1221] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d8bcc7273c..d76a8e4a3b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ - + + Bot instances + Support @@ -22,7 +24,6 @@ ---- ## How Does Modmail Work? @@ -36,7 +37,6 @@ Currently, the easiest and fastest way to set up the bot is by using Heroku, whi Interactive installation: [**https://taaku18.github.io/modmail/installation**](https://taaku18.github.io/modmail/installation) ---- # Notable Features From ca33e3c8941eb2f008a05a83e234a6043c3ee238 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 19:26:26 +1000 Subject: [PATCH 0051/1221] Update README.md --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index d76a8e4a3b..0264cf9903 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,7 @@ Interactive installation: [**https://taaku18.github.io/modmail/installation**](h ## Customizability -Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. Snippets are shortcuts for predefined messages that you can send. Add or remove snippets with the `?snippets` command. The level of customization is ever growing thanks to our exceptional contributors. - -## Linked Messages - - - -Have you sent something with the `?reply` command by accident? Don't fret, you can delete your original message, and the bot will automatically delete the corresponding message sent to the recipient of the thread! You can also use the `?edit` command to edit a message you sent. +Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. The level of customization is ever growing thanks to our exceptional contributors. ## Thread Logs From 589de9a9aacf8d6554beed8a385a0360782e6367 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 19:28:25 +1000 Subject: [PATCH 0052/1221] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0264cf9903..46a28c3f30 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ ## How Does Modmail Work? - + When a user sends a direct message to the bot, a channel is created within an isolated category. This channel is where messages will be relayed. To reply to the message, simply use the command `?reply` in the channel. A full list of commands can be found in the [wiki](https://github.com/kyb3r/modmail/wiki) or by using the `?help` command. @@ -40,6 +40,8 @@ Interactive installation: [**https://taaku18.github.io/modmail/installation**](h # Notable Features + + ## Customizability Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. The level of customization is ever growing thanks to our exceptional contributors. From bd725abb08c36ebaea415155270a91ff6f94590d Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 19:29:49 +1000 Subject: [PATCH 0053/1221] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 46a28c3f30..2088451dfb 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ ## How Does Modmail Work? - + When a user sends a direct message to the bot, a channel is created within an isolated category. This channel is where messages will be relayed. To reply to the message, simply use the command `?reply` in the channel. A full list of commands can be found in the [wiki](https://github.com/kyb3r/modmail/wiki) or by using the `?help` command. @@ -40,12 +40,12 @@ Interactive installation: [**https://taaku18.github.io/modmail/installation**](h # Notable Features - - ## Customizability Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. The level of customization is ever growing thanks to our exceptional contributors. + + ## Thread Logs Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://modmail-logs.herokuapp.com/logs/02032d65a6f3. From 0de4d3a09f444219c661830f08c44faf79430e29 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 19:30:26 +1000 Subject: [PATCH 0054/1221] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2088451dfb..1247edb376 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ ## How Does Modmail Work? - + When a user sends a direct message to the bot, a channel is created within an isolated category. This channel is where messages will be relayed. To reply to the message, simply use the command `?reply` in the channel. A full list of commands can be found in the [wiki](https://github.com/kyb3r/modmail/wiki) or by using the `?help` command. @@ -44,7 +44,7 @@ Interactive installation: [**https://taaku18.github.io/modmail/installation**](h Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. The level of customization is ever growing thanks to our exceptional contributors. - + ## Thread Logs From 6128b5d444f23689822d2d08ee1b4af72f3e3205 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 19:31:16 +1000 Subject: [PATCH 0055/1221] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1247edb376..86fa1facef 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ ## How Does Modmail Work? - + When a user sends a direct message to the bot, a channel is created within an isolated category. This channel is where messages will be relayed. To reply to the message, simply use the command `?reply` in the channel. A full list of commands can be found in the [wiki](https://github.com/kyb3r/modmail/wiki) or by using the `?help` command. @@ -44,7 +44,7 @@ Interactive installation: [**https://taaku18.github.io/modmail/installation**](h Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. The level of customization is ever growing thanks to our exceptional contributors. - + ## Thread Logs From a4c40c7c54425deb88a9c4fae638b826cc0b7777 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 19:32:42 +1000 Subject: [PATCH 0056/1221] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 86fa1facef..1ea3175a09 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,11 @@ Interactive installation: [**https://taaku18.github.io/modmail/installation**](h ## Customizability + + Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. The level of customization is ever growing thanks to our exceptional contributors. - + ## Thread Logs From 2bfa71a86efe31ad6d82be70dd8d6b80f30bd077 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 19:33:20 +1000 Subject: [PATCH 0057/1221] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ea3175a09..b54cd07716 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,14 @@ Interactive installation: [**https://taaku18.github.io/modmail/installation**](h ## Customizability - - Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. The level of customization is ever growing thanks to our exceptional contributors. ## Thread Logs + + Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://modmail-logs.herokuapp.com/logs/02032d65a6f3. # Contributing From cd0f2726e52beeb111e9b709d42f8cfdcd13daa4 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 19:33:44 +1000 Subject: [PATCH 0058/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b54cd07716..bdd4f3e712 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Modmail has a range of configuration variables that you can dynamically alter wi ## Thread Logs - + Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://modmail-logs.herokuapp.com/logs/02032d65a6f3. From 32e3e5ca37e32a93e74547ff9f8987248da44305 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 19:34:15 +1000 Subject: [PATCH 0059/1221] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index bdd4f3e712..0264cf9903 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,8 @@ Interactive installation: [**https://taaku18.github.io/modmail/installation**](h Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. The level of customization is ever growing thanks to our exceptional contributors. - - ## Thread Logs - - Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://modmail-logs.herokuapp.com/logs/02032d65a6f3. # Contributing From 262f52101d6c98ca360bb6575272ceb8bd4218ad Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 20:47:25 +1000 Subject: [PATCH 0060/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0264cf9903..d6f4d4b6f4 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Modmail has a range of configuration variables that you can dynamically alter wi ## Thread Logs -Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://modmail-logs.herokuapp.com/logs/02032d65a6f3. +Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://logs.modmail.tk/example. # Contributing From 5084f933d3acc223181c3976d835d07eb54ff955 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 12 May 2019 20:48:15 +1000 Subject: [PATCH 0061/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6f4d4b6f4..c74d559458 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Modmail has a range of configuration variables that you can dynamically alter wi ## Thread Logs -Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. An example of a logged conversation: https://logs.modmail.tk/example. +Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. You have the ability to query and search through logs using the bot via commands. An example of a logged conversation: https://logs.modmail.tk/example. # Contributing From 7cfcd642e818693cf9e1f362f70eae4103819bb3 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Sun, 12 May 2019 05:42:47 -0700 Subject: [PATCH 0062/1221] [v2.19.0] Added guild_age config (#226) * Added guild_age config * Updated changelog * Update CHANGELOG.md * Increment version --- CHANGELOG.md | 9 ++++++++- bot.py | 55 ++++++++++++++++++++++++++++++++++++++++++++------ core/config.py | 5 +++-- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d3eafac75..6ebbdbd697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +# v2.19.0 + +### What's new? + +- New config variable `guild_age`, similar to `account_age`, `guild_age` sets a limit as to how long a user has to wait after they joined the server to message Modmail. +- `guild_age` can be set the same way as `account_age`. + # v2.18.5 Fix help command bug when using external plugins. @@ -68,7 +76,6 @@ When updating to this version, all prior permission settings with guild-based pe - The help message no longer conceals inaccessible commands due to check failures. - # v2.17.2 ### Changed diff --git a/bot.py b/bot.py index 70f4106165..7643ad8771 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.18.5' +__version__ = '2.19.0' import asyncio import logging @@ -467,8 +467,10 @@ async def retrieve_emoji(self): async def process_modmail(self, message): """Processes messages sent to the bot.""" sent_emoji, blocked_emoji = await self.retrieve_emoji() + now = datetime.utcnow() account_age = self.config.get('account_age') + guild_age = self.config.get('guild_age') if account_age is None: account_age = isodate.duration.Duration() else: @@ -483,19 +485,41 @@ async def process_modmail(self, message): await self.config.update() account_age = isodate.duration.Duration() + if guild_age is None: + guild_age = isodate.duration.Duration() + else: + try: + guild_age = isodate.parse_duration(guild_age) + except isodate.ISO8601Error: + logger.warning('The guild join age limit needs to be a ' + 'ISO-8601 duration formatted duration string ' + f'greater than 0 days, not "%s".', str(guild_age)) + del self.config.cache['guild_age'] + await self.config.update() + guild_age = isodate.duration.Duration() + reason = self.blocked_users.get(str(message.author.id)) if reason is None: reason = '' + try: min_account_age = message.author.created_at + account_age except ValueError as e: logger.warning(e.args[0]) del self.config.cache['account_age'] await self.config.update() - min_account_age = message.author.created_at + min_account_age = now - if min_account_age > datetime.utcnow(): - # user account has not reached the required time + try: + min_guild_age = self.guild.get_member(message.author.id).joined_at + guild_age + except ValueError as e: + logger.warning(e.args[0]) + del self.config.cache['guild_age'] + await self.config.update() + min_guild_age = now + + if min_account_age > now: + # User account has not reached the required time reaction = blocked_emoji changed = False delta = human_timedelta(min_account_age) @@ -514,6 +538,26 @@ async def process_modmail(self, message): color=discord.Color.red() )) + elif 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) + + if str(message.author.id) not in self.blocked_users: + new_reason = f'System Message: Recently Joined. Required to wait for {delta}.' + self.config.blocked[str(message.author.id)] = new_reason + await self.config.update() + changed = True + + 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} ' + f'before you can contact {self.user.mention}.', + color=discord.Color.red() + )) + elif str(message.author.id) in self.blocked_users: reaction = blocked_emoji if reason.startswith('System Message: New Account.'): @@ -524,8 +568,7 @@ async def process_modmail(self, message): else: end_time = re.search(r'%(.+?)%$', reason) if end_time is not None: - after = (datetime.fromisoformat(end_time.group(1)) - - datetime.utcnow()).total_seconds() + after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() if after <= 0: # No longer blocked reaction = sent_emoji diff --git a/core/config.py b/core/config.py index 8e1224a76c..00b017c93d 100644 --- a/core/config.py +++ b/core/config.py @@ -19,7 +19,8 @@ class ConfigManager(ConfigManagerABC): # bot settings 'main_category_id', 'disable_autoupdates', 'prefix', 'mention', - 'main_color', 'user_typing', 'mod_typing', 'account_age', 'reply_without_command', + 'main_color', 'user_typing', 'mod_typing', 'account_age', 'guild_age', + 'reply_without_command', # logging 'log_channel_id', @@ -71,7 +72,7 @@ class ConfigManager(ConfigManagerABC): } time_deltas = { - 'account_age' + 'account_age', 'guild_age' } valid_keys = allowed_to_change_in_command | internal_keys | protected_keys From 60a27f023b9711bac650b480c8b78216b9fb5e3c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 12 May 2019 12:58:47 -0700 Subject: [PATCH 0063/1221] Help command and stuff --- bot.py | 8 ++++---- cogs/utility.py | 33 ++++++++++++++++++++------------- core/changelog.py | 4 ++-- core/models.py | 1 + 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/bot.py b/bot.py index 8463fd646a..44cf3b0f95 100644 --- a/bot.py +++ b/bot.py @@ -202,12 +202,12 @@ def run(self, *args, **kwargs): self.metadata_task.cancel() self.loop.run_until_complete(self.metadata_task) except asyncio.CancelledError: - logger.debug('data_task has been cancelled') + logger.debug(info('data_task has been cancelled.')) try: self.autoupdate_task.cancel() self.loop.run_until_complete(self.autoupdate_task) except asyncio.CancelledError: - logger.debug('autoupdate_task has been cancelled') + logger.debug(info('autoupdate_task has been cancelled.')) self.loop.run_until_complete(self.logout()) for task in asyncio.Task.all_tasks(): @@ -217,7 +217,7 @@ def run(self, *args, **kwargs): asyncio.gather(*asyncio.Task.all_tasks()) ) except asyncio.CancelledError: - logger.debug('All pending tasks has been cancelled') + logger.debug(info('All pending tasks has been cancelled.')) finally: self.loop.run_until_complete(self.session.close()) self.loop.close() @@ -922,7 +922,7 @@ async def metadata_loop(self): try: await self.session.post('https://api.modmail.tk/metadata', json=data) - logger.debug('Posted metadata') + logger.debug(info('Posted metadata')) except: pass diff --git a/cogs/utility.py b/cogs/utility.py index 97e9770689..d660a8c34c 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -6,6 +6,7 @@ from datetime import datetime from difflib import get_close_matches from io import StringIO +from operator import itemgetter from typing import Union from json import JSONDecodeError from pkg_resources import parse_version @@ -33,23 +34,27 @@ class Utility: def __init__(self, bot: Bot): self.bot = bot + @staticmethod + def get_perms_required(cmd) -> PermissionLevel: + for c in cmd.checks: + perm_level = getattr(c, 'permission_level', + PermissionLevel.INVALID) + if perm_level is not PermissionLevel.INVALID: + return perm_level + return PermissionLevel.INVALID + async def format_cog_help(self, ctx, cog): """Formats the text for a cog help""" prefix = self.bot.prefix - def perms_required(cmd): - for c in cmd.checks: - return getattr(c, 'permission_level', 0) - return 0 - fmts = [''] - for cmd in sorted(self.bot.commands, - key=lambda cmd: perms_required(cmd)): + for perm_level, cmd in sorted(((self.get_perms_required(c), c) for c in self.bot.commands), + key=itemgetter(0)): if cmd.instance is cog and not cmd.hidden: - new_fmt = f'`{prefix + cmd.qualified_name}` ' - perm_level = perms_required(cmd) - if perm_level is not None: + if perm_level is PermissionLevel.INVALID: + new_fmt = f'`{prefix + cmd.qualified_name}` ' + else: new_fmt = f'`[{perm_level}] {prefix + cmd.qualified_name}` ' new_fmt += f'- {cmd.short_doc}\n' @@ -88,15 +93,17 @@ async def format_command_help(self, cmd): prefix = self.bot.prefix - perm_level = next(getattr(c, 'permission_level', None) for c in cmd.checks) - perm_level = f'{perm_level.name} [{perm_level}]' if perm_level is not None else '' + perm_level = self.get_perms_required(cmd) + if perm_level is not PermissionLevel.INVALID: + perm_level = f'{perm_level.name} [{perm_level}]' + else: + perm_level = '' embed = Embed( title=f'`{prefix}{cmd.signature}`', color=self.bot.main_color, description=cmd.help ) - if not isinstance(cmd, commands.Group): embed.set_footer(text=f'Permission level: {perm_level}') diff --git a/core/changelog.py b/core/changelog.py index 1a884f3eaf..2620ff1d68 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -106,11 +106,11 @@ class Changelog: ---------------- RAW_CHANGELOG_URL : str The URL to Modmail changelog. + CHANGELOG_URL : str + The URL to Modmail changelog directly from in GitHub. VERSION_REGEX : re.Pattern The regex used to parse the versions. """ - - RAW_CHANGELOG_URL = 'https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md' CHANGELOG_URL = 'https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md' diff --git a/core/models.py b/core/models.py index 4d0a7c905b..2778f94fd4 100644 --- a/core/models.py +++ b/core/models.py @@ -20,6 +20,7 @@ class PermissionLevel(IntEnum): MOD = 3 SUPPORTER = 2 REGULAR = 1 + INVALID = -1 class Bot(abc.ABC, commands.Bot): From b755078ac536d8a83135c057a680e124b8ba49f4 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 12 May 2019 13:54:05 -0700 Subject: [PATCH 0064/1221] v2.19.1, on_plugin_ready and force update --- CHANGELOG.md | 8 ++++++++ bot.py | 2 +- cogs/plugins.py | 3 +++ cogs/utility.py | 10 +++++++--- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ebbdbd697..d40b49f6a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.19.1 + +### Changed + +- Ability to force an update despite having the same version number. Helpful to keep up-to-date with the latest GitHub commit. + - `?update force`. +- Plugin developers now have a new event called `on_plugin_ready`, this is coroutine is awaited when all plugins are loaded. Use `on_plugin_ready` instead of `on_ready` since `on_ready` will not get called in plugins. + # v2.19.0 ### What's new? diff --git a/bot.py b/bot.py index a88d0544a9..f4e32b0cf0 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.19.0' +__version__ = '2.19.1' import asyncio import logging diff --git a/cogs/plugins.py b/cogs/plugins.py index 45ba1a9b23..d8ee83d697 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -8,6 +8,7 @@ import sys from discord.ext import commands +from discord.utils import async_all from core import checks from core.models import Bot, PermissionLevel @@ -63,6 +64,8 @@ async def download_initial_plugins(self): except DownloadError as exc: msg = f'{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}' logger.error(error(msg)) + await async_all(env() for env in self.bot.extra_events['on_plugin_ready']) + logger.debug(info('on_plugin_ready called.')) async def download_plugin_repo(self, username, repo): try: diff --git a/cogs/utility.py b/cogs/utility.py index f0339841c3..e103914daa 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -346,15 +346,19 @@ async def github(self, ctx): @checks.has_permissions(PermissionLevel.OWNER) @github_access_token_required @trigger_typing - async def update(self, ctx): - """Updates the bot, this only works with heroku users.""" + async def update(self, ctx, *, flag: str = ''): + """Updates the bot, this only works with heroku users. + + To stay up-to-date with the latest commit from GitHub, specify "force" as the flag. + """ + changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version desc = (f'The latest version is [`{self.bot.version}`]' '(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)') - if parse_version(self.bot.version) >= parse_version(latest.version): + if parse_version(self.bot.version) >= parse_version(latest.version) and flag.lower() != 'force': embed = Embed( title='Already up to date', description=desc, From 2171c3075f8abd133f65dcee26302ddaa61d42aa Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 12 May 2019 22:33:40 -0700 Subject: [PATCH 0065/1221] minor code refactor --- bot.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/bot.py b/bot.py index f4e32b0cf0..58472b63b1 100644 --- a/bot.py +++ b/bot.py @@ -111,7 +111,6 @@ def __init__(self): self.metadata_task = self.loop.create_task(self.metadata_loop()) self.autoupdate_task = self.loop.create_task(self.autoupdate_loop()) self._load_extensions() - self.owner = None def _configure_logging(self): level_text = self.config.log_level.upper() @@ -934,7 +933,7 @@ async def autoupdate_loop(self): embed.add_field(name='Merge Commit', value=f"[`{short_sha}`]({html_url}) " f"{message} - {user['username']}") - logger.info(info('Updating bot.')) + logger.info(info('Bot has been updated.')) channel = self.log_channel await channel.send(embed=embed) @@ -942,12 +941,12 @@ async def autoupdate_loop(self): async def metadata_loop(self): await self.wait_until_ready() - self.owner = (await self.application_info()).owner + owner = (await self.application_info()).owner while not self.is_closed(): data = { - "owner_name": str(self.owner), - "owner_id": self.owner.id, + "owner_name": str(owner), + "owner_id": owner.id, "bot_id": self.user.id, "bot_name": str(self.user), "avatar_url": self.user.avatar_url, @@ -962,13 +961,9 @@ async def metadata_loop(self): "last_updated": str(datetime.utcnow()) } - - try: - await self.session.post('https://api.modmail.tk/metadata', json=data) + async with self.session.post('https://api.modmail.tk/metadata', json=data): logger.debug(info('Uploading metadata to Modmail server.')) - except: - pass - + await asyncio.sleep(3600) From dce75331f7a6b5837f7f3d8242f17b78fcacde7c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 12 May 2019 23:35:11 -0700 Subject: [PATCH 0066/1221] Fixed an oversight --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index d8ee83d697..d4f84d1697 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -64,7 +64,7 @@ async def download_initial_plugins(self): except DownloadError as exc: msg = f'{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}' logger.error(error(msg)) - await async_all(env() for env in self.bot.extra_events['on_plugin_ready']) + await async_all(env() for env in self.bot.extra_events.get('on_plugin_ready', [])) logger.debug(info('on_plugin_ready called.')) async def download_plugin_repo(self, username, repo): From 2d2f6c81f2003b978a59838e0f06c170b566ef95 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 13 May 2019 22:07:11 +1000 Subject: [PATCH 0067/1221] Add oauth whitelist commands --- cogs/utility.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++- core/config.py | 3 ++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index b775579e14..ef74518f0a 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -11,7 +11,7 @@ from pkg_resources import parse_version from textwrap import indent -from discord import Embed, Color, Activity, Role +from discord import Embed, Color, Activity, Role, Member from discord.enums import ActivityType, Status from discord.ext import commands @@ -1165,6 +1165,63 @@ def get_level(perm_level): p_session = PaginatorSession(ctx, *embeds) return await p_session.run() + + @commands.group(invoke_without_command=True) + @checks.has_permissions(PermissionLevel.OWNER) + async def oauth(self, ctx): + """Commands relating to Logviewer oauth2 login authentication.""" + cmd = self.bot.get_command('help') + await ctx.invoke(cmd, command='config') + + @oauth.command() + @checks.has_permissions(PermissionLevel.OWNER) + async def whitelist(self, ctx, target: Union[Member, Role]): + """Whitelist or un-whitelist a user or role from having access to logs.""" + whitelisted = self.bot.config['oauth_whitelist'] + + if target.id in whitelisted: + whitelisted.remove(target.id) + removed = True + else: + whitelisted.append(target.id) + removed = False + + await self.bot.config.update() + + em = Embed(color=self.bot.main_color) + em.title = 'Success' + em.description = ( + f"{'Un-w' if removed else 'W'}hitelisted " + f"{target.mention} to view logs." + ) + + await ctx.send(embed=em) + + @oauth.command() + @checks.has_permissions(PermissionLevel.OWNER) + async def get(self, ctx): + """Shows a list of users and roles that are whitelisted to view logs.""" + whitelisted = self.bot.config['oauth_whitelist'] + + users = [] + roles = [] + + for id in whitelisted: + user = bot.get_user(id) + if user: + users.append(users) + role = self.modmail_guild.get_role(id) + if role: + roles.append(role) + + em = Embed(color=self.bot.main_color) + em.title = 'Oauth Whitelist' + em.add_field(name='Users', value=' '.join(u.mention for u in users) or 'None') + em.add_field(name='Roles', value=' '.join(r.mention for r in roles) or 'None') + + await ctx.send(embed=em) + + @commands.command(hidden=True, name='eval') @checks.has_permissions(PermissionLevel.OWNER) diff --git a/core/config.py b/core/config.py index 8e1224a76c..a763dabda6 100644 --- a/core/config.py +++ b/core/config.py @@ -39,7 +39,7 @@ class ConfigManager(ConfigManagerABC): internal_keys = { # bot presence - 'activity_message', 'activity_type', 'status', + 'activity_message', 'activity_type', 'status', 'oauth_whitelist', # moderation 'blocked', 'command_permissions', 'level_permissions', @@ -107,6 +107,7 @@ def populate_cache(self): 'plugins': [], 'aliases': {}, 'blocked': {}, + 'oauth_whitelist': [], 'command_permissions': {}, 'level_permissions': {}, 'notification_squad': {}, From 3a7a00adc9437c5b2dea6788efe70c9854c2fffd Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 13 May 2019 22:09:32 +1000 Subject: [PATCH 0068/1221] Increment version --- CHANGELOG.md | 6 ++++++ bot.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d40b49f6a4..2603b01f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.20.0 + +### What's new? + +New `oauth` whitelist command which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app check the logviewer [repo](https://github.com/kyb3r/logviewer). + # v2.19.1 ### Changed diff --git a/bot.py b/bot.py index 58472b63b1..c03067f3fe 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.19.1' +__version__ = '2.20.0' import asyncio import logging From e589fc727075a3fd5ef79fa8b42aeba68d2a2b8e Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 13 May 2019 22:13:04 +1000 Subject: [PATCH 0069/1221] Update utility.py --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 1cbb5e3fa0..bab46d46d3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1214,7 +1214,7 @@ async def get(self, ctx): roles = [] for id in whitelisted: - user = bot.get_user(id) + user = self.bot.get_user(id) if user: users.append(users) role = self.modmail_guild.get_role(id) From d2b5c66ea9c85ab44366eb855e76e754ee0f8917 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 13 May 2019 22:17:17 +1000 Subject: [PATCH 0070/1221] fix --- cogs/utility.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index bab46d46d3..0b90256e28 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1214,10 +1214,11 @@ async def get(self, ctx): roles = [] for id in whitelisted: + print(id) user = self.bot.get_user(id) if user: users.append(users) - role = self.modmail_guild.get_role(id) + role = self.bot.modmail_guild.get_role(id) if role: roles.append(role) From 4e3577cd4fce794737e0ae04c5afd44fe4103a22 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 13 May 2019 22:28:37 +1000 Subject: [PATCH 0071/1221] typo --- cogs/utility.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 0b90256e28..ce6ba2d7a1 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1214,16 +1214,16 @@ async def get(self, ctx): roles = [] for id in whitelisted: - print(id) user = self.bot.get_user(id) if user: - users.append(users) + users.append(user) role = self.bot.modmail_guild.get_role(id) if role: roles.append(role) em = Embed(color=self.bot.main_color) em.title = 'Oauth Whitelist' + em.add_field(name='Users', value=' '.join(u.mention for u in users) or 'None') em.add_field(name='Roles', value=' '.join(r.mention for r in roles) or 'None') From 931e6bdbcaeff39e5e5c8172cc73821a690bab6d Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 13 May 2019 23:57:06 +1000 Subject: [PATCH 0072/1221] Fix possible error with messages from external guild --- bot.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index c03067f3fe..3cb9181480 100644 --- a/bot.py +++ b/bot.py @@ -510,7 +510,11 @@ async def process_modmail(self, message): min_account_age = now try: - min_guild_age = self.guild.get_member(message.author.id).joined_at + guild_age + member = self.guild.get_member(message.author.id) + if member: + min_guild_age = member.joined_at + guild_age + else: + min_guild_age = now except ValueError as e: logger.warning(e.args[0]) del self.config.cache['guild_age'] From bc76e8c247e9850d45e06554d5d7f95d550b9608 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 13 May 2019 23:59:44 +1000 Subject: [PATCH 0073/1221] fix issue with config --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index ce6ba2d7a1..48613ae98a 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -723,7 +723,7 @@ async def remove_config(self, ctx, key: str.lower): @config.command() @checks.has_permissions(PermissionLevel.OWNER) - async def get(self, ctx, key=None): + async def _get(self, ctx, key=None): """Shows the config variables that are currently set.""" keys = self.bot.config.allowed_to_change_in_command From 6ab78e39d6bd62b80b91ba6bc15e9207a28833c3 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 14 May 2019 00:01:34 +1000 Subject: [PATCH 0074/1221] Change name to 'get' --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 48613ae98a..6cc0576ff1 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -721,7 +721,7 @@ async def remove_config(self, ctx, key: str.lower): return await ctx.send(embed=embed) - @config.command() + @config.command(name='get') @checks.has_permissions(PermissionLevel.OWNER) async def _get(self, ctx, key=None): """Shows the config variables that are currently set.""" From 1948b86f0920a1e0c4654a561135c5e787bb8608 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 14 May 2019 09:17:31 +1000 Subject: [PATCH 0075/1221] correct help cmd --- cogs/utility.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 6cc0576ff1..c45bd943fd 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1178,12 +1178,12 @@ def get_level(perm_level): async def oauth(self, ctx): """Commands relating to Logviewer oauth2 login authentication.""" cmd = self.bot.get_command('help') - await ctx.invoke(cmd, command='config') + await ctx.invoke(cmd, command='oauth') - @oauth.command() + @oauth.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def whitelist(self, ctx, target: Union[Member, Role]): - """Whitelist or un-whitelist a user or role from having access to logs.""" + """Globally whitelist or un-whitelist a user or role to have access to logs.""" whitelisted = self.bot.config['oauth_whitelist'] if target.id in whitelisted: From 7da3f8612f2024c8e87c01d5561b2fa85b3bcad0 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 13 May 2019 20:13:35 -0700 Subject: [PATCH 0076/1221] Normalized function names, created more aliases, responder is an alias to supporter --- cogs/modmail.py | 33 +++++++++++---------- cogs/plugins.py | 16 +++++------ cogs/utility.py | 76 +++++++++++++++++++++++++------------------------ core/models.py | 1 + 4 files changed, 64 insertions(+), 62 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 11361c571c..3c76f38f09 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -106,7 +106,7 @@ async def snippets(self, ctx): @snippets.command(name='add') @checks.has_permissions(PermissionLevel.SUPPORTER) - async def add_(self, ctx, name: str.lower, *, value): + async def snippets_add(self, ctx, name: str.lower, *, value): """Add a snippet to the bot config.""" if 'snippets' not in self.bot.config.cache: self.bot.config['snippets'] = {} @@ -124,7 +124,7 @@ async def add_(self, ctx, name: str.lower, *, value): @snippets.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.SUPPORTER) - async def remove_(self, ctx, *, name: str.lower): + async def snippets_remove(self, ctx, *, name: str.lower): """Removes a snippet from bot config.""" if self.bot.config.snippets.get(name): @@ -386,7 +386,6 @@ def format_log_embeds(self, logs, avatar_url): if prefix == 'NONE': prefix = '' - log_url = self.bot.config.log_url.strip('/') + f'{prefix}/{key}' username = entry['recipient']['name'] + '#' @@ -450,9 +449,9 @@ async def logs(self, ctx, *, member: User = None): session = PaginatorSession(ctx, *embeds) await session.run() - @logs.command(name='closed-by') + @logs.command(name='closed-by', aliases=['closeby']) @checks.has_permissions(PermissionLevel.SUPPORTER) - async def closed_by(self, ctx, *, user: User = None): + async def logs_closed_by(self, ctx, *, user: User = None): """Returns all logs closed by a user.""" user = user or ctx.author @@ -481,9 +480,9 @@ async def closed_by(self, ctx, *, user: User = None): session = PaginatorSession(ctx, *embeds) await session.run() - @logs.command() + @logs.command(name='search', aliases=['find']) @checks.has_permissions(PermissionLevel.SUPPORTER) - async def search(self, ctx, limit: Optional[int] = None, *, query): + async def logs_search(self, ctx, limit: Optional[int] = None, *, query): """Searches all logs for a message that contains your query.""" await ctx.trigger_typing() @@ -518,7 +517,7 @@ async def search(self, ctx, limit: Optional[int] = None, *, query): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def reply(self, ctx, *, msg=''): + async def reply(self, ctx, *, msg: str): """Reply to users using this command. Supports attachments and images as well as @@ -531,7 +530,7 @@ async def reply(self, ctx, *, msg=''): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def anonreply(self, ctx, *, msg=''): + async def anonreply(self, ctx, *, msg: str): """Reply to a thread anonymously. You can edit the anonymous user's name, @@ -547,7 +546,7 @@ async def anonreply(self, ctx, *, msg=''): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def note(self, ctx, *, msg=''): + async def note(self, ctx, *, msg: str): """Take a note about the current thread, useful for noting context.""" ctx.message.content = msg async with ctx.typing(): @@ -579,7 +578,7 @@ async def find_linked_message(self, ctx, message_id): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def edit(self, ctx, message_id: Optional[int] = None, - *, new_message): + *, new_message: str): """Edit a message that was sent using the reply command. If no `message_id` is provided, the @@ -758,7 +757,6 @@ async def unblock(self, ctx, *, user: User = None): use only. """ - if user is None: thread = ctx.thread if thread: @@ -803,7 +801,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 = None): + async def delete(self, ctx, message_id: Optional[int] = None): """Delete a message that was sent using the reply command. Deletes the previous message, unless a message ID is provided, which in that case, @@ -811,10 +809,11 @@ async def delete(self, ctx, message_id = None): """ thread = ctx.thread - try: - message_id = int(message_id) - except ValueError: - raise commands.BadArgument('An integer message ID needs to be specified.') + if message_id is not None: + try: + message_id = int(message_id) + except ValueError: + raise commands.BadArgument('An integer message ID needs to be specified.') linked_message_id = await self.find_linked_message(ctx, message_id) diff --git a/cogs/plugins.py b/cogs/plugins.py index d4f84d1697..5bc90b78b6 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -124,9 +124,9 @@ async def plugin(self, ctx): cmd = self.bot.get_command('help') await ctx.invoke(cmd, command='plugin') - @plugin.command() + @plugin.command(name='add') @checks.has_permissions(PermissionLevel.OWNER) - async def add(self, ctx, *, plugin_name): + async def plugin_add(self, ctx, *, plugin_name): """Adds a plugin""" if plugin_name in self.bot.config.plugins: return await ctx.send('Plugin already installed') @@ -164,9 +164,9 @@ async def add(self, ctx, *, plugin_name): await message.edit(content='Invalid plugin name format. ' 'Use username/repo/plugin.') - @plugin.command() + @plugin.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.OWNER) - async def remove(self, ctx, *, plugin_name): + async def plugin_remove(self, ctx, *, plugin_name): """Removes a certain plugin""" if plugin_name in self.bot.config.plugins: username, repo, name = self.parse_plugin(plugin_name) @@ -199,9 +199,9 @@ def onerror(func, path, exc_info): else: await ctx.send('Plugin not installed.') - @plugin.command() + @plugin.command(name='update') @checks.has_permissions(PermissionLevel.OWNER) - async def update(self, ctx, *, plugin_name): + async def plugin_update(self, ctx, *, plugin_name): """Updates a certain plugin""" if plugin_name not in self.bot.config.plugins: return await ctx.send('Plugin not installed') @@ -232,9 +232,9 @@ async def update(self, ctx, *, plugin_name): except DownloadError as exc: await ctx.send(f'Unable to start plugin: `{exc}`') - @plugin.command(name='list') + @plugin.command(name='list', aliases=['show', 'view']) @checks.has_permissions(PermissionLevel.OWNER) - async def list_(self, ctx): + async def plugin_list(self, ctx): """Shows a list of currently enabled plugins""" if self.bot.config.plugins: msg = '```\n' + '\n'.join(self.bot.config.plugins) + '\n```' diff --git a/cogs/utility.py b/cogs/utility.py index c45bd943fd..f0e41c2817 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -278,10 +278,10 @@ async def debug(self, ctx): session = MessagePaginatorSession(ctx, *messages, embed=embed) return await session.run() - @debug.command() + @debug.command(name='hastebin', aliases=['haste']) @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing - async def hastebin(self, ctx): + async def debug_hastebin(self, ctx): """Upload logs to hastebin.""" haste_url = os.environ.get('HASTE_URL', 'https://hasteb.in') @@ -309,10 +309,10 @@ async def hastebin(self, ctx): embed.set_footer(text='Go to Heroku to see your logs.') await ctx.send(embed=embed) - @debug.command() + @debug.command(name='clear', aliases=['wipe']) @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing - async def clear(self, ctx): + async def debug_clear(self, ctx): """Clears the locally cached logs.""" with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../temp/logs.log'), 'w'): @@ -650,9 +650,9 @@ async def config(self, ctx): cmd = self.bot.get_command('help') await ctx.invoke(cmd, command='config') - @config.command() + @config.command(name='options', aliases=['list']) @checks.has_permissions(PermissionLevel.OWNER) - async def options(self, ctx): + async def config_options(self, ctx): """Return a list of valid config keys you can change.""" allowed = self.bot.config.allowed_to_change_in_command valid = ', '.join(f'`{k}`' for k in allowed) @@ -661,11 +661,11 @@ async def options(self, ctx): color=self.bot.main_color) return await ctx.send(embed=embed) - @config.command() + @config.command(name='set', aliases=['add']) @checks.has_permissions(PermissionLevel.OWNER) - async def set(self, ctx, key: str.lower, *, value): + async def config_set(self, ctx, key: str.lower, *, value: str): """ - Sets a configuration variable and its value + Sets a configuration variable and its value. """ keys = self.bot.config.allowed_to_change_in_command @@ -695,7 +695,7 @@ async def set(self, ctx, key: str.lower, *, value): @config.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.OWNER) - async def remove_config(self, ctx, key: str.lower): + async def config_remove(self, ctx, key: str.lower): """Deletes a key from the config.""" keys = self.bot.config.allowed_to_change_in_command if key in keys: @@ -723,7 +723,7 @@ async def remove_config(self, ctx, key: str.lower): @config.command(name='get') @checks.has_permissions(PermissionLevel.OWNER) - async def _get(self, ctx, key=None): + async def config_get(self, ctx, key: str.lower = None): """Shows the config variables that are currently set.""" keys = self.bot.config.allowed_to_change_in_command @@ -804,7 +804,7 @@ async def alias(self, ctx): @alias.command(name='add') @checks.has_permissions(PermissionLevel.MODERATOR) - async def add_(self, ctx, name: str.lower, *, value): + async def alias_add(self, ctx, name: str.lower, *, value): """Add an alias to the bot config.""" if 'aliases' not in self.bot.config.cache: self.bot.config['aliases'] = {} @@ -840,7 +840,7 @@ async def add_(self, ctx, name: str.lower, *, value): @alias.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.MODERATOR) - async def remove_alias(self, ctx, *, name: str.lower): + async def alias_remove(self, ctx, *, name: str.lower): """Removes a alias from bot config.""" if 'aliases' not in self.bot.config.cache: @@ -890,14 +890,15 @@ async def permissions(self, ctx): @permissions.group(name='add', invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms(self, ctx): + async def permissions_add(self, ctx): """Add a permission to a command or a permission level.""" cmd = self.bot.get_command('help') await ctx.invoke(cmd, command='perms add') - @add_perms.command(name='command') + @permissions_add.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms_command(self, ctx, command: str, *, user_or_role: Union[User, Role, str]): + async def permissions_add_command(self, ctx, command: str, *, + user_or_role: Union[User, Role, str]): """Add a user, role, or everyone permission to use a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -923,9 +924,10 @@ async def add_perms_command(self, ctx, command: str, *, user_or_role: Union[User ) return await ctx.send(embed=embed) - @add_perms.command(name='level', aliases=['group']) + @permissions_add.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def add_perms_level(self, ctx, level: str, *, user_or_role: Union[User, Role, str]): + async def permissions_add_level(self, ctx, level: str, *, + user_or_role: Union[User, Role, str]): """Add a user, role, or everyone permission to use commands of a permission level.""" if level.upper() not in PermissionLevel.__members__: embed = Embed( @@ -954,14 +956,15 @@ async def add_perms_level(self, ctx, level: str, *, user_or_role: Union[User, Ro @permissions.group(name='remove', aliases=['del', 'delete', 'rm', 'revoke'], invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms(self, ctx): + async def permissions_remove(self, ctx): """Remove a permission to use a command or permission level.""" cmd = self.bot.get_command('help') await ctx.invoke(cmd, command='perms remove') - @remove_perms.command(name='command') + @permissions_remove.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms_command(self, ctx, command: str, *, user_or_role: Union[User, Role, str]): + async def permissions_remove_command(self, ctx, command: str, *, + user_or_role: Union[User, Role, str]): """Remove a user, role, or everyone permission to use a command.""" if command not in self.bot.all_commands: embed = Embed( @@ -987,9 +990,10 @@ async def remove_perms_command(self, ctx, command: str, *, user_or_role: Union[U ) return await ctx.send(embed=embed) - @remove_perms.command(name='level', aliases=['group']) + @permissions_remove.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def remove_perms_level(self, ctx, level: str, *, user_or_role: Union[User, Role, str]): + async def permissions_remove_level(self, ctx, level: str, *, + user_or_role: Union[User, Role, str]): """Remove a user, role, or everyone permission to use commands of a permission level.""" if level.upper() not in PermissionLevel.__members__: embed = Embed( @@ -1017,7 +1021,7 @@ async def remove_perms_level(self, ctx, level: str, *, user_or_role: Union[User, @permissions.group(name='get', invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) - async def get_perms(self, ctx, *, user_or_role: Union[User, Role, str]): + async def permissions_get(self, ctx, *, user_or_role: Union[User, Role, str]): """View the currently-set permissions.""" if hasattr(user_or_role, 'id'): @@ -1056,9 +1060,9 @@ async def get_perms(self, ctx, *, user_or_role: Union[User, Role, str]): p_session = PaginatorSession(ctx, *embeds) return await p_session.run() - @get_perms.command(name='command') + @permissions_get.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) - async def get_perms_command(self, ctx, *, command: str = None): + async def permissions_get_command(self, ctx, *, command: str = None): """View the currently-set permissions for a command.""" def get_command(cmd): @@ -1114,9 +1118,9 @@ def get_command(cmd): p_session = PaginatorSession(ctx, *embeds) return await p_session.run() - @get_perms.command(name='level', aliases=['group']) + @permissions_get.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) - async def get_perms_level(self, ctx, *, level: str = None): + async def permissions_get_level(self, ctx, *, level: str = None): """View the currently-set permissions for commands of a permission level.""" def get_level(perm_level): @@ -1173,16 +1177,16 @@ def get_level(perm_level): p_session = PaginatorSession(ctx, *embeds) return await p_session.run() - @commands.group(invoke_without_command=True) + @commands.group(invoke_without_command=True, aliases=['oauth2', 'auth', 'authentication']) @checks.has_permissions(PermissionLevel.OWNER) async def oauth(self, ctx): """Commands relating to Logviewer oauth2 login authentication.""" cmd = self.bot.get_command('help') await ctx.invoke(cmd, command='oauth') - @oauth.group(invoke_without_command=True) + @oauth.group(name='whitelist', invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) - async def whitelist(self, ctx, target: Union[Member, Role]): + async def oauth_whitelist(self, ctx, target: Union[Member, Role]): """Globally whitelist or un-whitelist a user or role to have access to logs.""" whitelisted = self.bot.config['oauth_whitelist'] @@ -1204,12 +1208,12 @@ async def whitelist(self, ctx, target: Union[Member, Role]): await ctx.send(embed=em) - @oauth.command() + @oauth.command(name='show', aliases=['get', 'list', 'view']) @checks.has_permissions(PermissionLevel.OWNER) - async def get(self, ctx): + async def oauth_show(self, ctx): """Shows a list of users and roles that are whitelisted to view logs.""" whitelisted = self.bot.config['oauth_whitelist'] - + users = [] roles = [] @@ -1220,7 +1224,7 @@ async def get(self, ctx): role = self.bot.modmail_guild.get_role(id) if role: roles.append(role) - + em = Embed(color=self.bot.main_color) em.title = 'Oauth Whitelist' @@ -1228,8 +1232,6 @@ async def get(self, ctx): em.add_field(name='Roles', value=' '.join(r.mention for r in roles) or 'None') await ctx.send(embed=em) - - @commands.command(hidden=True, name='eval') @checks.has_permissions(PermissionLevel.OWNER) diff --git a/core/models.py b/core/models.py index 2778f94fd4..0d35fb5a9f 100644 --- a/core/models.py +++ b/core/models.py @@ -19,6 +19,7 @@ class PermissionLevel(IntEnum): MODERATOR = 3 MOD = 3 SUPPORTER = 2 + RESPONDER = 2 REGULAR = 1 INVALID = -1 From 41142a1fe0e06d2a972182abe6d06e4ddfcaaf91 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 13 May 2019 23:59:15 -0700 Subject: [PATCH 0077/1221] v1.1.1 support --- bot.py | 17 ++++++++++------- cogs/modmail.py | 3 ++- cogs/plugins.py | 20 ++++++++++---------- cogs/utility.py | 7 +++++-- core/clients.py | 9 +++++---- core/thread.py | 8 ++++---- requirements.txt | 2 +- 7 files changed, 37 insertions(+), 29 deletions(-) diff --git a/bot.py b/bot.py index 3cb9181480..c2aed39f41 100644 --- a/bot.py +++ b/bot.py @@ -46,7 +46,7 @@ from motor.motor_asyncio import AsyncIOMotorClient from core.changelog import Changelog -from core.clients import SelfHostedClient, PluginDatabaseClient +from core.clients import DatabaseClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import info, error from core.models import Bot, PermissionLevel @@ -103,9 +103,9 @@ def __init__(self): self._db = None self._configure_logging() - + # TODO: Raise fatal error if mongo_uri or other essentials are not found self._db = AsyncIOMotorClient(self.config.mongo_uri).modmail_bot - self._api = SelfHostedClient(self) + self._api = DatabaseClient(self) self.plugin_db = PluginDatabaseClient(self) self.metadata_task = self.loop.create_task(self.metadata_loop()) @@ -563,7 +563,8 @@ async def process_modmail(self, message): elif str(message.author.id) in self.blocked_users: reaction = blocked_emoji - if reason.startswith('System Message: New Account.'): + if reason.startswith('System Message: New Account.') or \ + reason.startswith('System Message: Recently Joined.'): # Met the age limit already reaction = sent_emoji del self.config.blocked[str(message.author.id)] @@ -718,8 +719,7 @@ async def on_raw_reaction_add(self, payload): return channel = await _thread.recipient.create_dm() - # TODO: change to fetch_message (breaking change in d.py) - message = await channel.get_message(payload.message_id) + message = await channel.fetch_message(payload.message_id) reaction = payload.emoji close_emoji = await self.convert_emoji( @@ -810,6 +810,9 @@ async def on_message_delete(self, message): if message_id == url.split('/')[-1]: return await msg.delete() + 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: return @@ -953,7 +956,7 @@ async def metadata_loop(self): "owner_id": owner.id, "bot_id": self.user.id, "bot_name": str(self.user), - "avatar_url": self.user.avatar_url, + "avatar_url": str(self.user.avatar_url), "guild_id": self.guild_id, "guild_name": self.guild.name, "member_count": len(self.guild.members), diff --git a/cogs/modmail.py b/cogs/modmail.py index d5bc7a9a8f..f93374f76b 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -818,7 +818,8 @@ async def delete(self, ctx, message_id: Optional[int] = None): linked_message_id = await self.find_linked_message(ctx, message_id) if linked_message_id is None: - return await ctx.send(embed=discord.Embed( + return await ctx.send( + embed=discord.Embed( title='Failed', description='Cannot find a message to delete.', color=discord.Color.red() diff --git a/cogs/plugins.py b/cogs/plugins.py index c3f10eb185..a56a0bfcea 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -111,8 +111,8 @@ async def load_plugin(self, username, repo, plugin_name): try: self.bot.load_extension(ext) - except ModuleNotFoundError as exc: - raise DownloadError('Invalid plugin structure') from exc + except commands.ExtensionError as exc: + raise DownloadError('Invalid plugin.') from exc else: msg = f'Loaded plugins.{username}-{repo}.{plugin_name}' logger.info(info(msg)) @@ -129,10 +129,10 @@ async def plugin(self, ctx): async def plugin_add(self, ctx, *, plugin_name): """Adds a plugin""" if plugin_name in self.bot.config.plugins: - return await ctx.send('Plugin already installed') + return await ctx.send('Plugin already installed.') if plugin_name in self.bot.cogs.keys(): # another class with the same name - return await ctx.send('Another cog exists with the same name') + return await ctx.send('Another cog exists with the same name.') message = await ctx.send('Downloading plugin...') async with ctx.typing(): @@ -143,14 +143,14 @@ async def plugin_add(self, ctx, *, plugin_name): await self.download_plugin_repo(*parsed_plugin[:-1]) except DownloadError as exc: return await ctx.send( - f'Unable to fetch plugin from Github: {exc}' + f'Unable to fetch plugin from Github: {exc}.' ) importlib.invalidate_caches() try: await self.load_plugin(*parsed_plugin) except DownloadError as exc: - return await ctx.send(f'Unable to load plugin: `{exc}`') + return await ctx.send(f'Unable to load plugin: `{exc}`.') # if it makes it here, it has passed all checks and should # be entered into the config @@ -204,7 +204,7 @@ def onerror(func, path, exc_info): async def plugin_update(self, ctx, *, plugin_name): """Updates a certain plugin""" if plugin_name not in self.bot.config.plugins: - return await ctx.send('Plugin not installed') + return await ctx.send('Plugin not installed.') async with ctx.typing(): username, repo, name = self.parse_plugin(plugin_name) @@ -217,7 +217,7 @@ async def plugin_update(self, ctx, *, plugin_name): ) except subprocess.CalledProcessError as exc: err = exc.stderr.decode('utf8').strip() - await ctx.send(f'Error while updating: {err}') + await ctx.send(f'Error while updating: {err}.') else: output = cmd.stdout.decode('utf8').strip() await ctx.send(f'```\n{output}\n```') @@ -230,7 +230,7 @@ async def plugin_update(self, ctx, *, plugin_name): try: await self.load_plugin(username, repo, name) except DownloadError as exc: - await ctx.send(f'Unable to start plugin: `{exc}`') + await ctx.send(f'Unable to start plugin: `{exc}`.') @plugin.command(name='list', aliases=['show', 'view']) @checks.has_permissions(PermissionLevel.OWNER) @@ -240,7 +240,7 @@ async def plugin_list(self, ctx): msg = '```\n' + '\n'.join(self.bot.config.plugins) + '\n```' await ctx.send(msg) else: - await ctx.send('No plugins installed') + await ctx.send('No plugins installed.') def setup(bot): diff --git a/cogs/utility.py b/cogs/utility.py index 060fc641a1..3733e1ee59 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -28,7 +28,7 @@ logger = logging.getLogger('Modmail') -class Utility: +class Utility(commands.Cog): """General commands that provide utility.""" def __init__(self, bot: Bot): @@ -40,9 +40,10 @@ async def format_cog_help(self, ctx, cog): prefix = self.bot.prefix fmts = [''] + # TODO: Use Utility.get_commands() / Utility.walk_commands() for perm_level, cmd in sorted(((get_perm_level(c), c) for c in self.bot.commands), key=itemgetter(0)): - if cmd.instance is cog and not cmd.hidden: + if cmd.cog is cog and not cmd.hidden: if perm_level is PermissionLevel.INVALID: new_fmt = f'`{prefix + cmd.qualified_name}` ' else: @@ -90,6 +91,7 @@ async def format_command_help(self, cmd): else: perm_level = '' + # TODO: cmd.signature broken embed = Embed( title=f'`{prefix}{cmd.signature}`', color=self.bot.main_color, @@ -148,6 +150,7 @@ async def format_not_found(self, ctx, command): async def help_(self, ctx, *, command: str = None): """Shows the help message.""" + # TODO: use https://gist.github.com/Rapptz/288294ca99fa1f042f0e39a92ddd88eb if command: cmd = self.bot.get_command(command) cog = self.bot.cogs.get(command) diff --git a/core/clients.py b/core/clients.py index f1290e541f..bb7bb37fb2 100644 --- a/core/clients.py +++ b/core/clients.py @@ -19,6 +19,7 @@ if prefix == 'NONE': prefix = '' + class ApiClient: """ This class represents the general request class for all type of clients. @@ -255,7 +256,7 @@ async def login(cls, bot: Bot) -> 'GitHub': return self -class SelfHostedClient(UserClient, ApiClient): +class DatabaseClient(UserClient, ApiClient): def __init__(self, bot: Bot): super().__init__(bot) @@ -308,14 +309,14 @@ async def create_log_entry(self, recipient, channel, creator): 'id': str(recipient.id), 'name': recipient.name, 'discriminator': recipient.discriminator, - 'avatar_url': recipient.avatar_url, + 'avatar_url': str(recipient.avatar_url), 'mod': False }, 'creator': { 'id': str(creator.id), 'name': creator.name, 'discriminator': creator.discriminator, - 'avatar_url': creator.avatar_url, + 'avatar_url': str(creator.avatar_url), 'mod': isinstance(creator, Member) }, 'closer': None, @@ -363,7 +364,7 @@ async def append_log(self, message, channel_id='', type_='thread_message'): 'id': str(message.author.id), 'name': message.author.name, 'discriminator': message.author.discriminator, - 'avatar_url': message.author.avatar_url, + 'avatar_url': str(message.author.avatar_url), 'mod': not isinstance(message.channel, DMChannel), }, 'content': message.content, diff --git a/core/thread.py b/core/thread.py index e42b163cc2..cb0162591a 100644 --- a/core/thread.py +++ b/core/thread.py @@ -216,14 +216,15 @@ async def _close(self, closer, silent=False, delete_channel=True, 'id': str(closer.id), 'name': closer.name, 'discriminator': closer.discriminator, - 'avatar_url': closer.avatar_url, + 'avatar_url': str(closer.avatar_url), 'mod': True } }) if log_data is not None and isinstance(log_data, dict): prefix = os.getenv('LOG_URL_PREFIX', '/logs') - if prefix == 'NONE': prefix = '' + if prefix == 'NONE': + prefix = '' log_url = f"{self.bot.config.log_url.strip('/')}{prefix}/{log_data['key']}" if log_data['messages']: @@ -703,7 +704,6 @@ def _format_info_embed(self, user, log_url, log_count, color): """Get information about a member of a server supports users from the guild or not.""" member = self.bot.guild.get_member(user.id) - avi = user.avatar_url time = datetime.utcnow() # key = log_url.split('/')[-1] @@ -742,7 +742,7 @@ def _format_info_embed(self, user, log_url, log_count, color): footer = 'User ID: ' + str(user.id) embed.set_footer(text=footer) - embed.set_author(name=str(user), icon_url=avi, url=log_url) + embed.set_author(name=str(user), icon_url=user.avatar_url, url=log_url) # embed.set_thumbnail(url=avi) if member: diff --git a/requirements.txt b/requirements.txt index e97008a573..e8922d395b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -git+https://github.com/Rapptz/discord.py +discord.py==1.1.1 colorama>=0.4.0 python-dateutil>=2.7.0 emoji>=0.2 From 502074742abe75949b0773ff8d71a280b9c06bfd Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 15 May 2019 12:42:55 +1000 Subject: [PATCH 0078/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c74d559458..7a2098ae2a 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ -When a user sends a direct message to the bot, a channel is created within an isolated category. This channel is where messages will be relayed. To reply to the message, simply use the command `?reply` in the channel. A full list of commands can be found in the [wiki](https://github.com/kyb3r/modmail/wiki) or by using the `?help` command. +When a user sends a direct message to the bot, a channel is created within an isolated category. This channel is where messages will be relayed. To reply to the message, simply use the command `?reply` in the channel. A full list of commands can be found by using the `?help` command. ## Installation From 082b7e193425be9750334f7041f1df006c1c1ae6 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 15 May 2019 13:44:17 +1000 Subject: [PATCH 0079/1221] Update features section. --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7a2098ae2a..c532c92da4 100644 --- a/README.md +++ b/README.md @@ -31,25 +31,58 @@ When a user sends a direct message to the bot, a channel is created within an isolated category. This channel is where messages will be relayed. To reply to the message, simply use the command `?reply` in the channel. A full list of commands can be found by using the `?help` command. -## Installation - -Currently, the easiest and fastest way to set up the bot is by using Heroku, which is a service that offers a free plan for hosting applications. If you choose to install the bot using Heroku, you will not need to download anything. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join our [Discord server](https://discord.gg/etJNHCQ) for help and support. Even if you don't have any issues, you should come and check out our awesome Discord community! :wink: +## Features + +* Aesthetic command interfaces e.g Pagination via reactions +* Highly Customisable: + * Bot activity, prefix, etc. + * Command permissions + * Interface elements (color, responses, reactions, etc.) + * Snippets and *command aliases* + * Minimum account/guild age in order to create a thread. +* Scheduled closing - e.g. `?close in 2 hours silently` +* Blocking users e.g. `?block @user spamming for 2 days` +* Get notified/subscribe for thread messages. (`?notify` and `?sub`) +* Linked thread messages + * Editing/deleting a message on one end is synced with the other. +* Thread logs, e.g. https://logs.modmail.tk/example + * When you close a thread, a log link is generated and posted to your log-channel. + * Rendered in styled HTML like Discord. + * Optional login in via Discord to protect your logs. + * See past logs of a user with `?logs` + * Searchable by text queries using `?logs search` +* Optional automatic updates to stay up to date with the latest features. + +This list is ever growing thanks to active development and our exceptional contributors. -Interactive installation: [**https://taaku18.github.io/modmail/installation**](https://taaku18.github.io/modmail/installation) +## Installation -# Notable Features +### Heroku +Currently, the easiest way to set up the bot is by using Heroku, a container-based cloud platform. Installation via Heroku is done in your web browser and keeps the bot online 24/7 for free. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. -## Customizability +### Locally +Installation locally for development reasons or otherwise is as follows, you will need `python 3.7` and `pipenv`. -Modmail has a range of configuration variables that you can dynamically alter with the `?config` command. You can use them to change the different aspects of the bot, for example, the embed color, responses, reactions, status, etc. Snippets and custom command aliases are also supported. The level of customization is ever growing thanks to our exceptional contributors. +Clone the repo +```console +$ git clone https://github.com/kyb3r/modmail +$ cd modmail +``` -## Thread Logs +Install dependancies +```console +$ pipenv install +``` -Thread conversations are automatically logged with a generated viewable website of the complete thread. Logs are rendered with styled HTML and presented in an aesthetically pleasing way—it blends seamlessly with the mobile version of Discord. You have the ability to query and search through logs using the bot via commands. An example of a logged conversation: https://logs.modmail.tk/example. +Rename the `config.json.example` to `config.json` and fill out the fields. +And finally, run the bot. +```console +$ pipenv run python3 bot.py +``` -# Contributing +## Contributing This project is licenced under MIT. If you have any new ideas, create an issue or a pull request. Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. -If you use Modmail and love it, consider supporting me on **[Patreon](https://www.patreon.com/kyber)** :heart: +If you use Modmail and love it, consider becoming a patron on **[Patreon](https://www.patreon.com/kyber)** :smile: From 375a5343e8a28d155bdf0a0cf1ea5934837ec28f Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 15 May 2019 13:46:25 +1000 Subject: [PATCH 0080/1221] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index c532c92da4..10f23e0f17 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,6 @@ ## How Does Modmail Work? - - When a user sends a direct message to the bot, a channel is created within an isolated category. This channel is where messages will be relayed. To reply to the message, simply use the command `?reply` in the channel. A full list of commands can be found by using the `?help` command. ## Features From 75d02429fa3731fb0406a6f632aa0f49f23c3b0d Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 15 May 2019 13:49:02 +1000 Subject: [PATCH 0081/1221] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 10f23e0f17..121c17e00e 100644 --- a/README.md +++ b/README.md @@ -31,24 +31,24 @@ When a user sends a direct message to the bot, a channel is created within an is ## Features -* Aesthetic command interfaces e.g Pagination via reactions * Highly Customisable: * Bot activity, prefix, etc. * Command permissions * Interface elements (color, responses, reactions, etc.) * Snippets and *command aliases* * Minimum account/guild age in order to create a thread. -* Scheduled closing - e.g. `?close in 2 hours silently` -* Blocking users e.g. `?block @user spamming for 2 days` -* Get notified/subscribe for thread messages. (`?notify` and `?sub`) -* Linked thread messages - * Editing/deleting a message on one end is synced with the other. * Thread logs, e.g. https://logs.modmail.tk/example * When you close a thread, a log link is generated and posted to your log-channel. * Rendered in styled HTML like Discord. * Optional login in via Discord to protect your logs. * See past logs of a user with `?logs` * Searchable by text queries using `?logs search` +* Aesthetic command interfaces e.g Pagination via reactions +* Scheduled closing - e.g. `?close in 2 hours silently` +* Blocking users e.g. `?block @user spamming for 2 days` +* Get notified/subscribe for thread messages. (`?notify` and `?sub`) +* Linked thread messages Editing/deleting messages is synced on both ends. + * Optional automatic updates to stay up to date with the latest features. This list is ever growing thanks to active development and our exceptional contributors. From 5d590eec1f4e3db32a7a11021fa89a41a4e16c90 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 15 May 2019 14:04:21 +1000 Subject: [PATCH 0082/1221] Update README.md --- README.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 121c17e00e..da54c7ba5d 100644 --- a/README.md +++ b/README.md @@ -25,33 +25,32 @@ -## How Does Modmail Work? +## What is Modmail? -When a user sends a direct message to the bot, a channel is created within an isolated category. This channel is where messages will be relayed. To reply to the message, simply use the command `?reply` in the channel. A full list of commands can be found by using the `?help` command. +Modmail's core functionality provides an efficient communications interface between server members and staff. When a member sends a direct message to the bot, a channel is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. ## Features -* Highly Customisable: +* **Highly Customisable**: * Bot activity, prefix, etc. * Command permissions * Interface elements (color, responses, reactions, etc.) * Snippets and *command aliases* * Minimum account/guild age in order to create a thread. -* Thread logs, e.g. https://logs.modmail.tk/example +* **Thread logs**, e.g. https://logs.modmail.tk/example * When you close a thread, a log link is generated and posted to your log-channel. * Rendered in styled HTML like Discord. * Optional login in via Discord to protect your logs. * See past logs of a user with `?logs` * Searchable by text queries using `?logs search` -* Aesthetic command interfaces e.g Pagination via reactions -* Scheduled closing - e.g. `?close in 2 hours silently` -* Blocking users e.g. `?block @user spamming for 2 days` -* Get notified/subscribe for thread messages. (`?notify` and `?sub`) -* Linked thread messages Editing/deleting messages is synced on both ends. - -* Optional automatic updates to stay up to date with the latest features. +* **Feature rich and robust** + * Scheduled tasks - `?close in 2 hours silently` + * Moderate usage by blocking users -`?block @user spamming for 2 days` + * Get notified/subscribe for thread messages. `?notify` and `?sub` + * Editing and deleting messages is synced on both ends. + * Optional automatic updates to stay up to date with the latest features. -This list is ever growing thanks to active development and our exceptional contributors. +This list is ever growing thanks to active development and our exceptional contributors. See a full list of documented commands by using the `help` command . ## Installation From 6303789d0fd0200d3d81010fda57370323ee10a5 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 15 May 2019 14:05:43 +1000 Subject: [PATCH 0083/1221] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index da54c7ba5d..3641ab42e4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

- A simple and functional Modmail bot for Discord. + A feature rich Modmail bot for Discord.

@@ -37,8 +37,8 @@ Modmail's core functionality provides an efficient communications interface betw * Interface elements (color, responses, reactions, etc.) * Snippets and *command aliases* * Minimum account/guild age in order to create a thread. -* **Thread logs**, e.g. https://logs.modmail.tk/example - * When you close a thread, a log link is generated and posted to your log-channel. +* **Thread logs**, + * When you close a thread, a [log link](https://logs.modmail.tk/example) is generated and posted to your log channel. * Rendered in styled HTML like Discord. * Optional login in via Discord to protect your logs. * See past logs of a user with `?logs` From 2e22af5eeb452815085508cca67c34fb137c6fcd Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 15 May 2019 14:26:27 +1000 Subject: [PATCH 0084/1221] Update README.md --- README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3641ab42e4..4a77e405b5 100644 --- a/README.md +++ b/README.md @@ -33,25 +33,23 @@ Modmail's core functionality provides an efficient communications interface betw * **Highly Customisable**: * Bot activity, prefix, etc. - * Command permissions + * Fully customisable command permission system. * Interface elements (color, responses, reactions, etc.) * Snippets and *command aliases* * Minimum account/guild age in order to create a thread. -* **Thread logs**, +* **Thread logs** * When you close a thread, a [log link](https://logs.modmail.tk/example) is generated and posted to your log channel. * Rendered in styled HTML like Discord. * Optional login in via Discord to protect your logs. * See past logs of a user with `?logs` * Searchable by text queries using `?logs search` -* **Feature rich and robust** - * Scheduled tasks - `?close in 2 hours silently` - * Moderate usage by blocking users -`?block @user spamming for 2 days` - * Get notified/subscribe for thread messages. `?notify` and `?sub` +* **Robust implementation** + * Scheduled tasks in human time, e.g. `?close in 2 hours silently`. * Editing and deleting messages is synced on both ends. - * Optional automatic updates to stay up to date with the latest features. + * Support for the full range of message content (mutliple images, files). + * Paginated commands interfaces via reactions. -This list is ever growing thanks to active development and our exceptional contributors. See a full list of documented commands by using the `help` command . - +This list is ever growing thanks to active development and our exceptional contributors. See a full list of documented commands by using the `help` command. ## Installation From df5c893e6993bad6b53677894373aad64ab242d6 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 15 May 2019 14:26:41 +1000 Subject: [PATCH 0085/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a77e405b5..f550708e22 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Modmail's core functionality provides an efficient communications interface betw ## Features -* **Highly Customisable**: +* **Highly Customisable** * Bot activity, prefix, etc. * Fully customisable command permission system. * Interface elements (color, responses, reactions, etc.) From 95d3b87a3d47e4965d515531f9ca1a92eb9f737c Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 15 May 2019 14:31:03 +1000 Subject: [PATCH 0086/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f550708e22..f349e73153 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Modmail's core functionality provides an efficient communications interface betw ## Features * **Highly Customisable** - * Bot activity, prefix, etc. + * Bot activity, prefix, category, log channel, etc. * Fully customisable command permission system. * Interface elements (color, responses, reactions, etc.) * Snippets and *command aliases* From 8ed9c88e9bcc7096f60088caee8bb2b40ea96891 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 15 May 2019 18:56:47 +1000 Subject: [PATCH 0087/1221] Fix help command for reply and note --- core/thread.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/thread.py b/core/thread.py index e42b163cc2..458c195205 100644 --- a/core/thread.py +++ b/core/thread.py @@ -4,10 +4,11 @@ import os import string import typing +from types import SimpleNamespace as param from datetime import datetime, timedelta import discord -from discord.ext.commands import UserInputError, CommandError +from discord.ext.commands import MissingRequiredArgument from core.models import Bot, ThreadManagerABC, ThreadABC from core.utils import is_image_url, days, match_user_id @@ -348,7 +349,7 @@ async def delete_message(self, message_id): async def note(self, message): if not message.content and not message.attachments: - raise UserInputError + raise MissingRequiredArgument(param(name='msg')) await asyncio.gather( self.bot.api.append_log(message, @@ -359,7 +360,7 @@ async def note(self, message): async def reply(self, message, anonymous=False): if not message.content and not message.attachments: - raise UserInputError + raise MissingRequiredArgument(param(name='msg')) if all(not g.get_member(self.id) for g in self.bot.guilds): return await message.channel.send( embed=discord.Embed( From 9c576f2901170618a7f995b50df0bb9770f95516 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 15 May 2019 18:57:48 +1000 Subject: [PATCH 0088/1221] Fix sending image only messages --- cogs/modmail.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 3c76f38f09..84c4ceb3c2 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -517,7 +517,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def reply(self, ctx, *, msg: str): + async def reply(self, ctx, *, msg: str=''): """Reply to users using this command. Supports attachments and images as well as @@ -530,7 +530,7 @@ async def reply(self, ctx, *, msg: str): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def anonreply(self, ctx, *, msg: str): + async def anonreply(self, ctx, *, msg: str=''): """Reply to a thread anonymously. You can edit the anonymous user's name, @@ -546,7 +546,7 @@ async def anonreply(self, ctx, *, msg: str): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def note(self, ctx, *, msg: str): + async def note(self, ctx, *, msg: str=''): """Take a note about the current thread, useful for noting context.""" ctx.message.content = msg async with ctx.typing(): From 532191538a1cf27361299b9d134ed282f1b317be Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 15 May 2019 22:34:36 +1000 Subject: [PATCH 0089/1221] Use pip instead --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f349e73153..0726b710b5 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ This list is ever growing thanks to active development and our exceptional contr Currently, the easiest way to set up the bot is by using Heroku, a container-based cloud platform. Installation via Heroku is done in your web browser and keeps the bot online 24/7 for free. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. ### Locally -Installation locally for development reasons or otherwise is as follows, you will need `python 3.7` and `pipenv`. +Installation locally for development reasons or otherwise is as follows, you will need `python 3.7`. Clone the repo ```console @@ -67,13 +67,13 @@ $ cd modmail Install dependancies ```console -$ pipenv install +$ pip install -r requirements.txt ``` Rename the `config.json.example` to `config.json` and fill out the fields. And finally, run the bot. ```console -$ pipenv run python3 bot.py +$ python3 bot.py ``` ## Contributing From e8eff73ab324dfa699e104307d87de32750a1f68 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 15 May 2019 20:16:12 -0700 Subject: [PATCH 0090/1221] v1.1.1 supported help command --- bot.py | 26 ++---- cogs/modmail.py | 18 ++-- cogs/plugins.py | 3 +- cogs/utility.py | 222 +++++++++++++++++++++++----------------------- core/checks.py | 1 + core/paginator.py | 5 +- core/utils.py | 7 ++ requirements.txt | 8 +- 8 files changed, 140 insertions(+), 150 deletions(-) diff --git a/bot.py b/bot.py index c2aed39f41..59b624ed91 100644 --- a/bot.py +++ b/bot.py @@ -48,7 +48,7 @@ from core.changelog import Changelog from core.clients import DatabaseClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import info, error +from core.utils import info, error, human_join from core.models import Bot, PermissionLevel from core.thread import ThreadManager from core.time import human_timedelta @@ -167,8 +167,6 @@ async def get_prefix(self, message=None): def _load_extensions(self): """Adds commands automatically""" - self.remove_command('help') - logger.info(LINE) logger.info(info('┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬')) logger.info(info('││││ │ │││││├─┤││')) @@ -834,13 +832,6 @@ async def on_error(self, event_method, *args, **kwargs): logger.error(error('Unexpected exception:'), exc_info=sys.exc_info()) async def on_command_error(self, ctx, exception): - - def human_join(strings): - if len(strings) <= 2: - return ' or '.join(strings) - else: - return ', '.join(strings[:len(strings)-1]) + ' or ' + strings[-1] - if isinstance(exception, commands.BadUnionArgument): msg = 'Could not find the specified ' + human_join([c.__name__ for c in exception.converters]) await ctx.trigger_typing() @@ -855,12 +846,17 @@ def human_join(strings): color=discord.Color.red(), description=str(exception) )) - elif isinstance(exception, commands.MissingRequiredArgument): - await ctx.invoke(self.get_command('help'), - command=str(ctx.command)) elif isinstance(exception, commands.CommandNotFound): logger.warning(error('CommandNotFound: ' + str(exception))) + elif isinstance(exception, commands.MissingRequiredArgument): + await ctx.send_help(ctx.command) elif isinstance(exception, commands.CheckFailure): + for check in ctx.command.checks: + if not await check(ctx) and hasattr(check, 'fail_msg'): + await ctx.send(embed=discord.Embed( + color=discord.Color.red(), + description=check.fail_msg + )) logger.warning(error('CheckFailure: ' + str(exception))) else: logger.error(error('Unexpected exception:'), exc_info=exception) @@ -975,9 +971,5 @@ async def metadata_loop(self): if __name__ == '__main__': - if os.name != 'nt': - import uvloop - - uvloop.install() bot = ModmailBot() bot.run() diff --git a/cogs/modmail.py b/cogs/modmail.py index f93374f76b..2a14d834e6 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -147,17 +147,10 @@ async def snippets_remove(self, ctx, *, name: str.lower): @commands.command() @checks.has_permissions(PermissionLevel.MODERATOR) + @checks.thread_only() async def move(self, ctx, *, category: discord.CategoryChannel): """Moves a thread to a specified category.""" thread = ctx.thread - if not thread: - embed = discord.Embed( - title='Error', - description='This is not a Modmail thread.', - color=discord.Color.red() - ) - return await ctx.send(embed=embed) - await thread.channel.edit(category=category, sync_permissions=True) await ctx.message.add_reaction('✅') @@ -820,10 +813,11 @@ async def delete(self, ctx, message_id: Optional[int] = None): if linked_message_id is None: return await ctx.send( embed=discord.Embed( - title='Failed', - description='Cannot find a message to delete.', - color=discord.Color.red() - )) + title='Failed', + description='Cannot find a message to delete.', + color=discord.Color.red() + ) + ) await thread.delete_message(linked_message_id) await ctx.message.add_reaction('✅') diff --git a/cogs/plugins.py b/cogs/plugins.py index a56a0bfcea..ec7b6186f3 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -121,8 +121,7 @@ async def load_plugin(self, username, repo, plugin_name): @checks.has_permissions(PermissionLevel.OWNER) async def plugin(self, ctx): """Plugin handler. Controls the plugins in the bot.""" - cmd = self.bot.get_command('help') - await ctx.invoke(cmd, command='plugin') + await ctx.send_help(ctx.command) @plugin.command(name='add') @checks.has_permissions(PermissionLevel.OWNER) diff --git a/cogs/utility.py b/cogs/utility.py index 3733e1ee59..e3878fab0c 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -6,7 +6,6 @@ from datetime import datetime from difflib import get_close_matches from io import StringIO -from operator import itemgetter from typing import Union from json import JSONDecodeError from pkg_resources import parse_version @@ -28,156 +27,158 @@ logger = logging.getLogger('Modmail') -class Utility(commands.Cog): - """General commands that provide utility.""" - - def __init__(self, bot: Bot): - self.bot = bot - - async def format_cog_help(self, ctx, cog): - """Formats the text for a cog help""" +class ModmailHelpCommand(commands.HelpCommand): + async def format_cog_help(self, cog): + bot = self.context.bot + prefix = self.clean_prefix - prefix = self.bot.prefix - - fmts = [''] - # TODO: Use Utility.get_commands() / Utility.walk_commands() - for perm_level, cmd in sorted(((get_perm_level(c), c) for c in self.bot.commands), - key=itemgetter(0)): - if cmd.cog is cog and not cmd.hidden: - if perm_level is PermissionLevel.INVALID: - new_fmt = f'`{prefix + cmd.qualified_name}` ' - else: - new_fmt = f'`[{perm_level}] {prefix + cmd.qualified_name}` ' + formats = [''] + for cmd in await self.filter_commands(cog.get_commands(), sort=True, key=get_perm_level): + perm_level = get_perm_level(cmd) + if perm_level is PermissionLevel.INVALID: + format_ = f'`{prefix + cmd.qualified_name}` ' + else: + format_ = f'`[{perm_level}] {prefix + cmd.qualified_name}` ' - new_fmt += f'- {cmd.short_doc}\n' - if len(new_fmt) + len(fmts[-1]) >= 1024: - fmts.append(new_fmt) - else: - fmts[-1] += new_fmt + format_ += f'- {cmd.short_doc}\n' + if not format_.strip(): + continue + if len(format_) + len(formats[-1]) >= 1024: + formats.append(format_) + else: + formats[-1] += format_ embeds = [] - for fmt in fmts: - if fmt == '': - continue + for format_ in formats: embed = Embed( - description='*' + (inspect.getdoc(cog) or - 'No description') + '*', - color=self.bot.main_color + description=f'*{cog.description or "No description."}*', + color=bot.main_color ) - embed.add_field(name='Commands', value=fmt) + embed.add_field(name='Commands', value=format_) continued = ' (Continued)' if len(embeds) > 0 else '' + embed.set_author(name=cog.qualified_name + ' - Help' + continued, + icon_url=bot.user.avatar_url) - embed.set_author(name=cog.__class__.__name__ + ' - Help' + continued, - icon_url=ctx.bot.user.avatar_url) - - embed.set_footer(text=f'Type "{prefix}help command" ' - 'for more info on a command.') + embed.set_footer(text=f'Type "{prefix}{self.command_attrs["name"]} command" ' + 'for more info on a specific command.') embeds.append(embed) - return embeds - async def format_command_help(self, cmd): - """Formats command help.""" - if cmd.hidden: - return None + async def send_bot_help(self, mapping): + embeds = [] + # TODO: Implement for no cog commands + for cog in sorted((key for key in mapping.keys() if key is not None), + key=lambda c: c.qualified_name): + embeds.extend(await self.format_cog_help(cog)) + + p_session = PaginatorSession(self.context, *embeds, destination=self.get_destination()) + return await p_session.run() - prefix = self.bot.prefix + async def send_cog_help(self, cog): + embeds = await self.format_cog_help(cog) + p_session = PaginatorSession(self.context, *embeds, destination=self.get_destination()) + return await p_session.run() - perm_level = get_perm_level(cmd) + async def send_command_help(self, command): + if not await self.filter_commands([command]): + return + perm_level = get_perm_level(command) if perm_level is not PermissionLevel.INVALID: perm_level = f'{perm_level.name} [{perm_level}]' else: perm_level = '' - # TODO: cmd.signature broken embed = Embed( - title=f'`{prefix}{cmd.signature}`', - color=self.bot.main_color, - description=cmd.help + title=self.get_command_signature(command), + color=self.context.bot.main_color, + description=command.help + ) + embed.set_footer(text=f'Permission level: {perm_level}') + await self.get_destination().send(embed=embed) + + async def send_group_help(self, group): + if not await self.filter_commands([group]): + return + + perm_level = get_perm_level(group) + if perm_level is not PermissionLevel.INVALID: + perm_level = f'{perm_level.name} [{perm_level}]' + else: + perm_level = '' + + embed = Embed( + title=self.get_command_signature(group), + color=self.context.bot.main_color, + description=group.help ) - if not isinstance(cmd, commands.Group): - embed.set_footer(text=f'Permission level: {perm_level}') - return embed - embed.add_field(name='Permission level', value=perm_level) + format_ = '' + length = len(group.commands) - fmt = '' - length = len(cmd.commands) - for i, c in enumerate(sorted(cmd.commands, key=lambda c: c.name)): + for i, command in enumerate( + await self.filter_commands(group.commands, sort=True, key=lambda c: c.name) + ): # Bug: fmt may run over the embed limit if length == i + 1: # last branch = '└─' else: branch = '├─' - fmt += f'`{branch} {c.name}` - {c.short_doc}\n' + format_ += f'`{branch} {command.name}` - {command.short_doc}\n' - embed.add_field(name='Sub Commands', value=fmt) + embed.add_field(name='Sub Commands', value=format_) embed.set_footer( - text=f'Type "{prefix}help {cmd} command" ' - 'for more info on a command.' + text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' + 'for more info on a command.' ) - return embed + await self.get_destination().send(embed=embed) + + async def send_error_message(self, msg): + logger.warning(error(f'CommandNotFound: {msg}')) - async def format_not_found(self, ctx, command): - prefix = ctx.prefix embed = Embed( - title='Unable to Find Command or Category', color=Color.red() ) - embed.set_footer(text=f'Type "{prefix}help" to get ' - 'a full list of commands.') + embed.set_footer(text=f'Command/Category "{self.remove_mentions(self.context.author.mention)}" not found.') choices = set() - for name, c in self.bot.all_commands.items(): + for name, c in self.context.bot.all_commands.items(): if not c.hidden: choices.add(name) - + command = self.context.kwargs.get('command') + # print(self.context.message.content[self.context.in]) closest = get_close_matches(command, choices, n=1, cutoff=0.75) if closest: - # Perhaps you meant: - # - `item` - embed.description = (f'**Perhaps you meant:**\n' - f'\u2000- `{closest[0]}`') - return embed - - @commands.command(name='help') - @checks.has_permissions(PermissionLevel.REGULAR) - @trigger_typing - async def help_(self, ctx, *, command: str = None): - """Shows the help message.""" - - # TODO: use https://gist.github.com/Rapptz/288294ca99fa1f042f0e39a92ddd88eb - if command: - cmd = self.bot.get_command(command) - cog = self.bot.cogs.get(command) - embeds = [] - - if cmd: - help_msg = await self.format_command_help(cmd) - if help_msg: - embeds = [help_msg] + embed.add_field(name=f'Perhaps you meant:', value=f'`{closest[0]}`') + else: + embed.title = 'Cannot find command or category' + embed.set_footer(text=f'Type "{self.clean_prefix}{self.command_attrs["name"]}" ' + 'for a list of all available commands.') + await self.get_destination().send(embed=embed) - elif cog: - # checks if cog has commands - embeds = await self.format_cog_help(ctx, cog) - if not embeds: - embeds = [await self.format_not_found(ctx, command)] +class Utility(commands.Cog): + """General commands that provide utility.""" - p_session = PaginatorSession(ctx, *embeds) - return await p_session.run() + def __init__(self, bot: Bot): + self.bot = bot + self._original_help_command = bot.help_command + self.bot.help_command = ModmailHelpCommand( + verify_checks=False, + command_attrs={'help': 'Shows this help message.'} + ) + # Looks a bit ugly + self.bot.help_command._command_impl = checks.has_permissions( + PermissionLevel.REGULAR + )(self.bot.help_command._command_impl) - embeds = [] - for cog in sorted(self.bot.cogs.values(), - key=lambda cog: cog.__class__.__name__): - embeds.extend(await self.format_cog_help(ctx, cog)) + self.bot.help_command.cog = self - p_session = PaginatorSession(ctx, *embeds) - return await p_session.run() + def cog_unload(self): + self.bot.help_command = self._original_help_command @commands.command() @checks.has_permissions(PermissionLevel.REGULAR) @@ -651,8 +652,7 @@ async def prefix(self, ctx, *, prefix=None): @checks.has_permissions(PermissionLevel.OWNER) async def config(self, ctx): """Change config vars for the bot.""" - cmd = self.bot.get_command('help') - await ctx.invoke(cmd, command='config') + await ctx.send_help(ctx.command) @config.command(name='options', aliases=['list']) @checks.has_permissions(PermissionLevel.OWNER) @@ -889,15 +889,13 @@ async def permissions(self, ctx): Note: You will still have to manually give/take permission to the Modmail category to users/roles. """ - cmd = self.bot.get_command('help') - await ctx.invoke(cmd, command='perms') + await ctx.send_help(ctx.command) @permissions.group(name='add', invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add(self, ctx): """Add a permission to a command or a permission level.""" - cmd = self.bot.get_command('help') - await ctx.invoke(cmd, command='perms add') + await ctx.send_help(ctx.command) @permissions_add.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) @@ -962,8 +960,7 @@ async def permissions_add_level(self, ctx, level: str, *, @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove(self, ctx): """Remove a permission to use a command or permission level.""" - cmd = self.bot.get_command('help') - await ctx.invoke(cmd, command='perms remove') + await ctx.send_help(ctx.command) @permissions_remove.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) @@ -1185,9 +1182,8 @@ def get_level(perm_level): @checks.has_permissions(PermissionLevel.OWNER) async def oauth(self, ctx): """Commands relating to Logviewer oauth2 login authentication.""" - cmd = self.bot.get_command('help') - await ctx.invoke(cmd, command='oauth') - + await ctx.send_help(ctx.command) + @oauth.group(name='whitelist', invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def oauth_whitelist(self, ctx, target: Union[Member, Role]): diff --git a/core/checks.py b/core/checks.py index cfdbf33276..e7075a6596 100644 --- a/core/checks.py +++ b/core/checks.py @@ -95,4 +95,5 @@ async def predicate(ctx): Otherwise, `False`. """ return ctx.thread is not None + predicate.fail_msg = 'This is not a Modmail thread.' return commands.check(predicate) diff --git a/core/paginator.py b/core/paginator.py index fa22f0d51f..6491f402da 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -43,11 +43,12 @@ class PaginatorSession: def __init__(self, ctx: commands.Context, *embeds, **options): self.ctx = ctx - self.timeout: int = options.get('timeout', 180) + self.timeout: int = options.get('timeout', 210) self.embeds: typing.List[Embed] = list(embeds) self.running = False self.base: Message = None self.current = 0 + self.destination = options.get('destination', ctx) self.reaction_map = { '⏮': self.first_page, '◀': self.previous_page, @@ -87,7 +88,7 @@ async def create_base(self, embed: Embed) -> None: embed : Embed The `Embed` to fill the base `Message`. """ - self.base = await self.ctx.send(embed=embed) + self.base = await self.destination.send(embed=embed) if len(self.embeds) == 1: self.running = False diff --git a/core/utils.py b/core/utils.py index 738fe686e6..9bd5b9d219 100644 --- a/core/utils.py +++ b/core/utils.py @@ -126,6 +126,13 @@ def parse_image_url(url: str) -> str: return '' +def human_join(strings): + if len(strings) <= 2: + return ' or '.join(strings) + else: + return ', '.join(strings[:len(strings) - 1]) + ' or ' + strings[-1] + + def days(day: typing.Union[str, int]) -> str: """ Humanize the number of days. diff --git a/requirements.txt b/requirements.txt index e8922d395b..2dc036e2ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ uvloop>=0.12.0 motor>=2.0.0 natural==0.2.0 isodate>=0.6.0 -dnspython~=1.16.0 # required by motor -parsedatetime==2.4 # to be replaced by dateparser -aiohttp<3.5 # to comply with discord.py -pylint # for style +dnspython~=1.16.0 # required by motor +parsedatetime==2.4 +aiohttp>=3.3.0,<3.6.0 # to comply with discord.py +pylint # for style From a2a3d9982a3a57fb3dc4062bfac03b0a8ef03fc5 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 17 May 2019 00:38:48 -0700 Subject: [PATCH 0091/1221] Better help messages --- cogs/modmail.py | 218 +++++++++++++++++++++++++++++++----------------- cogs/plugins.py | 17 ++-- cogs/utility.py | 175 +++++++++++++++++++++++++++----------- 3 files changed, 278 insertions(+), 132 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 529f3c0884..de287a5dd8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -29,7 +29,13 @@ def __init__(self, bot: Bot): @trigger_typing @checks.has_permissions(PermissionLevel.OWNER) async def setup(self, ctx): - """Sets up a server for Modmail""" + """ + Sets up a server for Modmail. + + You only need to run this command + once after configuring Modmail. + """ + if self.bot.main_category: return await ctx.send( f'{self.bot.modmail_guild} is already set up.' @@ -74,7 +80,24 @@ async def setup(self, ctx): @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippets(self, ctx): - """Returns a list of snippets that are currently set.""" + """ + Create pre-defined messages for use in threads. + + When `{prefix}snippets` is used by itself, this will retrieve + a list of snippets that are currently set. + + To use snippets: + + First create a snippet using: + - `{prefix}snippet add snippet-name A pre-defined text.` + + Afterwards, you can use your snippet in a thread channel + with `{prefix}snippet-name`, the message "A pre-defined text." + will be sent to the recipient. + + See also `{prefix}alias`. + """ + embeds = [] if self.bot.snippets: @@ -107,9 +130,13 @@ async def snippets(self, ctx): @snippets.command(name='add') @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippets_add(self, ctx, name: str.lower, *, value): - """Add a snippet to the bot config.""" - if 'snippets' not in self.bot.config.cache: - self.bot.config['snippets'] = {} + """ + Add a snippet. + + To add a multi-word snippet name, use quotes: ``` + {prefix}snippets add "two word" this is a two word snippet. + ``` + """ self.bot.config.snippets[name] = value await self.bot.config.update() @@ -125,7 +152,7 @@ async def snippets_add(self, ctx, name: str.lower, *, value): @snippets.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippets_remove(self, ctx, *, name: str.lower): - """Removes a snippet from bot config.""" + """Remove a snippet.""" if self.bot.config.snippets.get(name): embed = discord.Embed( @@ -149,7 +176,11 @@ async def snippets_remove(self, ctx, *, name: str.lower): @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() async def move(self, ctx, *, category: discord.CategoryChannel): - """Moves a thread to a specified category.""" + """ + Move a thread to a specified category. + + `category` may be a category ID, mention, or name. + """ thread = ctx.thread await thread.channel.edit(category=category, sync_permissions=True) await ctx.message.add_reaction('✅') @@ -183,19 +214,19 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): Close the current thread. Close after a period of time: - - `close in 5 hours` - - `close 2m30s` + - `{prefix}close in 5 hours` + - `{prefix}close 2m30s` Custom close messages: - - `close 2 hours The issue has been resolved.` - - `close We will contact you once we find out more.` + - `{prefix}close 2 hours The issue has been resolved.` + - `{prefix}close We will contact you once we find out more.` Silently close a thread (no message) - - `close silently` - - `close in 10m silently` + - `{prefix}close silently` + - `{prefix}close in 10m silently` Stop a thread from closing: - - `close cancel` + - `{prefix}close cancel` """ thread = ctx.thread @@ -217,8 +248,7 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): else: embed = discord.Embed( color=discord.Color.red(), - description='This thread has not already ' - 'been scheduled to close.' + description='This thread has not already been scheduled to close.' ) return await ctx.send(embed=embed) @@ -236,22 +266,25 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): @commands.command(aliases=['alert']) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def notify(self, ctx, *, role=None): + async def notify(self, ctx, *, role: Union[discord.Role, str.lower, None] = None): """ - Notify a given role or yourself to the next thread message received. + Notify a role or yourself to the next thread message received. + + Once a thread message is received `role` will only be pinged once. - Once a thread message is received you will be pinged once only. + Leave `role` empty to notify yourself. `@here` and `@everyone` can be substituted with `here` and `everyone`. + `role` may be a role ID, mention, or name. """ thread = ctx.thread - if not role: + if role is None or role == 'me': mention = ctx.author.mention - elif role.lower() in ('here', 'everyone'): - mention = '@' + role - else: - converter = commands.RoleConverter() - role = await converter.convert(ctx, role) + elif isinstance(role, discord.Role): mention = role.mention + elif role in {'here', 'everyone', '@here', '@everyone'}: + mention = '@' + role.lstrip('@') + else: + raise commands.BadArgument(f'{role} is not a valid role.') if str(thread.id) not in self.bot.config['notification_squad']: self.bot.config['notification_squad'][str(thread.id)] = [] @@ -273,23 +306,25 @@ async def notify(self, ctx, *, role=None): @commands.command(aliases=['sub']) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def subscribe(self, ctx, *, role=None): + async def subscribe(self, ctx, *, role: Union[discord.Role, str.lower, None] = None): """ - Notify yourself or a given role for every thread message received. + Notify a role or yourself for every thread message received. + + You will be pinged for every thread message received until you unsubscribe. - You will be pinged for every thread message - received until you unsubscribe. + Leave `role` empty to notify yourself. `@here` and `@everyone` can be substituted with `here` and `everyone`. + `role` may be a role ID, mention, or name. """ thread = ctx.thread - if not role: + if role is None: mention = ctx.author.mention - elif role.lower() in ('here', 'everyone'): - mention = '@' + role - else: - converter = commands.RoleConverter() - role = await converter.convert(ctx, role) + elif isinstance(role, discord.Role): mention = role.mention + elif role in {'here', 'everyone', '@here', '@everyone'}: + mention = '@' + role.lstrip('@') + else: + raise commands.BadArgument(f'{role} is not a valid role.') if str(thread.id) not in self.bot.config['subscriptions']: self.bot.config['subscriptions'][str(thread.id)] = [] @@ -314,7 +349,12 @@ async def subscribe(self, ctx, *, role=None): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def unsubscribe(self, ctx, *, role=None): - """Unsubscribe yourself or a given role from a thread.""" + """ + Unsubscribe a role yourself from a thread. + + Leave `role` empty to notify yourself. `@here` and `@everyone` can be substituted with `here` and `everyone`. + `role` may be a role ID, mention, or name. + """ thread = ctx.thread if not role: @@ -347,7 +387,7 @@ async def unsubscribe(self, ctx, *, role=None): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def nsfw(self, ctx): - """Flags a Modmail thread as nsfw.""" + """Flags a Modmail thread as NSFW (not safe for work).""" await ctx.channel.edit(nsfw=True) await ctx.message.add_reaction('✅') @@ -355,7 +395,7 @@ async def nsfw(self, ctx): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def loglink(self, ctx): - """Return the link to the current thread's logs.""" + """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( @@ -411,18 +451,22 @@ def format_log_embeds(self, logs, avatar_url): @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) - async def logs(self, ctx, *, member: User = None): - """Shows a list of previous Modmail thread logs of a member.""" + async def logs(self, ctx, *, user: User = None): + """ + Get previous Modmail thread logs of a member. + + Leave `user` blank when this command is used within a + thread channel to show logs for the current recipient. + `user` may be a user ID, mention, or name. + """ await ctx.trigger_typing() - if not member: + if not user: thread = ctx.thread if not thread: raise commands.MissingRequiredArgument(param(name='member')) user = thread.recipient - else: - user = member default_avatar = 'https://cdn.discordapp.com/embed/avatars/0.png' icon_url = getattr(user, 'avatar_url', default_avatar) @@ -445,8 +489,13 @@ async def logs(self, ctx, *, member: User = None): @logs.command(name='closed-by', aliases=['closeby']) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs_closed_by(self, ctx, *, user: User = None): - """Returns all logs closed by a user.""" - user = user or ctx.author + """ + Get all logs closed by the specified user. + + If no `user` is provided, the user will be whoever sent this command. + `user` may be a user ID, mention, or name. + """ + user = user if user is not None else ctx.author query = { 'guild_id': str(self.bot.guild_id), @@ -476,7 +525,11 @@ async def logs_closed_by(self, ctx, *, user: User = None): @logs.command(name='search', aliases=['find']) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs_search(self, ctx, limit: Optional[int] = None, *, query): - """Searches all logs for a message that contains your query.""" + """ + Retrieve all logs that contain messages with your query. + + Provide a `limit` to specify the maximum number of logs the bot should find. + """ await ctx.trigger_typing() @@ -511,7 +564,8 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def reply(self, ctx, *, msg: str = ''): - """Reply to users using this command. + """ + Reply to a Modmail thread. Supports attachments and images as well as automatically embedding image URLs. @@ -524,7 +578,8 @@ async def reply(self, ctx, *, msg: str = ''): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def anonreply(self, ctx, *, msg: str = ''): - """Reply to a thread anonymously. + """ + Reply to a thread anonymously. You can edit the anonymous user's name, avatar and tag using the config command. @@ -540,7 +595,11 @@ async def anonreply(self, ctx, *, msg: str = ''): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def note(self, ctx, *, msg: str = ''): - """Take a note about the current thread, useful for noting context.""" + """ + Take a note about the current thread. + + Useful for noting context. + """ ctx.message.content = msg async with ctx.typing(): await ctx.thread.note(ctx.message) @@ -571,14 +630,12 @@ async def find_linked_message(self, ctx, message_id): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def edit(self, ctx, message_id: Optional[int] = None, - *, new_message: str): - """Edit a message that was sent using the reply command. + *, 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 mod will be edited. - - `[message_id]` the id of the message that you want to edit. - `new_message` is the new message that will be edited in. + last message sent by a staff will be edited. """ thread = ctx.thread @@ -592,8 +649,8 @@ async def edit(self, ctx, message_id: Optional[int] = None, )) await asyncio.gather( - thread.edit_message(linked_message_id, new_message), - self.bot.api.edit_message(linked_message_id, new_message) + thread.edit_message(linked_message_id, message), + self.bot.api.edit_message(linked_message_id, message) ) await ctx.message.add_reaction('✅') @@ -604,10 +661,14 @@ async def edit(self, ctx, message_id: Optional[int] = None, async def contact(self, ctx, category: Optional[discord.CategoryChannel] = None, *, user: Union[discord.Member, discord.User]): - """Create a thread with a specified member. + """ + Create a thread with a specified member. + + If `category` is specified, the thread + will be created in that specified category. - If the optional category argument is passed, the thread - will be created in the specified category. + `category`, if specified, may be a category ID, mention, or name. + `user` may be a user ID, mention, or name. """ if user.bot: @@ -642,7 +703,7 @@ async def contact(self, ctx, @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing async def blocked(self, ctx): - """Returns a list of blocked users""" + """Retrieve a list of blocked users.""" embed = discord.Embed(title='Blocked Users', color=self.bot.main_color, description='Here is a list of blocked users.') @@ -679,9 +740,14 @@ async def block(self, ctx, user: Optional[User] = None, *, """ Block a user from using Modmail. - Note: reasons that start with "System Message: " are reserved for internal - use only. + You may chose to set a time as to when the user will automatically be unblocked. + + Leave `user` blank when this command is used within a + thread channel to block the current recipient. + `user` may be a user ID, mention, or name. + `after` may be a simple "human-readable" time text. See `{prefix}help close` for examples. """ + reason = '' if user is None: @@ -697,8 +763,8 @@ async def block(self, ctx, user: Optional[User] = None, *, reason = after.arg if reason.startswith('System Message: '): raise commands.BadArgument('The reason cannot start with `System Message:`.') - if re.search(r'%(.+?)%$', reason) is not None: - raise commands.MissingRequiredArgument(param(name='reason')) + if '%' in reason: + raise commands.BadArgument('The reason contains illegal character "%".') if after.dt > after.now: reason = f'{reason} %{after.dt.isoformat()}%' @@ -708,11 +774,11 @@ async def block(self, ctx, user: Optional[User] = None, *, mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`' extend = f' for `{reason}`' if reason is not None else '' - msg = self.bot.blocked_users.get(str(user.id)) - if msg is None: - msg = '' + msg = self.bot.blocked_users.get(str(user.id), '') - if str(user.id) not in self.bot.blocked_users or extend or msg.startswith('System Message: '): + if str(user.id) not in self.bot.blocked_users or \ + reason is not None or \ + msg.startswith('System Message: '): if str(user.id) in self.bot.blocked_users: old_reason = msg.strip().rstrip('.') or 'no reason' @@ -744,10 +810,11 @@ async def block(self, ctx, user: Optional[User] = None, *, @trigger_typing async def unblock(self, ctx, *, user: User = None): """ - Unblocks a user from using Modmail. + Unblock a user from using Modmail. - Note: reasons start with "System Message: " are reserved for internal - use only. + Leave `user` blank when this command is used within a + thread channel to unblock the current recipient. + `user` may be a user ID, mention, or name. """ if user is None: @@ -795,10 +862,11 @@ async def unblock(self, ctx, *, user: User = None): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def delete(self, ctx, message_id: Optional[int] = None): - """Delete a message that was sent using the reply command. + """ + Delete a message that was sent using the reply or anonreply command. - Deletes the previous message, unless a message ID is provided, which in that case, - deletes the message with that message ID. + Deletes the previous message, unless a message ID is provided, + which in that case, deletes the message with that message ID. """ thread = ctx.thread diff --git a/cogs/plugins.py b/cogs/plugins.py index ec7b6186f3..232925deb1 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -22,7 +22,8 @@ class DownloadError(Exception): class Plugins(commands.Cog): - """Plugins expand Modmail functionality by allowing third-party addons. + """ + Plugins expand Modmail functionality by allowing third-party addons. These addons could have a range of features from moderation to simply making your life as a moderator easier! @@ -125,8 +126,8 @@ async def plugin(self, ctx): @plugin.command(name='add') @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_add(self, ctx, *, plugin_name): - """Adds a plugin""" + async def plugin_add(self, ctx, *, plugin_name: str): + """Add a plugin.""" if plugin_name in self.bot.config.plugins: return await ctx.send('Plugin already installed.') if plugin_name in self.bot.cogs.keys(): @@ -165,8 +166,8 @@ async def plugin_add(self, ctx, *, plugin_name): @plugin.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_remove(self, ctx, *, plugin_name): - """Removes a certain plugin""" + async def plugin_remove(self, ctx, *, plugin_name: str): + """Remove a plugin.""" if plugin_name in self.bot.config.plugins: username, repo, name = self.parse_plugin(plugin_name) self.bot.unload_extension( @@ -200,8 +201,8 @@ def onerror(func, path, exc_info): @plugin.command(name='update') @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_update(self, ctx, *, plugin_name): - """Updates a certain plugin""" + async def plugin_update(self, ctx, *, plugin_name: str): + """Update a plugin.""" if plugin_name not in self.bot.config.plugins: return await ctx.send('Plugin not installed.') @@ -234,7 +235,7 @@ async def plugin_update(self, ctx, *, plugin_name): @plugin.command(name='list', aliases=['show', 'view']) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_list(self, ctx): - """Shows a list of currently enabled plugins""" + """Shows a list of currently enabled plugins.""" if self.bot.config.plugins: msg = '```\n' + '\n'.join(self.bot.config.plugins) + '\n```' await ctx.send(msg) diff --git a/cogs/utility.py b/cogs/utility.py index e3878fab0c..6718da0bde 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -66,6 +66,9 @@ async def format_cog_help(self, cog): embeds.append(embed) return embeds + def process_help_msg(self, help: str): + return help.format(prefix=self.clean_prefix) + async def send_bot_help(self, mapping): embeds = [] # TODO: Implement for no cog commands @@ -91,9 +94,9 @@ async def send_command_help(self, command): perm_level = '' embed = Embed( - title=self.get_command_signature(command), + title=f'`{self.get_command_signature(command)}`', color=self.context.bot.main_color, - description=command.help + description=self.process_help_msg(command.help) ) embed.set_footer(text=f'Permission level: {perm_level}') await self.get_destination().send(embed=embed) @@ -109,9 +112,9 @@ async def send_group_help(self, group): perm_level = '' embed = Embed( - title=self.get_command_signature(group), + title=f'`{self.get_command_signature(group)}`', color=self.context.bot.main_color, - description=group.help + description=self.process_help_msg(group.help) ) embed.add_field(name='Permission level', value=perm_level) @@ -184,7 +187,7 @@ def cog_unload(self): @checks.has_permissions(PermissionLevel.REGULAR) @trigger_typing async def changelog(self, ctx): - """Show a paginated changelog of the bot.""" + """Shows the changelog of the Modmail.""" changelog = await Changelog.from_url(self.bot) try: paginator = PaginatorSession(ctx, *changelog.embeds) @@ -196,7 +199,7 @@ async def changelog(self, ctx): @checks.has_permissions(PermissionLevel.REGULAR) @trigger_typing async def about(self, ctx): - """Shows information about the bot.""" + """Shows information about this bot.""" embed = Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) embed.set_author(name='Modmail - About', @@ -216,8 +219,6 @@ async def about(self, ctx): embed.add_field(name='Author', value='[`kyb3r`](https://github.com/kyb3r)') - footer = f'Bot ID: {self.bot.user.id}' - changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version @@ -237,7 +238,7 @@ async def about(self, ctx): @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing async def debug(self, ctx): - """Shows the recent logs of the bot.""" + """Shows the recent application-logs of the bot.""" with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../temp/logs.log'), 'r+') as f: @@ -286,7 +287,7 @@ async def debug(self, ctx): @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing async def debug_hastebin(self, ctx): - """Upload logs to hastebin.""" + """Stores application-logs to Hastebin.""" haste_url = os.environ.get('HASTE_URL', 'https://hasteb.in') @@ -331,7 +332,7 @@ async def debug_clear(self, ctx): @github_access_token_required @trigger_typing async def github(self, ctx): - """Shows the GitHub user your access token is linked to.""" + """Shows the GitHub user your Github_Access_Token is linked to.""" data = await self.bot.api.get_user_info() embed = Embed( @@ -351,9 +352,13 @@ async def github(self, ctx): @github_access_token_required @trigger_typing async def update(self, ctx, *, flag: str = ''): - """Updates the bot, this only works with heroku users. + """ + Update bot. + + This only works for Heroku users who have configured their bot for updates. - To stay up-to-date with the latest commit from GitHub, specify "force" as the flag. + To stay up-to-date with the latest commit + from GitHub, specify "force" as the flag. """ changelog = await Changelog.from_url(self.bot) @@ -415,19 +420,23 @@ async def update(self, ctx, *, flag: str = ''): @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def activity(self, ctx, activity_type: str.lower, *, message: str = ''): """ - Set a custom activity for the bot. + Set an activity status for the bot. Possible activity types: - `playing` - `streaming` - `listening` - `watching` - - `clear` - - When activity type is set to `clear`, the current activity is removed. When activity type is set to `listening`, it must be followed by a "to": "listening to..." + + When activity type is set to `streaming`, you can set + the linked twitch page: + - `{prefix}config set twitch_url https://www.twitch.tv/somechannel/` + + To remove the current activity status: + - `{prefix}activity clear` """ if activity_type == 'clear': self.bot.config['activity_type'] = None @@ -466,7 +475,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ''): @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def status(self, ctx, *, status_type: str.lower): """ - Set a custom status for the bot. + Set a status for the bot. Possible status types: - `online` @@ -474,9 +483,9 @@ async def status(self, ctx, *, status_type: str.lower): - `dnd` - `do_not_disturb` or `do not disturb` - `invisible` or `offline` - - `clear` - When status type is set to `clear`, the current status is removed. + To remove the current status: + - `{prefix}status clear` """ if status_type == 'clear': self.bot.config['status'] = None @@ -605,21 +614,24 @@ async def ping(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) - async def mention(self, ctx, *, mention=None): - """Changes what the bot mentions at the start of each thread.""" + async def mention(self, ctx, *, mention: str = None): + """ + Change what the bot mentions at the start of each thread. + + Type only `{prefix}mention` to retrieve your current "mention" message. + """ current = self.bot.config.get('mention', '@here') if mention is None: embed = Embed( title='Current text', color=self.bot.main_color, - description=f'{current}' + description=str(current) ) - else: embed = Embed( title='Changed mention!', - description=f'On thread creation the bot now says {mention}', + description=f'On thread creation the bot now says {mention}.', color=self.bot.main_color ) self.bot.config['mention'] = mention @@ -630,7 +642,11 @@ async def mention(self, ctx, *, mention=None): @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def prefix(self, ctx, *, prefix=None): - """Changes the prefix for the bot.""" + """ + Change the prefix of the bot. + + Type only `{prefix}prefix` to retrieve your current bot prefix. + """ current = self.bot.prefix embed = Embed( @@ -648,16 +664,27 @@ async def prefix(self, ctx, *, prefix=None): await self.bot.config.update() await ctx.send(embed=embed) - @commands.group(invoke_without_command=True) + @commands.group(aliases=['configuration'], invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def config(self, ctx): - """Change config vars for the bot.""" + """ + Modify changeable configuration variables for this bot. + + Type `{prefix}config options` to view a list + of valid configuration variables. + + To set a configuration variable: + - `{prefix}config set varname value here` + + To remove a configuration variable: + - `{prefix}config set remove varname` + """ await ctx.send_help(ctx.command) @config.command(name='options', aliases=['list']) @checks.has_permissions(PermissionLevel.OWNER) async def config_options(self, ctx): - """Return a list of valid config keys you can change.""" + """Return a list of valid configuration names you can change.""" allowed = self.bot.config.allowed_to_change_in_command valid = ', '.join(f'`{k}`' for k in allowed) embed = Embed(title='Valid Keys', @@ -668,9 +695,7 @@ async def config_options(self, ctx): @config.command(name='set', aliases=['add']) @checks.has_permissions(PermissionLevel.OWNER) async def config_set(self, ctx, key: str.lower, *, value: str): - """ - Sets a configuration variable and its value. - """ + """Set a configuration variable and its value.""" keys = self.bot.config.allowed_to_change_in_command @@ -700,7 +725,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): @config.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.OWNER) async def config_remove(self, ctx, key: str.lower): - """Deletes a key from the config.""" + """Delete a set configuration variable.""" keys = self.bot.config.allowed_to_change_in_command if key in keys: try: @@ -728,7 +753,11 @@ async def config_remove(self, ctx, key: str.lower): @config.command(name='get') @checks.has_permissions(PermissionLevel.OWNER) async def config_get(self, ctx, key: str.lower = None): - """Shows the config variables that are currently set.""" + """ + Show the configuration variables that are currently set. + + Leave `key` empty to show all currently set configuration variables. + """ keys = self.bot.config.allowed_to_change_in_command if key: @@ -772,7 +801,23 @@ async def config_get(self, ctx, key: str.lower = None): @commands.group(aliases=['aliases'], invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) async def alias(self, ctx): - """Returns a list of aliases that are currently set.""" + """ + Create shortcuts to bot commands. + + When `?alias` is used by itself, this will retrieve + a list of alias that are currently set. + + To use alias: + + First create a snippet using: + - `{prefix}alias add alias-name other-command` + + For example: + - `{prefix}alias add reply r` + - Now you can use `{prefix}r` as an replacement for `{prefix}reply`. + + See also `{prefix}snippets`. + """ embeds = [] desc = 'Here is a list of aliases that are currently configured.' @@ -809,7 +854,7 @@ async def alias(self, ctx): @alias.command(name='add') @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_add(self, ctx, name: str.lower, *, value): - """Add an alias to the bot config.""" + """Add an alias.""" if 'aliases' not in self.bot.config.cache: self.bot.config['aliases'] = {} @@ -845,7 +890,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): @alias.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_remove(self, ctx, *, name: str.lower): - """Removes a alias from bot config.""" + """Remove an alias.""" if 'aliases' not in self.bot.config.cache: self.bot.config['aliases'] = {} @@ -872,7 +917,8 @@ async def alias_remove(self, ctx, *, name: str.lower): @commands.group(aliases=['perms'], invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def permissions(self, ctx): - """Sets the permissions for Modmail commands. + """ + Set the permissions for Modmail commands. You may set permissions based on individual command names, or permission levels. @@ -884,7 +930,7 @@ async def permissions(self, ctx): - **Supporter** [2] (access to core Modmail supporting functions) - **Regular** [1] (most basic interactions such as help and about) - By default, owner is set to the bot owner and regular is @everyone. + By default, owner is set to the absolute bot owner and regular is `@everyone`. Note: You will still have to manually give/take permission to the Modmail category to users/roles. @@ -901,7 +947,12 @@ async def permissions_add(self, ctx): @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add_command(self, ctx, command: str, *, user_or_role: Union[User, Role, str]): - """Add a user, role, or everyone permission to use a command.""" + """ + Add a user, role, or everyone permission to use a command. + + Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, + `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". + """ if command not in self.bot.all_commands: embed = Embed( title='Error', @@ -930,7 +981,12 @@ async def permissions_add_command(self, ctx, command: str, *, @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add_level(self, ctx, level: str, *, user_or_role: Union[User, Role, str]): - """Add a user, role, or everyone permission to use commands of a permission level.""" + """ + Add a user, role, or everyone permission to use commands of a permission level. + + Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, + `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". + """ if level.upper() not in PermissionLevel.__members__: embed = Embed( title='Error', @@ -966,7 +1022,12 @@ async def permissions_remove(self, ctx): @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove_command(self, ctx, command: str, *, user_or_role: Union[User, Role, str]): - """Remove a user, role, or everyone permission to use a command.""" + """ + Remove a user, role, or everyone permission to use a command. + + Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, + `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". + """ if command not in self.bot.all_commands: embed = Embed( title='Error', @@ -995,7 +1056,12 @@ async def permissions_remove_command(self, ctx, command: str, *, @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove_level(self, ctx, level: str, *, user_or_role: Union[User, Role, str]): - """Remove a user, role, or everyone permission to use commands of a permission level.""" + """ + Remove a user, role, or everyone permission to use commands of a permission level. + + Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, + `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". + """ if level.upper() not in PermissionLevel.__members__: embed = Embed( title='Error', @@ -1023,7 +1089,14 @@ async def permissions_remove_level(self, ctx, level: str, *, @permissions.group(name='get', invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get(self, ctx, *, user_or_role: Union[User, Role, str]): - """View the currently-set permissions.""" + """ + View the currently-set permissions. + + You can specify `user_or_role` as an alternative to get-by-command or get-by-level. + + Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, + `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". + """ if hasattr(user_or_role, 'id'): value = user_or_role.id @@ -1064,7 +1137,7 @@ async def permissions_get(self, ctx, *, user_or_role: Union[User, Role, str]): @permissions_get.command(name='command') @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get_command(self, ctx, *, command: str = None): - """View the currently-set permissions for a command.""" + """View currently-set permissions for a command.""" def get_command(cmd): permissions = self.bot.config.command_permissions.get(cmd.name, []) @@ -1122,7 +1195,7 @@ def get_command(cmd): @permissions_get.command(name='level', aliases=['group']) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get_level(self, ctx, *, level: str = None): - """View the currently-set permissions for commands of a permission level.""" + """View currently-set permissions for commands of a permission level.""" def get_level(perm_level): permissions = self.bot.config.level_permissions.get(perm_level.name, []) @@ -1186,8 +1259,12 @@ async def oauth(self, ctx): @oauth.group(name='whitelist', invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) - async def oauth_whitelist(self, ctx, target: Union[Member, Role]): - """Globally whitelist or un-whitelist a user or role to have access to logs.""" + async def oauth_whitelist(self, ctx, target: Union[User, Role]): + """ + Whitelist or un-whitelist a user or role to have access to logs. + + `target` may be a role ID, name, mention, user ID, name, or mention. + """ whitelisted = self.bot.config['oauth_whitelist'] if target.id in whitelisted: @@ -1236,7 +1313,7 @@ async def oauth_show(self, ctx): @commands.command(hidden=True, name='eval') @checks.has_permissions(PermissionLevel.OWNER) async def eval_(self, ctx, *, body: str): - """Evaluates Python code""" + """Evaluates Python code.""" env = { 'ctx': ctx, From 4d170cab863e1e5e6ff2551b40af96f3218411ac Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 17 May 2019 09:40:25 -0700 Subject: [PATCH 0092/1221] Doc corrections --- cogs/modmail.py | 28 +++++++++++++++------------- cogs/utility.py | 8 ++++---- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index de287a5dd8..26001c82fc 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,5 +1,4 @@ import asyncio -import re import os from datetime import datetime from typing import Optional, Union @@ -177,7 +176,7 @@ async def snippets_remove(self, ctx, *, name: str.lower): @checks.thread_only() async def move(self, ctx, *, category: discord.CategoryChannel): """ - Move a thread to a specified category. + Move a thread to another category. `category` may be a category ID, mention, or name. """ @@ -268,12 +267,13 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): @checks.thread_only() async def notify(self, ctx, *, role: Union[discord.Role, str.lower, None] = None): """ - Notify a role or yourself to the next thread message received. + Notify a role or yourself when the next thread message received. - Once a thread message is received `role` will only be pinged once. + Once a thread message is received, `role` will only be pinged once. - Leave `role` empty to notify yourself. `@here` and `@everyone` can be substituted with `here` and `everyone`. - `role` may be a role ID, mention, or name. + Leave `role` empty to notify yourself. + `@here` and `@everyone` can be substituted with `here` and `everyone`. + `role` may be a role ID, mention, name, "everyone", or "here". """ thread = ctx.thread @@ -312,8 +312,9 @@ async def subscribe(self, ctx, *, role: Union[discord.Role, str.lower, None] = N You will be pinged for every thread message received until you unsubscribe. - Leave `role` empty to notify yourself. `@here` and `@everyone` can be substituted with `here` and `everyone`. - `role` may be a role ID, mention, or name. + Leave `role` empty to subscribe yourself. + `@here` and `@everyone` can be substituted with `here` and `everyone`. + `role` may be a role ID, mention, name, "everyone", or "here". """ thread = ctx.thread @@ -350,10 +351,11 @@ async def subscribe(self, ctx, *, role: Union[discord.Role, str.lower, None] = N @checks.thread_only() async def unsubscribe(self, ctx, *, role=None): """ - Unsubscribe a role yourself from a thread. + Unsubscribe a role or yourself from a thread. - Leave `role` empty to notify yourself. `@here` and `@everyone` can be substituted with `here` and `everyone`. - `role` may be a role ID, mention, or name. + Leave `role` empty to unsubscribe yourself. + `@here` and `@everyone` can be substituted with `here` and `everyone`. + `role` may be a role ID, mention, name, "everyone", or "here". """ thread = ctx.thread @@ -492,7 +494,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): """ Get all logs closed by the specified user. - If no `user` is provided, the user will be whoever sent this command. + If no `user` is provided, the user will be the person who sent this command. `user` may be a user ID, mention, or name. """ user = user if user is not None else ctx.author @@ -740,7 +742,7 @@ async def block(self, ctx, user: Optional[User] = None, *, """ Block a user from using Modmail. - You may chose to set a time as to when the user will automatically be unblocked. + You may choose to set a time as to when the user will automatically be unblocked. Leave `user` blank when this command is used within a thread channel to block the current recipient. diff --git a/cogs/utility.py b/cogs/utility.py index 6718da0bde..c9749bc038 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -11,7 +11,7 @@ from pkg_resources import parse_version from textwrap import indent -from discord import Embed, Color, Activity, Role, Member +from discord import Embed, Color, Activity, Role from discord.enums import ActivityType, Status from discord.ext import commands @@ -287,7 +287,7 @@ async def debug(self, ctx): @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing async def debug_hastebin(self, ctx): - """Stores application-logs to Hastebin.""" + """Posts application-logs to Hastebin.""" haste_url = os.environ.get('HASTE_URL', 'https://hasteb.in') @@ -353,7 +353,7 @@ async def github(self, ctx): @trigger_typing async def update(self, ctx, *, flag: str = ''): """ - Update bot. + Update Modmail. This only works for Heroku users who have configured their bot for updates. @@ -1015,7 +1015,7 @@ async def permissions_add_level(self, ctx, level: str, *, invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove(self, ctx): - """Remove a permission to use a command or permission level.""" + """Remove permission to use a command or permission level.""" await ctx.send_help(ctx.command) @permissions_remove.command(name='command') From 37d30f12e2de63d76f57e2b58f6efa0399f22367 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 18 May 2019 14:42:54 +1000 Subject: [PATCH 0093/1221] Add snippet variables --- bot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bot.py b/bot.py index 3cb9181480..2cbf6e191b 100644 --- a/bot.py +++ b/bot.py @@ -671,6 +671,11 @@ async def on_message(self, message): if cmd in self.snippets: message.content = f'{prefix}reply {self.snippets[cmd]}' + snippet = self.snippets[cmd] + if thread: + snippet = snippet.format(recipient=thread.recipient) + message.content = f'{prefix}reply {snippet}' + ctx = await self.get_context(message) if ctx.command: return await self.invoke(ctx) From 61b77e74237cdbf1eef51ac006a99cdf542c973a Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 18 May 2019 22:08:05 +1000 Subject: [PATCH 0094/1221] Fix permissions update on bot setup --- cogs/modmail.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 84c4ceb3c2..0911075387 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -67,7 +67,8 @@ async def setup(self, ctx): 'Consider setting permission groups to give access ' 'to roles or users the ability to use Modmail.\n' f'Type `{self.bot.prefix}permissions` for more info.') - if not self.bot.config.permissions: + + if not self.bot.config.get('permissions'): await self.bot.update_perms(PermissionLevel.REGULAR, -1) await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id) @@ -543,6 +544,7 @@ async def anonreply(self, ctx, *, msg: str=''): async with ctx.typing(): await ctx.thread.reply(ctx.message, anonymous=True) + @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() From 2cd8ef7a26ad33817ea8064775b99ee2500129ae Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 18 May 2019 22:08:53 +1000 Subject: [PATCH 0095/1221] Fix snippet variables --- bot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 2cbf6e191b..5181638b7c 100644 --- a/bot.py +++ b/bot.py @@ -669,8 +669,7 @@ async def on_message(self, message): if message.content.startswith(prefix): cmd = message.content[len(prefix):].strip() if cmd in self.snippets: - message.content = f'{prefix}reply {self.snippets[cmd]}' - + thread = await self.threads.find(channel=message.channel) snippet = self.snippets[cmd] if thread: snippet = snippet.format(recipient=thread.recipient) From 17e8d8d21215037f0e69ee33c2a7e6fe5f1f35ca Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 01:41:09 +1000 Subject: [PATCH 0096/1221] Add interface for devs to listen to thread creation events --- core/thread.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/core/thread.py b/core/thread.py index 458c195205..4030c68391 100644 --- a/core/thread.py +++ b/core/thread.py @@ -35,6 +35,7 @@ def __init__(self, manager: 'ThreadManager', self._id = recipient.id self._recipient = recipient self._channel = channel + self.genesis_message = None self._ready_event = asyncio.Event() self._close_task = None @@ -80,6 +81,8 @@ def ready(self, flag): async def setup(self, *, creator=None, category=None): """Create the thread channel and other io related initialisation tasks""" + self.bot.dispatch('thread_create', self) + recipient = self.recipient # in case it creates a channel outside of category @@ -124,17 +127,19 @@ async def setup(self, *, creator=None, category=None): else: mention = self.bot.config.get('mention', '@here') - async def send_info_embed(): + async def send_genesis_message(): try: msg = await channel.send(mention, embed=info_embed) - await msg.pin() + self.bot.loop.create_task(msg.pin()) + self.genesis_message = msg except: pass + finally: + self.ready = True + self.bot.dispatch('thread_ready', self) await channel.edit(topic=topic) - self.bot.loop.create_task(send_info_embed()) - - self.ready = True + self.bot.loop.create_task(send_genesis_message()) # Once thread is ready, tell the recipient. thread_creation_response = self.bot.config.get( From e395d9c3a6663cb5b0de774d2ff3b54bbe77c993 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 18 May 2019 10:44:58 -0700 Subject: [PATCH 0097/1221] Remove abc --- bot.py | 74 +++++---- cogs/modmail.py | 4 +- cogs/plugins.py | 4 +- cogs/utility.py | 4 +- core/changelog.py | 8 +- core/clients.py | 48 +++--- core/config.py | 32 ++-- core/models.py | 385 +--------------------------------------------- core/thread.py | 79 ++++++---- 9 files changed, 148 insertions(+), 490 deletions(-) diff --git a/bot.py b/bot.py index 59b624ed91..ef67d32dae 100644 --- a/bot.py +++ b/bot.py @@ -29,6 +29,7 @@ import os import re import sys +import typing from datetime import datetime from pkg_resources import parse_version @@ -49,7 +50,7 @@ from core.clients import DatabaseClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import info, error, human_join -from core.models import Bot, PermissionLevel +from core.models import PermissionLevel from core.thread import ThreadManager from core.time import human_timedelta @@ -93,7 +94,7 @@ def format(self, record): Style.RESET_ALL -class ModmailBot(Bot): +class ModmailBot(commands.Bot): def __init__(self): super().__init__(command_prefix=None) # implemented in `get_prefix` @@ -101,6 +102,8 @@ def __init__(self): self._session = None self._config = None self._db = None + self.start_time = datetime.utcnow() + self._connected = asyncio.Event() self._configure_logging() # TODO: Raise fatal error if mongo_uri or other essentials are not found @@ -112,6 +115,20 @@ def __init__(self): self.autoupdate_task = self.loop.create_task(self.autoupdate_loop()) self._load_extensions() + @property + def uptime(self) -> str: + now = datetime.utcnow() + delta = now - self.start_time + hours, remainder = divmod(int(delta.total_seconds()), 3600) + minutes, seconds = divmod(remainder, 60) + days, hours = divmod(hours, 24) + + fmt = '{h}h {m}m {s}s' + if days: + fmt = '{d}d ' + fmt + + return fmt.format(d=days, h=hours, m=minutes, s=seconds) + def _configure_logging(self): level_text = self.config.log_level.upper() logging_levels = { @@ -133,31 +150,31 @@ def _configure_logging(self): logger.info(info('Using default logging level: INFO')) @property - def version(self): + def version(self) -> str: return __version__ @property - def db(self): + def db(self) -> typing.Optional[AsyncIOMotorClient]: return self._db @property - def api(self): + def api(self) -> DatabaseClient: return self._api @property - def config(self): + def config(self) -> ConfigManager: if self._config is None: self._config = ConfigManager(self) return self._config @property - def session(self): + def session(self) -> ClientSession: if self._session is None: self._session = ClientSession(loop=self.loop) return self._session @property - def threads(self): + def threads(self) -> ThreadManager: if self._threads is None: self._threads = ThreadManager(self) return self._threads @@ -220,13 +237,13 @@ def run(self, *args, **kwargs): self.loop.close() logger.info(error(' - Shutting down bot - ')) - async def is_owner(self, user): + async def is_owner(self, user: discord.User) -> bool: raw = str(self.config.get('owners', '0')).split(',') allowed = {int(x) for x in raw} return (user.id in allowed) or await super().is_owner(user) @property - def log_channel(self): + def log_channel(self) -> typing.Optional[discord.TextChannel]: channel_id = self.config.get('log_channel_id') if channel_id is not None: return self.get_channel(int(channel_id)) @@ -235,23 +252,23 @@ def log_channel(self): return None @property - def snippets(self): + def snippets(self) -> typing.Dict[str, str]: return {k: v for k, v in self.config.get('snippets', {}).items() if v} @property - def aliases(self): + def aliases(self) -> typing.Dict[str, str]: return {k: v for k, v in self.config.get('aliases', {}).items() if v} @property - def token(self): + def token(self) -> str: return self.config.token @property - def guild_id(self): + def guild_id(self) -> int: return int(self.config.guild_id) @property - def guild(self): + def guild(self) -> discord.Guild: """ The guild that the bot is serving (the server where users message it from) @@ -259,7 +276,7 @@ def guild(self): return discord.utils.get(self.guilds, id=self.guild_id) @property - def modmail_guild(self): + def modmail_guild(self) -> discord.Guild: """ The guild that the bot is operating in (where the bot is creating threads) @@ -270,11 +287,11 @@ def modmail_guild(self): return discord.utils.get(self.guilds, id=int(modmail_guild_id)) @property - def using_multiple_server_setup(self): + def using_multiple_server_setup(self) -> bool: return self.modmail_guild != self.guild @property - def main_category(self): + def main_category(self) -> typing.Optional[discord.TextChannel]: category_id = self.config.get('main_category_id') if category_id is not None: return discord.utils.get(self.modmail_guild.categories, @@ -286,15 +303,15 @@ def main_category(self): return None @property - def blocked_users(self): + def blocked_users(self) -> typing.Dict[str, str]: return self.config.get('blocked', {}) @property - def prefix(self): + def prefix(self) -> str: return self.config.get('prefix', '?') @property - def mod_color(self): + def mod_color(self) -> typing.Union[discord.Color, int]: color = self.config.get('mod_color') if not color: return discord.Color.green() @@ -307,7 +324,7 @@ def mod_color(self): return color @property - def recipient_color(self): + def recipient_color(self) -> typing.Union[discord.Color, int]: color = self.config.get('recipient_color') if not color: return discord.Color.gold() @@ -320,7 +337,7 @@ def recipient_color(self): return color @property - def main_color(self): + def main_color(self) -> typing.Union[discord.Color, int]: color = self.config.get('main_color') if not color: return discord.Color.blurple() @@ -414,7 +431,7 @@ async def on_ready(self): logger.info(LINE) - async def convert_emoji(self, name): + async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() @@ -425,7 +442,7 @@ async def convert_emoji(self, name): logger.warning(f'{name} is not a valid emoji.') return name - async def retrieve_emoji(self): + async def retrieve_emoji(self) -> typing.Tuple[str, str]: # TODO: use a function to convert emojis @@ -461,7 +478,7 @@ async def retrieve_emoji(self): return sent_emoji, blocked_emoji - async def process_modmail(self, message): + async def process_modmail(self, message: discord.Message) -> None: """Processes messages sent to the bot.""" sent_emoji, blocked_emoji = await self.retrieve_emoji() now = datetime.utcnow() @@ -631,7 +648,8 @@ async def get_context(self, message, *, cls=commands.Context): return ctx - async def update_perms(self, name, value, add=True): + async def update_perms(self, name: typing.Union[PermissionLevel, str], + value: int, add: bool = True) -> None: if isinstance(name, PermissionLevel): permissions = self.config.level_permissions name = name.name @@ -862,7 +880,7 @@ async def on_command_error(self, ctx, exception): logger.error(error('Unexpected exception:'), exc_info=exception) @staticmethod - def overwrites(ctx): + def overwrites(ctx: commands.Context) -> dict: """Permission overwrites for the guild.""" overwrites = { ctx.guild.default_role: discord.PermissionOverwrite( diff --git a/cogs/modmail.py b/cogs/modmail.py index 26001c82fc..72d0ec5ce5 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -12,7 +12,7 @@ from core import checks from core.decorators import trigger_typing -from core.models import Bot, PermissionLevel +from core.models import PermissionLevel from core.paginator import PaginatorSession from core.time import UserFriendlyTime, human_timedelta from core.utils import format_preview, User @@ -21,7 +21,7 @@ class Modmail(commands.Cog): """Commands directly related to Modmail functionality.""" - def __init__(self, bot: Bot): + def __init__(self, bot): self.bot = bot @commands.command() diff --git a/cogs/plugins.py b/cogs/plugins.py index 232925deb1..2b42d6d186 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -11,7 +11,7 @@ from discord.utils import async_all from core import checks -from core.models import Bot, PermissionLevel +from core.models import PermissionLevel from core.utils import info, error logger = logging.getLogger('Modmail') @@ -30,7 +30,7 @@ class Plugins(commands.Cog): Learn how to create a plugin yourself here: https://github.com/kyb3r/modmail/wiki/Plugins """ - def __init__(self, bot: Bot): + def __init__(self, bot): self.bot = bot self.bot.loop.create_task(self.download_initial_plugins()) diff --git a/cogs/utility.py b/cogs/utility.py index c9749bc038..b0ddf1c908 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -20,7 +20,7 @@ from core import checks from core.changelog import Changelog from core.decorators import github_access_token_required, trigger_typing -from core.models import Bot, InvalidConfigError, PermissionLevel +from core.models import InvalidConfigError, PermissionLevel from core.paginator import PaginatorSession, MessagePaginatorSession from core.utils import cleanup_code, info, error, User, get_perm_level @@ -166,7 +166,7 @@ async def send_error_message(self, msg): class Utility(commands.Cog): """General commands that provide utility.""" - def __init__(self, bot: Bot): + def __init__(self, bot): self.bot = bot self._original_help_command = bot.help_command self.bot.help_command = ModmailHelpCommand( diff --git a/core/changelog.py b/core/changelog.py index 2620ff1d68..1427670875 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -4,8 +4,6 @@ from discord import Embed, Color -from core.models import Bot - class Version: """ @@ -34,7 +32,7 @@ class Version: General description of the version. """ - def __init__(self, bot: Bot, version: str, lines: str): + def __init__(self, bot, version: str, lines: str): self.bot = bot self.version = version.lstrip('vV') self.lines = [x for x in lines.splitlines() if x] @@ -116,7 +114,7 @@ class Changelog: CHANGELOG_URL = 'https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md' VERSION_REGEX = re.compile(r'# (v\d+\.\d+\.\d+)([\S\s]*?(?=# v|$))') - def __init__(self, bot: Bot, text: str): + def __init__(self, bot, text: str): self.bot = bot self.text = text self.versions = [Version(bot, *m) @@ -137,7 +135,7 @@ def embeds(self) -> List[Embed]: return [v.embed for v in self.versions] @classmethod - async def from_url(cls, bot: Bot, url: str = '') -> 'Changelog': + async def from_url(cls, bot, url: str = '') -> 'Changelog': """ Create a `Changelog` from a URL. diff --git a/core/clients.py b/core/clients.py index bb7bb37fb2..9106e5d52e 100644 --- a/core/clients.py +++ b/core/clients.py @@ -5,12 +5,11 @@ from json import JSONDecodeError from typing import Union, Optional -from discord import Member, DMChannel +from discord import Member, DMChannel, TextChannel, Message from discord.ext import commands from aiohttp import ClientResponseError, ClientResponse -from core.models import Bot, UserClient from core.utils import info logger = logging.getLogger('Modmail') @@ -39,7 +38,7 @@ class ApiClient: The HTTP headers that will be sent along with the requiest. """ - def __init__(self, bot: Bot): + def __init__(self, bot): self.bot = bot self.session = bot.session self.headers: dict = None @@ -161,7 +160,7 @@ class GitHub(ApiClient): FORK_URL = REPO + '/forks' STAR_URL = BASE + '/user/starred/kyb3r/modmail' - def __init__(self, bot: Bot, + def __init__(self, bot, access_token: str = '', username: str = '', **kwargs): @@ -233,7 +232,7 @@ async def star_repository(self) -> None: headers={'Content-Length': '0'}) @classmethod - async def login(cls, bot: Bot) -> 'GitHub': + async def login(cls, bot) -> 'GitHub': """ Logs in to GitHub with configuration variable information. @@ -256,9 +255,9 @@ async def login(cls, bot: Bot) -> 'GitHub': return self -class DatabaseClient(UserClient, ApiClient): +class DatabaseClient(ApiClient): - def __init__(self, bot: Bot): + def __init__(self, bot): super().__init__(bot) if self.token: self.headers = { @@ -266,7 +265,7 @@ def __init__(self, bot: Bot): } @property - def token(self): + def token(self) -> Optional[str]: return self.bot.config.get('github_access_token') @property @@ -277,7 +276,7 @@ def db(self): def logs(self): return self.db.logs - async def get_user_logs(self, user_id): + async def get_user_logs(self, user_id: Union[str, int]) -> list: query = { 'recipient.id': str(user_id), 'guild_id': str(self.bot.guild_id) @@ -288,14 +287,17 @@ async def get_user_logs(self, user_id): } return await self.logs.find(query, projection).to_list(None) - async def get_log(self, channel_id): + async def get_log(self, channel_id: Union[str, int]) -> dict: return await self.logs.find_one({'channel_id': str(channel_id)}) - async def get_log_link(self, channel_id): + async def get_log_link(self, channel_id: Union[str, int]) -> str: doc = await self.get_log(channel_id) return f"{self.bot.config.log_url.strip('/')}{prefix}/{doc['key']}" - async def create_log_entry(self, recipient, channel, creator): + async def create_log_entry(self, + recipient: Member, + channel: TextChannel, + creator: Member) -> str: key = secrets.token_hex(6) await self.logs.insert_one({ @@ -325,14 +327,14 @@ async def create_log_entry(self, recipient, channel, creator): return f"{self.bot.config.log_url.strip('/')}{prefix}/{key}" - async def get_config(self): + async def get_config(self) -> dict: conf = await self.db.config.find_one({'bot_id': self.bot.user.id}) if conf is None: await self.db.config.insert_one({'bot_id': self.bot.user.id}) return {'bot_id': self.bot.user.id} return conf - async def update_config(self, data): + async def update_config(self, data: dict): valid_keys = self.bot.config.valid_keys.difference( self.bot.config.protected_keys ) @@ -345,7 +347,8 @@ async def update_config(self, data): {'$set': toset, '$unset': unset} ) - async def edit_message(self, message_id, new_content): + async def edit_message(self, message_id: Union[int, str], + new_content: str) -> None: await self.logs.update_one({ 'messages.message_id': str(message_id) }, { @@ -355,7 +358,10 @@ async def edit_message(self, message_id, new_content): } }) - async def append_log(self, message, channel_id='', type_='thread_message'): + async def append_log(self, + message: Message, + channel_id: Union[str, int] = '', + type_: str = 'thread_message') -> dict: channel_id = str(channel_id) or str(message.channel.id) data = { 'timestamp': str(message.created_at), @@ -386,14 +392,16 @@ async def append_log(self, message, channel_id='', type_='thread_message'): return_document=True ) - async def post_log(self, channel_id, data): + 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 ) - async def update_repository(self): + async def update_repository(self) -> dict: user = await GitHub.login(self.bot) data = await user.update_repository() return { @@ -405,7 +413,7 @@ async def update_repository(self): } } - async def get_user_info(self): + async def get_user_info(self) -> dict: user = await GitHub.login(self.bot) return { 'user': { @@ -417,7 +425,7 @@ async def get_user_info(self): class PluginDatabaseClient: - def __init__(self, bot: Bot): + def __init__(self, bot): self.bot = bot def get_partition(self, cog): diff --git a/core/config.py b/core/config.py index 43ba3c8fcc..0dd1fc4049 100644 --- a/core/config.py +++ b/core/config.py @@ -1,17 +1,18 @@ import asyncio import json import os +import typing import isodate from discord.ext.commands import BadArgument from core._color_data import ALL_COLORS -from core.models import Bot, ConfigManagerABC, InvalidConfigError +from core.models import InvalidConfigError from core.time import UserFriendlyTime -class ConfigManager(ConfigManagerABC): +class ConfigManager: allowed_to_change_in_command = { # activity @@ -77,7 +78,7 @@ class ConfigManager(ConfigManagerABC): valid_keys = allowed_to_change_in_command | internal_keys | protected_keys - def __init__(self, bot: Bot): + def __init__(self, bot): self.bot = bot self._cache = {} self._ready_event = asyncio.Event() @@ -91,18 +92,18 @@ def api(self): return self.bot.api @property - def ready_event(self): + def ready_event(self) -> asyncio.Event: return self._ready_event @property - def cache(self): + def cache(self) -> dict: return self._cache @cache.setter - def cache(self, val): + def cache(self, val: dict): self._cache = val - def populate_cache(self): + def populate_cache(self) -> dict: data = { 'snippets': {}, 'plugins': [], @@ -130,7 +131,8 @@ def populate_cache(self): } return self.cache - async def clean_data(self, key, val): + async def clean_data(self, key: str, + val: typing.Any) -> typing.Tuple[str, str]: value_text = val clean_value = val @@ -176,31 +178,31 @@ async def clean_data(self, key, val): return clean_value, value_text - async def update(self, data=None): + async def update(self, data: typing.Optional[dict] = None) -> dict: """Updates the config with data from the cache""" if data is not None: self.cache.update(data) await self.api.update_config(self.cache) return self.cache - async def refresh(self): + async def refresh(self) -> dict: """Refreshes internal cache with data from database""" data = await self.api.get_config() self.cache.update(data) self.ready_event.set() return self.cache - async def wait_until_ready(self): + async def wait_until_ready(self) -> None: await self.ready_event.wait() - def __getattr__(self, value): + def __getattr__(self, value: str) -> typing.Any: return self.cache[value] - def __setitem__(self, key, item): + def __setitem__(self, key: str, item: typing.Any) -> None: self.cache[key] = item - def __getitem__(self, key): + def __getitem__(self, key: str) -> typing.Any: return self.cache[key] - def get(self, key, default=None): + def get(self, key: str, default: typing.Any = None) -> typing.Any: return self.cache.get(key, default) diff --git a/core/models.py b/core/models.py index 0d35fb5a9f..2995996e36 100644 --- a/core/models.py +++ b/core/models.py @@ -1,16 +1,8 @@ -import abc -import asyncio -import typing -from datetime import datetime from enum import IntEnum -from discord import Color, Member, User, CategoryChannel, DMChannel, Embed -from discord import Message, TextChannel, Guild +from discord import Color, Embed from discord.ext import commands -from aiohttp import ClientSession -from motor.motor_asyncio import AsyncIOMotorClient - class PermissionLevel(IntEnum): OWNER = 5 @@ -24,381 +16,6 @@ class PermissionLevel(IntEnum): INVALID = -1 -class Bot(abc.ABC, commands.Bot): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.start_time = datetime.utcnow() - self._connected = asyncio.Event() - - @property - def uptime(self) -> str: - now = datetime.utcnow() - delta = now - self.start_time - hours, remainder = divmod(int(delta.total_seconds()), 3600) - minutes, seconds = divmod(remainder, 60) - days, hours = divmod(hours, 24) - - fmt = '{h}h {m}m {s}s' - if days: - fmt = '{d}d ' + fmt - - return fmt.format(d=days, h=hours, m=minutes, s=seconds) - - @property - @abc.abstractmethod - def version(self) -> str: - raise NotImplementedError - - @property - @abc.abstractmethod - def db(self) -> typing.Optional[AsyncIOMotorClient]: - raise NotImplementedError - - @property - @abc.abstractmethod - def config(self) -> 'ConfigManagerABC': - raise NotImplementedError - - @property - @abc.abstractmethod - def session(self) -> ClientSession: - raise NotImplementedError - - @property - @abc.abstractmethod - def api(self) -> 'UserClient': - raise NotImplementedError - - @property - @abc.abstractmethod - def threads(self) -> 'ThreadManagerABC': - raise NotImplementedError - - @property - @abc.abstractmethod - def log_channel(self) -> typing.Optional[TextChannel]: - raise NotImplementedError - - @property - @abc.abstractmethod - def snippets(self) -> typing.Dict[str, str]: - raise NotImplementedError - - @property - @abc.abstractmethod - def aliases(self) -> typing.Dict[str, str]: - raise NotImplementedError - - @property - @abc.abstractmethod - def token(self) -> str: - raise NotImplementedError - - @property - @abc.abstractmethod - def guild_id(self) -> int: - raise NotImplementedError - - @property - @abc.abstractmethod - def guild(self) -> typing.Optional[Guild]: - raise NotImplementedError - - @property - @abc.abstractmethod - def modmail_guild(self) -> typing.Optional[Guild]: - raise NotImplementedError - - @property - @abc.abstractmethod - def using_multiple_server_setup(self) -> bool: - raise NotImplementedError - - @property - @abc.abstractmethod - def main_category(self) -> typing.Optional[TextChannel]: - raise NotImplementedError - - @property - @abc.abstractmethod - def blocked_users(self) -> typing.Dict[str, str]: - raise NotImplementedError - - @property - @abc.abstractmethod - def prefix(self) -> str: - raise NotImplementedError - - @property - @abc.abstractmethod - def mod_color(self) -> typing.Union[Color, int]: - raise NotImplementedError - - @property - @abc.abstractmethod - def recipient_color(self) -> typing.Union[Color, int]: - raise NotImplementedError - - @property - @abc.abstractmethod - def main_color(self) -> typing.Union[Color, int]: - raise NotImplementedError - - @abc.abstractmethod - async def process_modmail(self, message: Message) -> None: - raise NotImplementedError - - @abc.abstractmethod - async def convert_emoji(self, name: str) -> str: - raise NotImplementedError - - @abc.abstractmethod - async def update_perms(self, name: typing.Union[PermissionLevel, str], - value: int, add: bool = True) -> None: - raise NotImplementedError - - @staticmethod - @abc.abstractmethod - def overwrites(ctx: commands.Context) -> dict: - raise NotImplementedError - - -class UserClient(abc.ABC): - @property - @abc.abstractmethod - def token(self) -> typing.Optional[str]: - raise NotImplementedError - - @abc.abstractmethod - async def get_user_info(self) -> dict: - raise NotImplementedError - - @abc.abstractmethod - async def update_repository(self) -> dict: - raise NotImplementedError - - @abc.abstractmethod - async def get_user_logs(self, user_id: typing.Union[str, int]) -> list: - raise NotImplementedError - - @abc.abstractmethod - async def get_log(self, channel_id: typing.Union[str, int]) -> dict: - raise NotImplementedError - - @abc.abstractmethod - async def get_log_link(self, channel_id: typing.Union[str, int]) -> str: - raise NotImplementedError - - @abc.abstractmethod - async def get_config(self) -> dict: - raise NotImplementedError - - @abc.abstractmethod - async def update_config(self, data: dict): - raise NotImplementedError - - @abc.abstractmethod - async def create_log_entry(self, - recipient: Member, - channel: TextChannel, - creator: Member) -> str: - raise NotImplementedError - - @abc.abstractmethod - async def append_log(self, - message: Message, - channel_id: typing.Union[str, int] = '', - type_: str = 'thread_message') -> dict: - raise NotImplementedError - - @abc.abstractmethod - async def post_log(self, - channel_id: typing.Union[int, str], - data: dict) -> dict: - raise NotImplementedError - - @abc.abstractmethod - async def edit_message(self, message_id: typing.Union[int, str], - new_content: str) -> None: - raise NotImplementedError - - -class ConfigManagerABC(abc.ABC): - @property - @abc.abstractmethod - def api(self) -> 'UserClient': - raise NotImplementedError - - @abc.abstractmethod - def populate_cache(self) -> dict: - raise NotImplementedError - - @property - @abc.abstractmethod - def ready_event(self) -> asyncio.Event: - raise NotImplementedError - - @property - @abc.abstractmethod - def cache(self) -> dict: - raise NotImplementedError - - @cache.setter - @abc.abstractmethod - def cache(self, val: dict): - raise NotImplementedError - - @abc.abstractmethod - async def clean_data(self, key: str, - val: typing.Any) -> typing.Tuple[str, str]: - raise NotImplementedError - - @abc.abstractmethod - async def update(self, data: typing.Optional[dict] = None) -> dict: - raise NotImplementedError - - @abc.abstractmethod - async def refresh(self) -> dict: - raise NotImplementedError - - @abc.abstractmethod - async def wait_until_ready(self) -> None: - raise NotImplementedError - - @abc.abstractmethod - def get(self, key: str, default: typing.Any = None): - raise NotImplementedError - - @abc.abstractmethod - def __getattr__(self, value: str) -> typing.Any: - raise NotImplementedError - - @abc.abstractmethod - def __setitem__(self, key: str, item: typing.Any) -> None: - raise NotImplementedError - - @abc.abstractmethod - def __getitem__(self, key: str) -> typing.Any: - raise NotImplementedError - - -class ThreadABC(abc.ABC): - @abc.abstractmethod - async def wait_until_ready(self) -> None: - raise NotImplementedError - - @property - @abc.abstractmethod - def id(self) -> int: - raise NotImplementedError - - @property - @abc.abstractmethod - def channel(self) -> typing.Union[TextChannel, DMChannel]: - raise NotImplementedError - - @property - @abc.abstractmethod - def recipient(self) -> typing.Optional[typing.Union[User, Member]]: - raise NotImplementedError - - @property - @abc.abstractmethod - def ready(self) -> bool: - raise NotImplementedError - - @ready.setter - @abc.abstractmethod - def ready(self, flag: bool): - raise NotImplementedError - - @property - @abc.abstractmethod - def close_task(self) -> asyncio.TimerHandle: - raise NotImplementedError - - @close_task.setter - def close_task(self, val: asyncio.TimerHandle): - raise NotImplementedError - - @abc.abstractmethod - async def close(self, *, closer: typing.Union[Member, User], - after: int = 0, - silent: bool = False, - delete_channel: bool = True, - message: str = None) -> None: - raise NotImplementedError - - @abc.abstractmethod - async def cancel_closure(self) -> None: - raise NotImplementedError - - @abc.abstractmethod - async def edit_message(self, message_id: typing.Union[int, str], - message: str) -> None: - raise NotImplementedError - - @abc.abstractmethod - async def note(self, message: Message) -> None: - raise NotImplementedError - - @abc.abstractmethod - async def reply(self, message: Message, - anonymous: bool = False) -> None: - raise NotImplementedError - - @abc.abstractmethod - async def send(self, message: Message, - destination: typing.Union[TextChannel, DMChannel, - User, Member] = None, - from_mod: bool = False, - note: bool = False, - anonymous: bool = False) -> None: - raise NotImplementedError - - @abc.abstractmethod - def get_notifications(self) -> str: - raise NotImplementedError - - -class ThreadManagerABC(abc.ABC): - @abc.abstractmethod - async def populate_cache(self) -> None: - raise NotImplementedError - - @abc.abstractmethod - def __len__(self) -> int: - raise NotImplementedError - - @abc.abstractmethod - def __iter__(self) -> typing.Iterator: - raise NotImplementedError - - @abc.abstractmethod - def __getitem__(self, item: str) -> 'ThreadABC': - raise NotImplementedError - - @abc.abstractmethod - async def find(self, *, - recipient: typing.Union[Member, User] = None, - channel: TextChannel = None, - recipient_id: int = None) -> \ - typing.Optional['ThreadABC']: - raise NotImplementedError - - @abc.abstractmethod - def create(self, recipient: typing.Union[Member, User], *, - creator: typing.Union[Member, User] = None, - category: CategoryChannel = None) -> 'ThreadABC': - raise NotImplementedError - - @abc.abstractmethod - async def find_or_create(self, - recipient: typing.Union[Member, User]) \ - -> 'ThreadABC': - raise NotImplementedError - - class InvalidConfigError(commands.BadArgument): def __init__(self, msg, *args): super().__init__(msg, *args) diff --git a/core/thread.py b/core/thread.py index 9296be28b9..3c0eb830d4 100644 --- a/core/thread.py +++ b/core/thread.py @@ -10,14 +10,13 @@ import discord from discord.ext.commands import MissingRequiredArgument, CommandError -from core.models import Bot, ThreadManagerABC, ThreadABC from core.utils import is_image_url, days, match_user_id from core.utils import truncate, ignore, error logger = logging.getLogger('Modmail') -class Thread(ThreadABC): +class Thread: """Represents a discord Modmail thread""" def __init__(self, manager: 'ThreadManager', @@ -42,36 +41,36 @@ def __repr__(self): return (f'Thread(recipient="{self.recipient or self.id}", ' f'channel={self.channel.id})') - async def wait_until_ready(self): + async def wait_until_ready(self) -> None: """Blocks execution until the thread is fully set up.""" await self._ready_event.wait() @property - def id(self): + def id(self) -> int: return self._id @property - def close_task(self): + def close_task(self) -> asyncio.TimerHandle: return self._close_task @close_task.setter - def close_task(self, val): + def close_task(self, val: asyncio.TimerHandle): self._close_task = val @property - def channel(self): + def channel(self) -> typing.Union[discord.TextChannel, discord.DMChannel]: return self._channel @property - def recipient(self): + def recipient(self) -> typing.Optional[typing.Union[discord.User, discord.Member]]: return self._recipient @property - def ready(self): + def ready(self) -> bool: return self._ready_event.is_set() @ready.setter - def ready(self, flag): + def ready(self, flag: bool): if flag: self._ready_event.set() else: @@ -94,7 +93,7 @@ async def setup(self, *, creator=None, category=None): overwrites = None channel = await self.bot.modmail_guild.create_text_channel( - name=self.manager._format_channel_name(recipient), + name=self.manager.format_channel_name(recipient), category=category, overwrites=overwrites, reason='Creating a thread channel' @@ -114,9 +113,9 @@ async def setup(self, *, creator=None, category=None): log_url = log_count = None # ensure core functionality still works - info_embed = self.manager._format_info_embed(recipient, log_url, - log_count, - discord.Color.green()) + info_embed = self.manager.format_info_embed(recipient, log_url, + log_count, + discord.Color.green()) topic = f'User ID: {recipient.id}' if creator: @@ -169,8 +168,11 @@ def _close_after(self, closer, silent, delete_channel, message): self._close(closer, silent, delete_channel, message, True) ) - async def close(self, *, closer, after=0, silent=False, - delete_channel=True, message=None): + async def close(self, *, closer: typing.Union[discord.Member, discord.User], + after: int = 0, + silent: bool = False, + delete_channel: bool = True, + message: str = None) -> None: """Close a thread now or after a set time in seconds""" # restarts the after timer @@ -304,7 +306,7 @@ async def _close(self, closer, silent=False, delete_channel=True, await asyncio.gather(*tasks) - async def cancel_closure(self): + async def cancel_closure(self) -> None: if self.close_task is not None: self.close_task.cancel() self.close_task = None @@ -323,7 +325,8 @@ async def _find_thread_message(channel, message_id): if str(message_id) == str(embed.author.url).split('/')[-1]: return msg - async def edit_message(self, message_id, message): + async def edit_message(self, message_id: typing.Union[int, str], + message: str) -> None: msg_recipient, msg_channel = await asyncio.gather( self._find_thread_message(self.recipient, message_id), self._find_thread_message(self.channel, message_id) @@ -348,7 +351,7 @@ async def delete_message(self, message_id): msg_channel.delete() ) - async def note(self, message): + async def note(self, message: discord.Message) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(param(name='msg')) @@ -359,7 +362,8 @@ async def note(self, message): self.send(message, self.channel, note=True) ) - async def reply(self, message, anonymous=False): + async def reply(self, message: discord.Message, + anonymous: bool = False) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(param(name='msg')) if all(not g.get_member(self.id) for g in self.bot.guilds): @@ -417,8 +421,12 @@ async def reply(self, message, anonymous=False): await asyncio.gather(*tasks) - async def send(self, message, destination=None, - from_mod=False, note=False, anonymous=False): + async def send(self, message: discord.Message, + destination: typing.Union[discord.TextChannel, discord.DMChannel, + discord.User, discord.Member] = None, + from_mod: bool = False, + note: bool = False, + anonymous: bool = False) -> None: if self.close_task is not None: # cancel closing if a thread message is sent. self.bot.loop.create_task( @@ -569,7 +577,7 @@ async def send(self, message, destination=None, if delete_message: self.bot.loop.create_task(ignore(message.delete())) - def get_notifications(self): + def get_notifications(self) -> str: config = self.bot.config key = str(self.id) @@ -584,14 +592,14 @@ def get_notifications(self): return ' '.join(mentions) -class ThreadManager(ThreadManagerABC): +class ThreadManager: """Class that handles storing, finding and creating Modmail threads.""" - def __init__(self, bot: Bot): + def __init__(self, bot): self.bot = bot self.cache = {} - async def populate_cache(self): + 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: @@ -604,10 +612,13 @@ def __len__(self): def __iter__(self): return iter(self.cache.values()) - def __getitem__(self, item): + def __getitem__(self, item: str) -> Thread: return self.cache[item] - async def find(self, *, recipient=None, channel=None, recipient_id=None): + async def find(self, *, + recipient: typing.Union[discord.Member, discord.User] = None, + channel: discord.TextChannel = None, + recipient_id: int = None) -> Thread: """Finds a thread from cache or from discord channel topics.""" if recipient is None and channel is not None: return await self._find_from_channel(channel) @@ -672,7 +683,9 @@ async def _find_from_channel(self, channel): return thread - def create(self, recipient, *, creator=None, category=None): + def create(self, recipient: typing.Union[discord.Member, discord.User], *, + creator: typing.Union[discord.Member, discord.User] = None, + category: discord.CategoryChannel = None) -> Thread: """Creates a Modmail thread""" # create thread immediately so messages can be processed thread = Thread(self, recipient) @@ -685,10 +698,12 @@ def create(self, recipient, *, creator=None, category=None): )) return thread - async def find_or_create(self, recipient): + async def find_or_create(self, + recipient: typing.Union[discord.Member, + discord.User]) -> Thread: return await self.find(recipient=recipient) or self.create(recipient) - def _format_channel_name(self, author): + def format_channel_name(self, author): """Sanitises a username for use with text channel names""" name = author.name.lower() allowed = string.ascii_letters + string.digits + '-' @@ -701,7 +716,7 @@ def _format_channel_name(self, author): return new_name - def _format_info_embed(self, user, log_url, log_count, color): + def format_info_embed(self, user, log_url, log_count, color): """Get information about a member of a server supports users from the guild or not.""" member = self.bot.guild.get_member(user.id) From 7e3d1636f7c7d947093432b0280a425f468edfef Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 18 May 2019 10:47:32 -0700 Subject: [PATCH 0098/1221] Rename client --- bot.py | 6 +++--- core/clients.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bot.py b/bot.py index eb88288bad..73d4281d09 100644 --- a/bot.py +++ b/bot.py @@ -47,7 +47,7 @@ from motor.motor_asyncio import AsyncIOMotorClient from core.changelog import Changelog -from core.clients import DatabaseClient, PluginDatabaseClient +from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import info, error, human_join from core.models import PermissionLevel @@ -108,7 +108,7 @@ def __init__(self): self._configure_logging() # TODO: Raise fatal error if mongo_uri or other essentials are not found self._db = AsyncIOMotorClient(self.config.mongo_uri).modmail_bot - self._api = DatabaseClient(self) + self._api = ApiClient(self) self.plugin_db = PluginDatabaseClient(self) self.metadata_task = self.loop.create_task(self.metadata_loop()) @@ -158,7 +158,7 @@ def db(self) -> typing.Optional[AsyncIOMotorClient]: return self._db @property - def api(self) -> DatabaseClient: + def api(self) -> ApiClient: return self._api @property diff --git a/core/clients.py b/core/clients.py index 9106e5d52e..ca929f44fc 100644 --- a/core/clients.py +++ b/core/clients.py @@ -19,7 +19,7 @@ prefix = '' -class ApiClient: +class RequestClient: """ This class represents the general request class for all type of clients. @@ -107,7 +107,7 @@ def filter_valid(self, data): return {k: v for k, v in data.items() if k in valid_keys} -class GitHub(ApiClient): +class GitHub(RequestClient): """ The client for interacting with GitHub API. @@ -255,7 +255,7 @@ async def login(cls, bot) -> 'GitHub': return self -class DatabaseClient(ApiClient): +class ApiClient(RequestClient): def __init__(self, bot): super().__init__(bot) From 933b56d2fa2e2bbb920c2d536684e38f43bc83a2 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 18 May 2019 10:51:05 -0700 Subject: [PATCH 0099/1221] Return of uvloop --- bot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bot.py b/bot.py index 73d4281d09..b8ec982b10 100644 --- a/bot.py +++ b/bot.py @@ -993,5 +993,10 @@ async def metadata_loop(self): if __name__ == '__main__': + if os.name != 'nt': + import uvloop + + uvloop.install() + bot = ModmailBot() bot.run() From af65a09169b11d7c76896f73377290ac96bf77ac Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 18 May 2019 22:38:26 -0700 Subject: [PATCH 0100/1221] Updated changelog, pylint --- .pylintrc | 7 ++++--- CHANGELOG.md | 11 +++++++++++ bot.py | 43 +++++++++++++++++++------------------------ cogs/modmail.py | 7 +++---- cogs/plugins.py | 2 +- cogs/utility.py | 22 +++++++++++----------- core/config.py | 4 ++-- core/utils.py | 3 +-- 8 files changed, 52 insertions(+), 47 deletions(-) diff --git a/.pylintrc b/.pylintrc index f3ed27c42a..d1e6db5252 100644 --- a/.pylintrc +++ b/.pylintrc @@ -79,7 +79,8 @@ disable=raw-checker-failed, too-many-instance-attributes, too-many-arguments, too-few-public-methods, - too-many-lines + too-many-lines, + line-too-long # Enable the message, report, category or checker with the given id(s). You can @@ -361,12 +362,12 @@ good-names=i, k, ex, _, - v, id, db, f, dt, - c + ch, + ts # Include a hint for the correct naming format with invalid-name. include-naming-hint=yes diff --git a/CHANGELOG.md b/CHANGELOG.md index 2603b01f6b..95dcf129c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.20.1 + +### Changes + +This update contains mostly internal changes. + - Implemented support for the new discord.py v1.1.1. + - New error message when using thread-only commands outside of threads. + - Improved help text for most commands. + - Completely revamped help command, few user changes. + - Removed abc (internal). + # v2.20.0 ### What's new? diff --git a/bot.py b/bot.py index b8ec982b10..726c9f135a 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.20.0' +__version__ = '2.20.1' import asyncio import logging @@ -32,7 +32,6 @@ import typing from datetime import datetime -from pkg_resources import parse_version from types import SimpleNamespace import discord @@ -45,6 +44,7 @@ from colorama import init, Fore, Style from emoji import UNICODE_EMOJI from motor.motor_asyncio import AsyncIOMotorClient +from pkg_resources import parse_version from core.changelog import Changelog from core.clients import ApiClient, PluginDatabaseClient @@ -240,7 +240,7 @@ def run(self, *args, **kwargs): async def is_owner(self, user: discord.User) -> bool: raw = str(self.config.get('owners', '0')).split(',') allowed = {int(x) for x in raw} - return (user.id in allowed) or await super().is_owner(user) + return (user.id in allowed) or await super().is_owner(user) @property def log_channel(self) -> typing.Optional[discord.TextChannel]: @@ -518,8 +518,8 @@ async def process_modmail(self, message: discord.Message) -> None: try: min_account_age = message.author.created_at + account_age - except ValueError as e: - logger.warning(e.args[0]) + except ValueError as exc: + logger.warning(exc.args[0]) del self.config.cache['account_age'] await self.config.update() min_account_age = now @@ -530,8 +530,8 @@ async def process_modmail(self, message: discord.Message) -> None: min_guild_age = member.joined_at + guild_age else: min_guild_age = now - except ValueError as e: - logger.warning(e.args[0]) + except ValueError as exc: + logger.warning(exc.args[0]) del self.config.cache['guild_age'] await self.config.update() min_guild_age = now @@ -641,11 +641,6 @@ async def get_context(self, message, *, cls=commands.Context): ctx.prefix = self.prefix # Sane prefix (No mentions) ctx.command = self.all_commands.get(invoker) - has_ai = hasattr(ctx, '_alias_invoked') - if ctx.command is self.get_command('eval') and has_ai: - # ctx.command.checks = None # Let anyone use the command. - pass - return ctx async def update_perms(self, name: typing.Union[PermissionLevel, str], @@ -691,7 +686,7 @@ async def on_message(self, message): if thread: snippet = snippet.format(recipient=thread.recipient) message.content = f'{prefix}reply {snippet}' - + ctx = await self.get_context(message) if ctx.command: return await self.invoke(ctx) @@ -853,29 +848,29 @@ async def on_error(self, event_method, *args, **kwargs): logger.error(error('Ignoring exception in {}'.format(event_method))) logger.error(error('Unexpected exception:'), exc_info=sys.exc_info()) - async def on_command_error(self, ctx, exception): + async def on_command_error(self, context, exception): if isinstance(exception, commands.BadUnionArgument): msg = 'Could not find the specified ' + human_join([c.__name__ for c in exception.converters]) - await ctx.trigger_typing() - await ctx.send(embed=discord.Embed( - color=discord.Color.red(), + await context.trigger_typing() + await context.send(embed=discord.Embed( + color=discord.Color.red(), description=msg )) elif isinstance(exception, commands.BadArgument): - await ctx.trigger_typing() - await ctx.send(embed=discord.Embed( - color=discord.Color.red(), + await context.trigger_typing() + await context.send(embed=discord.Embed( + color=discord.Color.red(), description=str(exception) )) elif isinstance(exception, commands.CommandNotFound): logger.warning(error('CommandNotFound: ' + str(exception))) elif isinstance(exception, commands.MissingRequiredArgument): - await ctx.send_help(ctx.command) + await context.send_help(context.command) elif isinstance(exception, commands.CheckFailure): - for check in ctx.command.checks: - if not await check(ctx) and hasattr(check, 'fail_msg'): - await ctx.send(embed=discord.Embed( + for check in context.command.checks: + if not await check(context) and hasattr(check, 'fail_msg'): + await context.send(embed=discord.Embed( color=discord.Color.red(), description=check.fail_msg )) diff --git a/cogs/modmail.py b/cogs/modmail.py index 2fb473ad9c..468647d723 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -53,7 +53,7 @@ async def setup(self, ctx): embed = discord.Embed( title='Friendly Reminder:', - description='You may use the `config set log_channel_id ' + description=f'You may use the `{self.bot.prefix}config set log_channel_id ' '` command to set up a custom log channel' ', then you can delete the default ' f'{log_channel.mention} channel.', @@ -72,7 +72,7 @@ async def setup(self, ctx): 'Consider setting permission groups to give access ' 'to roles or users the ability to use Modmail.\n' f'Type `{self.bot.prefix}permissions` for more info.') - + if not self.bot.config.get('permissions'): await self.bot.update_perms(PermissionLevel.REGULAR, -1) await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id) @@ -278,7 +278,7 @@ async def notify(self, ctx, *, role: Union[discord.Role, str.lower, None] = None """ thread = ctx.thread - if role is None or role == 'me': + if role is None: mention = ctx.author.mention elif isinstance(role, discord.Role): mention = role.mention @@ -594,7 +594,6 @@ async def anonreply(self, ctx, *, msg: str = ''): async with ctx.typing(): await ctx.thread.reply(ctx.message, anonymous=True) - @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() diff --git a/cogs/plugins.py b/cogs/plugins.py index 2b42d6d186..0f3b8ef6f6 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -180,7 +180,7 @@ async def plugin_remove(self, ctx, *, plugin_name: str): if not any(i.startswith(f'{username}/{repo}') for i in self.bot.config.plugins): # if there are no more of such repos, delete the folder - def onerror(func, path, exc_info): + def onerror(func, path, exc_info): # pylint: disable=W0613 if not os.access(path, os.W_OK): # Is the error an access error? os.chmod(path, stat.S_IWUSR) diff --git a/cogs/utility.py b/cogs/utility.py index b0ddf1c908..bcd61621a6 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -8,7 +8,6 @@ from io import StringIO from typing import Union from json import JSONDecodeError -from pkg_resources import parse_version from textwrap import indent from discord import Embed, Color, Activity, Role @@ -16,6 +15,7 @@ from discord.ext import commands from aiohttp import ClientResponseError +from pkg_resources import parse_version from core import checks from core.changelog import Changelog @@ -57,7 +57,7 @@ async def format_cog_help(self, cog): embed.add_field(name='Commands', value=format_) - continued = ' (Continued)' if len(embeds) > 0 else '' + continued = ' (Continued)' if embeds else '' embed.set_author(name=cog.qualified_name + ' - Help' + continued, icon_url=bot.user.avatar_url) @@ -66,8 +66,8 @@ async def format_cog_help(self, cog): embeds.append(embed) return embeds - def process_help_msg(self, help: str): - return help.format(prefix=self.clean_prefix) + def process_help_msg(self, help_: str): + return help_.format(prefix=self.clean_prefix) async def send_bot_help(self, mapping): embeds = [] @@ -138,7 +138,7 @@ async def send_group_help(self, group): ) await self.get_destination().send(embed=embed) - async def send_error_message(self, msg): + async def send_error_message(self, msg): # pylint: disable=W0221 logger.warning(error(f'CommandNotFound: {msg}')) embed = Embed( @@ -583,9 +583,9 @@ async def set_presence(self, *, presence = {'activity': (None, 'No activity has been set.'), 'status': (None, 'No status has been set.')} if activity is not None: - to = 'to ' if activity.type == ActivityType.listening else '' + use_to = 'to ' if activity.type == ActivityType.listening else '' msg = f'Activity set to: {activity.type.name.capitalize()} ' - msg += f'{to}{activity.name}.' + msg += f'{use_to}{activity.name}.' presence['activity'] = (activity, msg) if status is not None: msg = f'Status set to: {status.value}.' @@ -1276,14 +1276,14 @@ async def oauth_whitelist(self, ctx, target: Union[User, Role]): await self.bot.config.update() - em = Embed(color=self.bot.main_color) - em.title = 'Success' - em.description = ( + embed = Embed(color=self.bot.main_color) + embed.title = 'Success' + embed.description = ( f"{'Un-w' if removed else 'W'}hitelisted " f"{target.mention} to view logs." ) - await ctx.send(embed=em) + await ctx.send(embed=embed) @oauth.command(name='show', aliases=['get', 'list', 'view']) @checks.has_permissions(PermissionLevel.OWNER) diff --git a/core/config.py b/core/config.py index 0dd1fc4049..68dd49d552 100644 --- a/core/config.py +++ b/core/config.py @@ -166,8 +166,8 @@ async def clean_data(self, key: str, time = await converter.convert(None, val) if time.arg: raise ValueError - except BadArgument as e: - raise InvalidConfigError(*e.args) + except BadArgument as exc: + raise InvalidConfigError(*exc.args) except Exception: raise InvalidConfigError( 'Unrecognized time, please use ISO-8601 duration format ' diff --git a/core/utils.py b/core/utils.py index 9bd5b9d219..6c1672936c 100644 --- a/core/utils.py +++ b/core/utils.py @@ -129,8 +129,7 @@ def parse_image_url(url: str) -> str: def human_join(strings): if len(strings) <= 2: return ' or '.join(strings) - else: - return ', '.join(strings[:len(strings) - 1]) + ' or ' + strings[-1] + return ', '.join(strings[:len(strings) - 1]) + ' or ' + strings[-1] def days(day: typing.Union[str, int]) -> str: From 987e36eb77e08ac6b4a526ed1a5629b23d24d1f3 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 18 May 2019 23:04:05 -0700 Subject: [PATCH 0101/1221] Unnotify, notify/sub with users, pylint --- .pylintrc | 1 - CHANGELOG.md | 7 +++- cogs/modmail.py | 101 +++++++++++++++++++++++++++++++++--------------- cogs/utility.py | 30 +++++++------- core/config.py | 6 +-- core/utils.py | 8 ++-- 6 files changed, 98 insertions(+), 55 deletions(-) diff --git a/.pylintrc b/.pylintrc index d1e6db5252..b5a048b89a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -359,7 +359,6 @@ function-naming-style=snake_case # Good variable names which should always be accepted, separated by a comma. good-names=i, j, - k, ex, _, id, diff --git a/CHANGELOG.md b/CHANGELOG.md index 95dcf129c1..d036f786ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # v2.20.1 +### What's new? + + - New error message when using thread-only commands outside of threads. + - `?unnotify`, ability to undo `?notify`. + - `?notify` and `?subscribe` now accepts other users. + ### Changes This update contains mostly internal changes. - Implemented support for the new discord.py v1.1.1. - - New error message when using thread-only commands outside of threads. - Improved help text for most commands. - Completely revamped help command, few user changes. - Removed abc (internal). diff --git a/cogs/modmail.py b/cogs/modmail.py index 468647d723..7d5d4b9a5a 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -266,26 +266,26 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): @commands.command(aliases=['alert']) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def notify(self, ctx, *, role: Union[discord.Role, str.lower, None] = None): + async def notify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): """ - Notify a role or yourself when the next thread message received. + Notify a user, role, or yourself when the next thread message received. - Once a thread message is received, `role` will only be pinged once. + Once a thread message is received, `user_or_role` will only be pinged once. - Leave `role` empty to notify yourself. + Leave `user_or_role` empty to notify yourself. `@here` and `@everyone` can be substituted with `here` and `everyone`. - `role` may be a role ID, mention, name, "everyone", or "here". + `user_or_role` may be a user ID, mention, name. role ID, mention, name, "everyone", or "here". """ thread = ctx.thread - if role is None: + if user_or_role is None: mention = ctx.author.mention - elif isinstance(role, discord.Role): - mention = role.mention - elif role in {'here', 'everyone', '@here', '@everyone'}: - mention = '@' + role.lstrip('@') + elif hasattr(user_or_role, 'mention'): + mention = user_or_role.mention + elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: + mention = '@' + user_or_role.lstrip('@') else: - raise commands.BadArgument(f'{role} is not a valid role.') + raise commands.BadArgument(f'{user_or_role} is not a valid role.') if str(thread.id) not in self.bot.config['notification_squad']: self.bot.config['notification_squad'][str(thread.id)] = [] @@ -304,29 +304,68 @@ async def notify(self, ctx, *, role: Union[discord.Role, str.lower, None] = None 'on the next message received.') return await ctx.send(embed=embed) + @commands.command(aliases=['unalert']) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def unnotify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): + """ + Un-notify a user, role, or yourself from a thread. + + Leave `user_or_role` empty to un-notify yourself. + `@here` and `@everyone` can be substituted with `here` and `everyone`. + `user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here". + """ + thread = ctx.thread + + if user_or_role is None: + mention = ctx.author.mention + elif hasattr(user_or_role, 'mention'): + mention = user_or_role.mention + elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: + mention = '@' + user_or_role.lstrip('@') + else: + mention = f'`{user_or_role}`' + + if str(thread.id) not in self.bot.config['notification_squad']: + self.bot.config['notification_squad'][str(thread.id)] = [] + + mentions = self.bot.config['notification_squad'][str(thread.id)] + + if mention not in mentions: + embed = discord.Embed(color=discord.Color.red(), + description=f'{mention} does not have a ' + 'pending notification.') + else: + mentions.remove(mention) + await self.bot.config.update() + embed = discord.Embed(color=self.bot.main_color, + description=f'{mention} will no longer ' + 'be notified.') + return await ctx.send(embed=embed) + @commands.command(aliases=['sub']) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def subscribe(self, ctx, *, role: Union[discord.Role, str.lower, None] = None): + async def subscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): """ - Notify a role or yourself for every thread message received. + Notify a user, role, or yourself for every thread message received. You will be pinged for every thread message received until you unsubscribe. - Leave `role` empty to subscribe yourself. + Leave `user_or_role` empty to subscribe yourself. `@here` and `@everyone` can be substituted with `here` and `everyone`. - `role` may be a role ID, mention, name, "everyone", or "here". + `user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here". """ thread = ctx.thread - if role is None: + if user_or_role is None: mention = ctx.author.mention - elif isinstance(role, discord.Role): - mention = role.mention - elif role in {'here', 'everyone', '@here', '@everyone'}: - mention = '@' + role.lstrip('@') + elif hasattr(user_or_role, 'mention'): + mention = user_or_role.mention + elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: + mention = '@' + user_or_role.lstrip('@') else: - raise commands.BadArgument(f'{role} is not a valid role.') + raise commands.BadArgument(f'{user_or_role} is not a valid role.') if str(thread.id) not in self.bot.config['subscriptions']: self.bot.config['subscriptions'][str(thread.id)] = [] @@ -350,24 +389,24 @@ async def subscribe(self, ctx, *, role: Union[discord.Role, str.lower, None] = N @commands.command(aliases=['unsub']) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def unsubscribe(self, ctx, *, role=None): + async def unsubscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): """ - Unsubscribe a role or yourself from a thread. + Unsubscribe a user, role, or yourself from a thread. - Leave `role` empty to unsubscribe yourself. + Leave `user_or_role` empty to unsubscribe yourself. `@here` and `@everyone` can be substituted with `here` and `everyone`. - `role` may be a role ID, mention, name, "everyone", or "here". + `user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here". """ thread = ctx.thread - if not role: + if user_or_role is None: mention = ctx.author.mention - elif role.lower() in ('here', 'everyone'): - mention = '@' + role + elif hasattr(user_or_role, 'mention'): + mention = user_or_role.mention + elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: + mention = '@' + user_or_role.lstrip('@') else: - converter = commands.RoleConverter() - role = await converter.convert(ctx, role) - mention = role.mention + mention = f'`{user_or_role}`' if str(thread.id) not in self.bot.config['subscriptions']: self.bot.config['subscriptions'][str(thread.id)] = [] diff --git a/cogs/utility.py b/cogs/utility.py index bcd61621a6..9a95fc2383 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -148,8 +148,8 @@ async def send_error_message(self, msg): # pylint: disable=W0221 choices = set() - for name, c in self.context.bot.all_commands.items(): - if not c.hidden: + for name, cmd in self.context.bot.all_commands.items(): + if not cmd.hidden: choices.add(name) command = self.context.kwargs.get('command') # print(self.context.message.content[self.context.in]) @@ -789,8 +789,8 @@ async def config_get(self, ctx, key: str.lower = None): icon_url=self.bot.user.avatar_url) config = { - k: v for k, v in self.bot.config.cache.items() - if v and k in keys + key: val for key, val in self.bot.config.cache.items() + if val and key in keys } for k, v in reversed(list(config.items())): @@ -1250,7 +1250,7 @@ def get_level(perm_level): p_session = PaginatorSession(ctx, *embeds) return await p_session.run() - + @commands.group(invoke_without_command=True, aliases=['oauth2', 'auth', 'authentication']) @checks.has_permissions(PermissionLevel.OWNER) async def oauth(self, ctx): @@ -1273,7 +1273,7 @@ async def oauth_whitelist(self, ctx, target: Union[User, Role]): else: whitelisted.append(target.id) removed = False - + await self.bot.config.update() embed = Embed(color=self.bot.main_color) @@ -1284,7 +1284,7 @@ async def oauth_whitelist(self, ctx, target: Union[User, Role]): ) await ctx.send(embed=embed) - + @oauth.command(name='show', aliases=['get', 'list', 'view']) @checks.has_permissions(PermissionLevel.OWNER) async def oauth_show(self, ctx): @@ -1294,21 +1294,21 @@ async def oauth_show(self, ctx): users = [] roles = [] - for id in whitelisted: - user = self.bot.get_user(id) + for id_ in whitelisted: + user = self.bot.get_user(id_) if user: users.append(user) - role = self.bot.modmail_guild.get_role(id) + role = self.bot.modmail_guild.get_role(id_) if role: roles.append(role) - em = Embed(color=self.bot.main_color) - em.title = 'Oauth Whitelist' + embed = Embed(color=self.bot.main_color) + embed.title = 'Oauth Whitelist' - em.add_field(name='Users', value=' '.join(u.mention for u in users) or 'None') - em.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=em) + await ctx.send(embed=embed) @commands.command(hidden=True, name='eval') @checks.has_permissions(PermissionLevel.OWNER) diff --git a/core/config.py b/core/config.py index 68dd49d552..10efecb6c6 100644 --- a/core/config.py +++ b/core/config.py @@ -147,9 +147,9 @@ async def clean_data(self, key: str, val = val[1:] if len(val) != 6: raise InvalidConfigError('Invalid color name or hex.') - for v in val: - if v not in {'0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}: + for letter in val: + if letter not in {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}: raise InvalidConfigError('Invalid color name or hex.') clean_value = '#' + val value_text = clean_value diff --git a/core/utils.py b/core/utils.py index 6c1672936c..cbd70e5226 100644 --- a/core/utils.py +++ b/core/utils.py @@ -195,12 +195,12 @@ def match_user_id(text: str) -> int: def get_perm_level(cmd) -> PermissionLevel: - for c in cmd.checks: - perm = getattr(c, 'permission_level', None) + for check in cmd.checks: + perm = getattr(check, 'permission_level', None) if perm is not None: return perm - for c in cmd.checks: - if 'is_owner' in str(c): + for check in cmd.checks: + if 'is_owner' in str(check): return PermissionLevel.OWNER return PermissionLevel.INVALID From 23cf9c51354c4a838f00ae290c8a5fe161c1852b Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 16:30:58 +1000 Subject: [PATCH 0102/1221] Shorten help message to fit on one line --- cogs/modmail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7d5d4b9a5a..46a0c8bf6e 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -268,7 +268,7 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): @checks.thread_only() async def notify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): """ - Notify a user, role, or yourself when the next thread message received. + 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. @@ -905,7 +905,7 @@ async def unblock(self, ctx, *, user: User = None): @checks.thread_only() async def delete(self, ctx, message_id: Optional[int] = None): """ - Delete a message that was sent using the reply or anonreply command. + Delete a message that was sent using the reply command. Deletes the previous message, unless a message ID is provided, which in that case, deletes the message with that message ID. From ca2cc9d54478f872a025aed5e0669e4a1a8326b7 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 16:54:52 +1000 Subject: [PATCH 0103/1221] Add better error messages for db faliures --- bot.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 726c9f135a..ff28d006ce 100644 --- a/bot.py +++ b/bot.py @@ -903,7 +903,23 @@ async def validate_database_connection(self): except Exception as exc: logger.critical(error('Something went wrong ' 'while connecting to the database.')) - logger.critical(error(f'{type(exc).__name__}: {str(exc)}')) + message = f'{type(exc).__name__}: {str(exc)}' + logger.critical(error(message)) + + if 'ServerSelectionTimeoutError' in message: + logger.critical(error( + "This may have been caused by not whitelisting " + "IPs correctly. Make sure to whitelist all " + "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" + )) + + if 'OperationFailure' in message: + logger.critical(error("This is due to having invalid credentials in your MONGO_URI.")) + logger.critical(error( + "Recheck the username/password and make sure to url encode them. " + "https://www.urlencoder.io/" + )) + return await self.logout() else: logger.info(info('Successfully connected to the database.')) From d1261fbe283c0885b89797c0628bad69dbe40ba8 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 17:04:04 +1000 Subject: [PATCH 0104/1221] Fix help for oauth whitelist --- cogs/utility.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 9a95fc2383..7b14d96097 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -136,6 +136,7 @@ async def send_group_help(self, group): text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' 'for more info on a command.' ) + await self.get_destination().send(embed=embed) async def send_error_message(self, msg): # pylint: disable=W0221 @@ -1257,11 +1258,10 @@ async def oauth(self, ctx): """Commands relating to Logviewer oauth2 login authentication.""" await ctx.send_help(ctx.command) - @oauth.group(name='whitelist', invoke_without_command=True) + @oauth.command(name='whitelist') @checks.has_permissions(PermissionLevel.OWNER) async def oauth_whitelist(self, ctx, target: Union[User, Role]): - """ - Whitelist or un-whitelist a user or role to have access to logs. + """Whitelist or un-whitelist a user or role to have access to logs. `target` may be a role ID, name, mention, user ID, name, or mention. """ From 0d48e56a2fdd61ae23dccf1da755e8fd685d9267 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 19:48:49 +1000 Subject: [PATCH 0105/1221] Make debug command start at most recent logs --- cogs/utility.py | 1 + core/paginator.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 7b14d96097..59c696e117 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -282,6 +282,7 @@ async def debug(self, ctx): embed.set_footer(text='Debug logs - Navigate using the reactions below.') session = MessagePaginatorSession(ctx, *messages, embed=embed) + session.current = len(messages) - 1 return await session.run() @debug.command(name='hastebin', aliases=['haste']) diff --git a/core/paginator.py b/core/paginator.py index 6491f402da..aa025694ba 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -148,7 +148,7 @@ async def run(self) -> typing.Optional[Message]: If it's closed before running ends. """ if not self.running: - await self.show_page(0) + await self.show_page(self.current) while self.running: try: reaction, user = await self.ctx.bot.wait_for( @@ -269,7 +269,7 @@ async def create_base(self, msg: str) -> None: The message content to fill the base `Message`. """ if self.embed is not None: - footer_text = f'Page 1 of {len(self.messages)}' + footer_text = f'Page {self.current+1} of {len(self.messages)}' if self.footer_text: footer_text = footer_text + ' • ' + self.footer_text self.embed.set_footer(text=footer_text, @@ -298,7 +298,7 @@ async def show_page(self, index: int) -> None: """ if not 0 <= index < len(self.messages): return - + self.current = index page = self.messages[index] @@ -342,7 +342,7 @@ async def run(self) -> typing.Optional[Message]: If it's closed before running ends. """ if not self.running: - await self.show_page(0) + await self.show_page(self.current) while self.running: try: reaction, user = await self.ctx.bot.wait_for( From 133615fcf2020cccf05f3ef0a3f32cdca88e27b5 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 19:49:00 +1000 Subject: [PATCH 0106/1221] Add a plugin registry --- cogs/plugins.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 0f3b8ef6f6..1709de2e51 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -6,13 +6,17 @@ import stat import subprocess import sys +import json +from pkg_resources import parse_version +import discord from discord.ext import commands from discord.utils import async_all from core import checks from core.models import PermissionLevel from core.utils import info, error +from core.paginator import PaginatorSession logger = logging.getLogger('Modmail') @@ -33,6 +37,8 @@ class Plugins(commands.Cog): def __init__(self, bot): self.bot = bot self.bot.loop.create_task(self.download_initial_plugins()) + with open('plugins/registry.json') as f: + self.registry = json.load(f) @staticmethod def _asubprocess_run(cmd): @@ -128,6 +134,12 @@ async def plugin(self, ctx): @checks.has_permissions(PermissionLevel.OWNER) async def plugin_add(self, ctx, *, plugin_name: str): """Add a plugin.""" + if plugin_name in self.registry: + info = self.registry[plugin_name] + plugin_name = info['repository'] + '/' + plugin_name + required_version = info['bot_version'] + if parse_version(self.bot.version) < parse_version(required_version): + return await ctx.send(f"Bot version too low, plugin requires version `{required_version}`") if plugin_name in self.bot.config.plugins: return await ctx.send('Plugin already installed.') if plugin_name in self.bot.cogs.keys(): @@ -168,6 +180,9 @@ async def plugin_add(self, ctx, *, plugin_name: str): @checks.has_permissions(PermissionLevel.OWNER) async def plugin_remove(self, ctx, *, plugin_name: str): """Remove a plugin.""" + if plugin_name in self.registry: + info = self.registry[plugin_name] + plugin_name = info['repository'] + '/' + plugin_name if plugin_name in self.bot.config.plugins: username, repo, name = self.parse_plugin(plugin_name) self.bot.unload_extension( @@ -203,6 +218,9 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 @checks.has_permissions(PermissionLevel.OWNER) async def plugin_update(self, ctx, *, plugin_name: str): """Update a plugin.""" + if plugin_name in self.registry: + info = self.registry[plugin_name] + plugin_name = info['repository'] + '/' + plugin_name if plugin_name not in self.bot.config.plugins: return await ctx.send('Plugin not installed.') @@ -232,9 +250,9 @@ async def plugin_update(self, ctx, *, plugin_name: str): except DownloadError as exc: await ctx.send(f'Unable to start plugin: `{exc}`.') - @plugin.command(name='list', aliases=['show', 'view']) + @plugin.command(name='enabled') @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_list(self, ctx): + async def plugin_enabled(self, ctx): """Shows a list of currently enabled plugins.""" if self.bot.config.plugins: msg = '```\n' + '\n'.join(self.bot.config.plugins) + '\n```' @@ -242,6 +260,39 @@ async def plugin_list(self, ctx): else: await ctx.send('No plugins installed.') + @plugin.command(name='registry', aliases=['list']) + @checks.has_permissions(PermissionLevel.OWNER) + async def plugin_registry(self, ctx): + """Shows a list of all approved plugins.""" + + embeds = [] + + for name, info in self.registry.items(): + repo = f"https://github.com/{info['repository']}" + + em = discord.Embed( + color=self.bot.main_color, + description=info['description'], + url=repo, + title=info['repository'] + ) + + em.add_field( + name='Installation', + value=f'```{self.bot.prefix}plugins add {name}```') + + em.set_author(name=info['title'], icon_url=info.get('icon_url')) + if info.get('thumbnail_url'): + em.set_thumbnail(url=info.get('thumbnail_url')) + + embeds.append(em) + + paginator = PaginatorSession(ctx, *embeds) + await paginator.run() + + + + def setup(bot): bot.add_cog(Plugins(bot)) From 8bee7ca89b05d57e487e8fed62b95fa35bbdaaaa Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 19:53:41 +1000 Subject: [PATCH 0107/1221] Add plugin registry file --- .gitignore | 2 +- plugins/registry.json | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 plugins/registry.json diff --git a/.gitignore b/.gitignore index 001a767f8f..141aeeb25b 100644 --- a/.gitignore +++ b/.gitignore @@ -126,5 +126,5 @@ dmypy.json # Modmail config.json -plugins/ +plugins/*-plugins/ temp/ \ No newline at end of file diff --git a/plugins/registry.json b/plugins/registry.json new file mode 100644 index 0000000000..52a37e3da8 --- /dev/null +++ b/plugins/registry.json @@ -0,0 +1,10 @@ +{ + "profanity-filter": { + "repository": "kyb3r/modmail-plugins", + "description": "Checks for profanity in a message using machine learning and deletes it.", + "bot_version": "2.20.1", + "title": "Profanity Filter Plugin", + "icon_url": "https://i.imgur.com/951szZ3.jpg", + "thumbnail_url": "https://i.imgur.com/951szZ3.jpg" + } +} \ No newline at end of file From c94ae55c7ebd75a2f2c500a0ed0f2afc13d25398 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 20:02:25 +1000 Subject: [PATCH 0108/1221] Bot populates plugin registry from github directly --- cogs/plugins.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 1709de2e51..9cdc79a85b 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -8,6 +8,7 @@ import sys import json from pkg_resources import parse_version +import random import discord from discord.ext import commands @@ -36,9 +37,14 @@ class Plugins(commands.Cog): """ def __init__(self, bot): self.bot = bot + self.registry = {} self.bot.loop.create_task(self.download_initial_plugins()) - with open('plugins/registry.json') as f: - self.registry = json.load(f) + self.bot.loop.create_task(self.populate_registry()) + + async def populate_registry(self): + url = 'https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json' + async with self.bot.session.get(url) as resp: + self.registry = json.loads(await resp.text()) @staticmethod def _asubprocess_run(cmd): From 471a9dd5ea6f8f3f31d9c9c80dc27b1107115288 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 20:02:27 +1000 Subject: [PATCH 0109/1221] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d036f786ac..a5290c40b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.20.2 + +### Plugin Registry + +Plugin developers can now make a PR to include their plugin in the `plugin registry` command. +Add your plugin in the `plugins/registry.json` file in the main repository. + +### Changed + +`?debug` command now shows the most recent logs first. (Starts at the last page) # v2.20.1 From 1fe3607774ef5330f3a2e6896804f658678a0c23 Mon Sep 17 00:00:00 2001 From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com> Date: Sun, 19 May 2019 16:12:19 +0600 Subject: [PATCH 0110/1221] update version number (#234) --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index ff28d006ce..7e45199920 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.20.1' +__version__ = '2.20.2' import asyncio import logging From ef84531d0ef25c95b0d097d4bd3241a7cea02598 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 20:26:42 +1000 Subject: [PATCH 0111/1221] Randomly shuffle registry plugins --- cogs/plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 9cdc79a85b..17a2037188 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -273,7 +273,10 @@ async def plugin_registry(self, ctx): embeds = [] - for name, info in self.registry.items(): + registry = list(self.registry.items()) + random.shuffle(registry) + + for name, info in registry: repo = f"https://github.com/{info['repository']}" em = discord.Embed( From e986a6c3d915a8f16947f6b874da4565ade8115a Mon Sep 17 00:00:00 2001 From: Robin Mahieu <42642013+papiersnipper@users.noreply.github.com> Date: Sun, 19 May 2019 13:37:44 +0200 Subject: [PATCH 0112/1221] Added Papiersnipper's plugins --- plugins/registry.json | 44 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 52a37e3da8..086cbef4c5 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -6,5 +6,45 @@ "title": "Profanity Filter Plugin", "icon_url": "https://i.imgur.com/951szZ3.jpg", "thumbnail_url": "https://i.imgur.com/951szZ3.jpg" - } -} \ No newline at end of file + }, + "autorole": { + "repository": "papiersnipper/modmail-plugins", + "description": "Easily auto-assign a role to a user when they join your server.", + "bot_version": "2.20.1", + "title": "Autorole Plugin", + "icon_url": "https://i.imgur.com/67bEi82.png", + "thumbnail_url": "https://i.imgur.com/67bEi82.png" + }, + "embedder": { + "repository": "papiersnipper/modmail-plugins", + "description": "Easily make embeds for a nicer presence.", + "bot_version": "2.20.1", + "title": "Embedder Plugin", + "icon_url": "https://i.imgur.com/l3uzJwD.png", + "thumbnail_url": "https://i.imgur.com/l3uzJwD.png" + }, + "leveling": { + "repository": "papiersnipper/modmail-plugins", + "description": "A leveling system for your server: see who's active and who's not.", + "bot_version": "2.20.1", + "title": "Leveling Plugin", + "icon_url": "https://i.imgur.com/VuZ60QX.png", + "thumbnail_url": "https://i.imgur.com/VuZ60QX.png" + }, + "purger": { + "repository": "papiersnipper/modmail-plugins", + "description": "Delete multiple messages at a time.", + "bot_version": "2.20.1", + "title": "Purger Plugin", + "icon_url": "https://i.imgur.com/HnC42jM.png", + "thumbnail_url": "https://i.imgur.com/HnC42jM.png" + }, + "supporters": { + "repository": "papiersnipper/modmail-plugins", + "description": "Let your users know who is part of the support team.", + "bot_version": "2.20.1", + "title": "Supporters Plugin", + "icon_url": "https://i.imgur.com/1QXRutA.png", + "thumbnail_url": "https://i.imgur.com/1QXRutA.png" + } +} From 12b8582f78a98338b523c1f075aee77eab1cfe1f Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 21:45:21 +1000 Subject: [PATCH 0113/1221] Add ability to specify a plugin --- cogs/plugins.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 17a2037188..454eab208c 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -268,14 +268,32 @@ async def plugin_enabled(self, ctx): @plugin.command(name='registry', aliases=['list']) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_registry(self, ctx): + async def plugin_registry(self, ctx, plugin_name=None): """Shows a list of all approved plugins.""" embeds = [] + + registry = list(self.registry.items()) random.shuffle(registry) + def find_index(name): + index = 0 + for n, info in registry: + if name == n: + return index + index += 1 + + if plugin_name and plugin_name in self.registry: + index = find_index(plugin_name) + else: + return await ctx.send(embed=discord.Embed( + color=discord.Color.red(), + description=f'Could not find a plugin with name "{plugin_name}" within the registry.' + ) + ) + for name, info in registry: repo = f"https://github.com/{info['repository']}" @@ -297,11 +315,10 @@ async def plugin_registry(self, ctx): embeds.append(em) paginator = PaginatorSession(ctx, *embeds) + paginator.current = index await paginator.run() - - def setup(bot): bot.add_cog(Plugins(bot)) From 7761cd26fd5ae7240aef6d756d0afc5d21bacd30 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 21:48:58 +1000 Subject: [PATCH 0114/1221] Fix registry shuffle and search --- cogs/plugins.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 454eab208c..1a4b974d01 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -268,7 +268,7 @@ async def plugin_enabled(self, ctx): @plugin.command(name='registry', aliases=['list']) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_registry(self, ctx, plugin_name=None): + async def plugin_registry(self, ctx, *, plugin_name:str=None): """Shows a list of all approved plugins.""" embeds = [] @@ -285,9 +285,10 @@ def find_index(name): return index index += 1 - if plugin_name and plugin_name in self.registry: + index = 0 + if plugin_name in self.registry: index = find_index(plugin_name) - else: + elif plugin_name is not None: return await ctx.send(embed=discord.Embed( color=discord.Color.red(), description=f'Could not find a plugin with name "{plugin_name}" within the registry.' From c6be68660eb637e991665b6a0ba6259dab96f057 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 22:05:37 +1000 Subject: [PATCH 0115/1221] Fix command not found text --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 59c696e117..86f4195926 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -145,7 +145,7 @@ async def send_error_message(self, msg): # pylint: disable=W0221 embed = Embed( color=Color.red() ) - embed.set_footer(text=f'Command/Category "{self.remove_mentions(self.context.author.mention)}" not found.') + embed.set_footer(text=f'Command/Category "{self.context.kwargs.get("command")}" not found.') choices = set() From b7916951e70c798737e64d35d4ace94ad84a6afc Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 22:05:53 +1000 Subject: [PATCH 0116/1221] Add close matches support for registry --- cogs/plugins.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 1a4b974d01..64bb80c4a3 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -8,6 +8,7 @@ import sys import json from pkg_resources import parse_version +from difflib import get_close_matches import random import discord @@ -289,11 +290,15 @@ def find_index(name): if plugin_name in self.registry: index = find_index(plugin_name) elif plugin_name is not None: - return await ctx.send(embed=discord.Embed( + em = discord.Embed( color=discord.Color.red(), description=f'Could not find a plugin with name "{plugin_name}" within the registry.' ) - ) + + matches = get_close_matches(plugin_name, self.registry.keys()) + if matches: + em.add_field(name='Perhaps you meant', value='\n'.join(f'`{m}`' for m in matches)) + return await ctx.send(embed=em) for name, info in registry: repo = f"https://github.com/{info['repository']}" From 6457bd85f68c69cc73c68a474167a6f1389bc04d Mon Sep 17 00:00:00 2001 From: Piyush Bhangale Date: Sun, 19 May 2019 18:28:31 +0530 Subject: [PATCH 0117/1221] Add Piyush's Plugins ;) --- plugins/registry.json | 74 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 086cbef4c5..8e40fe2cd7 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -46,5 +46,77 @@ "title": "Supporters Plugin", "icon_url": "https://i.imgur.com/1QXRutA.png", "thumbnail_url": "https://i.imgur.com/1QXRutA.png" - } + }, + "Tags": { + "repository": "officialpiyush/modmail-plugins", + "description": "Tag Management For Your Server", + "bot_version": "2.20.1", + "title": "Tags Plugin", + "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", + "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" + }, + "announcement": { + "repository": "officialpiyush/modmail-plugins", + "description": "Easily make announcements in your server!", + "bot_version": "2.20.1", + "title": "Announcement Plugin", + "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", + "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" + }, + "code": { + "repository": "officialpiyush/modmail-plugins", + "description": "Run Codes from discord bot. Same like R.Danny's longue Commands", + "bot_version": "2.20.1", + "title": "code Plugin", + "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", + "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" + }, + "dm-on-join": { + "repository": "officialpiyush/modmail-plugins", + "description": "DM New Users when they join", + "bot_version": "2.20.1", + "title": "dm-on-join Plugin", + "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", + "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" + }, + "hastebin": { + "repository": "officialpiyush/modmail-plugins", + "description": "Easily Upload Text To hastebin!", + "bot_version": "2.20.1", + "title": "hastebin Plugin", + "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", + "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" + }, + "leave-server": { + "repository": "officialpiyush/modmail-plugins", + "description": "Don't Want Your Bot in a server? Did Someone invited it without your permission? Then this plugin is useful for you!", + "bot_version": "2.20.1", + "title": "leave-server Plugin", + "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", + "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" + }, + "moderation": { + "repository": "officialpiyush/modmail-plugins", + "description": "Rule Offenders in your server?Lift the ban hammer on them or warn them! Moderation Plugin To The Rescue :tada:", + "bot_version": "2.20.1", + "title": "moderation Plugin", + "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", + "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" + }, + "reboot": { + "repository": "officialpiyush/modmail-plugins", + "description": "Too Lazy to go to heroku and restart? Install this plugin and reboot using a single command, reboots within 3 seconds!", + "bot_version": "2.20.1", + "title": "reboot Plugin", + "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", + "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" + }, + "translator": { + "repository": "officialpiyush/modmail-plugins", + "description": "You can auto translate thread msgs using this plugin or translate text fromm any language to English!", + "bot_version": "2.20.1", + "title": "translator Plugin", + "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", + "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" + } } From b47c8b52487899e22d94a098741dd21bdf7af05e Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 23:04:33 +1000 Subject: [PATCH 0118/1221] Handle no commands in a cog --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 86f4195926..b2f20c15e9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -55,7 +55,7 @@ async def format_cog_help(self, cog): color=bot.main_color ) - embed.add_field(name='Commands', value=format_) + embed.add_field(name='Commands', value=format_ or 'No commands.') continued = ' (Continued)' if embeds else '' embed.set_author(name=cog.qualified_name + ' - Help' + continued, From c0e50cfa917ddd19d3b4fb016cbf01f1921ef441 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 23:15:03 +1000 Subject: [PATCH 0119/1221] Embed inline = false --- cogs/utility.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index b2f20c15e9..c18eabbe55 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -117,7 +117,7 @@ async def send_group_help(self, group): description=self.process_help_msg(group.help) ) - embed.add_field(name='Permission level', value=perm_level) + embed.add_field(name='Permission level', value=perm_level, inline=False) format_ = '' length = len(group.commands) @@ -131,7 +131,7 @@ async def send_group_help(self, group): branch = '├─' format_ += f'`{branch} {command.name}` - {command.short_doc}\n' - embed.add_field(name='Sub Commands', value=format_) + embed.add_field(name='Sub Commands', value=format_, inline=False) embed.set_footer( text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' 'for more info on a command.' From 630d3dbd611ad84fa2338fb9475652a97de4e94e Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 19 May 2019 23:36:27 +1000 Subject: [PATCH 0120/1221] Potentially fix plugin update command --- cogs/plugins.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 64bb80c4a3..bad138445a 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -126,7 +126,7 @@ async def load_plugin(self, username, repo, plugin_name): try: self.bot.load_extension(ext) except commands.ExtensionError as exc: - raise DownloadError('Invalid plugin.') from exc + raise DownloadError('Invalid plugin') from exc else: msg = f'Loaded plugins.{username}-{repo}.{plugin_name}' logger.info(info(msg)) @@ -250,8 +250,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): if output != 'Already up to date.': # repo was updated locally, now perform the cog reload ext = f'plugins.{username}-{repo}.{name}.{name}' - importlib.reload(importlib.import_module(ext)) - + self.bot.unload_extension(ext) try: await self.load_plugin(username, repo, name) except DownloadError as exc: From d182128f82c3ba85aa5a6d8982d6c37f5d585c91 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 19 May 2019 23:48:53 +1000 Subject: [PATCH 0121/1221] Remove reboot and tags plugins --- plugins/registry.json | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 8e40fe2cd7..3ab40815ae 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -47,14 +47,6 @@ "icon_url": "https://i.imgur.com/1QXRutA.png", "thumbnail_url": "https://i.imgur.com/1QXRutA.png" }, - "Tags": { - "repository": "officialpiyush/modmail-plugins", - "description": "Tag Management For Your Server", - "bot_version": "2.20.1", - "title": "Tags Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, "announcement": { "repository": "officialpiyush/modmail-plugins", "description": "Easily make announcements in your server!", @@ -103,14 +95,6 @@ "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, - "reboot": { - "repository": "officialpiyush/modmail-plugins", - "description": "Too Lazy to go to heroku and restart? Install this plugin and reboot using a single command, reboots within 3 seconds!", - "bot_version": "2.20.1", - "title": "reboot Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, "translator": { "repository": "officialpiyush/modmail-plugins", "description": "You can auto translate thread msgs using this plugin or translate text fromm any language to English!", From 59c466c5247fc0c733dafce2cd4a8672e26de448 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 20 May 2019 00:01:12 +1000 Subject: [PATCH 0122/1221] Populate registry everytime using the command --- cogs/plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index bad138445a..22dbef7fb1 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -271,9 +271,9 @@ async def plugin_enabled(self, ctx): async def plugin_registry(self, ctx, *, plugin_name:str=None): """Shows a list of all approved plugins.""" - embeds = [] - + await self.populate_registry() + embeds = [] registry = list(self.registry.items()) random.shuffle(registry) From 013af1aced635502a9e8f1ec272464c65c42cfe4 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 20 May 2019 00:04:00 +1000 Subject: [PATCH 0123/1221] Force removal of plugin from config --- cogs/plugins.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 22dbef7fb1..e8e5752c09 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -191,10 +191,13 @@ async def plugin_remove(self, ctx, *, plugin_name: str): info = self.registry[plugin_name] plugin_name = info['repository'] + '/' + plugin_name if plugin_name in self.bot.config.plugins: - username, repo, name = self.parse_plugin(plugin_name) - self.bot.unload_extension( - f'plugins.{username}-{repo}.{name}.{name}' - ) + try: + username, repo, name = self.parse_plugin(plugin_name) + self.bot.unload_extension( + f'plugins.{username}-{repo}.{name}.{name}' + ) + except: + pass self.bot.config.plugins.remove(plugin_name) From 5f3bd34e10b0c3044bc26aacd386f3e1691758a8 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 20 May 2019 00:06:20 +1000 Subject: [PATCH 0124/1221] Handle no help message --- cogs/utility.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index c18eabbe55..8549984f7f 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -67,7 +67,7 @@ async def format_cog_help(self, cog): return embeds def process_help_msg(self, help_: str): - return help_.format(prefix=self.clean_prefix) + return help_.format(prefix=self.clean_prefix) if help_ else 'No help message.' async def send_bot_help(self, mapping): embeds = [] @@ -124,7 +124,8 @@ async def send_group_help(self, group): for i, command in enumerate( await self.filter_commands(group.commands, sort=True, key=lambda c: c.name) ): - # Bug: fmt may run over the embed limit + # BUG: fmt may run over the embed limit + # TODO: paginate this if length == i + 1: # last branch = '└─' else: From 3e942e1cbdebab5b6c977c5c6f9ec8be2a580712 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 20 May 2019 00:09:21 +1000 Subject: [PATCH 0125/1221] Add aliases for plugin commands --- cogs/plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index e8e5752c09..b80df11676 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -137,7 +137,7 @@ async def plugin(self, ctx): """Plugin handler. Controls the plugins in the bot.""" await ctx.send_help(ctx.command) - @plugin.command(name='add') + @plugin.command(name='add', aliases=['install']) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_add(self, ctx, *, plugin_name: str): """Add a plugin.""" @@ -259,7 +259,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): except DownloadError as exc: await ctx.send(f'Unable to start plugin: `{exc}`.') - @plugin.command(name='enabled') + @plugin.command(name='enabled', aliases=['installed']) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_enabled(self, ctx): """Shows a list of currently enabled plugins.""" From a725d4fdf76748b8d9dc322515da88dca9d3e6f3 Mon Sep 17 00:00:00 2001 From: Kyber Date: Mon, 20 May 2019 00:19:44 +1000 Subject: [PATCH 0126/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0726b710b5..0565ef4f5b 100644 --- a/README.md +++ b/README.md @@ -80,4 +80,4 @@ $ python3 bot.py This project is licenced under MIT. If you have any new ideas, create an issue or a pull request. Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. -If you use Modmail and love it, consider becoming a patron on **[Patreon](https://www.patreon.com/kyber)** :smile: +This bot is open source and always will be. If you like this project and would like to show your appreciation, heres the link for our **[Patreon](https://www.patreon.com/kyber)**. From eeffb5e6755168a306aac7a0aa239c8fe06de5d7 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 19 May 2019 22:28:12 +0800 Subject: [PATCH 0127/1221] Add welcomer plugin --- plugins/registry.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 3ab40815ae..60ed870403 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -102,5 +102,13 @@ "title": "translator Plugin", "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" + }, + "welcomer": { + "repository": "fourjr/modmail-plugins", + "description": "Add messages to welcome new members! Allows for embedded messages as well. [Read more](https://github.com/fourjr/modmail-plugins/blob/master/welcomer/README.md)", + "bot_version": "2.20.1", + "title": "New member messages plugin", + "icon_url": "https://avatars0.githubusercontent.com/u/28086837?s=1080&v=4", + "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" } } From 5c1280be6becb85164c707f39a35c3841e7efbba Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 19 May 2019 22:31:47 +0800 Subject: [PATCH 0128/1221] ocd --- plugins/registry.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 60ed870403..ac3b2e7a6c 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -59,7 +59,7 @@ "repository": "officialpiyush/modmail-plugins", "description": "Run Codes from discord bot. Same like R.Danny's longue Commands", "bot_version": "2.20.1", - "title": "code Plugin", + "title": "Code Plugin", "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, @@ -67,7 +67,7 @@ "repository": "officialpiyush/modmail-plugins", "description": "DM New Users when they join", "bot_version": "2.20.1", - "title": "dm-on-join Plugin", + "title": "DM-on-join Plugin", "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, @@ -75,23 +75,23 @@ "repository": "officialpiyush/modmail-plugins", "description": "Easily Upload Text To hastebin!", "bot_version": "2.20.1", - "title": "hastebin Plugin", + "title": "Hastebin Plugin", "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, "leave-server": { "repository": "officialpiyush/modmail-plugins", - "description": "Don't Want Your Bot in a server? Did Someone invited it without your permission? Then this plugin is useful for you!", + "description": "Don't want your bot in a server? Did someone invite it without your permission? If so, this plugin is useful for you!", "bot_version": "2.20.1", - "title": "leave-server Plugin", + "title": "Leave-server Plugin", "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, "moderation": { "repository": "officialpiyush/modmail-plugins", - "description": "Rule Offenders in your server?Lift the ban hammer on them or warn them! Moderation Plugin To The Rescue :tada:", + "description": "Rule offenders in your server? Lift the ban hammer or warn them! Moderation plugin to the rescue :tada:", "bot_version": "2.20.1", - "title": "moderation Plugin", + "title": "Moderation Plugin", "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, @@ -99,7 +99,7 @@ "repository": "officialpiyush/modmail-plugins", "description": "You can auto translate thread msgs using this plugin or translate text fromm any language to English!", "bot_version": "2.20.1", - "title": "translator Plugin", + "title": "Translator Plugin", "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, @@ -108,7 +108,7 @@ "description": "Add messages to welcome new members! Allows for embedded messages as well. [Read more](https://github.com/fourjr/modmail-plugins/blob/master/welcomer/README.md)", "bot_version": "2.20.1", "title": "New member messages plugin", - "icon_url": "https://avatars0.githubusercontent.com/u/28086837?s=1080&v=4", + "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" } } From 0145c53c6e309fba089f5771d23adb1688c40626 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 20 May 2019 00:36:18 +1000 Subject: [PATCH 0129/1221] Sneaky plug --- cogs/modmail.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 46a0c8bf6e..7aa1347d5c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -52,7 +52,7 @@ async def setup(self, ctx): ) embed = discord.Embed( - title='Friendly Reminder:', + title='Friendly Reminder', description=f'You may use the `{self.bot.prefix}config set log_channel_id ' '` command to set up a custom log channel' ', then you can delete the default ' @@ -60,6 +60,10 @@ async def setup(self, ctx): color=self.bot.main_color ) + embed.add_field(name='Thanks for using the 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)!') + embed.set_footer(text=f'Type "{self.bot.prefix}help" ' 'for a complete list of commands.') await log_channel.send(embed=embed) From 7144ea0ecdd4a4a8efd61f8e861f37448cb84e1e Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 20 May 2019 01:35:33 +1000 Subject: [PATCH 0130/1221] Add a compact view command for registry --- cogs/plugins.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index b80df11676..03fc48438f 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -269,7 +269,7 @@ async def plugin_enabled(self, ctx): else: await ctx.send('No plugins installed.') - @plugin.command(name='registry', aliases=['list']) + @plugin.group(invoke_without_command=True, name='registry', aliases=['list']) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_registry(self, ctx, *, plugin_name:str=None): """Shows a list of all approved plugins.""" @@ -304,6 +304,7 @@ def find_index(name): for name, info in registry: repo = f"https://github.com/{info['repository']}" + url = f"{repo}/tree/master/name" em = discord.Embed( color=self.bot.main_color, @@ -316,9 +317,11 @@ def find_index(name): name='Installation', value=f'```{self.bot.prefix}plugins add {name}```') - em.set_author(name=info['title'], icon_url=info.get('icon_url')) + em.set_author(name=info['title'], icon_url=info.get('icon_url'), url=url) if info.get('thumbnail_url'): em.set_thumbnail(url=info.get('thumbnail_url')) + if info.get('image_url'): + em.set_image(url=info.get('image_url')) embeds.append(em) @@ -326,6 +329,45 @@ def find_index(name): paginator.current = index await paginator.run() + @plugin_registry.command(name='compact') + async def plugin_registry_compact(self, ctx): + """Shows a compact view of all plugins within the registry.""" + + await self.populate_registry() + + registry = list(self.registry.items()) + registry.sort(key=lambda elem: elem[0]) + + pages = [''] + + for name, info in registry: + repo = f"https://github.com/{info['repository']}" + url = f"{repo}/tree/master/{name}" + desc = info['description'].replace('\n', '') + fmt = f"[`{name}`]({url}) - {desc}" + length = len(fmt) - len(url) - 4 + fmt = fmt[:75 + len(url)].strip() + '...' if length > 75 else fmt + if len(fmt) + len(pages[-1]) >= 1024: + pages.append(fmt+'\n') + else: + pages[-1] += fmt + '\n' + + embeds = [] + + for page in pages: + em = discord.Embed( + color=self.bot.main_color, + description=page, + title='Plugin Registry (Compact View)', + ) + embeds.append(em) + + paginator = PaginatorSession(ctx, *embeds) + await paginator.run() + + + + def setup(bot): From d037f6b756b9b745e2c96e1b13d4a84a876b006a Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 20 May 2019 01:36:13 +1000 Subject: [PATCH 0131/1221] Fix author url in registry --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 03fc48438f..3b0f281ff3 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -304,7 +304,7 @@ def find_index(name): for name, info in registry: repo = f"https://github.com/{info['repository']}" - url = f"{repo}/tree/master/name" + url = f"{repo}/tree/master/{name}" em = discord.Embed( color=self.bot.main_color, From 7aef4575ff6c70672f7666ba1aff0d822fcfb3f4 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 May 2019 17:45:52 +0200 Subject: [PATCH 0132/1221] Use embeds instead of normal messages --- cogs/plugins.py | 119 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 30 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index b80df11676..f2be26ccf1 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -41,7 +41,7 @@ def __init__(self, bot): self.registry = {} self.bot.loop.create_task(self.download_initial_plugins()) self.bot.loop.create_task(self.populate_registry()) - + async def populate_registry(self): url = 'https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json' async with self.bot.session.get(url) as resp: @@ -146,14 +146,30 @@ async def plugin_add(self, ctx, *, plugin_name: str): plugin_name = info['repository'] + '/' + plugin_name required_version = info['bot_version'] if parse_version(self.bot.version) < parse_version(required_version): - return await ctx.send(f"Bot version too low, plugin requires version `{required_version}`") + em = discord.Embed( + description=f'Your bot\'s version is too low. This plugin requires version `{required_version}`.', + color=self.bot.main_color + ) + return await ctx.send(embed=em) if plugin_name in self.bot.config.plugins: - return await ctx.send('Plugin already installed.') + em = discord.Embed( + description='This plugin is already installed.', + color=self.bot.main_color + ) + return await ctx.send(embed=em) if plugin_name in self.bot.cogs.keys(): # another class with the same name - return await ctx.send('Another cog exists with the same name.') + em = discord.Embed( + description='There\'s another cog installed with the same name.', + color=self.bot.main_color + ) + return await ctx.send(embed=em) - message = await ctx.send('Downloading plugin...') + em = discord.Embed( + description='Downloading this plugin...', + color=self.bot.main_color + ) + message = await ctx.send(embed=em) async with ctx.typing(): if len(plugin_name.split('/')) >= 3: parsed_plugin = self.parse_plugin(plugin_name) @@ -161,15 +177,21 @@ async def plugin_add(self, ctx, *, plugin_name: str): try: await self.download_plugin_repo(*parsed_plugin[:-1]) except DownloadError as exc: - return await ctx.send( - f'Unable to fetch plugin from Github: {exc}.' + em = discord.Embed( + description=f'Unable to fetch this plugin from Github: {exc}.', + color=self.bot.main_color ) + return await ctx.send(embed=em) importlib.invalidate_caches() try: await self.load_plugin(*parsed_plugin) except DownloadError as exc: - return await ctx.send(f'Unable to load plugin: `{exc}`.') + em = discord.Embed( + description=f'Unable to load this plugin: {exc}.', + color=self.bot.main_color + ) + return await ctx.send(embed=em) # if it makes it here, it has passed all checks and should # be entered into the config @@ -177,11 +199,18 @@ async def plugin_add(self, ctx, *, plugin_name: str): self.bot.config.plugins.append(plugin_name) await self.bot.config.update() - await message.edit(content='Plugin installed. Any plugin that ' - 'you install is of your OWN RISK.') + em = discord.Embed( + description='The plugin is installed.\n' + '*Please note: any plugin that you install is of your OWN RISK*', + color=self.bot.main_color + ) + await self.bot.edit_message(message, embed=em) else: - await message.edit(content='Invalid plugin name format. ' - 'Use username/repo/plugin.') + em = discord.Embed( + description='Invalid plugin name format: use username/repo/plugin.', + color=self.bot.main_color + ) + await self.bot.edit_message(message, embed=em) @plugin.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.OWNER) @@ -196,7 +225,7 @@ async def plugin_remove(self, ctx, *, plugin_name: str): self.bot.unload_extension( f'plugins.{username}-{repo}.{name}.{name}' ) - except: + except Exception: pass self.bot.config.plugins.remove(plugin_name) @@ -219,10 +248,17 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 raise exc await self.bot.config.update() - await ctx.send('Plugin uninstalled and ' - 'all related data is erased.') + em = discord.Embed( + description='The plugin is uninstalled and all it\'s data is erased.', + color=self.bot.main_color + ) + await ctx.send(embed=em) else: - await ctx.send('Plugin not installed.') + em = discord.Embed( + description='That plugin is not installed.', + color=self.bot.main_color + ) + await ctx.send(embed=em) @plugin.command(name='update') @checks.has_permissions(PermissionLevel.OWNER) @@ -232,7 +268,11 @@ async def plugin_update(self, ctx, *, plugin_name: str): info = self.registry[plugin_name] plugin_name = info['repository'] + '/' + plugin_name if plugin_name not in self.bot.config.plugins: - return await ctx.send('Plugin not installed.') + em = discord.Embed( + description='That plugin is not installed.', + color=self.bot.main_color + ) + return await ctx.send(embed=em) async with ctx.typing(): username, repo, name = self.parse_plugin(plugin_name) @@ -245,10 +285,18 @@ async def plugin_update(self, ctx, *, plugin_name: str): ) except subprocess.CalledProcessError as exc: err = exc.stderr.decode('utf8').strip() - await ctx.send(f'Error while updating: {err}.') + em = discord.Embed( + description=f'An error occured while updating: {err}.', + color=self.bot.main_color + ) + await ctx.send(embed=em) else: output = cmd.stdout.decode('utf8').strip() - await ctx.send(f'```\n{output}\n```') + em = discord.Embed( + description='```\n{output}\n```', + color=self.bot.main_color + ) + await ctx.send(embed=em) if output != 'Already up to date.': # repo was updated locally, now perform the cog reload @@ -257,7 +305,11 @@ async def plugin_update(self, ctx, *, plugin_name: str): try: await self.load_plugin(username, repo, name) except DownloadError as exc: - await ctx.send(f'Unable to start plugin: `{exc}`.') + em = discord.Embed( + description=f'Unable to start the plugin: `{exc}`.', + color=self.bot.main_color + ) + await ctx.send(embed=em) @plugin.command(name='enabled', aliases=['installed']) @checks.has_permissions(PermissionLevel.OWNER) @@ -265,13 +317,21 @@ async def plugin_enabled(self, ctx): """Shows a list of currently enabled plugins.""" if self.bot.config.plugins: msg = '```\n' + '\n'.join(self.bot.config.plugins) + '\n```' - await ctx.send(msg) + em = discord.Embed( + description=msg, + color=self.bot.main_color + ) + await ctx.send(embed=em) else: - await ctx.send('No plugins installed.') + em = discord.Embed( + description='There are no plugins installed.', + color=self.bot.main_color + ) + await ctx.send(embed=em) @plugin.command(name='registry', aliases=['list']) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_registry(self, ctx, *, plugin_name:str=None): + async def plugin_registry(self, ctx, *, plugin_name: str = None): """Shows a list of all approved plugins.""" await self.populate_registry() @@ -293,9 +353,9 @@ def find_index(name): index = find_index(plugin_name) elif plugin_name is not None: em = discord.Embed( - color=discord.Color.red(), - description=f'Could not find a plugin with name "{plugin_name}" within the registry.' - ) + color=discord.Color.red(), + description=f'Could not find a plugin with name "{plugin_name}" within the registry.' + ) matches = get_close_matches(plugin_name, self.registry.keys()) if matches: @@ -311,11 +371,11 @@ def find_index(name): url=repo, title=info['repository'] ) - + em.add_field( - name='Installation', + name='Installation', value=f'```{self.bot.prefix}plugins add {name}```') - + em.set_author(name=info['title'], icon_url=info.get('icon_url')) if info.get('thumbnail_url'): em.set_thumbnail(url=info.get('thumbnail_url')) @@ -327,6 +387,5 @@ def find_index(name): await paginator.run() - def setup(bot): bot.add_cog(Plugins(bot)) From c4566750520c91482a3468b1852a4fb383244d5b Mon Sep 17 00:00:00 2001 From: "whitesource-bolt-for-github[bot]" Date: Sun, 19 May 2019 15:52:39 +0000 Subject: [PATCH 0133/1221] Initial WhiteSource configuration file --- .whitesource | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .whitesource diff --git a/.whitesource b/.whitesource new file mode 100644 index 0000000000..f056952141 --- /dev/null +++ b/.whitesource @@ -0,0 +1,8 @@ +{ + "generalSettings": { + "shouldScanRepo": true + }, + "checkRunSettings": { + "vulnerableCheckRunConclusionLevel": "failure" + } +} \ No newline at end of file From 04df6ea4a69fa287ef38125562b73a6abb39ba5e Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 20 May 2019 01:54:16 +1000 Subject: [PATCH 0134/1221] Increment version --- CHANGELOG.md | 6 ++++++ bot.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5290c40b4..11b4d08de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.21.0 + +### Added + +New `?plugin registry compact` command which shows a more compact view of all plugins. + # v2.20.2 ### Plugin Registry diff --git a/bot.py b/bot.py index 7e45199920..59a93743ab 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.20.2' +__version__ = '2.21.0' import asyncio import logging From 7e12199af9eda49b7b1a2369f43fc00e7b915bbf Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 20 May 2019 01:58:17 +1000 Subject: [PATCH 0135/1221] Update embed limits to 2048 --- cogs/plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 3b0f281ff3..4122fc6f22 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -347,7 +347,7 @@ async def plugin_registry_compact(self, ctx): fmt = f"[`{name}`]({url}) - {desc}" length = len(fmt) - len(url) - 4 fmt = fmt[:75 + len(url)].strip() + '...' if length > 75 else fmt - if len(fmt) + len(pages[-1]) >= 1024: + if len(fmt) + len(pages[-1]) >= 2048: pages.append(fmt+'\n') else: pages[-1] += fmt + '\n' @@ -358,8 +358,8 @@ async def plugin_registry_compact(self, ctx): em = discord.Embed( color=self.bot.main_color, description=page, - title='Plugin Registry (Compact View)', ) + em.set_author(name='Plugin Registry', icon_url=self.bot.user.avatar_url) embeds.append(em) paginator = PaginatorSession(ctx, *embeds) From 395daa2a6654da7590f2e7afdfeb1670adb07f46 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 May 2019 17:59:39 +0200 Subject: [PATCH 0136/1221] Refactor code for better readability --- cogs/plugins.py | 54 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index f2be26ccf1..a58c8bf0ab 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -49,8 +49,7 @@ async def populate_registry(self): @staticmethod def _asubprocess_run(cmd): - return subprocess.run(cmd, shell=True, check=True, - capture_output=True) + return subprocess.run(cmd, shell=True, check=True, capture_output=True) @staticmethod def parse_plugin(name): @@ -60,10 +59,12 @@ def parse_plugin(name): result[2] = '/'.join(result[2:]) except IndexError: return None + return tuple(result) async def download_initial_plugins(self): await self.bot._connected.wait() + for i in self.bot.config.plugins: parsed_plugin = self.parse_plugin(i) @@ -78,29 +79,29 @@ async def download_initial_plugins(self): except DownloadError as exc: msg = f'{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}' logger.error(error(msg)) + await async_all(env() for env in self.bot.extra_events.get('on_plugin_ready', [])) + logger.debug(info('on_plugin_ready called.')) async def download_plugin_repo(self, username, repo): try: cmd = f'git clone https://github.com/{username}/{repo} ' cmd += f'plugins/{username}-{repo} -q' - await self.bot.loop.run_in_executor( - None, - self._asubprocess_run, - cmd - ) + + await self.bot.loop.run_in_executor(None, self._asubprocess_run, cmd) # -q (quiet) so there's no terminal output unless there's an error except subprocess.CalledProcessError as exc: err = exc.stderr.decode('utf-8').strip() - if not err.endswith('already exists and is ' - 'not an empty directory.'): + + if not err.endswith('already exists and is not an empty directory.'): # don't raise error if the plugin folder exists raise DownloadError(error) from exc async def load_plugin(self, username, repo, plugin_name): ext = f'plugins.{username}-{repo}.{plugin_name}.{plugin_name}' dirname = f'plugins/{username}-{repo}/{plugin_name}' + if 'requirements.txt' in os.listdir(dirname): # Install PIP requirements try: @@ -113,6 +114,7 @@ async def load_plugin(self, username, repo, plugin_name): # so there's no terminal output unless there's an error except subprocess.CalledProcessError as exc: err = exc.stderr.decode('utf8').strip() + if err: raise DownloadError( f'Unable to download requirements: ```\n{error}\n```' @@ -135,28 +137,33 @@ async def load_plugin(self, username, repo, plugin_name): @checks.has_permissions(PermissionLevel.OWNER) async def plugin(self, ctx): """Plugin handler. Controls the plugins in the bot.""" + await ctx.send_help(ctx.command) @plugin.command(name='add', aliases=['install']) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_add(self, ctx, *, plugin_name: str): """Add a plugin.""" + if plugin_name in self.registry: info = self.registry[plugin_name] plugin_name = info['repository'] + '/' + plugin_name required_version = info['bot_version'] + if parse_version(self.bot.version) < parse_version(required_version): em = discord.Embed( description=f'Your bot\'s version is too low. This plugin requires version `{required_version}`.', color=self.bot.main_color ) return await ctx.send(embed=em) + if plugin_name in self.bot.config.plugins: em = discord.Embed( description='This plugin is already installed.', color=self.bot.main_color ) return await ctx.send(embed=em) + if plugin_name in self.bot.cogs.keys(): # another class with the same name em = discord.Embed( @@ -170,6 +177,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): color=self.bot.main_color ) message = await ctx.send(embed=em) + async with ctx.typing(): if len(plugin_name.split('/')) >= 3: parsed_plugin = self.parse_plugin(plugin_name) @@ -184,6 +192,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): return await ctx.send(embed=em) importlib.invalidate_caches() + try: await self.load_plugin(*parsed_plugin) except DownloadError as exc: @@ -216,15 +225,16 @@ async def plugin_add(self, ctx, *, plugin_name: str): @checks.has_permissions(PermissionLevel.OWNER) async def plugin_remove(self, ctx, *, plugin_name: str): """Remove a plugin.""" + if plugin_name in self.registry: info = self.registry[plugin_name] plugin_name = info['repository'] + '/' + plugin_name + if plugin_name in self.bot.config.plugins: try: username, repo, name = self.parse_plugin(plugin_name) - self.bot.unload_extension( - f'plugins.{username}-{repo}.{name}.{name}' - ) + + self.bot.unload_extension(f'plugins.{username}-{repo}.{name}.{name}') except Exception: pass @@ -240,14 +250,14 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 os.chmod(path, stat.S_IWUSR) func(path) - shutil.rmtree(f'plugins/{username}-{repo}', - onerror=onerror) + shutil.rmtree(f'plugins/{username}-{repo}', onerror=onerror) except Exception as exc: logger.error(str(exc)) self.bot.config.plugins.append(plugin_name) raise exc await self.bot.config.update() + em = discord.Embed( description='The plugin is uninstalled and all it\'s data is erased.', color=self.bot.main_color @@ -264,9 +274,11 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 @checks.has_permissions(PermissionLevel.OWNER) async def plugin_update(self, ctx, *, plugin_name: str): """Update a plugin.""" + if plugin_name in self.registry: info = self.registry[plugin_name] plugin_name = info['repository'] + '/' + plugin_name + if plugin_name not in self.bot.config.plugins: em = discord.Embed( description='That plugin is not installed.', @@ -276,22 +288,22 @@ async def plugin_update(self, ctx, *, plugin_name: str): async with ctx.typing(): username, repo, name = self.parse_plugin(plugin_name) + try: cmd = f'cd plugins/{username}-{repo} && git pull' - cmd = await self.bot.loop.run_in_executor( - None, - self._asubprocess_run, - cmd - ) + cmd = await self.bot.loop.run_in_executor(None, self._asubprocess_run, cmd) except subprocess.CalledProcessError as exc: err = exc.stderr.decode('utf8').strip() + em = discord.Embed( description=f'An error occured while updating: {err}.', color=self.bot.main_color ) await ctx.send(embed=em) + else: output = cmd.stdout.decode('utf8').strip() + em = discord.Embed( description='```\n{output}\n```', color=self.bot.main_color @@ -302,6 +314,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): # repo was updated locally, now perform the cog reload ext = f'plugins.{username}-{repo}.{name}.{name}' self.bot.unload_extension(ext) + try: await self.load_plugin(username, repo, name) except DownloadError as exc: @@ -315,6 +328,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): @checks.has_permissions(PermissionLevel.OWNER) async def plugin_enabled(self, ctx): """Shows a list of currently enabled plugins.""" + if self.bot.config.plugins: msg = '```\n' + '\n'.join(self.bot.config.plugins) + '\n```' em = discord.Embed( @@ -358,8 +372,10 @@ def find_index(name): ) matches = get_close_matches(plugin_name, self.registry.keys()) + if matches: em.add_field(name='Perhaps you meant', value='\n'.join(f'`{m}`' for m in matches)) + return await ctx.send(embed=em) for name, info in registry: From 21637a827ba4c0ccc164b2ea3a8a2fa008ca9b8e Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 May 2019 18:30:56 +0200 Subject: [PATCH 0137/1221] Edit messages properly --- cogs/plugins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index b948309bf0..4c0506c86a 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -213,13 +213,13 @@ async def plugin_add(self, ctx, *, plugin_name: str): '*Please note: any plugin that you install is of your OWN RISK*', color=self.bot.main_color ) - await self.bot.edit_message(message, embed=em) + await message.edit(embed=em) else: em = discord.Embed( description='Invalid plugin name format: use username/repo/plugin.', color=self.bot.main_color ) - await self.bot.edit_message(message, embed=em) + await message.edit(embed=em) @plugin.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.OWNER) @@ -259,7 +259,7 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 await self.bot.config.update() em = discord.Embed( - description='The plugin is uninstalled and all it\'s data is erased.', + description='The plugin is uninstalled and all its data is erased.', color=self.bot.main_color ) await ctx.send(embed=em) From 1afaef6c74fed52c7338a88a7b0c80cd58ad7474 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 May 2019 19:43:47 +0200 Subject: [PATCH 0138/1221] Forgot an f-string --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index ee00047f7b..ce69003464 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -305,7 +305,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): output = cmd.stdout.decode('utf8').strip() em = discord.Embed( - description='```\n{output}\n```', + description=f'```\n{output}\n```', color=self.bot.main_color ) await ctx.send(embed=em) From dd535813b1a648529a926d3d5a98353ac01a4cb1 Mon Sep 17 00:00:00 2001 From: Agent-Fennec <12753219+Agent-Fennec@users.noreply.github.com> Date: Sun, 19 May 2019 21:04:45 -0400 Subject: [PATCH 0139/1221] Delete .whitesource --- .whitesource | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .whitesource diff --git a/.whitesource b/.whitesource deleted file mode 100644 index f056952141..0000000000 --- a/.whitesource +++ /dev/null @@ -1,8 +0,0 @@ -{ - "generalSettings": { - "shouldScanRepo": true - }, - "checkRunSettings": { - "vulnerableCheckRunConclusionLevel": "failure" - } -} \ No newline at end of file From 940ba2ad5c7ae7d9dd948096e825d4ecce053203 Mon Sep 17 00:00:00 2001 From: Agent-Fennec <12753219+Agent-Fennec@users.noreply.github.com> Date: Sun, 19 May 2019 21:41:24 -0400 Subject: [PATCH 0140/1221] Update plugins.py --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index ce69003464..ee00047f7b 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -305,7 +305,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): output = cmd.stdout.decode('utf8').strip() em = discord.Embed( - description=f'```\n{output}\n```', + description='```\n{output}\n```', color=self.bot.main_color ) await ctx.send(embed=em) From 18ce9127e15461b8a8b895a37e50ff504d0c4704 Mon Sep 17 00:00:00 2001 From: Agent-Fennec <12753219+Agent-Fennec@users.noreply.github.com> Date: Sun, 19 May 2019 21:47:55 -0400 Subject: [PATCH 0141/1221] fix --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index ee00047f7b..ce69003464 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -305,7 +305,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): output = cmd.stdout.decode('utf8').strip() em = discord.Embed( - description='```\n{output}\n```', + description=f'```\n{output}\n```', color=self.bot.main_color ) await ctx.send(embed=em) From f81d14b63970019e0742218a4df256b6690da90b Mon Sep 17 00:00:00 2001 From: MiTonder <48181821+MiTonder@users.noreply.github.com> Date: Mon, 20 May 2019 13:30:43 +0200 Subject: [PATCH 0142/1221] Alias command update --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 8549984f7f..b2def0fa58 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -816,7 +816,7 @@ async def alias(self, ctx): - `{prefix}alias add alias-name other-command` For example: - - `{prefix}alias add reply r` + - `{prefix}alias add r reply` - Now you can use `{prefix}r` as an replacement for `{prefix}reply`. See also `{prefix}snippets`. From 4e5e4bc02e41c23ce1046c98f9c64e6b931db331 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 May 2019 09:34:04 -0700 Subject: [PATCH 0143/1221] names --- cogs/plugins.py | 126 ++++++++++++++++++++++++------------------------ cogs/utility.py | 5 +- 2 files changed, 67 insertions(+), 64 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index ce69003464..2dae1429e2 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -1,24 +1,25 @@ import importlib +import json import logging import os +import random import shutil import site import stat import subprocess import sys -import json -from pkg_resources import parse_version from difflib import get_close_matches -import random import discord from discord.ext import commands from discord.utils import async_all +from pkg_resources import parse_version + from core import checks from core.models import PermissionLevel -from core.utils import info, error from core.paginator import PaginatorSession +from core.utils import info, error logger = logging.getLogger('Modmail') @@ -146,37 +147,37 @@ async def plugin_add(self, ctx, *, plugin_name: str): """Add a plugin.""" if plugin_name in self.registry: - info = self.registry[plugin_name] - plugin_name = info['repository'] + '/' + plugin_name - required_version = info['bot_version'] + details = self.registry[plugin_name] + plugin_name = details['repository'] + '/' + plugin_name + required_version = details['bot_version'] if parse_version(self.bot.version) < parse_version(required_version): - em = discord.Embed( + embed = discord.Embed( description=f'Your bot\'s version is too low. This plugin requires version `{required_version}`.', color=self.bot.main_color ) - return await ctx.send(embed=em) + return await ctx.send(embed=embed) if plugin_name in self.bot.config.plugins: - em = discord.Embed( + embed = discord.Embed( description='This plugin is already installed.', color=self.bot.main_color ) - return await ctx.send(embed=em) + return await ctx.send(embed=embed) if plugin_name in self.bot.cogs.keys(): # another class with the same name - em = discord.Embed( + embed = discord.Embed( description='There\'s another cog installed with the same name.', color=self.bot.main_color ) - return await ctx.send(embed=em) + return await ctx.send(embed=embed) - em = discord.Embed( + embed = discord.Embed( description='Downloading this plugin...', color=self.bot.main_color ) - message = await ctx.send(embed=em) + message = await ctx.send(embed=embed) async with ctx.typing(): if len(plugin_name.split('/')) >= 3: @@ -185,22 +186,22 @@ async def plugin_add(self, ctx, *, plugin_name: str): try: await self.download_plugin_repo(*parsed_plugin[:-1]) except DownloadError as exc: - em = discord.Embed( + embed = discord.Embed( description=f'Unable to fetch this plugin from Github: {exc}.', color=self.bot.main_color ) - return await ctx.send(embed=em) + return await ctx.send(embed=embed) importlib.invalidate_caches() try: await self.load_plugin(*parsed_plugin) except DownloadError as exc: - em = discord.Embed( + embed = discord.Embed( description=f'Unable to load this plugin: {exc}.', color=self.bot.main_color ) - return await ctx.send(embed=em) + return await ctx.send(embed=embed) # if it makes it here, it has passed all checks and should # be entered into the config @@ -208,18 +209,18 @@ async def plugin_add(self, ctx, *, plugin_name: str): self.bot.config.plugins.append(plugin_name) await self.bot.config.update() - em = discord.Embed( + embed = discord.Embed( description='The plugin is installed.\n' '*Please note: any plugin that you install is of your OWN RISK*', color=self.bot.main_color ) - await message.edit(embed=em) + await message.edit(embed=embed) else: - em = discord.Embed( + embed = discord.Embed( description='Invalid plugin name format: use username/repo/plugin.', color=self.bot.main_color ) - await message.edit(embed=em) + await message.edit(embed=embed) @plugin.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.OWNER) @@ -227,8 +228,8 @@ async def plugin_remove(self, ctx, *, plugin_name: str): """Remove a plugin.""" if plugin_name in self.registry: - info = self.registry[plugin_name] - plugin_name = info['repository'] + '/' + plugin_name + details = self.registry[plugin_name] + plugin_name = details['repository'] + '/' + plugin_name if plugin_name in self.bot.config.plugins: try: @@ -241,6 +242,7 @@ async def plugin_remove(self, ctx, *, plugin_name: str): self.bot.config.plugins.remove(plugin_name) try: + # BUG: Local variables 'username' and 'repo' might be referenced before assignment if not any(i.startswith(f'{username}/{repo}') for i in self.bot.config.plugins): # if there are no more of such repos, delete the folder @@ -258,17 +260,17 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 await self.bot.config.update() - em = discord.Embed( + embed = discord.Embed( description='The plugin is uninstalled and all its data is erased.', color=self.bot.main_color ) - await ctx.send(embed=em) + await ctx.send(embed=embed) else: - em = discord.Embed( + embed = discord.Embed( description='That plugin is not installed.', color=self.bot.main_color ) - await ctx.send(embed=em) + await ctx.send(embed=embed) @plugin.command(name='update') @checks.has_permissions(PermissionLevel.OWNER) @@ -276,15 +278,15 @@ async def plugin_update(self, ctx, *, plugin_name: str): """Update a plugin.""" if plugin_name in self.registry: - info = self.registry[plugin_name] - plugin_name = info['repository'] + '/' + plugin_name + details = self.registry[plugin_name] + plugin_name = details['repository'] + '/' + plugin_name if plugin_name not in self.bot.config.plugins: - em = discord.Embed( + embed = discord.Embed( description='That plugin is not installed.', color=self.bot.main_color ) - return await ctx.send(embed=em) + return await ctx.send(embed=embed) async with ctx.typing(): username, repo, name = self.parse_plugin(plugin_name) @@ -295,20 +297,20 @@ async def plugin_update(self, ctx, *, plugin_name: str): except subprocess.CalledProcessError as exc: err = exc.stderr.decode('utf8').strip() - em = discord.Embed( + embed = discord.Embed( description=f'An error occured while updating: {err}.', color=self.bot.main_color ) - await ctx.send(embed=em) + await ctx.send(embed=embed) else: output = cmd.stdout.decode('utf8').strip() - em = discord.Embed( + embed = discord.Embed( description=f'```\n{output}\n```', color=self.bot.main_color ) - await ctx.send(embed=em) + await ctx.send(embed=embed) if output != 'Already up to date.': # repo was updated locally, now perform the cog reload @@ -331,17 +333,17 @@ async def plugin_enabled(self, ctx): if self.bot.config.plugins: msg = '```\n' + '\n'.join(self.bot.config.plugins) + '\n```' - em = discord.Embed( + embed = discord.Embed( description=msg, color=self.bot.main_color ) - await ctx.send(embed=em) + await ctx.send(embed=embed) else: - em = discord.Embed( + embed = discord.Embed( description='There are no plugins installed.', color=self.bot.main_color ) - await ctx.send(embed=em) + await ctx.send(embed=embed) @plugin.group(invoke_without_command=True, name='registry', aliases=['list']) @checks.has_permissions(PermissionLevel.OWNER) @@ -366,7 +368,7 @@ def find_index(name): if plugin_name in self.registry: index = find_index(plugin_name) elif plugin_name is not None: - em = discord.Embed( + embed = discord.Embed( color=discord.Color.red(), description=f'Could not find a plugin with name "{plugin_name}" within the registry.' ) @@ -374,32 +376,32 @@ def find_index(name): matches = get_close_matches(plugin_name, self.registry.keys()) if matches: - em.add_field(name='Perhaps you meant', value='\n'.join(f'`{m}`' for m in matches)) + embed.add_field(name='Perhaps you meant', value='\n'.join(f'`{m}`' for m in matches)) - return await ctx.send(embed=em) + return await ctx.send(embed=embed) - for name, info in registry: - repo = f"https://github.com/{info['repository']}" + for name, details in registry: + repo = f"https://github.com/{details['repository']}" url = f"{repo}/tree/master/{name}" - em = discord.Embed( + embed = discord.Embed( color=self.bot.main_color, - description=info['description'], + description=details['description'], url=repo, - title=info['repository'] + title=details['repository'] ) - em.add_field( + embed.add_field( name='Installation', value=f'```{self.bot.prefix}plugins add {name}```') - em.set_author(name=info['title'], icon_url=info.get('icon_url'), url=url) - if info.get('thumbnail_url'): - em.set_thumbnail(url=info.get('thumbnail_url')) - if info.get('image_url'): - em.set_image(url=info.get('image_url')) + embed.set_author(name=details['title'], icon_url=details.get('icon_url'), url=url) + if details.get('thumbnail_url'): + embed.set_thumbnail(url=details.get('thumbnail_url')) + if details.get('image_url'): + embed.set_image(url=details.get('image_url')) - embeds.append(em) + embeds.append(embed) paginator = PaginatorSession(ctx, *embeds) paginator.current = index @@ -416,10 +418,10 @@ async def plugin_registry_compact(self, ctx): pages = [''] - for name, info in registry: - repo = f"https://github.com/{info['repository']}" + for name, details in registry: + repo = f"https://github.com/{details['repository']}" url = f"{repo}/tree/master/{name}" - desc = info['description'].replace('\n', '') + desc = details['description'].replace('\n', '') fmt = f"[`{name}`]({url}) - {desc}" length = len(fmt) - len(url) - 4 fmt = fmt[:75 + len(url)].strip() + '...' if length > 75 else fmt @@ -431,12 +433,12 @@ async def plugin_registry_compact(self, ctx): embeds = [] for page in pages: - em = discord.Embed( + embed = discord.Embed( color=self.bot.main_color, description=page, ) - em.set_author(name='Plugin Registry', icon_url=self.bot.user.avatar_url) - embeds.append(em) + embed.set_author(name='Plugin Registry', icon_url=self.bot.user.avatar_url) + embeds.append(embed) paginator = PaginatorSession(ctx, *embeds) await paginator.run() diff --git a/cogs/utility.py b/cogs/utility.py index 8549984f7f..5add898780 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -680,7 +680,7 @@ async def config(self, ctx): - `{prefix}config set varname value here` To remove a configuration variable: - - `{prefix}config set remove varname` + - `{prefix}config remove varname` """ await ctx.send_help(ctx.command) @@ -1263,7 +1263,8 @@ async def oauth(self, ctx): @oauth.command(name='whitelist') @checks.has_permissions(PermissionLevel.OWNER) async def oauth_whitelist(self, ctx, target: Union[User, Role]): - """Whitelist or un-whitelist a user or role to have access to logs. + """ + Whitelist or un-whitelist a user or role to have access to logs. `target` may be a role ID, name, mention, user ID, name, or mention. """ From 68fed96886d897f6772e29495c446944ef7948c6 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 May 2019 22:22:00 -0700 Subject: [PATCH 0144/1221] Sneaky plug v2 --- cogs/modmail.py | 7 ++++--- cogs/utility.py | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7aa1347d5c..378a5fc59f 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -60,9 +60,10 @@ async def setup(self, ctx): color=self.bot.main_color ) - embed.add_field(name='Thanks for using the 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)!') + embed.add_field(name='Thanks for using the 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)!') embed.set_footer(text=f'Type "{self.bot.prefix}help" ' 'for a complete list of commands.') diff --git a/cogs/utility.py b/cogs/utility.py index 49aa2d5ddb..1cc5749b86 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -233,6 +233,9 @@ async def about(self, ctx): value='https://github.com/kyb3r/modmail', inline=False) + embed.add_field(name='\u200b', + value='Support this bot on [Patreon](https://patreon.com/kyber).') + embed.set_footer(text=footer) await ctx.send(embed=embed) From 2e95f20d08a824555cc5fb4df9c696e97a5c0d3a Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 21 May 2019 16:13:33 +1000 Subject: [PATCH 0145/1221] Fix activity help and add multiple close matches --- cogs/utility.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 8549984f7f..43336f90d1 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -7,6 +7,7 @@ from difflib import get_close_matches from io import StringIO from typing import Union +from types import SimpleNamespace as param from json import JSONDecodeError from textwrap import indent @@ -154,10 +155,9 @@ async def send_error_message(self, msg): # pylint: disable=W0221 if not cmd.hidden: choices.add(name) command = self.context.kwargs.get('command') - # print(self.context.message.content[self.context.in]) - closest = get_close_matches(command, choices, n=1, cutoff=0.75) + closest = get_close_matches(command, choices) if closest: - embed.add_field(name=f'Perhaps you meant:', value=f'`{closest[0]}`') + embed.add_field(name=f'Perhaps you meant:', value="\n".join(f'`{x}`' for x in closest)) else: embed.title = 'Cannot find command or category' embed.set_footer(text=f'Type "{self.clean_prefix}{self.command_attrs["name"]}" ' @@ -453,7 +453,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ''): return await ctx.send(embed=embed) if not message: - raise commands.UserInputError + raise commands.MissingRequiredArgument(param(name='message')) activity, msg = (await self.set_presence( activity_identifier=activity_type, @@ -461,7 +461,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ''): activity_message=message ))['activity'] if activity is None: - raise commands.UserInputError + raise commands.MissingRequiredArgument(param(name='activity')) self.bot.config['activity_type'] = activity.type.value self.bot.config['activity_message'] = message @@ -506,7 +506,7 @@ async def status(self, ctx, *, status_type: str.lower): status_by_key=True ))['status'] if status is None: - raise commands.UserInputError + raise commands.MissingRequiredArgument(param(name='status')) self.bot.config['status'] = status.value await self.bot.config.update() From b742c1a1869d8d203664b6768b240ad28347bfdd Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 21 May 2019 16:13:44 +1000 Subject: [PATCH 0146/1221] Consistency --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index ce69003464..c4d30daf68 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -374,7 +374,7 @@ def find_index(name): matches = get_close_matches(plugin_name, self.registry.keys()) if matches: - em.add_field(name='Perhaps you meant', value='\n'.join(f'`{m}`' for m in matches)) + em.add_field(name='Perhaps you meant:', value='\n'.join(f'`{m}`' for m in matches)) return await ctx.send(embed=em) From 897cc76e76fd58b737355b952bc035eaffa24fb1 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 21 May 2019 00:31:16 -0700 Subject: [PATCH 0147/1221] Resolve todo, pylint --- bot.py | 42 ++++++++++++++++-------------------------- cogs/plugins.py | 10 ++++------ cogs/utility.py | 4 ++-- core/paginator.py | 2 +- 4 files changed, 23 insertions(+), 35 deletions(-) diff --git a/bot.py b/bot.py index 59a93743ab..110f9d84a3 100644 --- a/bot.py +++ b/bot.py @@ -439,42 +439,32 @@ async def convert_emoji(self, name: str) -> str: try: name = await converter.convert(ctx, name.strip(':')) except commands.BadArgument: - logger.warning(f'{name} is not a valid emoji.') + logger.warning(info('%s is not a valid emoji.'), name) + raise return name async def retrieve_emoji(self) -> typing.Tuple[str, str]: - # TODO: use a function to convert emojis - - ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) - converter = commands.EmojiConverter() - sent_emoji = self.config.get('sent_emoji', '✅') blocked_emoji = self.config.get('blocked_emoji', '🚫') - if sent_emoji not in UNICODE_EMOJI: + if sent_emoji != 'disable': try: - sent_emoji = await converter.convert( - ctx, sent_emoji.strip(':') - ) + sent_emoji = await self.convert_emoji(sent_emoji) except commands.BadArgument: - if sent_emoji != 'disable': - logger.warning(info(f'Sent Emoji ({sent_emoji}) ' - f'is not a valid emoji.')) - del self.config.cache['sent_emoji'] - await self.config.update() + logger.warning(info('Removed sent emoji (%s).'), sent_emoji) + del self.config.cache['sent_emoji'] + await self.config.update() + sent_emoji = '✅' - if blocked_emoji not in UNICODE_EMOJI: + if blocked_emoji != 'disable': try: - blocked_emoji = await converter.convert( - ctx, blocked_emoji.strip(':') - ) + blocked_emoji = await self.convert_emoji(blocked_emoji) except commands.BadArgument: - if blocked_emoji != 'disable': - logger.warning(info(f'Blocked emoji ({blocked_emoji}) ' - 'is not a valid emoji.')) - del self.config.cache['blocked_emoji'] - await self.config.update() + logger.warning(info('Removed blocked emoji (%s).'), blocked_emoji) + del self.config.cache['blocked_emoji'] + await self.config.update() + blocked_emoji = '🚫' return sent_emoji, blocked_emoji @@ -912,14 +902,14 @@ async def validate_database_connection(self): "IPs correctly. Make sure to whitelist all " "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" )) - + if 'OperationFailure' in message: logger.critical(error("This is due to having invalid credentials in your MONGO_URI.")) logger.critical(error( "Recheck the username/password and make sure to url encode them. " "https://www.urlencoder.io/" )) - + return await self.logout() else: logger.info(info('Successfully connected to the database.')) diff --git a/cogs/plugins.py b/cogs/plugins.py index 1592fd640a..699d68d713 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -357,12 +357,10 @@ async def plugin_registry(self, ctx, *, plugin_name: str = None): registry = list(self.registry.items()) random.shuffle(registry) - def find_index(name): - index = 0 - for n, info in registry: - if name == n: - return index - index += 1 + def find_index(find_name): + for i, (n, _) in enumerate(registry): + if find_name == n: + return i index = 0 if plugin_name in self.registry: diff --git a/cogs/utility.py b/cogs/utility.py index 6c7def05f8..6fc2252af9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -799,8 +799,8 @@ async def config_get(self, ctx, key: str.lower = None): if val and key in keys } - for k, v in reversed(list(config.items())): - embed.add_field(name=k, value=f'`{v}`', inline=False) + for name, value in reversed(list(config.items())): + embed.add_field(name=name, value=f'`{value}`', inline=False) return await ctx.send(embed=embed) diff --git a/core/paginator.py b/core/paginator.py index aa025694ba..0b28e41dd8 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -298,7 +298,7 @@ async def show_page(self, index: int) -> None: """ if not 0 <= index < len(self.messages): return - + self.current = index page = self.messages[index] From 1f40ceba2f90cbdc585a4b4519c7775807015e18 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 21 May 2019 19:35:32 +1000 Subject: [PATCH 0148/1221] Handle channel deletion without audit log perms --- core/thread.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/thread.py b/core/thread.py index 77bb3e2db9..d9edb4b553 100644 --- a/core/thread.py +++ b/core/thread.py @@ -635,6 +635,14 @@ async def find(self, *, try: thread = self.cache[recipient_id] + if not self.bot.get_channel(thread.channel.id): # deleted channel + 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, From 8a98bb968c4f0c2628d6d0cd3cbd89203030a33f Mon Sep 17 00:00:00 2001 From: Agent-Fennec <12753219+Agent-Fennec@users.noreply.github.com> Date: Tue, 21 May 2019 09:02:59 -0400 Subject: [PATCH 0149/1221] formatting --- bot.py | 6 +++--- cogs/modmail.py | 6 +++--- cogs/plugins.py | 5 +++-- cogs/utility.py | 2 +- core/clients.py | 4 ++-- core/thread.py | 8 ++++---- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/bot.py b/bot.py index 110f9d84a3..ee77375ce5 100644 --- a/bot.py +++ b/bot.py @@ -845,14 +845,14 @@ async def on_command_error(self, context, exception): await context.send(embed=discord.Embed( color=discord.Color.red(), description=msg - )) + )) elif isinstance(exception, commands.BadArgument): await context.trigger_typing() await context.send(embed=discord.Embed( color=discord.Color.red(), description=str(exception) - )) + )) elif isinstance(exception, commands.CommandNotFound): logger.warning(error('CommandNotFound: ' + str(exception))) elif isinstance(exception, commands.MissingRequiredArgument): @@ -901,7 +901,7 @@ async def validate_database_connection(self): "This may have been caused by not whitelisting " "IPs correctly. Make sure to whitelist all " "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" - )) + )) if 'OperationFailure' in message: logger.critical(error("This is due to having invalid credentials in your MONGO_URI.")) diff --git a/cogs/modmail.py b/cogs/modmail.py index 378a5fc59f..ce0661108d 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -563,7 +563,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): embed = discord.Embed( color=discord.Color.red(), description='No log entries have been found for that query' - ) + ) return await ctx.send(embed=embed) session = PaginatorSession(ctx, *embeds) @@ -585,7 +585,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): 'open': False, '$text': { '$search': f'"{query}"' - } + } } projection = { @@ -601,7 +601,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): embed = discord.Embed( color=discord.Color.red(), description='No log entries have been found for that query' - ) + ) return await ctx.send(embed=embed) session = PaginatorSession(ctx, *embeds) diff --git a/cogs/plugins.py b/cogs/plugins.py index 699d68d713..b273b9770c 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -37,6 +37,7 @@ class Plugins(commands.Cog): Learn how to create a plugin yourself here: https://github.com/kyb3r/modmail/wiki/Plugins """ + def __init__(self, bot): self.bot = bot self.registry = {} @@ -387,7 +388,7 @@ def find_index(find_name): description=details['description'], url=repo, title=details['repository'] - ) + ) embed.add_field( name='Installation', @@ -434,7 +435,7 @@ async def plugin_registry_compact(self, ctx): embed = discord.Embed( color=self.bot.main_color, description=page, - ) + ) embed.set_author(name='Plugin Registry', icon_url=self.bot.user.avatar_url) embeds.append(embed) diff --git a/cogs/utility.py b/cogs/utility.py index 6fc2252af9..adff8ec59f 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1287,7 +1287,7 @@ async def oauth_whitelist(self, ctx, target: Union[User, Role]): embed.description = ( f"{'Un-w' if removed else 'W'}hitelisted " f"{target.mention} to view logs." - ) + ) await ctx.send(embed=embed) diff --git a/core/clients.py b/core/clients.py index ca929f44fc..f64620a8ff 100644 --- a/core/clients.py +++ b/core/clients.py @@ -280,7 +280,7 @@ async def get_user_logs(self, user_id: Union[str, int]) -> list: query = { 'recipient.id': str(user_id), 'guild_id': str(self.bot.guild_id) - } + } projection = { 'messages': {'$slice': 5} @@ -345,7 +345,7 @@ async def update_config(self, data: dict): return await self.db.config.update_one( {'bot_id': self.bot.user.id}, {'$set': toset, '$unset': unset} - ) + ) async def edit_message(self, message_id: Union[int, str], new_content: str) -> None: diff --git a/core/thread.py b/core/thread.py index d9edb4b553..d148b0c6c8 100644 --- a/core/thread.py +++ b/core/thread.py @@ -635,13 +635,13 @@ async def find(self, *, try: thread = self.cache[recipient_id] - if not self.bot.get_channel(thread.channel.id): # deleted channel + if not self.bot.get_channel(thread.channel.id): # deleted channel self.bot.loop.create_task( thread.close( - closer=self.bot.user, - silent=True, + closer=self.bot.user, + silent=True, delete_channel=False - )) + )) thread = None except KeyError: channel = discord.utils.get( From b8cb4dc330cffa4117b0638e454b29c96bca87d9 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 22 May 2019 00:51:37 +1000 Subject: [PATCH 0150/1221] Catch all errors for installing plugin --- cogs/plugins.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 699d68d713..f27d27826d 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -196,7 +196,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): try: await self.load_plugin(*parsed_plugin) - except DownloadError as exc: + except Exception as exc: embed = discord.Embed( description=f'Unable to load this plugin: {exc}.', color=self.bot.main_color @@ -214,13 +214,13 @@ async def plugin_add(self, ctx, *, plugin_name: str): '*Please note: any plugin that you install is of your OWN RISK*', color=self.bot.main_color ) - await message.edit(embed=embed) + await ctx.send(embed=embed) else: embed = discord.Embed( description='Invalid plugin name format: use username/repo/plugin.', color=self.bot.main_color ) - await message.edit(embed=embed) + await ctx.send(embed=embed) @plugin.command(name='remove', aliases=['del', 'delete', 'rm']) @checks.has_permissions(PermissionLevel.OWNER) @@ -345,7 +345,7 @@ async def plugin_enabled(self, ctx): ) await ctx.send(embed=embed) - @plugin.group(invoke_without_command=True, name='registry', aliases=['list']) + @plugin.group(invoke_without_command=True, name='registry', aliases=['list', 'info']) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_registry(self, ctx, *, plugin_name: str = None): """Shows a list of all approved plugins.""" From ecfc3ab21930e962f50ba80c884505ef702a819e Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 08:38:24 +1000 Subject: [PATCH 0151/1221] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..990f13124e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team on [discord](https://discord.gg/etJNHCQ). All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq From 2161cf922e6c15c7970ebe9427f1fe394cf933a3 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 08:59:38 +1000 Subject: [PATCH 0152/1221] Create CONTRIBUTING.md --- CONTRIBUTING.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..6d001f9004 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing to Modmail + +We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: + +- Reporting a bug +- Discussing the current state of the code +- Submitting a fix +- Proposing new features +- Becoming a maintainer + +If you are proposing new features, please discuss them with us in the [development server](https://discord.gg/etJNHCQ) before you start working on them! + +## We Develop with Github +We use github to host code, to track issues and feature requests, as well as accept pull requests. + +## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests +Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: + +1. Fork the repo and create your branch from `master`. +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. Issue that pull request! + +## Any contributions you make will be under the MIT Software License +In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. + +## Report bugs using Github's [issues](https://github.com/kyb3r/modmail/issues) +We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! + +## Write bug reports with detail, background, and sample code +**Great Bug Reports** tend to have: + +- A quick summary and/or background +- Steps to reproduce + - Be specific! +- What you expected would happen +- What actually happens +- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) + + +## Use a Consistent Coding Style +We use [black](https://github.com/python/black) for a unified code style. + +## License +By contributing, you agree that your contributions will be licensed under its MIT License. + +## References +This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) From c79d9b6448a874f98bd685058b13d79c139a16b7 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 09:30:32 +1000 Subject: [PATCH 0153/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0565ef4f5b..ae8e62a03c 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,6 @@ $ python3 bot.py ## Contributing -This project is licenced under MIT. If you have any new ideas, create an issue or a pull request. Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. +This project is licenced under MIT. Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guildelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. This bot is open source and always will be. If you like this project and would like to show your appreciation, heres the link for our **[Patreon](https://www.patreon.com/kyber)**. From 205dc39b44f67f9fcdf36fb89a4608f6ddc40bd8 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 09:32:26 +1000 Subject: [PATCH 0154/1221] fix spelling --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae8e62a03c..b92f790a1a 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,6 @@ $ python3 bot.py ## Contributing -This project is licenced under MIT. Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guildelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. +This project is licenced under MIT. Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. This bot is open source and always will be. If you like this project and would like to show your appreciation, heres the link for our **[Patreon](https://www.patreon.com/kyber)**. From 00a1d1b12253a089b81682c8b23f35434d8ffff4 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 10:04:18 +1000 Subject: [PATCH 0155/1221] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b92f790a1a..686edab8e3 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Modmail's core functionality provides an efficient communications interface betw ## Features + + * **Highly Customisable** * Bot activity, prefix, category, log channel, etc. * Fully customisable command permission system. From b7bc8f5cab275a14f5f4519f6c3a70311aa3e2d2 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 10:06:59 +1000 Subject: [PATCH 0156/1221] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 686edab8e3..c27b121160 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,13 @@ ## What is Modmail? + + + Modmail's core functionality provides an efficient communications interface between server members and staff. When a member sends a direct message to the bot, a channel is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. ## Features - * **Highly Customisable** * Bot activity, prefix, category, log channel, etc. From f4b934713cf44a8c2216bcc0e63e09f54704e8ae Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 10:07:58 +1000 Subject: [PATCH 0157/1221] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c27b121160..da7325c8f2 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,15 @@ MIT License + +
## What is Modmail? - + Modmail's core functionality provides an efficient communications interface between server members and staff. When a member sends a direct message to the bot, a channel is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. From 1b4a1bdd31492198a64f58d283013093fbdd2c1c Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 10:08:26 +1000 Subject: [PATCH 0158/1221] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da7325c8f2..7165eff5cf 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ MIT License - +
+ From 9d5827bfd510565ff34602df11d2cfe9b95e494e Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 10:16:27 +1000 Subject: [PATCH 0159/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7165eff5cf..6dbfaa842a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@
- + From 6589dd79e68d3f1695c5935d01d9a071336b58b8 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 10:19:49 +1000 Subject: [PATCH 0160/1221] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 6dbfaa842a..e44ea5b013 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,7 @@ ## What is Modmail? - - -Modmail's core functionality provides an efficient communications interface between server members and staff. When a member sends a direct message to the bot, a channel is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. +Modmail's core functionality provides an efficient way for server members to communicate with server staff. When a member sends a direct message to the bot, a channel is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. ## Features From 2b3369a246899a4109b98a87529ac340a4dc88d1 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 12:09:20 +1000 Subject: [PATCH 0161/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e44ea5b013..de1761c602 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ ## What is Modmail? -Modmail's core functionality provides an efficient way for server members to communicate with server staff. When a member sends a direct message to the bot, a channel is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. +Modmail's core functionality provides an efficient way for server members to communicate with server staff. When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. ## Features From a7126674aaa23d01846849b7407a98daa64dc85a Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 22 May 2019 14:04:24 +1000 Subject: [PATCH 0162/1221] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index de1761c602..c25c35a8f8 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,10 @@ And finally, run the bot. $ python3 bot.py ``` +## Plugins + +Modmail supports the use of third party plugins to extend or add functionality to the bot. This allows the introduction of niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third party plugins can be found using the `plugins registry` command. To develop your own, check out the [documentation](https://github.com/kyb3r/modmail/wiki/Plugins) for plugins. + ## Contributing This project is licenced under MIT. Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. From 14430585015112bdb2d321bd579312c852a1e5ec Mon Sep 17 00:00:00 2001 From: Agent-Fennec <12753219+Agent-Fennec@users.noreply.github.com> Date: Sat, 25 May 2019 00:21:43 -0400 Subject: [PATCH 0163/1221] black for code style (#247) * style fixes --- .lint.py | 9 +- .pylintrc | 4 +- bot.py | 519 +++++----- cogs/modmail.py | 543 ++++++----- cogs/plugins.py | 207 ++-- cogs/utility.py | 839 ++++++++-------- core/_color_data.py | 2239 +++++++++++++++++++++---------------------- core/changelog.py | 31 +- core/checks.py | 39 +- core/clients.py | 263 +++-- core/config.py | 157 +-- core/decorators.py | 14 +- core/models.py | 4 +- core/paginator.py | 92 +- core/thread.py | 467 ++++----- core/time.py | 103 +- core/utils.py | 44 +- 17 files changed, 2836 insertions(+), 2738 deletions(-) diff --git a/.lint.py b/.lint.py index 768c10c143..1cd6feba43 100644 --- a/.lint.py +++ b/.lint.py @@ -6,12 +6,11 @@ THRESHOLD = 9.75 -cogs = [join('cogs', c) for c in listdir('cogs') if c.endswith('.py')] -core = [join('core', c) for c in listdir('core') if c.endswith('.py')] +cogs = [join("cogs", c) for c in listdir("cogs") if c.endswith(".py")] +core = [join("core", c) for c in listdir("core") if c.endswith(".py")] -results = Run(['bot.py', *cogs, *core], - do_exit=False) +results = Run(["bot.py", *cogs, *core], do_exit=False) -score = results.linter.stats['global_note'] +score = results.linter.stats["global_note"] if score <= THRESHOLD: sys.exit(1) diff --git a/.pylintrc b/.pylintrc index b5a048b89a..60d8d81041 100644 --- a/.pylintrc +++ b/.pylintrc @@ -80,7 +80,9 @@ disable=raw-checker-failed, too-many-arguments, too-few-public-methods, too-many-lines, - line-too-long + line-too-long, + bad-continuation, + invalid-name # Enable the message, report, category or checker with the given id(s). You can diff --git a/bot.py b/bot.py index ee77375ce5..a9c7bb3dc6 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.21.0' +__version__ = "2.21.0" import asyncio import logging @@ -34,6 +34,8 @@ from datetime import datetime from types import SimpleNamespace +from pkg_resources import parse_version + import discord from discord.ext import commands from discord.ext.commands.view import StringView @@ -44,7 +46,6 @@ from colorama import init, Fore, Style from emoji import UNICODE_EMOJI from motor.motor_asyncio import AsyncIOMotorClient -from pkg_resources import parse_version from core.changelog import Changelog from core.clients import ApiClient, PluginDatabaseClient @@ -56,46 +57,42 @@ init() -logger = logging.getLogger('Modmail') +logger = logging.getLogger("Modmail") logger.setLevel(logging.INFO) ch = logging.StreamHandler(stream=sys.stdout) ch.setLevel(logging.INFO) -formatter = logging.Formatter('%(filename)s - %(levelname)s: %(message)s') +formatter = logging.Formatter("%(filename)s - %(levelname)s: %(message)s") ch.setFormatter(formatter) logger.addHandler(ch) class FileFormatter(logging.Formatter): - ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") def format(self, record): - record.msg = self.ansi_escape.sub('', record.msg) + record.msg = self.ansi_escape.sub("", record.msg) return super().format(record) -temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp') +temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp") if not os.path.exists(temp_dir): os.mkdir(temp_dir) -ch_debug = logging.FileHandler( - os.path.join(temp_dir, 'logs.log'), - mode='a+' -) +ch_debug = logging.FileHandler(os.path.join(temp_dir, "logs.log"), mode="a+") ch_debug.setLevel(logging.DEBUG) -formatter_debug = FileFormatter('%(asctime)s %(filename)s - ' - '%(levelname)s: %(message)s', - datefmt='%Y-%m-%d %H:%M:%S') +formatter_debug = FileFormatter( + "%(asctime)s %(filename)s - " "%(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) ch_debug.setFormatter(formatter_debug) logger.addHandler(ch_debug) -LINE = Fore.BLACK + Style.BRIGHT + '-------------------------' + \ - Style.RESET_ALL +LINE = Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL class ModmailBot(commands.Bot): - def __init__(self): super().__init__(command_prefix=None) # implemented in `get_prefix` self._threads = None @@ -123,20 +120,20 @@ def uptime(self) -> str: minutes, seconds = divmod(remainder, 60) days, hours = divmod(hours, 24) - fmt = '{h}h {m}m {s}s' + fmt = "{h}h {m}m {s}s" if days: - fmt = '{d}d ' + fmt + fmt = "{d}d " + fmt return fmt.format(d=days, h=hours, m=minutes, s=seconds) def _configure_logging(self): level_text = self.config.log_level.upper() logging_levels = { - 'CRITICAL': logging.CRITICAL, - 'ERROR': logging.ERROR, - 'WARNING': logging.WARNING, - 'INFO': logging.INFO, - 'DEBUG': logging.DEBUG, + "CRITICAL": logging.CRITICAL, + "ERROR": logging.ERROR, + "WARNING": logging.WARNING, + "INFO": logging.INFO, + "DEBUG": logging.DEBUG, } log_level = logging_levels.get(level_text) @@ -144,10 +141,10 @@ def _configure_logging(self): if log_level is not None: logger.setLevel(log_level) ch.setLevel(log_level) - logger.info(info('Logging level: ' + level_text)) + logger.info(info("Logging level: " + level_text)) else: - logger.info(error('Invalid logging level set. ')) - logger.info(info('Using default logging level: INFO')) + logger.info(error("Invalid logging level set. ")) + logger.info(info("Using default logging level: INFO")) @property def version(self) -> str: @@ -180,71 +177,69 @@ def threads(self) -> ThreadManager: return self._threads async def get_prefix(self, message=None): - return [self.prefix, f'<@{self.user.id}> ', f'<@!{self.user.id}> '] + return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] def _load_extensions(self): """Adds commands automatically""" logger.info(LINE) - logger.info(info('┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬')) - logger.info(info('││││ │ │││││├─┤││')) - logger.info(info('┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘')) - logger.info(info(f'v{__version__}')) - logger.info(info('Authors: kyb3r, fourjr, Taaku18')) + logger.info(info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")) + logger.info(info("││││ │ │││││├─┤││")) + logger.info(info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")) + logger.info(info(f"v{__version__}")) + logger.info(info("Authors: kyb3r, fourjr, Taaku18")) logger.info(LINE) - for file in os.listdir('cogs'): - if not file.endswith('.py'): + for file in os.listdir("cogs"): + if not file.endswith(".py"): continue - cog = f'cogs.{file[:-3]}' - logger.info(info(f'Loading {cog}')) + cog = f"cogs.{file[:-3]}" + logger.info(info(f"Loading {cog}")) try: self.load_extension(cog) except Exception: - logger.exception(error(f'Failed to load {cog}')) + logger.exception(error(f"Failed to load {cog}")) def run(self, *args, **kwargs): try: self.loop.run_until_complete(self.start(self.token)) except discord.LoginFailure: - logger.critical(error('Invalid token')) + logger.critical(error("Invalid token")) except KeyboardInterrupt: pass except Exception: - logger.critical(error('Fatal exception'), exc_info=True) + logger.critical(error("Fatal exception"), exc_info=True) finally: try: self.metadata_task.cancel() self.loop.run_until_complete(self.metadata_task) except asyncio.CancelledError: - logger.debug(info('data_task has been cancelled.')) + logger.debug(info("data_task has been cancelled.")) try: self.autoupdate_task.cancel() self.loop.run_until_complete(self.autoupdate_task) except asyncio.CancelledError: - logger.debug(info('autoupdate_task has been cancelled.')) + logger.debug(info("autoupdate_task has been cancelled.")) self.loop.run_until_complete(self.logout()) for task in asyncio.Task.all_tasks(): task.cancel() try: - self.loop.run_until_complete( - asyncio.gather(*asyncio.Task.all_tasks()) - ) + self.loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks())) except asyncio.CancelledError: - logger.debug(info('All pending tasks has been cancelled.')) + logger.debug(info("All pending tasks has been cancelled.")) finally: self.loop.run_until_complete(self.session.close()) self.loop.close() - logger.info(error(' - Shutting down bot - ')) + logger.info(error(" - Shutting down bot - ")) async def is_owner(self, user: discord.User) -> bool: - raw = str(self.config.get('owners', '0')).split(',') + raw = str(self.config.get("owners", "0")).split(",") allowed = {int(x) for x in raw} return (user.id in allowed) or await super().is_owner(user) @property def log_channel(self) -> typing.Optional[discord.TextChannel]: - channel_id = self.config.get('log_channel_id') + channel_id = self.config.get("log_channel_id") if channel_id is not None: return self.get_channel(int(channel_id)) if self.main_category is not None: @@ -253,11 +248,11 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: @property def snippets(self) -> typing.Dict[str, str]: - return {k: v for k, v in self.config.get('snippets', {}).items() if v} + return {k: v for k, v in self.config.get("snippets", {}).items() if v} @property def aliases(self) -> typing.Dict[str, str]: - return {k: v for k, v in self.config.get('aliases', {}).items() if v} + return {k: v for k, v in self.config.get("aliases", {}).items() if v} @property def token(self) -> str: @@ -281,7 +276,7 @@ def modmail_guild(self) -> discord.Guild: The guild that the bot is operating in (where the bot is creating threads) """ - modmail_guild_id = self.config.get('modmail_guild_id') + modmail_guild_id = self.config.get("modmail_guild_id") if not modmail_guild_id: return self.guild return discord.utils.get(self.guilds, id=int(modmail_guild_id)) @@ -292,59 +287,57 @@ def using_multiple_server_setup(self) -> bool: @property def main_category(self) -> typing.Optional[discord.TextChannel]: - category_id = self.config.get('main_category_id') + category_id = self.config.get("main_category_id") if category_id is not None: - return discord.utils.get(self.modmail_guild.categories, - id=int(category_id)) + return discord.utils.get(self.modmail_guild.categories, id=int(category_id)) if self.modmail_guild: - return discord.utils.get(self.modmail_guild.categories, - name='Modmail') + return discord.utils.get(self.modmail_guild.categories, name="Modmail") return None @property def blocked_users(self) -> typing.Dict[str, str]: - return self.config.get('blocked', {}) + return self.config.get("blocked", {}) @property def prefix(self) -> str: - return self.config.get('prefix', '?') + return self.config.get("prefix", "?") @property def mod_color(self) -> typing.Union[discord.Color, int]: - color = self.config.get('mod_color') + color = self.config.get("mod_color") if not color: return discord.Color.green() try: - color = int(color.lstrip('#'), base=16) + color = int(color.lstrip("#"), base=16) except ValueError: - logger.error(error('Invalid mod_color provided')) + logger.error(error("Invalid mod_color provided")) return discord.Color.green() else: return color @property def recipient_color(self) -> typing.Union[discord.Color, int]: - color = self.config.get('recipient_color') + color = self.config.get("recipient_color") if not color: return discord.Color.gold() try: - color = int(color.lstrip('#'), base=16) + color = int(color.lstrip("#"), base=16) except ValueError: - logger.error(error('Invalid recipient_color provided')) + logger.error(error("Invalid recipient_color provided")) return discord.Color.gold() else: return color @property def main_color(self) -> typing.Union[discord.Color, int]: - color = self.config.get('main_color') + color = self.config.get("main_color") if not color: return discord.Color.blurple() try: - color = int(color.lstrip('#'), base=16) + color = int(color.lstrip("#"), base=16) except ValueError: - logger.error(error('Invalid main_color provided')) + logger.error(error("Invalid main_color provided")) return discord.Color.blurple() else: return color @@ -353,7 +346,7 @@ async def on_connect(self): logger.info(LINE) await self.validate_database_connection() logger.info(LINE) - logger.info(info('Connected to gateway.')) + logger.info(info("Connected to gateway.")) await self.config.refresh() if self.db: @@ -363,39 +356,40 @@ async def on_connect(self): async def setup_indexes(self): """Setup text indexes so we can use the $search operator""" coll = self.db.logs - index_name = 'messages.content_text_messages.author.name_text_key_text' + index_name = "messages.content_text_messages.author.name_text_key_text" index_info = await coll.index_information() # Backwards compatibility - old_index = 'messages.content_text_messages.author.name_text' + old_index = "messages.content_text_messages.author.name_text" if old_index in index_info: - logger.info(info(f'Dropping old index: {old_index}')) + logger.info(info(f"Dropping old index: {old_index}")) await coll.drop_index(old_index) if index_name not in index_info: logger.info(info('Creating "text" index for logs collection.')) - logger.info(info('Name: ' + index_name)) - await coll.create_index([ - ('messages.content', 'text'), - ('messages.author.name', 'text'), - ('key', 'text') - ]) + logger.info(info("Name: " + index_name)) + await coll.create_index( + [ + ("messages.content", "text"), + ("messages.author.name", "text"), + ("key", "text"), + ] + ) async def on_ready(self): """Bot startup, sets uptime.""" await self._connected.wait() logger.info(LINE) - logger.info(info('Client ready.')) + logger.info(info("Client ready.")) logger.info(LINE) - logger.info(info(f'Logged in as: {self.user}')) - logger.info(info(f'User ID: {self.user.id}')) - logger.info(info(f'Guild ID: {self.guild.id if self.guild else 0}')) + logger.info(info(f"Logged in as: {self.user}")) + logger.info(info(f"User ID: {self.user.id}")) + logger.info(info(f"Guild ID: {self.guild.id if self.guild else 0}")) logger.info(LINE) if not self.guild: - logger.error(error('WARNING - The GUILD_ID ' - 'provided does not exist!')) + logger.error(error("WARNING - The GUILD_ID " "provided does not exist!")) else: await self.threads.populate_cache() @@ -404,12 +398,14 @@ async def on_ready(self): # closures closures = self.config.closures.copy() - logger.info(info(f'There are {len(closures)} thread(s) ' - 'pending to be closed.')) + logger.info( + info(f"There are {len(closures)} thread(s) " "pending to be closed.") + ) for recipient_id, items in 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 @@ -422,11 +418,11 @@ async def on_ready(self): continue await thread.close( - closer=self.get_user(items['closer_id']), + closer=self.get_user(items["closer_id"]), after=after, - silent=items['silent'], - delete_channel=items['delete_channel'], - message=items['message'] + silent=items["silent"], + delete_channel=items["delete_channel"], + message=items["message"], ) logger.info(LINE) @@ -437,34 +433,34 @@ async def convert_emoji(self, name: str) -> str: if name not in UNICODE_EMOJI: try: - name = await converter.convert(ctx, name.strip(':')) + name = await converter.convert(ctx, name.strip(":")) except commands.BadArgument: - logger.warning(info('%s is not a valid emoji.'), name) + logger.warning(info("%s is not a valid emoji."), name) raise return name async def retrieve_emoji(self) -> typing.Tuple[str, str]: - sent_emoji = self.config.get('sent_emoji', '✅') - blocked_emoji = self.config.get('blocked_emoji', '🚫') + sent_emoji = self.config.get("sent_emoji", "✅") + blocked_emoji = self.config.get("blocked_emoji", "🚫") - if sent_emoji != 'disable': + if sent_emoji != "disable": try: sent_emoji = await self.convert_emoji(sent_emoji) except commands.BadArgument: - logger.warning(info('Removed sent emoji (%s).'), sent_emoji) - del self.config.cache['sent_emoji'] + logger.warning(info("Removed sent emoji (%s)."), sent_emoji) + del self.config.cache["sent_emoji"] await self.config.update() - sent_emoji = '✅' + sent_emoji = "✅" - if blocked_emoji != 'disable': + if blocked_emoji != "disable": try: blocked_emoji = await self.convert_emoji(blocked_emoji) except commands.BadArgument: - logger.warning(info('Removed blocked emoji (%s).'), blocked_emoji) - del self.config.cache['blocked_emoji'] + logger.warning(info("Removed blocked emoji (%s)."), blocked_emoji) + del self.config.cache["blocked_emoji"] await self.config.update() - blocked_emoji = '🚫' + blocked_emoji = "🚫" return sent_emoji, blocked_emoji @@ -473,19 +469,21 @@ async def process_modmail(self, message: discord.Message) -> None: sent_emoji, blocked_emoji = await self.retrieve_emoji() now = datetime.utcnow() - account_age = self.config.get('account_age') - guild_age = self.config.get('guild_age') + account_age = self.config.get("account_age") + guild_age = self.config.get("guild_age") if account_age is None: account_age = isodate.duration.Duration() else: try: account_age = isodate.parse_duration(account_age) except isodate.ISO8601Error: - logger.warning('The account age limit needs to be a ' - 'ISO-8601 duration formatted duration string ' - f'greater than 0 days, not "%s".', - str(account_age)) - del self.config.cache['account_age'] + logger.warning( + "The account age limit needs to be a " + "ISO-8601 duration formatted duration string " + f'greater than 0 days, not "%s".', + str(account_age), + ) + del self.config.cache["account_age"] await self.config.update() account_age = isodate.duration.Duration() @@ -495,22 +493,25 @@ async def process_modmail(self, message: discord.Message) -> None: try: guild_age = isodate.parse_duration(guild_age) except isodate.ISO8601Error: - logger.warning('The guild join age limit needs to be a ' - 'ISO-8601 duration formatted duration string ' - f'greater than 0 days, not "%s".', str(guild_age)) - del self.config.cache['guild_age'] + logger.warning( + "The guild join age limit needs to be a " + "ISO-8601 duration formatted duration string " + f'greater than 0 days, not "%s".', + str(guild_age), + ) + del self.config.cache["guild_age"] await self.config.update() guild_age = isodate.duration.Duration() reason = self.blocked_users.get(str(message.author.id)) if reason is None: - reason = '' + reason = "" try: min_account_age = message.author.created_at + account_age except ValueError as exc: logger.warning(exc.args[0]) - del self.config.cache['account_age'] + del self.config.cache["account_age"] await self.config.update() min_account_age = now @@ -522,7 +523,7 @@ async def process_modmail(self, message: discord.Message) -> None: min_guild_age = now except ValueError as exc: logger.warning(exc.args[0]) - del self.config.cache['guild_age'] + del self.config.cache["guild_age"] await self.config.update() min_guild_age = now @@ -533,18 +534,22 @@ async def process_modmail(self, message: discord.Message) -> None: delta = human_timedelta(min_account_age) 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.config.blocked[str(message.author.id)] = new_reason await self.config.update() changed = True - 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} ' - f'before you can contact {self.user.mention}.', - color=discord.Color.red() - )) + 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} " + f"before you can contact {self.user.mention}.", + color=discord.Color.red(), + ) + ) elif min_guild_age > now: # User has not stayed in the guild for long enough @@ -553,31 +558,38 @@ async def process_modmail(self, message: discord.Message) -> None: delta = human_timedelta(min_guild_age) 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.config.blocked[str(message.author.id)] = new_reason await self.config.update() changed = True - 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} ' - f'before you can contact {self.user.mention}.', - color=discord.Color.red() - )) + 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} " + f"before you can contact {self.user.mention}.", + color=discord.Color.red(), + ) + ) elif str(message.author.id) in self.blocked_users: reaction = blocked_emoji - if reason.startswith('System Message: New Account.') or \ - reason.startswith('System Message: Recently Joined.'): + if reason.startswith("System Message: New Account.") or reason.startswith( + "System Message: Recently Joined." + ): # Met the age limit already reaction = sent_emoji del self.config.blocked[str(message.author.id)] await self.config.update() else: - end_time = re.search(r'%(.+?)%$', reason) + end_time = re.search(r"%(.+?)%$", reason) 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 @@ -586,7 +598,7 @@ async def process_modmail(self, message: discord.Message) -> None: else: reaction = sent_emoji - if reaction != 'disable': + if reaction != "disable": try: await message.add_reaction(reaction) except (discord.HTTPException, discord.InvalidArgument): @@ -619,11 +631,11 @@ async def get_context(self, message, *, cls=commands.Context): invoker = view.get_word().lower() # Check if there is any aliases being called. - alias = self.config.get('aliases', {}).get(invoker) + alias = self.config.get("aliases", {}).get(invoker) if alias is not None: ctx._alias_invoked = True - len_ = len(f'{invoked_prefix}{invoker}') - view = StringView(f'{alias}{ctx.message.content[len_:]}') + len_ = len(f"{invoked_prefix}{invoker}") + view = StringView(f"{alias}{ctx.message.content[len_:]}") ctx.view = view invoker = view.get_word() @@ -633,8 +645,9 @@ async def get_context(self, message, *, cls=commands.Context): return ctx - async def update_perms(self, name: typing.Union[PermissionLevel, str], - value: int, add: bool = True) -> None: + async def update_perms( + self, name: typing.Union[PermissionLevel, str], value: int, add: bool = True + ) -> None: if isinstance(name, PermissionLevel): permissions = self.config.level_permissions name = name.name @@ -650,14 +663,11 @@ async def update_perms(self, name: typing.Union[PermissionLevel, str], else: if value in permissions[name]: permissions[name].remove(value) - logger.info( - info(f'Updating permissions for {name}, {value} (add={add}).') - ) + logger.info(info(f"Updating permissions for {name}, {value} (add={add}).")) await self.config.update() async def on_message(self, message): - if message.type == discord.MessageType.pins_add and \ - message.author == self.user: + if message.type == discord.MessageType.pins_add and message.author == self.user: await message.delete() if message.author.bot: @@ -669,13 +679,13 @@ async def on_message(self, message): prefix = self.prefix if message.content.startswith(prefix): - cmd = message.content[len(prefix):].strip() + cmd = message.content[len(prefix) :].strip() if cmd in self.snippets: thread = await self.threads.find(channel=message.channel) snippet = self.snippets[cmd] if thread: snippet = snippet.format(recipient=thread.recipient) - message.content = f'{prefix}reply {snippet}' + message.content = f"{prefix}reply {snippet}" ctx = await self.get_context(message) if ctx.command: @@ -683,27 +693,27 @@ async def on_message(self, message): thread = await self.threads.find(channel=ctx.channel) if thread is not None: - if self.config.get('reply_without_command'): + if self.config.get("reply_without_command"): await thread.reply(message) else: - await self.api.append_log(message, type_='internal') + await self.api.append_log(message, type_="internal") elif ctx.invoked_with: exc = commands.CommandNotFound( 'Command "{}" is not found'.format(ctx.invoked_with) ) - self.dispatch('command_error', ctx, exc) + self.dispatch("command_error", ctx, exc) async def on_typing(self, channel, user, _): if user.bot: return if isinstance(channel, discord.DMChannel): - if not self.config.get('user_typing'): + if not self.config.get("user_typing"): return thread = await self.threads.find(recipient=user) if thread: await thread.channel.trigger_typing() else: - if not self.config.get('mod_typing'): + if not self.config.get("mod_typing"): return thread = await self.threads.find(channel=channel) if thread and thread.recipient: @@ -727,21 +737,21 @@ async def on_raw_reaction_add(self, payload): message = await channel.fetch_message(payload.message_id) reaction = payload.emoji - close_emoji = await self.convert_emoji( - self.config.get('close_emoji', '🔒') - ) + close_emoji = await self.convert_emoji(self.config.get("close_emoji", "🔒")) - if isinstance(channel, discord.DMChannel) and str(reaction) == str(close_emoji): # closing thread + if isinstance(channel, discord.DMChannel) and str(reaction) == str( + close_emoji + ): # closing thread thread = await self.threads.find(recipient=user) ts = message.embeds[0].timestamp if message.embeds else None if thread and ts == thread.channel.created_at: # the reacted message is the corresponding thread creation embed - if not self.config.get('disable_recipient_thread_close'): + if not self.config.get("disable_recipient_thread_close"): await thread.close(closer=user) elif not isinstance(channel, discord.DMChannel): if not message.embeds: return - message_id = str(message.embeds[0].author.url).split('/')[-1] + 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 @@ -763,16 +773,12 @@ async def on_guild_channel_delete(self, channel): return if not isinstance(channel, discord.TextChannel): - if int(self.config.get('main_category_id')) == channel.id: - await self.config.update({ - 'main_category_id': None - }) + if int(self.config.get("main_category_id")) == channel.id: + await self.config.update({"main_category_id": None}) return - if int(self.config.get('log_channel_id')) == channel.id: - await self.config.update({ - 'log_channel_id': None - }) + if int(self.config.get("log_channel_id")) == channel.id: + await self.config.update({"log_channel_id": None}) return thread = await self.threads.find(channel=channel) @@ -785,8 +791,8 @@ async def on_member_remove(self, member): thread = await self.threads.find(recipient=member) if thread: embed = discord.Embed( - description='The recipient has left the server.', - color=discord.Color.red() + description="The recipient has left the server.", + color=discord.Color.red(), ) await thread.channel.send(embed=embed) @@ -794,16 +800,14 @@ async def on_member_join(self, member): thread = await self.threads.find(recipient=member) if thread: embed = discord.Embed( - description='The recipient has joined the server.', - color=self.mod_color + description="The recipient has joined the server.", color=self.mod_color ) await thread.channel.send(embed=embed) async def on_message_delete(self, message): """Support for deleting linked messages""" - if message.embeds and not isinstance(message.channel, - discord.DMChannel): - message_id = str(message.embeds[0].author.url).split('/')[-1] + 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) @@ -812,7 +816,7 @@ async def on_message_delete(self, message): 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]: + if message_id == url.split("/")[-1]: return await msg.delete() async def on_bulk_message_delete(self, messages): @@ -826,109 +830,114 @@ async def on_message_edit(self, before, after): async for msg in thread.channel.history(): if msg.embeds: embed = msg.embeds[0] - matches = str(embed.author.url).split('/') + 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) + await self.api.edit_message(str(after.id), after.content) break async def on_error(self, event_method, *args, **kwargs): - logger.error(error('Ignoring exception in {}'.format(event_method))) - logger.error(error('Unexpected exception:'), exc_info=sys.exc_info()) + logger.error(error("Ignoring exception in {}".format(event_method))) + logger.error(error("Unexpected exception:"), exc_info=sys.exc_info()) async def on_command_error(self, context, exception): if isinstance(exception, commands.BadUnionArgument): - msg = 'Could not find the specified ' + human_join([c.__name__ for c in exception.converters]) + msg = "Could not find the specified " + human_join( + [c.__name__ for c in exception.converters] + ) await context.trigger_typing() - await context.send(embed=discord.Embed( - color=discord.Color.red(), - description=msg - )) + await context.send( + embed=discord.Embed(color=discord.Color.red(), description=msg) + ) elif isinstance(exception, commands.BadArgument): await context.trigger_typing() - await context.send(embed=discord.Embed( - color=discord.Color.red(), - description=str(exception) - )) + await context.send( + embed=discord.Embed( + color=discord.Color.red(), description=str(exception) + ) + ) elif isinstance(exception, commands.CommandNotFound): - logger.warning(error('CommandNotFound: ' + str(exception))) + logger.warning(error("CommandNotFound: " + str(exception))) elif isinstance(exception, commands.MissingRequiredArgument): await context.send_help(context.command) elif isinstance(exception, commands.CheckFailure): for check in context.command.checks: - if not await check(context) and hasattr(check, 'fail_msg'): - await context.send(embed=discord.Embed( - color=discord.Color.red(), - description=check.fail_msg - )) - logger.warning(error('CheckFailure: ' + str(exception))) + if not await check(context) and hasattr(check, "fail_msg"): + await context.send( + embed=discord.Embed( + color=discord.Color.red(), description=check.fail_msg + ) + ) + logger.warning(error("CheckFailure: " + str(exception))) else: - logger.error(error('Unexpected exception:'), exc_info=exception) + logger.error(error("Unexpected exception:"), exc_info=exception) @staticmethod def overwrites(ctx: commands.Context) -> dict: """Permission overwrites for the guild.""" overwrites = { - ctx.guild.default_role: discord.PermissionOverwrite( - read_messages=False - ), - ctx.guild.me: discord.PermissionOverwrite( - read_messages=True - ) + ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), + ctx.guild.me: discord.PermissionOverwrite(read_messages=True), } for role in ctx.guild.roles: if role.permissions.administrator: - overwrites[role] = discord.PermissionOverwrite( - read_messages=True - ) + overwrites[role] = discord.PermissionOverwrite(read_messages=True) return overwrites async def validate_database_connection(self): try: - await self.db.command('buildinfo') + await self.db.command("buildinfo") except Exception as exc: - logger.critical(error('Something went wrong ' - 'while connecting to the database.')) - message = f'{type(exc).__name__}: {str(exc)}' + logger.critical( + error("Something went wrong " "while connecting to the database.") + ) + message = f"{type(exc).__name__}: {str(exc)}" logger.critical(error(message)) - if 'ServerSelectionTimeoutError' in message: - logger.critical(error( - "This may have been caused by not whitelisting " - "IPs correctly. Make sure to whitelist all " - "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" - )) + if "ServerSelectionTimeoutError" in message: + logger.critical( + error( + "This may have been caused by not whitelisting " + "IPs correctly. Make sure to whitelist all " + "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" + ) + ) - if 'OperationFailure' in message: - logger.critical(error("This is due to having invalid credentials in your MONGO_URI.")) - logger.critical(error( - "Recheck the username/password and make sure to url encode them. " - "https://www.urlencoder.io/" - )) + if "OperationFailure" in message: + logger.critical( + error( + "This is due to having invalid credentials in your MONGO_URI." + ) + ) + logger.critical( + error( + "Recheck the username/password and make sure to url encode them. " + "https://www.urlencoder.io/" + ) + ) return await self.logout() else: - logger.info(info('Successfully connected to the database.')) + logger.info(info("Successfully connected to the database.")) async def autoupdate_loop(self): await self.wait_until_ready() - if self.config.get('disable_autoupdates'): - logger.warning(info('Autoupdates disabled.')) + if self.config.get("disable_autoupdates"): + logger.warning(info("Autoupdates disabled.")) logger.info(LINE) return - if not self.config.get('github_access_token'): - logger.warning(info('GitHub access token not found.')) - logger.warning(info('Autoupdates disabled.')) + if not self.config.get("github_access_token"): + logger.warning(info("GitHub access token not found.")) + logger.warning(info("Autoupdates disabled.")) logger.info(LINE) return - logger.info(info('Autoupdate loop started.')) + logger.info(info("Autoupdate loop started.")) while not self.is_closed(): changelog = await Changelog.from_url(self) @@ -939,27 +948,32 @@ async def autoupdate_loop(self): embed = discord.Embed(color=discord.Color.green()) - commit_data = data['data'] - user = data['user'] - embed.set_author(name=user['username'] + ' - Updating Bot', - icon_url=user['avatar_url'], - url=user['url']) + commit_data = data["data"] + user = data["user"] + embed.set_author( + name=user["username"] + " - Updating Bot", + icon_url=user["avatar_url"], + url=user["url"], + ) - embed.set_footer(text=f'Updating Modmail v{self.version} ' - f'-> v{latest.version}') + embed.set_footer( + text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" + ) embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=value) if commit_data: - message = commit_data['commit']['message'] + message = commit_data["commit"]["message"] html_url = commit_data["html_url"] - short_sha = commit_data['sha'][:6] - embed.add_field(name='Merge Commit', - value=f"[`{short_sha}`]({html_url}) " - f"{message} - {user['username']}") - logger.info(info('Bot has been updated.')) + short_sha = commit_data["sha"][:6] + embed.add_field( + name="Merge Commit", + value=f"[`{short_sha}`]({html_url}) " + f"{message} - {user['username']}", + ) + logger.info(info("Bot has been updated.")) channel = self.log_channel await channel.send(embed=embed) @@ -979,22 +993,21 @@ async def metadata_loop(self): "guild_id": self.guild_id, "guild_name": self.guild.name, "member_count": len(self.guild.members), - "uptime": (datetime.utcnow() - - self.start_time).total_seconds(), - "latency": f'{self.ws.latency * 1000:.4f}', + "uptime": (datetime.utcnow() - self.start_time).total_seconds(), + "latency": f"{self.ws.latency * 1000:.4f}", "version": self.version, "selfhosted": True, - "last_updated": str(datetime.utcnow()) + "last_updated": str(datetime.utcnow()), } - async with self.session.post('https://api.modmail.tk/metadata', json=data): - logger.debug(info('Uploading metadata to Modmail server.')) + async with self.session.post("https://api.modmail.tk/metadata", json=data): + logger.debug(info("Uploading metadata to Modmail server.")) await asyncio.sleep(3600) -if __name__ == '__main__': - if os.name != 'nt': +if __name__ == "__main__": + if os.name != "nt": import uvloop uvloop.install() diff --git a/cogs/modmail.py b/cogs/modmail.py index ce0661108d..e5c57becb2 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -36,49 +36,51 @@ async def setup(self, ctx): """ if self.bot.main_category: - return await ctx.send( - f'{self.bot.modmail_guild} is already set up.' - ) + return await ctx.send(f"{self.bot.modmail_guild} is already set up.") category = await self.bot.modmail_guild.create_category( - name='Modmail', - overwrites=self.bot.overwrites(ctx) + name="Modmail", overwrites=self.bot.overwrites(ctx) ) await category.edit(position=0) log_channel = await self.bot.modmail_guild.create_text_channel( - name='bot-logs', category=category + name="bot-logs", category=category ) embed = discord.Embed( - title='Friendly Reminder', - description=f'You may use the `{self.bot.prefix}config set log_channel_id ' - '` command to set up a custom log channel' - ', then you can delete the default ' - f'{log_channel.mention} channel.', - color=self.bot.main_color + title="Friendly Reminder", + description=f"You may use the `{self.bot.prefix}config set log_channel_id " + "` command to set up a custom log channel" + ", then you can delete the default " + f"{log_channel.mention} channel.", + color=self.bot.main_color, ) - embed.add_field(name='Thanks for using the 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)!') + embed.add_field( + name="Thanks for using the 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)!", + ) - 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 - self.bot.config['log_channel_id'] = log_channel.id + self.bot.config["main_category_id"] = category.id + self.bot.config["log_channel_id"] = log_channel.id await self.bot.config.update() - await ctx.send('Successfully set up server.\n' - 'Consider setting permission groups to give access ' - 'to roles or users the ability to use Modmail.\n' - f'Type `{self.bot.prefix}permissions` for more info.') + await ctx.send( + "Successfully set up server.\n" + "Consider setting permission groups to give access " + "to roles or users the ability to use Modmail.\n" + f"Type `{self.bot.prefix}permissions` for more info." + ) - if not self.bot.config.get('permissions'): + if not self.bot.config.get("permissions"): await self.bot.update_perms(PermissionLevel.REGULAR, -1) await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id) @@ -106,33 +108,36 @@ async def snippets(self, ctx): embeds = [] if self.bot.snippets: - embed = discord.Embed(color=self.bot.main_color, - description='Here is a list of snippets ' - 'that are currently configured.') + embed = discord.Embed( + color=self.bot.main_color, + description="Here is a list of snippets " + "that are currently configured.", + ) else: embed = discord.Embed( color=discord.Color.red(), - description='You dont have any snippets at the moment.' + description="You dont have any snippets at the moment.", ) embed.set_footer( - text=f'Do {self.bot.prefix}help snippets for more commands.' + text=f"Do {self.bot.prefix}help snippets for more commands." ) - embed.set_author(name='Snippets', icon_url=ctx.guild.icon_url) + embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) embeds.append(embed) for name, value in self.bot.snippets.items(): if len(embed.fields) == 5: - embed = discord.Embed(color=self.bot.main_color, - description=embed.description) - embed.set_author(name='Snippets', icon_url=ctx.guild.icon_url) + embed = discord.Embed( + color=self.bot.main_color, description=embed.description + ) + embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) embeds.append(embed) embed.add_field(name=name, value=value, inline=False) session = PaginatorSession(ctx, *embeds) await session.run() - @snippets.command(name='add') + @snippets.command(name="add") @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippets_add(self, ctx, name: str.lower, *, value): """ @@ -147,32 +152,32 @@ async def snippets_add(self, ctx, name: str.lower, *, value): await self.bot.config.update() embed = discord.Embed( - title='Added snippet', + title="Added snippet", color=self.bot.main_color, - description=f'`{name}` points to: {value}' + description=f"`{name}` points to: {value}", ) await ctx.send(embed=embed) - @snippets.command(name='remove', aliases=['del', 'delete', 'rm']) + @snippets.command(name="remove", aliases=["del", "delete", "rm"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippets_remove(self, ctx, *, name: str.lower): """Remove a snippet.""" if self.bot.config.snippets.get(name): embed = discord.Embed( - title='Removed snippet', + title="Removed snippet", color=self.bot.main_color, - description=f'`{name}` no longer exists.' + description=f"`{name}` no longer exists.", ) - del self.bot.config['snippets'][name] + del self.bot.config["snippets"][name] await self.bot.config.update() else: embed = discord.Embed( - title='Error', + title="Error", color=discord.Color.red(), - description=f'Snippet `{name}` does not exist.' + description=f"Snippet `{name}` does not exist.", ) await ctx.send(embed=embed) @@ -188,30 +193,31 @@ async def move(self, ctx, *, category: discord.CategoryChannel): """ thread = ctx.thread await thread.channel.edit(category=category, sync_permissions=True) - await ctx.message.add_reaction('✅') + await ctx.message.add_reaction("✅") @staticmethod async def send_scheduled_close_message(ctx, after, silent=False): human_delta = human_timedelta(after.dt) - silent = '*silently* ' if silent else '' + silent = "*silently* " if silent else "" embed = discord.Embed( - title='Scheduled close', - description=f'This thread will close {silent}in {human_delta}.', - color=discord.Color.red() + title="Scheduled close", + description=f"This thread will close {silent}in {human_delta}.", + color=discord.Color.red(), ) if after.arg and not silent: - embed.add_field(name='Message', value=after.arg) + 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) - @commands.command(usage='[after] [close message]') + @commands.command(usage="[after] [close message]") @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def close(self, ctx, *, after: UserFriendlyTime = None): @@ -240,20 +246,21 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): close_after = (after.dt - now).total_seconds() if after else 0 message = after.arg if after else None - silent = str(message).lower() in {'silent', 'silently'} - cancel = str(message).lower() == 'cancel' + silent = str(message).lower() in {"silent", "silently"} + cancel = str(message).lower() == "cancel" if cancel: if thread.close_task is not None: await thread.cancel_closure() - embed = discord.Embed(color=discord.Color.red(), - description='Scheduled close ' - 'has been cancelled.') + embed = discord.Embed( + color=discord.Color.red(), + description="Scheduled close " "has been cancelled.", + ) else: embed = discord.Embed( color=discord.Color.red(), - description='This thread has not already been scheduled to close.' + description="This thread has not already been scheduled to close.", ) return await ctx.send(embed=embed) @@ -262,16 +269,15 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): await self.send_scheduled_close_message(ctx, after, silent) await thread.close( - closer=ctx.author, - after=close_after, - message=message, - silent=silent, + closer=ctx.author, after=close_after, message=message, silent=silent ) - @commands.command(aliases=['alert']) + @commands.command(aliases=["alert"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def notify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): + async def notify( + self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None + ): """ Notify a user or role when the next thread message received. @@ -285,34 +291,39 @@ async def notify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower if user_or_role is None: mention = ctx.author.mention - elif hasattr(user_or_role, 'mention'): + elif hasattr(user_or_role, "mention"): mention = user_or_role.mention - elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: - mention = '@' + user_or_role.lstrip('@') + elif user_or_role in {"here", "everyone", "@here", "@everyone"}: + mention = "@" + user_or_role.lstrip("@") else: - raise commands.BadArgument(f'{user_or_role} is not a valid role.') + raise commands.BadArgument(f"{user_or_role} is not a valid role.") - if str(thread.id) not in self.bot.config['notification_squad']: - self.bot.config['notification_squad'][str(thread.id)] = [] + if str(thread.id) not in self.bot.config["notification_squad"]: + self.bot.config["notification_squad"][str(thread.id)] = [] - mentions = self.bot.config['notification_squad'][str(thread.id)] + mentions = self.bot.config["notification_squad"][str(thread.id)] if mention in mentions: - embed = discord.Embed(color=discord.Color.red(), - description=f'{mention} is already ' - 'going to be mentioned.') + embed = discord.Embed( + color=discord.Color.red(), + description=f"{mention} is already " "going to be mentioned.", + ) else: mentions.append(mention) await self.bot.config.update() - embed = discord.Embed(color=self.bot.main_color, - description=f'{mention} will be mentioned ' - 'on the next message received.') + embed = discord.Embed( + color=self.bot.main_color, + description=f"{mention} will be mentioned " + "on the next message received.", + ) return await ctx.send(embed=embed) - @commands.command(aliases=['unalert']) + @commands.command(aliases=["unalert"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def unnotify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): + async def unnotify( + self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None + ): """ Un-notify a user, role, or yourself from a thread. @@ -324,34 +335,38 @@ async def unnotify(self, ctx, *, user_or_role: Union[discord.Role, User, str.low if user_or_role is None: mention = ctx.author.mention - elif hasattr(user_or_role, 'mention'): + elif hasattr(user_or_role, "mention"): mention = user_or_role.mention - elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: - mention = '@' + user_or_role.lstrip('@') + elif user_or_role in {"here", "everyone", "@here", "@everyone"}: + mention = "@" + user_or_role.lstrip("@") else: - mention = f'`{user_or_role}`' + mention = f"`{user_or_role}`" - if str(thread.id) not in self.bot.config['notification_squad']: - self.bot.config['notification_squad'][str(thread.id)] = [] + if str(thread.id) not in self.bot.config["notification_squad"]: + self.bot.config["notification_squad"][str(thread.id)] = [] - mentions = self.bot.config['notification_squad'][str(thread.id)] + mentions = self.bot.config["notification_squad"][str(thread.id)] if mention not in mentions: - embed = discord.Embed(color=discord.Color.red(), - description=f'{mention} does not have a ' - 'pending notification.') + embed = discord.Embed( + color=discord.Color.red(), + description=f"{mention} does not have a " "pending notification.", + ) else: mentions.remove(mention) await self.bot.config.update() - embed = discord.Embed(color=self.bot.main_color, - description=f'{mention} will no longer ' - 'be notified.') + embed = discord.Embed( + color=self.bot.main_color, + description=f"{mention} will no longer " "be notified.", + ) return await ctx.send(embed=embed) - @commands.command(aliases=['sub']) + @commands.command(aliases=["sub"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def subscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): + async def subscribe( + self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None + ): """ Notify a user, role, or yourself for every thread message received. @@ -365,36 +380,39 @@ async def subscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lo if user_or_role is None: mention = ctx.author.mention - elif hasattr(user_or_role, 'mention'): + elif hasattr(user_or_role, "mention"): mention = user_or_role.mention - elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: - mention = '@' + user_or_role.lstrip('@') + elif user_or_role in {"here", "everyone", "@here", "@everyone"}: + mention = "@" + user_or_role.lstrip("@") else: - raise commands.BadArgument(f'{user_or_role} is not a valid role.') + raise commands.BadArgument(f"{user_or_role} is not a valid role.") - if str(thread.id) not in self.bot.config['subscriptions']: - self.bot.config['subscriptions'][str(thread.id)] = [] + if str(thread.id) not in self.bot.config["subscriptions"]: + self.bot.config["subscriptions"][str(thread.id)] = [] - mentions = self.bot.config['subscriptions'][str(thread.id)] + mentions = self.bot.config["subscriptions"][str(thread.id)] if mention in mentions: - embed = discord.Embed(color=discord.Color.red(), - description=f'{mention} is already ' - 'subscribed to this thread.') + embed = discord.Embed( + color=discord.Color.red(), + 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) - @commands.command(aliases=['unsub']) + @commands.command(aliases=["unsub"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def unsubscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): + async def unsubscribe( + self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None + ): """ Unsubscribe a user, role, or yourself from a thread. @@ -406,28 +424,30 @@ async def unsubscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str. if user_or_role is None: mention = ctx.author.mention - elif hasattr(user_or_role, 'mention'): + elif hasattr(user_or_role, "mention"): mention = user_or_role.mention - elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: - mention = '@' + user_or_role.lstrip('@') + elif user_or_role in {"here", "everyone", "@here", "@everyone"}: + mention = "@" + user_or_role.lstrip("@") else: - mention = f'`{user_or_role}`' + mention = f"`{user_or_role}`" - if str(thread.id) not in self.bot.config['subscriptions']: - self.bot.config['subscriptions'][str(thread.id)] = [] + if str(thread.id) not in self.bot.config["subscriptions"]: + self.bot.config["subscriptions"][str(thread.id)] = [] - mentions = self.bot.config['subscriptions'][str(thread.id)] + mentions = self.bot.config["subscriptions"][str(thread.id)] if mention not in mentions: - embed = discord.Embed(color=discord.Color.red(), - description=f'{mention} is not already ' - 'subscribed to this thread.') + embed = discord.Embed( + color=discord.Color.red(), + 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.') + embed = discord.Embed( + color=self.bot.main_color, + description=f"{mention} is now unsubscribed " "to this thread.", + ) return await ctx.send(embed=embed) @commands.command() @@ -436,7 +456,7 @@ async def unsubscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str. async def nsfw(self, ctx): """Flags a Modmail thread as NSFW (not safe for work).""" await ctx.channel.edit(nsfw=True) - await ctx.message.add_reaction('✅') + await ctx.message.add_reaction("✅") @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -445,54 +465,47 @@ 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 - ) + embed=discord.Embed(color=self.bot.main_color, description=log_link) ) def format_log_embeds(self, logs, avatar_url): embeds = [] logs = tuple(logs) - title = f'Total Results Found ({len(logs)})' + title = f"Total Results Found ({len(logs)})" for entry in logs: - key = entry['key'] + key = entry["key"] - created_at = parser.parse(entry['created_at']) + created_at = parser.parse(entry["created_at"]) - prefix = os.getenv('LOG_URL_PREFIX', '/logs') - if prefix == 'NONE': - prefix = '' + prefix = os.getenv("LOG_URL_PREFIX", "/logs") + if prefix == "NONE": + prefix = "" - log_url = self.bot.config.log_url.strip('/') + f'{prefix}/{key}' + log_url = self.bot.config.log_url.strip("/") + f"{prefix}/{key}" - username = entry['recipient']['name'] + '#' - username += entry['recipient']['discriminator'] + username = entry["recipient"]["name"] + "#" + 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 = 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.url = log_url - embed.add_field(name='Created', - value=duration(created_at, now=datetime.utcnow())) - embed.add_field(name='Closed By', - value=f"<@{entry['closer']['id']}>") - - 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='Link', value=log_url) - embed.set_footer( - text='Recipient ID: ' + str(entry['recipient']['id']) + embed.add_field( + name="Created", value=duration(created_at, now=datetime.utcnow()) ) + embed.add_field(name="Closed By", value=f"<@{entry['closer']['id']}>") + + 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="Link", value=log_url) + embed.set_footer(text="Recipient ID: " + str(entry["recipient"]["id"])) embeds.append(embed) return embeds @@ -512,28 +525,29 @@ async def logs(self, ctx, *, user: User = None): if not user: thread = ctx.thread if not thread: - raise commands.MissingRequiredArgument(param(name='member')) + raise commands.MissingRequiredArgument(param(name="member")) user = thread.recipient - default_avatar = 'https://cdn.discordapp.com/embed/avatars/0.png' - icon_url = getattr(user, 'avatar_url', default_avatar) + default_avatar = "https://cdn.discordapp.com/embed/avatars/0.png" + icon_url = getattr(user, "avatar_url", default_avatar) logs = await self.bot.api.get_user_logs(user.id) - if not any(not log['open'] for log in logs): - embed = discord.Embed(color=discord.Color.red(), - description='This user does not ' - 'have any previous logs.') + if not any(not log["open"] for log in logs): + embed = discord.Embed( + color=discord.Color.red(), + description="This user does not " "have any previous logs.", + ) return await ctx.send(embed=embed) - logs = reversed([e for e in logs if not e['open']]) + logs = reversed([e for e in logs if not e["open"]]) embeds = self.format_log_embeds(logs, avatar_url=icon_url) session = PaginatorSession(ctx, *embeds) await session.run() - @logs.command(name='closed-by', aliases=['closeby']) + @logs.command(name="closed-by", aliases=["closeby"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs_closed_by(self, ctx, *, user: User = None): """ @@ -545,31 +559,28 @@ 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) + "guild_id": str(self.bot.guild_id), + "open": False, + "closer.id": str(user.id), } - projection = { - 'messages': {'$slice': 5} - } + projection = {"messages": {"$slice": 5}} entries = await self.bot.db.logs.find(query, projection).to_list(None) - embeds = self.format_log_embeds(entries, - avatar_url=self.bot.guild.icon_url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) if not embeds: embed = discord.Embed( color=discord.Color.red(), - description='No log entries have been found for that query' + description="No log entries have been found for that query", ) return await ctx.send(embed=embed) session = PaginatorSession(ctx, *embeds) await session.run() - @logs.command(name='search', aliases=['find']) + @logs.command(name="search", aliases=["find"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs_search(self, ctx, limit: Optional[int] = None, *, query): """ @@ -581,26 +592,21 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): await ctx.trigger_typing() query = { - 'guild_id': str(self.bot.guild_id), - 'open': False, - '$text': { - '$search': f'"{query}"' - } + "guild_id": str(self.bot.guild_id), + "open": False, + "$text": {"$search": f'"{query}"'}, } - projection = { - 'messages': {'$slice': 5} - } + projection = {"messages": {"$slice": 5}} entries = await self.bot.db.logs.find(query, projection).to_list(limit) - embeds = self.format_log_embeds(entries, - avatar_url=self.bot.guild.icon_url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) if not embeds: embed = discord.Embed( color=discord.Color.red(), - description='No log entries have been found for that query' + description="No log entries have been found for that query", ) return await ctx.send(embed=embed) @@ -610,7 +616,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def reply(self, ctx, *, msg: str = ''): + async def reply(self, ctx, *, msg: str = ""): """ Reply to a Modmail thread. @@ -624,7 +630,7 @@ async def reply(self, ctx, *, msg: str = ''): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def anonreply(self, ctx, *, msg: str = ''): + async def anonreply(self, ctx, *, msg: str = ""): """ Reply to a thread anonymously. @@ -641,7 +647,7 @@ async def anonreply(self, ctx, *, msg: str = ''): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def note(self, ctx, *, msg: str = ''): + async def note(self, ctx, *, msg: str = ""): """ Take a note about the current thread. @@ -664,11 +670,11 @@ async def find_linked_message(self, ctx, message_id): if embed.color.value != 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] + 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] + linked_message_id = str(url).split("/")[-1] break return linked_message_id @@ -676,8 +682,7 @@ async def find_linked_message(self, ctx, message_id): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def edit(self, ctx, message_id: Optional[int] = None, - *, message: str): + async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): """ Edit a message that was sent using the reply or anonreply command. @@ -689,25 +694,31 @@ async def edit(self, ctx, message_id: Optional[int] = None, linked_message_id = await self.find_linked_message(ctx, message_id) if linked_message_id is None: - return await ctx.send(embed=discord.Embed( - title='Failed', - description='Cannot find a message to edit.', - color=discord.Color.red() - )) + return await ctx.send( + embed=discord.Embed( + title="Failed", + description="Cannot find a message to edit.", + color=discord.Color.red(), + ) + ) await asyncio.gather( thread.edit_message(linked_message_id, message), - self.bot.api.edit_message(linked_message_id, message) + self.bot.api.edit_message(linked_message_id, message), ) - await ctx.message.add_reaction('✅') + await ctx.message.add_reaction("✅") @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @trigger_typing - async def contact(self, ctx, - category: Optional[discord.CategoryChannel] = None, *, - user: Union[discord.Member, discord.User]): + async def contact( + self, + ctx, + category: Optional[discord.CategoryChannel] = None, + *, + user: Union[discord.Member, discord.User], + ): """ Create a thread with a specified member. @@ -721,7 +732,7 @@ async def contact(self, ctx, if user.bot: embed = discord.Embed( color=discord.Color.red(), - description='Cannot start a thread with a bot.' + description="Cannot start a thread with a bot.", ) return await ctx.send(embed=embed) @@ -729,19 +740,20 @@ async def contact(self, ctx, if exists: embed = discord.Embed( color=discord.Color.red(), - description='A thread for this user already ' - f'exists in {exists.channel.mention}.' + description="A thread for this user already " + f"exists in {exists.channel.mention}.", ) else: - thread = self.bot.threads.create(user, creator=ctx.author, - category=category) + thread = self.bot.threads.create( + user, creator=ctx.author, category=category + ) await thread.wait_until_ready() embed = discord.Embed( - title='Created thread', - description=f'Thread started in {thread.channel.mention} ' - f'for {user.mention}.', - color=self.bot.main_color + title="Created thread", + description=f"Thread started in {thread.channel.mention} " + f"for {user.mention}.", + color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -751,9 +763,11 @@ async def contact(self, ctx, @trigger_typing async def blocked(self, ctx): """Retrieve a list of blocked users.""" - embed = discord.Embed(title='Blocked Users', - color=self.bot.main_color, - description='Here is a list of blocked users.') + embed = discord.Embed( + title="Blocked Users", + color=self.bot.main_color, + description="Here is a list of blocked users.", + ) users = [] not_reachable = [] @@ -766,24 +780,25 @@ async def blocked(self, ctx): not_reachable.append((id_, reason)) if users: - val = '\n'.join(u.mention + (f' - `{r}`' if r else '') - for u, r in users) - embed.add_field(name='Currently Known', value=val) + val = "\n".join(u.mention + (f" - `{r}`" if r else "") for u, r in users) + embed.add_field(name="Currently Known", value=val) if not_reachable: - val = '\n'.join(f'`{i}`' + (f' - `{r}`' if r else '') - for i, r in not_reachable) - embed.add_field(name='Unknown', value=val, inline=False) + val = "\n".join( + f"`{i}`" + (f" - `{r}`" if r else "") for i, r in not_reachable + ) + embed.add_field(name="Unknown", value=val, inline=False) if not users and not not_reachable: - embed.description = 'Currently there are no blocked users.' + embed.description = "Currently there are no blocked users." await ctx.send(embed=embed) @commands.command() @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. @@ -795,59 +810,63 @@ async def block(self, ctx, user: Optional[User] = None, *, `after` may be a simple "human-readable" time text. See `{prefix}help close` for examples. """ - reason = '' + reason = "" if user is None: thread = ctx.thread if thread: user = thread.recipient elif after is None: - raise commands.MissingRequiredArgument(param(name='user')) + raise commands.MissingRequiredArgument(param(name="user")) else: raise commands.BadArgument(f'User "{after.arg}" not found') if after is not None: reason = after.arg - if reason.startswith('System Message: '): - raise commands.BadArgument('The reason cannot start with `System Message:`.') - if '%' in reason: + if reason.startswith("System Message: "): + raise commands.BadArgument( + "The reason cannot start with `System Message:`." + ) + if "%" in reason: raise commands.BadArgument('The reason contains illegal character "%".') if after.dt > after.now: - reason = f'{reason} %{after.dt.isoformat()}%' + reason = f"{reason} %{after.dt.isoformat()}%" if not reason: reason = None - mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`' + mention = user.mention if hasattr(user, "mention") else f"`{user.id}`" - extend = f' for `{reason}`' if reason is not None else '' - msg = self.bot.blocked_users.get(str(user.id), '') + extend = f" for `{reason}`" if reason is not None else "" + msg = self.bot.blocked_users.get(str(user.id), "") - if str(user.id) not in self.bot.blocked_users or \ - reason is not None or \ - msg.startswith('System Message: '): + if ( + str(user.id) not in self.bot.blocked_users + or reason is not None + or msg.startswith("System Message: ") + ): if str(user.id) in self.bot.blocked_users: - old_reason = msg.strip().rstrip('.') or 'no reason' + old_reason = msg.strip().rstrip(".") or "no reason" embed = discord.Embed( - title='Success', - description=f'{mention} was previously blocked for ' + title="Success", + description=f"{mention} was previously blocked for " f'"{old_reason}". {mention} is now blocked{extend}.', - color=self.bot.main_color + color=self.bot.main_color, ) else: embed = discord.Embed( - title='Success', + title="Success", color=self.bot.main_color, - description=f'{mention} is now blocked{extend}.' + description=f"{mention} is now blocked{extend}.", ) self.bot.config.blocked[str(user.id)] = reason await self.bot.config.update() else: embed = discord.Embed( - title='Error', + title="Error", color=discord.Color.red(), - description=f'{mention} is already blocked.' + description=f"{mention} is already blocked.", ) return await ctx.send(embed=embed) @@ -869,38 +888,38 @@ async def unblock(self, ctx, *, user: User = None): if thread: user = thread.recipient else: - raise commands.MissingRequiredArgument(param(name='user')) + raise commands.MissingRequiredArgument(param(name="user")) - mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`' + mention = user.mention if hasattr(user, "mention") else f"`{user.id}`" if str(user.id) in self.bot.blocked_users: msg = self.bot.blocked_users.get(str(user.id)) if msg is None: - msg = '' + msg = "" del self.bot.config.blocked[str(user.id)] await self.bot.config.update() - if msg.startswith('System Message: '): + if msg.startswith("System Message: "): # If the user is blocked internally (for example: below minimum account age) # Show an extended message stating the original internal message - reason = msg[16:].strip().rstrip('.') or 'no reason' + reason = msg[16:].strip().rstrip(".") or "no reason" embed = discord.Embed( - title='Success', - description=f'{mention} was previously blocked internally due to ' + title="Success", + description=f"{mention} was previously blocked internally due to " f'"{reason}". {mention} is no longer blocked.', - color=self.bot.main_color + color=self.bot.main_color, ) else: embed = discord.Embed( - title='Success', + title="Success", color=self.bot.main_color, - description=f'{mention} is no longer blocked.' + description=f"{mention} is no longer blocked.", ) else: embed = discord.Embed( - title='Error', - description=f'{mention} is not blocked.', - color=discord.Color.red() + title="Error", + description=f"{mention} is not blocked.", + color=discord.Color.red(), ) return await ctx.send(embed=embed) @@ -921,21 +940,23 @@ 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) if linked_message_id is None: return await ctx.send( embed=discord.Embed( - title='Failed', - description='Cannot find a message to delete.', - color=discord.Color.red() + title="Failed", + description="Cannot find a message to delete.", + color=discord.Color.red(), ) ) await thread.delete_message(linked_message_id) - await ctx.message.add_reaction('✅') + await ctx.message.add_reaction("✅") def setup(bot): diff --git a/cogs/plugins.py b/cogs/plugins.py index d8d4999c04..e77e6c22d3 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -10,18 +10,19 @@ import sys from difflib import get_close_matches +from pkg_resources import parse_version + import discord from discord.ext import commands from discord.utils import async_all -from pkg_resources import parse_version from core import checks from core.models import PermissionLevel from core.paginator import PaginatorSession from core.utils import info, error -logger = logging.getLogger('Modmail') +logger = logging.getLogger("Modmail") class DownloadError(Exception): @@ -45,7 +46,7 @@ def __init__(self, bot): self.bot.loop.create_task(self.populate_registry()) async def populate_registry(self): - url = 'https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json' + url = "https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json" async with self.bot.session.get(url) as resp: self.registry = json.loads(await resp.text()) @@ -57,8 +58,8 @@ def _asubprocess_run(cmd): def parse_plugin(name): # returns: (username, repo, plugin_name) try: - result = name.split('/') - result[2] = '/'.join(result[2:]) + result = name.split("/") + result[2] = "/".join(result[2:]) except IndexError: return None @@ -73,53 +74,56 @@ async def download_initial_plugins(self): try: await self.download_plugin_repo(*parsed_plugin[:-1]) except DownloadError as exc: - msg = f'{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}' + msg = f"{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}" logger.error(error(msg)) else: try: await self.load_plugin(*parsed_plugin) except DownloadError as exc: - msg = f'{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}' + msg = f"{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}" logger.error(error(msg)) - await async_all(env() for env in self.bot.extra_events.get('on_plugin_ready', [])) + await async_all( + env() for env in self.bot.extra_events.get("on_plugin_ready", []) + ) - logger.debug(info('on_plugin_ready called.')) + logger.debug(info("on_plugin_ready called.")) async def download_plugin_repo(self, username, repo): try: - cmd = f'git clone https://github.com/{username}/{repo} ' - cmd += f'plugins/{username}-{repo} -q' + cmd = f"git clone https://github.com/{username}/{repo} " + cmd += f"plugins/{username}-{repo} -q" await self.bot.loop.run_in_executor(None, self._asubprocess_run, cmd) # -q (quiet) so there's no terminal output unless there's an error except subprocess.CalledProcessError as exc: - err = exc.stderr.decode('utf-8').strip() + err = exc.stderr.decode("utf-8").strip() - if not err.endswith('already exists and is not an empty directory.'): + if not err.endswith("already exists and is not an empty directory."): # don't raise error if the plugin folder exists raise DownloadError(error) from exc async def load_plugin(self, username, repo, plugin_name): - ext = f'plugins.{username}-{repo}.{plugin_name}.{plugin_name}' - dirname = f'plugins/{username}-{repo}/{plugin_name}' + ext = f"plugins.{username}-{repo}.{plugin_name}.{plugin_name}" + dirname = f"plugins/{username}-{repo}/{plugin_name}" - if 'requirements.txt' in os.listdir(dirname): + if "requirements.txt" in os.listdir(dirname): # Install PIP requirements try: await self.bot.loop.run_in_executor( - None, self._asubprocess_run, - f'python3 -m pip install -U -r {dirname}/' - 'requirements.txt --user -q -q' + None, + self._asubprocess_run, + f"python3 -m pip install -U -r {dirname}/" + "requirements.txt --user -q -q", ) # -q -q (quiet) # so there's no terminal output unless there's an error except subprocess.CalledProcessError as exc: - err = exc.stderr.decode('utf8').strip() + err = exc.stderr.decode("utf8").strip() if err: raise DownloadError( - f'Unable to download requirements: ```\n{error}\n```' + f"Unable to download requirements: ```\n{error}\n```" ) from exc else: if not os.path.exists(site.USER_SITE): @@ -130,66 +134,65 @@ async def load_plugin(self, username, repo, plugin_name): try: self.bot.load_extension(ext) except commands.ExtensionError as exc: - raise DownloadError('Invalid plugin') from exc + raise DownloadError("Invalid plugin") from exc else: - msg = f'Loaded plugins.{username}-{repo}.{plugin_name}' + msg = f"Loaded plugins.{username}-{repo}.{plugin_name}" logger.info(info(msg)) - @commands.group(aliases=['plugins'], invoke_without_command=True) + @commands.group(aliases=["plugins"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def plugin(self, ctx): """Plugin handler. Controls the plugins in the bot.""" await ctx.send_help(ctx.command) - @plugin.command(name='add', aliases=['install']) + @plugin.command(name="add", aliases=["install"]) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_add(self, ctx, *, plugin_name: str): """Add a plugin.""" if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details['repository'] + '/' + plugin_name - required_version = details['bot_version'] + plugin_name = details["repository"] + "/" + plugin_name + required_version = details["bot_version"] if parse_version(self.bot.version) < parse_version(required_version): embed = discord.Embed( - description=f'Your bot\'s version is too low. This plugin requires version `{required_version}`.', - color=self.bot.main_color + description=f"Your bot's version is too low. This plugin requires version `{required_version}`.", + color=self.bot.main_color, ) return await ctx.send(embed=embed) if plugin_name in self.bot.config.plugins: embed = discord.Embed( - description='This plugin is already installed.', - color=self.bot.main_color + description="This plugin is already installed.", + color=self.bot.main_color, ) return await ctx.send(embed=embed) if plugin_name in self.bot.cogs.keys(): # another class with the same name embed = discord.Embed( - description='There\'s another cog installed with the same name.', - color=self.bot.main_color + description="There's another cog installed with the same name.", + color=self.bot.main_color, ) return await ctx.send(embed=embed) embed = discord.Embed( - description='Downloading this plugin...', - color=self.bot.main_color + description="Downloading this plugin...", color=self.bot.main_color ) message = await ctx.send(embed=embed) async with ctx.typing(): - if len(plugin_name.split('/')) >= 3: + if len(plugin_name.split("/")) >= 3: parsed_plugin = self.parse_plugin(plugin_name) try: await self.download_plugin_repo(*parsed_plugin[:-1]) except DownloadError as exc: embed = discord.Embed( - description=f'Unable to fetch this plugin from Github: {exc}.', - color=self.bot.main_color + description=f"Unable to fetch this plugin from Github: {exc}.", + color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -199,8 +202,8 @@ async def plugin_add(self, ctx, *, plugin_name: str): await self.load_plugin(*parsed_plugin) except Exception as exc: embed = discord.Embed( - description=f'Unable to load this plugin: {exc}.', - color=self.bot.main_color + description=f"Unable to load this plugin: {exc}.", + color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -211,32 +214,32 @@ async def plugin_add(self, ctx, *, plugin_name: str): await self.bot.config.update() embed = discord.Embed( - description='The plugin is installed.\n' - '*Please note: any plugin that you install is of your OWN RISK*', - color=self.bot.main_color + description="The plugin is installed.\n" + "*Please note: any plugin that you install is of your OWN RISK*", + color=self.bot.main_color, ) await ctx.send(embed=embed) else: embed = discord.Embed( - description='Invalid plugin name format: use username/repo/plugin.', - color=self.bot.main_color + description="Invalid plugin name format: use username/repo/plugin.", + color=self.bot.main_color, ) await ctx.send(embed=embed) - @plugin.command(name='remove', aliases=['del', 'delete', 'rm']) + @plugin.command(name="remove", aliases=["del", "delete", "rm"]) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_remove(self, ctx, *, plugin_name: str): """Remove a plugin.""" if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details['repository'] + '/' + plugin_name + plugin_name = details["repository"] + "/" + plugin_name if plugin_name in self.bot.config.plugins: try: username, repo, name = self.parse_plugin(plugin_name) - self.bot.unload_extension(f'plugins.{username}-{repo}.{name}.{name}') + self.bot.unload_extension(f"plugins.{username}-{repo}.{name}.{name}") except Exception: pass @@ -244,8 +247,9 @@ async def plugin_remove(self, ctx, *, plugin_name: str): try: # BUG: Local variables 'username' and 'repo' might be referenced before assignment - if not any(i.startswith(f'{username}/{repo}') - for i in self.bot.config.plugins): + if not any( + i.startswith(f"{username}/{repo}") for i in self.bot.config.plugins + ): # if there are no more of such repos, delete the folder def onerror(func, path, exc_info): # pylint: disable=W0613 if not os.access(path, os.W_OK): @@ -253,7 +257,7 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 os.chmod(path, stat.S_IWUSR) func(path) - shutil.rmtree(f'plugins/{username}-{repo}', onerror=onerror) + shutil.rmtree(f"plugins/{username}-{repo}", onerror=onerror) except Exception as exc: logger.error(str(exc)) self.bot.config.plugins.append(plugin_name) @@ -262,30 +266,28 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 await self.bot.config.update() embed = discord.Embed( - description='The plugin is uninstalled and all its data is erased.', - color=self.bot.main_color + description="The plugin is uninstalled and all its data is erased.", + color=self.bot.main_color, ) await ctx.send(embed=embed) else: embed = discord.Embed( - description='That plugin is not installed.', - color=self.bot.main_color + description="That plugin is not installed.", color=self.bot.main_color ) await ctx.send(embed=embed) - @plugin.command(name='update') + @plugin.command(name="update") @checks.has_permissions(PermissionLevel.OWNER) async def plugin_update(self, ctx, *, plugin_name: str): """Update a plugin.""" if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details['repository'] + '/' + plugin_name + plugin_name = details["repository"] + "/" + plugin_name if plugin_name not in self.bot.config.plugins: embed = discord.Embed( - description='That plugin is not installed.', - color=self.bot.main_color + description="That plugin is not installed.", color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -293,60 +295,59 @@ async def plugin_update(self, ctx, *, plugin_name: str): username, repo, name = self.parse_plugin(plugin_name) try: - cmd = f'cd plugins/{username}-{repo} && git pull' - cmd = await self.bot.loop.run_in_executor(None, self._asubprocess_run, cmd) + cmd = f"cd plugins/{username}-{repo} && git pull" + cmd = await self.bot.loop.run_in_executor( + None, self._asubprocess_run, cmd + ) except subprocess.CalledProcessError as exc: - err = exc.stderr.decode('utf8').strip() + err = exc.stderr.decode("utf8").strip() embed = discord.Embed( - description=f'An error occured while updating: {err}.', - color=self.bot.main_color + description=f"An error occured while updating: {err}.", + color=self.bot.main_color, ) await ctx.send(embed=embed) else: - output = cmd.stdout.decode('utf8').strip() + output = cmd.stdout.decode("utf8").strip() embed = discord.Embed( - description=f'```\n{output}\n```', - color=self.bot.main_color + description=f"```\n{output}\n```", color=self.bot.main_color ) await ctx.send(embed=embed) - if output != 'Already up to date.': + if output != "Already up to date.": # repo was updated locally, now perform the cog reload - ext = f'plugins.{username}-{repo}.{name}.{name}' + ext = f"plugins.{username}-{repo}.{name}.{name}" self.bot.unload_extension(ext) try: await self.load_plugin(username, repo, name) except DownloadError as exc: em = discord.Embed( - description=f'Unable to start the plugin: `{exc}`.', - color=self.bot.main_color + description=f"Unable to start the plugin: `{exc}`.", + color=self.bot.main_color, ) await ctx.send(embed=em) - @plugin.command(name='enabled', aliases=['installed']) + @plugin.command(name="enabled", aliases=["installed"]) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_enabled(self, ctx): """Shows a list of currently enabled plugins.""" if self.bot.config.plugins: - msg = '```\n' + '\n'.join(self.bot.config.plugins) + '\n```' - embed = discord.Embed( - description=msg, - color=self.bot.main_color - ) + msg = "```\n" + "\n".join(self.bot.config.plugins) + "\n```" + embed = discord.Embed(description=msg, color=self.bot.main_color) await ctx.send(embed=embed) else: embed = discord.Embed( - description='There are no plugins installed.', - color=self.bot.main_color + description="There are no plugins installed.", color=self.bot.main_color ) await ctx.send(embed=embed) - @plugin.group(invoke_without_command=True, name='registry', aliases=['list', 'info']) + @plugin.group( + invoke_without_command=True, name="registry", aliases=["list", "info"] + ) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_registry(self, ctx, *, plugin_name: str = None): """Shows a list of all approved plugins.""" @@ -369,13 +370,16 @@ def find_index(find_name): elif plugin_name is not None: embed = discord.Embed( color=discord.Color.red(), - description=f'Could not find a plugin with name "{plugin_name}" within the registry.' + description=f'Could not find a plugin with name "{plugin_name}" within the registry.', ) matches = get_close_matches(plugin_name, self.registry.keys()) if matches: - embed.add_field(name='Perhaps you meant:', value='\n'.join(f'`{m}`' for m in matches)) + embed.add_field( + name="Perhaps you meant:", + value="\n".join(f"`{m}`" for m in matches), + ) return await ctx.send(embed=embed) @@ -385,20 +389,22 @@ def find_index(find_name): embed = discord.Embed( color=self.bot.main_color, - description=details['description'], + description=details["description"], url=repo, - title=details['repository'] + title=details["repository"], ) embed.add_field( - name='Installation', - value=f'```{self.bot.prefix}plugins add {name}```') + name="Installation", value=f"```{self.bot.prefix}plugins add {name}```" + ) - embed.set_author(name=details['title'], icon_url=details.get('icon_url'), url=url) - if details.get('thumbnail_url'): - embed.set_thumbnail(url=details.get('thumbnail_url')) - if details.get('image_url'): - embed.set_image(url=details.get('image_url')) + embed.set_author( + name=details["title"], icon_url=details.get("icon_url"), url=url + ) + if details.get("thumbnail_url"): + embed.set_thumbnail(url=details.get("thumbnail_url")) + if details.get("image_url"): + embed.set_image(url=details.get("image_url")) embeds.append(embed) @@ -406,7 +412,7 @@ def find_index(find_name): paginator.current = index await paginator.run() - @plugin_registry.command(name='compact') + @plugin_registry.command(name="compact") async def plugin_registry_compact(self, ctx): """Shows a compact view of all plugins within the registry.""" @@ -415,28 +421,25 @@ async def plugin_registry_compact(self, ctx): registry = list(self.registry.items()) registry.sort(key=lambda elem: elem[0]) - pages = [''] + pages = [""] for name, details in registry: repo = f"https://github.com/{details['repository']}" url = f"{repo}/tree/master/{name}" - desc = details['description'].replace('\n', '') + desc = details["description"].replace("\n", "") fmt = f"[`{name}`]({url}) - {desc}" length = len(fmt) - len(url) - 4 - fmt = fmt[:75 + len(url)].strip() + '...' if length > 75 else fmt + fmt = fmt[: 75 + len(url)].strip() + "..." if length > 75 else fmt if len(fmt) + len(pages[-1]) >= 2048: - pages.append(fmt+'\n') + pages.append(fmt + "\n") else: - pages[-1] += fmt + '\n' + pages[-1] += fmt + "\n" embeds = [] for page in pages: - embed = discord.Embed( - color=self.bot.main_color, - description=page, - ) - embed.set_author(name='Plugin Registry', icon_url=self.bot.user.avatar_url) + embed = discord.Embed(color=self.bot.main_color, description=page) + embed.set_author(name="Plugin Registry", icon_url=self.bot.user.avatar_url) embeds.append(embed) paginator = PaginatorSession(ctx, *embeds) diff --git a/cogs/utility.py b/cogs/utility.py index adff8ec59f..d2b56627eb 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -11,13 +11,13 @@ from json import JSONDecodeError from textwrap import indent +from pkg_resources import parse_version + from discord import Embed, Color, Activity, Role from discord.enums import ActivityType, Status from discord.ext import commands from aiohttp import ClientResponseError -from pkg_resources import parse_version - from core import checks from core.changelog import Changelog from core.decorators import github_access_token_required, trigger_typing @@ -25,7 +25,7 @@ from core.paginator import PaginatorSession, MessagePaginatorSession from core.utils import cleanup_code, info, error, User, get_perm_level -logger = logging.getLogger('Modmail') +logger = logging.getLogger("Modmail") class ModmailHelpCommand(commands.HelpCommand): @@ -33,15 +33,17 @@ async def format_cog_help(self, cog): bot = self.context.bot prefix = self.clean_prefix - formats = [''] - for cmd in await self.filter_commands(cog.get_commands(), sort=True, key=get_perm_level): + formats = [""] + for cmd in await self.filter_commands( + cog.get_commands(), sort=True, key=get_perm_level + ): perm_level = get_perm_level(cmd) if perm_level is PermissionLevel.INVALID: - format_ = f'`{prefix + cmd.qualified_name}` ' + format_ = f"`{prefix + cmd.qualified_name}` " else: - format_ = f'`[{perm_level}] {prefix + cmd.qualified_name}` ' + format_ = f"`[{perm_level}] {prefix + cmd.qualified_name}` " - format_ += f'- {cmd.short_doc}\n' + format_ += f"- {cmd.short_doc}\n" if not format_.strip(): continue if len(format_) + len(formats[-1]) >= 1024: @@ -53,36 +55,46 @@ async def format_cog_help(self, cog): for format_ in formats: embed = Embed( description=f'*{cog.description or "No description."}*', - color=bot.main_color + color=bot.main_color, ) - embed.add_field(name='Commands', value=format_ or 'No commands.') + embed.add_field(name="Commands", value=format_ or "No commands.") - continued = ' (Continued)' if embeds else '' - embed.set_author(name=cog.qualified_name + ' - Help' + continued, - icon_url=bot.user.avatar_url) + continued = " (Continued)" if embeds else "" + embed.set_author( + name=cog.qualified_name + " - Help" + continued, + icon_url=bot.user.avatar_url, + ) - embed.set_footer(text=f'Type "{prefix}{self.command_attrs["name"]} command" ' - 'for more info on a specific command.') + embed.set_footer( + text=f'Type "{prefix}{self.command_attrs["name"]} command" ' + "for more info on a specific command." + ) embeds.append(embed) return embeds def process_help_msg(self, help_: str): - return help_.format(prefix=self.clean_prefix) if help_ else 'No help message.' + return help_.format(prefix=self.clean_prefix) if help_ else "No help message." async def send_bot_help(self, mapping): embeds = [] # TODO: Implement for no cog commands - for cog in sorted((key for key in mapping.keys() if key is not None), - key=lambda c: c.qualified_name): + for cog in sorted( + (key for key in mapping.keys() if key is not None), + key=lambda c: c.qualified_name, + ): embeds.extend(await self.format_cog_help(cog)) - p_session = PaginatorSession(self.context, *embeds, destination=self.get_destination()) + p_session = PaginatorSession( + self.context, *embeds, destination=self.get_destination() + ) return await p_session.run() async def send_cog_help(self, cog): embeds = await self.format_cog_help(cog) - p_session = PaginatorSession(self.context, *embeds, destination=self.get_destination()) + p_session = PaginatorSession( + self.context, *embeds, destination=self.get_destination() + ) return await p_session.run() async def send_command_help(self, command): @@ -90,16 +102,16 @@ async def send_command_help(self, command): return perm_level = get_perm_level(command) if perm_level is not PermissionLevel.INVALID: - perm_level = f'{perm_level.name} [{perm_level}]' + perm_level = f"{perm_level.name} [{perm_level}]" else: - perm_level = '' + perm_level = "" embed = Embed( - title=f'`{self.get_command_signature(command)}`', + title=f"`{self.get_command_signature(command)}`", color=self.context.bot.main_color, - description=self.process_help_msg(command.help) + description=self.process_help_msg(command.help), ) - embed.set_footer(text=f'Permission level: {perm_level}') + embed.set_footer(text=f"Permission level: {perm_level}") await self.get_destination().send(embed=embed) async def send_group_help(self, group): @@ -108,60 +120,64 @@ async def send_group_help(self, group): perm_level = get_perm_level(group) if perm_level is not PermissionLevel.INVALID: - perm_level = f'{perm_level.name} [{perm_level}]' + perm_level = f"{perm_level.name} [{perm_level}]" else: - perm_level = '' + perm_level = "" embed = Embed( - title=f'`{self.get_command_signature(group)}`', + title=f"`{self.get_command_signature(group)}`", color=self.context.bot.main_color, - description=self.process_help_msg(group.help) + description=self.process_help_msg(group.help), ) - embed.add_field(name='Permission level', value=perm_level, inline=False) - format_ = '' + embed.add_field(name="Permission level", value=perm_level, inline=False) + format_ = "" length = len(group.commands) for i, command in enumerate( - await self.filter_commands(group.commands, sort=True, key=lambda c: c.name) + await self.filter_commands(group.commands, sort=True, key=lambda c: c.name) ): # BUG: fmt may run over the embed limit # TODO: paginate this if length == i + 1: # last - branch = '└─' + branch = "└─" else: - branch = '├─' - format_ += f'`{branch} {command.name}` - {command.short_doc}\n' + branch = "├─" + format_ += f"`{branch} {command.name}` - {command.short_doc}\n" - embed.add_field(name='Sub Commands', value=format_, inline=False) + embed.add_field(name="Sub Commands", value=format_, inline=False) embed.set_footer( text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' - 'for more info on a command.' + "for more info on a command." ) await self.get_destination().send(embed=embed) async def send_error_message(self, msg): # pylint: disable=W0221 - logger.warning(error(f'CommandNotFound: {msg}')) + logger.warning(error(f"CommandNotFound: {msg}")) - embed = Embed( - color=Color.red() + embed = Embed(color=Color.red()) + embed.set_footer( + text=f'Command/Category "{self.context.kwargs.get("command")}" not found.' ) - embed.set_footer(text=f'Command/Category "{self.context.kwargs.get("command")}" not found.') choices = set() for name, cmd in self.context.bot.all_commands.items(): if not cmd.hidden: choices.add(name) - command = self.context.kwargs.get('command') + command = self.context.kwargs.get("command") 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=f"Perhaps you meant:", value="\n".join(f"`{x}`" for x in closest) + ) else: - embed.title = 'Cannot find command or category' - embed.set_footer(text=f'Type "{self.clean_prefix}{self.command_attrs["name"]}" ' - 'for a list of all available commands.') + embed.title = "Cannot find command or category" + embed.set_footer( + text=f'Type "{self.clean_prefix}{self.command_attrs["name"]}" ' + "for a list of all available commands." + ) await self.get_destination().send(embed=embed) @@ -172,8 +188,7 @@ def __init__(self, bot): self.bot = bot self._original_help_command = bot.help_command self.bot.help_command = ModmailHelpCommand( - verify_checks=False, - command_attrs={'help': 'Shows this help message.'} + verify_checks=False, command_attrs={"help": "Shows this help message."} ) # Looks a bit ugly self.bot.help_command._command_impl = checks.has_permissions( @@ -197,29 +212,25 @@ async def changelog(self, ctx): except: await ctx.send(changelog.CHANGELOG_URL) - @commands.command(aliases=['bot', 'info']) + @commands.command(aliases=["bot", "info"]) @checks.has_permissions(PermissionLevel.REGULAR) @trigger_typing async def about(self, ctx): """Shows information about this bot.""" - embed = Embed(color=self.bot.main_color, - timestamp=datetime.utcnow()) - embed.set_author(name='Modmail - About', - icon_url=self.bot.user.avatar_url) + embed = Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) + embed.set_author(name="Modmail - About", icon_url=self.bot.user.avatar_url) embed.set_thumbnail(url=self.bot.user.avatar_url) - desc = 'This is an open source Discord bot that serves as a means for ' - desc += 'members to easily communicate with server administrators in ' - desc += 'an organised manner.' + desc = "This is an open source Discord bot that serves as a means for " + desc += "members to easily communicate with server administrators in " + desc += "an organised manner." embed.description = desc - embed.add_field(name='Uptime', value=self.bot.uptime) - embed.add_field(name='Latency', value=f'{self.bot.latency * 1000:.2f} ms') + embed.add_field(name="Uptime", value=self.bot.uptime) + embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms") - embed.add_field(name='Version', - value=f'`{self.bot.version}`') - embed.add_field(name='Author', - value='[`kyb3r`](https://github.com/kyb3r)') + embed.add_field(name="Version", value=f"`{self.bot.version}`") + embed.add_field(name="Author", value="[`kyb3r`](https://github.com/kyb3r)") changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version @@ -227,14 +238,16 @@ async def about(self, ctx): if parse_version(self.bot.version) < parse_version(latest.version): footer = f"A newer version is available v{latest.version}" else: - footer = 'You are up to date with the latest version.' + footer = "You are up to date with the latest version." - embed.add_field(name='GitHub', - value='https://github.com/kyb3r/modmail', - inline=False) + embed.add_field( + name="GitHub", value="https://github.com/kyb3r/modmail", inline=False + ) - embed.add_field(name='\u200b', - value='Support this bot on [Patreon](https://patreon.com/kyber).') + embed.add_field( + name="\u200b", + value="Support this bot on [Patreon](https://patreon.com/kyber).", + ) embed.set_footer(text=footer) await ctx.send(embed=embed) @@ -245,93 +258,105 @@ async def about(self, ctx): async def debug(self, ctx): """Shows the recent application-logs of the bot.""" - with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), - '../temp/logs.log'), 'r+') as f: + with open( + os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../temp/logs.log" + ), + "r+", + ) as f: logs = f.read().strip() if not logs: embed = Embed( color=self.bot.main_color, - title='Debug Logs:', - description='You don\'t have any logs at the moment.' + title="Debug Logs:", + description="You don't have any logs at the moment.", ) - embed.set_footer(text='Go to Heroku to see your logs.') + embed.set_footer(text="Go to Heroku to see your logs.") return await ctx.send(embed=embed) messages = [] # Using Scala formatting because it's similar to Python for exceptions # and it does a fine job formatting the logs. - msg = '```Scala\n' + msg = "```Scala\n" for line in logs.splitlines(keepends=True): - if msg != '```Scala\n': + if msg != "```Scala\n": if len(line) + len(msg) + 3 > 2000: - msg += '```' + msg += "```" messages.append(msg) - msg = '```Scala\n' + msg = "```Scala\n" msg += line if len(msg) + 3 > 2000: - msg = msg[:1993] + '[...]```' + msg = msg[:1993] + "[...]```" messages.append(msg) - msg = '```Scala\n' + msg = "```Scala\n" - if msg != '```Scala\n': - msg += '```' + if msg != "```Scala\n": + msg += "```" messages.append(msg) - embed = Embed( - color=self.bot.main_color - ) - embed.set_footer(text='Debug logs - Navigate using the reactions below.') + embed = Embed(color=self.bot.main_color) + embed.set_footer(text="Debug logs - Navigate using the reactions below.") session = MessagePaginatorSession(ctx, *messages, embed=embed) session.current = len(messages) - 1 return await session.run() - @debug.command(name='hastebin', aliases=['haste']) + @debug.command(name="hastebin", aliases=["haste"]) @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing async def debug_hastebin(self, ctx): """Posts application-logs to Hastebin.""" - haste_url = os.environ.get('HASTE_URL', 'https://hasteb.in') + haste_url = os.environ.get("HASTE_URL", "https://hasteb.in") - with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), - '../temp/logs.log'), 'r+') as f: + with open( + os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../temp/logs.log" + ), + "r+", + ) as f: logs = 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: key = (await resp.json())["key"] embed = Embed( - title='Debug Logs', + title="Debug Logs", color=self.bot.main_color, - description=f'{haste_url}/' + key + description=f"{haste_url}/" + key, ) except (JSONDecodeError, ClientResponseError, IndexError): embed = Embed( - title='Debug Logs', + 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.') + embed.set_footer(text="Go to Heroku to see your logs.") await ctx.send(embed=embed) - @debug.command(name='clear', aliases=['wipe']) + @debug.command(name="clear", aliases=["wipe"]) @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing async def debug_clear(self, ctx): """Clears the locally cached logs.""" - with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), - '../temp/logs.log'), 'w'): + with open( + os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../temp/logs.log" + ), + "w", + ): pass - await ctx.send(embed=Embed( - color=self.bot.main_color, - description='Cached logs are now cleared.' - )) + await ctx.send( + embed=Embed( + color=self.bot.main_color, description="Cached logs are now cleared." + ) + ) @commands.command() @checks.has_permissions(PermissionLevel.OWNER) @@ -342,22 +367,20 @@ async def github(self, ctx): data = await self.bot.api.get_user_info() embed = Embed( - title='GitHub', - description='Current User', - color=self.bot.main_color + title="GitHub", description="Current User", color=self.bot.main_color ) - user = data['user'] - embed.set_author(name=user['username'], - icon_url=user['avatar_url'], - url=user['url']) - embed.set_thumbnail(url=user['avatar_url']) + user = data["user"] + embed.set_author( + name=user["username"], icon_url=user["avatar_url"], url=user["url"] + ) + embed.set_thumbnail(url=user["avatar_url"]) await ctx.send(embed=embed) @commands.command() @checks.has_permissions(PermissionLevel.OWNER) @github_access_token_required @trigger_typing - async def update(self, ctx, *, flag: str = ''): + async def update(self, ctx, *, flag: str = ""): """ Update Modmail. @@ -370,61 +393,69 @@ async def update(self, ctx, *, flag: str = ''): changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version - desc = (f'The latest version is [`{self.bot.version}`]' - '(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)') + desc = ( + f"The latest version is [`{self.bot.version}`]" + "(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)" + ) - if parse_version(self.bot.version) >= parse_version(latest.version) and flag.lower() != 'force': + if ( + parse_version(self.bot.version) >= parse_version(latest.version) + and flag.lower() != "force" + ): embed = Embed( - title='Already up to date', - description=desc, - color=self.bot.main_color + title="Already up to date", description=desc, color=self.bot.main_color ) data = await self.bot.api.get_user_info() - if not data.get('error'): - user = data['user'] - embed.set_author(name=user['username'], - icon_url=user['avatar_url'], - url=user['url']) + if not data.get("error"): + user = data["user"] + embed.set_author( + name=user["username"], icon_url=user["avatar_url"], url=user["url"] + ) else: data = await self.bot.api.update_repository() - commit_data = data['data'] - user = data['user'] + commit_data = data["data"] + user = data["user"] if commit_data: embed = Embed(color=self.bot.main_color) - embed.set_footer(text=f'Updating Modmail v{self.bot.version} ' - f'-> v{latest.version}') + embed.set_footer( + text=f"Updating Modmail v{self.bot.version} " + f"-> v{latest.version}" + ) - embed.set_author(name=user['username'] + ' - Updating bot', - icon_url=user['avatar_url'], - url=user['url']) + embed.set_author( + name=user["username"] + " - Updating bot", + icon_url=user["avatar_url"], + url=user["url"], + ) embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=value) # message = commit_data['commit']['message'] html_url = commit_data["html_url"] - short_sha = commit_data['sha'][:6] - embed.add_field(name='Merge Commit', - value=f'[`{short_sha}`]({html_url})') + short_sha = commit_data["sha"][:6] + embed.add_field( + name="Merge Commit", value=f"[`{short_sha}`]({html_url})" + ) else: embed = Embed( - title='Already up to date with master repository.', - description='No further updates required', - color=self.bot.main_color + title="Already up to date with master repository.", + description="No further updates required", + color=self.bot.main_color, + ) + embed.set_author( + name=user["username"], icon_url=user["avatar_url"], url=user["url"] ) - embed.set_author(name=user['username'], - icon_url=user['avatar_url'], - url=user['url']) return await ctx.send(embed=embed) - @commands.command(aliases=['presence']) + @commands.command(aliases=["presence"]) @checks.has_permissions(PermissionLevel.ADMINISTRATOR) - async def activity(self, ctx, activity_type: str.lower, *, message: str = ''): + async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): """ Set an activity status for the bot. @@ -444,36 +475,33 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ''): To remove the current activity status: - `{prefix}activity clear` """ - if activity_type == 'clear': - self.bot.config['activity_type'] = None - self.bot.config['activity_message'] = None + if activity_type == "clear": + self.bot.config["activity_type"] = None + self.bot.config["activity_message"] = None await self.bot.config.update() await self.set_presence() - embed = Embed( - title='Activity Removed', - color=self.bot.main_color - ) + embed = Embed(title="Activity Removed", color=self.bot.main_color) return await ctx.send(embed=embed) if not message: - raise commands.MissingRequiredArgument(param(name='message')) + raise commands.MissingRequiredArgument(param(name="message")) - activity, msg = (await self.set_presence( - activity_identifier=activity_type, - activity_by_key=True, - activity_message=message - ))['activity'] + activity, msg = ( + await self.set_presence( + activity_identifier=activity_type, + activity_by_key=True, + activity_message=message, + ) + )["activity"] if activity is None: - raise commands.MissingRequiredArgument(param(name='activity')) + raise commands.MissingRequiredArgument(param(name="activity")) - self.bot.config['activity_type'] = activity.type.value - self.bot.config['activity_message'] = message + self.bot.config["activity_type"] = activity.type.value + self.bot.config["activity_message"] = message await self.bot.config.update() embed = Embed( - title='Activity Changed', - description=msg, - color=self.bot.main_color + title="Activity Changed", description=msg, color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -493,44 +521,41 @@ async def status(self, ctx, *, status_type: str.lower): To remove the current status: - `{prefix}status clear` """ - if status_type == 'clear': - self.bot.config['status'] = None + if status_type == "clear": + self.bot.config["status"] = None await self.bot.config.update() await self.set_presence() - embed = Embed( - title='Status Removed', - color=self.bot.main_color - ) + embed = Embed(title="Status Removed", color=self.bot.main_color) return await ctx.send(embed=embed) - status_type = status_type.replace(' ', '_') + status_type = status_type.replace(" ", "_") - status, msg = (await self.set_presence( - status_identifier=status_type, - status_by_key=True - ))['status'] + status, msg = ( + await self.set_presence(status_identifier=status_type, status_by_key=True) + )["status"] if status is None: - raise commands.MissingRequiredArgument(param(name='status')) + raise commands.MissingRequiredArgument(param(name="status")) - self.bot.config['status'] = status.value + self.bot.config["status"] = status.value await self.bot.config.update() embed = Embed( - title='Status Changed', - description=msg, - color=self.bot.main_color + title="Status Changed", description=msg, color=self.bot.main_color ) return await ctx.send(embed=embed) - async def set_presence(self, *, - status_identifier=None, - status_by_key=True, - activity_identifier=None, - activity_by_key=True, - activity_message=None): + async def set_presence( + self, + *, + status_identifier=None, + status_by_key=True, + activity_identifier=None, + activity_by_key=True, + activity_message=None, + ): activity = status = None if status_identifier is None: - status_identifier = self.bot.config.get('status', None) + status_identifier = self.bot.config.get("status", None) status_by_key = False try: @@ -540,14 +565,15 @@ async def set_presence(self, *, status = Status(status_identifier) except (KeyError, ValueError): if status_identifier is not None: - msg = f'Invalid status type: {status_identifier}' + msg = f"Invalid status type: {status_identifier}" logger.warning(error(msg)) if activity_identifier is None: if activity_message is not None: - raise ValueError('activity_message must be None ' - 'if activity_identifier is None.') - activity_identifier = self.bot.config.get('activity_type', None) + raise ValueError( + "activity_message must be None " "if activity_identifier is None." + ) + activity_identifier = self.bot.config.get("activity_type", None) activity_by_key = False try: @@ -557,45 +583,44 @@ async def set_presence(self, *, activity_type = ActivityType(activity_identifier) except (KeyError, ValueError): if activity_identifier is not None: - msg = f'Invalid activity type: {activity_identifier}' + msg = f"Invalid activity type: {activity_identifier}" logger.warning(error(msg)) else: url = None activity_message = ( - activity_message or - self.bot.config.get('activity_message', '') + activity_message or self.bot.config.get("activity_message", "") ).strip() if activity_type == ActivityType.listening: - if activity_message.lower().startswith('to '): + if activity_message.lower().startswith("to "): # The actual message is after listening to [...] # discord automatically add the "to" activity_message = activity_message[3:].strip() elif activity_type == ActivityType.streaming: url = self.bot.config.get( - 'twitch_url', 'https://www.twitch.tv/discord-modmail/' + "twitch_url", "https://www.twitch.tv/discord-modmail/" ) if activity_message: - activity = Activity(type=activity_type, - name=activity_message, - url=url) + activity = Activity(type=activity_type, name=activity_message, url=url) else: - msg = 'You must supply an activity message to use custom activity.' + msg = "You must supply an activity message to use custom activity." logger.warning(error(msg)) await self.bot.change_presence(activity=activity, status=status) - presence = {'activity': (None, 'No activity has been set.'), - 'status': (None, 'No status has been set.')} + presence = { + "activity": (None, "No activity has been set."), + "status": (None, "No status has been set."), + } if activity is not None: - use_to = 'to ' if activity.type == ActivityType.listening else '' - msg = f'Activity set to: {activity.type.name.capitalize()} ' - msg += f'{use_to}{activity.name}.' - presence['activity'] = (activity, msg) + use_to = "to " if activity.type == ActivityType.listening else "" + msg = f"Activity set to: {activity.type.name.capitalize()} " + msg += f"{use_to}{activity.name}." + presence["activity"] = (activity, msg) if status is not None: - msg = f'Status set to: {status.value}.' - presence['status'] = (status, msg) + msg = f"Status set to: {status.value}." + presence["status"] = (status, msg) return presence @commands.Cog.listener() @@ -603,8 +628,8 @@ async def on_ready(self): # Wait until config cache is populated with stuff from db await self.bot.config.wait_until_ready() presence = await self.set_presence() - logger.info(info(presence['activity'][1])) - logger.info(info(presence['status'][1])) + logger.info(info(presence["activity"][1])) + logger.info(info(presence["status"][1])) @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) @@ -612,9 +637,9 @@ async def on_ready(self): async def ping(self, ctx): """Pong! Returns your websocket latency.""" embed = Embed( - title='Pong! Websocket Latency:', - description=f'{self.bot.ws.latency * 1000:.4f} ms', - color=self.bot.main_color + title="Pong! Websocket Latency:", + description=f"{self.bot.ws.latency * 1000:.4f} ms", + color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -626,21 +651,21 @@ async def mention(self, ctx, *, mention: str = None): Type only `{prefix}mention` to retrieve your current "mention" message. """ - current = self.bot.config.get('mention', '@here') + current = self.bot.config.get("mention", "@here") if mention is None: embed = Embed( - title='Current text', + title="Current text", color=self.bot.main_color, - description=str(current) + description=str(current), ) else: embed = Embed( - title='Changed mention!', - description=f'On thread creation the bot now says {mention}.', - color=self.bot.main_color + title="Changed mention!", + description=f"On thread creation the bot now says {mention}.", + color=self.bot.main_color, ) - self.bot.config['mention'] = mention + self.bot.config["mention"] = mention await self.bot.config.update() return await ctx.send(embed=embed) @@ -656,21 +681,19 @@ async def prefix(self, ctx, *, prefix=None): current = self.bot.prefix embed = Embed( - title='Current prefix', - color=self.bot.main_color, - description=f'{current}' + title="Current prefix", color=self.bot.main_color, description=f"{current}" ) if prefix is None: await ctx.send(embed=embed) else: - embed.title = 'Changed prefix!' - embed.description = f'Set prefix to `{prefix}`' - self.bot.config['prefix'] = prefix + embed.title = "Changed prefix!" + embed.description = f"Set prefix to `{prefix}`" + self.bot.config["prefix"] = prefix await self.bot.config.update() await ctx.send(embed=embed) - @commands.group(aliases=['configuration'], invoke_without_command=True) + @commands.group(aliases=["configuration"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def config(self, ctx): """ @@ -687,18 +710,16 @@ async def config(self, ctx): """ await ctx.send_help(ctx.command) - @config.command(name='options', aliases=['list']) + @config.command(name="options", aliases=["list"]) @checks.has_permissions(PermissionLevel.OWNER) async def config_options(self, ctx): """Return a list of valid configuration names you can change.""" allowed = self.bot.config.allowed_to_change_in_command - valid = ', '.join(f'`{k}`' for k in allowed) - embed = Embed(title='Valid Keys', - description=valid, - color=self.bot.main_color) + valid = ", ".join(f"`{k}`" for k in allowed) + embed = Embed(title="Valid Keys", description=valid, color=self.bot.main_color) return await ctx.send(embed=embed) - @config.command(name='set', aliases=['add']) + @config.command(name="set", aliases=["add"]) @checks.has_permissions(PermissionLevel.OWNER) async def config_set(self, ctx, key: str.lower, *, value: str): """Set a configuration variable and its value.""" @@ -713,22 +734,22 @@ async def config_set(self, ctx, key: str.lower, *, value: str): else: await self.bot.config.update({key: value}) embed = Embed( - title='Success', + title="Success", color=self.bot.main_color, - description=f'Set `{key}` to `{value_text}`' + description=f"Set `{key}` to `{value_text}`", ) else: embed = Embed( - title='Error', + title="Error", color=Color.red(), - description=f'{key} is an invalid key.' + description=f"{key} is an invalid key.", ) - valid_keys = [f'`{k}`' for k in keys] - embed.add_field(name='Valid keys', value=', '.join(valid_keys)) + valid_keys = [f"`{k}`" for k in keys] + embed.add_field(name="Valid keys", value=", ".join(valid_keys)) return await ctx.send(embed=embed) - @config.command(name='remove', aliases=['del', 'delete', 'rm']) + @config.command(name="remove", aliases=["del", "delete", "rm"]) @checks.has_permissions(PermissionLevel.OWNER) async def config_remove(self, ctx, key: str.lower): """Delete a set configuration variable.""" @@ -741,22 +762,22 @@ async def config_remove(self, ctx, key: str.lower): # when no values were set pass embed = Embed( - title='Success', + title="Success", color=self.bot.main_color, - description=f'`{key}` had been deleted from the config.' + description=f"`{key}` had been deleted from the config.", ) else: embed = Embed( - title='Error', + title="Error", color=Color.red(), - description=f'{key} is an invalid key.' + description=f"{key} is an invalid key.", ) - valid_keys = [f'`{k}`' for k in keys] - embed.add_field(name='Valid keys', value=', '.join(valid_keys)) + valid_keys = [f"`{k}`" for k in keys] + embed.add_field(name="Valid keys", value=", ".join(valid_keys)) return await ctx.send(embed=embed) - @config.command(name='get') + @config.command(name="get") @checks.has_permissions(PermissionLevel.OWNER) async def config_get(self, ctx, key: str.lower = None): """ @@ -768,43 +789,41 @@ async def config_get(self, ctx, key: str.lower = None): if key: if key in keys: - desc = f'`{key}` is set to `{self.bot.config.get(key)}`' - embed = Embed( - color=self.bot.main_color, - description=desc + desc = f"`{key}` is set to `{self.bot.config.get(key)}`" + embed = 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 = Embed( - title='Error', + title="Error", color=Color.red(), - description=f'`{key}` is an invalid key.' + description=f"`{key}` is an invalid key.", ) - valid_keys = [f'`{k}`' for k in keys] - embed.add_field(name='Valid keys', value=', '.join(valid_keys)) + valid_keys = [f"`{k}`" for k in keys] + embed.add_field(name="Valid keys", value=", ".join(valid_keys)) else: embed = Embed( color=self.bot.main_color, - description='Here is a list of currently ' - 'set configuration variables.' + description="Here is a list of currently " + "set configuration variables.", ) - embed.set_author(name='Current config', - icon_url=self.bot.user.avatar_url) + embed.set_author(name="Current config", icon_url=self.bot.user.avatar_url) config = { - key: val for key, val in self.bot.config.cache.items() + key: val + for key, val in self.bot.config.cache.items() if val and key in keys } for name, value in reversed(list(config.items())): - embed.add_field(name=name, value=f'`{value}`', inline=False) + embed.add_field(name=name, value=f"`{value}`", inline=False) return await ctx.send(embed=embed) - @commands.group(aliases=['aliases'], invoke_without_command=True) + @commands.group(aliases=["aliases"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) async def alias(self, ctx): """ @@ -826,30 +845,26 @@ async def alias(self, ctx): """ embeds = [] - desc = 'Here is a list of aliases that are currently configured.' + desc = "Here is a list of aliases that are currently configured." if self.bot.aliases: - embed = Embed( - color=self.bot.main_color, - description=desc - ) + embed = Embed(color=self.bot.main_color, description=desc) else: embed = Embed( color=self.bot.main_color, - description='You dont have any aliases at the moment.' + description="You dont have any aliases at the moment.", ) - embed.set_author(name='Command aliases', icon_url=ctx.guild.icon_url) - embed.set_footer(text=f'Do {self.bot.prefix}' - 'help aliases for more commands.') + embed.set_author(name="Command aliases", icon_url=ctx.guild.icon_url) + embed.set_footer(text=f"Do {self.bot.prefix}" "help aliases for more commands.") embeds.append(embed) for name, value in self.bot.aliases.items(): if len(embed.fields) == 5: embed = Embed(color=self.bot.main_color, description=desc) - embed.set_author(name='Command aliases', - icon_url=ctx.guild.icon_url) - embed.set_footer(text=f'Do {self.bot.prefix}help ' - 'aliases for more commands.') + embed.set_author(name="Command aliases", icon_url=ctx.guild.icon_url) + embed.set_footer( + text=f"Do {self.bot.prefix}help " "aliases for more commands." + ) embeds.append(embed) embed.add_field(name=name, value=value, inline=False) @@ -857,28 +872,28 @@ async def alias(self, ctx): session = PaginatorSession(ctx, *embeds) return await session.run() - @alias.command(name='add') + @alias.command(name="add") @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_add(self, ctx, name: str.lower, *, value): """Add an alias.""" - if 'aliases' not in self.bot.config.cache: - self.bot.config['aliases'] = {} + if "aliases" not in self.bot.config.cache: + self.bot.config["aliases"] = {} if self.bot.get_command(name) or self.bot.config.aliases.get(name): embed = Embed( - title='Error', + title="Error", color=Color.red(), - description='A command or alias already exists ' - f'with the same name: `{name}`.' + description="A command or alias already exists " + f"with the same name: `{name}`.", ) return await ctx.send(embed=embed) if not self.bot.get_command(value.split()[0]): embed = Embed( - title='Error', + title="Error", color=Color.red(), - description='The command you are attempting to point ' - f'to does not exist: `{value.split()[0]}`.' + description="The command you are attempting to point " + f"to does not exist: `{value.split()[0]}`.", ) return await ctx.send(embed=embed) @@ -886,41 +901,41 @@ async def alias_add(self, ctx, name: str.lower, *, value): await self.bot.config.update() embed = Embed( - title='Added alias', + title="Added alias", color=self.bot.main_color, - description=f'`{name}` points to: {value}' + description=f"`{name}` points to: {value}", ) return await ctx.send(embed=embed) - @alias.command(name='remove', aliases=['del', 'delete', 'rm']) + @alias.command(name="remove", aliases=["del", "delete", "rm"]) @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_remove(self, ctx, *, name: str.lower): """Remove an alias.""" - if 'aliases' not in self.bot.config.cache: - self.bot.config['aliases'] = {} + if "aliases" not in self.bot.config.cache: + self.bot.config["aliases"] = {} if self.bot.config.aliases.get(name): - del self.bot.config['aliases'][name] + del self.bot.config["aliases"][name] await self.bot.config.update() embed = Embed( - title='Removed alias', + title="Removed alias", color=self.bot.main_color, - description=f'`{name}` no longer exists.' + description=f"`{name}` no longer exists.", ) else: embed = Embed( - title='Error', + title="Error", color=Color.red(), - description=f'Alias `{name}` does not exist.' + description=f"Alias `{name}` does not exist.", ) return await ctx.send(embed=embed) - @commands.group(aliases=['perms'], invoke_without_command=True) + @commands.group(aliases=["perms"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def permissions(self, ctx): """ @@ -943,16 +958,17 @@ async def permissions(self, ctx): """ await ctx.send_help(ctx.command) - @permissions.group(name='add', invoke_without_command=True) + @permissions.group(name="add", invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add(self, ctx): """Add a permission to a command or a permission level.""" await ctx.send_help(ctx.command) - @permissions_add.command(name='command') + @permissions_add.command(name="command") @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_add_command(self, ctx, command: str, *, - user_or_role: Union[User, Role, str]): + async def permissions_add_command( + self, ctx, command: str, *, user_or_role: Union[User, Role, str] + ): """ Add a user, role, or everyone permission to use a command. @@ -961,32 +977,33 @@ async def permissions_add_command(self, ctx, command: str, *, """ if command not in self.bot.all_commands: embed = Embed( - title='Error', + title="Error", color=Color.red(), - description='The command you are attempting to point ' - f'to does not exist: `{command}`.' + description="The command you are attempting to point " + f"to does not exist: `{command}`.", ) return await ctx.send(embed=embed) - if hasattr(user_or_role, 'id'): + if hasattr(user_or_role, "id"): value = user_or_role.id - elif user_or_role in {'everyone', 'all'}: + elif user_or_role in {"everyone", "all"}: value = -1 else: raise commands.BadArgument(f'User or Role "{user_or_role}" not found') await self.bot.update_perms(self.bot.all_commands[command].name, value) embed = Embed( - title='Success', + title="Success", color=self.bot.main_color, - description=f'Permission for {command} was successfully updated.' + description=f"Permission for {command} was successfully updated.", ) return await ctx.send(embed=embed) - @permissions_add.command(name='level', aliases=['group']) + @permissions_add.command(name="level", aliases=["group"]) @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_add_level(self, ctx, level: str, *, - user_or_role: Union[User, Role, str]): + async def permissions_add_level( + self, ctx, level: str, *, user_or_role: Union[User, Role, str] + ): """ Add a user, role, or everyone permission to use commands of a permission level. @@ -995,39 +1012,43 @@ async def permissions_add_level(self, ctx, level: str, *, """ if level.upper() not in PermissionLevel.__members__: embed = Embed( - title='Error', + title="Error", color=Color.red(), - description='The permission level you are attempting to point ' - f'to does not exist: `{level}`.' + description="The permission level you are attempting to point " + f"to does not exist: `{level}`.", ) return await ctx.send(embed=embed) - if hasattr(user_or_role, 'id'): + if hasattr(user_or_role, "id"): value = user_or_role.id - elif user_or_role in {'everyone', 'all'}: + elif user_or_role in {"everyone", "all"}: value = -1 else: raise commands.BadArgument(f'User or Role "{user_or_role}" not found') await self.bot.update_perms(PermissionLevel[level.upper()], value) embed = Embed( - title='Success', + title="Success", color=self.bot.main_color, - description=f'Permission for {level} was successfully updated.' + description=f"Permission for {level} was successfully updated.", ) return await ctx.send(embed=embed) - @permissions.group(name='remove', aliases=['del', 'delete', 'rm', 'revoke'], - invoke_without_command=True) + @permissions.group( + name="remove", + aliases=["del", "delete", "rm", "revoke"], + invoke_without_command=True, + ) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove(self, ctx): """Remove permission to use a command or permission level.""" await ctx.send_help(ctx.command) - @permissions_remove.command(name='command') + @permissions_remove.command(name="command") @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_remove_command(self, ctx, command: str, *, - user_or_role: Union[User, Role, str]): + async def permissions_remove_command( + self, ctx, command: str, *, user_or_role: Union[User, Role, str] + ): """ Remove a user, role, or everyone permission to use a command. @@ -1036,32 +1057,35 @@ async def permissions_remove_command(self, ctx, command: str, *, """ if command not in self.bot.all_commands: embed = Embed( - title='Error', + title="Error", color=Color.red(), - description='The command you are attempting to point ' - f'to does not exist: `{command}`.' + description="The command you are attempting to point " + f"to does not exist: `{command}`.", ) return await ctx.send(embed=embed) - if hasattr(user_or_role, 'id'): + if hasattr(user_or_role, "id"): value = user_or_role.id - elif user_or_role in {'everyone', 'all'}: + elif user_or_role in {"everyone", "all"}: value = -1 else: raise commands.BadArgument(f'User or Role "{user_or_role}" not found') - await self.bot.update_perms(self.bot.all_commands[command].name, value, add=False) + await self.bot.update_perms( + self.bot.all_commands[command].name, value, add=False + ) embed = Embed( - title='Success', + title="Success", color=self.bot.main_color, - description=f'Permission for {command} was successfully updated.' + description=f"Permission for {command} was successfully updated.", ) return await ctx.send(embed=embed) - @permissions_remove.command(name='level', aliases=['group']) + @permissions_remove.command(name="level", aliases=["group"]) @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_remove_level(self, ctx, level: str, *, - user_or_role: Union[User, Role, str]): + async def permissions_remove_level( + self, ctx, level: str, *, user_or_role: Union[User, Role, str] + ): """ Remove a user, role, or everyone permission to use commands of a permission level. @@ -1070,29 +1094,29 @@ async def permissions_remove_level(self, ctx, level: str, *, """ if level.upper() not in PermissionLevel.__members__: embed = Embed( - title='Error', + title="Error", color=Color.red(), - description='The permission level you are attempting to point ' - f'to does not exist: `{level}`.' + description="The permission level you are attempting to point " + f"to does not exist: `{level}`.", ) return await ctx.send(embed=embed) - if hasattr(user_or_role, 'id'): + if hasattr(user_or_role, "id"): value = user_or_role.id - elif user_or_role in {'everyone', 'all'}: + elif user_or_role in {"everyone", "all"}: value = -1 else: raise commands.BadArgument(f'User or Role "{user_or_role}" not found') await self.bot.update_perms(PermissionLevel[level.upper()], value, add=False) embed = Embed( - title='Success', + title="Success", color=self.bot.main_color, - description=f'Permission for {level} was successfully updated.' + description=f"Permission for {level} was successfully updated.", ) return await ctx.send(embed=embed) - @permissions.group(name='get', invoke_without_command=True) + @permissions.group(name="get", invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get(self, ctx, *, user_or_role: Union[User, Role, str]): """ @@ -1104,9 +1128,9 @@ async def permissions_get(self, ctx, *, user_or_role: Union[User, Role, str]): `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". """ - if hasattr(user_or_role, 'id'): + if hasattr(user_or_role, "id"): value = user_or_role.id - elif user_or_role in {'everyone', 'all'}: + elif user_or_role in {"everyone", "all"}: value = -1 else: raise commands.BadArgument(f'User or Role "{user_or_role}" not found') @@ -1121,26 +1145,34 @@ async def permissions_get(self, ctx, *, user_or_role: Union[User, Role, str]): permissions = self.bot.config.level_permissions.get(level.name, []) if value in permissions: levels.append(level.name) - mention = user_or_role.name if hasattr(user_or_role, 'name') else user_or_role - desc_cmd = ', '.join(map(lambda x: f'`{x}`', cmds)) if cmds else 'No permission entries found.' - desc_level = ', '.join(map(lambda x: f'`{x}`', levels)) if levels else 'No permission entries found.' + mention = user_or_role.name if hasattr(user_or_role, "name") else user_or_role + desc_cmd = ( + ", ".join(map(lambda x: f"`{x}`", cmds)) + if cmds + else "No permission entries found." + ) + desc_level = ( + ", ".join(map(lambda x: f"`{x}`", levels)) + if levels + else "No permission entries found." + ) embeds = [ Embed( - title=f'{mention} has permission with the following commands:', + title=f"{mention} has permission with the following commands:", description=desc_cmd, - color=self.bot.main_color + color=self.bot.main_color, ), Embed( - title=f'{mention} has permission with the following permission groups:', + title=f"{mention} has permission with the following permission groups:", description=desc_level, - color=self.bot.main_color - ) + color=self.bot.main_color, + ), ] p_session = PaginatorSession(ctx, *embeds) return await p_session.run() - @permissions_get.command(name='command') + @permissions_get.command(name="command") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get_command(self, ctx, *, command: str = None): """View currently-set permissions for a command.""" @@ -1149,15 +1181,15 @@ def get_command(cmd): permissions = self.bot.config.command_permissions.get(cmd.name, []) if not permissions: embed = Embed( - title=f'Permission entries for command `{cmd.name}`:', - description='No permission entries found.', + title=f"Permission entries for command `{cmd.name}`:", + description="No permission entries found.", color=self.bot.main_color, ) else: values = [] for perm in permissions: if perm == -1: - values.insert(0, '**everyone**') + values.insert(0, "**everyone**") continue member = ctx.guild.get_member(perm) if member is not None: @@ -1174,9 +1206,9 @@ def get_command(cmd): values.append(str(perm)) embed = Embed( - title=f'Permission entries for command `{cmd.name}`:', - description=', '.join(values), - color=self.bot.main_color + title=f"Permission entries for command `{cmd.name}`:", + description=", ".join(values), + color=self.bot.main_color, ) return embed @@ -1184,10 +1216,10 @@ def get_command(cmd): if command is not None: if command not in self.bot.all_commands: embed = Embed( - title='Error', + title="Error", color=Color.red(), - description='The command you are attempting to point ' - f'to does not exist: `{command}`.' + description="The command you are attempting to point " + f"to does not exist: `{command}`.", ) return await ctx.send(embed=embed) embeds.append(get_command(self.bot.all_commands[command])) @@ -1198,7 +1230,7 @@ def get_command(cmd): p_session = PaginatorSession(ctx, *embeds) return await p_session.run() - @permissions_get.command(name='level', aliases=['group']) + @permissions_get.command(name="level", aliases=["group"]) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get_level(self, ctx, *, level: str = None): """View currently-set permissions for commands of a permission level.""" @@ -1207,16 +1239,16 @@ def get_level(perm_level): permissions = self.bot.config.level_permissions.get(perm_level.name, []) if not permissions: embed = Embed( - title='Permission entries for permission ' - f'level `{perm_level.name}`:', - description='No permission entries found.', + title="Permission entries for permission " + f"level `{perm_level.name}`:", + description="No permission entries found.", color=self.bot.main_color, ) else: values = [] for perm in permissions: if perm == -1: - values.insert(0, '**everyone**') + values.insert(0, "**everyone**") continue member = ctx.guild.get_member(perm) if member is not None: @@ -1233,8 +1265,8 @@ def get_level(perm_level): values.append(str(perm)) embed = Embed( - title=f'Permission entries for permission level `{perm_level.name}`:', - description=', '.join(values), + title=f"Permission entries for permission level `{perm_level.name}`:", + description=", ".join(values), color=self.bot.main_color, ) return embed @@ -1243,10 +1275,10 @@ def get_level(perm_level): if level is not None: if level.upper() not in PermissionLevel.__members__: embed = Embed( - title='Error', + title="Error", color=Color.red(), - description='The permission level you are attempting to point ' - f'to does not exist: `{level}`.' + description="The permission level you are attempting to point " + f"to does not exist: `{level}`.", ) return await ctx.send(embed=embed) embeds.append(get_level(PermissionLevel[level.upper()])) @@ -1257,13 +1289,15 @@ def get_level(perm_level): p_session = PaginatorSession(ctx, *embeds) return await p_session.run() - @commands.group(invoke_without_command=True, aliases=['oauth2', 'auth', 'authentication']) + @commands.group( + invoke_without_command=True, aliases=["oauth2", "auth", "authentication"] + ) @checks.has_permissions(PermissionLevel.OWNER) async def oauth(self, ctx): """Commands relating to Logviewer oauth2 login authentication.""" await ctx.send_help(ctx.command) - @oauth.command(name='whitelist') + @oauth.command(name="whitelist") @checks.has_permissions(PermissionLevel.OWNER) async def oauth_whitelist(self, ctx, target: Union[User, Role]): """ @@ -1271,7 +1305,7 @@ async def oauth_whitelist(self, ctx, target: Union[User, Role]): `target` may be a role ID, name, mention, user ID, name, or mention. """ - whitelisted = self.bot.config['oauth_whitelist'] + whitelisted = self.bot.config["oauth_whitelist"] if target.id in whitelisted: whitelisted.remove(target.id) @@ -1283,19 +1317,18 @@ async def oauth_whitelist(self, ctx, target: Union[User, Role]): await self.bot.config.update() embed = Embed(color=self.bot.main_color) - embed.title = 'Success' + embed.title = "Success" embed.description = ( - f"{'Un-w' if removed else 'W'}hitelisted " - f"{target.mention} to view logs." + f"{'Un-w' if removed else 'W'}hitelisted " f"{target.mention} to view logs." ) await ctx.send(embed=embed) - @oauth.command(name='show', aliases=['get', 'list', 'view']) + @oauth.command(name="show", aliases=["get", "list", "view"]) @checks.has_permissions(PermissionLevel.OWNER) async def oauth_show(self, ctx): """Shows a list of users and roles that are whitelisted to view logs.""" - whitelisted = self.bot.config['oauth_whitelist'] + whitelisted = self.bot.config["oauth_whitelist"] users = [] roles = [] @@ -1309,27 +1342,31 @@ async def oauth_show(self, ctx): roles.append(role) embed = Embed(color=self.bot.main_color) - embed.title = 'Oauth Whitelist' + 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) - @commands.command(hidden=True, name='eval') + @commands.command(hidden=True, name="eval") @checks.has_permissions(PermissionLevel.OWNER) async def eval_(self, ctx, *, body: str): """Evaluates Python code.""" env = { - 'ctx': ctx, - 'bot': self.bot, - 'channel': ctx.channel, - 'author': ctx.author, - 'guild': ctx.guild, - 'message': ctx.message, - 'source': inspect.getsource, - 'discord': __import__('discord') + "ctx": ctx, + "bot": self.bot, + "channel": ctx.channel, + "author": ctx.author, + "guild": ctx.guild, + "message": ctx.message, + "source": inspect.getsource, + "discord": __import__("discord"), } env.update(globals()) @@ -1351,46 +1388,46 @@ def paginate(text: str): appd_index = curr if appd_index != len(text) - 1: pages.append(text[last:curr]) - return list(filter(lambda a: a != '', pages)) + return list(filter(lambda a: a != "", pages)) try: exec(to_compile, env) # pylint: disable=exec-used except Exception as exc: # pylint: disable=broad-except - await ctx.send(f'```py\n{exc.__class__.__name__}: {exc}\n```') - return await ctx.message.add_reaction('\u2049') + await ctx.send(f"```py\n{exc.__class__.__name__}: {exc}\n```") + return await ctx.message.add_reaction("\u2049") - func = env['func'] + func = env["func"] try: with redirect_stdout(stdout): ret = await func() except Exception: # pylint: disable=broad-except value = stdout.getvalue() - await ctx.send(f'```py\n{value}{traceback.format_exc()}\n```') - return await ctx.message.add_reaction('\u2049') + await ctx.send(f"```py\n{value}{traceback.format_exc()}\n```") + return await ctx.message.add_reaction("\u2049") else: value = stdout.getvalue() if ret is None: if value: try: - await ctx.send(f'```py\n{value}\n```') + await ctx.send(f"```py\n{value}\n```") except Exception: # pylint: disable=broad-except paginated_text = paginate(value) for page in paginated_text: if page == paginated_text[-1]: - await ctx.send(f'```py\n{page}\n```') + await ctx.send(f"```py\n{page}\n```") break - await ctx.send(f'```py\n{page}\n```') + await ctx.send(f"```py\n{page}\n```") else: try: - await ctx.send(f'```py\n{value}{ret}\n```') + await ctx.send(f"```py\n{value}{ret}\n```") except Exception: # pylint: disable=broad-except paginated_text = paginate(f"{value}{ret}") for page in paginated_text: if page == paginated_text[-1]: - await ctx.send(f'```py\n{page}\n```') + await ctx.send(f"```py\n{page}\n```") break - await ctx.send(f'```py\n{page}\n```') + await ctx.send(f"```py\n{page}\n```") def setup(bot): diff --git a/core/_color_data.py b/core/_color_data.py index 4215055768..13346e903e 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -5,34 +5,33 @@ BASE_COLORS = { - 'b': '#0000ff', - 'g': '#007f00', - 'r': '#ff0000', - 'c': '#00bfbf', - 'm': '#bf00bf', - 'y': '#bfbf00', - 'k': '#000000', - 'w': '#ffffff' + "b": "#0000ff", + "g": "#007f00", + "r": "#ff0000", + "c": "#00bfbf", + "m": "#bf00bf", + "y": "#bfbf00", + "k": "#000000", + "w": "#ffffff", } # These colors are from Tableau TABLEAU_COLORS = { - 'blue': '#1f77b4', - 'orange': '#ff7f0e', - 'green': '#2ca02c', - 'red': '#d62728', - 'purple': '#9467bd', - 'brown': '#8c564b', - 'pink': '#e377c2', - 'gray': '#7f7f7f', - 'olive': '#bcbd22', - 'cyan': '#17becf', + "blue": "#1f77b4", + "orange": "#ff7f0e", + "green": "#2ca02c", + "red": "#d62728", + "purple": "#9467bd", + "brown": "#8c564b", + "pink": "#e377c2", + "gray": "#7f7f7f", + "olive": "#bcbd22", + "cyan": "#17becf", } # Normalize name to "tab:" to avoid name collisions. -TABLEAU_COLORS_NORM = {'tab:' + name: value - for name, value in TABLEAU_COLORS.items()} +TABLEAU_COLORS_NORM = {"tab:" + name: value for name, value in TABLEAU_COLORS.items()} # This mapping of color names -> hex values is taken from @@ -43,1117 +42,1115 @@ # # License: http://creativecommons.org/publicdomain/zero/1.0/ XKCD_COLORS = { - 'cloudy blue': '#acc2d9', - 'dark pastel green': '#56ae57', - 'dust': '#b2996e', - 'electric lime': '#a8ff04', - 'fresh green': '#69d84f', - 'light eggplant': '#894585', - 'nasty green': '#70b23f', - 'really light blue': '#d4ffff', - 'tea': '#65ab7c', - 'warm purple': '#952e8f', - 'yellowish tan': '#fcfc81', - 'cement': '#a5a391', - 'dark grass green': '#388004', - 'dusty teal': '#4c9085', - 'grey teal': '#5e9b8a', - 'macaroni and cheese': '#efb435', - 'pinkish tan': '#d99b82', - 'spruce': '#0a5f38', - 'strong blue': '#0c06f7', - 'toxic green': '#61de2a', - 'windows blue': '#3778bf', - 'blue blue': '#2242c7', - 'blue with a hint of purple': '#533cc6', - 'booger': '#9bb53c', - 'bright sea green': '#05ffa6', - 'dark green blue': '#1f6357', - 'deep turquoise': '#017374', - 'green teal': '#0cb577', - 'strong pink': '#ff0789', - 'bland': '#afa88b', - 'deep aqua': '#08787f', - 'lavender pink': '#dd85d7', - 'light moss green': '#a6c875', - 'light seafoam green': '#a7ffb5', - 'olive yellow': '#c2b709', - 'pig pink': '#e78ea5', - 'deep lilac': '#966ebd', - 'desert': '#ccad60', - 'dusty lavender': '#ac86a8', - 'purpley grey': '#947e94', - 'purply': '#983fb2', - 'candy pink': '#ff63e9', - 'light pastel green': '#b2fba5', - 'boring green': '#63b365', - 'kiwi green': '#8ee53f', - 'light grey green': '#b7e1a1', - 'orange pink': '#ff6f52', - 'tea green': '#bdf8a3', - 'very light brown': '#d3b683', - 'egg shell': '#fffcc4', - 'eggplant purple': '#430541', - 'powder pink': '#ffb2d0', - 'reddish grey': '#997570', - 'baby shit brown': '#ad900d', - 'liliac': '#c48efd', - 'stormy blue': '#507b9c', - 'ugly brown': '#7d7103', - 'custard': '#fffd78', - 'darkish pink': '#da467d', - 'deep brown': '#410200', - 'greenish beige': '#c9d179', - 'manilla': '#fffa86', - 'off blue': '#5684ae', - 'battleship grey': '#6b7c85', - 'browny green': '#6f6c0a', - 'bruise': '#7e4071', - 'kelley green': '#009337', - 'sickly yellow': '#d0e429', - 'sunny yellow': '#fff917', - 'azul': '#1d5dec', - 'darkgreen': '#054907', - 'green/yellow': '#b5ce08', - 'lichen': '#8fb67b', - 'light light green': '#c8ffb0', - 'pale gold': '#fdde6c', - 'sun yellow': '#ffdf22', - 'tan green': '#a9be70', - 'burple': '#6832e3', - 'butterscotch': '#fdb147', - 'toupe': '#c7ac7d', - 'dark cream': '#fff39a', - 'indian red': '#850e04', - 'light lavendar': '#efc0fe', - 'poison green': '#40fd14', - 'baby puke green': '#b6c406', - 'bright yellow green': '#9dff00', - 'charcoal grey': '#3c4142', - 'squash': '#f2ab15', - 'cinnamon': '#ac4f06', - 'light pea green': '#c4fe82', - 'radioactive green': '#2cfa1f', - 'raw sienna': '#9a6200', - 'baby purple': '#ca9bf7', - 'cocoa': '#875f42', - 'light royal blue': '#3a2efe', - 'orangeish': '#fd8d49', - 'rust brown': '#8b3103', - 'sand brown': '#cba560', - 'swamp': '#698339', - 'tealish green': '#0cdc73', - 'burnt siena': '#b75203', - 'camo': '#7f8f4e', - 'dusk blue': '#26538d', - 'fern': '#63a950', - 'old rose': '#c87f89', - 'pale light green': '#b1fc99', - 'peachy pink': '#ff9a8a', - 'rosy pink': '#f6688e', - 'light bluish green': '#76fda8', - 'light bright green': '#53fe5c', - 'light neon green': '#4efd54', - 'light seafoam': '#a0febf', - 'tiffany blue': '#7bf2da', - 'washed out green': '#bcf5a6', - 'browny orange': '#ca6b02', - 'nice blue': '#107ab0', - 'sapphire': '#2138ab', - 'greyish teal': '#719f91', - 'orangey yellow': '#fdb915', - 'parchment': '#fefcaf', - 'straw': '#fcf679', - 'very dark brown': '#1d0200', - 'terracota': '#cb6843', - 'ugly blue': '#31668a', - 'clear blue': '#247afd', - 'creme': '#ffffb6', - 'foam green': '#90fda9', - 'grey/green': '#86a17d', - 'light gold': '#fddc5c', - 'seafoam blue': '#78d1b6', - 'topaz': '#13bbaf', - 'violet pink': '#fb5ffc', - 'wintergreen': '#20f986', - 'yellow tan': '#ffe36e', - 'dark fuchsia': '#9d0759', - 'indigo blue': '#3a18b1', - 'light yellowish green': '#c2ff89', - 'pale magenta': '#d767ad', - 'rich purple': '#720058', - 'sunflower yellow': '#ffda03', - 'green/blue': '#01c08d', - 'leather': '#ac7434', - 'racing green': '#014600', - 'vivid purple': '#9900fa', - 'dark royal blue': '#02066f', - 'hazel': '#8e7618', - 'muted pink': '#d1768f', - 'booger green': '#96b403', - 'canary': '#fdff63', - 'cool grey': '#95a3a6', - 'dark taupe': '#7f684e', - 'darkish purple': '#751973', - 'true green': '#089404', - 'coral pink': '#ff6163', - 'dark sage': '#598556', - 'dark slate blue': '#214761', - 'flat blue': '#3c73a8', - 'mushroom': '#ba9e88', - 'rich blue': '#021bf9', - 'dirty purple': '#734a65', - 'greenblue': '#23c48b', - 'icky green': '#8fae22', - 'light khaki': '#e6f2a2', - 'warm blue': '#4b57db', - 'dark hot pink': '#d90166', - 'deep sea blue': '#015482', - 'carmine': '#9d0216', - 'dark yellow green': '#728f02', - 'pale peach': '#ffe5ad', - 'plum purple': '#4e0550', - 'golden rod': '#f9bc08', - 'neon red': '#ff073a', - 'old pink': '#c77986', - 'very pale blue': '#d6fffe', - 'blood orange': '#fe4b03', - 'grapefruit': '#fd5956', - 'sand yellow': '#fce166', - 'clay brown': '#b2713d', - 'dark blue grey': '#1f3b4d', - 'flat green': '#699d4c', - 'light green blue': '#56fca2', - 'warm pink': '#fb5581', - 'dodger blue': '#3e82fc', - 'gross green': '#a0bf16', - 'ice': '#d6fffa', - 'metallic blue': '#4f738e', - 'pale salmon': '#ffb19a', - 'sap green': '#5c8b15', - 'algae': '#54ac68', - 'bluey grey': '#89a0b0', - 'greeny grey': '#7ea07a', - 'highlighter green': '#1bfc06', - 'light light blue': '#cafffb', - 'light mint': '#b6ffbb', - 'raw umber': '#a75e09', - 'vivid blue': '#152eff', - 'deep lavender': '#8d5eb7', - 'dull teal': '#5f9e8f', - 'light greenish blue': '#63f7b4', - 'mud green': '#606602', - 'pinky': '#fc86aa', - 'red wine': '#8c0034', - 'shit green': '#758000', - 'tan brown': '#ab7e4c', - 'darkblue': '#030764', - 'rosa': '#fe86a4', - 'lipstick': '#d5174e', - 'pale mauve': '#fed0fc', - 'claret': '#680018', - 'dandelion': '#fedf08', - 'orangered': '#fe420f', - 'poop green': '#6f7c00', - 'ruby': '#ca0147', - 'dark': '#1b2431', - 'greenish turquoise': '#00fbb0', - 'pastel red': '#db5856', - 'piss yellow': '#ddd618', - 'bright cyan': '#41fdfe', - 'dark coral': '#cf524e', - 'algae green': '#21c36f', - 'darkish red': '#a90308', - 'reddy brown': '#6e1005', - 'blush pink': '#fe828c', - 'camouflage green': '#4b6113', - 'lawn green': '#4da409', - 'putty': '#beae8a', - 'vibrant blue': '#0339f8', - 'dark sand': '#a88f59', - 'purple/blue': '#5d21d0', - 'saffron': '#feb209', - 'twilight': '#4e518b', - 'warm brown': '#964e02', - 'bluegrey': '#85a3b2', - 'bubble gum pink': '#ff69af', - 'duck egg blue': '#c3fbf4', - 'greenish cyan': '#2afeb7', - 'petrol': '#005f6a', - 'royal': '#0c1793', - 'butter': '#ffff81', - 'dusty orange': '#f0833a', - 'off yellow': '#f1f33f', - 'pale olive green': '#b1d27b', - 'orangish': '#fc824a', - 'leaf': '#71aa34', - 'light blue grey': '#b7c9e2', - 'dried blood': '#4b0101', - 'lightish purple': '#a552e6', - 'rusty red': '#af2f0d', - 'lavender blue': '#8b88f8', - 'light grass green': '#9af764', - 'light mint green': '#a6fbb2', - 'sunflower': '#ffc512', - 'velvet': '#750851', - 'brick orange': '#c14a09', - 'lightish red': '#fe2f4a', - 'pure blue': '#0203e2', - 'twilight blue': '#0a437a', - 'violet red': '#a50055', - 'yellowy brown': '#ae8b0c', - 'carnation': '#fd798f', - 'muddy yellow': '#bfac05', - 'dark seafoam green': '#3eaf76', - 'deep rose': '#c74767', - 'dusty red': '#b9484e', - 'grey/blue': '#647d8e', - 'lemon lime': '#bffe28', - 'purple/pink': '#d725de', - 'brown yellow': '#b29705', - 'purple brown': '#673a3f', - 'wisteria': '#a87dc2', - 'banana yellow': '#fafe4b', - 'lipstick red': '#c0022f', - 'water blue': '#0e87cc', - 'brown grey': '#8d8468', - 'vibrant purple': '#ad03de', - 'baby green': '#8cff9e', - 'barf green': '#94ac02', - 'eggshell blue': '#c4fff7', - 'sandy yellow': '#fdee73', - 'cool green': '#33b864', - 'pale': '#fff9d0', - 'blue/grey': '#758da3', - 'hot magenta': '#f504c9', - 'greyblue': '#77a1b5', - 'purpley': '#8756e4', - 'baby shit green': '#889717', - 'brownish pink': '#c27e79', - 'dark aquamarine': '#017371', - 'diarrhea': '#9f8303', - 'light mustard': '#f7d560', - 'pale sky blue': '#bdf6fe', - 'turtle green': '#75b84f', - 'bright olive': '#9cbb04', - 'dark grey blue': '#29465b', - 'greeny brown': '#696006', - 'lemon green': '#adf802', - 'light periwinkle': '#c1c6fc', - 'seaweed green': '#35ad6b', - 'sunshine yellow': '#fffd37', - 'ugly purple': '#a442a0', - 'medium pink': '#f36196', - 'puke brown': '#947706', - 'very light pink': '#fff4f2', - 'viridian': '#1e9167', - 'bile': '#b5c306', - 'faded yellow': '#feff7f', - 'very pale green': '#cffdbc', - 'vibrant green': '#0add08', - 'bright lime': '#87fd05', - 'spearmint': '#1ef876', - 'light aquamarine': '#7bfdc7', - 'light sage': '#bcecac', - 'yellowgreen': '#bbf90f', - 'baby poo': '#ab9004', - 'dark seafoam': '#1fb57a', - 'deep teal': '#00555a', - 'heather': '#a484ac', - 'rust orange': '#c45508', - 'dirty blue': '#3f829d', - 'fern green': '#548d44', - 'bright lilac': '#c95efb', - 'weird green': '#3ae57f', - 'peacock blue': '#016795', - 'avocado green': '#87a922', - 'faded orange': '#f0944d', - 'grape purple': '#5d1451', - 'hot green': '#25ff29', - 'lime yellow': '#d0fe1d', - 'mango': '#ffa62b', - 'shamrock': '#01b44c', - 'bubblegum': '#ff6cb5', - 'purplish brown': '#6b4247', - 'vomit yellow': '#c7c10c', - 'pale cyan': '#b7fffa', - 'key lime': '#aeff6e', - 'tomato red': '#ec2d01', - 'lightgreen': '#76ff7b', - 'merlot': '#730039', - 'night blue': '#040348', - 'purpleish pink': '#df4ec8', - 'apple': '#6ecb3c', - 'baby poop green': '#8f9805', - 'green apple': '#5edc1f', - 'heliotrope': '#d94ff5', - 'yellow/green': '#c8fd3d', - 'almost black': '#070d0d', - 'cool blue': '#4984b8', - 'leafy green': '#51b73b', - 'mustard brown': '#ac7e04', - 'dusk': '#4e5481', - 'dull brown': '#876e4b', - 'frog green': '#58bc08', - 'vivid green': '#2fef10', - 'bright light green': '#2dfe54', - 'fluro green': '#0aff02', - 'kiwi': '#9cef43', - 'seaweed': '#18d17b', - 'navy green': '#35530a', - 'ultramarine blue': '#1805db', - 'iris': '#6258c4', - 'pastel orange': '#ff964f', - 'yellowish orange': '#ffab0f', - 'perrywinkle': '#8f8ce7', - 'tealish': '#24bca8', - 'dark plum': '#3f012c', - 'pear': '#cbf85f', - 'pinkish orange': '#ff724c', - 'midnight purple': '#280137', - 'light urple': '#b36ff6', - 'dark mint': '#48c072', - 'greenish tan': '#bccb7a', - 'light burgundy': '#a8415b', - 'turquoise blue': '#06b1c4', - 'ugly pink': '#cd7584', - 'sandy': '#f1da7a', - 'electric pink': '#ff0490', - 'muted purple': '#805b87', - 'mid green': '#50a747', - 'greyish': '#a8a495', - 'neon yellow': '#cfff04', - 'banana': '#ffff7e', - 'carnation pink': '#ff7fa7', - 'tomato': '#ef4026', - 'sea': '#3c9992', - 'muddy brown': '#886806', - 'turquoise green': '#04f489', - 'buff': '#fef69e', - 'fawn': '#cfaf7b', - 'muted blue': '#3b719f', - 'pale rose': '#fdc1c5', - 'dark mint green': '#20c073', - 'amethyst': '#9b5fc0', - 'blue/green': '#0f9b8e', - 'chestnut': '#742802', - 'sick green': '#9db92c', - 'pea': '#a4bf20', - 'rusty orange': '#cd5909', - 'stone': '#ada587', - 'rose red': '#be013c', - 'pale aqua': '#b8ffeb', - 'deep orange': '#dc4d01', - 'earth': '#a2653e', - 'mossy green': '#638b27', - 'grassy green': '#419c03', - 'pale lime green': '#b1ff65', - 'light grey blue': '#9dbcd4', - 'pale grey': '#fdfdfe', - 'asparagus': '#77ab56', - 'blueberry': '#464196', - 'purple red': '#990147', - 'pale lime': '#befd73', - 'greenish teal': '#32bf84', - 'caramel': '#af6f09', - 'deep magenta': '#a0025c', - 'light peach': '#ffd8b1', - 'milk chocolate': '#7f4e1e', - 'ocher': '#bf9b0c', - 'off green': '#6ba353', - 'purply pink': '#f075e6', - 'lightblue': '#7bc8f6', - 'dusky blue': '#475f94', - 'golden': '#f5bf03', - 'light beige': '#fffeb6', - 'butter yellow': '#fffd74', - 'dusky purple': '#895b7b', - 'french blue': '#436bad', - 'ugly yellow': '#d0c101', - 'greeny yellow': '#c6f808', - 'orangish red': '#f43605', - 'shamrock green': '#02c14d', - 'orangish brown': '#b25f03', - 'tree green': '#2a7e19', - 'deep violet': '#490648', - 'gunmetal': '#536267', - 'blue/purple': '#5a06ef', - 'cherry': '#cf0234', - 'sandy brown': '#c4a661', - 'warm grey': '#978a84', - 'dark indigo': '#1f0954', - 'midnight': '#03012d', - 'bluey green': '#2bb179', - 'grey pink': '#c3909b', - 'soft purple': '#a66fb5', - 'blood': '#770001', - 'brown red': '#922b05', - 'medium grey': '#7d7f7c', - 'berry': '#990f4b', - 'poo': '#8f7303', - 'purpley pink': '#c83cb9', - 'light salmon': '#fea993', - 'snot': '#acbb0d', - 'easter purple': '#c071fe', - 'light yellow green': '#ccfd7f', - 'dark navy blue': '#00022e', - 'drab': '#828344', - 'light rose': '#ffc5cb', - 'rouge': '#ab1239', - 'purplish red': '#b0054b', - 'slime green': '#99cc04', - 'baby poop': '#937c00', - 'irish green': '#019529', - 'pink/purple': '#ef1de7', - 'dark navy': '#000435', - 'greeny blue': '#42b395', - 'light plum': '#9d5783', - 'pinkish grey': '#c8aca9', - 'dirty orange': '#c87606', - 'rust red': '#aa2704', - 'pale lilac': '#e4cbff', - 'orangey red': '#fa4224', - 'primary blue': '#0804f9', - 'kermit green': '#5cb200', - 'brownish purple': '#76424e', - 'murky green': '#6c7a0e', - 'wheat': '#fbdd7e', - 'very dark purple': '#2a0134', - 'bottle green': '#044a05', - 'watermelon': '#fd4659', - 'deep sky blue': '#0d75f8', - 'fire engine red': '#fe0002', - 'yellow ochre': '#cb9d06', - 'pumpkin orange': '#fb7d07', - 'pale olive': '#b9cc81', - 'light lilac': '#edc8ff', - 'lightish green': '#61e160', - 'carolina blue': '#8ab8fe', - 'mulberry': '#920a4e', - 'shocking pink': '#fe02a2', - 'auburn': '#9a3001', - 'bright lime green': '#65fe08', - 'celadon': '#befdb7', - 'pinkish brown': '#b17261', - 'poo brown': '#885f01', - 'bright sky blue': '#02ccfe', - 'celery': '#c1fd95', - 'dirt brown': '#836539', - 'strawberry': '#fb2943', - 'dark lime': '#84b701', - 'copper': '#b66325', - 'medium brown': '#7f5112', - 'muted green': '#5fa052', - "robin's egg": '#6dedfd', - 'bright aqua': '#0bf9ea', - 'bright lavender': '#c760ff', - 'ivory': '#ffffcb', - 'very light purple': '#f6cefc', - 'light navy': '#155084', - 'pink red': '#f5054f', - 'olive brown': '#645403', - 'poop brown': '#7a5901', - 'mustard green': '#a8b504', - 'ocean green': '#3d9973', - 'very dark blue': '#000133', - 'dusty green': '#76a973', - 'light navy blue': '#2e5a88', - 'minty green': '#0bf77d', - 'adobe': '#bd6c48', - 'barney': '#ac1db8', - 'jade green': '#2baf6a', - 'bright light blue': '#26f7fd', - 'light lime': '#aefd6c', - 'dark khaki': '#9b8f55', - 'orange yellow': '#ffad01', - 'ocre': '#c69c04', - 'maize': '#f4d054', - 'faded pink': '#de9dac', - 'british racing green': '#05480d', - 'sandstone': '#c9ae74', - 'mud brown': '#60460f', - 'light sea green': '#98f6b0', - 'robin egg blue': '#8af1fe', - 'aqua marine': '#2ee8bb', - 'dark sea green': '#11875d', - 'soft pink': '#fdb0c0', - 'orangey brown': '#b16002', - 'cherry red': '#f7022a', - 'burnt yellow': '#d5ab09', - 'brownish grey': '#86775f', - 'camel': '#c69f59', - 'purplish grey': '#7a687f', - 'marine': '#042e60', - 'greyish pink': '#c88d94', - 'pale turquoise': '#a5fbd5', - 'pastel yellow': '#fffe71', - 'bluey purple': '#6241c7', - 'canary yellow': '#fffe40', - 'faded red': '#d3494e', - 'sepia': '#985e2b', - 'coffee': '#a6814c', - 'bright magenta': '#ff08e8', - 'mocha': '#9d7651', - 'ecru': '#feffca', - 'purpleish': '#98568d', - 'cranberry': '#9e003a', - 'darkish green': '#287c37', - 'brown orange': '#b96902', - 'dusky rose': '#ba6873', - 'melon': '#ff7855', - 'sickly green': '#94b21c', - 'silver': '#c5c9c7', - 'purply blue': '#661aee', - 'purpleish blue': '#6140ef', - 'hospital green': '#9be5aa', - 'shit brown': '#7b5804', - 'mid blue': '#276ab3', - 'amber': '#feb308', - 'easter green': '#8cfd7e', - 'soft blue': '#6488ea', - 'cerulean blue': '#056eee', - 'golden brown': '#b27a01', - 'bright turquoise': '#0ffef9', - 'red pink': '#fa2a55', - 'red purple': '#820747', - 'greyish brown': '#7a6a4f', - 'vermillion': '#f4320c', - 'russet': '#a13905', - 'steel grey': '#6f828a', - 'lighter purple': '#a55af4', - 'bright violet': '#ad0afd', - 'prussian blue': '#004577', - 'slate green': '#658d6d', - 'dirty pink': '#ca7b80', - 'dark blue green': '#005249', - 'pine': '#2b5d34', - 'yellowy green': '#bff128', - 'dark gold': '#b59410', - 'bluish': '#2976bb', - 'darkish blue': '#014182', - 'dull red': '#bb3f3f', - 'pinky red': '#fc2647', - 'bronze': '#a87900', - 'pale teal': '#82cbb2', - 'military green': '#667c3e', - 'barbie pink': '#fe46a5', - 'bubblegum pink': '#fe83cc', - 'pea soup green': '#94a617', - 'dark mustard': '#a88905', - 'shit': '#7f5f00', - 'medium purple': '#9e43a2', - 'very dark green': '#062e03', - 'dirt': '#8a6e45', - 'dusky pink': '#cc7a8b', - 'red violet': '#9e0168', - 'lemon yellow': '#fdff38', - 'pistachio': '#c0fa8b', - 'dull yellow': '#eedc5b', - 'dark lime green': '#7ebd01', - 'denim blue': '#3b5b92', - 'teal blue': '#01889f', - 'lightish blue': '#3d7afd', - 'purpley blue': '#5f34e7', - 'light indigo': '#6d5acf', - 'swamp green': '#748500', - 'brown green': '#706c11', - 'dark maroon': '#3c0008', - 'hot purple': '#cb00f5', - 'dark forest green': '#002d04', - 'faded blue': '#658cbb', - 'drab green': '#749551', - 'light lime green': '#b9ff66', - 'snot green': '#9dc100', - 'yellowish': '#faee66', - 'light blue green': '#7efbb3', - 'bordeaux': '#7b002c', - 'light mauve': '#c292a1', - 'ocean': '#017b92', - 'marigold': '#fcc006', - 'muddy green': '#657432', - 'dull orange': '#d8863b', - 'steel': '#738595', - 'electric purple': '#aa23ff', - 'fluorescent green': '#08ff08', - 'yellowish brown': '#9b7a01', - 'blush': '#f29e8e', - 'soft green': '#6fc276', - 'bright orange': '#ff5b00', - 'lemon': '#fdff52', - 'purple grey': '#866f85', - 'acid green': '#8ffe09', - 'pale lavender': '#eecffe', - 'violet blue': '#510ac9', - 'light forest green': '#4f9153', - 'burnt red': '#9f2305', - 'khaki green': '#728639', - 'cerise': '#de0c62', - 'faded purple': '#916e99', - 'apricot': '#ffb16d', - 'dark olive green': '#3c4d03', - 'grey brown': '#7f7053', - 'green grey': '#77926f', - 'true blue': '#010fcc', - 'pale violet': '#ceaefa', - 'periwinkle blue': '#8f99fb', - 'light sky blue': '#c6fcff', - 'blurple': '#5539cc', - 'green brown': '#544e03', - 'bluegreen': '#017a79', - 'bright teal': '#01f9c6', - 'brownish yellow': '#c9b003', - 'pea soup': '#929901', - 'forest': '#0b5509', - 'barney purple': '#a00498', - 'ultramarine': '#2000b1', - 'purplish': '#94568c', - 'puke yellow': '#c2be0e', - 'bluish grey': '#748b97', - 'dark periwinkle': '#665fd1', - 'dark lilac': '#9c6da5', - 'reddish': '#c44240', - 'light maroon': '#a24857', - 'dusty purple': '#825f87', - 'terra cotta': '#c9643b', - 'avocado': '#90b134', - 'marine blue': '#01386a', - 'teal green': '#25a36f', - 'slate grey': '#59656d', - 'lighter green': '#75fd63', - 'electric green': '#21fc0d', - 'dusty blue': '#5a86ad', - 'golden yellow': '#fec615', - 'bright yellow': '#fffd01', - 'light lavender': '#dfc5fe', - 'umber': '#b26400', - 'poop': '#7f5e00', - 'dark peach': '#de7e5d', - 'jungle green': '#048243', - 'eggshell': '#ffffd4', - 'denim': '#3b638c', - 'yellow brown': '#b79400', - 'dull purple': '#84597e', - 'chocolate brown': '#411900', - 'wine red': '#7b0323', - 'neon blue': '#04d9ff', - 'dirty green': '#667e2c', - 'light tan': '#fbeeac', - 'ice blue': '#d7fffe', - 'cadet blue': '#4e7496', - 'dark mauve': '#874c62', - 'very light blue': '#d5ffff', - 'grey purple': '#826d8c', - 'pastel pink': '#ffbacd', - 'very light green': '#d1ffbd', - 'dark sky blue': '#448ee4', - 'evergreen': '#05472a', - 'dull pink': '#d5869d', - 'aubergine': '#3d0734', - 'mahogany': '#4a0100', - 'reddish orange': '#f8481c', - 'deep green': '#02590f', - 'vomit green': '#89a203', - 'purple pink': '#e03fd8', - 'dusty pink': '#d58a94', - 'faded green': '#7bb274', - 'camo green': '#526525', - 'pinky purple': '#c94cbe', - 'pink purple': '#db4bda', - 'brownish red': '#9e3623', - 'dark rose': '#b5485d', - 'mud': '#735c12', - 'brownish': '#9c6d57', - 'emerald green': '#028f1e', - 'pale brown': '#b1916e', - 'dull blue': '#49759c', - 'burnt umber': '#a0450e', - 'medium green': '#39ad48', - 'clay': '#b66a50', - 'light aqua': '#8cffdb', - 'light olive green': '#a4be5c', - 'brownish orange': '#cb7723', - 'dark aqua': '#05696b', - 'purplish pink': '#ce5dae', - 'dark salmon': '#c85a53', - 'greenish grey': '#96ae8d', - 'jade': '#1fa774', - 'ugly green': '#7a9703', - 'dark beige': '#ac9362', - 'emerald': '#01a049', - 'pale red': '#d9544d', - 'light magenta': '#fa5ff7', - 'sky': '#82cafc', - 'light cyan': '#acfffc', - 'yellow orange': '#fcb001', - 'reddish purple': '#910951', - 'reddish pink': '#fe2c54', - 'orchid': '#c875c4', - 'dirty yellow': '#cdc50a', - 'orange red': '#fd411e', - 'deep red': '#9a0200', - 'orange brown': '#be6400', - 'cobalt blue': '#030aa7', - 'neon pink': '#fe019a', - 'rose pink': '#f7879a', - 'greyish purple': '#887191', - 'raspberry': '#b00149', - 'aqua green': '#12e193', - 'salmon pink': '#fe7b7c', - 'tangerine': '#ff9408', - 'brownish green': '#6a6e09', - 'red brown': '#8b2e16', - 'greenish brown': '#696112', - 'pumpkin': '#e17701', - 'pine green': '#0a481e', - 'charcoal': '#343837', - 'baby pink': '#ffb7ce', - 'cornflower': '#6a79f7', - 'blue violet': '#5d06e9', - 'chocolate': '#3d1c02', - 'greyish green': '#82a67d', - 'scarlet': '#be0119', - 'green yellow': '#c9ff27', - 'dark olive': '#373e02', - 'sienna': '#a9561e', - 'pastel purple': '#caa0ff', - 'terracotta': '#ca6641', - 'aqua blue': '#02d8e9', - 'sage green': '#88b378', - 'blood red': '#980002', - 'deep pink': '#cb0162', - 'grass': '#5cac2d', - 'moss': '#769958', - 'pastel blue': '#a2bffe', - 'bluish green': '#10a674', - 'green blue': '#06b48b', - 'dark tan': '#af884a', - 'greenish blue': '#0b8b87', - 'pale orange': '#ffa756', - 'vomit': '#a2a415', - 'forrest green': '#154406', - 'dark lavender': '#856798', - 'dark violet': '#34013f', - 'purple blue': '#632de9', - 'dark cyan': '#0a888a', - 'olive drab': '#6f7632', - 'pinkish': '#d46a7e', - 'cobalt': '#1e488f', - 'neon purple': '#bc13fe', - 'light turquoise': '#7ef4cc', - 'apple green': '#76cd26', - 'dull green': '#74a662', - 'wine': '#80013f', - 'powder blue': '#b1d1fc', - 'off white': '#ffffe4', - 'electric blue': '#0652ff', - 'dark turquoise': '#045c5a', - 'blue purple': '#5729ce', - 'azure': '#069af3', - 'bright red': '#ff000d', - 'pinkish red': '#f10c45', - 'cornflower blue': '#5170d7', - 'light olive': '#acbf69', - 'grape': '#6c3461', - 'greyish blue': '#5e819d', - 'purplish blue': '#601ef9', - 'yellowish green': '#b0dd16', - 'greenish yellow': '#cdfd02', - 'medium blue': '#2c6fbb', - 'dusty rose': '#c0737a', - 'light violet': '#d6b4fc', - 'midnight blue': '#020035', - 'bluish purple': '#703be7', - 'red orange': '#fd3c06', - 'dark magenta': '#960056', - 'greenish': '#40a368', - 'ocean blue': '#03719c', - 'coral': '#fc5a50', - 'cream': '#ffffc2', - 'reddish brown': '#7f2b0a', - 'burnt sienna': '#b04e0f', - 'brick': '#a03623', - 'sage': '#87ae73', - 'grey green': '#789b73', - 'white': '#ffffff', - "robin's egg blue": '#98eff9', - 'moss green': '#658b38', - 'steel blue': '#5a7d9a', - 'eggplant': '#380835', - 'light yellow': '#fffe7a', - 'leaf green': '#5ca904', - 'light grey': '#d8dcd6', - 'puke': '#a5a502', - 'pinkish purple': '#d648d7', - 'sea blue': '#047495', - 'pale purple': '#b790d4', - 'slate blue': '#5b7c99', - 'blue grey': '#607c8e', - 'hunter green': '#0b4008', - 'fuchsia': '#ed0dd9', - 'crimson': '#8c000f', - 'pale yellow': '#ffff84', - 'ochre': '#bf9005', - 'mustard yellow': '#d2bd0a', - 'light red': '#ff474c', - 'cerulean': '#0485d1', - 'pale pink': '#ffcfdc', - 'deep blue': '#040273', - 'rust': '#a83c09', - 'light teal': '#90e4c1', - 'slate': '#516572', - 'goldenrod': '#fac205', - 'dark yellow': '#d5b60a', - 'dark grey': '#363737', - 'army green': '#4b5d16', - 'grey blue': '#6b8ba4', - 'seafoam': '#80f9ad', - 'puce': '#a57e52', - 'spring green': '#a9f971', - 'dark orange': '#c65102', - 'sand': '#e2ca76', - 'pastel green': '#b0ff9d', - 'mint': '#9ffeb0', - 'light orange': '#fdaa48', - 'bright pink': '#fe01b1', - 'chartreuse': '#c1f80a', - 'deep purple': '#36013f', - 'dark brown': '#341c02', - 'taupe': '#b9a281', - 'pea green': '#8eab12', - 'puke green': '#9aae07', - 'kelly green': '#02ab2e', - 'seafoam green': '#7af9ab', - 'blue green': '#137e6d', - 'khaki': '#aaa662', - 'burgundy': '#610023', - 'dark teal': '#014d4e', - 'brick red': '#8f1402', - 'royal purple': '#4b006e', - 'plum': '#580f41', - 'mint green': '#8fff9f', - 'gold': '#dbb40c', - 'baby blue': '#a2cffe', - 'yellow green': '#c0fb2d', - 'bright purple': '#be03fd', - 'dark red': '#840000', - 'pale blue': '#d0fefe', - 'grass green': '#3f9b0b', - 'navy': '#01153e', - 'aquamarine': '#04d8b2', - 'burnt orange': '#c04e01', - 'neon green': '#0cff0c', - 'bright blue': '#0165fc', - 'rose': '#cf6275', - 'light pink': '#ffd1df', - 'mustard': '#ceb301', - 'indigo': '#380282', - 'lime': '#aaff32', - 'sea green': '#53fca1', - 'periwinkle': '#8e82fe', - 'dark pink': '#cb416b', - 'olive green': '#677a04', - 'peach': '#ffb07c', - 'pale green': '#c7fdb5', - 'light brown': '#ad8150', - 'hot pink': '#ff028d', - 'black': '#000000', - 'lilac': '#cea2fd', - 'navy blue': '#001146', - 'royal blue': '#0504aa', - 'beige': '#e6daa6', - 'salmon': '#ff796c', - 'olive': '#6e750e', - 'maroon': '#650021', - 'bright green': '#01ff07', - 'dark purple': '#35063e', - 'mauve': '#ae7181', - 'forest green': '#06470c', - 'aqua': '#13eac9', - 'cyan': '#00ffff', - 'tan': '#d1b26f', - 'dark blue': '#00035b', - 'lavender': '#c79fef', - 'turquoise': '#06c2ac', - 'dark green': '#033500', - 'violet': '#9a0eea', - 'light purple': '#bf77f6', - 'lime green': '#89fe05', - 'grey': '#929591', - 'sky blue': '#75bbfd', - 'yellow': '#ffff14', - 'magenta': '#c20078', - 'light green': '#96f97b', - 'orange': '#f97306', - 'teal': '#029386', - 'light blue': '#95d0fc', - 'red': '#e50000', - 'brown': '#653700', - 'pink': '#ff81c0', - 'blue': '#0343df', - 'green': '#15b01a', - 'purple': '#7e1e9c' + "cloudy blue": "#acc2d9", + "dark pastel green": "#56ae57", + "dust": "#b2996e", + "electric lime": "#a8ff04", + "fresh green": "#69d84f", + "light eggplant": "#894585", + "nasty green": "#70b23f", + "really light blue": "#d4ffff", + "tea": "#65ab7c", + "warm purple": "#952e8f", + "yellowish tan": "#fcfc81", + "cement": "#a5a391", + "dark grass green": "#388004", + "dusty teal": "#4c9085", + "grey teal": "#5e9b8a", + "macaroni and cheese": "#efb435", + "pinkish tan": "#d99b82", + "spruce": "#0a5f38", + "strong blue": "#0c06f7", + "toxic green": "#61de2a", + "windows blue": "#3778bf", + "blue blue": "#2242c7", + "blue with a hint of purple": "#533cc6", + "booger": "#9bb53c", + "bright sea green": "#05ffa6", + "dark green blue": "#1f6357", + "deep turquoise": "#017374", + "green teal": "#0cb577", + "strong pink": "#ff0789", + "bland": "#afa88b", + "deep aqua": "#08787f", + "lavender pink": "#dd85d7", + "light moss green": "#a6c875", + "light seafoam green": "#a7ffb5", + "olive yellow": "#c2b709", + "pig pink": "#e78ea5", + "deep lilac": "#966ebd", + "desert": "#ccad60", + "dusty lavender": "#ac86a8", + "purpley grey": "#947e94", + "purply": "#983fb2", + "candy pink": "#ff63e9", + "light pastel green": "#b2fba5", + "boring green": "#63b365", + "kiwi green": "#8ee53f", + "light grey green": "#b7e1a1", + "orange pink": "#ff6f52", + "tea green": "#bdf8a3", + "very light brown": "#d3b683", + "egg shell": "#fffcc4", + "eggplant purple": "#430541", + "powder pink": "#ffb2d0", + "reddish grey": "#997570", + "baby shit brown": "#ad900d", + "liliac": "#c48efd", + "stormy blue": "#507b9c", + "ugly brown": "#7d7103", + "custard": "#fffd78", + "darkish pink": "#da467d", + "deep brown": "#410200", + "greenish beige": "#c9d179", + "manilla": "#fffa86", + "off blue": "#5684ae", + "battleship grey": "#6b7c85", + "browny green": "#6f6c0a", + "bruise": "#7e4071", + "kelley green": "#009337", + "sickly yellow": "#d0e429", + "sunny yellow": "#fff917", + "azul": "#1d5dec", + "darkgreen": "#054907", + "green/yellow": "#b5ce08", + "lichen": "#8fb67b", + "light light green": "#c8ffb0", + "pale gold": "#fdde6c", + "sun yellow": "#ffdf22", + "tan green": "#a9be70", + "burple": "#6832e3", + "butterscotch": "#fdb147", + "toupe": "#c7ac7d", + "dark cream": "#fff39a", + "indian red": "#850e04", + "light lavendar": "#efc0fe", + "poison green": "#40fd14", + "baby puke green": "#b6c406", + "bright yellow green": "#9dff00", + "charcoal grey": "#3c4142", + "squash": "#f2ab15", + "cinnamon": "#ac4f06", + "light pea green": "#c4fe82", + "radioactive green": "#2cfa1f", + "raw sienna": "#9a6200", + "baby purple": "#ca9bf7", + "cocoa": "#875f42", + "light royal blue": "#3a2efe", + "orangeish": "#fd8d49", + "rust brown": "#8b3103", + "sand brown": "#cba560", + "swamp": "#698339", + "tealish green": "#0cdc73", + "burnt siena": "#b75203", + "camo": "#7f8f4e", + "dusk blue": "#26538d", + "fern": "#63a950", + "old rose": "#c87f89", + "pale light green": "#b1fc99", + "peachy pink": "#ff9a8a", + "rosy pink": "#f6688e", + "light bluish green": "#76fda8", + "light bright green": "#53fe5c", + "light neon green": "#4efd54", + "light seafoam": "#a0febf", + "tiffany blue": "#7bf2da", + "washed out green": "#bcf5a6", + "browny orange": "#ca6b02", + "nice blue": "#107ab0", + "sapphire": "#2138ab", + "greyish teal": "#719f91", + "orangey yellow": "#fdb915", + "parchment": "#fefcaf", + "straw": "#fcf679", + "very dark brown": "#1d0200", + "terracota": "#cb6843", + "ugly blue": "#31668a", + "clear blue": "#247afd", + "creme": "#ffffb6", + "foam green": "#90fda9", + "grey/green": "#86a17d", + "light gold": "#fddc5c", + "seafoam blue": "#78d1b6", + "topaz": "#13bbaf", + "violet pink": "#fb5ffc", + "wintergreen": "#20f986", + "yellow tan": "#ffe36e", + "dark fuchsia": "#9d0759", + "indigo blue": "#3a18b1", + "light yellowish green": "#c2ff89", + "pale magenta": "#d767ad", + "rich purple": "#720058", + "sunflower yellow": "#ffda03", + "green/blue": "#01c08d", + "leather": "#ac7434", + "racing green": "#014600", + "vivid purple": "#9900fa", + "dark royal blue": "#02066f", + "hazel": "#8e7618", + "muted pink": "#d1768f", + "booger green": "#96b403", + "canary": "#fdff63", + "cool grey": "#95a3a6", + "dark taupe": "#7f684e", + "darkish purple": "#751973", + "true green": "#089404", + "coral pink": "#ff6163", + "dark sage": "#598556", + "dark slate blue": "#214761", + "flat blue": "#3c73a8", + "mushroom": "#ba9e88", + "rich blue": "#021bf9", + "dirty purple": "#734a65", + "greenblue": "#23c48b", + "icky green": "#8fae22", + "light khaki": "#e6f2a2", + "warm blue": "#4b57db", + "dark hot pink": "#d90166", + "deep sea blue": "#015482", + "carmine": "#9d0216", + "dark yellow green": "#728f02", + "pale peach": "#ffe5ad", + "plum purple": "#4e0550", + "golden rod": "#f9bc08", + "neon red": "#ff073a", + "old pink": "#c77986", + "very pale blue": "#d6fffe", + "blood orange": "#fe4b03", + "grapefruit": "#fd5956", + "sand yellow": "#fce166", + "clay brown": "#b2713d", + "dark blue grey": "#1f3b4d", + "flat green": "#699d4c", + "light green blue": "#56fca2", + "warm pink": "#fb5581", + "dodger blue": "#3e82fc", + "gross green": "#a0bf16", + "ice": "#d6fffa", + "metallic blue": "#4f738e", + "pale salmon": "#ffb19a", + "sap green": "#5c8b15", + "algae": "#54ac68", + "bluey grey": "#89a0b0", + "greeny grey": "#7ea07a", + "highlighter green": "#1bfc06", + "light light blue": "#cafffb", + "light mint": "#b6ffbb", + "raw umber": "#a75e09", + "vivid blue": "#152eff", + "deep lavender": "#8d5eb7", + "dull teal": "#5f9e8f", + "light greenish blue": "#63f7b4", + "mud green": "#606602", + "pinky": "#fc86aa", + "red wine": "#8c0034", + "shit green": "#758000", + "tan brown": "#ab7e4c", + "darkblue": "#030764", + "rosa": "#fe86a4", + "lipstick": "#d5174e", + "pale mauve": "#fed0fc", + "claret": "#680018", + "dandelion": "#fedf08", + "orangered": "#fe420f", + "poop green": "#6f7c00", + "ruby": "#ca0147", + "dark": "#1b2431", + "greenish turquoise": "#00fbb0", + "pastel red": "#db5856", + "piss yellow": "#ddd618", + "bright cyan": "#41fdfe", + "dark coral": "#cf524e", + "algae green": "#21c36f", + "darkish red": "#a90308", + "reddy brown": "#6e1005", + "blush pink": "#fe828c", + "camouflage green": "#4b6113", + "lawn green": "#4da409", + "putty": "#beae8a", + "vibrant blue": "#0339f8", + "dark sand": "#a88f59", + "purple/blue": "#5d21d0", + "saffron": "#feb209", + "twilight": "#4e518b", + "warm brown": "#964e02", + "bluegrey": "#85a3b2", + "bubble gum pink": "#ff69af", + "duck egg blue": "#c3fbf4", + "greenish cyan": "#2afeb7", + "petrol": "#005f6a", + "royal": "#0c1793", + "butter": "#ffff81", + "dusty orange": "#f0833a", + "off yellow": "#f1f33f", + "pale olive green": "#b1d27b", + "orangish": "#fc824a", + "leaf": "#71aa34", + "light blue grey": "#b7c9e2", + "dried blood": "#4b0101", + "lightish purple": "#a552e6", + "rusty red": "#af2f0d", + "lavender blue": "#8b88f8", + "light grass green": "#9af764", + "light mint green": "#a6fbb2", + "sunflower": "#ffc512", + "velvet": "#750851", + "brick orange": "#c14a09", + "lightish red": "#fe2f4a", + "pure blue": "#0203e2", + "twilight blue": "#0a437a", + "violet red": "#a50055", + "yellowy brown": "#ae8b0c", + "carnation": "#fd798f", + "muddy yellow": "#bfac05", + "dark seafoam green": "#3eaf76", + "deep rose": "#c74767", + "dusty red": "#b9484e", + "grey/blue": "#647d8e", + "lemon lime": "#bffe28", + "purple/pink": "#d725de", + "brown yellow": "#b29705", + "purple brown": "#673a3f", + "wisteria": "#a87dc2", + "banana yellow": "#fafe4b", + "lipstick red": "#c0022f", + "water blue": "#0e87cc", + "brown grey": "#8d8468", + "vibrant purple": "#ad03de", + "baby green": "#8cff9e", + "barf green": "#94ac02", + "eggshell blue": "#c4fff7", + "sandy yellow": "#fdee73", + "cool green": "#33b864", + "pale": "#fff9d0", + "blue/grey": "#758da3", + "hot magenta": "#f504c9", + "greyblue": "#77a1b5", + "purpley": "#8756e4", + "baby shit green": "#889717", + "brownish pink": "#c27e79", + "dark aquamarine": "#017371", + "diarrhea": "#9f8303", + "light mustard": "#f7d560", + "pale sky blue": "#bdf6fe", + "turtle green": "#75b84f", + "bright olive": "#9cbb04", + "dark grey blue": "#29465b", + "greeny brown": "#696006", + "lemon green": "#adf802", + "light periwinkle": "#c1c6fc", + "seaweed green": "#35ad6b", + "sunshine yellow": "#fffd37", + "ugly purple": "#a442a0", + "medium pink": "#f36196", + "puke brown": "#947706", + "very light pink": "#fff4f2", + "viridian": "#1e9167", + "bile": "#b5c306", + "faded yellow": "#feff7f", + "very pale green": "#cffdbc", + "vibrant green": "#0add08", + "bright lime": "#87fd05", + "spearmint": "#1ef876", + "light aquamarine": "#7bfdc7", + "light sage": "#bcecac", + "yellowgreen": "#bbf90f", + "baby poo": "#ab9004", + "dark seafoam": "#1fb57a", + "deep teal": "#00555a", + "heather": "#a484ac", + "rust orange": "#c45508", + "dirty blue": "#3f829d", + "fern green": "#548d44", + "bright lilac": "#c95efb", + "weird green": "#3ae57f", + "peacock blue": "#016795", + "avocado green": "#87a922", + "faded orange": "#f0944d", + "grape purple": "#5d1451", + "hot green": "#25ff29", + "lime yellow": "#d0fe1d", + "mango": "#ffa62b", + "shamrock": "#01b44c", + "bubblegum": "#ff6cb5", + "purplish brown": "#6b4247", + "vomit yellow": "#c7c10c", + "pale cyan": "#b7fffa", + "key lime": "#aeff6e", + "tomato red": "#ec2d01", + "lightgreen": "#76ff7b", + "merlot": "#730039", + "night blue": "#040348", + "purpleish pink": "#df4ec8", + "apple": "#6ecb3c", + "baby poop green": "#8f9805", + "green apple": "#5edc1f", + "heliotrope": "#d94ff5", + "yellow/green": "#c8fd3d", + "almost black": "#070d0d", + "cool blue": "#4984b8", + "leafy green": "#51b73b", + "mustard brown": "#ac7e04", + "dusk": "#4e5481", + "dull brown": "#876e4b", + "frog green": "#58bc08", + "vivid green": "#2fef10", + "bright light green": "#2dfe54", + "fluro green": "#0aff02", + "kiwi": "#9cef43", + "seaweed": "#18d17b", + "navy green": "#35530a", + "ultramarine blue": "#1805db", + "iris": "#6258c4", + "pastel orange": "#ff964f", + "yellowish orange": "#ffab0f", + "perrywinkle": "#8f8ce7", + "tealish": "#24bca8", + "dark plum": "#3f012c", + "pear": "#cbf85f", + "pinkish orange": "#ff724c", + "midnight purple": "#280137", + "light urple": "#b36ff6", + "dark mint": "#48c072", + "greenish tan": "#bccb7a", + "light burgundy": "#a8415b", + "turquoise blue": "#06b1c4", + "ugly pink": "#cd7584", + "sandy": "#f1da7a", + "electric pink": "#ff0490", + "muted purple": "#805b87", + "mid green": "#50a747", + "greyish": "#a8a495", + "neon yellow": "#cfff04", + "banana": "#ffff7e", + "carnation pink": "#ff7fa7", + "tomato": "#ef4026", + "sea": "#3c9992", + "muddy brown": "#886806", + "turquoise green": "#04f489", + "buff": "#fef69e", + "fawn": "#cfaf7b", + "muted blue": "#3b719f", + "pale rose": "#fdc1c5", + "dark mint green": "#20c073", + "amethyst": "#9b5fc0", + "blue/green": "#0f9b8e", + "chestnut": "#742802", + "sick green": "#9db92c", + "pea": "#a4bf20", + "rusty orange": "#cd5909", + "stone": "#ada587", + "rose red": "#be013c", + "pale aqua": "#b8ffeb", + "deep orange": "#dc4d01", + "earth": "#a2653e", + "mossy green": "#638b27", + "grassy green": "#419c03", + "pale lime green": "#b1ff65", + "light grey blue": "#9dbcd4", + "pale grey": "#fdfdfe", + "asparagus": "#77ab56", + "blueberry": "#464196", + "purple red": "#990147", + "pale lime": "#befd73", + "greenish teal": "#32bf84", + "caramel": "#af6f09", + "deep magenta": "#a0025c", + "light peach": "#ffd8b1", + "milk chocolate": "#7f4e1e", + "ocher": "#bf9b0c", + "off green": "#6ba353", + "purply pink": "#f075e6", + "lightblue": "#7bc8f6", + "dusky blue": "#475f94", + "golden": "#f5bf03", + "light beige": "#fffeb6", + "butter yellow": "#fffd74", + "dusky purple": "#895b7b", + "french blue": "#436bad", + "ugly yellow": "#d0c101", + "greeny yellow": "#c6f808", + "orangish red": "#f43605", + "shamrock green": "#02c14d", + "orangish brown": "#b25f03", + "tree green": "#2a7e19", + "deep violet": "#490648", + "gunmetal": "#536267", + "blue/purple": "#5a06ef", + "cherry": "#cf0234", + "sandy brown": "#c4a661", + "warm grey": "#978a84", + "dark indigo": "#1f0954", + "midnight": "#03012d", + "bluey green": "#2bb179", + "grey pink": "#c3909b", + "soft purple": "#a66fb5", + "blood": "#770001", + "brown red": "#922b05", + "medium grey": "#7d7f7c", + "berry": "#990f4b", + "poo": "#8f7303", + "purpley pink": "#c83cb9", + "light salmon": "#fea993", + "snot": "#acbb0d", + "easter purple": "#c071fe", + "light yellow green": "#ccfd7f", + "dark navy blue": "#00022e", + "drab": "#828344", + "light rose": "#ffc5cb", + "rouge": "#ab1239", + "purplish red": "#b0054b", + "slime green": "#99cc04", + "baby poop": "#937c00", + "irish green": "#019529", + "pink/purple": "#ef1de7", + "dark navy": "#000435", + "greeny blue": "#42b395", + "light plum": "#9d5783", + "pinkish grey": "#c8aca9", + "dirty orange": "#c87606", + "rust red": "#aa2704", + "pale lilac": "#e4cbff", + "orangey red": "#fa4224", + "primary blue": "#0804f9", + "kermit green": "#5cb200", + "brownish purple": "#76424e", + "murky green": "#6c7a0e", + "wheat": "#fbdd7e", + "very dark purple": "#2a0134", + "bottle green": "#044a05", + "watermelon": "#fd4659", + "deep sky blue": "#0d75f8", + "fire engine red": "#fe0002", + "yellow ochre": "#cb9d06", + "pumpkin orange": "#fb7d07", + "pale olive": "#b9cc81", + "light lilac": "#edc8ff", + "lightish green": "#61e160", + "carolina blue": "#8ab8fe", + "mulberry": "#920a4e", + "shocking pink": "#fe02a2", + "auburn": "#9a3001", + "bright lime green": "#65fe08", + "celadon": "#befdb7", + "pinkish brown": "#b17261", + "poo brown": "#885f01", + "bright sky blue": "#02ccfe", + "celery": "#c1fd95", + "dirt brown": "#836539", + "strawberry": "#fb2943", + "dark lime": "#84b701", + "copper": "#b66325", + "medium brown": "#7f5112", + "muted green": "#5fa052", + "robin's egg": "#6dedfd", + "bright aqua": "#0bf9ea", + "bright lavender": "#c760ff", + "ivory": "#ffffcb", + "very light purple": "#f6cefc", + "light navy": "#155084", + "pink red": "#f5054f", + "olive brown": "#645403", + "poop brown": "#7a5901", + "mustard green": "#a8b504", + "ocean green": "#3d9973", + "very dark blue": "#000133", + "dusty green": "#76a973", + "light navy blue": "#2e5a88", + "minty green": "#0bf77d", + "adobe": "#bd6c48", + "barney": "#ac1db8", + "jade green": "#2baf6a", + "bright light blue": "#26f7fd", + "light lime": "#aefd6c", + "dark khaki": "#9b8f55", + "orange yellow": "#ffad01", + "ocre": "#c69c04", + "maize": "#f4d054", + "faded pink": "#de9dac", + "british racing green": "#05480d", + "sandstone": "#c9ae74", + "mud brown": "#60460f", + "light sea green": "#98f6b0", + "robin egg blue": "#8af1fe", + "aqua marine": "#2ee8bb", + "dark sea green": "#11875d", + "soft pink": "#fdb0c0", + "orangey brown": "#b16002", + "cherry red": "#f7022a", + "burnt yellow": "#d5ab09", + "brownish grey": "#86775f", + "camel": "#c69f59", + "purplish grey": "#7a687f", + "marine": "#042e60", + "greyish pink": "#c88d94", + "pale turquoise": "#a5fbd5", + "pastel yellow": "#fffe71", + "bluey purple": "#6241c7", + "canary yellow": "#fffe40", + "faded red": "#d3494e", + "sepia": "#985e2b", + "coffee": "#a6814c", + "bright magenta": "#ff08e8", + "mocha": "#9d7651", + "ecru": "#feffca", + "purpleish": "#98568d", + "cranberry": "#9e003a", + "darkish green": "#287c37", + "brown orange": "#b96902", + "dusky rose": "#ba6873", + "melon": "#ff7855", + "sickly green": "#94b21c", + "silver": "#c5c9c7", + "purply blue": "#661aee", + "purpleish blue": "#6140ef", + "hospital green": "#9be5aa", + "shit brown": "#7b5804", + "mid blue": "#276ab3", + "amber": "#feb308", + "easter green": "#8cfd7e", + "soft blue": "#6488ea", + "cerulean blue": "#056eee", + "golden brown": "#b27a01", + "bright turquoise": "#0ffef9", + "red pink": "#fa2a55", + "red purple": "#820747", + "greyish brown": "#7a6a4f", + "vermillion": "#f4320c", + "russet": "#a13905", + "steel grey": "#6f828a", + "lighter purple": "#a55af4", + "bright violet": "#ad0afd", + "prussian blue": "#004577", + "slate green": "#658d6d", + "dirty pink": "#ca7b80", + "dark blue green": "#005249", + "pine": "#2b5d34", + "yellowy green": "#bff128", + "dark gold": "#b59410", + "bluish": "#2976bb", + "darkish blue": "#014182", + "dull red": "#bb3f3f", + "pinky red": "#fc2647", + "bronze": "#a87900", + "pale teal": "#82cbb2", + "military green": "#667c3e", + "barbie pink": "#fe46a5", + "bubblegum pink": "#fe83cc", + "pea soup green": "#94a617", + "dark mustard": "#a88905", + "shit": "#7f5f00", + "medium purple": "#9e43a2", + "very dark green": "#062e03", + "dirt": "#8a6e45", + "dusky pink": "#cc7a8b", + "red violet": "#9e0168", + "lemon yellow": "#fdff38", + "pistachio": "#c0fa8b", + "dull yellow": "#eedc5b", + "dark lime green": "#7ebd01", + "denim blue": "#3b5b92", + "teal blue": "#01889f", + "lightish blue": "#3d7afd", + "purpley blue": "#5f34e7", + "light indigo": "#6d5acf", + "swamp green": "#748500", + "brown green": "#706c11", + "dark maroon": "#3c0008", + "hot purple": "#cb00f5", + "dark forest green": "#002d04", + "faded blue": "#658cbb", + "drab green": "#749551", + "light lime green": "#b9ff66", + "snot green": "#9dc100", + "yellowish": "#faee66", + "light blue green": "#7efbb3", + "bordeaux": "#7b002c", + "light mauve": "#c292a1", + "ocean": "#017b92", + "marigold": "#fcc006", + "muddy green": "#657432", + "dull orange": "#d8863b", + "steel": "#738595", + "electric purple": "#aa23ff", + "fluorescent green": "#08ff08", + "yellowish brown": "#9b7a01", + "blush": "#f29e8e", + "soft green": "#6fc276", + "bright orange": "#ff5b00", + "lemon": "#fdff52", + "purple grey": "#866f85", + "acid green": "#8ffe09", + "pale lavender": "#eecffe", + "violet blue": "#510ac9", + "light forest green": "#4f9153", + "burnt red": "#9f2305", + "khaki green": "#728639", + "cerise": "#de0c62", + "faded purple": "#916e99", + "apricot": "#ffb16d", + "dark olive green": "#3c4d03", + "grey brown": "#7f7053", + "green grey": "#77926f", + "true blue": "#010fcc", + "pale violet": "#ceaefa", + "periwinkle blue": "#8f99fb", + "light sky blue": "#c6fcff", + "blurple": "#5539cc", + "green brown": "#544e03", + "bluegreen": "#017a79", + "bright teal": "#01f9c6", + "brownish yellow": "#c9b003", + "pea soup": "#929901", + "forest": "#0b5509", + "barney purple": "#a00498", + "ultramarine": "#2000b1", + "purplish": "#94568c", + "puke yellow": "#c2be0e", + "bluish grey": "#748b97", + "dark periwinkle": "#665fd1", + "dark lilac": "#9c6da5", + "reddish": "#c44240", + "light maroon": "#a24857", + "dusty purple": "#825f87", + "terra cotta": "#c9643b", + "avocado": "#90b134", + "marine blue": "#01386a", + "teal green": "#25a36f", + "slate grey": "#59656d", + "lighter green": "#75fd63", + "electric green": "#21fc0d", + "dusty blue": "#5a86ad", + "golden yellow": "#fec615", + "bright yellow": "#fffd01", + "light lavender": "#dfc5fe", + "umber": "#b26400", + "poop": "#7f5e00", + "dark peach": "#de7e5d", + "jungle green": "#048243", + "eggshell": "#ffffd4", + "denim": "#3b638c", + "yellow brown": "#b79400", + "dull purple": "#84597e", + "chocolate brown": "#411900", + "wine red": "#7b0323", + "neon blue": "#04d9ff", + "dirty green": "#667e2c", + "light tan": "#fbeeac", + "ice blue": "#d7fffe", + "cadet blue": "#4e7496", + "dark mauve": "#874c62", + "very light blue": "#d5ffff", + "grey purple": "#826d8c", + "pastel pink": "#ffbacd", + "very light green": "#d1ffbd", + "dark sky blue": "#448ee4", + "evergreen": "#05472a", + "dull pink": "#d5869d", + "aubergine": "#3d0734", + "mahogany": "#4a0100", + "reddish orange": "#f8481c", + "deep green": "#02590f", + "vomit green": "#89a203", + "purple pink": "#e03fd8", + "dusty pink": "#d58a94", + "faded green": "#7bb274", + "camo green": "#526525", + "pinky purple": "#c94cbe", + "pink purple": "#db4bda", + "brownish red": "#9e3623", + "dark rose": "#b5485d", + "mud": "#735c12", + "brownish": "#9c6d57", + "emerald green": "#028f1e", + "pale brown": "#b1916e", + "dull blue": "#49759c", + "burnt umber": "#a0450e", + "medium green": "#39ad48", + "clay": "#b66a50", + "light aqua": "#8cffdb", + "light olive green": "#a4be5c", + "brownish orange": "#cb7723", + "dark aqua": "#05696b", + "purplish pink": "#ce5dae", + "dark salmon": "#c85a53", + "greenish grey": "#96ae8d", + "jade": "#1fa774", + "ugly green": "#7a9703", + "dark beige": "#ac9362", + "emerald": "#01a049", + "pale red": "#d9544d", + "light magenta": "#fa5ff7", + "sky": "#82cafc", + "light cyan": "#acfffc", + "yellow orange": "#fcb001", + "reddish purple": "#910951", + "reddish pink": "#fe2c54", + "orchid": "#c875c4", + "dirty yellow": "#cdc50a", + "orange red": "#fd411e", + "deep red": "#9a0200", + "orange brown": "#be6400", + "cobalt blue": "#030aa7", + "neon pink": "#fe019a", + "rose pink": "#f7879a", + "greyish purple": "#887191", + "raspberry": "#b00149", + "aqua green": "#12e193", + "salmon pink": "#fe7b7c", + "tangerine": "#ff9408", + "brownish green": "#6a6e09", + "red brown": "#8b2e16", + "greenish brown": "#696112", + "pumpkin": "#e17701", + "pine green": "#0a481e", + "charcoal": "#343837", + "baby pink": "#ffb7ce", + "cornflower": "#6a79f7", + "blue violet": "#5d06e9", + "chocolate": "#3d1c02", + "greyish green": "#82a67d", + "scarlet": "#be0119", + "green yellow": "#c9ff27", + "dark olive": "#373e02", + "sienna": "#a9561e", + "pastel purple": "#caa0ff", + "terracotta": "#ca6641", + "aqua blue": "#02d8e9", + "sage green": "#88b378", + "blood red": "#980002", + "deep pink": "#cb0162", + "grass": "#5cac2d", + "moss": "#769958", + "pastel blue": "#a2bffe", + "bluish green": "#10a674", + "green blue": "#06b48b", + "dark tan": "#af884a", + "greenish blue": "#0b8b87", + "pale orange": "#ffa756", + "vomit": "#a2a415", + "forrest green": "#154406", + "dark lavender": "#856798", + "dark violet": "#34013f", + "purple blue": "#632de9", + "dark cyan": "#0a888a", + "olive drab": "#6f7632", + "pinkish": "#d46a7e", + "cobalt": "#1e488f", + "neon purple": "#bc13fe", + "light turquoise": "#7ef4cc", + "apple green": "#76cd26", + "dull green": "#74a662", + "wine": "#80013f", + "powder blue": "#b1d1fc", + "off white": "#ffffe4", + "electric blue": "#0652ff", + "dark turquoise": "#045c5a", + "blue purple": "#5729ce", + "azure": "#069af3", + "bright red": "#ff000d", + "pinkish red": "#f10c45", + "cornflower blue": "#5170d7", + "light olive": "#acbf69", + "grape": "#6c3461", + "greyish blue": "#5e819d", + "purplish blue": "#601ef9", + "yellowish green": "#b0dd16", + "greenish yellow": "#cdfd02", + "medium blue": "#2c6fbb", + "dusty rose": "#c0737a", + "light violet": "#d6b4fc", + "midnight blue": "#020035", + "bluish purple": "#703be7", + "red orange": "#fd3c06", + "dark magenta": "#960056", + "greenish": "#40a368", + "ocean blue": "#03719c", + "coral": "#fc5a50", + "cream": "#ffffc2", + "reddish brown": "#7f2b0a", + "burnt sienna": "#b04e0f", + "brick": "#a03623", + "sage": "#87ae73", + "grey green": "#789b73", + "white": "#ffffff", + "robin's egg blue": "#98eff9", + "moss green": "#658b38", + "steel blue": "#5a7d9a", + "eggplant": "#380835", + "light yellow": "#fffe7a", + "leaf green": "#5ca904", + "light grey": "#d8dcd6", + "puke": "#a5a502", + "pinkish purple": "#d648d7", + "sea blue": "#047495", + "pale purple": "#b790d4", + "slate blue": "#5b7c99", + "blue grey": "#607c8e", + "hunter green": "#0b4008", + "fuchsia": "#ed0dd9", + "crimson": "#8c000f", + "pale yellow": "#ffff84", + "ochre": "#bf9005", + "mustard yellow": "#d2bd0a", + "light red": "#ff474c", + "cerulean": "#0485d1", + "pale pink": "#ffcfdc", + "deep blue": "#040273", + "rust": "#a83c09", + "light teal": "#90e4c1", + "slate": "#516572", + "goldenrod": "#fac205", + "dark yellow": "#d5b60a", + "dark grey": "#363737", + "army green": "#4b5d16", + "grey blue": "#6b8ba4", + "seafoam": "#80f9ad", + "puce": "#a57e52", + "spring green": "#a9f971", + "dark orange": "#c65102", + "sand": "#e2ca76", + "pastel green": "#b0ff9d", + "mint": "#9ffeb0", + "light orange": "#fdaa48", + "bright pink": "#fe01b1", + "chartreuse": "#c1f80a", + "deep purple": "#36013f", + "dark brown": "#341c02", + "taupe": "#b9a281", + "pea green": "#8eab12", + "puke green": "#9aae07", + "kelly green": "#02ab2e", + "seafoam green": "#7af9ab", + "blue green": "#137e6d", + "khaki": "#aaa662", + "burgundy": "#610023", + "dark teal": "#014d4e", + "brick red": "#8f1402", + "royal purple": "#4b006e", + "plum": "#580f41", + "mint green": "#8fff9f", + "gold": "#dbb40c", + "baby blue": "#a2cffe", + "yellow green": "#c0fb2d", + "bright purple": "#be03fd", + "dark red": "#840000", + "pale blue": "#d0fefe", + "grass green": "#3f9b0b", + "navy": "#01153e", + "aquamarine": "#04d8b2", + "burnt orange": "#c04e01", + "neon green": "#0cff0c", + "bright blue": "#0165fc", + "rose": "#cf6275", + "light pink": "#ffd1df", + "mustard": "#ceb301", + "indigo": "#380282", + "lime": "#aaff32", + "sea green": "#53fca1", + "periwinkle": "#8e82fe", + "dark pink": "#cb416b", + "olive green": "#677a04", + "peach": "#ffb07c", + "pale green": "#c7fdb5", + "light brown": "#ad8150", + "hot pink": "#ff028d", + "black": "#000000", + "lilac": "#cea2fd", + "navy blue": "#001146", + "royal blue": "#0504aa", + "beige": "#e6daa6", + "salmon": "#ff796c", + "olive": "#6e750e", + "maroon": "#650021", + "bright green": "#01ff07", + "dark purple": "#35063e", + "mauve": "#ae7181", + "forest green": "#06470c", + "aqua": "#13eac9", + "cyan": "#00ffff", + "tan": "#d1b26f", + "dark blue": "#00035b", + "lavender": "#c79fef", + "turquoise": "#06c2ac", + "dark green": "#033500", + "violet": "#9a0eea", + "light purple": "#bf77f6", + "lime green": "#89fe05", + "grey": "#929591", + "sky blue": "#75bbfd", + "yellow": "#ffff14", + "magenta": "#c20078", + "light green": "#96f97b", + "orange": "#f97306", + "teal": "#029386", + "light blue": "#95d0fc", + "red": "#e50000", + "brown": "#653700", + "pink": "#ff81c0", + "blue": "#0343df", + "green": "#15b01a", + "purple": "#7e1e9c", } # Normalize name to "xkcd:" to avoid name collisions. -XKCD_COLORS_NORM = {'xkcd:' + name: value - for name, value in XKCD_COLORS.items()} +XKCD_COLORS_NORM = {"xkcd:" + name: value for name, value in XKCD_COLORS.items()} # https://drafts.csswg.org/css-color-4/#named-colors CSS4_COLORS = { - 'aliceblue': '#F0F8FF', - 'antiquewhite': '#FAEBD7', - 'aqua': '#00FFFF', - 'aquamarine': '#7FFFD4', - 'azure': '#F0FFFF', - 'beige': '#F5F5DC', - 'bisque': '#FFE4C4', - 'black': '#000000', - 'blanchedalmond': '#FFEBCD', - 'blue': '#0000FF', - 'blueviolet': '#8A2BE2', - 'brown': '#A52A2A', - 'burlywood': '#DEB887', - 'cadetblue': '#5F9EA0', - 'chartreuse': '#7FFF00', - 'chocolate': '#D2691E', - 'coral': '#FF7F50', - 'cornflowerblue': '#6495ED', - 'cornsilk': '#FFF8DC', - 'crimson': '#DC143C', - 'cyan': '#00FFFF', - 'darkblue': '#00008B', - 'darkcyan': '#008B8B', - 'darkgoldenrod': '#B8860B', - 'darkgray': '#A9A9A9', - 'darkgreen': '#006400', - 'darkgrey': '#A9A9A9', - 'darkkhaki': '#BDB76B', - 'darkmagenta': '#8B008B', - 'darkolivegreen': '#556B2F', - 'darkorange': '#FF8C00', - 'darkorchid': '#9932CC', - 'darkred': '#8B0000', - 'darksalmon': '#E9967A', - 'darkseagreen': '#8FBC8F', - 'darkslateblue': '#483D8B', - 'darkslategray': '#2F4F4F', - 'darkslategrey': '#2F4F4F', - 'darkturquoise': '#00CED1', - 'darkviolet': '#9400D3', - 'deeppink': '#FF1493', - 'deepskyblue': '#00BFFF', - 'dimgray': '#696969', - 'dimgrey': '#696969', - 'dodgerblue': '#1E90FF', - 'firebrick': '#B22222', - 'floralwhite': '#FFFAF0', - 'forestgreen': '#228B22', - 'fuchsia': '#FF00FF', - 'gainsboro': '#DCDCDC', - 'ghostwhite': '#F8F8FF', - 'gold': '#FFD700', - 'goldenrod': '#DAA520', - 'gray': '#808080', - 'green': '#008000', - 'greenyellow': '#ADFF2F', - 'grey': '#808080', - 'honeydew': '#F0FFF0', - 'hotpink': '#FF69B4', - 'indianred': '#CD5C5C', - 'indigo': '#4B0082', - 'ivory': '#FFFFF0', - 'khaki': '#F0E68C', - 'lavender': '#E6E6FA', - 'lavenderblush': '#FFF0F5', - 'lawngreen': '#7CFC00', - 'lemonchiffon': '#FFFACD', - 'lightblue': '#ADD8E6', - 'lightcoral': '#F08080', - 'lightcyan': '#E0FFFF', - 'lightgoldenrodyellow': '#FAFAD2', - 'lightgray': '#D3D3D3', - 'lightgreen': '#90EE90', - 'lightgrey': '#D3D3D3', - 'lightpink': '#FFB6C1', - 'lightsalmon': '#FFA07A', - 'lightseagreen': '#20B2AA', - 'lightskyblue': '#87CEFA', - 'lightslategray': '#778899', - 'lightslategrey': '#778899', - 'lightsteelblue': '#B0C4DE', - 'lightyellow': '#FFFFE0', - 'lime': '#00FF00', - 'limegreen': '#32CD32', - 'linen': '#FAF0E6', - 'magenta': '#FF00FF', - 'maroon': '#800000', - 'mediumaquamarine': '#66CDAA', - 'mediumblue': '#0000CD', - 'mediumorchid': '#BA55D3', - 'mediumpurple': '#9370DB', - 'mediumseagreen': '#3CB371', - 'mediumslateblue': '#7B68EE', - 'mediumspringgreen': '#00FA9A', - 'mediumturquoise': '#48D1CC', - 'mediumvioletred': '#C71585', - 'midnightblue': '#191970', - 'mintcream': '#F5FFFA', - 'mistyrose': '#FFE4E1', - 'moccasin': '#FFE4B5', - 'navajowhite': '#FFDEAD', - 'navy': '#000080', - 'oldlace': '#FDF5E6', - 'olive': '#808000', - 'olivedrab': '#6B8E23', - 'orange': '#FFA500', - 'orangered': '#FF4500', - 'orchid': '#DA70D6', - 'palegoldenrod': '#EEE8AA', - 'palegreen': '#98FB98', - 'paleturquoise': '#AFEEEE', - 'palevioletred': '#DB7093', - 'papayawhip': '#FFEFD5', - 'peachpuff': '#FFDAB9', - 'peru': '#CD853F', - 'pink': '#FFC0CB', - 'plum': '#DDA0DD', - 'powderblue': '#B0E0E6', - 'purple': '#800080', - 'rebeccapurple': '#663399', - 'red': '#FF0000', - 'rosybrown': '#BC8F8F', - 'royalblue': '#4169E1', - 'saddlebrown': '#8B4513', - 'salmon': '#FA8072', - 'sandybrown': '#F4A460', - 'seagreen': '#2E8B57', - 'seashell': '#FFF5EE', - 'sienna': '#A0522D', - 'silver': '#C0C0C0', - 'skyblue': '#87CEEB', - 'slateblue': '#6A5ACD', - 'slategray': '#708090', - 'slategrey': '#708090', - 'snow': '#FFFAFA', - 'springgreen': '#00FF7F', - 'steelblue': '#4682B4', - 'tan': '#D2B48C', - 'teal': '#008080', - 'thistle': '#D8BFD8', - 'tomato': '#FF6347', - 'turquoise': '#40E0D0', - 'violet': '#EE82EE', - 'wheat': '#F5DEB3', - 'white': '#FFFFFF', - 'whitesmoke': '#F5F5F5', - 'yellow': '#FFFF00', - 'yellowgreen': '#9ACD32' + "aliceblue": "#F0F8FF", + "antiquewhite": "#FAEBD7", + "aqua": "#00FFFF", + "aquamarine": "#7FFFD4", + "azure": "#F0FFFF", + "beige": "#F5F5DC", + "bisque": "#FFE4C4", + "black": "#000000", + "blanchedalmond": "#FFEBCD", + "blue": "#0000FF", + "blueviolet": "#8A2BE2", + "brown": "#A52A2A", + "burlywood": "#DEB887", + "cadetblue": "#5F9EA0", + "chartreuse": "#7FFF00", + "chocolate": "#D2691E", + "coral": "#FF7F50", + "cornflowerblue": "#6495ED", + "cornsilk": "#FFF8DC", + "crimson": "#DC143C", + "cyan": "#00FFFF", + "darkblue": "#00008B", + "darkcyan": "#008B8B", + "darkgoldenrod": "#B8860B", + "darkgray": "#A9A9A9", + "darkgreen": "#006400", + "darkgrey": "#A9A9A9", + "darkkhaki": "#BDB76B", + "darkmagenta": "#8B008B", + "darkolivegreen": "#556B2F", + "darkorange": "#FF8C00", + "darkorchid": "#9932CC", + "darkred": "#8B0000", + "darksalmon": "#E9967A", + "darkseagreen": "#8FBC8F", + "darkslateblue": "#483D8B", + "darkslategray": "#2F4F4F", + "darkslategrey": "#2F4F4F", + "darkturquoise": "#00CED1", + "darkviolet": "#9400D3", + "deeppink": "#FF1493", + "deepskyblue": "#00BFFF", + "dimgray": "#696969", + "dimgrey": "#696969", + "dodgerblue": "#1E90FF", + "firebrick": "#B22222", + "floralwhite": "#FFFAF0", + "forestgreen": "#228B22", + "fuchsia": "#FF00FF", + "gainsboro": "#DCDCDC", + "ghostwhite": "#F8F8FF", + "gold": "#FFD700", + "goldenrod": "#DAA520", + "gray": "#808080", + "green": "#008000", + "greenyellow": "#ADFF2F", + "grey": "#808080", + "honeydew": "#F0FFF0", + "hotpink": "#FF69B4", + "indianred": "#CD5C5C", + "indigo": "#4B0082", + "ivory": "#FFFFF0", + "khaki": "#F0E68C", + "lavender": "#E6E6FA", + "lavenderblush": "#FFF0F5", + "lawngreen": "#7CFC00", + "lemonchiffon": "#FFFACD", + "lightblue": "#ADD8E6", + "lightcoral": "#F08080", + "lightcyan": "#E0FFFF", + "lightgoldenrodyellow": "#FAFAD2", + "lightgray": "#D3D3D3", + "lightgreen": "#90EE90", + "lightgrey": "#D3D3D3", + "lightpink": "#FFB6C1", + "lightsalmon": "#FFA07A", + "lightseagreen": "#20B2AA", + "lightskyblue": "#87CEFA", + "lightslategray": "#778899", + "lightslategrey": "#778899", + "lightsteelblue": "#B0C4DE", + "lightyellow": "#FFFFE0", + "lime": "#00FF00", + "limegreen": "#32CD32", + "linen": "#FAF0E6", + "magenta": "#FF00FF", + "maroon": "#800000", + "mediumaquamarine": "#66CDAA", + "mediumblue": "#0000CD", + "mediumorchid": "#BA55D3", + "mediumpurple": "#9370DB", + "mediumseagreen": "#3CB371", + "mediumslateblue": "#7B68EE", + "mediumspringgreen": "#00FA9A", + "mediumturquoise": "#48D1CC", + "mediumvioletred": "#C71585", + "midnightblue": "#191970", + "mintcream": "#F5FFFA", + "mistyrose": "#FFE4E1", + "moccasin": "#FFE4B5", + "navajowhite": "#FFDEAD", + "navy": "#000080", + "oldlace": "#FDF5E6", + "olive": "#808000", + "olivedrab": "#6B8E23", + "orange": "#FFA500", + "orangered": "#FF4500", + "orchid": "#DA70D6", + "palegoldenrod": "#EEE8AA", + "palegreen": "#98FB98", + "paleturquoise": "#AFEEEE", + "palevioletred": "#DB7093", + "papayawhip": "#FFEFD5", + "peachpuff": "#FFDAB9", + "peru": "#CD853F", + "pink": "#FFC0CB", + "plum": "#DDA0DD", + "powderblue": "#B0E0E6", + "purple": "#800080", + "rebeccapurple": "#663399", + "red": "#FF0000", + "rosybrown": "#BC8F8F", + "royalblue": "#4169E1", + "saddlebrown": "#8B4513", + "salmon": "#FA8072", + "sandybrown": "#F4A460", + "seagreen": "#2E8B57", + "seashell": "#FFF5EE", + "sienna": "#A0522D", + "silver": "#C0C0C0", + "skyblue": "#87CEEB", + "slateblue": "#6A5ACD", + "slategray": "#708090", + "slategrey": "#708090", + "snow": "#FFFAFA", + "springgreen": "#00FF7F", + "steelblue": "#4682B4", + "tan": "#D2B48C", + "teal": "#008080", + "thistle": "#D8BFD8", + "tomato": "#FF6347", + "turquoise": "#40E0D0", + "violet": "#EE82EE", + "wheat": "#F5DEB3", + "white": "#FFFFFF", + "whitesmoke": "#F5F5F5", + "yellow": "#FFFF00", + "yellowgreen": "#9ACD32", } # Normalize name to "tab:" to avoid name collisions. -CSS4_COLORS_NORM = {'css:' + name: value - for name, value in CSS4_COLORS.items()} +CSS4_COLORS_NORM = {"css:" + name: value for name, value in CSS4_COLORS.items()} ALL_COLORS = {} ALL_COLORS.update(BASE_COLORS) diff --git a/core/changelog.py b/core/changelog.py index 1427670875..9a10b4dc35 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -34,10 +34,10 @@ class Version: def __init__(self, bot, version: str, lines: str): self.bot = bot - self.version = version.lstrip('vV') + self.version = version.lstrip("vV") self.lines = [x for x in lines.splitlines() if x] self.fields = defaultdict(str) - self.description = '' + self.description = "" self.parse() def __repr__(self) -> str: @@ -50,16 +50,16 @@ def parse(self) -> None: curr_action = None for line in self.lines: - if line.startswith('### '): + if line.startswith("### "): curr_action = line[4:] elif curr_action is None: - self.description += line + '\n' + self.description += line + "\n" else: - self.fields[curr_action] += line + '\n' + self.fields[curr_action] += line + "\n" @property def url(self) -> str: - return Changelog.CHANGELOG_URL + '#v' + self.version.replace('.', '') + return Changelog.CHANGELOG_URL + "#v" + self.version.replace(".", "") @property def embed(self) -> Embed: @@ -68,14 +68,14 @@ def embed(self) -> Embed: """ embed = Embed(color=Color.green(), description=self.description) embed.set_author( - name=f'v{self.version} - Changelog', + name=f"v{self.version} - Changelog", icon_url=self.bot.user.avatar_url, - url=self.url + url=self.url, ) for name, value in self.fields.items(): embed.add_field(name=name, value=value) - embed.set_footer(text=f'Current version: v{self.bot.version}') + embed.set_footer(text=f"Current version: v{self.bot.version}") embed.set_thumbnail(url=self.bot.user.avatar_url) return embed @@ -110,15 +110,16 @@ class Changelog: The regex used to parse the versions. """ - RAW_CHANGELOG_URL = 'https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md' - CHANGELOG_URL = 'https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md' - VERSION_REGEX = re.compile(r'# (v\d+\.\d+\.\d+)([\S\s]*?(?=# v|$))') + RAW_CHANGELOG_URL = ( + "https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md" + ) + CHANGELOG_URL = "https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md" + VERSION_REGEX = re.compile(r"# (v\d+\.\d+\.\d+)([\S\s]*?(?=# v|$))") def __init__(self, bot, text: str): self.bot = bot self.text = text - self.versions = [Version(bot, *m) - for m in self.VERSION_REGEX.findall(text)] + self.versions = [Version(bot, *m) for m in self.VERSION_REGEX.findall(text)] @property def latest_version(self) -> Version: @@ -135,7 +136,7 @@ def embeds(self) -> List[Embed]: return [v.embed for v in self.versions] @classmethod - async def from_url(cls, bot, url: str = '') -> 'Changelog': + async def from_url(cls, bot, url: str = "") -> "Changelog": """ Create a `Changelog` from a URL. diff --git a/core/checks.py b/core/checks.py index e7075a6596..0779d3d617 100644 --- a/core/checks.py +++ b/core/checks.py @@ -5,7 +5,7 @@ from core.models import PermissionLevel from core.utils import error -logger = logging.getLogger('Modmail') +logger = logging.getLogger("Modmail") def has_permissions(permission_level: PermissionLevel = PermissionLevel.REGULAR): @@ -28,25 +28,33 @@ async def setup(ctx): """ async def predicate(ctx): - has_perm = await check_permissions(ctx, ctx.command.qualified_name, permission_level) - - if not has_perm and ctx.command.qualified_name != 'help': - logger.error(error(f'You does not have permission to use this command: ' - f'`{ctx.command.qualified_name}` ({permission_level.name}).')) + has_perm = await check_permissions( + ctx, ctx.command.qualified_name, permission_level + ) + + if not has_perm and ctx.command.qualified_name != "help": + logger.error( + error( + f"You does not have permission to use this command: " + f"`{ctx.command.qualified_name}` ({permission_level.name})." + ) + ) return has_perm predicate.permission_level = permission_level return commands.check(predicate) -async def check_permissions(ctx: commands.Context, - command_name: str, - permission_level: PermissionLevel) -> bool: +async def check_permissions(ctx, command_name, permission_level) -> bool: + """Logic for checking permissions for a command for a user""" if await ctx.bot.is_owner(ctx.author): # Direct bot owner (creator) has absolute power over the bot return True - if permission_level != PermissionLevel.OWNER and ctx.channel.permissions_for(ctx.author).administrator: + if ( + permission_level != PermissionLevel.OWNER + and ctx.channel.permissions_for(ctx.author).administrator + ): # Administrators have permission to all non-owner commands return True @@ -57,7 +65,9 @@ async def check_permissions(ctx: commands.Context, # -1 is for @everyone if -1 in command_permissions[command_name]: return True - has_perm_role = any(role.id in command_permissions[command_name] for role in author_roles) + has_perm_role = any( + role.id in command_permissions[command_name] for role in author_roles + ) has_perm_id = ctx.author.id in command_permissions[command_name] return has_perm_role or has_perm_id @@ -67,7 +77,9 @@ async def check_permissions(ctx: commands.Context, if level >= permission_level and level.name in level_permissions: if -1 in level_permissions[level.name]: return True - has_perm_role = any(role.id in level_permissions[level.name] for role in author_roles) + has_perm_role = any( + role.id in level_permissions[level.name] for role in author_roles + ) has_perm_id = ctx.author.id in level_permissions[level.name] if has_perm_role or has_perm_id: return True @@ -95,5 +107,6 @@ async def predicate(ctx): Otherwise, `False`. """ return ctx.thread is not None - predicate.fail_msg = 'This is not a Modmail thread.' + + predicate.fail_msg = "This is not a Modmail thread." return commands.check(predicate) diff --git a/core/clients.py b/core/clients.py index f64620a8ff..c26a4b6935 100644 --- a/core/clients.py +++ b/core/clients.py @@ -12,11 +12,11 @@ from core.utils import info -logger = logging.getLogger('Modmail') +logger = logging.getLogger("Modmail") -prefix = os.getenv('LOG_URL_PREFIX', '/logs') -if prefix == 'NONE': - prefix = '' +prefix = os.getenv("LOG_URL_PREFIX", "/logs") +if prefix == "NONE": + prefix = "" class RequestClient: @@ -43,12 +43,14 @@ def __init__(self, bot): self.session = bot.session self.headers: dict = None - async def request(self, url: str, - method: str = 'GET', - payload: dict = None, - return_response: bool = False, - headers: dict = None) -> Union[ClientResponse, - dict, str]: + async def request( + self, + url: str, + method: str = "GET", + payload: dict = None, + return_response: bool = False, + headers: dict = None, + ) -> Union[ClientResponse, dict, str]: """ Makes a HTTP request. @@ -78,8 +80,9 @@ async def request(self, url: str, headers.update(self.headers) else: headers = self.headers - 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: @@ -153,24 +156,21 @@ class GitHub(RequestClient): URL to star Modmail. """ - BASE = 'https://api.github.com' - REPO = BASE + '/repos/kyb3r/modmail' - HEAD = REPO + '/git/refs/heads/master' - MERGE_URL = BASE + '/repos/{username}/modmail/merges' - FORK_URL = REPO + '/forks' - STAR_URL = BASE + '/user/starred/kyb3r/modmail' - - def __init__(self, bot, - access_token: str = '', - username: str = '', - **kwargs): + BASE = "https://api.github.com" + REPO = BASE + "/repos/kyb3r/modmail" + HEAD = REPO + "/git/refs/heads/master" + MERGE_URL = BASE + "/repos/{username}/modmail/merges" + FORK_URL = REPO + "/forks" + STAR_URL = BASE + "/user/starred/kyb3r/modmail" + + def __init__(self, bot, access_token: str = "", username: str = "", **kwargs): super().__init__(bot) self.access_token = access_token self.username = username - self.avatar_url: str = kwargs.pop('avatar_url', '') - self.url: str = kwargs.pop('url', '') + self.avatar_url: str = kwargs.pop("avatar_url", "") + self.url: str = kwargs.pop("url", "") if self.access_token: - self.headers = {'Authorization': 'token ' + str(access_token)} + self.headers = {"Authorization": "token " + str(access_token)} async def update_repository(self, sha: str = None) -> Optional[dict]: """ @@ -191,17 +191,13 @@ async def update_repository(self, sha: str = None) -> Optional[dict]: if sha is None: resp: dict = await self.request(self.HEAD) - sha = resp['object']['sha'] + sha = resp["object"]["sha"] - payload = { - 'base': 'master', - 'head': sha, - 'commit_message': 'Updating bot' - } + payload = {"base": "master", "head": sha, "commit_message": "Updating bot"} merge_url = self.MERGE_URL.format(username=self.username) - resp = await self.request(merge_url, method='POST', payload=payload) + resp = await self.request(merge_url, method="POST", payload=payload) if isinstance(resp, dict): return resp @@ -209,7 +205,7 @@ async def fork_repository(self) -> None: """ Forks Modmail's repository. """ - await self.request(self.FORK_URL, method='POST') + await self.request(self.FORK_URL, method="POST") async def has_starred(self) -> bool: """ @@ -228,11 +224,10 @@ async def star_repository(self) -> None: """ Stars Modmail's repository. """ - await self.request(self.STAR_URL, method='PUT', - headers={'Content-Length': '0'}) + await self.request(self.STAR_URL, method="PUT", headers={"Content-Length": "0"}) @classmethod - async def login(cls, bot) -> 'GitHub': + async def login(cls, bot) -> "GitHub": """ Logs in to GitHub with configuration variable information. @@ -246,27 +241,24 @@ async def login(cls, bot) -> 'GitHub': GitHub The newly created `GitHub` object. """ - self = cls(bot, bot.config.get('github_access_token'), ) - resp: dict = await self.request('https://api.github.com/user') - self.username: str = resp['login'] - self.avatar_url: str = resp['avatar_url'] - self.url: str = resp['html_url'] - logger.info(info(f'GitHub logged in to: {self.username}')) + self = cls(bot, bot.config.get("github_access_token")) + resp: dict = await self.request("https://api.github.com/user") + self.username: str = resp["login"] + self.avatar_url: str = resp["avatar_url"] + self.url: str = resp["html_url"] + logger.info(info(f"GitHub logged in to: {self.username}")) return self class ApiClient(RequestClient): - def __init__(self, bot): super().__init__(bot) if self.token: - self.headers = { - 'Authorization': 'Bearer ' + self.token - } + self.headers = {"Authorization": "Bearer " + self.token} @property def token(self) -> Optional[str]: - return self.bot.config.get('github_access_token') + return self.bot.config.get("github_access_token") @property def db(self): @@ -277,61 +269,57 @@ def logs(self): return self.db.logs async def get_user_logs(self, user_id: Union[str, int]) -> list: - query = { - 'recipient.id': str(user_id), - 'guild_id': str(self.bot.guild_id) - } + query = {"recipient.id": str(user_id), "guild_id": str(self.bot.guild_id)} - projection = { - 'messages': {'$slice': 5} - } + projection = {"messages": {"$slice": 5}} return await self.logs.find(query, projection).to_list(None) async def get_log(self, channel_id: Union[str, int]) -> dict: - return await self.logs.find_one({'channel_id': str(channel_id)}) + return await self.logs.find_one({"channel_id": str(channel_id)}) async def get_log_link(self, channel_id: Union[str, int]) -> str: doc = await self.get_log(channel_id) return f"{self.bot.config.log_url.strip('/')}{prefix}/{doc['key']}" - async def create_log_entry(self, - recipient: Member, - channel: TextChannel, - creator: Member) -> str: + async def create_log_entry( + self, recipient: Member, channel: TextChannel, creator: Member + ) -> str: key = secrets.token_hex(6) - await self.logs.insert_one({ - 'key': key, - 'open': True, - 'created_at': str(datetime.utcnow()), - 'closed_at': None, - 'channel_id': str(channel.id), - 'guild_id': str(self.bot.guild_id), - 'recipient': { - 'id': str(recipient.id), - 'name': recipient.name, - 'discriminator': recipient.discriminator, - 'avatar_url': str(recipient.avatar_url), - 'mod': False - }, - 'creator': { - 'id': str(creator.id), - 'name': creator.name, - 'discriminator': creator.discriminator, - 'avatar_url': str(creator.avatar_url), - 'mod': isinstance(creator, Member) - }, - 'closer': None, - 'messages': [] - }) + await self.logs.insert_one( + { + "key": key, + "open": True, + "created_at": str(datetime.utcnow()), + "closed_at": None, + "channel_id": str(channel.id), + "guild_id": str(self.bot.guild_id), + "recipient": { + "id": str(recipient.id), + "name": recipient.name, + "discriminator": recipient.discriminator, + "avatar_url": str(recipient.avatar_url), + "mod": False, + }, + "creator": { + "id": str(creator.id), + "name": creator.name, + "discriminator": creator.discriminator, + "avatar_url": str(creator.avatar_url), + "mod": isinstance(creator, Member), + }, + "closer": None, + "messages": [], + } + ) return f"{self.bot.config.log_url.strip('/')}{prefix}/{key}" async def get_config(self) -> dict: - conf = await self.db.config.find_one({'bot_id': self.bot.user.id}) + conf = await self.db.config.find_one({"bot_id": self.bot.user.id}) if conf is None: - await self.db.config.insert_one({'bot_id': self.bot.user.id}) - return {'bot_id': self.bot.user.id} + await self.db.config.insert_one({"bot_id": self.bot.user.id}) + return {"bot_id": self.bot.user.id} return conf async def update_config(self, data: dict): @@ -343,83 +331,78 @@ async def update_config(self, data: dict): unset = {k: 1 for k in valid_keys if k not in data} return await self.db.config.update_one( - {'bot_id': self.bot.user.id}, - {'$set': toset, '$unset': unset} + {"bot_id": self.bot.user.id}, {"$set": toset, "$unset": unset} ) - async def edit_message(self, message_id: Union[int, str], - new_content: str) -> None: - await self.logs.update_one({ - 'messages.message_id': str(message_id) - }, { - '$set': { - 'messages.$.content': new_content, - 'messages.$.edited': True - } - }) + async def edit_message(self, message_id: Union[int, str], new_content: str) -> None: + await self.logs.update_one( + {"messages.message_id": str(message_id)}, + {"$set": {"messages.$.content": new_content, "messages.$.edited": True}}, + ) - async def append_log(self, - message: Message, - channel_id: Union[str, int] = '', - type_: str = 'thread_message') -> dict: + async def append_log( + self, + message: Message, + channel_id: Union[str, int] = "", + type_: str = "thread_message", + ) -> dict: channel_id = str(channel_id) or str(message.channel.id) data = { - 'timestamp': str(message.created_at), - 'message_id': str(message.id), - 'author': { - 'id': str(message.author.id), - 'name': message.author.name, - 'discriminator': message.author.discriminator, - 'avatar_url': str(message.author.avatar_url), - 'mod': not isinstance(message.channel, DMChannel), + "timestamp": str(message.created_at), + "message_id": str(message.id), + "author": { + "id": str(message.author.id), + "name": message.author.name, + "discriminator": message.author.discriminator, + "avatar_url": str(message.author.avatar_url), + "mod": not isinstance(message.channel, DMChannel), }, - 'content': message.content, - 'type': type_, - 'attachments': [ + "content": message.content, + "type": type_, + "attachments": [ { - 'id': a.id, - 'filename': a.filename, - 'is_image': a.width is not None, - 'size': a.size, - 'url': a.url - } for a in message.attachments - ] + "id": a.id, + "filename": a.filename, + "is_image": a.width is not None, + "size": a.size, + "url": a.url, + } + for a in message.attachments + ], } return await self.logs.find_one_and_update( - {'channel_id': channel_id}, - {'$push': {f'messages': data}}, - return_document=True + {"channel_id": channel_id}, + {"$push": {f"messages": data}}, + return_document=True, ) - async def post_log(self, - channel_id: Union[int, str], - data: dict) -> dict: + 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": {k: v for k, v in data.items()}}, + return_document=True, ) async def update_repository(self) -> dict: user = await GitHub.login(self.bot) data = await user.update_repository() return { - 'data': data, - 'user': { - 'username': user.username, - 'avatar_url': user.avatar_url, - 'url': user.url - } + "data": data, + "user": { + "username": user.username, + "avatar_url": user.avatar_url, + "url": user.url, + }, } async def get_user_info(self) -> dict: user = await GitHub.login(self.bot) return { - 'user': { - 'username': user.username, - 'avatar_url': user.avatar_url, - 'url': user.url + "user": { + "username": user.username, + "avatar_url": user.avatar_url, + "url": user.url, } } diff --git a/core/config.py b/core/config.py index 10efecb6c6..b4cb8922c3 100644 --- a/core/config.py +++ b/core/config.py @@ -16,65 +16,80 @@ class ConfigManager: allowed_to_change_in_command = { # activity - 'twitch_url', - + "twitch_url", # bot settings - 'main_category_id', 'disable_autoupdates', 'prefix', 'mention', - 'main_color', 'user_typing', 'mod_typing', 'account_age', 'guild_age', - 'reply_without_command', - + "main_category_id", + "disable_autoupdates", + "prefix", + "mention", + "main_color", + "user_typing", + "mod_typing", + "account_age", + "guild_age", + "reply_without_command", # logging - 'log_channel_id', - + "log_channel_id", # threads - 'sent_emoji', 'blocked_emoji', 'close_emoji', 'disable_recipient_thread_close', - 'thread_creation_response', 'thread_creation_footer', 'thread_creation_title', - 'thread_close_footer', 'thread_close_title', 'thread_close_response', - 'thread_self_close_response', - + "sent_emoji", + "blocked_emoji", + "close_emoji", + "disable_recipient_thread_close", + "thread_creation_response", + "thread_creation_footer", + "thread_creation_title", + "thread_close_footer", + "thread_close_title", + "thread_close_response", + "thread_self_close_response", # moderation - 'recipient_color', 'mod_tag', 'mod_color', - + "recipient_color", + "mod_tag", + "mod_color", # anonymous message - 'anon_username', 'anon_avatar_url', 'anon_tag' + "anon_username", + "anon_avatar_url", + "anon_tag", } internal_keys = { # bot presence - 'activity_message', 'activity_type', 'status', 'oauth_whitelist', - + "activity_message", + "activity_type", + "status", + "oauth_whitelist", # moderation - 'blocked', 'command_permissions', 'level_permissions', - + "blocked", + "command_permissions", + "level_permissions", # threads - 'snippets', 'notification_squad', 'subscriptions', 'closures', - + "snippets", + "notification_squad", + "subscriptions", + "closures", # misc - 'aliases', 'plugins' + "aliases", + "plugins", } protected_keys = { # Modmail - 'modmail_guild_id', 'guild_id', - 'log_url', 'mongo_uri', 'owners', - + "modmail_guild_id", + "guild_id", + "log_url", + "mongo_uri", + "owners", # bot - 'token', - + "token", # GitHub - 'github_access_token', - + "github_access_token", # Logging - 'log_level' + "log_level", } - colors = { - 'mod_color', 'recipient_color', 'main_color' - } + colors = {"mod_color", "recipient_color", "main_color"} - time_deltas = { - 'account_age', 'guild_age' - } + time_deltas = {"account_age", "guild_age"} valid_keys = allowed_to_change_in_command | internal_keys | protected_keys @@ -105,34 +120,32 @@ def cache(self, val: dict): def populate_cache(self) -> dict: data = { - 'snippets': {}, - 'plugins': [], - 'aliases': {}, - 'blocked': {}, - 'oauth_whitelist': [], - 'command_permissions': {}, - 'level_permissions': {}, - 'notification_squad': {}, - 'subscriptions': {}, - 'closures': {}, - 'log_level': 'INFO' + "snippets": {}, + "plugins": [], + "aliases": {}, + "blocked": {}, + "oauth_whitelist": [], + "command_permissions": {}, + "level_permissions": {}, + "notification_squad": {}, + "subscriptions": {}, + "closures": {}, + "log_level": "INFO", } data.update(os.environ) - if os.path.exists('config.json'): - with open('config.json') as f: + if os.path.exists("config.json"): + with open("config.json") as f: # Config json should override env vars data.update(json.load(f)) self.cache = { - k.lower(): v for k, v in data.items() - if k.lower() in self.valid_keys + k.lower(): v for k, v in data.items() if k.lower() in self.valid_keys } return self.cache - async def clean_data(self, key: str, - val: typing.Any) -> typing.Tuple[str, str]: + async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: value_text = val clean_value = val @@ -142,20 +155,36 @@ async def clean_data(self, key: str, if hex_ is None: if not isinstance(val, str): - raise InvalidConfigError('Invalid color name or hex.') - if val.startswith('#'): + raise InvalidConfigError("Invalid color name or hex.") + if val.startswith("#"): val = val[1:] if len(val) != 6: - raise InvalidConfigError('Invalid color name or hex.') + raise InvalidConfigError("Invalid color name or hex.") for letter in val: - if letter not in {'0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}: - raise InvalidConfigError('Invalid color name or hex.') - clean_value = '#' + val + if letter not in { + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "a", + "b", + "c", + "d", + "e", + "f", + }: + raise InvalidConfigError("Invalid color name or hex.") + clean_value = "#" + val value_text = clean_value else: clean_value = hex_ - value_text = f'{val} ({clean_value})' + value_text = f"{val} ({clean_value})" elif key in self.time_deltas: try: @@ -170,11 +199,11 @@ async def clean_data(self, key: str, raise InvalidConfigError(*exc.args) except Exception: raise InvalidConfigError( - 'Unrecognized time, please use ISO-8601 duration format ' + "Unrecognized time, please use ISO-8601 duration format " 'string or a simpler "human readable" time.' ) clean_value = isodate.duration_isoformat(time.dt - converter.now) - value_text = f'{val} ({clean_value})' + value_text = f"{val} ({clean_value})" return clean_value, value_text diff --git a/core/decorators.py b/core/decorators.py index 4d9ef729e9..03deb09e61 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -16,15 +16,15 @@ async def wrapper(self, ctx: commands.Context, *args, **kwargs): def github_access_token_required(func): @functools.wraps(func) async def wrapper(self, ctx: commands.Context, *args, **kwargs): - if self.bot.config.get('github_access_token'): + if self.bot.config.get("github_access_token"): return await func(self, ctx, *args, **kwargs) - desc = ('You can only use this command if you have a ' - 'configured `GITHUB_ACCESS_TOKEN`. Get a ' - 'personal access token from developer settings.') - embed = Embed(color=Color.red(), - title='Unauthorized', - description=desc) + desc = ( + "You can only use this command if you have a " + "configured `GITHUB_ACCESS_TOKEN`. Get a " + "personal access token from developer settings." + ) + embed = Embed(color=Color.red(), title="Unauthorized", description=desc) await ctx.send(embed=embed) return wrapper diff --git a/core/models.py b/core/models.py index 2995996e36..3877ab23d6 100644 --- a/core/models.py +++ b/core/models.py @@ -23,6 +23,4 @@ def __init__(self, msg, *args): @property def embed(self): - return Embed(title="Error", - description=self.msg, - color=Color.red()) + return Embed(title="Error", description=self.msg, color=Color.red()) diff --git a/core/paginator.py b/core/paginator.py index 0b28e41dd8..ec9ca4416f 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -1,5 +1,5 @@ import typing -from asyncio import TimeoutError +import asyncio from discord import User, Reaction, Message, Embed from discord import HTTPException, InvalidArgument @@ -43,27 +43,26 @@ class PaginatorSession: def __init__(self, ctx: commands.Context, *embeds, **options): self.ctx = ctx - self.timeout: int = options.get('timeout', 210) + self.timeout: int = options.get("timeout", 210) self.embeds: typing.List[Embed] = list(embeds) self.running = False self.base: Message = None self.current = 0 - self.destination = options.get('destination', ctx) + self.destination = options.get("destination", ctx) self.reaction_map = { - '⏮': self.first_page, - '◀': self.previous_page, - '▶': self.next_page, - '⏭': self.last_page, + "⏮": self.first_page, + "◀": self.previous_page, + "▶": self.next_page, + "⏭": self.last_page, # '⏹': self.close } - if options.get('edit_footer', True) and len(self.embeds) > 1: + if options.get("edit_footer", True) and len(self.embeds) > 1: for i, embed in enumerate(self.embeds): - footer_text = f'Page {i + 1} of {len(self.embeds)}' + footer_text = f"Page {i + 1} of {len(self.embeds)}" if embed.footer.text: - footer_text = footer_text + ' • ' + embed.footer.text - embed.set_footer(text=footer_text, - icon_url=embed.footer.icon_url) + 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: """ @@ -77,7 +76,7 @@ def add_page(self, embed: Embed) -> None: if isinstance(embed, Embed): self.embeds.append(embed) else: - raise TypeError('Page must be an Embed object.') + raise TypeError("Page must be an Embed object.") async def create_base(self, embed: Embed) -> None: """ @@ -96,7 +95,7 @@ async def create_base(self, embed: Embed) -> None: self.running = True for reaction in self.reaction_map: - if len(self.embeds) == 2 and reaction in '⏮⏭': + if len(self.embeds) == 2 and reaction in "⏮⏭": continue await self.base.add_reaction(reaction) @@ -134,9 +133,11 @@ def react_check(self, reaction: Reaction, user: User) -> bool: ------- bool """ - return (reaction.message.id == self.base.id and - user.id == self.ctx.author.id and - reaction.emoji in self.reaction_map.keys()) + return ( + reaction.message.id == self.base.id + and user.id == self.ctx.author.id + and reaction.emoji in self.reaction_map.keys() + ) async def run(self) -> typing.Optional[Message]: """ @@ -152,11 +153,9 @@ async def run(self) -> typing.Optional[Message]: while self.running: try: reaction, user = await self.ctx.bot.wait_for( - 'reaction_add', - check=self.react_check, - timeout=self.timeout + "reaction_add", check=self.react_check, timeout=self.timeout ) - except TimeoutError: + except asyncio.TimeoutError: return await self.close(delete=False) else: action = self.reaction_map.get(reaction.emoji) @@ -196,7 +195,7 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: self.running = False try: - await self.ctx.message.add_reaction('✅') + await self.ctx.message.add_reaction("✅") except (HTTPException, InvalidArgument): pass @@ -222,10 +221,11 @@ async def last_page(self) -> None: class MessagePaginatorSession: - def __init__(self, ctx: commands.Context, *messages, - embed: Embed = None, **options): + def __init__( + self, ctx: commands.Context, *messages, embed: Embed = None, **options + ): self.ctx = ctx - self.timeout: int = options.get('timeout', 180) + self.timeout: int = options.get("timeout", 180) self.messages: typing.List[str] = list(messages) self.running = False @@ -238,10 +238,10 @@ def __init__(self, ctx: commands.Context, *messages, self.current = 0 self.reaction_map = { - '⏮': self.first_page, - '◀': self.previous_page, - '▶': self.next_page, - '⏭': self.last_page, + "⏮": self.first_page, + "◀": self.previous_page, + "▶": self.next_page, + "⏭": self.last_page, # '⏹': self.close } @@ -257,7 +257,7 @@ def add_page(self, msg: str) -> None: if isinstance(msg, str): self.messages.append(msg) else: - raise TypeError('Page must be a str object.') + raise TypeError("Page must be a str object.") async def create_base(self, msg: str) -> None: """ @@ -269,11 +269,10 @@ async def create_base(self, msg: str) -> None: The message content to fill the base `Message`. """ if self.embed is not None: - footer_text = f'Page {self.current+1} of {len(self.messages)}' + footer_text = f"Page {self.current+1} of {len(self.messages)}" if self.footer_text: - footer_text = footer_text + ' • ' + self.footer_text - self.embed.set_footer(text=footer_text, - icon_url=self.embed.footer.icon_url) + footer_text = footer_text + " • " + self.footer_text + self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url) self.base = await self.ctx.send(content=msg, embed=self.embed) @@ -283,7 +282,7 @@ async def create_base(self, msg: str) -> None: self.running = True for reaction in self.reaction_map: - if len(self.messages) == 2 and reaction in '⏮⏭': + if len(self.messages) == 2 and reaction in "⏮⏭": continue await self.base.add_reaction(reaction) @@ -303,11 +302,10 @@ async def show_page(self, index: int) -> None: page = self.messages[index] if self.embed is not None: - footer_text = f'Page {self.current + 1} of {len(self.messages)}' + footer_text = f"Page {self.current + 1} of {len(self.messages)}" if self.footer_text: - footer_text = footer_text + ' • ' + self.footer_text - self.embed.set_footer(text=footer_text, - icon_url=self.embed.footer.icon_url) + footer_text = footer_text + " • " + self.footer_text + self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url) if self.running: await self.base.edit(content=page, embed=self.embed) @@ -328,9 +326,11 @@ def react_check(self, reaction: Reaction, user: User) -> bool: ------- bool """ - return (reaction.message.id == self.base.id and - user.id == self.ctx.author.id and - reaction.emoji in self.reaction_map.keys()) + return ( + reaction.message.id == self.base.id + and user.id == self.ctx.author.id + and reaction.emoji in self.reaction_map.keys() + ) async def run(self) -> typing.Optional[Message]: """ @@ -346,11 +346,9 @@ async def run(self) -> typing.Optional[Message]: while self.running: try: reaction, user = await self.ctx.bot.wait_for( - 'reaction_add', - check=self.react_check, - timeout=self.timeout + "reaction_add", check=self.react_check, timeout=self.timeout ) - except TimeoutError: + except asyncio.TimeoutError: return await self.close(delete=False) else: action = self.reaction_map.get(reaction.emoji) @@ -390,7 +388,7 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: self.running = False try: - await self.ctx.message.add_reaction('✅') + await self.ctx.message.add_reaction("✅") except (HTTPException, InvalidArgument): pass diff --git a/core/thread.py b/core/thread.py index d148b0c6c8..37a23f2f81 100644 --- a/core/thread.py +++ b/core/thread.py @@ -13,16 +13,18 @@ from core.utils import is_image_url, days, match_user_id from core.utils import truncate, ignore, error -logger = logging.getLogger('Modmail') +logger = logging.getLogger("Modmail") class Thread: """Represents a discord Modmail thread""" - def __init__(self, manager: 'ThreadManager', - recipient: typing.Union[discord.Member, discord.User, int], - channel: typing.Union[discord.DMChannel, - discord.TextChannel] = None): + def __init__( + self, + manager: "ThreadManager", + recipient: typing.Union[discord.Member, discord.User, int], + channel: typing.Union[discord.DMChannel, discord.TextChannel] = None, + ): self.manager = manager self.bot = manager.bot if isinstance(recipient, int): @@ -30,7 +32,7 @@ def __init__(self, manager: 'ThreadManager', self._recipient = None else: if recipient.bot: - raise CommandError('Recipient cannot be a bot.') + raise CommandError("Recipient cannot be a bot.") self._id = recipient.id self._recipient = recipient self._channel = channel @@ -39,8 +41,10 @@ def __init__(self, manager: 'ThreadManager', self._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.""" @@ -80,14 +84,15 @@ def ready(self, flag: bool): async def setup(self, *, creator=None, category=None): """Create the thread channel and other io related initialisation tasks""" - self.bot.dispatch('thread_create', self) + self.bot.dispatch("thread_create", self) recipient = self.recipient # 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 @@ -99,32 +104,31 @@ async def setup(self, *, creator=None, category=None): name=self.manager.format_channel_name(recipient), category=category, overwrites=overwrites, - reason='Creating a thread channel' + reason="Creating a thread channel", ) self._channel = channel try: log_url, log_data = await asyncio.gather( - self.bot.api.create_log_entry(recipient, channel, - creator or recipient), - self.bot.api.get_user_logs(recipient.id) + self.bot.api.create_log_entry(recipient, channel, creator or recipient), + self.bot.api.get_user_logs(recipient.id), ) - log_count = sum(1 for log in log_data if not log['open']) + log_count = sum(1 for log in log_data if not log["open"]) except: # Something went wrong with database? log_url = log_count = None # ensure core functionality still works - info_embed = self.manager.format_info_embed(recipient, log_url, - log_count, - discord.Color.green()) + info_embed = self.manager.format_info_embed( + recipient, log_url, log_count, discord.Color.green() + ) - topic = f'User ID: {recipient.id}' + topic = f"User ID: {recipient.id}" if creator: mention = None else: - mention = self.bot.config.get('mention', '@here') + mention = self.bot.config.get("mention", "@here") async def send_genesis_message(): try: @@ -135,15 +139,15 @@ async def send_genesis_message(): pass finally: self.ready = True - self.bot.dispatch('thread_ready', self) + self.bot.dispatch("thread_ready", self) await channel.edit(topic=topic) self.bot.loop.create_task(send_genesis_message()) # Once thread is ready, tell the recipient. thread_creation_response = self.bot.config.get( - 'thread_creation_response', - 'The staff team will get back to you as soon as possible.' + "thread_creation_response", + "The staff team will get back to you as soon as possible.", ) embed = discord.Embed( @@ -152,19 +156,18 @@ async def send_genesis_message(): timestamp=channel.created_at, ) - footer = 'Your message has been sent' - if not self.bot.config.get('disable_recipient_thread_close'): - footer = 'Click the lock to close the thread' + footer = "Your message has been sent" + if not self.bot.config.get("disable_recipient_thread_close"): + footer = "Click the lock to close the thread" - footer = self.bot.config.get('thread_creation_footer', footer) + footer = self.bot.config.get("thread_creation_footer", footer) embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) - embed.title = self.bot.config.get('thread_creation_title', - 'Thread Created') + embed.title = self.bot.config.get("thread_creation_title", "Thread Created") if creator is None: msg = await recipient.send(embed=embed) - if not self.bot.config.get('disable_recipient_thread_close'): - close_emoji = self.bot.config.get('close_emoji', '🔒') + if not self.bot.config.get("disable_recipient_thread_close"): + close_emoji = self.bot.config.get("close_emoji", "🔒") close_emoji = await self.bot.convert_emoji(close_emoji) await msg.add_reaction(close_emoji) @@ -173,11 +176,15 @@ def _close_after(self, closer, silent, delete_channel, message): self._close(closer, silent, delete_channel, message, True) ) - async def close(self, *, closer: typing.Union[discord.Member, discord.User], - after: int = 0, - silent: bool = False, - delete_channel: bool = True, - message: str = None) -> None: + async def close( + self, + *, + closer: typing.Union[discord.Member, discord.User], + after: int = 0, + silent: bool = False, + delete_channel: bool = True, + message: str = None, + ) -> None: """Close a thread now or after a set time in seconds""" # restarts the after timer @@ -190,24 +197,24 @@ async def close(self, *, closer: typing.Union[discord.Member, discord.User], now = datetime.utcnow() items = { # 'initiation_time': now.isoformat(), - 'time': (now + timedelta(seconds=after)).isoformat(), - 'closer_id': closer.id, - 'silent': silent, - 'delete_channel': delete_channel, - 'message': message + "time": (now + timedelta(seconds=after)).isoformat(), + "closer_id": closer.id, + "silent": silent, + "delete_channel": delete_channel, + "message": message, } self.bot.config.closures[str(self.id)] = items await self.bot.config.update() self.close_task = self.bot.loop.call_later( - after, self._close_after, closer, - silent, delete_channel, message + after, self._close_after, closer, silent, delete_channel, message ) else: await self._close(closer, silent, delete_channel, message) - async def _close(self, closer, silent=False, delete_channel=True, - message=None, scheduled=False): + async def _close( + self, closer, silent=False, delete_channel=True, message=None, scheduled=False + ): del self.manager.cache[self.id] await self.cancel_closure() @@ -216,30 +223,33 @@ async def _close(self, closer, silent=False, delete_channel=True, del self.bot.config.subscriptions[str(self.id)] # Logging - log_data = await self.bot.api.post_log(self.channel.id, { - 'open': False, - 'closed_at': str(datetime.utcnow()), - 'close_message': message if not silent else None, - 'closer': { - 'id': str(closer.id), - 'name': closer.name, - 'discriminator': closer.discriminator, - 'avatar_url': str(closer.avatar_url), - 'mod': True - } - }) + log_data = await self.bot.api.post_log( + self.channel.id, + { + "open": False, + "closed_at": str(datetime.utcnow()), + "close_message": message if not silent else None, + "closer": { + "id": str(closer.id), + "name": closer.name, + "discriminator": closer.discriminator, + "avatar_url": str(closer.avatar_url), + "mod": True, + }, + }, + ) if log_data is not None and isinstance(log_data, dict): - prefix = os.getenv('LOG_URL_PREFIX', '/logs') - if prefix == 'NONE': - prefix = '' + prefix = os.getenv("LOG_URL_PREFIX", "/logs") + if prefix == "NONE": + prefix = "" log_url = f"{self.bot.config.log_url.strip('/')}{prefix}/{log_data['key']}" - if log_data['messages']: - content = str(log_data['messages'][0]['content']) - sneak_peak = content.replace('\n', '') + if log_data["messages"]: + content = str(log_data["messages"][0]["content"]) + sneak_peak = content.replace("\n", "") else: - sneak_peak = 'No content' + sneak_peak = "No content" desc = f"[`{log_data['key']}`]({log_url}): " desc += truncate(sneak_peak, max=75 - 13) @@ -252,23 +262,21 @@ async def _close(self, closer, silent=False, delete_channel=True, if self.recipient is not None: user = f"{self.recipient} (`{self.id}`)" else: - user = f'`{self.id}`' + user = f"`{self.id}`" if self.id == closer.id: - _closer = 'the Recipient' + _closer = "the Recipient" else: - _closer = f'{closer} ({closer.id})' + _closer = f"{closer} ({closer.id})" embed.title = user - event = 'Thread Closed as Scheduled' if scheduled else 'Thread Closed' + event = "Thread Closed as Scheduled" if scheduled else "Thread Closed" # embed.set_author(name=f'Event: {event}', url=log_url) - embed.set_footer(text=f'{event} by {_closer}') + embed.set_footer(text=f"{event} by {_closer}") embed.timestamp = datetime.utcnow() - tasks = [ - self.bot.config.update() - ] + tasks = [self.bot.config.update()] try: tasks.append(self.bot.log_channel.send(embed=embed)) @@ -278,30 +286,29 @@ async def _close(self, closer, silent=False, delete_channel=True, # Thread closed message embed = discord.Embed( - title=self.bot.config.get('thread_close_title', 'Thread Closed'), + title=self.bot.config.get("thread_close_title", "Thread Closed"), color=discord.Color.red(), - timestamp=datetime.utcnow()) + timestamp=datetime.utcnow(), + ) if not message: if self.id == closer.id: message = self.bot.config.get( - 'thread_self_close_response', - 'You have closed this Modmail thread.' + "thread_self_close_response", "You have closed this Modmail thread." ) else: message = self.bot.config.get( - 'thread_close_response', - '{closer.mention} has closed this Modmail thread.' + "thread_close_response", + "{closer.mention} has closed this Modmail thread.", ) - message = message.format(closer=closer, loglink=log_url, - logkey=log_data['key']) + message = message.format(closer=closer, loglink=log_url, logkey=log_data["key"]) embed.description = message - footer = self.bot.config.get('thread_close_footer', - 'Replying will create a new thread') - embed.set_footer(text=footer, - icon_url=self.bot.guild.icon_url) + footer = self.bot.config.get( + "thread_close_footer", "Replying will create a new thread" + ) + embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) if not silent and self.recipient is not None: tasks.append(self.recipient.send(embed=embed)) @@ -327,14 +334,15 @@ async def _find_thread_message(channel, message_id): continue embed = msg.embeds[0] if embed and embed.author and embed.author.url: - if str(message_id) == str(embed.author.url).split('/')[-1]: + if str(message_id) == str(embed.author.url).split("/")[-1]: return msg - async def edit_message(self, message_id: typing.Union[int, str], - message: str) -> None: + async def edit_message( + self, message_id: typing.Union[int, str], message: str + ) -> None: msg_recipient, msg_channel = await asyncio.gather( self._find_thread_message(self.recipient, message_id), - self._find_thread_message(self.channel, message_id) + self._find_thread_message(self.channel, message_id), ) embed_recipient = msg_recipient.embeds[0] @@ -343,74 +351,74 @@ async def edit_message(self, message_id: typing.Union[int, str], embed_channel.description = message await asyncio.gather( msg_recipient.edit(embed=embed_recipient), - msg_channel.edit(embed=embed_channel) + msg_channel.edit(embed=embed_channel), ) 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) - ) - await asyncio.gather( - msg_recipient.delete(), - msg_channel.delete() + self._find_thread_message(self.channel, message_id), ) + 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(param(name='msg')) + raise MissingRequiredArgument(param(name="msg")) await asyncio.gather( - self.bot.api.append_log(message, - self.channel.id, - type_='system'), - self.send(message, self.channel, note=True) + self.bot.api.append_log(message, self.channel.id, type_="system"), + self.send(message, self.channel, note=True), ) - async def reply(self, message: discord.Message, - anonymous: bool = False) -> None: + async def reply(self, message: discord.Message, anonymous: bool = False) -> None: if not message.content and not message.attachments: - raise MissingRequiredArgument(param(name='msg')) + raise MissingRequiredArgument(param(name="msg")) if all(not g.get_member(self.id) for g in self.bot.guilds): return await message.channel.send( embed=discord.Embed( color=discord.Color.red(), - description='Your message could not be delivered since ' - 'the recipient shares no servers with the bot.' - )) + description="Your message could not be delivered since " + "the recipient shares no servers with the bot.", + ) + ) tasks = [] try: - await self.send(message, - destination=self.recipient, - from_mod=True, - anonymous=anonymous) + await self.send( + message, destination=self.recipient, from_mod=True, anonymous=anonymous + ) except Exception: - logger.info(error('Message delivery failed:'), exc_info=True) - tasks.append(message.channel.send( - embed=discord.Embed( - color=discord.Color.red(), - description='Your message could not be delivered as ' - 'the recipient is only accepting direct ' - 'messages from friends, or the bot was ' - 'blocked by the recipient.' + logger.info(error("Message delivery failed:"), exc_info=True) + tasks.append( + message.channel.send( + embed=discord.Embed( + color=discord.Color.red(), + description="Your message could not be delivered as " + "the recipient is only accepting direct " + "messages from friends, or the bot was " + "blocked by the recipient.", + ) ) - )) + ) 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' - )) + self.bot.api.append_log( + message, + self.channel.id, + type_="anonymous" if anonymous else "thread_message", + ) + ) if self.close_task is not None: # Cancel closing if a thread message is sent. @@ -419,73 +427,76 @@ async def reply(self, message: discord.Message, self.channel.send( embed=discord.Embed( color=discord.Color.red(), - description='Scheduled close has been cancelled.' + description="Scheduled close has been cancelled.", ) ) ) await asyncio.gather(*tasks) - async def send(self, message: discord.Message, - destination: typing.Union[discord.TextChannel, discord.DMChannel, - discord.User, discord.Member] = None, - from_mod: bool = False, - note: bool = False, - anonymous: bool = False) -> None: + async def send( + self, + message: discord.Message, + destination: typing.Union[ + discord.TextChannel, discord.DMChannel, discord.User, discord.Member + ] = None, + from_mod: bool = False, + note: bool = False, + anonymous: bool = False, + ) -> None: if self.close_task is not None: # cancel closing if a thread message is sent. + self.bot.loop.create_task(self.cancel_closure()) self.bot.loop.create_task( - self.cancel_closure() - ) - self.bot.loop.create_task( - self.channel.send(embed=discord.Embed( - color=discord.Color.red(), - description='Scheduled close has been cancelled.' - )) + self.channel.send( + embed=discord.Embed( + color=discord.Color.red(), + description="Scheduled close has been cancelled.", + ) + ) ) if not self.ready: 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, self.channel.id)) destination = destination or self.channel author = message.author - embed = discord.Embed( - description=message.content, - timestamp=message.created_at - ) + 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.get('mod_tag', - str(message.author.top_role)) - name = self.bot.config.get('anon_username', tag) - avatar_url = self.bot.config.get('anon_avatar_url', - self.bot.guild.icon_url) + tag = self.bot.config.get("mod_tag", str(message.author.top_role)) + name = self.bot.config.get("anon_username", tag) + avatar_url = self.bot.config.get( + "anon_avatar_url", self.bot.guild.icon_url + ) else: # Normal message name = str(author) avatar_url = author.avatar_url - embed.set_author(name=name, - icon_url=avatar_url, - url=message.jump_url) + embed.set_author(name=name, icon_url=avatar_url, url=message.jump_url) else: # Special note messages - embed.set_author(name=f'Note ({author.name})', - icon_url=system_avatar_url, - url=message.jump_url) + embed.set_author( + name=f"Note ({author.name})", + icon_url=system_avatar_url, + url=message.jump_url, + ) delete_message = not bool(message.attachments) @@ -495,8 +506,7 @@ async def send(self, message: discord.Message, attachments = [x for x in attachments if not is_image_url(*x)] image_links = [ - (link, None) for link in re.findall(r'(https?://[^\s]+)', - message.content) + (link, None) for link in re.findall(r"(https?://[^\s]+)", message.content) ] image_links = [x for x in image_links if is_image_url(*x)] images.extend(image_links) @@ -510,12 +520,11 @@ async def send(self, message: discord.Message, for att in images: if not prioritize_uploads or ( - is_image_url(*att) and not embedded_image and att[1] + is_image_url(*att) and not embedded_image and att[1] ): embed.set_image(url=att[0]) if att[1]: - embed.add_field(name='Image', - value=f'[{att[1]}]({att[0]})') + embed.add_field(name="Image", value=f"[{att[1]}]({att[0]})") embedded_image = True elif att[1] is not None: if note: @@ -530,7 +539,7 @@ async def send(self, message: discord.Message, img_embed.title = att[1] img_embed.url = att[0] img_embed.set_footer( - text=f'Additional Image Upload ({additional_count})' + text=f"Additional Image Upload ({additional_count})" ) img_embed.timestamp = message.created_at additional_images.append(destination.send(embed=img_embed)) @@ -539,8 +548,9 @@ async def send(self, message: discord.Message, file_upload_count = 1 for att in attachments: - embed.add_field(name=f'File upload ({file_upload_count})', - value=f'[{att[1]}]({att[0]})') + embed.add_field( + name=f"File upload ({file_upload_count})", value=f"[{att[1]}]({att[0]})" + ) file_upload_count += 1 if from_mod: @@ -548,21 +558,18 @@ async def send(self, message: discord.Message, embed.color = self.bot.mod_color # pylint: disable=E0237 # Anonymous reply sent in thread channel if anonymous and isinstance(destination, discord.TextChannel): - embed.set_footer(text='Anonymous Reply') + embed.set_footer(text="Anonymous Reply") # Normal messages elif not anonymous: - tag = self.bot.config.get('mod_tag', - str(message.author.top_role)) + tag = self.bot.config.get("mod_tag", str(message.author.top_role)) embed.set_footer(text=tag) # Normal messages else: - embed.set_footer( - text=self.bot.config.get('anon_tag', 'Response') - ) + embed.set_footer(text=self.bot.config.get("anon_tag", "Response")) elif note: # noinspection PyUnresolvedReferences,PyDunderSlots embed.color = discord.Color.blurple() # pylint: disable=E0237 else: - embed.set_footer(text=f'Recipient') + embed.set_footer(text=f"Recipient") # noinspection PyUnresolvedReferences,PyDunderSlots embed.color = self.bot.recipient_color # pylint: disable=E0237 @@ -587,14 +594,14 @@ def get_notifications(self) -> str: key = str(self.id) mentions = [] - mentions.extend(config['subscriptions'].get(key, [])) + mentions.extend(config["subscriptions"].get(key, [])) - if key in config['notification_squad']: - mentions.extend(config['notification_squad'][key]) - del config['notification_squad'][key] + if key in config["notification_squad"]: + mentions.extend(config["notification_squad"][key]) + del config["notification_squad"][key] self.bot.loop.create_task(config.update()) - return ' '.join(mentions) + return " ".join(mentions) class ThreadManager: @@ -606,8 +613,10 @@ 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: + if ( + channel.category != self.bot.main_category + and not self.bot.using_multiple_server_setup + ): continue await self.find(channel=channel) @@ -620,10 +629,13 @@ def __iter__(self): def __getitem__(self, item: str) -> Thread: return self.cache[item] - async def find(self, *, - recipient: typing.Union[discord.Member, discord.User] = None, - channel: discord.TextChannel = None, - recipient_id: int = None) -> Thread: + async def find( + self, + *, + recipient: typing.Union[discord.Member, discord.User] = None, + channel: discord.TextChannel = None, + recipient_id: int = None, + ) -> Thread: """Finds a thread from cache or from discord channel topics.""" if recipient is None and channel is not None: return await self._find_from_channel(channel) @@ -638,15 +650,13 @@ async def find(self, *, if not self.bot.get_channel(thread.channel.id): # deleted channel self.bot.loop.create_task( thread.close( - closer=self.bot.user, - silent=True, - delete_channel=False - )) + 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}' + self.bot.modmail_guild.text_channels, topic=f"User ID: {recipient_id}" ) if channel: thread = Thread(self, recipient, channel) @@ -696,36 +706,36 @@ async def _find_from_channel(self, channel): return thread - def create(self, recipient: typing.Union[discord.Member, discord.User], *, - creator: typing.Union[discord.Member, discord.User] = None, - category: discord.CategoryChannel = None) -> Thread: + def create( + self, + recipient: typing.Union[discord.Member, discord.User], + *, + creator: typing.Union[discord.Member, discord.User] = None, + category: discord.CategoryChannel = None, + ) -> Thread: """Creates a Modmail thread""" # create thread immediately so messages can be processed thread = Thread(self, recipient) self.cache[recipient.id] = thread # Schedule thread setup for later - self.bot.loop.create_task(thread.setup( - creator=creator, - category=category - )) + self.bot.loop.create_task(thread.setup(creator=creator, category=category)) return thread - async def find_or_create(self, - recipient: typing.Union[discord.Member, - discord.User]) -> Thread: + async def find_or_create( + self, recipient: typing.Union[discord.Member, discord.User] + ) -> Thread: return await self.find(recipient=recipient) or self.create(recipient) def format_channel_name(self, author): """Sanitises a username for use with text channel names""" name = author.name.lower() - allowed = string.ascii_letters + string.digits + '-' - new_name = ''.join(l for l in name if l in allowed) or 'null' - new_name += f'-{author.discriminator}' + allowed = string.ascii_letters + string.digits + "-" + new_name = "".join(l for l in name if l in allowed) or "null" + new_name += f"-{author.discriminator}" - while new_name in [c.name for c in - self.bot.modmail_guild.text_channels]: - new_name += '-x' # two channels with same name + while new_name in [c.name for c in self.bot.modmail_guild.text_channels]: + new_name += "-x" # two channels with same name return new_name @@ -737,39 +747,37 @@ def format_info_embed(self, user, log_url, log_count, color): # key = log_url.split('/')[-1] - role_names = '' + role_names = "" if member: sep_server = self.bot.using_multiple_server_setup - separator = ', ' if sep_server else ' ' + separator = ", " if sep_server else " " roles = [] for role in sorted(member.roles, key=lambda r: r.position): - if role.name == '@everyone': + if role.name == "@everyone": continue fmt = role.name if sep_server else role.mention roles.append(fmt) if len(separator.join(roles)) > 1024: - roles.append('...') + roles.append("...") while len(separator.join(roles)) > 1024: roles.pop(-2) break role_names = separator.join(roles) - embed = discord.Embed(color=color, - description=user.mention, - timestamp=time) + embed = discord.Embed(color=color, description=user.mention, timestamp=time) created = str((time - user.created_at).days) # if not role_names: # embed.add_field(name='Mention', value=user.mention) # embed.add_field(name='Registered', value=created + days(created)) - embed.description += f' was created {days(created)}' + embed.description += f" was created {days(created)}" - footer = 'User ID: ' + str(user.id) + footer = "User ID: " + str(user.id) embed.set_footer(text=footer) embed.set_author(name=str(user), icon_url=user.avatar_url, url=log_url) # embed.set_thumbnail(url=avi) @@ -777,25 +785,22 @@ def format_info_embed(self, user, log_url, log_count, color): if member: joined = str((time - member.joined_at).days) # embed.add_field(name='Joined', value=joined + days(joined)) - embed.description += f', joined {days(joined)}' + embed.description += f", joined {days(joined)}" if member.nick: - embed.add_field(name='Nickname', - value=member.nick, - inline=True) + embed.add_field(name="Nickname", value=member.nick, inline=True) if role_names: - embed.add_field(name='Roles', - value=role_names, - inline=True) + embed.add_field(name="Roles", value=role_names, inline=True) else: - embed.set_footer(text=f'{footer} | Note: this member ' - 'is not part of this server.') + embed.set_footer( + text=f"{footer} | Note: this member " "is not part of this server." + ) if log_count: # embed.add_field(name='Past logs', value=f'{log_count}') - thread = 'thread' if log_count == 1 else 'threads' + thread = "thread" if log_count == 1 else "threads" embed.description += f" with **{log_count}** past {thread}." else: - embed.description += '.' + embed.description += "." return embed diff --git a/core/time.py b/core/time.py index e8b3b7456e..c001bebcd1 100644 --- a/core/time.py +++ b/core/time.py @@ -14,11 +14,12 @@ from core.utils import error -logger = logging.getLogger('Modmail') +logger = logging.getLogger("Modmail") class ShortTime: - compiled = re.compile(r""" + compiled = re.compile( + r""" (?:(?P[0-9])(?:years?|y))? # e.g. 2y (?:(?P[0-9]{1,2})(?:months?|mo))? # e.g. 9mo (?:(?P[0-9]{1,4})(?:weeks?|w))? # e.g. 10w @@ -26,22 +27,24 @@ class ShortTime: (?:(?P[0-9]{1,5})(?:hours?|h))? # e.g. 12h (?:(?P[0-9]{1,5})(?:min(?:ute)?s?|m))? # e.g. 10m (?:(?P[0-9]{1,5})(?:sec(?:ond)?s?|s))? # e.g. 15s - """, re.VERBOSE) + """, + re.VERBOSE, + ) def __init__(self, argument): match = self.compiled.fullmatch(argument) if match is None or not match.group(0): - raise BadArgument('invalid time provided') + raise BadArgument("invalid time provided") - data = {k: int(v) for k, v in match.groupdict(default='0').items()} + data = {k: int(v) for k, v in match.groupdict(default="0").items()} now = datetime.utcnow() self.dt = now + relativedelta(**data) # Monkey patch mins and secs into the units -units = pdt.pdtLocales['en_US'].units -units['minutes'].append('mins') -units['seconds'].append('secs') +units = pdt.pdtLocales["en_US"].units +units["minutes"].append("mins") +units["seconds"].append("secs") class HumanTime: @@ -51,16 +54,16 @@ def __init__(self, argument): now = datetime.utcnow() dt, status = self.calendar.parseDT(argument, sourceTime=now) if not status.hasDateOrTime: - raise BadArgument( - 'invalid time provided, try e.g. "tomorrow" or "3 days"' - ) + raise BadArgument('invalid time provided, try e.g. "tomorrow" or "3 days"') 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) + dt = dt.replace( + hour=now.hour, + minute=now.minute, + second=now.second, + microsecond=now.microsecond, + ) self.dt = dt self._past = dt < now @@ -82,7 +85,7 @@ def __init__(self, argument): super().__init__(argument) if self._past: - raise BadArgument('this time is in the past') + raise BadArgument("this time is in the past") class UserFriendlyTime(Converter): @@ -93,7 +96,7 @@ def __init__(self, converter: Converter = None): converter = converter() if converter is not None and not isinstance(converter, Converter): - raise TypeError('commands.Converter subclass necessary.') + raise TypeError("commands.Converter subclass necessary.") self.raw: str = None self.dt: datetime = None self.arg = None @@ -102,7 +105,7 @@ def __init__(self, converter: Converter = None): async def check_constraints(self, ctx, now, remaining): if self.dt < now: - raise BadArgument('This time is in the past.') + raise BadArgument("This time is in the past.") if self.converter is not None: self.arg = await self.converter.convert(ctx, remaining) @@ -112,7 +115,7 @@ async def check_constraints(self, ctx, now, remaining): async def convert(self, ctx, argument): self.raw = argument - remaining = '' + remaining = "" try: calendar = HumanTime.calendar regex = ShortTime.compiled @@ -120,24 +123,23 @@ async def convert(self, ctx, argument): match = regex.match(argument) if match is not None and match.group(0): - data = {k: int(v) for k, v in - match.groupdict(default='0').items()} - remaining = argument[match.end():].strip() + 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) # apparently nlp does not like "from now" # it likes "from x" in other cases though # so let me handle the 'now' case - if argument.endswith(' from now'): + if argument.endswith(" from now"): argument = argument[:-9].strip() # handles "for xxx hours" - if argument.startswith('for '): + if argument.startswith("for "): argument = argument[4:].strip() - if argument[0:2] == 'me': + 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 '): + if argument[0:6] in ("me to ", "me in ", "me at "): argument = argument[6:] elements = calendar.nlp(argument, sourceTime=self.now) @@ -157,16 +159,19 @@ async def convert(self, ctx, argument): if begin not in (0, 1) and end != len(argument): raise BadArgument( - 'Time is either in an inappropriate location, which must ' - 'be either at the end or beginning of your input, or I ' - 'just flat out did not understand what you meant. Sorry.') + "Time is either in an inappropriate location, which must " + "be either at the end or beginning of your input, or I " + "just flat out did not understand what you meant. Sorry." + ) if not status.hasTime: # replace it with the current time - dt = dt.replace(hour=self.now.hour, - minute=self.now.minute, - second=self.now.second, - microsecond=self.now.microsecond) + dt = dt.replace( + hour=self.now.hour, + minute=self.now.minute, + second=self.now.second, + microsecond=self.now.microsecond, + ) # if midnight is provided, just default to next day if status.accuracy == pdt.pdtContext.ACU_HALFDAY: @@ -178,26 +183,20 @@ async def convert(self, ctx, argument): if begin == 1: # check if it's quoted: if argument[0] != '"': - raise BadArgument( - 'Expected quote before time input...' - ) + raise BadArgument("Expected quote before time input...") if not (end < len(argument) and argument[end] == '"'): - raise BadArgument( - 'If the time is quoted, you must unquote it.' - ) + raise BadArgument("If the time is quoted, you must unquote it.") - remaining = argument[end + 1:].lstrip(' ,.!') + remaining = argument[end + 1 :].lstrip(" ,.!") else: - remaining = argument[end:].lstrip(' ,.!') + remaining = argument[end:].lstrip(" ,.!") elif len(argument) == end: remaining = argument[:begin].strip() return await self.check_constraints(ctx, self.now, remaining) except Exception: - logger.exception( - error('Something went wrong while parsing the time') - ) + logger.exception(error("Something went wrong while parsing the time")) raise @@ -205,15 +204,15 @@ def human_timedelta(dt, *, source=None): now = source or datetime.utcnow() if dt > now: delta = relativedelta(dt, now) - suffix = '' + suffix = "" else: delta = relativedelta(now, dt) - suffix = ' ago' + suffix = " ago" if delta.microseconds and delta.seconds: delta = delta + relativedelta(seconds=+1) - attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] + attrs = ["years", "months", "days", "hours", "minutes", "seconds"] output = [] for attr in attrs: @@ -222,14 +221,14 @@ def human_timedelta(dt, *, source=None): continue if elem > 1: - output.append(f'{elem} {attr}') + output.append(f"{elem} {attr}") else: - output.append(f'{elem} {attr[:-1]}') + output.append(f"{elem} {attr[:-1]}") if not output: - return 'now' + return "now" if len(output) == 1: return output[0] + suffix if len(output) == 2: - return f'{output[0]} and {output[1]}{suffix}' - return f'{output[0]}, {output[1]} and {output[2]}{suffix}' + return f"{output[0]} and {output[1]}{suffix}" + return f"{output[0]}, {output[1]} and {output[2]}{suffix}" diff --git a/core/utils.py b/core/utils.py index cbd70e5226..87d2e0a16b 100644 --- a/core/utils.py +++ b/core/utils.py @@ -56,7 +56,7 @@ def truncate(text: str, max: int = 50) -> str: str The truncated text. """ - return text[:max-3].strip() + '...' if len(text) > max else text + return text[: max - 3].strip() + "..." if len(text) > max else text def format_preview(messages: typing.List[typing.Dict[str, typing.Any]]): @@ -74,17 +74,17 @@ def format_preview(messages: typing.List[typing.Dict[str, typing.Any]]): A formatted string preview. """ messages = messages[:3] - out = '' + out = "" for message in messages: - if message.get('type') in ('note', 'internal'): + if message.get("type") in ("note", "internal"): continue - author = message['author'] - content = message['content'].replace('\n', ' ') - name = author['name'] + '#' + str(author['discriminator']) - prefix = '[M]' if author['mod'] else '[R]' - out += truncate(f'`{prefix} {name}:` {content}', max=75) + '\n' + author = message["author"] + content = message["content"].replace("\n", " ") + name = author["name"] + "#" + str(author["discriminator"]) + prefix = "[M]" if author["mod"] else "[R]" + out += truncate(f"`{prefix} {name}:` {content}", max=75) + "\n" - return out or 'No Messages' + return out or "No Messages" def is_image_url(url: str, _=None) -> bool: @@ -118,18 +118,18 @@ def parse_image_url(url: str) -> str: str The converted URL, or '' if the URL isn't in the proper format. """ - types = ['.png', '.jpg', '.gif', '.jpeg', '.webp'] + types = [".png", ".jpg", ".gif", ".jpeg", ".webp"] url = parse.urlsplit(url) if any(url.path.lower().endswith(i) for i in types): - return parse.urlunsplit((*url[:3], 'size=128', url[-1])) - return '' + return parse.urlunsplit((*url[:3], "size=128", url[-1])) + return "" def human_join(strings): if len(strings) <= 2: - return ' or '.join(strings) - return ', '.join(strings[:len(strings) - 1]) + ' or ' + strings[-1] + return " or ".join(strings) + return ", ".join(strings[: len(strings) - 1]) + " or " + strings[-1] def days(day: typing.Union[str, int]) -> str: @@ -148,8 +148,8 @@ def days(day: typing.Union[str, int]) -> str: """ day = int(day) if day == 0: - return '**today**' - return f'{day} day ago' if day == 1 else f'{day} days ago' + return "**today**" + return f"{day} day ago" if day == 1 else f"{day} days ago" def cleanup_code(content: str) -> str: @@ -167,11 +167,11 @@ def cleanup_code(content: str) -> str: The cleaned content. """ # remove ```py\n``` - if content.startswith('```') and content.endswith('```'): - return '\n'.join(content.split('\n')[1:-1]) + if content.startswith("```") and content.endswith("```"): + return "\n".join(content.split("\n")[1:-1]) # remove `foo` - return content.strip('` \n') + return content.strip("` \n") def match_user_id(text: str) -> int: @@ -188,7 +188,7 @@ def match_user_id(text: str) -> int: int The user ID if found. Otherwise, -1. """ - match = re.match(r'^User ID: (\d+)$', text) + match = re.match(r"^User ID: (\d+)$", text) if match is not None: return int(match.group(1)) return -1 @@ -196,11 +196,11 @@ def match_user_id(text: str) -> int: def get_perm_level(cmd) -> PermissionLevel: for check in cmd.checks: - perm = getattr(check, 'permission_level', None) + perm = getattr(check, "permission_level", None) if perm is not None: return perm for check in cmd.checks: - if 'is_owner' in str(check): + if "is_owner" in str(check): return PermissionLevel.OWNER return PermissionLevel.INVALID From 79616fe2d401acf8f7d6ec816230e79b7f704c43 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 25 May 2019 16:37:09 +1000 Subject: [PATCH 0164/1221] Add a delete button for paginators --- core/paginator.py | 96 ++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 52 deletions(-) diff --git a/core/paginator.py b/core/paginator.py index 0b28e41dd8..edb27e4eb7 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -43,27 +43,26 @@ class PaginatorSession: def __init__(self, ctx: commands.Context, *embeds, **options): self.ctx = ctx - self.timeout: int = options.get('timeout', 210) + self.timeout: int = options.get("timeout", 210) self.embeds: typing.List[Embed] = list(embeds) self.running = False self.base: Message = None self.current = 0 - self.destination = options.get('destination', ctx) + self.destination = options.get("destination", ctx) self.reaction_map = { - '⏮': self.first_page, - '◀': self.previous_page, - '▶': self.next_page, - '⏭': self.last_page, - # '⏹': self.close + "⏮": self.first_page, + "◀": self.previous_page, + "▶": self.next_page, + "⏭": self.last_page, + "🛑": self.close, } - if options.get('edit_footer', True) and len(self.embeds) > 1: + if options.get("edit_footer", True) and len(self.embeds) > 1: for i, embed in enumerate(self.embeds): - footer_text = f'Page {i + 1} of {len(self.embeds)}' + footer_text = f"Page {i + 1} of {len(self.embeds)}" if embed.footer.text: - footer_text = footer_text + ' • ' + embed.footer.text - embed.set_footer(text=footer_text, - icon_url=embed.footer.icon_url) + 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: """ @@ -77,7 +76,7 @@ def add_page(self, embed: Embed) -> None: if isinstance(embed, Embed): self.embeds.append(embed) else: - raise TypeError('Page must be an Embed object.') + raise TypeError("Page must be an Embed object.") async def create_base(self, embed: Embed) -> None: """ @@ -96,7 +95,7 @@ async def create_base(self, embed: Embed) -> None: self.running = True for reaction in self.reaction_map: - if len(self.embeds) == 2 and reaction in '⏮⏭': + if len(self.embeds) == 2 and reaction in "⏮⏭": continue await self.base.add_reaction(reaction) @@ -134,9 +133,11 @@ def react_check(self, reaction: Reaction, user: User) -> bool: ------- bool """ - return (reaction.message.id == self.base.id and - user.id == self.ctx.author.id and - reaction.emoji in self.reaction_map.keys()) + return ( + reaction.message.id == self.base.id + and user.id == self.ctx.author.id + and reaction.emoji in self.reaction_map.keys() + ) async def run(self) -> typing.Optional[Message]: """ @@ -152,9 +153,7 @@ async def run(self) -> typing.Optional[Message]: while self.running: try: reaction, user = await self.ctx.bot.wait_for( - 'reaction_add', - check=self.react_check, - timeout=self.timeout + "reaction_add", check=self.react_check, timeout=self.timeout ) except TimeoutError: return await self.close(delete=False) @@ -195,10 +194,7 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: """ self.running = False - try: - await self.ctx.message.add_reaction('✅') - except (HTTPException, InvalidArgument): - pass + self.bot.loop.create if delete: return await self.base.delete() @@ -222,10 +218,11 @@ async def last_page(self) -> None: class MessagePaginatorSession: - def __init__(self, ctx: commands.Context, *messages, - embed: Embed = None, **options): + def __init__( + self, ctx: commands.Context, *messages, embed: Embed = None, **options + ): self.ctx = ctx - self.timeout: int = options.get('timeout', 180) + self.timeout: int = options.get("timeout", 180) self.messages: typing.List[str] = list(messages) self.running = False @@ -238,11 +235,11 @@ def __init__(self, ctx: commands.Context, *messages, self.current = 0 self.reaction_map = { - '⏮': self.first_page, - '◀': self.previous_page, - '▶': self.next_page, - '⏭': self.last_page, - # '⏹': self.close + "⏮": self.first_page, + "◀": self.previous_page, + "▶": self.next_page, + "⏭": self.last_page, + "🛑": self.close, } def add_page(self, msg: str) -> None: @@ -257,7 +254,7 @@ def add_page(self, msg: str) -> None: if isinstance(msg, str): self.messages.append(msg) else: - raise TypeError('Page must be a str object.') + raise TypeError("Page must be a str object.") async def create_base(self, msg: str) -> None: """ @@ -269,11 +266,10 @@ async def create_base(self, msg: str) -> None: The message content to fill the base `Message`. """ if self.embed is not None: - footer_text = f'Page {self.current+1} of {len(self.messages)}' + footer_text = f"Page {self.current+1} of {len(self.messages)}" if self.footer_text: - footer_text = footer_text + ' • ' + self.footer_text - self.embed.set_footer(text=footer_text, - icon_url=self.embed.footer.icon_url) + footer_text = footer_text + " • " + self.footer_text + self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url) self.base = await self.ctx.send(content=msg, embed=self.embed) @@ -283,7 +279,7 @@ async def create_base(self, msg: str) -> None: self.running = True for reaction in self.reaction_map: - if len(self.messages) == 2 and reaction in '⏮⏭': + if len(self.messages) == 2 and reaction in "⏮⏭": continue await self.base.add_reaction(reaction) @@ -303,11 +299,10 @@ async def show_page(self, index: int) -> None: page = self.messages[index] if self.embed is not None: - footer_text = f'Page {self.current + 1} of {len(self.messages)}' + footer_text = f"Page {self.current + 1} of {len(self.messages)}" if self.footer_text: - footer_text = footer_text + ' • ' + self.footer_text - self.embed.set_footer(text=footer_text, - icon_url=self.embed.footer.icon_url) + footer_text = footer_text + " • " + self.footer_text + self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url) if self.running: await self.base.edit(content=page, embed=self.embed) @@ -328,9 +323,11 @@ def react_check(self, reaction: Reaction, user: User) -> bool: ------- bool """ - return (reaction.message.id == self.base.id and - user.id == self.ctx.author.id and - reaction.emoji in self.reaction_map.keys()) + return ( + reaction.message.id == self.base.id + and user.id == self.ctx.author.id + and reaction.emoji in self.reaction_map.keys() + ) async def run(self) -> typing.Optional[Message]: """ @@ -346,9 +343,7 @@ async def run(self) -> typing.Optional[Message]: while self.running: try: reaction, user = await self.ctx.bot.wait_for( - 'reaction_add', - check=self.react_check, - timeout=self.timeout + "reaction_add", check=self.react_check, timeout=self.timeout ) except TimeoutError: return await self.close(delete=False) @@ -389,10 +384,7 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: """ self.running = False - try: - await self.ctx.message.add_reaction('✅') - except (HTTPException, InvalidArgument): - pass + self.ctx.bot.loop.create_task(self.ctx.message.add_reaction("✅")) if delete: return await self.base.delete() From 0ad1351af91648a5187d9864ad8d6a890b741b97 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 25 May 2019 16:37:15 +1000 Subject: [PATCH 0165/1221] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b4d08de6..2f653c8427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.22.0 + +### Added + +Added a 🛑 reaction to the paginators to delete the embed. + +### Fixed + +`?blocked` is now paginated using reactions. This fixes [#249](https://github.com/kyb3r/modmail/issues/249) + # v2.21.0 ### Added From 9e4eba42f5792860a031ccbde9990960d998682f Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 25 May 2019 16:38:13 +1000 Subject: [PATCH 0166/1221] paginate blocked command fixes #249 --- cogs/modmail.py | 567 +++++++++++++++++++++++++----------------------- 1 file changed, 299 insertions(+), 268 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index ce0661108d..e2a6d56245 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -36,49 +36,51 @@ async def setup(self, ctx): """ if self.bot.main_category: - return await ctx.send( - f'{self.bot.modmail_guild} is already set up.' - ) + return await ctx.send(f"{self.bot.modmail_guild} is already set up.") category = await self.bot.modmail_guild.create_category( - name='Modmail', - overwrites=self.bot.overwrites(ctx) + name="Modmail", overwrites=self.bot.overwrites(ctx) ) await category.edit(position=0) log_channel = await self.bot.modmail_guild.create_text_channel( - name='bot-logs', category=category + name="bot-logs", category=category ) embed = discord.Embed( - title='Friendly Reminder', - description=f'You may use the `{self.bot.prefix}config set log_channel_id ' - '` command to set up a custom log channel' - ', then you can delete the default ' - f'{log_channel.mention} channel.', - color=self.bot.main_color + title="Friendly Reminder", + description=f"You may use the `{self.bot.prefix}config set log_channel_id " + "` command to set up a custom log channel" + ", then you can delete the default " + f"{log_channel.mention} channel.", + color=self.bot.main_color, ) - embed.add_field(name='Thanks for using the 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)!') + embed.add_field( + name="Thanks for using the 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)!", + ) - 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 - self.bot.config['log_channel_id'] = log_channel.id + self.bot.config["main_category_id"] = category.id + self.bot.config["log_channel_id"] = log_channel.id await self.bot.config.update() - await ctx.send('Successfully set up server.\n' - 'Consider setting permission groups to give access ' - 'to roles or users the ability to use Modmail.\n' - f'Type `{self.bot.prefix}permissions` for more info.') + await ctx.send( + "Successfully set up server.\n" + "Consider setting permission groups to give access " + "to roles or users the ability to use Modmail.\n" + f"Type `{self.bot.prefix}permissions` for more info." + ) - if not self.bot.config.get('permissions'): + if not self.bot.config.get("permissions"): await self.bot.update_perms(PermissionLevel.REGULAR, -1) await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id) @@ -106,33 +108,36 @@ async def snippets(self, ctx): embeds = [] if self.bot.snippets: - embed = discord.Embed(color=self.bot.main_color, - description='Here is a list of snippets ' - 'that are currently configured.') + embed = discord.Embed( + color=self.bot.main_color, + description="Here is a list of snippets " + "that are currently configured.", + ) else: embed = discord.Embed( color=discord.Color.red(), - description='You dont have any snippets at the moment.' + description="You dont have any snippets at the moment.", ) embed.set_footer( - text=f'Do {self.bot.prefix}help snippets for more commands.' + text=f"Do {self.bot.prefix}help snippets for more commands." ) - embed.set_author(name='Snippets', icon_url=ctx.guild.icon_url) + embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) embeds.append(embed) for name, value in self.bot.snippets.items(): if len(embed.fields) == 5: - embed = discord.Embed(color=self.bot.main_color, - description=embed.description) - embed.set_author(name='Snippets', icon_url=ctx.guild.icon_url) + embed = discord.Embed( + color=self.bot.main_color, description=embed.description + ) + embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) embeds.append(embed) embed.add_field(name=name, value=value, inline=False) session = PaginatorSession(ctx, *embeds) await session.run() - @snippets.command(name='add') + @snippets.command(name="add") @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippets_add(self, ctx, name: str.lower, *, value): """ @@ -147,32 +152,32 @@ async def snippets_add(self, ctx, name: str.lower, *, value): await self.bot.config.update() embed = discord.Embed( - title='Added snippet', + title="Added snippet", color=self.bot.main_color, - description=f'`{name}` points to: {value}' + description=f"`{name}` points to: {value}", ) await ctx.send(embed=embed) - @snippets.command(name='remove', aliases=['del', 'delete', 'rm']) + @snippets.command(name="remove", aliases=["del", "delete", "rm"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippets_remove(self, ctx, *, name: str.lower): """Remove a snippet.""" if self.bot.config.snippets.get(name): embed = discord.Embed( - title='Removed snippet', + title="Removed snippet", color=self.bot.main_color, - description=f'`{name}` no longer exists.' + description=f"`{name}` no longer exists.", ) - del self.bot.config['snippets'][name] + del self.bot.config["snippets"][name] await self.bot.config.update() else: embed = discord.Embed( - title='Error', + title="Error", color=discord.Color.red(), - description=f'Snippet `{name}` does not exist.' + description=f"Snippet `{name}` does not exist.", ) await ctx.send(embed=embed) @@ -188,30 +193,31 @@ async def move(self, ctx, *, category: discord.CategoryChannel): """ thread = ctx.thread await thread.channel.edit(category=category, sync_permissions=True) - await ctx.message.add_reaction('✅') + await ctx.message.add_reaction("✅") @staticmethod async def send_scheduled_close_message(ctx, after, silent=False): human_delta = human_timedelta(after.dt) - silent = '*silently* ' if silent else '' + silent = "*silently* " if silent else "" embed = discord.Embed( - title='Scheduled close', - description=f'This thread will close {silent}in {human_delta}.', - color=discord.Color.red() + title="Scheduled close", + description=f"This thread will close {silent}in {human_delta}.", + color=discord.Color.red(), ) if after.arg and not silent: - embed.add_field(name='Message', value=after.arg) + 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) - @commands.command(usage='[after] [close message]') + @commands.command(usage="[after] [close message]") @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def close(self, ctx, *, after: UserFriendlyTime = None): @@ -240,20 +246,21 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): close_after = (after.dt - now).total_seconds() if after else 0 message = after.arg if after else None - silent = str(message).lower() in {'silent', 'silently'} - cancel = str(message).lower() == 'cancel' + silent = str(message).lower() in {"silent", "silently"} + cancel = str(message).lower() == "cancel" if cancel: if thread.close_task is not None: await thread.cancel_closure() - embed = discord.Embed(color=discord.Color.red(), - description='Scheduled close ' - 'has been cancelled.') + embed = discord.Embed( + color=discord.Color.red(), + description="Scheduled close has been cancelled.", + ) else: embed = discord.Embed( color=discord.Color.red(), - description='This thread has not already been scheduled to close.' + description="This thread has not already been scheduled to close.", ) return await ctx.send(embed=embed) @@ -262,16 +269,15 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): await self.send_scheduled_close_message(ctx, after, silent) await thread.close( - closer=ctx.author, - after=close_after, - message=message, - silent=silent, + closer=ctx.author, after=close_after, message=message, silent=silent ) - @commands.command(aliases=['alert']) + @commands.command(aliases=["alert"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def notify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): + async def notify( + self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None + ): """ Notify a user or role when the next thread message received. @@ -285,34 +291,39 @@ async def notify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower if user_or_role is None: mention = ctx.author.mention - elif hasattr(user_or_role, 'mention'): + elif hasattr(user_or_role, "mention"): mention = user_or_role.mention - elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: - mention = '@' + user_or_role.lstrip('@') + elif user_or_role in {"here", "everyone", "@here", "@everyone"}: + mention = "@" + user_or_role.lstrip("@") else: - raise commands.BadArgument(f'{user_or_role} is not a valid role.') + raise commands.BadArgument(f"{user_or_role} is not a valid role.") - if str(thread.id) not in self.bot.config['notification_squad']: - self.bot.config['notification_squad'][str(thread.id)] = [] + if str(thread.id) not in self.bot.config["notification_squad"]: + self.bot.config["notification_squad"][str(thread.id)] = [] - mentions = self.bot.config['notification_squad'][str(thread.id)] + mentions = self.bot.config["notification_squad"][str(thread.id)] if mention in mentions: - embed = discord.Embed(color=discord.Color.red(), - description=f'{mention} is already ' - 'going to be mentioned.') + embed = discord.Embed( + color=discord.Color.red(), + description=f"{mention} is already " "going to be mentioned.", + ) else: mentions.append(mention) await self.bot.config.update() - embed = discord.Embed(color=self.bot.main_color, - description=f'{mention} will be mentioned ' - 'on the next message received.') + embed = discord.Embed( + color=self.bot.main_color, + description=f"{mention} will be mentioned " + "on the next message received.", + ) return await ctx.send(embed=embed) - @commands.command(aliases=['unalert']) + @commands.command(aliases=["unalert"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def unnotify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): + async def unnotify( + self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None + ): """ Un-notify a user, role, or yourself from a thread. @@ -324,34 +335,38 @@ async def unnotify(self, ctx, *, user_or_role: Union[discord.Role, User, str.low if user_or_role is None: mention = ctx.author.mention - elif hasattr(user_or_role, 'mention'): + elif hasattr(user_or_role, "mention"): mention = user_or_role.mention - elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: - mention = '@' + user_or_role.lstrip('@') + elif user_or_role in {"here", "everyone", "@here", "@everyone"}: + mention = "@" + user_or_role.lstrip("@") else: - mention = f'`{user_or_role}`' + mention = f"`{user_or_role}`" - if str(thread.id) not in self.bot.config['notification_squad']: - self.bot.config['notification_squad'][str(thread.id)] = [] + if str(thread.id) not in self.bot.config["notification_squad"]: + self.bot.config["notification_squad"][str(thread.id)] = [] - mentions = self.bot.config['notification_squad'][str(thread.id)] + mentions = self.bot.config["notification_squad"][str(thread.id)] if mention not in mentions: - embed = discord.Embed(color=discord.Color.red(), - description=f'{mention} does not have a ' - 'pending notification.') + embed = discord.Embed( + color=discord.Color.red(), + description=f"{mention} does not have a " "pending notification.", + ) else: mentions.remove(mention) await self.bot.config.update() - embed = discord.Embed(color=self.bot.main_color, - description=f'{mention} will no longer ' - 'be notified.') + embed = discord.Embed( + color=self.bot.main_color, + description=f"{mention} will no longer " "be notified.", + ) return await ctx.send(embed=embed) - @commands.command(aliases=['sub']) + @commands.command(aliases=["sub"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def subscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): + async def subscribe( + self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None + ): """ Notify a user, role, or yourself for every thread message received. @@ -365,36 +380,39 @@ async def subscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lo if user_or_role is None: mention = ctx.author.mention - elif hasattr(user_or_role, 'mention'): + elif hasattr(user_or_role, "mention"): mention = user_or_role.mention - elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: - mention = '@' + user_or_role.lstrip('@') + elif user_or_role in {"here", "everyone", "@here", "@everyone"}: + mention = "@" + user_or_role.lstrip("@") else: - raise commands.BadArgument(f'{user_or_role} is not a valid role.') + raise commands.BadArgument(f"{user_or_role} is not a valid role.") - if str(thread.id) not in self.bot.config['subscriptions']: - self.bot.config['subscriptions'][str(thread.id)] = [] + if str(thread.id) not in self.bot.config["subscriptions"]: + self.bot.config["subscriptions"][str(thread.id)] = [] - mentions = self.bot.config['subscriptions'][str(thread.id)] + mentions = self.bot.config["subscriptions"][str(thread.id)] if mention in mentions: - embed = discord.Embed(color=discord.Color.red(), - description=f'{mention} is already ' - 'subscribed to this thread.') + embed = discord.Embed( + color=discord.Color.red(), + 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) - @commands.command(aliases=['unsub']) + @commands.command(aliases=["unsub"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def unsubscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): + async def unsubscribe( + self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None + ): """ Unsubscribe a user, role, or yourself from a thread. @@ -406,28 +424,30 @@ async def unsubscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str. if user_or_role is None: mention = ctx.author.mention - elif hasattr(user_or_role, 'mention'): + elif hasattr(user_or_role, "mention"): mention = user_or_role.mention - elif user_or_role in {'here', 'everyone', '@here', '@everyone'}: - mention = '@' + user_or_role.lstrip('@') + elif user_or_role in {"here", "everyone", "@here", "@everyone"}: + mention = "@" + user_or_role.lstrip("@") else: - mention = f'`{user_or_role}`' + mention = f"`{user_or_role}`" - if str(thread.id) not in self.bot.config['subscriptions']: - self.bot.config['subscriptions'][str(thread.id)] = [] + if str(thread.id) not in self.bot.config["subscriptions"]: + self.bot.config["subscriptions"][str(thread.id)] = [] - mentions = self.bot.config['subscriptions'][str(thread.id)] + mentions = self.bot.config["subscriptions"][str(thread.id)] if mention not in mentions: - embed = discord.Embed(color=discord.Color.red(), - description=f'{mention} is not already ' - 'subscribed to this thread.') + embed = discord.Embed( + color=discord.Color.red(), + 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.') + embed = discord.Embed( + color=self.bot.main_color, + description=f"{mention} is now unsubscribed " "to this thread.", + ) return await ctx.send(embed=embed) @commands.command() @@ -436,7 +456,7 @@ async def unsubscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str. async def nsfw(self, ctx): """Flags a Modmail thread as NSFW (not safe for work).""" await ctx.channel.edit(nsfw=True) - await ctx.message.add_reaction('✅') + await ctx.message.add_reaction("✅") @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -445,54 +465,47 @@ 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 - ) + embed=discord.Embed(color=self.bot.main_color, description=log_link) ) def format_log_embeds(self, logs, avatar_url): embeds = [] logs = tuple(logs) - title = f'Total Results Found ({len(logs)})' + title = f"Total Results Found ({len(logs)})" for entry in logs: - key = entry['key'] + key = entry["key"] - created_at = parser.parse(entry['created_at']) + created_at = parser.parse(entry["created_at"]) - prefix = os.getenv('LOG_URL_PREFIX', '/logs') - if prefix == 'NONE': - prefix = '' + prefix = os.getenv("LOG_URL_PREFIX", "/logs") + if prefix == "NONE": + prefix = "" - log_url = self.bot.config.log_url.strip('/') + f'{prefix}/{key}' + log_url = self.bot.config.log_url.strip("/") + f"{prefix}/{key}" - username = entry['recipient']['name'] + '#' - username += entry['recipient']['discriminator'] + username = entry["recipient"]["name"] + "#" + 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 = 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.url = log_url - embed.add_field(name='Created', - value=duration(created_at, now=datetime.utcnow())) - embed.add_field(name='Closed By', - value=f"<@{entry['closer']['id']}>") - - 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='Link', value=log_url) - embed.set_footer( - text='Recipient ID: ' + str(entry['recipient']['id']) + embed.add_field( + name="Created", value=duration(created_at, now=datetime.utcnow()) ) + embed.add_field(name="Closed By", value=f"<@{entry['closer']['id']}>") + + 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="Link", value=log_url) + embed.set_footer(text="Recipient ID: " + str(entry["recipient"]["id"])) embeds.append(embed) return embeds @@ -512,28 +525,29 @@ async def logs(self, ctx, *, user: User = None): if not user: thread = ctx.thread if not thread: - raise commands.MissingRequiredArgument(param(name='member')) + raise commands.MissingRequiredArgument(param(name="member")) user = thread.recipient - default_avatar = 'https://cdn.discordapp.com/embed/avatars/0.png' - icon_url = getattr(user, 'avatar_url', default_avatar) + default_avatar = "https://cdn.discordapp.com/embed/avatars/0.png" + icon_url = getattr(user, "avatar_url", default_avatar) logs = await self.bot.api.get_user_logs(user.id) - if not any(not log['open'] for log in logs): - embed = discord.Embed(color=discord.Color.red(), - description='This user does not ' - 'have any previous logs.') + if not any(not log["open"] for log in logs): + embed = discord.Embed( + color=discord.Color.red(), + description="This user does not " "have any previous logs.", + ) return await ctx.send(embed=embed) - logs = reversed([e for e in logs if not e['open']]) + logs = reversed([e for e in logs if not e["open"]]) embeds = self.format_log_embeds(logs, avatar_url=icon_url) session = PaginatorSession(ctx, *embeds) await session.run() - @logs.command(name='closed-by', aliases=['closeby']) + @logs.command(name="closed-by", aliases=["closeby"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs_closed_by(self, ctx, *, user: User = None): """ @@ -545,31 +559,28 @@ 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) + "guild_id": str(self.bot.guild_id), + "open": False, + "closer.id": str(user.id), } - projection = { - 'messages': {'$slice': 5} - } + projection = {"messages": {"$slice": 5}} entries = await self.bot.db.logs.find(query, projection).to_list(None) - embeds = self.format_log_embeds(entries, - avatar_url=self.bot.guild.icon_url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) if not embeds: embed = discord.Embed( color=discord.Color.red(), - description='No log entries have been found for that query' + description="No log entries have been found for that query", ) return await ctx.send(embed=embed) session = PaginatorSession(ctx, *embeds) await session.run() - @logs.command(name='search', aliases=['find']) + @logs.command(name="search", aliases=["find"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs_search(self, ctx, limit: Optional[int] = None, *, query): """ @@ -581,26 +592,21 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): await ctx.trigger_typing() query = { - 'guild_id': str(self.bot.guild_id), - 'open': False, - '$text': { - '$search': f'"{query}"' - } + "guild_id": str(self.bot.guild_id), + "open": False, + "$text": {"$search": f'"{query}"'}, } - projection = { - 'messages': {'$slice': 5} - } + projection = {"messages": {"$slice": 5}} entries = await self.bot.db.logs.find(query, projection).to_list(limit) - embeds = self.format_log_embeds(entries, - avatar_url=self.bot.guild.icon_url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) if not embeds: embed = discord.Embed( color=discord.Color.red(), - description='No log entries have been found for that query' + description="No log entries have been found for that query", ) return await ctx.send(embed=embed) @@ -610,7 +616,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def reply(self, ctx, *, msg: str = ''): + async def reply(self, ctx, *, msg: str = ""): """ Reply to a Modmail thread. @@ -624,7 +630,7 @@ async def reply(self, ctx, *, msg: str = ''): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def anonreply(self, ctx, *, msg: str = ''): + async def anonreply(self, ctx, *, msg: str = ""): """ Reply to a thread anonymously. @@ -641,7 +647,7 @@ async def anonreply(self, ctx, *, msg: str = ''): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def note(self, ctx, *, msg: str = ''): + async def note(self, ctx, *, msg: str = ""): """ Take a note about the current thread. @@ -664,11 +670,11 @@ async def find_linked_message(self, ctx, message_id): if embed.color.value != 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] + 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] + linked_message_id = str(url).split("/")[-1] break return linked_message_id @@ -676,8 +682,7 @@ async def find_linked_message(self, ctx, message_id): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def edit(self, ctx, message_id: Optional[int] = None, - *, message: str): + async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): """ Edit a message that was sent using the reply or anonreply command. @@ -689,25 +694,31 @@ async def edit(self, ctx, message_id: Optional[int] = None, linked_message_id = await self.find_linked_message(ctx, message_id) if linked_message_id is None: - return await ctx.send(embed=discord.Embed( - title='Failed', - description='Cannot find a message to edit.', - color=discord.Color.red() - )) + return await ctx.send( + embed=discord.Embed( + title="Failed", + description="Cannot find a message to edit.", + color=discord.Color.red(), + ) + ) await asyncio.gather( thread.edit_message(linked_message_id, message), - self.bot.api.edit_message(linked_message_id, message) + self.bot.api.edit_message(linked_message_id, message), ) - await ctx.message.add_reaction('✅') + await ctx.message.add_reaction("✅") @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @trigger_typing - async def contact(self, ctx, - category: Optional[discord.CategoryChannel] = None, *, - user: Union[discord.Member, discord.User]): + async def contact( + self, + ctx, + category: Optional[discord.CategoryChannel] = None, + *, + user: Union[discord.Member, discord.User], + ): """ Create a thread with a specified member. @@ -721,7 +732,7 @@ async def contact(self, ctx, if user.bot: embed = discord.Embed( color=discord.Color.red(), - description='Cannot start a thread with a bot.' + description="Cannot start a thread with a bot.", ) return await ctx.send(embed=embed) @@ -729,19 +740,20 @@ async def contact(self, ctx, if exists: embed = discord.Embed( color=discord.Color.red(), - description='A thread for this user already ' - f'exists in {exists.channel.mention}.' + description="A thread for this user already " + f"exists in {exists.channel.mention}.", ) else: - thread = self.bot.threads.create(user, creator=ctx.author, - category=category) + thread = self.bot.threads.create( + user, creator=ctx.author, category=category + ) await thread.wait_until_ready() embed = discord.Embed( - title='Created thread', - description=f'Thread started in {thread.channel.mention} ' - f'for {user.mention}.', - color=self.bot.main_color + title="Created thread", + description=f"Thread started in {thread.channel.mention} " + f"for {user.mention}.", + color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -751,39 +763,52 @@ async def contact(self, ctx, @trigger_typing async def blocked(self, ctx): """Retrieve a list of blocked users.""" - embed = discord.Embed(title='Blocked Users', - color=self.bot.main_color, - description='Here is a list of blocked users.') + + embeds = [ + discord.Embed( + title="Blocked Users", color=self.bot.main_color, description="" + ) + ] users = [] - not_reachable = [] for id_, reason in self.bot.blocked_users.items(): user = self.bot.get_user(int(id_)) if user: - users.append((user, reason)) + users.append((user.mention, reason)) else: - not_reachable.append((id_, reason)) + try: + user = await self.bot.fetch_user(id_) + users.append((str(user), reason)) + except discord.NotFound: + pass if users: - val = '\n'.join(u.mention + (f' - `{r}`' if r else '') - for u, r in users) - embed.add_field(name='Currently Known', value=val) - if not_reachable: - val = '\n'.join(f'`{i}`' + (f' - `{r}`' if r else '') - for i, r in not_reachable) - embed.add_field(name='Unknown', value=val, inline=False) - - if not users and not not_reachable: - embed.description = 'Currently there are no blocked users.' + em = embeds[-1] + + for mention, reason in users: + line = mention + f" - `{reason or 'No reason provided'}`\n" + if len(em.description) + len(line) > 2048: + embeds.append( + discord.Embed( + title="Blocked Users (Continued)", + color=self.bot.main_color, + description=line, + ) + ) + else: + em.description += line + else: + embeds[-1].description = "Currently there are no blocked users." - await ctx.send(embed=embed) + await PaginatorSession(ctx, *embeds).run() @commands.command() @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. @@ -795,59 +820,63 @@ async def block(self, ctx, user: Optional[User] = None, *, `after` may be a simple "human-readable" time text. See `{prefix}help close` for examples. """ - reason = '' + reason = "" if user is None: thread = ctx.thread if thread: user = thread.recipient elif after is None: - raise commands.MissingRequiredArgument(param(name='user')) + raise commands.MissingRequiredArgument(param(name="user")) else: raise commands.BadArgument(f'User "{after.arg}" not found') if after is not None: reason = after.arg - if reason.startswith('System Message: '): - raise commands.BadArgument('The reason cannot start with `System Message:`.') - if '%' in reason: + if reason.startswith("System Message: "): + raise commands.BadArgument( + "The reason cannot start with `System Message:`." + ) + if "%" in reason: raise commands.BadArgument('The reason contains illegal character "%".') if after.dt > after.now: - reason = f'{reason} %{after.dt.isoformat()}%' + reason = f"{reason} %{after.dt.isoformat()}%" if not reason: reason = None - mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`' + mention = user.mention if hasattr(user, "mention") else f"`{user.id}`" - extend = f' for `{reason}`' if reason is not None else '' - msg = self.bot.blocked_users.get(str(user.id), '') + extend = f" for `{reason}`" if reason is not None else "" + msg = self.bot.blocked_users.get(str(user.id), "") - if str(user.id) not in self.bot.blocked_users or \ - reason is not None or \ - msg.startswith('System Message: '): + if ( + str(user.id) not in self.bot.blocked_users + or reason is not None + or msg.startswith("System Message: ") + ): if str(user.id) in self.bot.blocked_users: - old_reason = msg.strip().rstrip('.') or 'no reason' + old_reason = msg.strip().rstrip(".") or "no reason" embed = discord.Embed( - title='Success', - description=f'{mention} was previously blocked for ' + title="Success", + description=f"{mention} was previously blocked for " f'"{old_reason}". {mention} is now blocked{extend}.', - color=self.bot.main_color + color=self.bot.main_color, ) else: embed = discord.Embed( - title='Success', + title="Success", color=self.bot.main_color, - description=f'{mention} is now blocked{extend}.' + description=f"{mention} is now blocked{extend}.", ) self.bot.config.blocked[str(user.id)] = reason await self.bot.config.update() else: embed = discord.Embed( - title='Error', + title="Error", color=discord.Color.red(), - description=f'{mention} is already blocked.' + description=f"{mention} is already blocked.", ) return await ctx.send(embed=embed) @@ -869,38 +898,38 @@ async def unblock(self, ctx, *, user: User = None): if thread: user = thread.recipient else: - raise commands.MissingRequiredArgument(param(name='user')) + raise commands.MissingRequiredArgument(param(name="user")) - mention = user.mention if hasattr(user, 'mention') else f'`{user.id}`' + mention = user.mention if hasattr(user, "mention") else f"`{user.id}`" if str(user.id) in self.bot.blocked_users: msg = self.bot.blocked_users.get(str(user.id)) if msg is None: - msg = '' + msg = "" del self.bot.config.blocked[str(user.id)] await self.bot.config.update() - if msg.startswith('System Message: '): + if msg.startswith("System Message: "): # If the user is blocked internally (for example: below minimum account age) # Show an extended message stating the original internal message - reason = msg[16:].strip().rstrip('.') or 'no reason' + reason = msg[16:].strip().rstrip(".") or "no reason" embed = discord.Embed( - title='Success', - description=f'{mention} was previously blocked internally due to ' + title="Success", + description=f"{mention} was previously blocked internally due to " f'"{reason}". {mention} is no longer blocked.', - color=self.bot.main_color + color=self.bot.main_color, ) else: embed = discord.Embed( - title='Success', + title="Success", color=self.bot.main_color, - description=f'{mention} is no longer blocked.' + description=f"{mention} is no longer blocked.", ) else: embed = discord.Embed( - title='Error', - description=f'{mention} is not blocked.', - color=discord.Color.red() + title="Error", + description=f"{mention} is not blocked.", + color=discord.Color.red(), ) return await ctx.send(embed=embed) @@ -921,21 +950,23 @@ 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) if linked_message_id is None: return await ctx.send( embed=discord.Embed( - title='Failed', - description='Cannot find a message to delete.', - color=discord.Color.red() + title="Failed", + description="Cannot find a message to delete.", + color=discord.Color.red(), ) ) await thread.delete_message(linked_message_id) - await ctx.message.add_reaction('✅') + await ctx.message.add_reaction("✅") def setup(bot): From 520591020894879d235126fce8bba3d8ae59a696 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 25 May 2019 16:41:36 +1000 Subject: [PATCH 0167/1221] Increment version --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index a9c7bb3dc6..630820ad4d 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = "2.21.0" +__version__ = "2.21.1" import asyncio import logging From 72dac8fda99e72008a35b1915af75fe26de37b4f Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 25 May 2019 16:42:05 +1000 Subject: [PATCH 0168/1221] Formatting --- core/paginator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/paginator.py b/core/paginator.py index ccb33a44c9..58fdcaea20 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -195,7 +195,7 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: self.running = False self.ctx.bot.loop.create_task(self.ctx.message.add_reaction("✅")) - + if delete: return await self.base.delete() @@ -218,7 +218,7 @@ async def last_page(self) -> None: class MessagePaginatorSession: - + # TODO: Subclass MessagePaginatorSession from PaginatorSession def __init__( From 30e3e342d12629357c28483d573ff40a44559056 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 25 May 2019 16:56:33 +1000 Subject: [PATCH 0169/1221] Fix bot version --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 630820ad4d..e127d997e7 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = "2.21.1" +__version__ = "2.22.0" import asyncio import logging From b8560a3b312652cd666e949ff3f4ed3eadf8e9f0 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 25 May 2019 21:54:07 -0700 Subject: [PATCH 0170/1221] Corrected some lint errors --- bot.py | 7 +++---- cogs/plugins.py | 5 ++--- cogs/utility.py | 4 ++-- pyproject.toml | 20 ++++++++++++++++++++ 4 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 pyproject.toml diff --git a/bot.py b/bot.py index e127d997e7..f42cfcb0af 100644 --- a/bot.py +++ b/bot.py @@ -34,8 +34,6 @@ from datetime import datetime from types import SimpleNamespace -from pkg_resources import parse_version - import discord from discord.ext import commands from discord.ext.commands.view import StringView @@ -46,6 +44,7 @@ from colorama import init, Fore, Style from emoji import UNICODE_EMOJI from motor.motor_asyncio import AsyncIOMotorClient +from pkg_resources import parse_version from core.changelog import Changelog from core.clients import ApiClient, PluginDatabaseClient @@ -480,7 +479,7 @@ async def process_modmail(self, message: discord.Message) -> None: logger.warning( "The account age limit needs to be a " "ISO-8601 duration formatted duration string " - f'greater than 0 days, not "%s".', + 'greater than 0 days, not "%s".', str(account_age), ) del self.config.cache["account_age"] @@ -496,7 +495,7 @@ async def process_modmail(self, message: discord.Message) -> None: logger.warning( "The guild join age limit needs to be a " "ISO-8601 duration formatted duration string " - f'greater than 0 days, not "%s".', + 'greater than 0 days, not "%s".', str(guild_age), ) del self.config.cache["guild_age"] diff --git a/cogs/plugins.py b/cogs/plugins.py index e77e6c22d3..9c62483c2f 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -10,12 +10,11 @@ import sys from difflib import get_close_matches -from pkg_resources import parse_version - import discord from discord.ext import commands from discord.utils import async_all +from pkg_resources import parse_version from core import checks from core.models import PermissionLevel @@ -181,7 +180,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description="Downloading this plugin...", color=self.bot.main_color ) - message = await ctx.send(embed=embed) + await ctx.send(embed=embed) async with ctx.typing(): if len(plugin_name.split("/")) >= 3: diff --git a/cogs/utility.py b/cogs/utility.py index d2b56627eb..b4ba0d28d1 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -11,13 +11,13 @@ from json import JSONDecodeError from textwrap import indent -from pkg_resources import parse_version - from discord import Embed, Color, Activity, Role from discord.enums import ActivityType, Status from discord.ext import commands from aiohttp import ClientResponseError +from pkg_resources import parse_version + from core import checks from core.changelog import Changelog from core.decorators import github_access_token_required, trigger_typing diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..42d03a7617 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.black] +line-length = 88 +target-version = ['py37'] +include = '\.pyi?$' +exclude = ''' + +( + /( + \.eggs + | \.git + | \.venv + | venv + | _build + | build + | dist + | plugins + | temp + )/ +) +''' \ No newline at end of file From 2cab112a0483bdcca5afd52883fdb5134937fa94 Mon Sep 17 00:00:00 2001 From: Piyush Bhangale Date: Sun, 26 May 2019 23:34:08 +0530 Subject: [PATCH 0171/1221] Add tag plugin (#250) --- plugins/registry.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index ac3b2e7a6c..af839286f0 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -110,5 +110,13 @@ "title": "New member messages plugin", "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" + }, + "tags": { + "repository": "officialpiyush/modmail-plugins", + "description": "Tag Management For Your Server", + "bot_version": "2.20.1", + "title": "Tags Plugin", + "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", + "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" } } From f2ae0e51b397e9ce339d5424cb1d3117fc55df7f Mon Sep 17 00:00:00 2001 From: Kyber Date: Tue, 28 May 2019 19:17:07 +1000 Subject: [PATCH 0172/1221] No longer "open source" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c25c35a8f8..dad18ee6c8 100644 --- a/README.md +++ b/README.md @@ -89,4 +89,4 @@ Modmail supports the use of third party plugins to extend or add functionality t This project is licenced under MIT. Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. -This bot is open source and always will be. If you like this project and would like to show your appreciation, heres the link for our **[Patreon](https://www.patreon.com/kyber)**. +This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, here's the link for our **[Patreon](https://www.patreon.com/kyber)**. From 6ab0c757441c3dbe4ed579cc6ee1e4c7fef7becc Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 28 May 2019 22:47:07 +1000 Subject: [PATCH 0173/1221] Add mutual guilds field --- core/thread.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 37a23f2f81..a8a584ab09 100644 --- a/core/thread.py +++ b/core/thread.py @@ -793,7 +793,7 @@ def format_info_embed(self, user, log_url, log_count, color): embed.add_field(name="Roles", value=role_names, inline=True) else: embed.set_footer( - text=f"{footer} | Note: this member " "is not part of this server." + text=f"{footer} | Note: this member is not part of the main server." ) if log_count: @@ -803,4 +803,10 @@ def format_info_embed(self, user, log_url, log_count, color): else: embed.description += "." + mutual_guilds = [g for g in self.bot.guilds if user in g] + if user not in self.bot.modmail_guild or len(mutual_guilds) > 1: + embed.add_field( + name="Mutual Servers", value=", ".join(g.name for g in mutual_guilds) + ) + return embed From 2c865f32c8444f589207e74d29e46a3cfb7a2a4a Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 28 May 2019 22:50:18 +1000 Subject: [PATCH 0174/1221] Fix bug --- core/thread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index a8a584ab09..08b75e76a5 100644 --- a/core/thread.py +++ b/core/thread.py @@ -803,8 +803,8 @@ def format_info_embed(self, user, log_url, log_count, color): else: embed.description += "." - mutual_guilds = [g for g in self.bot.guilds if user in g] - if user not in self.bot.modmail_guild or len(mutual_guilds) > 1: + mutual_guilds = [g for g in self.bot.guilds if user in g.members] + if user not in self.bot.modmail_guild.members or len(mutual_guilds) > 1: embed.add_field( name="Mutual Servers", value=", ".join(g.name for g in mutual_guilds) ) From 29ae9cbd4b064c86940b1a8886cc4e6a4aca6d7d Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 28 May 2019 22:53:25 +1000 Subject: [PATCH 0175/1221] Move info embed formatting --- core/thread.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/thread.py b/core/thread.py index 08b75e76a5..59b814643d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -120,10 +120,6 @@ async def setup(self, *, creator=None, category=None): log_url = log_count = None # ensure core functionality still works - info_embed = self.manager.format_info_embed( - recipient, log_url, log_count, discord.Color.green() - ) - topic = f"User ID: {recipient.id}" if creator: mention = None @@ -131,11 +127,14 @@ async def setup(self, *, creator=None, category=None): mention = self.bot.config.get("mention", "@here") async def send_genesis_message(): + info_embed = self.manager.format_info_embed( + recipient, log_url, log_count, discord.Color.green() + ) try: msg = await channel.send(mention, embed=info_embed) self.bot.loop.create_task(msg.pin()) self.genesis_message = msg - except: + except Exception as e: pass finally: self.ready = True From 0c04257fb3c4a87a8f4eab87f8d0d962fcfa353f Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 28 May 2019 22:57:57 +1000 Subject: [PATCH 0176/1221] Check if the user is in self.guild --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 59b814643d..aee5be9093 100644 --- a/core/thread.py +++ b/core/thread.py @@ -803,7 +803,7 @@ def format_info_embed(self, user, log_url, log_count, color): embed.description += "." mutual_guilds = [g for g in self.bot.guilds if user in g.members] - if user not in self.bot.modmail_guild.members or len(mutual_guilds) > 1: + if user not in self.bot.guild.members or len(mutual_guilds) > 1: embed.add_field( name="Mutual Servers", value=", ".join(g.name for g in mutual_guilds) ) From 79ba8debe0c58f2fadc8fcd2712e987be7c195b6 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 28 May 2019 23:02:47 +1000 Subject: [PATCH 0177/1221] Shorten message --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index aee5be9093..a1dd9e2c54 100644 --- a/core/thread.py +++ b/core/thread.py @@ -792,7 +792,7 @@ def format_info_embed(self, user, log_url, log_count, color): embed.add_field(name="Roles", value=role_names, inline=True) else: embed.set_footer( - text=f"{footer} | Note: this member is not part of the main server." + text=f"{footer} • user not in main server" ) if log_count: From ba7646c490c7011a150470ae3adfa30deeeba33e Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 28 May 2019 23:05:04 +1000 Subject: [PATCH 0178/1221] better message --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index a1dd9e2c54..dcbf2fb921 100644 --- a/core/thread.py +++ b/core/thread.py @@ -792,7 +792,7 @@ def format_info_embed(self, user, log_url, log_count, color): embed.add_field(name="Roles", value=role_names, inline=True) else: embed.set_footer( - text=f"{footer} • user not in main server" + text=f"{footer} • (Not in main server)" ) if log_count: From 8fa7b39e7d9cd84ff35259df03098e1428f39730 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 28 May 2019 23:07:25 +1000 Subject: [PATCH 0179/1221] Lower case --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index dcbf2fb921..9fa24c6889 100644 --- a/core/thread.py +++ b/core/thread.py @@ -792,7 +792,7 @@ def format_info_embed(self, user, log_url, log_count, color): embed.add_field(name="Roles", value=role_names, inline=True) else: embed.set_footer( - text=f"{footer} • (Not in main server)" + text=f"{footer} • (not in main server)" ) if log_count: From 0cc493890a185d92e7ad4ae9e6340a2284468cf1 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 28 May 2019 23:18:33 +1000 Subject: [PATCH 0180/1221] Add pin notes functionality resolves #258 --- cogs/modmail.py | 3 ++- core/thread.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index e2a6d56245..d6f5db87ad 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -655,7 +655,8 @@ async def note(self, ctx, *, msg: str = ""): """ ctx.message.content = msg async with ctx.typing(): - await ctx.thread.note(ctx.message) + msg = await ctx.thread.note(ctx.message) + await msg.pin() async def find_linked_message(self, ctx, message_id): linked_message_id = None diff --git a/core/thread.py b/core/thread.py index 9fa24c6889..e57f488c92 100644 --- a/core/thread.py +++ b/core/thread.py @@ -364,11 +364,13 @@ async def note(self, message: discord.Message) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(param(name="msg")) - await asyncio.gather( + _, msg = await asyncio.gather( self.bot.api.append_log(message, self.channel.id, type_="system"), self.send(message, self.channel, note=True), ) + return msg + async def reply(self, message: discord.Message, anonymous: bool = False) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(param(name="msg")) @@ -579,7 +581,8 @@ async def send( else: mentions = None - await destination.send(mentions, embed=embed) + _msg = await destination.send(mentions, embed=embed) + if additional_images: self.ready = False await asyncio.gather(*additional_images) @@ -588,6 +591,8 @@ async def send( if delete_message: self.bot.loop.create_task(ignore(message.delete())) + return _msg + def get_notifications(self) -> str: config = self.bot.config key = str(self.id) @@ -791,9 +796,7 @@ def format_info_embed(self, user, log_url, log_count, color): if role_names: embed.add_field(name="Roles", value=role_names, inline=True) else: - embed.set_footer( - text=f"{footer} • (not in main server)" - ) + embed.set_footer(text=f"{footer} • (not in main server)") if log_count: # embed.add_field(name='Past logs', value=f'{log_count}') From 13c36e77ca60348d059a3df7a88cbb904aec7896 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 28 May 2019 23:25:32 +1000 Subject: [PATCH 0181/1221] Increment version --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index f42cfcb0af..978fe3091d 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = "2.22.0" +__version__ = "2.23.0" import asyncio import logging From f003a75b1b2c48768816ff3b69827239412a5a0b Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 28 May 2019 23:25:39 +1000 Subject: [PATCH 0182/1221] Update CHANGELOG.md --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f653c8427..416251843e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.23.0 + +### Added + +Added a "Mutual servers" field to the genesis embed if: +a) The user is not in the main guild. +b) The user shares more than 1 server with the bot. + +### Changed + +Notes taken using the `?note` command are now automatically pinned within the thread channel. + # v2.22.0 ### Added From 74de72314828d1c2aef8b0f3b8893aca166a1872 Mon Sep 17 00:00:00 2001 From: Agent-Fennec <12753219+Agent-Fennec@users.noreply.github.com> Date: Tue, 28 May 2019 09:33:20 -0400 Subject: [PATCH 0183/1221] fix alias tip (#255) --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index b4ba0d28d1..7375a1e861 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -829,7 +829,7 @@ async def alias(self, ctx): """ Create shortcuts to bot commands. - When `?alias` is used by itself, this will retrieve + When `{prefix}alias` is used by itself, this will retrieve a list of alias that are currently set. To use alias: From 7ce53308ac7620151891c17a5e6ce5d7a80e220e Mon Sep 17 00:00:00 2001 From: Kyber Date: Tue, 28 May 2019 23:48:24 +1000 Subject: [PATCH 0184/1221] Change the license (#257) * New license instead of MIT * Update LICENSE * Modify 1a * Remove the buying a pint clause * Update LICENSE * Remove all dick references * Grammar issue * Reword credit * Prevent charging for installation * Update LICENSE * Update LICENSE * Update LICENSE * Update LICENSE --- LICENSE | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/LICENSE b/LICENSE index 2a09c00236..ef39b570c4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,17 +1,20 @@ -MIT License +LICENSE Copyright (c) 2017-2019 kyb3r Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +in the Software without restriction, including the rights to use, copy, modify +and distribute copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following terms and conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +- The above copyright notice shall be included in all copies of the Software. +You may not: + - Claim credit for, or refuse to give credit to the creator(s) of the Software. + - Sell copies of the Software and of derivative works. + - Modify the original Software to contain hidden harmful content. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE From 60e0396155a666526cc3e3b386987124dbd56325 Mon Sep 17 00:00:00 2001 From: Kyber Date: Tue, 28 May 2019 23:51:25 +1000 Subject: [PATCH 0185/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dad18ee6c8..1a083ee8f5 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,6 @@ Modmail supports the use of third party plugins to extend or add functionality t ## Contributing -This project is licenced under MIT. Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, here's the link for our **[Patreon](https://www.patreon.com/kyber)**. From 5a03f7e95f62a5534f2bb8a6ecc14d10bea90153 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 29 May 2019 00:41:53 +1000 Subject: [PATCH 0186/1221] Update clients.py --- core/clients.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/clients.py b/core/clients.py index c26a4b6935..8947f61b86 100644 --- a/core/clients.py +++ b/core/clients.py @@ -288,6 +288,7 @@ async def create_log_entry( await self.logs.insert_one( { + "_id": key, "key": key, "open": True, "created_at": str(datetime.utcnow()), From aeb16d04c7bebea26a6309786c2ed2f8e5b1ba2e Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 29 May 2019 01:45:48 +1000 Subject: [PATCH 0187/1221] Update registry.json --- plugins/registry.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index af839286f0..27e62ac015 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -7,6 +7,14 @@ "icon_url": "https://i.imgur.com/951szZ3.jpg", "thumbnail_url": "https://i.imgur.com/951szZ3.jpg" }, + "dragory-migrate": { + "repository": "kyb3r/modmail-plugins", + "description": "Migrate your logs from Dragory's modmail bot to this one with a simple command. Added at the request of users.", + "bot_version": "2.20.1", + "title": "Dragory Logs Migration", + "icon_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png", + "thumbnail_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png" + }, "autorole": { "repository": "papiersnipper/modmail-plugins", "description": "Easily auto-assign a role to a user when they join your server.", From 5d2b5846edecc6aae6a68295c9f47a439d7360c2 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 29 May 2019 01:52:19 +1000 Subject: [PATCH 0188/1221] Fix grammar --- cogs/plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 9c62483c2f..64e7a32ebf 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -133,6 +133,9 @@ async def load_plugin(self, username, repo, plugin_name): try: self.bot.load_extension(ext) except commands.ExtensionError as exc: + # TODO: Add better error handling for plugin load faliure + import traceback + traceback.print_exc() raise DownloadError("Invalid plugin") from exc else: msg = f"Loaded plugins.{username}-{repo}.{plugin_name}" @@ -214,7 +217,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description="The plugin is installed.\n" - "*Please note: any plugin that you install is of your OWN RISK*", + "*Please note: any plugin that you install is at your OWN RISK*", color=self.bot.main_color, ) await ctx.send(embed=embed) From a293572a770f3536ca1cfc67753b904494d1cac4 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 29 May 2019 15:36:59 +1000 Subject: [PATCH 0189/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a083ee8f5..f2242e7a06 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ This list is ever growing thanks to active development and our exceptional contr ## Installation ### Heroku -Currently, the easiest way to set up the bot is by using Heroku, a container-based cloud platform. Installation via Heroku is done in your web browser and keeps the bot online 24/7 for free. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. +Currently, the easiest way to set up the bot is by using Heroku, a container-based cloud platform. Installation via Heroku is done in your web browser and keeps the bot online 24/7 for free (If you verify with a credit card). The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. ### Locally Installation locally for development reasons or otherwise is as follows, you will need `python 3.7`. From ed22851487db30cefa8eb7d3757286bde9ee821c Mon Sep 17 00:00:00 2001 From: Kyber Date: Thu, 30 May 2019 14:04:50 +1000 Subject: [PATCH 0190/1221] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f2242e7a06..c58f493aaa 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,12 @@ ## What is Modmail? +Modmail is akin to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox/feed for server staff to communicate with the userbase and vice versa in a seamless way that DMing a single person never could achieve. -Modmail's core functionality provides an efficient way for server members to communicate with server staff. When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. +While the core functionality is to get into contact with the server personal, it's extended use includes but is not limited to organisational means. Feature or event requests, reporting other users and a comment/feedback/suggestion box under the guise of anonymity because Modmail threads aren't public. + +## How does it work? +When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. ## Features From 0f6c9a38d56a5c4dd24bec16fc3dcfc7eb3e6281 Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Thu, 30 May 2019 12:37:31 +0200 Subject: [PATCH 0191/1221] Add support for plugin packages in Windows --- cogs/plugins.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 9c62483c2f..7765f4d80e 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -109,14 +109,21 @@ async def load_plugin(self, username, repo, plugin_name): if "requirements.txt" in os.listdir(dirname): # Install PIP requirements try: - await self.bot.loop.run_in_executor( - None, - self._asubprocess_run, - f"python3 -m pip install -U -r {dirname}/" - "requirements.txt --user -q -q", - ) - # -q -q (quiet) - # so there's no terminal output unless there's an error + if os.name == "nt": # Windows + await self.bot.loop.run_in_executor( + None, + self._asubprocess_run, + f"pip install -r {dirname}/requirements.txt", + ) + else: + await self.bot.loop.run_in_executor( + None, + self._asubprocess_run, + f"python3 -m pip install -U -r {dirname}/" + "requirements.txt --user -q -q", + ) + # -q -q (quiet) + # so there's no terminal output unless there's an error except subprocess.CalledProcessError as exc: err = exc.stderr.decode("utf8").strip() From d26838136e28de2f5b4ade633e78be53119a7793 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Thu, 30 May 2019 21:09:16 +1000 Subject: [PATCH 0192/1221] Change license in bot file --- LICENSE | 2 +- bot.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/LICENSE b/LICENSE index ef39b570c4..a50b6ef69b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -LICENSE +License Copyright (c) 2017-2019 kyb3r diff --git a/bot.py b/bot.py index 978fe3091d..0f3f2ddf84 100644 --- a/bot.py +++ b/bot.py @@ -1,18 +1,21 @@ """ -MIT License +License Copyright (c) 2017-2019 kyb3r Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +in the Software without restriction, including the rights to use, copy, modify +and distribute copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following terms and conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +- The above copyright notice shall be included in all copies of the Software. +You may not: + - Claim credit for, or refuse to give credit to the creator(s) of the Software. + - Sell copies of the Software and of derivative works. + - Modify the original Software to contain hidden harmful content. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -1008,8 +1011,6 @@ async def metadata_loop(self): if __name__ == "__main__": if os.name != "nt": import uvloop - uvloop.install() - bot = ModmailBot() bot.run() From c51994f72e50b8ab537fcaf42955348a1f76417f Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Thu, 30 May 2019 13:14:52 +0200 Subject: [PATCH 0193/1221] Added -q tags --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 7765f4d80e..520cdc551c 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -113,7 +113,7 @@ async def load_plugin(self, username, repo, plugin_name): await self.bot.loop.run_in_executor( None, self._asubprocess_run, - f"pip install -r {dirname}/requirements.txt", + f"pip install -r {dirname}/requirements.txt -q -q", ) else: await self.bot.loop.run_in_executor( From cb7aa4ec624c8c629bbffc6e9690f0c49d50986b Mon Sep 17 00:00:00 2001 From: Piyush Bhangale Date: Fri, 31 May 2019 22:44:35 +0530 Subject: [PATCH 0194/1221] Add backupdb --- plugins/registry.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 27e62ac015..ea552210cc 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -126,5 +126,13 @@ "title": "Tags Plugin", "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - } + }, + "backupdb": { + "repository": "officialpiyush/modmail-plugins", + "description": "Backup you're current modmail DB with a single command! **Requires `BACKUP_MONGO_URI` in either config.json or environment variables**", + "bot_version": "2.20.1", + "title": "Backup Database (backupdb)", + "icon_url": "https://images.ionadev.ml/b/nKAlOC4.jpg", + "thumbnail_url": "https://images.ionadev.ml/b/nKAlOC4.jpg" + } } From 46e95473643c64ef97256c09737e92f30775aa50 Mon Sep 17 00:00:00 2001 From: Piyush Bhangale Date: Sat, 1 Jun 2019 07:12:49 +0530 Subject: [PATCH 0195/1221] Fix Plugins don't being updated if we force push on plugins repo (#268) * make plugins update using force pull * revert force on git clone :sweat: * make force update * Use fetch --all --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 8b3f309df9..b0036dd846 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -304,7 +304,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): username, repo, name = self.parse_plugin(plugin_name) try: - cmd = f"cd plugins/{username}-{repo} && git pull" + cmd = f"cd plugins/{username}-{repo} && git reset --hard origin/master && git fetch --all && git pull" cmd = await self.bot.loop.run_in_executor( None, self._asubprocess_run, cmd ) From a0367d7c7222a03066359041c0a3fac55e2c81fb Mon Sep 17 00:00:00 2001 From: Kyber Date: Sat, 1 Jun 2019 16:47:39 +1000 Subject: [PATCH 0196/1221] Update registry.json --- plugins/registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index ea552210cc..5abbedba6a 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -129,7 +129,7 @@ }, "backupdb": { "repository": "officialpiyush/modmail-plugins", - "description": "Backup you're current modmail DB with a single command! **Requires `BACKUP_MONGO_URI` in either config.json or environment variables**", + "description": "Backup you're current modmail DB with a single command!\n**Requires `BACKUP_MONGO_URI` in either config.json or environment variables**", "bot_version": "2.20.1", "title": "Backup Database (backupdb)", "icon_url": "https://images.ionadev.ml/b/nKAlOC4.jpg", From 99dfcb2e3b974b8aa6bc205ecb00b578f391949f Mon Sep 17 00:00:00 2001 From: Kyber Date: Sat, 1 Jun 2019 16:49:28 +1000 Subject: [PATCH 0197/1221] Update registry.json --- plugins/registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 5abbedba6a..4258f80064 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -129,7 +129,7 @@ }, "backupdb": { "repository": "officialpiyush/modmail-plugins", - "description": "Backup you're current modmail DB with a single command!\n**Requires `BACKUP_MONGO_URI` in either config.json or environment variables**", + "description": "Backup you're current modmail DB with a single command!\n\n**Requires `BACKUP_MONGO_URI` in either config.json or environment variables**", "bot_version": "2.20.1", "title": "Backup Database (backupdb)", "icon_url": "https://images.ionadev.ml/b/nKAlOC4.jpg", From 071d9a78e7f2d38ef486071b37d813b90ce4bcc3 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 1 Jun 2019 23:40:15 +0800 Subject: [PATCH 0198/1221] Support branches in plugin add. --- CHANGELOG.md | 6 ++ bot.py | 6 +- cogs/plugins.py | 92 +++++++++++-------- plugins/fourjr-modmail-plugins-develop | 1 + plugins/fourjr-modmail-plugins-master | 1 + plugins/kyb3r-modmail-plugins-master | 1 + plugins/officialpiyush-modmail-plugins-master | 1 + plugins/papiersnipper-modmail-plugins-master | 1 + plugins/registry.json | 19 +++- win-requirements.txt | 11 +++ 10 files changed, 99 insertions(+), 40 deletions(-) create mode 160000 plugins/fourjr-modmail-plugins-develop create mode 160000 plugins/fourjr-modmail-plugins-master create mode 160000 plugins/kyb3r-modmail-plugins-master create mode 160000 plugins/officialpiyush-modmail-plugins-master create mode 160000 plugins/papiersnipper-modmail-plugins-master create mode 100644 win-requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 416251843e..38bcc5fe89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v2.23.1 + +### Added + +Branch support for `plugin add` and in registry. Typically for developers. + # v2.23.0 ### Added diff --git a/bot.py b/bot.py index 0f3f2ddf84..229a8a8b8e 100644 --- a/bot.py +++ b/bot.py @@ -25,7 +25,7 @@ SOFTWARE. """ -__version__ = "2.23.0" +__version__ = "2.23.1" import asyncio import logging @@ -386,6 +386,7 @@ async def on_ready(self): logger.info(info("Client ready.")) logger.info(LINE) logger.info(info(f"Logged in as: {self.user}")) + logger.info(info(f"Prefix: {self.prefix}")) logger.info(info(f"User ID: {self.user.id}")) logger.info(info(f"Guild ID: {self.guild.id if self.guild else 0}")) logger.info(LINE) @@ -983,6 +984,9 @@ async def autoupdate_loop(self): async def metadata_loop(self): await self.wait_until_ready() + if not self.guild: + return + owner = (await self.application_info()).owner while not self.is_closed(): diff --git a/cogs/plugins.py b/cogs/plugins.py index b0036dd846..9448eeada8 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -1,3 +1,4 @@ +import asyncio import importlib import json import logging @@ -13,13 +14,12 @@ import discord from discord.ext import commands from discord.utils import async_all - from pkg_resources import parse_version from core import checks from core.models import PermissionLevel from core.paginator import PaginatorSession -from core.utils import info, error +from core.utils import error, info logger = logging.getLogger("Modmail") @@ -55,10 +55,20 @@ def _asubprocess_run(cmd): @staticmethod def parse_plugin(name): - # returns: (username, repo, plugin_name) + # returns: (username, repo, plugin_name, branch) + # default branch = master try: result = name.split("/") result[2] = "/".join(result[2:]) + if '@' in result[2]: + # branch is specified + # for example, fourjr/modmail-plugins/welcomer@develop is a valid name + branchsplit_result = result[2].split('@') + result.append(branchsplit_result[-1]) + result[2] = '@'.join(branchsplit_result[:-1]) + else: + result.append('master') + except IndexError: return None @@ -68,18 +78,18 @@ async def download_initial_plugins(self): await self.bot._connected.wait() for i in self.bot.config.plugins: - parsed_plugin = self.parse_plugin(i) + username, repo, name, branch = self.parse_plugin(i) try: - await self.download_plugin_repo(*parsed_plugin[:-1]) + await self.download_plugin_repo(username, repo, branch) except DownloadError as exc: - msg = f"{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}" + msg = f"{username}/{repo}@{branch} - {exc}" logger.error(error(msg)) else: try: - await self.load_plugin(*parsed_plugin) + await self.load_plugin(username, repo, name, branch) except DownloadError as exc: - msg = f"{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}" + msg = f"{username}/{repo}@{branch}[{name}] - {exc}" logger.error(error(msg)) await async_all( @@ -88,10 +98,11 @@ async def download_initial_plugins(self): logger.debug(info("on_plugin_ready called.")) - async def download_plugin_repo(self, username, repo): + async def download_plugin_repo(self, username, repo, branch): try: - cmd = f"git clone https://github.com/{username}/{repo} " - cmd += f"plugins/{username}-{repo} -q" + cmd = f"git clone https://github.com/{username}/{repo} " + cmd += f"plugins/{username}-{repo}-{branch} " + cmd += f"-b {branch} -q" await self.bot.loop.run_in_executor(None, self._asubprocess_run, cmd) # -q (quiet) so there's no terminal output unless there's an error @@ -100,11 +111,13 @@ async def download_plugin_repo(self, username, repo): if not err.endswith("already exists and is not an empty directory."): # don't raise error if the plugin folder exists - raise DownloadError(error) from exc + msg = f'Download Error: {username}/{repo}@{branch}' + logger.error(msg) + raise DownloadError(err) from exc - async def load_plugin(self, username, repo, plugin_name): - ext = f"plugins.{username}-{repo}.{plugin_name}.{plugin_name}" - dirname = f"plugins/{username}-{repo}/{plugin_name}" + async def load_plugin(self, username, repo, plugin_name, branch): + ext = f"plugins.{username}-{repo}-{branch}.{plugin_name}.{plugin_name}" + dirname = f"plugins/{username}-{repo}-{branch}/{plugin_name}" if "requirements.txt" in os.listdir(dirname): # Install PIP requirements @@ -113,7 +126,7 @@ async def load_plugin(self, username, repo, plugin_name): await self.bot.loop.run_in_executor( None, self._asubprocess_run, - f"pip install -r {dirname}/requirements.txt -q -q", + f"pip install -r {dirname}/requirements.txt --user -q -q", ) else: await self.bot.loop.run_in_executor( @@ -128,8 +141,10 @@ async def load_plugin(self, username, repo, plugin_name): err = exc.stderr.decode("utf8").strip() if err: + msg = f'Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]' + logger.error(error(msg)) raise DownloadError( - f"Unable to download requirements: ```\n{error}\n```" + f"Unable to download requirements: ```\n{err}\n```" ) from exc else: if not os.path.exists(site.USER_SITE): @@ -137,15 +152,15 @@ async def load_plugin(self, username, repo, plugin_name): sys.path.insert(0, site.USER_SITE) + await asyncio.sleep(0.5) try: self.bot.load_extension(ext) except commands.ExtensionError as exc: - # TODO: Add better error handling for plugin load faliure - import traceback - traceback.print_exc() + msg = f'Plugin Load Failure: {username}/{repo}@{branch}[{plugin_name}]' + logger.error(error(msg)) raise DownloadError("Invalid plugin") from exc else: - msg = f"Loaded plugins.{username}-{repo}.{plugin_name}" + msg = f"Loaded Plugin: {username}/{repo}@{branch}[{plugin_name}]" logger.info(info(msg)) @commands.group(aliases=["plugins"], invoke_without_command=True) @@ -162,7 +177,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details["repository"] + "/" + plugin_name + plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"] required_version = details["bot_version"] if parse_version(self.bot.version) < parse_version(required_version): @@ -194,13 +209,13 @@ async def plugin_add(self, ctx, *, plugin_name: str): async with ctx.typing(): if len(plugin_name.split("/")) >= 3: - parsed_plugin = self.parse_plugin(plugin_name) + username, repo, name, branch = self.parse_plugin(plugin_name) try: - await self.download_plugin_repo(*parsed_plugin[:-1]) + await self.download_plugin_repo(username, repo, branch) except DownloadError as exc: embed = discord.Embed( - description=f"Unable to fetch this plugin from Github: {exc}.", + description=f"Unable to fetch this plugin from Github: `{exc}`.", color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -208,10 +223,10 @@ async def plugin_add(self, ctx, *, plugin_name: str): importlib.invalidate_caches() try: - await self.load_plugin(*parsed_plugin) + await self.load_plugin(username, repo, name, branch) except Exception as exc: embed = discord.Embed( - description=f"Unable to load this plugin: {exc}.", + description=f"Unable to load this plugin: `{exc}`.", color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -224,7 +239,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description="The plugin is installed.\n" - "*Please note: any plugin that you install is at your OWN RISK*", + "*Please note: Any plugin that you install is at your **own risk***", color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -242,13 +257,13 @@ async def plugin_remove(self, ctx, *, plugin_name: str): if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details["repository"] + "/" + plugin_name + plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"] if plugin_name in self.bot.config.plugins: try: - username, repo, name = self.parse_plugin(plugin_name) + username, repo, name, branch = self.parse_plugin(plugin_name) - self.bot.unload_extension(f"plugins.{username}-{repo}.{name}.{name}") + self.bot.unload_extension(f"plugins.{username}-{repo}-{branch}.{name}.{name}") except Exception: pass @@ -266,10 +281,11 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 os.chmod(path, stat.S_IWUSR) func(path) - shutil.rmtree(f"plugins/{username}-{repo}", onerror=onerror) + shutil.rmtree(f"plugins/{username}-{repo}-{branch}", onerror=onerror) except Exception as exc: logger.error(str(exc)) self.bot.config.plugins.append(plugin_name) + logger.error(error(exc)) raise exc await self.bot.config.update() @@ -292,7 +308,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details["repository"] + "/" + plugin_name + plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"] if plugin_name not in self.bot.config.plugins: embed = discord.Embed( @@ -301,10 +317,10 @@ async def plugin_update(self, ctx, *, plugin_name: str): return await ctx.send(embed=embed) async with ctx.typing(): - username, repo, name = self.parse_plugin(plugin_name) + username, repo, name, branch = self.parse_plugin(plugin_name) try: - cmd = f"cd plugins/{username}-{repo} && git reset --hard origin/master && git fetch --all && git pull" + cmd = f"cd plugins/{username}-{repo}-{branch} && git reset --hard origin/master && git fetch --all && git pull" cmd = await self.bot.loop.run_in_executor( None, self._asubprocess_run, cmd ) @@ -327,11 +343,11 @@ async def plugin_update(self, ctx, *, plugin_name: str): if output != "Already up to date.": # repo was updated locally, now perform the cog reload - ext = f"plugins.{username}-{repo}.{name}.{name}" + ext = f"plugins.{username}-{repo}-{branch}.{name}.{name}" self.bot.unload_extension(ext) try: - await self.load_plugin(username, repo, name) + await self.load_plugin(username, repo, name, branch) except DownloadError as exc: em = discord.Embed( description=f"Unable to start the plugin: `{exc}`.", @@ -434,7 +450,7 @@ async def plugin_registry_compact(self, ctx): for name, details in registry: repo = f"https://github.com/{details['repository']}" - url = f"{repo}/tree/master/{name}" + url = f"{repo}/tree/{details['branch']}/{name}" desc = details["description"].replace("\n", "") fmt = f"[`{name}`]({url}) - {desc}" length = len(fmt) - len(url) - 4 diff --git a/plugins/fourjr-modmail-plugins-develop b/plugins/fourjr-modmail-plugins-develop new file mode 160000 index 0000000000..61921e55e6 --- /dev/null +++ b/plugins/fourjr-modmail-plugins-develop @@ -0,0 +1 @@ +Subproject commit 61921e55e60ccce6a96616b3389474b2b514ca94 diff --git a/plugins/fourjr-modmail-plugins-master b/plugins/fourjr-modmail-plugins-master new file mode 160000 index 0000000000..b3ecfd7463 --- /dev/null +++ b/plugins/fourjr-modmail-plugins-master @@ -0,0 +1 @@ +Subproject commit b3ecfd7463671f32a32e1b798143ee0689835c92 diff --git a/plugins/kyb3r-modmail-plugins-master b/plugins/kyb3r-modmail-plugins-master new file mode 160000 index 0000000000..cdddc083d3 --- /dev/null +++ b/plugins/kyb3r-modmail-plugins-master @@ -0,0 +1 @@ +Subproject commit cdddc083d328b21ad696cbd0362e5206fe4c57b3 diff --git a/plugins/officialpiyush-modmail-plugins-master b/plugins/officialpiyush-modmail-plugins-master new file mode 160000 index 0000000000..a389b6cd1f --- /dev/null +++ b/plugins/officialpiyush-modmail-plugins-master @@ -0,0 +1 @@ +Subproject commit a389b6cd1f550c20ec7ce0ad4ba6e5daa657971c diff --git a/plugins/papiersnipper-modmail-plugins-master b/plugins/papiersnipper-modmail-plugins-master new file mode 160000 index 0000000000..9be8f4f910 --- /dev/null +++ b/plugins/papiersnipper-modmail-plugins-master @@ -0,0 +1 @@ +Subproject commit 9be8f4f910794991b6aea0573969d48c5793cd70 diff --git a/plugins/registry.json b/plugins/registry.json index 4258f80064..ad19066545 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1,6 +1,7 @@ { "profanity-filter": { "repository": "kyb3r/modmail-plugins", + "branch": "master", "description": "Checks for profanity in a message using machine learning and deletes it.", "bot_version": "2.20.1", "title": "Profanity Filter Plugin", @@ -9,6 +10,7 @@ }, "dragory-migrate": { "repository": "kyb3r/modmail-plugins", + "branch": "master", "description": "Migrate your logs from Dragory's modmail bot to this one with a simple command. Added at the request of users.", "bot_version": "2.20.1", "title": "Dragory Logs Migration", @@ -17,6 +19,7 @@ }, "autorole": { "repository": "papiersnipper/modmail-plugins", + "branch": "master", "description": "Easily auto-assign a role to a user when they join your server.", "bot_version": "2.20.1", "title": "Autorole Plugin", @@ -25,6 +28,7 @@ }, "embedder": { "repository": "papiersnipper/modmail-plugins", + "branch": "master", "description": "Easily make embeds for a nicer presence.", "bot_version": "2.20.1", "title": "Embedder Plugin", @@ -33,6 +37,7 @@ }, "leveling": { "repository": "papiersnipper/modmail-plugins", + "branch": "master", "description": "A leveling system for your server: see who's active and who's not.", "bot_version": "2.20.1", "title": "Leveling Plugin", @@ -41,6 +46,7 @@ }, "purger": { "repository": "papiersnipper/modmail-plugins", + "branch": "master", "description": "Delete multiple messages at a time.", "bot_version": "2.20.1", "title": "Purger Plugin", @@ -49,6 +55,7 @@ }, "supporters": { "repository": "papiersnipper/modmail-plugins", + "branch": "master", "description": "Let your users know who is part of the support team.", "bot_version": "2.20.1", "title": "Supporters Plugin", @@ -57,6 +64,7 @@ }, "announcement": { "repository": "officialpiyush/modmail-plugins", + "branch": "master", "description": "Easily make announcements in your server!", "bot_version": "2.20.1", "title": "Announcement Plugin", @@ -65,7 +73,8 @@ }, "code": { "repository": "officialpiyush/modmail-plugins", - "description": "Run Codes from discord bot. Same like R.Danny's longue Commands", + "branch": "master", + "description": "Run Codes from discord bot. Similar to R.Danny's longue Commands", "bot_version": "2.20.1", "title": "Code Plugin", "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", @@ -73,6 +82,7 @@ }, "dm-on-join": { "repository": "officialpiyush/modmail-plugins", + "branch": "master", "description": "DM New Users when they join", "bot_version": "2.20.1", "title": "DM-on-join Plugin", @@ -81,6 +91,7 @@ }, "hastebin": { "repository": "officialpiyush/modmail-plugins", + "branch": "master", "description": "Easily Upload Text To hastebin!", "bot_version": "2.20.1", "title": "Hastebin Plugin", @@ -89,6 +100,7 @@ }, "leave-server": { "repository": "officialpiyush/modmail-plugins", + "branch": "master", "description": "Don't want your bot in a server? Did someone invite it without your permission? If so, this plugin is useful for you!", "bot_version": "2.20.1", "title": "Leave-server Plugin", @@ -97,6 +109,7 @@ }, "moderation": { "repository": "officialpiyush/modmail-plugins", + "branch": "master", "description": "Rule offenders in your server? Lift the ban hammer or warn them! Moderation plugin to the rescue :tada:", "bot_version": "2.20.1", "title": "Moderation Plugin", @@ -105,6 +118,7 @@ }, "translator": { "repository": "officialpiyush/modmail-plugins", + "branch": "master", "description": "You can auto translate thread msgs using this plugin or translate text fromm any language to English!", "bot_version": "2.20.1", "title": "Translator Plugin", @@ -113,6 +127,7 @@ }, "welcomer": { "repository": "fourjr/modmail-plugins", + "branch": "master", "description": "Add messages to welcome new members! Allows for embedded messages as well. [Read more](https://github.com/fourjr/modmail-plugins/blob/master/welcomer/README.md)", "bot_version": "2.20.1", "title": "New member messages plugin", @@ -121,6 +136,7 @@ }, "tags": { "repository": "officialpiyush/modmail-plugins", + "branch": "master", "description": "Tag Management For Your Server", "bot_version": "2.20.1", "title": "Tags Plugin", @@ -129,6 +145,7 @@ }, "backupdb": { "repository": "officialpiyush/modmail-plugins", + "branch": "master", "description": "Backup you're current modmail DB with a single command!\n\n**Requires `BACKUP_MONGO_URI` in either config.json or environment variables**", "bot_version": "2.20.1", "title": "Backup Database (backupdb)", diff --git a/win-requirements.txt b/win-requirements.txt new file mode 100644 index 0000000000..41be1de4ad --- /dev/null +++ b/win-requirements.txt @@ -0,0 +1,11 @@ +discord.py==1.1.1 +colorama>=0.4.0 +python-dateutil>=2.7.0 +emoji>=0.2 +motor>=2.0.0 +natural==0.2.0 +isodate>=0.6.0 +dnspython~=1.16.0 # required by motor +parsedatetime==2.4 +aiohttp>=3.3.0,<3.6.0 # to comply with discord.py +pylint # for style From 48ba445453af0af8797d77af1804831ecd3284c0 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 1 Jun 2019 23:44:01 +0800 Subject: [PATCH 0199/1221] 2.24.0 --- CHANGELOG.md | 2 +- bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38bcc5fe89..94310538fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# v2.23.1 +# v2.24.0 ### Added diff --git a/bot.py b/bot.py index 229a8a8b8e..d0c3967ef4 100644 --- a/bot.py +++ b/bot.py @@ -25,7 +25,7 @@ SOFTWARE. """ -__version__ = "2.23.1" +__version__ = "2.24.0" import asyncio import logging From 674377b259170e014e28e2c4eb839660d69434d7 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sun, 2 Jun 2019 00:30:34 +0800 Subject: [PATCH 0200/1221] 2.24.1 - Fix branch and plugin update --- CHANGELOG.md | 6 +++++- cogs/plugins.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94310538fd..92b9e2bb68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# v2.24.0 +# v2.24.1 + +### Fixed + +Fixed a bug with branches and `plugin update`. ### Added diff --git a/cogs/plugins.py b/cogs/plugins.py index 9448eeada8..33fe27a515 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -320,7 +320,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): username, repo, name, branch = self.parse_plugin(plugin_name) try: - cmd = f"cd plugins/{username}-{repo}-{branch} && git reset --hard origin/master && git fetch --all && git pull" + cmd = f"cd plugins/{username}-{repo}-{branch} && git reset --hard origin/{branch} && git fetch --all && git pull" cmd = await self.bot.loop.run_in_executor( None, self._asubprocess_run, cmd ) From 4b6f2813c35703f48ea70d5213e6e93ba6f26c33 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sun, 2 Jun 2019 00:33:08 +0800 Subject: [PATCH 0201/1221] im bad at words --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b9e2bb68..1a4b1901e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Fixed a bug with branches and `plugin update`. +# v2.23.0 + ### Added Branch support for `plugin add` and in registry. Typically for developers. -# v2.23.0 - ### Added Added a "Mutual servers" field to the genesis embed if: From c711304ac7ef0abaa93e2a948aa84175e5d9f427 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sun, 2 Jun 2019 00:34:40 +0800 Subject: [PATCH 0202/1221] ok what im actually bad now --- CHANGELOG.md | 4 +++- bot.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a4b1901e5..ca6093a462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Fixed a bug with branches and `plugin update`. -# v2.23.0 +# v2.24.0 ### Added Branch support for `plugin add` and in registry. Typically for developers. +# v2.23.0 + ### Added Added a "Mutual servers" field to the genesis embed if: diff --git a/bot.py b/bot.py index d0c3967ef4..f61a60e934 100644 --- a/bot.py +++ b/bot.py @@ -25,7 +25,7 @@ SOFTWARE. """ -__version__ = "2.24.0" +__version__ = "2.24.1" import asyncio import logging From 165e1bba06225ddcc12589910663ec6701a9adb5 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 2 Jun 2019 14:49:08 +1000 Subject: [PATCH 0203/1221] remove on_plugin_ready --- cogs/plugins.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 64e7a32ebf..f6271ad52d 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -81,13 +81,7 @@ async def download_initial_plugins(self): except DownloadError as exc: msg = f"{parsed_plugin[0]}/{parsed_plugin[1]} - {exc}" logger.error(error(msg)) - - await async_all( - env() for env in self.bot.extra_events.get("on_plugin_ready", []) - ) - - logger.debug(info("on_plugin_ready called.")) - + async def download_plugin_repo(self, username, repo): try: cmd = f"git clone https://github.com/{username}/{repo} " From 3cad9285201a086574348a3cab95e014b1acc776 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 2 Jun 2019 14:49:44 +1000 Subject: [PATCH 0204/1221] add a bot_id field in log entries --- core/clients.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/clients.py b/core/clients.py index 8947f61b86..4551af64f8 100644 --- a/core/clients.py +++ b/core/clients.py @@ -295,6 +295,7 @@ async def create_log_entry( "closed_at": None, "channel_id": str(channel.id), "guild_id": str(self.bot.guild_id), + "bot_id": str(self.bot.user.id), "recipient": { "id": str(recipient.id), "name": recipient.name, From 2c9d57be4545194ca1e92353d001616c928d3e30 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 2 Jun 2019 15:49:31 +1000 Subject: [PATCH 0205/1221] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c58f493aaa..351c5a1e40 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ ## What is Modmail? -Modmail is akin to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox/feed for server staff to communicate with the userbase and vice versa in a seamless way that DMing a single person never could achieve. +Modmail is akin to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox/feed for server staff to communicate with the userbase and vice versa in a seamless way. -While the core functionality is to get into contact with the server personal, it's extended use includes but is not limited to organisational means. Feature or event requests, reporting other users and a comment/feedback/suggestion box under the guise of anonymity because Modmail threads aren't public. +While the core functionality is to get into contact with the server personal, it's extended use includes but is not limited to organisational use. Feature or event requests, reporting other users and a comment/feedback/suggestion box under a veil of anonymity since Modmail threads aren't public. ## How does it work? When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. From 4d542650aa452292d9cd268a5335f8803368d335 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 2 Jun 2019 15:58:26 +1000 Subject: [PATCH 0206/1221] Patron only features (#273) * Patron only features * pip3 --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 351c5a1e40..4177b6c2b7 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ When a member sends a direct message to the bot, a channel or "thread" is create * **Thread logs** * When you close a thread, a [log link](https://logs.modmail.tk/example) is generated and posted to your log channel. * Rendered in styled HTML like Discord. - * Optional login in via Discord to protect your logs. + * Login in via Discord to protect your logs ([Patron only feature](https://patreon.com/kyber)). * See past logs of a user with `?logs` * Searchable by text queries using `?logs search` * **Robust implementation** @@ -62,8 +62,9 @@ This list is ever growing thanks to active development and our exceptional contr ## Installation -### Heroku -Currently, the easiest way to set up the bot is by using Heroku, a container-based cloud platform. Installation via Heroku is done in your web browser and keeps the bot online 24/7 for free (If you verify with a credit card). The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. +### Paid service + +We offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! ### Locally Installation locally for development reasons or otherwise is as follows, you will need `python 3.7`. @@ -74,9 +75,9 @@ $ git clone https://github.com/kyb3r/modmail $ cd modmail ``` -Install dependancies +Install dependancies (For windows users, remove `uvloop` from the `requirements.txt` file.) ```console -$ pip install -r requirements.txt +$ pip3 install -r requirements.txt ``` Rename the `config.json.example` to `config.json` and fill out the fields. @@ -85,6 +86,9 @@ And finally, run the bot. $ python3 bot.py ``` +### Heroku +This bot can be hosted on heroku. Installation via Heroku is done in your web browser. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. + ## Plugins Modmail supports the use of third party plugins to extend or add functionality to the bot. This allows the introduction of niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third party plugins can be found using the `plugins registry` command. To develop your own, check out the [documentation](https://github.com/kyb3r/modmail/wiki/Plugins) for plugins. From 13df6a20257056fadda4a71beabd909695244d50 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 2 Jun 2019 16:01:21 +1000 Subject: [PATCH 0207/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4177b6c2b7..95b64bdcc7 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ This list is ever growing thanks to active development and our exceptional contr ## Installation -### Paid service +### Hosting for patrons We offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! From 9d5e1247715e99214754263555a3fedc356d6e58 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 2 Jun 2019 16:40:28 +1000 Subject: [PATCH 0208/1221] Change license to gnu gpl A modified license not reviewed by lawyers is weak. --- LICENSE | 698 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 674 insertions(+), 24 deletions(-) diff --git a/LICENSE b/LICENSE index a50b6ef69b..f288702d2f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,24 +1,674 @@ -License - -Copyright (c) 2017-2019 kyb3r - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including the rights to use, copy, modify -and distribute copies of the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following terms and conditions: - -- The above copyright notice shall be included in all copies of the Software. - -You may not: - - Claim credit for, or refuse to give credit to the creator(s) of the Software. - - Sell copies of the Software and of derivative works. - - Modify the original Software to contain hidden harmful content. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. From 9927064e69edf08cb698bcfe48e18dcb414d2c7a Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 2 Jun 2019 18:05:33 +1000 Subject: [PATCH 0209/1221] Be a bit more lowkey with the plug. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 95b64bdcc7..1e0a12a184 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,6 @@ This list is ever growing thanks to active development and our exceptional contr ## Installation -### Hosting for patrons - -We offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! - ### Locally Installation locally for development reasons or otherwise is as follows, you will need `python 3.7`. @@ -86,6 +82,10 @@ And finally, run the bot. $ python3 bot.py ``` +### Hosting for patrons + +We offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! + ### Heroku This bot can be hosted on heroku. Installation via Heroku is done in your web browser. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. From 5a220c64bd433c72bd725313590054e871b004e2 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 2 Jun 2019 19:04:21 +1000 Subject: [PATCH 0210/1221] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 1e0a12a184..9f8afc92cb 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,6 @@ Modmail is akin to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox/feed for server staff to communicate with the userbase and vice versa in a seamless way. -While the core functionality is to get into contact with the server personal, it's extended use includes but is not limited to organisational use. Feature or event requests, reporting other users and a comment/feedback/suggestion box under a veil of anonymity since Modmail threads aren't public. - ## How does it work? When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. @@ -84,7 +82,7 @@ $ python3 bot.py ### Hosting for patrons -We offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! +If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! ### Heroku This bot can be hosted on heroku. Installation via Heroku is done in your web browser. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. From 031a9a317a01d0609b32f272488da1a8ba3aed69 Mon Sep 17 00:00:00 2001 From: Taven Date: Sun, 2 Jun 2019 15:05:27 +0530 Subject: [PATCH 0211/1221] Set presence every hour automatically (#276) This fixes the case where the presence goes away after a few hours. --- cogs/utility.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 7375a1e861..0692a2bb73 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1,3 +1,4 @@ +import asyncio import inspect import logging import os @@ -197,6 +198,12 @@ def __init__(self, bot): self.bot.help_command.cog = self + # Class Variables + self.presence = None + + # Tasks + self.presence_task = self.bot.loop.create_task(self.loop_presence()) + def cog_unload(self): self.bot.help_command = self._original_help_command @@ -627,9 +634,16 @@ async def set_presence( async def on_ready(self): # Wait until config cache is populated with stuff from db await self.bot.config.wait_until_ready() - presence = await self.set_presence() - logger.info(info(presence["activity"][1])) - logger.info(info(presence["status"][1])) + logger.info(info(self.presence["activity"][1])) + logger.info(info(self.presence["status"][1])) + + async def loop_presence(self): + """Set presence to the configured value every hour.""" + await self.bot.config.wait_until_ready() + await self.bot.wait_until_ready() + while not self.bot.is_closed(): + self.presence = await self.set_presence() + await asyncio.sleep(3600) @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) From 450815523c95055e23dbed6b3e1b35e57d9778d0 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 3 Jun 2019 00:52:33 +1000 Subject: [PATCH 0212/1221] Remove old license --- bot.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/bot.py b/bot.py index f61a60e934..a261a03f7b 100644 --- a/bot.py +++ b/bot.py @@ -1,30 +1,3 @@ -""" -License - -Copyright (c) 2017-2019 kyb3r - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including the rights to use, copy, modify -and distribute copies of the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following terms and conditions: - -- The above copyright notice shall be included in all copies of the Software. - -You may not: - - Claim credit for, or refuse to give credit to the creator(s) of the Software. - - Sell copies of the Software and of derivative works. - - Modify the original Software to contain hidden harmful content. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - __version__ = "2.24.1" import asyncio From 31f76137f3abe85fe997ce4b7c905c3377e2844c Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 3 Jun 2019 00:52:39 +1000 Subject: [PATCH 0213/1221] Add pipfiles --- Pipfile | 23 ++++ Pipfile.lock | 368 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 391 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000000..4451d863ea --- /dev/null +++ b/Pipfile @@ -0,0 +1,23 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +colorama = ">=0.4.0" +python-dateutil = ">=2.7.0" +emoji = ">=0.2" +uvloop = ">=0.12.0" +motor = ">=2.0.0" +natural = "==0.2.0" +isodate = ">=0.6.0" +dnspython = "~=1.16.0" +parsedatetime = "==2.4" +aiohttp = "<3.6.0,>=3.3.0" +pylint = "*" +"discord.py" = "==1.1.1" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000000..add876d35d --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,368 @@ +{ + "_meta": { + "hash": { + "sha256": "3f613333c008005bbc2ca702d38629d9abbbdbcc7a2374f57d15068340f07822" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiohttp": { + "hashes": [ + "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55", + "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed", + "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10", + "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5", + "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1", + "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939", + "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390", + "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa", + "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc", + "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5", + "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d", + "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf", + "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6", + "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72", + "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12", + "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366", + "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4", + "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300", + "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d", + "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303", + "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6", + "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889" + ], + "index": "pypi", + "version": "==3.5.4" + }, + "astroid": { + "hashes": [ + "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", + "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" + ], + "version": "==2.2.5" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "colorama": { + "hashes": [ + "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", + "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" + ], + "index": "pypi", + "version": "==0.4.1" + }, + "discord.py": { + "hashes": [ + "sha256:d0ab22f1fee1fcc02ac50a67ff49a5d1f6d7bc7eba77e34e35bd160b3ad3d7e8" + ], + "index": "pypi", + "version": "==1.1.1" + }, + "dnspython": { + "hashes": [ + "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", + "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d" + ], + "index": "pypi", + "version": "==1.16.0" + }, + "emoji": { + "hashes": [ + "sha256:9ae01495fc3fcc04e9136ca1af8cae58726ec5dfaaa92f61f0732cbae9a12fa9", + "sha256:cecf8ed7cc535c999dc96e3fe7e7aacd176deb9ef62b3ef022c535e155084c44" + ], + "index": "pypi", + "version": "==0.5.2" + }, + "future": { + "hashes": [ + "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" + ], + "version": "==0.17.1" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "isodate": { + "hashes": [ + "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", + "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81" + ], + "index": "pypi", + "version": "==0.6.0" + }, + "isort": { + "hashes": [ + "sha256:c40744b6bc5162bbb39c1257fe298b7a393861d50978b565f3ccd9cb9de0182a", + "sha256:f57abacd059dc3bd666258d1efb0377510a89777fda3e3274e3c01f7c03ae22d" + ], + "version": "==4.3.20" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", + "sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", + "sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", + "sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", + "sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", + "sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", + "sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", + "sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", + "sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", + "sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", + "sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", + "sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", + "sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", + "sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", + "sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", + "sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", + "sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", + "sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1" + ], + "version": "==1.4.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "motor": { + "hashes": [ + "sha256:462fbb824f4289481c158227a2579d6adaf1ec7c70cf7ebe60ed6ceb321e5869", + "sha256:d035c09ab422bc50bf3efb134f7405694cae76268545bd21e14fb22e2638f84e" + ], + "index": "pypi", + "version": "==2.0.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" + }, + "natural": { + "hashes": [ + "sha256:18c83662d2d33fd7e6eee4e3b0d7366e1ce86225664e3127a2aaf0a3233f7df2" + ], + "index": "pypi", + "version": "==0.2.0" + }, + "parsedatetime": { + "hashes": [ + "sha256:3d817c58fb9570d1eec1dd46fa9448cd644eeed4fb612684b02dfda3a79cb84b", + "sha256:9ee3529454bf35c40a77115f5a596771e59e1aee8c53306f346c461b8e913094" + ], + "index": "pypi", + "version": "==2.4" + }, + "pylint": { + "hashes": [ + "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", + "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" + ], + "index": "pypi", + "version": "==2.3.1" + }, + "pymongo": { + "hashes": [ + "sha256:32421df60d06f479d71b6b539642e410ece3006e8910688e68df962c8eb40a21", + "sha256:324b22a8443e11faca44c96b20e7ec8a9e59a1e664457edeeb4f796080b31cde", + "sha256:4505ff8b7923dd7a8bed1bf25c9c4d0df5ab0b8b2821f2296533f2149a55f401", + "sha256:460b224681ea711e48e3638d15be2249024031b7dcb9622ba19c2e85bd5a26cc", + "sha256:47473b70c5f3cd5ddd2c49ab3b9ceafdafbbed5bc963f147df22a9343d7978f5", + "sha256:49375839af76834e9c5c3cc78c78386873fd0b2ad9a0860a7dc4ec9fe73af9dd", + "sha256:4a65f0f71ece86c860d30a1436b646db8ea32aec518845ef2903ca569faec32e", + "sha256:530621906c5dd6d27305b39c4e017701e5f4299aa68b93cde70eb985f94ca26f", + "sha256:54f4770b5810e8dc3cbeed675874195f02bb2bc4e95a9d665068edfb3baff4f7", + "sha256:5ed9382410e938b0ff76041c34018210504729a83bcf4f6a70c7092c28169f6f", + "sha256:61cad83637ae12c1c825130d7f9325cd6c162e3a64e8747a8144866020be3ff4", + "sha256:61e8e1c58b4fdf47ab79b7c7db8bb022c1e40b3b5fcbbaeea5fc94dc5c75638d", + "sha256:6e04e496af7d156b66cce70460011c621ecbadf5dcdce325c7acbb3cd6ea245d", + "sha256:7ef89ec435e89da902451dde6845066fe2770befaf0301fe2a1ac426b51fced3", + "sha256:854e8425e5eb775ccfffad04ecd094c99923d60a2c2d49babb5c435e836a91fa", + "sha256:9569796d48498e4db4e1d56284b626a8ed15f641ce3a8b2085f06bb03f4c2c88", + "sha256:9d50c99c6388863cbfdc5db9bad62e3a7c2e5fc151554a07c7f3c2530334a34f", + "sha256:9ea016c2c011df21f77c1f806ce45129a344ba2d414bd50f9e065b13a4a134be", + "sha256:a8421f0823174888fb12a5fa675322e756499d71e77ff712b4412d4b8f3c6503", + "sha256:aef7d88384ada699976350a285c7a333f96ebc959e98e7d2c98589f47bbf3b7f", + "sha256:b4d7ff9957ee770cf03bd7156a68a2f2e838e60712d9608eadc8741c15d01e72", + "sha256:c1db85c39e6a60588f855dbc7bd68fb0dab796096148ab5aa4abecaff19e1c6e", + "sha256:cee2fc0b94e66e7230da12fc4b3d34793c49957e16ee04f6468a94e264a1e41d", + "sha256:cf1dea28379a16b23e47db312883f07b3ba8d9d6abc1c59e51d4c8ae1820ab43", + "sha256:d1cd175df7c8b5fc976bade78bf4d9fb5aa7ab465c0f59931e380bbe188ef8fc", + "sha256:d48a94edf3cdd34524936a72ea01b352682b337f33a42db10ba29a96c37147d3", + "sha256:d9cc103a4e97f78bc77a1d72759ab3722f6cdf0374ad4fb4b0c53bd3238bdf98", + "sha256:fcb9ae8aa9158106c5d98a4349ec0d90b68f052d620b2d24622ba03b91e4d81d" + ], + "version": "==3.8.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + ], + "index": "pypi", + "version": "==2.8.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "typed-ast": { + "hashes": [ + "sha256:132eae51d6ef3ff4a8c47c393a4ef5ebf0d1aecc96880eb5d6c8ceab7017cc9b", + "sha256:18141c1484ab8784006c839be8b985cfc82a2e9725837b0ecfa0203f71c4e39d", + "sha256:2baf617f5bbbfe73fd8846463f5aeafc912b5ee247f410700245d68525ec584a", + "sha256:3d90063f2cbbe39177e9b4d888e45777012652d6110156845b828908c51ae462", + "sha256:4304b2218b842d610aa1a1d87e1dc9559597969acc62ce717ee4dfeaa44d7eee", + "sha256:4983ede548ffc3541bae49a82675996497348e55bafd1554dc4e4a5d6eda541a", + "sha256:5315f4509c1476718a4825f45a203b82d7fdf2a6f5f0c8f166435975b1c9f7d4", + "sha256:6cdfb1b49d5345f7c2b90d638822d16ba62dc82f7616e9b4caa10b72f3f16649", + "sha256:7b325f12635598c604690efd7a0197d0b94b7d7778498e76e0710cd582fd1c7a", + "sha256:8d3b0e3b8626615826f9a626548057c5275a9733512b137984a68ba1598d3d2f", + "sha256:8f8631160c79f53081bd23446525db0bc4c5616f78d04021e6e434b286493fd7", + "sha256:912de10965f3dc89da23936f1cc4ed60764f712e5fa603a09dd904f88c996760", + "sha256:b010c07b975fe853c65d7bbe9d4ac62f1c69086750a574f6292597763781ba18", + "sha256:c908c10505904c48081a5415a1e295d8403e353e0c14c42b6d67f8f97fae6616", + "sha256:c94dd3807c0c0610f7c76f078119f4ea48235a953512752b9175f9f98f5ae2bd", + "sha256:ce65dee7594a84c466e79d7fb7d3303e7295d16a83c22c7c4037071b059e2c21", + "sha256:eaa9cfcb221a8a4c2889be6f93da141ac777eb8819f077e1d09fb12d00a09a93", + "sha256:f3376bc31bad66d46d44b4e6522c5c21976bf9bca4ef5987bb2bf727f4506cbb", + "sha256:f9202fa138544e13a4ec1a6792c35834250a85958fde1251b6a22e07d1260ae7" + ], + "markers": "implementation_name == 'cpython'", + "version": "==1.3.5" + }, + "uvloop": { + "hashes": [ + "sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573", + "sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64", + "sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0", + "sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5", + "sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26", + "sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7", + "sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115", + "sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021", + "sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f", + "sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe" + ], + "index": "pypi", + "version": "==0.12.2" + }, + "websockets": { + "hashes": [ + "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", + "sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6", + "sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1", + "sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538", + "sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4", + "sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908", + "sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0", + "sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d", + "sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c", + "sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d", + "sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c", + "sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb", + "sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf", + "sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e", + "sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96", + "sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584", + "sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484", + "sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d", + "sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559", + "sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff", + "sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454" + ], + "version": "==6.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + }, + "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" + } + }, + "develop": {} +} From 2cd3046a6b2ba5629ac655e43f1cd6790e6669da Mon Sep 17 00:00:00 2001 From: Kyber Date: Tue, 4 Jun 2019 11:30:33 +1000 Subject: [PATCH 0214/1221] Delete requirements.txt In favour of pipfiles --- requirements.txt | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2dc036e2ae..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -discord.py==1.1.1 -colorama>=0.4.0 -python-dateutil>=2.7.0 -emoji>=0.2 -uvloop>=0.12.0 -motor>=2.0.0 -natural==0.2.0 -isodate>=0.6.0 -dnspython~=1.16.0 # required by motor -parsedatetime==2.4 -aiohttp>=3.3.0,<3.6.0 # to comply with discord.py -pylint # for style From 90bdc85cb529ac1583814ce1853f43f613e3665b Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 4 Jun 2019 11:34:18 +1000 Subject: [PATCH 0215/1221] dont install uvloop on windows --- Pipfile | 2 +- Pipfile.lock | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index 4451d863ea..fda1d3a532 100644 --- a/Pipfile +++ b/Pipfile @@ -9,7 +9,7 @@ verify_ssl = true colorama = ">=0.4.0" python-dateutil = ">=2.7.0" emoji = ">=0.2" -uvloop = ">=0.12.0" +uvloop = {version=">=0.12.0", sys_platform = "!= 'win32'"} motor = ">=2.0.0" natural = "==0.2.0" isodate = ">=0.6.0" diff --git a/Pipfile.lock b/Pipfile.lock index add876d35d..8b4f831a3b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3f613333c008005bbc2ca702d38629d9abbbdbcc7a2374f57d15068340f07822" + "sha256": "f550e423d6429c852f502c5b8cee64f84edd89980e90db9fcee100495330a18c" }, "pipfile-spec": 6, "requires": { @@ -313,6 +313,7 @@ "sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe" ], "index": "pypi", + "markers": "sys_platform != 'win32'", "version": "==0.12.2" }, "websockets": { From 9275fc23c01fc57f4a4d2a519e0021c74e48f108 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 4 Jun 2019 11:36:03 +1000 Subject: [PATCH 0216/1221] pipenv instructions --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9f8afc92cb..2563497e93 100644 --- a/README.md +++ b/README.md @@ -69,15 +69,15 @@ $ git clone https://github.com/kyb3r/modmail $ cd modmail ``` -Install dependancies (For windows users, remove `uvloop` from the `requirements.txt` file.) +Install dependancies ```console -$ pip3 install -r requirements.txt +$ pipenv install ``` Rename the `config.json.example` to `config.json` and fill out the fields. And finally, run the bot. ```console -$ python3 bot.py +$ pipenv run python3 bot.py ``` ### Hosting for patrons From 32ddc9b72019ef2cb7b29678322e65e68b4fe167 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 3 Jun 2019 20:06:15 -0700 Subject: [PATCH 0217/1221] Add colors plugin to registry, ignore plugins stuff --- .gitignore | 3 ++- plugins/registry.json | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 141aeeb25b..4d9ae2593a 100644 --- a/.gitignore +++ b/.gitignore @@ -126,5 +126,6 @@ dmypy.json # Modmail config.json -plugins/*-plugins/ +plugins/ +!plugins/registry.json temp/ \ No newline at end of file diff --git a/plugins/registry.json b/plugins/registry.json index ad19066545..2bcd4dfa0e 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -151,5 +151,14 @@ "title": "Backup Database (backupdb)", "icon_url": "https://images.ionadev.ml/b/nKAlOC4.jpg", "thumbnail_url": "https://images.ionadev.ml/b/nKAlOC4.jpg" - } + }, + "colors": { + "repository": "Taaku18/modmail-plugins", + "branch": "master", + "description": "Conversions between hex, RGB, and color names.", + "bot_version": "2.20.1", + "title": "Colors!!", + "icon_url": "https://cdn1.iconfinder.com/data/icons/weather-19/32/rainbow-512.png", + "thumbnail_url": "https://i.imgur.com/fSxnc9W.jpg" + } } From c198a76fc985c7dae4ab8764b2783fe7da0e2be9 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 3 Jun 2019 20:06:56 -0700 Subject: [PATCH 0218/1221] Removed plugins folders --- plugins/fourjr-modmail-plugins-develop | 1 - plugins/fourjr-modmail-plugins-master | 1 - plugins/kyb3r-modmail-plugins-master | 1 - plugins/officialpiyush-modmail-plugins-master | 1 - plugins/papiersnipper-modmail-plugins-master | 1 - 5 files changed, 5 deletions(-) delete mode 160000 plugins/fourjr-modmail-plugins-develop delete mode 160000 plugins/fourjr-modmail-plugins-master delete mode 160000 plugins/kyb3r-modmail-plugins-master delete mode 160000 plugins/officialpiyush-modmail-plugins-master delete mode 160000 plugins/papiersnipper-modmail-plugins-master diff --git a/plugins/fourjr-modmail-plugins-develop b/plugins/fourjr-modmail-plugins-develop deleted file mode 160000 index 61921e55e6..0000000000 --- a/plugins/fourjr-modmail-plugins-develop +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 61921e55e60ccce6a96616b3389474b2b514ca94 diff --git a/plugins/fourjr-modmail-plugins-master b/plugins/fourjr-modmail-plugins-master deleted file mode 160000 index b3ecfd7463..0000000000 --- a/plugins/fourjr-modmail-plugins-master +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b3ecfd7463671f32a32e1b798143ee0689835c92 diff --git a/plugins/kyb3r-modmail-plugins-master b/plugins/kyb3r-modmail-plugins-master deleted file mode 160000 index cdddc083d3..0000000000 --- a/plugins/kyb3r-modmail-plugins-master +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cdddc083d328b21ad696cbd0362e5206fe4c57b3 diff --git a/plugins/officialpiyush-modmail-plugins-master b/plugins/officialpiyush-modmail-plugins-master deleted file mode 160000 index a389b6cd1f..0000000000 --- a/plugins/officialpiyush-modmail-plugins-master +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a389b6cd1f550c20ec7ce0ad4ba6e5daa657971c diff --git a/plugins/papiersnipper-modmail-plugins-master b/plugins/papiersnipper-modmail-plugins-master deleted file mode 160000 index 9be8f4f910..0000000000 --- a/plugins/papiersnipper-modmail-plugins-master +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9be8f4f910794991b6aea0573969d48c5793cd70 From c3e156764323689f2ed3b6da3fd623e4f1284c1f Mon Sep 17 00:00:00 2001 From: Kyber Date: Tue, 4 Jun 2019 14:04:14 +1000 Subject: [PATCH 0219/1221] Delete win-requirements.txt --- win-requirements.txt | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 win-requirements.txt diff --git a/win-requirements.txt b/win-requirements.txt deleted file mode 100644 index 41be1de4ad..0000000000 --- a/win-requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -discord.py==1.1.1 -colorama>=0.4.0 -python-dateutil>=2.7.0 -emoji>=0.2 -motor>=2.0.0 -natural==0.2.0 -isodate>=0.6.0 -dnspython~=1.16.0 # required by motor -parsedatetime==2.4 -aiohttp>=3.3.0,<3.6.0 # to comply with discord.py -pylint # for style From 9003932eee8aafe83f131f59c5198e08dddc0bd9 Mon Sep 17 00:00:00 2001 From: Kyber Date: Tue, 4 Jun 2019 14:49:23 +1000 Subject: [PATCH 0220/1221] Delete LICENSE --- LICENSE | 674 -------------------------------------------------------- 1 file changed, 674 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702d2f..0000000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. From ca9d60bba1d8462c9112a2e4ba109ef38d88d625 Mon Sep 17 00:00:00 2001 From: Kyber Date: Tue, 4 Jun 2019 14:50:13 +1000 Subject: [PATCH 0221/1221] Use agpl instead --- LICENSE | 661 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 661 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. From f44201731261815139452a9f4e6dc6c46aae932f Mon Sep 17 00:00:00 2001 From: Kyber Date: Tue, 4 Jun 2019 14:57:43 +1000 Subject: [PATCH 0222/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2563497e93..2d9aa2f37b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ - MIT License + MIT License
From 72f7a21a2e704271c699234f246921d454a389e6 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 5 Jun 2019 00:26:06 -0700 Subject: [PATCH 0223/1221] ?note no longer ping notify and subscribed users --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index e57f488c92..d2d31489a8 100644 --- a/core/thread.py +++ b/core/thread.py @@ -576,7 +576,7 @@ async def send( await destination.trigger_typing() - if not from_mod: + if not from_mod and not note: mentions = self.get_notifications() else: mentions = None From 6ea9063a88e09e5156c36e400048af500103b276 Mon Sep 17 00:00:00 2001 From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com> Date: Thu, 6 Jun 2019 16:03:11 +0600 Subject: [PATCH 0224/1221] Add autoupdate instructions using Pull --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d9aa2f37b..aa30af4fe2 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,9 @@ $ pipenv run python3 bot.py If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! ### Heroku -This bot can be hosted on heroku. Installation via Heroku is done in your web browser. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. +This bot can be hosted on heroku. Installation via Heroku is done in your web browser. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. + +You can also set up autoupdates. To do this, [install the Pull app in your fork](https://github.com/apps/pull). Then go to the Deploy tab in your Heroku account, select GitHub and connect your fork. Turn on auto-deploy for the master branch. ## Plugins From 2415f3b91d6bd42e3214d2ec8649cdd150db07d4 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 9 Jun 2019 03:47:58 +1000 Subject: [PATCH 0225/1221] Spacing --- bot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot.py b/bot.py index a261a03f7b..32fb426471 100644 --- a/bot.py +++ b/bot.py @@ -988,6 +988,7 @@ async def metadata_loop(self): if __name__ == "__main__": if os.name != "nt": import uvloop + uvloop.install() bot = ModmailBot() bot.run() From e2a195a2b700cd5ce8876850e6525703775cd635 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 9 Jun 2019 03:48:46 +1000 Subject: [PATCH 0226/1221] Refactor editing thread messages - resolves #283 --- core/thread.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/core/thread.py b/core/thread.py index d2d31489a8..0952cbb50f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -336,22 +336,23 @@ async def _find_thread_message(channel, message_id): if str(message_id) == str(embed.author.url).split("/")[-1]: return msg - async def edit_message( - self, message_id: typing.Union[int, str], message: str - ) -> None: - msg_recipient, msg_channel = await asyncio.gather( + 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), ) - embed_recipient = msg_recipient.embeds[0] - embed_channel = msg_recipient.embeds[0] - embed_recipient.description = message - embed_channel.description = message - await asyncio.gather( - msg_recipient.edit(embed=embed_recipient), - msg_channel.edit(embed=embed_channel), - ) + channel_embed = channel_msg.embeds[0] + channel_embed.description = message + + tasks = [channel_msg.edit(embed=channel_embed)] + + if recipient_msg: + recipient_embed = recipient_msg.embeds[0] + recipient_embed.description = message + tasks.append(recipient_msg.edit(embed=recipient_embed)) + + await asyncio.gather(*tasks) async def delete_message(self, message_id): msg_recipient, msg_channel = await asyncio.gather( From 865e8767b2936a101dda8a9d7f5b9ba4b86cb5a8 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 9 Jun 2019 03:49:09 +1000 Subject: [PATCH 0227/1221] Enforce black code style --- cogs/plugins.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 920486b63b..086273a32f 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -60,14 +60,14 @@ def parse_plugin(name): try: result = name.split("/") result[2] = "/".join(result[2:]) - if '@' in result[2]: + if "@" in result[2]: # branch is specified # for example, fourjr/modmail-plugins/welcomer@develop is a valid name - branchsplit_result = result[2].split('@') + branchsplit_result = result[2].split("@") result.append(branchsplit_result[-1]) - result[2] = '@'.join(branchsplit_result[:-1]) + result[2] = "@".join(branchsplit_result[:-1]) else: - result.append('master') + result.append("master") except IndexError: return None @@ -94,7 +94,7 @@ async def download_initial_plugins(self): async def download_plugin_repo(self, username, repo, branch): try: - cmd = f"git clone https://github.com/{username}/{repo} " + cmd = f"git clone https://github.com/{username}/{repo} " cmd += f"plugins/{username}-{repo}-{branch} " cmd += f"-b {branch} -q" @@ -105,7 +105,7 @@ async def download_plugin_repo(self, username, repo, branch): if not err.endswith("already exists and is not an empty directory."): # don't raise error if the plugin folder exists - msg = f'Download Error: {username}/{repo}@{branch}' + msg = f"Download Error: {username}/{repo}@{branch}" logger.error(msg) raise DownloadError(err) from exc @@ -135,7 +135,7 @@ async def load_plugin(self, username, repo, plugin_name, branch): err = exc.stderr.decode("utf8").strip() if err: - msg = f'Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]' + msg = f"Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]" logger.error(error(msg)) raise DownloadError( f"Unable to download requirements: ```\n{err}\n```" @@ -150,7 +150,7 @@ async def load_plugin(self, username, repo, plugin_name, branch): try: self.bot.load_extension(ext) except commands.ExtensionError as exc: - msg = f'Plugin Load Failure: {username}/{repo}@{branch}[{plugin_name}]' + msg = f"Plugin Load Failure: {username}/{repo}@{branch}[{plugin_name}]" logger.error(error(msg)) raise DownloadError("Invalid plugin") from exc else: @@ -171,7 +171,9 @@ async def plugin_add(self, ctx, *, plugin_name: str): if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"] + plugin_name = ( + details["repository"] + "/" + plugin_name + "@" + details["branch"] + ) required_version = details["bot_version"] if parse_version(self.bot.version) < parse_version(required_version): @@ -251,13 +253,17 @@ async def plugin_remove(self, ctx, *, plugin_name: str): if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"] + plugin_name = ( + details["repository"] + "/" + plugin_name + "@" + details["branch"] + ) if plugin_name in self.bot.config.plugins: try: username, repo, name, branch = self.parse_plugin(plugin_name) - self.bot.unload_extension(f"plugins.{username}-{repo}-{branch}.{name}.{name}") + self.bot.unload_extension( + f"plugins.{username}-{repo}-{branch}.{name}.{name}" + ) except Exception: pass @@ -275,7 +281,9 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 os.chmod(path, stat.S_IWUSR) func(path) - shutil.rmtree(f"plugins/{username}-{repo}-{branch}", onerror=onerror) + shutil.rmtree( + f"plugins/{username}-{repo}-{branch}", onerror=onerror + ) except Exception as exc: logger.error(str(exc)) self.bot.config.plugins.append(plugin_name) @@ -302,7 +310,9 @@ async def plugin_update(self, ctx, *, plugin_name: str): if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"] + plugin_name = ( + details["repository"] + "/" + plugin_name + "@" + details["branch"] + ) if plugin_name not in self.bot.config.plugins: embed = discord.Embed( From 70b722c6996a5fcceb4924fb50f2a5bcdd7b885a Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 9 Jun 2019 03:49:59 +1000 Subject: [PATCH 0228/1221] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca6093a462..565ffb33e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [Unreleased] + +### Fixed +- Editing notes now works, minor bug with edit command is fixed. + # v2.24.1 ### Fixed From 675cecf39df1226654b320bbeaea61016f3d3547 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 9 Jun 2019 12:35:23 +1000 Subject: [PATCH 0229/1221] Better channel names --- core/thread.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/core/thread.py b/core/thread.py index 0952cbb50f..f1b45c0b66 100644 --- a/core/thread.py +++ b/core/thread.py @@ -471,7 +471,7 @@ async def send( embed = discord.Embed(description=message.content, timestamp=message.created_at) system_avatar_url = ( - "https://discordapp.com/assets/" "f78426a064bc9dd24847519259bc42af.png" + "https://discordapp.com/assets/f78426a064bc9dd24847519259bc42af.png" ) if not note: @@ -508,8 +508,13 @@ async def send( attachments = [x for x in attachments if not is_image_url(*x)] image_links = [ - (link, None) for link in re.findall(r"(https?://[^\s]+)", message.content) + (link, None) + for link in re.findall( + r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)", + message.content, + ) ] + image_links = [x for x in image_links if is_image_url(*x)] images.extend(image_links) @@ -727,20 +732,20 @@ def create( self.bot.loop.create_task(thread.setup(creator=creator, category=category)) return thread - async def find_or_create( - self, recipient: typing.Union[discord.Member, discord.User] - ) -> Thread: + async def find_or_create(self, recipient) -> Thread: return await self.find(recipient=recipient) or self.create(recipient) def format_channel_name(self, author): """Sanitises a username for use with text channel names""" name = author.name.lower() - allowed = string.ascii_letters + string.digits + "-" - new_name = "".join(l for l in name if l in allowed) or "null" - new_name += f"-{author.discriminator}" + new_name = ( + "".join(l for l in name if l not in string.punctuation and l.printable()) + or "null" + ) + new_name += f"⧫{author.discriminator}" while new_name in [c.name for c in self.bot.modmail_guild.text_channels]: - new_name += "-x" # two channels with same name + new_name += "⧫x" # two channels with same name return new_name From 844f15897b6db482041241fdc07b342caa2cc073 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 9 Jun 2019 12:44:03 +1000 Subject: [PATCH 0230/1221] better logic in handling dead threads --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index f1b45c0b66..a7245476f6 100644 --- a/core/thread.py +++ b/core/thread.py @@ -657,7 +657,7 @@ async def find( try: thread = self.cache[recipient_id] - if not self.bot.get_channel(thread.channel.id): # deleted channel + 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 From a87e7040a29b2f6f75c572fbeba8680fcafcad89 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 9 Jun 2019 12:45:49 +1000 Subject: [PATCH 0231/1221] Fix an attribute error --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index a7245476f6..384624b22f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -739,7 +739,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.printable()) + "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" ) new_name += f"⧫{author.discriminator}" From 85db1b616b6a5b8cb07f49e7f4dd50e1ee2ca32f Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 9 Jun 2019 12:57:08 +1000 Subject: [PATCH 0232/1221] switch back to dashes --- core/thread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index 384624b22f..a278b7a51b 100644 --- a/core/thread.py +++ b/core/thread.py @@ -742,10 +742,10 @@ def format_channel_name(self, author): "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" ) - new_name += f"⧫{author.discriminator}" + new_name += f"-{author.discriminator}" while new_name in [c.name for c in self.bot.modmail_guild.text_channels]: - new_name += "⧫x" # two channels with same name + new_name += "-x" # two channels with same name return new_name From 76dd6a95c34ef73636f7d3ec5c60b041473eb2e3 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 9 Jun 2019 12:58:14 +1000 Subject: [PATCH 0233/1221] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 565ffb33e3..7b9015be6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # [Unreleased] +### Changed +- Channel names now can contain unicode characters. + ### Fixed - Editing notes now works, minor bug with edit command is fixed. From 37713fbfa3d9361b693eca7634df354674306b5a Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 9 Jun 2019 21:46:24 +1000 Subject: [PATCH 0234/1221] Enforce embed field limits --- cogs/utility.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 0692a2bb73..d501173577 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -131,7 +131,9 @@ async def send_group_help(self, group): description=self.process_help_msg(group.help), ) - embed.add_field(name="Permission level", value=perm_level, inline=False) + if perm_level: + embed.add_field(name="Permission level", value=perm_level, inline=False) + format_ = "" length = len(group.commands) @@ -146,7 +148,7 @@ async def send_group_help(self, group): branch = "├─" format_ += f"`{branch} {command.name}` - {command.short_doc}\n" - embed.add_field(name="Sub Commands", value=format_, inline=False) + embed.add_field(name="Sub Commands", value=format_[:1024], inline=False) embed.set_footer( text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' "for more info on a command." From 5bdfb6f1fa6327b05163459be3e2ced0b1d5a2f5 Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sun, 9 Jun 2019 21:22:55 -0400 Subject: [PATCH 0235/1221] Fix Typo in Snippet Help Dialogue (#284) Change the template command in the help dialogue to create a snippet. (Line 99) --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index d6f5db87ad..15f01f99ac 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -96,7 +96,7 @@ async def snippets(self, ctx): To use snippets: First create a snippet using: - - `{prefix}snippet add snippet-name A pre-defined text.` + - `{prefix}snippets add snippet-name A pre-defined text.` Afterwards, you can use your snippet in a thread channel with `{prefix}snippet-name`, the message "A pre-defined text." From d07a4896289b7cfd7361fccbc8869620bb2ce0c8 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 10 Jun 2019 12:25:11 +1000 Subject: [PATCH 0236/1221] Fix small bug with scheduled closing --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index a278b7a51b..388c4c3f38 100644 --- a/core/thread.py +++ b/core/thread.py @@ -669,7 +669,7 @@ async def find( self.bot.modmail_guild.text_channels, topic=f"User ID: {recipient_id}" ) if channel: - thread = Thread(self, recipient, channel) + thread = Thread(self, recipient or recipient_id, channel) self.cache[recipient_id] = thread thread.ready = True return thread From f2959e71e525d79f9067bc5d805d52230a877a6a Mon Sep 17 00:00:00 2001 From: Kyber Date: Mon, 10 Jun 2019 12:43:27 +1000 Subject: [PATCH 0237/1221] Create SPONSORS.json --- SPONSORS.json | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 SPONSORS.json diff --git a/SPONSORS.json b/SPONSORS.json new file mode 100644 index 0000000000..187138c750 --- /dev/null +++ b/SPONSORS.json @@ -0,0 +1,27 @@ +[{ + "embed": { + "title": "You can have a custom page for yourself here.", + "description": "Generate your own embed from [here](https://leovoel.github.io/embed-visualizer/) and make a pull request editing the `SPONSERS.json` file.", + "url": "https://patreon.com/kyber", + "color": 6071775, + "timestamp": "2019-06-10T02:37:45.739Z", + "footer": { + "icon_url": "https://cdn.discordapp.com/embed/avatars/0.png", + "text": "footer text" + }, + "thumbnail": { + "url": "https://cdn.discordapp.com/embed/avatars/0.png" + }, + "author": { + "name": "Nothing here yet!", + "url": "https://patreon.com/kyber", + "icon_url": "https://cdn.discordapp.com/embed/avatars/0.png" + }, + "fields": [ + { + "name": "🤔", + "value": "This command lists the sponsors of this project on [**Patreon**](https://patreon.com/kyb3r)" + } + ] + } +}] From ef9d9e5cbf7ea6e1e2af737fee3dbcd9d4c3bd5d Mon Sep 17 00:00:00 2001 From: Kyber Date: Mon, 10 Jun 2019 12:54:04 +1000 Subject: [PATCH 0238/1221] Remove timestamp --- SPONSORS.json | 1 - 1 file changed, 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 187138c750..27984e6829 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -4,7 +4,6 @@ "description": "Generate your own embed from [here](https://leovoel.github.io/embed-visualizer/) and make a pull request editing the `SPONSERS.json` file.", "url": "https://patreon.com/kyber", "color": 6071775, - "timestamp": "2019-06-10T02:37:45.739Z", "footer": { "icon_url": "https://cdn.discordapp.com/embed/avatars/0.png", "text": "footer text" From 902df98e3c6aa358c0eb9d2114d2f9ab753cdf37 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 10 Jun 2019 13:00:17 +1000 Subject: [PATCH 0239/1221] Add sponsors command --- cogs/utility.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index d501173577..b9369d39b6 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -9,7 +9,7 @@ from io import StringIO from typing import Union from types import SimpleNamespace as param -from json import JSONDecodeError +from json import JSONDecodeError, loads from textwrap import indent from discord import Embed, Color, Activity, Role @@ -237,7 +237,6 @@ async def about(self, ctx): embed.add_field(name="Uptime", value=self.bot.uptime) embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms") - embed.add_field(name="Version", value=f"`{self.bot.version}`") embed.add_field(name="Author", value="[`kyb3r`](https://github.com/kyb3r)") @@ -253,14 +252,32 @@ async def about(self, ctx): name="GitHub", value="https://github.com/kyb3r/modmail", inline=False ) - embed.add_field( - name="\u200b", - value="Support this bot on [Patreon](https://patreon.com/kyber).", - ) + embed.add_field(name="Donate", value="[Patreon](https://patreon.com/kyber)") + + embed.add_field(name="Sponsers", value=f"`{self.bot.prefix}sponsors`") embed.set_footer(text=footer) await ctx.send(embed=embed) + @commands.command() + @checks.has_permissions(PermissionLevel.REGULAR) + @trigger_typing + async def sponsors(self, ctx): + """Shows a list of sponsors.""" + resp = await self.bot.session.get( + "https://raw.githubusercontent.com/kyb3r/modmail/master/SPONSORS.json" + ) + data = loads(await resp.text()) + + embeds = [] + + for elem in data: + em = Embed.from_dict(elem["embed"]) + embeds.append(em) + + session = PaginatorSession(ctx, *embeds) + await session.run() + @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) @trigger_typing From f0c4a160b1c99b47d20db6f914fb4a21d8122166 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 10 Jun 2019 13:00:27 +1000 Subject: [PATCH 0240/1221] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b9015be6f..143e6a31cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # [Unreleased] +### Added + +- Sponsors command that will list sponsors. + ### Changed - Channel names now can contain unicode characters. From dc979dcc4c043f69d4f14b9e93534d984cecd9b2 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 10 Jun 2019 14:18:04 +1000 Subject: [PATCH 0241/1221] Remove sponsors field --- cogs/utility.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index b9369d39b6..b857623c41 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -254,8 +254,6 @@ async def about(self, ctx): embed.add_field(name="Donate", value="[Patreon](https://patreon.com/kyber)") - embed.add_field(name="Sponsers", value=f"`{self.bot.prefix}sponsors`") - embed.set_footer(text=footer) await ctx.send(embed=embed) From 948b4173541b0f7a003be942d6cea5b7ec457576 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 10 Jun 2019 15:50:40 +1000 Subject: [PATCH 0242/1221] Fix the url regex --- core/thread.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index 388c4c3f38..8197581f3c 100644 --- a/core/thread.py +++ b/core/thread.py @@ -510,8 +510,7 @@ async def send( image_links = [ (link, None) for link in re.findall( - r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)", - message.content, + r"https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+", message.content ) ] From 20c652b40a4a7fe74070579e8abecb5d212e6158 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 10 Jun 2019 15:55:41 +1000 Subject: [PATCH 0243/1221] Proper url regex --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 8197581f3c..f736cacabe 100644 --- a/core/thread.py +++ b/core/thread.py @@ -510,7 +510,7 @@ async def send( image_links = [ (link, None) for link in re.findall( - r"https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+", message.content + r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", message.content ) ] From 828445abf13446047cf113f95450522df281bd49 Mon Sep 17 00:00:00 2001 From: MiTonder <48181821+MiTonder@users.noreply.github.com> Date: Mon, 10 Jun 2019 16:05:06 +0200 Subject: [PATCH 0244/1221] Added some more (#285) --- SPONSORS.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 27984e6829..c25d5914c1 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -1,7 +1,7 @@ [{ "embed": { "title": "You can have a custom page for yourself here.", - "description": "Generate your own embed from [here](https://leovoel.github.io/embed-visualizer/) and make a pull request editing the `SPONSERS.json` file.", + "description": "Generate your own embed from [here](https://leovoel.github.io/embed-visualizer/) and make a pull request editing the `SPONSORS.json` file on [GitHub](https://github.com/kyb3r/modmail/blob/master/SPONSORS.json).", "url": "https://patreon.com/kyber", "color": 6071775, "footer": { From 94f84532db5a9a377ef1632e62c7c57f8134fb98 Mon Sep 17 00:00:00 2001 From: MiTonder <48181821+MiTonder@users.noreply.github.com> Date: Mon, 10 Jun 2019 17:46:44 +0200 Subject: [PATCH 0245/1221] Indret image here Icons instead of the images that is there (#286) --- SPONSORS.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index c25d5914c1..24133a86ff 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -5,16 +5,16 @@ "url": "https://patreon.com/kyber", "color": 6071775, "footer": { - "icon_url": "https://cdn.discordapp.com/embed/avatars/0.png", + "icon_url": "https://vignette.wikia.nocookie.net/smashtopia/images/c/c9/Insert_Image_Here.jpg/revision/latest?cb=20170827054712", "text": "footer text" }, "thumbnail": { - "url": "https://cdn.discordapp.com/embed/avatars/0.png" + "url": "https://vignette.wikia.nocookie.net/smashtopia/images/c/c9/Insert_Image_Here.jpg/revision/latest?cb=20170827054712" }, "author": { "name": "Nothing here yet!", "url": "https://patreon.com/kyber", - "icon_url": "https://cdn.discordapp.com/embed/avatars/0.png" + "icon_url": "https://vignette.wikia.nocookie.net/smashtopia/images/c/c9/Insert_Image_Here.jpg/revision/latest?cb=20170827054712" }, "fields": [ { From 4002a35c8e46624c4ddf90291905925617a45d4a Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 11 Jun 2019 01:48:44 +1000 Subject: [PATCH 0246/1221] Update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4d9ae2593a..0c953c5e8e 100644 --- a/.gitignore +++ b/.gitignore @@ -128,4 +128,5 @@ dmypy.json config.json plugins/ !plugins/registry.json -temp/ \ No newline at end of file +temp/ +test.py From 84caa8bb4084a4033df801d90261e90b11e7b560 Mon Sep 17 00:00:00 2001 From: Kyber Date: Tue, 11 Jun 2019 01:50:08 +1000 Subject: [PATCH 0247/1221] correct patreon link --- SPONSORS.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 24133a86ff..29011d0adc 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -19,7 +19,7 @@ "fields": [ { "name": "🤔", - "value": "This command lists the sponsors of this project on [**Patreon**](https://patreon.com/kyb3r)" + "value": "This command lists the sponsors of this project on [**Patreon**](https://patreon.com/kyber)" } ] } From abdcbfa5461e87f99d6a13b4232a1227d5f5c5f9 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 11 Jun 2019 09:19:55 +1000 Subject: [PATCH 0248/1221] Randomly shuffle sponsor embeds --- cogs/utility.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cogs/utility.py b/cogs/utility.py index b857623c41..ae85fabd08 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -3,6 +3,7 @@ import logging import os import traceback +import random from contextlib import redirect_stdout from datetime import datetime from difflib import get_close_matches @@ -273,6 +274,8 @@ async def sponsors(self, ctx): em = Embed.from_dict(elem["embed"]) embeds.append(em) + random.shuffle(embeds) + session = PaginatorSession(ctx, *embeds) await session.run() From b71e741aa3c04887b05ff693e48dcb772189ebd4 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 12 Jun 2019 12:12:58 +1000 Subject: [PATCH 0249/1221] Better wording. --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aa30af4fe2..28b037e589 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,13 @@ ## What is Modmail? -Modmail is akin to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox/feed for server staff to communicate with the userbase and vice versa in a seamless way. +Modmail is similar to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. ## How does it work? -When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. +When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. All threads are logged and you can view previous threads through the corresponding generated log link. Here is an [**example**](https://logs.modmail.tk/example) ## Features - * **Highly Customisable** * Bot activity, prefix, category, log channel, etc. * Fully customisable command permission system. From 6918f92d4c8bd2803a0c393571b85b400940849c Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 12 Jun 2019 12:13:27 +1000 Subject: [PATCH 0250/1221] Seperate sentences --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 28b037e589..9d77038fc2 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ Modmail is similar to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. ## How does it work? -When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. All threads are logged and you can view previous threads through the corresponding generated log link. Here is an [**example**](https://logs.modmail.tk/example) +When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. + +All threads are logged and you can view previous threads through the corresponding generated log link. Here is an [**example**](https://logs.modmail.tk/example) ## Features From 3c6c949aaadcb8d8374995f1e520e1beb9821a12 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sat, 15 Jun 2019 12:27:36 +1000 Subject: [PATCH 0251/1221] Fix response failing to send --- CHANGELOG.md | 1 + cogs/utility.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 143e6a31cf..9056f1b5e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Editing notes now works, minor bug with edit command is fixed. +- Bug in the `oauth` command where the response message fails to send when an ID is provided. # v2.24.1 diff --git a/cogs/utility.py b/cogs/utility.py index ae85fabd08..ba8551eb43 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1328,7 +1328,10 @@ def get_level(perm_level): ) @checks.has_permissions(PermissionLevel.OWNER) async def oauth(self, ctx): - """Commands relating to Logviewer oauth2 login authentication.""" + """Commands relating to Logviewer oauth2 login authentication. + + This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature. + """ await ctx.send_help(ctx.command) @oauth.command(name="whitelist") @@ -1352,6 +1355,10 @@ async def oauth_whitelist(self, ctx, target: Union[User, Role]): embed = Embed(color=self.bot.main_color) embed.title = "Success" + + if not hasattr(target, 'mention'): + 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." ) From 6a30de035468e8c2f693a9d2729d7e7e1f368031 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 16 Jun 2019 12:16:15 +1000 Subject: [PATCH 0252/1221] Dont do a user install if running in a venv --- cogs/plugins.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 086273a32f..e9045a5c29 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -115,19 +115,23 @@ async def load_plugin(self, username, repo, plugin_name, branch): if "requirements.txt" in os.listdir(dirname): # Install PIP requirements + + venv = hasattr(sys, 'real_prefix') # in a virtual env + user_install = '--user' if not venv else '' + try: if os.name == "nt": # Windows await self.bot.loop.run_in_executor( None, self._asubprocess_run, - f"pip install -r {dirname}/requirements.txt --user -q -q", + f"pip install -r {dirname}/requirements.txt {user_install} -q -q", ) else: await self.bot.loop.run_in_executor( None, self._asubprocess_run, f"python3 -m pip install -U -r {dirname}/" - "requirements.txt --user -q -q", + f"requirements.txt {user_install} -q -q", ) # -q -q (quiet) # so there's no terminal output unless there's an error From f7809139da2d896e19a261bef31860bf7c3d3447 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 16 Jun 2019 12:46:21 +1000 Subject: [PATCH 0253/1221] Implement multiple log files for bots --- bot.py | 27 ++++++++++++++++----------- cogs/utility.py | 12 +++++++++--- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/bot.py b/bot.py index 32fb426471..81b17daf0b 100644 --- a/bot.py +++ b/bot.py @@ -54,16 +54,6 @@ def format(self, record): if not os.path.exists(temp_dir): os.mkdir(temp_dir) -ch_debug = logging.FileHandler(os.path.join(temp_dir, "logs.log"), mode="a+") - -ch_debug.setLevel(logging.DEBUG) -formatter_debug = FileFormatter( - "%(asctime)s %(filename)s - " "%(levelname)s: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", -) -ch_debug.setFormatter(formatter_debug) -logger.addHandler(ch_debug) - LINE = Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL @@ -111,6 +101,19 @@ def _configure_logging(self): "DEBUG": logging.DEBUG, } + + log_file_name = self.config.token.split('.')[0] + ch_debug = logging.FileHandler(os.path.join(temp_dir, f"{log_file_name}.log"), mode="a+") + + ch_debug.setLevel(logging.DEBUG) + formatter_debug = FileFormatter( + "%(asctime)s %(filename)s - " "%(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ch_debug.setFormatter(formatter_debug) + logger.addHandler(ch_debug) + + log_level = logging_levels.get(level_text) logger.info(LINE) if log_level is not None: @@ -359,9 +362,11 @@ async def on_ready(self): logger.info(info("Client ready.")) logger.info(LINE) logger.info(info(f"Logged in as: {self.user}")) - logger.info(info(f"Prefix: {self.prefix}")) logger.info(info(f"User ID: {self.user.id}")) + logger.info(info(f"Prefix: {self.prefix}")) + logger.info(info(f"Guild Name: {self.guild.name if self.guild else 'None'}")) logger.info(info(f"Guild ID: {self.guild.id if self.guild else 0}")) + logger.info(LINE) if not self.guild: diff --git a/cogs/utility.py b/cogs/utility.py index ba8551eb43..72758645c7 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -285,9 +285,11 @@ async def sponsors(self, ctx): async def debug(self, ctx): """Shows the recent application-logs of the bot.""" + log_file_name = self.bot.config.token.split('.')[0] + with open( os.path.join( - os.path.dirname(os.path.abspath(__file__)), "../temp/logs.log" + os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "r+", ) as f: @@ -338,10 +340,11 @@ async def debug_hastebin(self, ctx): """Posts application-logs to Hastebin.""" haste_url = os.environ.get("HASTE_URL", "https://hasteb.in") + log_file_name = self.bot.config.token.split('.')[0] with open( os.path.join( - os.path.dirname(os.path.abspath(__file__)), "../temp/logs.log" + os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "r+", ) as f: @@ -372,9 +375,12 @@ async def debug_hastebin(self, ctx): @trigger_typing async def debug_clear(self, ctx): """Clears the locally cached logs.""" + + log_file_name = self.bot.config.token.split('.')[0] + with open( os.path.join( - os.path.dirname(os.path.abspath(__file__)), "../temp/logs.log" + os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "w", ): From 1069232cf6c084aa663b380f2537304aa00de6e3 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 16 Jun 2019 12:46:23 +1000 Subject: [PATCH 0254/1221] Update CHANGELOG.md --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9056f1b5e2..3983572bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sponsors command that will list sponsors. ### Changed -- Channel names now can contain unicode characters. +- Channel names now can contain unicode characters. +- Debug logs are now located in a unique file for each bot. (Internal change) ### Fixed - Editing notes now works, minor bug with edit command is fixed. - Bug in the `oauth` command where the response message fails to send when an ID is provided. +- Plugin requirement installation now works in virtual environments + # v2.24.1 From 7326eca4cd9e4a7ef573e9e26360a0b2f1f86b56 Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 16 Jun 2019 13:20:45 +1000 Subject: [PATCH 0255/1221] Update and rename config.json.example to .env.example --- .env.example | 5 +++++ config.json.example | 7 ------- 2 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 .env.example delete mode 100644 config.json.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..b3554bf9c9 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +TOKEN=MyBotToken +LOG_URL=https://logviewername.herokuapp.com/ +GUILD_ID=1234567890 +OWNERS=Owner1ID,Owner2ID,Owner3ID +MONGO_URI=mongodb+srv://mongodburi diff --git a/config.json.example b/config.json.example deleted file mode 100644 index 0e36d9111e..0000000000 --- a/config.json.example +++ /dev/null @@ -1,7 +0,0 @@ -{ - "TOKEN": "MyBotToken", - "LOG_URL": "https://logviewername.herokuapp.com/", - "GUILD_ID": 1234567890, - "OWNERS": "Owner1ID,Owner2ID,Owner3ID", - "MONGO_URI": "mongodb+srv://mongodburi" -} From 125e2635f2149981d9927f7ef5b5df9e18e24c7e Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 16 Jun 2019 13:21:29 +1000 Subject: [PATCH 0256/1221] Use .env instead of config.json --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d77038fc2..effaa06338 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Install dependancies $ pipenv install ``` -Rename the `config.json.example` to `config.json` and fill out the fields. +Rename the `.env.example` to `.env` and fill out the fields. And finally, run the bot. ```console $ pipenv run python3 bot.py From b0b26380d44d7e0bb588b826d0f84d7cca77c10d Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 16 Jun 2019 15:20:05 +1000 Subject: [PATCH 0257/1221] Enforce black code style --- bot.py | 10 +++++----- cogs/plugins.py | 4 ++-- core/thread.py | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bot.py b/bot.py index 81b17daf0b..116735d6af 100644 --- a/bot.py +++ b/bot.py @@ -101,9 +101,10 @@ def _configure_logging(self): "DEBUG": logging.DEBUG, } - - log_file_name = self.config.token.split('.')[0] - ch_debug = logging.FileHandler(os.path.join(temp_dir, f"{log_file_name}.log"), mode="a+") + log_file_name = self.config.token.split(".")[0] + ch_debug = logging.FileHandler( + os.path.join(temp_dir, f"{log_file_name}.log"), mode="a+" + ) ch_debug.setLevel(logging.DEBUG) formatter_debug = FileFormatter( @@ -113,7 +114,6 @@ def _configure_logging(self): ch_debug.setFormatter(formatter_debug) logger.addHandler(ch_debug) - log_level = logging_levels.get(level_text) logger.info(LINE) if log_level is not None: @@ -366,7 +366,7 @@ async def on_ready(self): logger.info(info(f"Prefix: {self.prefix}")) logger.info(info(f"Guild Name: {self.guild.name if self.guild else 'None'}")) logger.info(info(f"Guild ID: {self.guild.id if self.guild else 0}")) - + logger.info(LINE) if not self.guild: diff --git a/cogs/plugins.py b/cogs/plugins.py index e9045a5c29..c138af960e 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -116,8 +116,8 @@ async def load_plugin(self, username, repo, plugin_name, branch): if "requirements.txt" in os.listdir(dirname): # Install PIP requirements - venv = hasattr(sys, 'real_prefix') # in a virtual env - user_install = '--user' if not venv else '' + venv = hasattr(sys, "real_prefix") # in a virtual env + user_install = "--user" if not venv else "" try: if os.name == "nt": # Windows diff --git a/core/thread.py b/core/thread.py index f736cacabe..ea743f9b6f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -510,7 +510,8 @@ async def send( image_links = [ (link, None) for link in re.findall( - r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", message.content + r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + message.content, ) ] From b69afd7e2ba5c353f3d350e63a22ce1a2af61842 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 16 Jun 2019 15:22:10 +1000 Subject: [PATCH 0258/1221] Make default cogs always appear first, --- cogs/utility.py | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 72758645c7..8782872388 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -78,13 +78,24 @@ async def format_cog_help(self, cog): def process_help_msg(self, help_: str): return help_.format(prefix=self.clean_prefix) if help_ else "No help message." - async def send_bot_help(self, mapping): + async def send_bot_help(self, cogs): embeds = [] # TODO: Implement for no cog commands - for cog in sorted( - (key for key in mapping.keys() if key is not None), - key=lambda c: c.qualified_name, - ): + + cogs = list(filter(None, cogs)) + + bot = self.context.bot + + # always come first + 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) + + for cog in default_cogs: embeds.extend(await self.format_cog_help(cog)) p_session = PaginatorSession( @@ -285,11 +296,12 @@ async def sponsors(self, ctx): async def debug(self, ctx): """Shows the recent application-logs of the bot.""" - log_file_name = self.bot.config.token.split('.')[0] + log_file_name = self.bot.config.token.split(".")[0] 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: @@ -340,11 +352,12 @@ async def debug_hastebin(self, ctx): """Posts application-logs to Hastebin.""" haste_url = os.environ.get("HASTE_URL", "https://hasteb.in") - log_file_name = self.bot.config.token.split('.')[0] + log_file_name = self.bot.config.token.split(".")[0] 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: @@ -376,11 +389,12 @@ async def debug_hastebin(self, ctx): async def debug_clear(self, ctx): """Clears the locally cached logs.""" - log_file_name = self.bot.config.token.split('.')[0] + log_file_name = self.bot.config.token.split(".")[0] 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", ): @@ -1362,9 +1376,11 @@ async def oauth_whitelist(self, ctx, target: Union[User, Role]): embed = Embed(color=self.bot.main_color) embed.title = "Success" - if not hasattr(target, 'mention'): - target = self.bot.get_user(target.id) or self.bot.modmail_guild.get_role(target.id) - + if not hasattr(target, "mention"): + 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." ) From 9d279e63a5a6b36786f346ed490ba4121927f057 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Sun, 16 Jun 2019 15:23:05 +1000 Subject: [PATCH 0259/1221] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3983572bb2..da7e959818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Channel names now can contain unicode characters. - Debug logs are now located in a unique file for each bot. (Internal change) +- Default cogs always appear first in the help command now. ### Fixed - Editing notes now works, minor bug with edit command is fixed. From eb79f2e5753128c9cb17205d77c10f37939e4dfe Mon Sep 17 00:00:00 2001 From: Kyber Date: Tue, 18 Jun 2019 12:07:08 +1000 Subject: [PATCH 0260/1221] Use pipenv for travis ci --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4c9a94db6a..7458b1db96 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ matrix: dist: xenial install: - - pip install -r requirements.txt - - pip install pylint + - pipenv install + - pipenv install pylint -script: python .lint.py \ No newline at end of file +script: python .lint.py From 4222da3c0112c9fba3264985de00e73b7dc57e94 Mon Sep 17 00:00:00 2001 From: Kyber Date: Thu, 20 Jun 2019 01:44:47 +1000 Subject: [PATCH 0261/1221] Remove update functionality from the bot. (#293) * Remove autoupdate functionality * Remove update command * Remove github access token required * Update CHANGELOG.md --- CHANGELOG.md | 8 ++++- bot.py | 62 ------------------------------------- cogs/utility.py | 77 ---------------------------------------------- core/decorators.py | 19 +----------- 4 files changed, 8 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da7e959818..2aac41b89b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# [Unreleased] +# [Unreleased] (3.0.0) ### Added - Sponsors command that will list sponsors. +### Removed + +- Removed autoupdate functionality and the `update` command in favour of the [Pull app](https://github.com/apps/pull). + +Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/updating) + ### Changed - Channel names now can contain unicode characters. - Debug logs are now located in a unique file for each bot. (Internal change) diff --git a/bot.py b/bot.py index 116735d6af..6cdd8f4522 100644 --- a/bot.py +++ b/bot.py @@ -74,7 +74,6 @@ def __init__(self): self.plugin_db = PluginDatabaseClient(self) self.metadata_task = self.loop.create_task(self.metadata_loop()) - self.autoupdate_task = self.loop.create_task(self.autoupdate_loop()) self._load_extensions() @property @@ -192,11 +191,6 @@ def run(self, *args, **kwargs): self.loop.run_until_complete(self.metadata_task) except asyncio.CancelledError: logger.debug(info("data_task has been cancelled.")) - try: - self.autoupdate_task.cancel() - self.loop.run_until_complete(self.autoupdate_task) - except asyncio.CancelledError: - logger.debug(info("autoupdate_task has been cancelled.")) self.loop.run_until_complete(self.logout()) for task in asyncio.Task.all_tasks(): @@ -904,62 +898,6 @@ async def validate_database_connection(self): else: logger.info(info("Successfully connected to the database.")) - async def autoupdate_loop(self): - await self.wait_until_ready() - - if self.config.get("disable_autoupdates"): - logger.warning(info("Autoupdates disabled.")) - logger.info(LINE) - return - - if not self.config.get("github_access_token"): - logger.warning(info("GitHub access token not found.")) - logger.warning(info("Autoupdates disabled.")) - logger.info(LINE) - return - - logger.info(info("Autoupdate loop started.")) - - while not self.is_closed(): - changelog = await Changelog.from_url(self) - latest = changelog.latest_version - - if parse_version(self.version) < parse_version(latest.version): - data = await self.api.update_repository() - - embed = discord.Embed(color=discord.Color.green()) - - commit_data = data["data"] - user = data["user"] - embed.set_author( - name=user["username"] + " - Updating Bot", - icon_url=user["avatar_url"], - url=user["url"], - ) - - embed.set_footer( - text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" - ) - - embed.description = latest.description - for name, value in latest.fields.items(): - embed.add_field(name=name, value=value) - - if commit_data: - message = commit_data["commit"]["message"] - html_url = commit_data["html_url"] - short_sha = commit_data["sha"][:6] - embed.add_field( - name="Merge Commit", - value=f"[`{short_sha}`]({html_url}) " - f"{message} - {user['username']}", - ) - logger.info(info("Bot has been updated.")) - channel = self.log_channel - await channel.send(embed=embed) - - await asyncio.sleep(3600) - async def metadata_loop(self): await self.wait_until_ready() if not self.guild: diff --git a/cogs/utility.py b/cogs/utility.py index 8782872388..d3c4cff095 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -423,83 +423,6 @@ async def github(self, ctx): embed.set_thumbnail(url=user["avatar_url"]) await ctx.send(embed=embed) - @commands.command() - @checks.has_permissions(PermissionLevel.OWNER) - @github_access_token_required - @trigger_typing - async def update(self, ctx, *, flag: str = ""): - """ - Update Modmail. - - This only works for Heroku users who have configured their bot for updates. - - To stay up-to-date with the latest commit - from GitHub, specify "force" as the flag. - """ - - changelog = await Changelog.from_url(self.bot) - latest = changelog.latest_version - - desc = ( - f"The latest version is [`{self.bot.version}`]" - "(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)" - ) - - if ( - parse_version(self.bot.version) >= parse_version(latest.version) - and flag.lower() != "force" - ): - embed = Embed( - title="Already up to date", description=desc, color=self.bot.main_color - ) - - data = await self.bot.api.get_user_info() - if not data.get("error"): - user = data["user"] - embed.set_author( - name=user["username"], icon_url=user["avatar_url"], url=user["url"] - ) - else: - data = await self.bot.api.update_repository() - - commit_data = data["data"] - user = data["user"] - - if commit_data: - embed = Embed(color=self.bot.main_color) - - embed.set_footer( - text=f"Updating Modmail v{self.bot.version} " - f"-> v{latest.version}" - ) - - embed.set_author( - name=user["username"] + " - Updating bot", - icon_url=user["avatar_url"], - url=user["url"], - ) - - embed.description = latest.description - for name, value in latest.fields.items(): - embed.add_field(name=name, value=value) - # message = commit_data['commit']['message'] - html_url = commit_data["html_url"] - short_sha = commit_data["sha"][:6] - embed.add_field( - name="Merge Commit", value=f"[`{short_sha}`]({html_url})" - ) - else: - embed = Embed( - title="Already up to date with master repository.", - description="No further updates required", - color=self.bot.main_color, - ) - embed.set_author( - name=user["username"], icon_url=user["avatar_url"], url=user["url"] - ) - - return await ctx.send(embed=embed) - @commands.command(aliases=["presence"]) @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): diff --git a/core/decorators.py b/core/decorators.py index 03deb09e61..c87f34129b 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -10,21 +10,4 @@ async def wrapper(self, ctx: commands.Context, *args, **kwargs): await ctx.trigger_typing() return await func(self, ctx, *args, **kwargs) - return wrapper - - -def github_access_token_required(func): - @functools.wraps(func) - async def wrapper(self, ctx: commands.Context, *args, **kwargs): - if self.bot.config.get("github_access_token"): - return await func(self, ctx, *args, **kwargs) - - desc = ( - "You can only use this command if you have a " - "configured `GITHUB_ACCESS_TOKEN`. Get a " - "personal access token from developer settings." - ) - embed = Embed(color=Color.red(), title="Unauthorized", description=desc) - await ctx.send(embed=embed) - - return wrapper + return wrapper \ No newline at end of file From a05c5e1f12e8a52636c80b175f2c5cabadcb8e6f Mon Sep 17 00:00:00 2001 From: Kyber Date: Thu, 20 Jun 2019 01:54:14 +1000 Subject: [PATCH 0262/1221] Revert "Remove update functionality from the bot. (#293)" (#294) This reverts commit 4222da3c0112c9fba3264985de00e73b7dc57e94. --- CHANGELOG.md | 8 +---- bot.py | 62 +++++++++++++++++++++++++++++++++++++ cogs/utility.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++ core/decorators.py | 19 +++++++++++- 4 files changed, 158 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aac41b89b..da7e959818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# [Unreleased] (3.0.0) +# [Unreleased] ### Added - Sponsors command that will list sponsors. -### Removed - -- Removed autoupdate functionality and the `update` command in favour of the [Pull app](https://github.com/apps/pull). - -Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/updating) - ### Changed - Channel names now can contain unicode characters. - Debug logs are now located in a unique file for each bot. (Internal change) diff --git a/bot.py b/bot.py index 6cdd8f4522..116735d6af 100644 --- a/bot.py +++ b/bot.py @@ -74,6 +74,7 @@ def __init__(self): self.plugin_db = PluginDatabaseClient(self) self.metadata_task = self.loop.create_task(self.metadata_loop()) + self.autoupdate_task = self.loop.create_task(self.autoupdate_loop()) self._load_extensions() @property @@ -191,6 +192,11 @@ def run(self, *args, **kwargs): self.loop.run_until_complete(self.metadata_task) except asyncio.CancelledError: logger.debug(info("data_task has been cancelled.")) + try: + self.autoupdate_task.cancel() + self.loop.run_until_complete(self.autoupdate_task) + except asyncio.CancelledError: + logger.debug(info("autoupdate_task has been cancelled.")) self.loop.run_until_complete(self.logout()) for task in asyncio.Task.all_tasks(): @@ -898,6 +904,62 @@ async def validate_database_connection(self): else: logger.info(info("Successfully connected to the database.")) + async def autoupdate_loop(self): + await self.wait_until_ready() + + if self.config.get("disable_autoupdates"): + logger.warning(info("Autoupdates disabled.")) + logger.info(LINE) + return + + if not self.config.get("github_access_token"): + logger.warning(info("GitHub access token not found.")) + logger.warning(info("Autoupdates disabled.")) + logger.info(LINE) + return + + logger.info(info("Autoupdate loop started.")) + + while not self.is_closed(): + changelog = await Changelog.from_url(self) + latest = changelog.latest_version + + if parse_version(self.version) < parse_version(latest.version): + data = await self.api.update_repository() + + embed = discord.Embed(color=discord.Color.green()) + + commit_data = data["data"] + user = data["user"] + embed.set_author( + name=user["username"] + " - Updating Bot", + icon_url=user["avatar_url"], + url=user["url"], + ) + + embed.set_footer( + text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" + ) + + embed.description = latest.description + for name, value in latest.fields.items(): + embed.add_field(name=name, value=value) + + if commit_data: + message = commit_data["commit"]["message"] + html_url = commit_data["html_url"] + short_sha = commit_data["sha"][:6] + embed.add_field( + name="Merge Commit", + value=f"[`{short_sha}`]({html_url}) " + f"{message} - {user['username']}", + ) + logger.info(info("Bot has been updated.")) + channel = self.log_channel + await channel.send(embed=embed) + + await asyncio.sleep(3600) + async def metadata_loop(self): await self.wait_until_ready() if not self.guild: diff --git a/cogs/utility.py b/cogs/utility.py index d3c4cff095..8782872388 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -423,6 +423,83 @@ async def github(self, ctx): embed.set_thumbnail(url=user["avatar_url"]) await ctx.send(embed=embed) + @commands.command() + @checks.has_permissions(PermissionLevel.OWNER) + @github_access_token_required + @trigger_typing + async def update(self, ctx, *, flag: str = ""): + """ + Update Modmail. + + This only works for Heroku users who have configured their bot for updates. + + To stay up-to-date with the latest commit + from GitHub, specify "force" as the flag. + """ + + changelog = await Changelog.from_url(self.bot) + latest = changelog.latest_version + + desc = ( + f"The latest version is [`{self.bot.version}`]" + "(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)" + ) + + if ( + parse_version(self.bot.version) >= parse_version(latest.version) + and flag.lower() != "force" + ): + embed = Embed( + title="Already up to date", description=desc, color=self.bot.main_color + ) + + data = await self.bot.api.get_user_info() + if not data.get("error"): + user = data["user"] + embed.set_author( + name=user["username"], icon_url=user["avatar_url"], url=user["url"] + ) + else: + data = await self.bot.api.update_repository() + + commit_data = data["data"] + user = data["user"] + + if commit_data: + embed = Embed(color=self.bot.main_color) + + embed.set_footer( + text=f"Updating Modmail v{self.bot.version} " + f"-> v{latest.version}" + ) + + embed.set_author( + name=user["username"] + " - Updating bot", + icon_url=user["avatar_url"], + url=user["url"], + ) + + embed.description = latest.description + for name, value in latest.fields.items(): + embed.add_field(name=name, value=value) + # message = commit_data['commit']['message'] + html_url = commit_data["html_url"] + short_sha = commit_data["sha"][:6] + embed.add_field( + name="Merge Commit", value=f"[`{short_sha}`]({html_url})" + ) + else: + embed = Embed( + title="Already up to date with master repository.", + description="No further updates required", + color=self.bot.main_color, + ) + embed.set_author( + name=user["username"], icon_url=user["avatar_url"], url=user["url"] + ) + + return await ctx.send(embed=embed) + @commands.command(aliases=["presence"]) @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): diff --git a/core/decorators.py b/core/decorators.py index c87f34129b..03deb09e61 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -10,4 +10,21 @@ async def wrapper(self, ctx: commands.Context, *args, **kwargs): await ctx.trigger_typing() return await func(self, ctx, *args, **kwargs) - return wrapper \ No newline at end of file + return wrapper + + +def github_access_token_required(func): + @functools.wraps(func) + async def wrapper(self, ctx: commands.Context, *args, **kwargs): + if self.bot.config.get("github_access_token"): + return await func(self, ctx, *args, **kwargs) + + desc = ( + "You can only use this command if you have a " + "configured `GITHUB_ACCESS_TOKEN`. Get a " + "personal access token from developer settings." + ) + embed = Embed(color=Color.red(), title="Unauthorized", description=desc) + await ctx.send(embed=embed) + + return wrapper From d897bfba3f0b8cd16290ab94bbf333a189bed148 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Fri, 21 Jun 2019 15:57:05 +0800 Subject: [PATCH 0263/1221] $$ --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..cfe06ffd17 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +patreon: kyb3r +github: [kyb3r, fourjr, Taaku18] From af9a6ed557cecba61192096383c19cf5411f03f9 Mon Sep 17 00:00:00 2001 From: Kyber Date: Fri, 21 Jun 2019 17:58:46 +1000 Subject: [PATCH 0264/1221] Update FUNDING.yml --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index cfe06ffd17..d78f1898fe 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -patreon: kyb3r +patreon: kyber github: [kyb3r, fourjr, Taaku18] From 7faf53d3ff96000bdab05a38b223b057e1e3284b Mon Sep 17 00:00:00 2001 From: Kyber Date: Fri, 21 Jun 2019 17:59:30 +1000 Subject: [PATCH 0265/1221] Update FUNDING.yml --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d78f1898fe..16ee1be130 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ patreon: kyber -github: [kyb3r, fourjr, Taaku18] +github: kyb3r From 097f94b9f3df2128489758a2ec9090402c694e32 Mon Sep 17 00:00:00 2001 From: Kyber Date: Fri, 21 Jun 2019 18:01:19 +1000 Subject: [PATCH 0266/1221] Update FUNDING.yml --- .github/FUNDING.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 16ee1be130..fbcb038e69 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1 @@ patreon: kyber -github: kyb3r From b448633f4011b1446fec52a7fae27483684320cb Mon Sep 17 00:00:00 2001 From: MiTonder <48181821+MiTonder@users.noreply.github.com> Date: Fri, 21 Jun 2019 14:16:39 +0200 Subject: [PATCH 0267/1221] Add stats plugin to registry (#296) I have worked on stats plugin for a while now, and i have finally finished it (partly) i added paginators to it and therefor i think it can be good in the registry. --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 2bcd4dfa0e..074eeac347 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -160,5 +160,14 @@ "title": "Colors!!", "icon_url": "https://cdn1.iconfinder.com/data/icons/weather-19/32/rainbow-512.png", "thumbnail_url": "https://i.imgur.com/fSxnc9W.jpg" + }, + "stats": { + "repository": "MiTonder/modmail-plugins", + "branch": "master", + "description": "Get useful stats directly in an embed about either the ModMail bot, a user or the server.", + "bot_version": "2.24.1", + "title": "Get Stats", + "icon_url": "https://image.flaticon.com/icons/png/512/117/117761.png", + "thumbnail_url": "http://www.pngmart.com/files/7/Statistics-PNG-Clipart.png" } } From 25d8528a5671a08ee99739e18c843d183e8baccf Mon Sep 17 00:00:00 2001 From: ThE KinG <43880032+TheKinG2149@users.noreply.github.com> Date: Fri, 21 Jun 2019 17:47:41 +0530 Subject: [PATCH 0268/1221] A fun commands plugin. (#297) --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 074eeac347..b9151b2c69 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -160,6 +160,15 @@ "title": "Colors!!", "icon_url": "https://cdn1.iconfinder.com/data/icons/weather-19/32/rainbow-512.png", "thumbnail_url": "https://i.imgur.com/fSxnc9W.jpg" + }, + "fun": { + "repository": "TheKinG2149/modmail-plugins", + "branch": "master", + "description": "Some fun commands like 8ball,dadjokes", + "bot_version": "2.24.1", + "title": "Fun", + "icon_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png", + "thumbnail_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png" }, "stats": { "repository": "MiTonder/modmail-plugins", From 6138285fe7ebc0d4273290e1e543f155d86a73bc Mon Sep 17 00:00:00 2001 From: Taven Date: Sat, 22 Jun 2019 12:21:29 +0530 Subject: [PATCH 0269/1221] Set presence every 10 mins (#301) According to d.py support, the disappearance of presence is common among all discord bots and is likely a bug on discord's side. This will reduce resetting of the presence to 10 mins. Sources: https://imgur.com/a/zJYuVXG https://discordapp.com/channels/336642139381301249/381965515721146390/591864012376113163 --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 8782872388..4d70096cb3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -683,7 +683,7 @@ async def loop_presence(self): await self.bot.wait_until_ready() while not self.bot.is_closed(): self.presence = await self.set_presence() - await asyncio.sleep(3600) + await asyncio.sleep(600) @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) From a5b24efdc1039ccce10b843f8afc618c45ce405d Mon Sep 17 00:00:00 2001 From: Vivek Joshy Date: Sun, 23 Jun 2019 22:12:29 +0530 Subject: [PATCH 0270/1221] This introduces `thread_auto_close` and `thread_auto_close_response`. Both these can be used to configure modmail to auto close a thread after some time has passed. `thread_auto_close_response` has a `%t` variable that can be used to insert human friendly time into the closing message. --- core/config.py | 4 ++- core/thread.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/core/config.py b/core/config.py index b4cb8922c3..325f31c9d3 100644 --- a/core/config.py +++ b/core/config.py @@ -35,6 +35,8 @@ class ConfigManager: "blocked_emoji", "close_emoji", "disable_recipient_thread_close", + "thread_auto_close", + "thread_auto_close_response", "thread_creation_response", "thread_creation_footer", "thread_creation_title", @@ -89,7 +91,7 @@ class ConfigManager: colors = {"mod_color", "recipient_color", "main_color"} - time_deltas = {"account_age", "guild_age"} + time_deltas = {"account_age", "guild_age", "thread_auto_close"} valid_keys = allowed_to_change_in_command | internal_keys | protected_keys diff --git a/core/thread.py b/core/thread.py index ea743f9b6f..5ca0891943 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1,15 +1,17 @@ import asyncio import logging -import re import os +import re import string import typing -from types import SimpleNamespace as param from datetime import datetime, timedelta +from types import SimpleNamespace as param import discord +import isodate from discord.ext.commands import MissingRequiredArgument, CommandError +from core.time import human_timedelta from core.utils import is_image_url, days, match_user_id from core.utils import truncate, ignore, error @@ -39,6 +41,7 @@ def __init__( self.genesis_message = None self._ready_event = asyncio.Event() self._close_task = None + self._auto_close_task = None def __repr__(self): return ( @@ -218,6 +221,10 @@ async def _close( await self.cancel_closure() + # Cancel auto closing the thread if closed by any means. + if self._auto_close_task: + self._auto_close_task.cancel() + if str(self.id) in self.bot.config.subscriptions: del self.bot.config.subscriptions[str(self.id)] @@ -336,6 +343,72 @@ async def _find_thread_message(channel, message_id): if str(message_id) == str(embed.author.url).split("/")[-1]: return msg + async def _grab_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") + if timeout is None: + return timeout + else: + try: + timeout = isodate.parse_duration(timeout) + except isodate.ISO8601Error: + logger.warning( + "The auto_close_thread limit needs to be a " + "ISO-8601 duration formatted duration string " + 'greater than 0 days, not "%s".', + str(timeout), + ) + del self.bot.config.cache["thread_auto_close"] + await self.bot.config.update() + timeout = None + return timeout + + async def _restart_close_timer(self): + """ + This will create or restart a timer to automatically close this + thread. + """ + timeout = await self._grab_timeout() + + # Exit if timeout was not set + if timeout is None: + return + + # Set timeout seconds + seconds = timeout.total_seconds() + # seconds = 20 # Uncomment to debug with just 20 seconds + reset_time = datetime.utcnow() + timedelta(seconds=seconds) + human_time = human_timedelta(dt=reset_time) + + # Grab message + close_message = self.bot.config.get( + "thread_auto_close_response", + f"This thread has been closed automatically after no response from" + f" you for {human_time}." + ) + time_marker_regex = "%t" + if len(re.findall(time_marker_regex, close_message)) == 1: + close_message = re.sub(time_marker_regex, str(human_time), close_message) + elif len(re.findall(time_marker_regex, close_message)) > 1: + logger.warning( + "The thread_auto_close_response should only contain one" + f" '{time_marker_regex}' to specify time." + ) + + if self._auto_close_task: + self._auto_close_task.cancel() + self._auto_close_task = self.bot.loop.call_later( + seconds, self._close_after, self.bot.user, False, True, + close_message + ) + 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), @@ -422,6 +495,8 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None ) ) + await self._restart_close_timer() + if self.close_task is not None: # Cancel closing if a thread message is sent. await self.cancel_closure() @@ -464,6 +539,10 @@ async def send( if not from_mod and not note: self.bot.loop.create_task(self.bot.api.append_log(message, self.channel.id)) + # Cancel auto closing if we get a new message from user + if self._auto_close_task: + self._auto_close_task.cancel() + destination = destination or self.channel author = message.author From 6e054497356f4db43db6c2ecae677c986d5d023c Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 24 Jun 2019 14:59:06 +1000 Subject: [PATCH 0271/1221] Send channel creation error to log channel - closes #303 --- CHANGELOG.md | 1 + core/thread.py | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da7e959818..8835b072f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Sponsors command that will list sponsors. +- An alert will now be sent to the log channel if a thread channel fails to create. This could be due to a variety of problems such as insufficient permissions or the category channel limit is met. ### Changed - Channel names now can contain unicode characters. diff --git a/core/thread.py b/core/thread.py index ea743f9b6f..b0e3c951c1 100644 --- a/core/thread.py +++ b/core/thread.py @@ -100,12 +100,25 @@ async def setup(self, *, creator=None, category=None): if category is not None: overwrites = None - channel = await self.bot.modmail_guild.create_text_channel( - name=self.manager.format_channel_name(recipient), - category=category, - overwrites=overwrites, - reason="Creating a thread channel", - ) + try: + channel = await self.bot.modmail_guild.create_text_channel( + name=self.manager.format_channel_name(recipient), + category=category, + overwrites=overwrites, + reason="Creating a thread channel", + ) + except discord.HTTPException as e: # Failed to create due to 50 channel limit. + del self.manager.cache[self.id] + log_channel = self.bot.log_channel + + em = discord.Embed(color=discord.Color.red()) + em.title = 'Error while trying to create a thread' + em.description = e.message + em.add_field(name='Recipient', value=recipient.mention) + + if log_channel is not None: + return await log_channel.send(embed=em) + self._channel = channel From fd639722ff52ac22ab04c1a113c8ca14ba43e636 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Tue, 25 Jun 2019 10:30:16 +1000 Subject: [PATCH 0272/1221] Small bug fix --- core/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/utils.py b/core/utils.py index 87d2e0a16b..bf88af1bff 100644 --- a/core/utils.py +++ b/core/utils.py @@ -79,7 +79,7 @@ def format_preview(messages: typing.List[typing.Dict[str, typing.Any]]): if message.get("type") in ("note", "internal"): continue author = message["author"] - content = message["content"].replace("\n", " ") + content = str(message["content"]).replace("\n", " ") name = author["name"] + "#" + str(author["discriminator"]) prefix = "[M]" if author["mod"] else "[R]" out += truncate(f"`{prefix} {name}:` {content}", max=75) + "\n" From 21ec0dab271420739a77d14f9031fd76390ef2c1 Mon Sep 17 00:00:00 2001 From: Vivek Joshy Date: Tue, 25 Jun 2019 21:05:08 +0530 Subject: [PATCH 0273/1221] Refactor _grab_timeout to _fetch_timeout Reason: https://github.com/kyb3r/modmail/pull/302#discussion_r296491229 --- core/thread.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/thread.py b/core/thread.py index 5ca0891943..c9a716157e 100644 --- a/core/thread.py +++ b/core/thread.py @@ -343,9 +343,7 @@ async def _find_thread_message(channel, message_id): if str(message_id) == str(embed.author.url).split("/")[-1]: return msg - async def _grab_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. From b5f7d06a7d754e7778afba5f29954e056a50ce13 Mon Sep 17 00:00:00 2001 From: Vivek Joshy Date: Tue, 25 Jun 2019 21:10:49 +0530 Subject: [PATCH 0274/1221] Format changed files using black Reason: https://github.com/kyb3r/modmail/pull/302#discussion_r296549859 --- core/thread.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/thread.py b/core/thread.py index c9a716157e..e8bf5c87e7 100644 --- a/core/thread.py +++ b/core/thread.py @@ -343,7 +343,9 @@ 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. @@ -389,7 +391,7 @@ async def _restart_close_timer(self): close_message = self.bot.config.get( "thread_auto_close_response", f"This thread has been closed automatically after no response from" - f" you for {human_time}." + f" you for {human_time}.", ) time_marker_regex = "%t" if len(re.findall(time_marker_regex, close_message)) == 1: @@ -403,8 +405,7 @@ async def _restart_close_timer(self): if self._auto_close_task: self._auto_close_task.cancel() self._auto_close_task = self.bot.loop.call_later( - seconds, self._close_after, self.bot.user, False, True, - close_message + seconds, self._close_after, self.bot.user, False, True, close_message ) async def edit_message(self, message_id: int, message: str) -> None: From d11f65d00be4bed02649b75bd33f9ec334753299 Mon Sep 17 00:00:00 2001 From: Vivek Joshy Date: Tue, 25 Jun 2019 21:13:40 +0530 Subject: [PATCH 0275/1221] Change thread auto close message Reason: https://github.com/kyb3r/modmail/pull/302#discussion_r296491362 --- core/thread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index e8bf5c87e7..fbef70cf9b 100644 --- a/core/thread.py +++ b/core/thread.py @@ -390,8 +390,8 @@ async def _restart_close_timer(self): # Grab message close_message = self.bot.config.get( "thread_auto_close_response", - f"This thread has been closed automatically after no response from" - f" you for {human_time}.", + f"This thread has been closed automatically due to inactivity " + f"after {human_time}.", ) time_marker_regex = "%t" if len(re.findall(time_marker_regex, close_message)) == 1: From dcf6fe63f8e5832d79fef7696405b368e011287c Mon Sep 17 00:00:00 2001 From: Vivek Joshy Date: Tue, 25 Jun 2019 21:22:46 +0530 Subject: [PATCH 0276/1221] Update changelog to reflect new config options --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da7e959818..ceb78b34e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Sponsors command that will list sponsors. +- Threads will close automatically after some time when `thread_auto_close` is set. +- Custom closing message can be set with `thread_auto_close_response`. + ### Changed - Channel names now can contain unicode characters. From fc95df377c2247df07a834ead0577a67274f6674 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 26 Jun 2019 09:26:51 +1000 Subject: [PATCH 0277/1221] Remove update functionality and github commands. (#304) * Remove autoupdate functionality * Remove update command * Remove github access token required * Update CHANGELOG.md * Remove github command --- CHANGELOG.md | 8 +++- bot.py | 62 ----------------------------- cogs/utility.py | 99 +--------------------------------------------- core/decorators.py | 19 +-------- 4 files changed, 10 insertions(+), 178 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c529373ec..2b30408d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# [Unreleased] +# [Unreleased] (3.0.0) ### Added @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Threads will close automatically after some time when `thread_auto_close` is set. - Custom closing message can be set with `thread_auto_close_response`. +### Removed + +- Removed autoupdate functionality and the `update` command in favour of the [Pull app](https://github.com/apps/pull). + +Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/updating) + ### Changed - Channel names now can contain unicode characters. - Debug logs are now located in a unique file for each bot. (Internal change) diff --git a/bot.py b/bot.py index 116735d6af..6cdd8f4522 100644 --- a/bot.py +++ b/bot.py @@ -74,7 +74,6 @@ def __init__(self): self.plugin_db = PluginDatabaseClient(self) self.metadata_task = self.loop.create_task(self.metadata_loop()) - self.autoupdate_task = self.loop.create_task(self.autoupdate_loop()) self._load_extensions() @property @@ -192,11 +191,6 @@ def run(self, *args, **kwargs): self.loop.run_until_complete(self.metadata_task) except asyncio.CancelledError: logger.debug(info("data_task has been cancelled.")) - try: - self.autoupdate_task.cancel() - self.loop.run_until_complete(self.autoupdate_task) - except asyncio.CancelledError: - logger.debug(info("autoupdate_task has been cancelled.")) self.loop.run_until_complete(self.logout()) for task in asyncio.Task.all_tasks(): @@ -904,62 +898,6 @@ async def validate_database_connection(self): else: logger.info(info("Successfully connected to the database.")) - async def autoupdate_loop(self): - await self.wait_until_ready() - - if self.config.get("disable_autoupdates"): - logger.warning(info("Autoupdates disabled.")) - logger.info(LINE) - return - - if not self.config.get("github_access_token"): - logger.warning(info("GitHub access token not found.")) - logger.warning(info("Autoupdates disabled.")) - logger.info(LINE) - return - - logger.info(info("Autoupdate loop started.")) - - while not self.is_closed(): - changelog = await Changelog.from_url(self) - latest = changelog.latest_version - - if parse_version(self.version) < parse_version(latest.version): - data = await self.api.update_repository() - - embed = discord.Embed(color=discord.Color.green()) - - commit_data = data["data"] - user = data["user"] - embed.set_author( - name=user["username"] + " - Updating Bot", - icon_url=user["avatar_url"], - url=user["url"], - ) - - embed.set_footer( - text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" - ) - - embed.description = latest.description - for name, value in latest.fields.items(): - embed.add_field(name=name, value=value) - - if commit_data: - message = commit_data["commit"]["message"] - html_url = commit_data["html_url"] - short_sha = commit_data["sha"][:6] - embed.add_field( - name="Merge Commit", - value=f"[`{short_sha}`]({html_url}) " - f"{message} - {user['username']}", - ) - logger.info(info("Bot has been updated.")) - channel = self.log_channel - await channel.send(embed=embed) - - await asyncio.sleep(3600) - async def metadata_loop(self): await self.wait_until_ready() if not self.guild: diff --git a/cogs/utility.py b/cogs/utility.py index 4d70096cb3..c88339cf55 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -22,7 +22,7 @@ from core import checks from core.changelog import Changelog -from core.decorators import github_access_token_required, trigger_typing +from core.decorators import trigger_typing from core.models import InvalidConfigError, PermissionLevel from core.paginator import PaginatorSession, MessagePaginatorSession from core.utils import cleanup_code, info, error, User, get_perm_level @@ -404,102 +404,7 @@ async def debug_clear(self, ctx): color=self.bot.main_color, description="Cached logs are now cleared." ) ) - - @commands.command() - @checks.has_permissions(PermissionLevel.OWNER) - @github_access_token_required - @trigger_typing - async def github(self, ctx): - """Shows the GitHub user your Github_Access_Token is linked to.""" - data = await self.bot.api.get_user_info() - - embed = Embed( - title="GitHub", description="Current User", color=self.bot.main_color - ) - user = data["user"] - embed.set_author( - name=user["username"], icon_url=user["avatar_url"], url=user["url"] - ) - embed.set_thumbnail(url=user["avatar_url"]) - await ctx.send(embed=embed) - - @commands.command() - @checks.has_permissions(PermissionLevel.OWNER) - @github_access_token_required - @trigger_typing - async def update(self, ctx, *, flag: str = ""): - """ - Update Modmail. - - This only works for Heroku users who have configured their bot for updates. - - To stay up-to-date with the latest commit - from GitHub, specify "force" as the flag. - """ - - changelog = await Changelog.from_url(self.bot) - latest = changelog.latest_version - - desc = ( - f"The latest version is [`{self.bot.version}`]" - "(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)" - ) - - if ( - parse_version(self.bot.version) >= parse_version(latest.version) - and flag.lower() != "force" - ): - embed = Embed( - title="Already up to date", description=desc, color=self.bot.main_color - ) - - data = await self.bot.api.get_user_info() - if not data.get("error"): - user = data["user"] - embed.set_author( - name=user["username"], icon_url=user["avatar_url"], url=user["url"] - ) - else: - data = await self.bot.api.update_repository() - - commit_data = data["data"] - user = data["user"] - - if commit_data: - embed = Embed(color=self.bot.main_color) - - embed.set_footer( - text=f"Updating Modmail v{self.bot.version} " - f"-> v{latest.version}" - ) - - embed.set_author( - name=user["username"] + " - Updating bot", - icon_url=user["avatar_url"], - url=user["url"], - ) - - embed.description = latest.description - for name, value in latest.fields.items(): - embed.add_field(name=name, value=value) - # message = commit_data['commit']['message'] - html_url = commit_data["html_url"] - short_sha = commit_data["sha"][:6] - embed.add_field( - name="Merge Commit", value=f"[`{short_sha}`]({html_url})" - ) - else: - embed = Embed( - title="Already up to date with master repository.", - description="No further updates required", - color=self.bot.main_color, - ) - embed.set_author( - name=user["username"], icon_url=user["avatar_url"], url=user["url"] - ) - - return await ctx.send(embed=embed) - + @commands.command(aliases=["presence"]) @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): diff --git a/core/decorators.py b/core/decorators.py index 03deb09e61..c87f34129b 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -10,21 +10,4 @@ async def wrapper(self, ctx: commands.Context, *args, **kwargs): await ctx.trigger_typing() return await func(self, ctx, *args, **kwargs) - return wrapper - - -def github_access_token_required(func): - @functools.wraps(func) - async def wrapper(self, ctx: commands.Context, *args, **kwargs): - if self.bot.config.get("github_access_token"): - return await func(self, ctx, *args, **kwargs) - - desc = ( - "You can only use this command if you have a " - "configured `GITHUB_ACCESS_TOKEN`. Get a " - "personal access token from developer settings." - ) - embed = Embed(color=Color.red(), title="Unauthorized", description=desc) - await ctx.send(embed=embed) - - return wrapper + return wrapper \ No newline at end of file From 0d19bf4c05ea26572e9e0523cd5dc6b640bfd481 Mon Sep 17 00:00:00 2001 From: Kyber Date: Wed, 26 Jun 2019 09:30:41 +1000 Subject: [PATCH 0278/1221] Increment version (#305) * Increment version * Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- bot.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b30408d8f..acc2d15437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# [Unreleased] (3.0.0) +# v3.0.0 ### Added @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Threads will close automatically after some time when `thread_auto_close` is set. - Custom closing message can be set with `thread_auto_close_response`. -### Removed +### Breaking Changes - Removed autoupdate functionality and the `update` command in favour of the [Pull app](https://github.com/apps/pull). diff --git a/bot.py b/bot.py index 6cdd8f4522..56a0c6a6aa 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "2.24.1" +__version__ = "3.0.0" import asyncio import logging From 79e25f78a4a662003defe452110de589a1183e02 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 10:07:55 +1000 Subject: [PATCH 0279/1221] Fix attribute error --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index a0a3cbcc4e..62b5ae917f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -388,7 +388,7 @@ async def _restart_close_timer(self): This will create or restart a timer to automatically close this thread. """ - timeout = await self._grab_timeout() + timeout = await self._fetch_timeout() # Exit if timeout was not set if timeout is None: From 3420571922663f98a0adf8daf2c4f666f61d6423 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 10:23:31 +1000 Subject: [PATCH 0280/1221] Use existing functions for closing --- core/thread.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/thread.py b/core/thread.py index 62b5ae917f..4103b717bd 100644 --- a/core/thread.py +++ b/core/thread.py @@ -415,11 +415,7 @@ async def _restart_close_timer(self): f" '{time_marker_regex}' to specify time." ) - if self._auto_close_task: - self._auto_close_task.cancel() - self._auto_close_task = self.bot.loop.call_later( - seconds, self._close_after, self.bot.user, False, True, close_message - ) + await self.close(closer=self.bot.user, after=seconds, message=close_message) async def edit_message(self, message_id: int, message: str) -> None: recipient_msg, channel_msg = await asyncio.gather( From 77fb37b0537c6642de620f9d3afbfa0c2ccae103 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 10:29:23 +1000 Subject: [PATCH 0281/1221] Restart close timer after cancel closure --- core/thread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index 4103b717bd..fc8936f03f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -503,9 +503,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None ) ) - await self._restart_close_timer() - if self.close_task is not None: # Cancel closing if a thread message is sent. await self.cancel_closure() tasks.append( @@ -516,6 +514,8 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None ) ) ) + + await self._restart_close_timer() await asyncio.gather(*tasks) From 29495ac69fe724f34848b959fd83e5b720e955c6 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 10:42:49 +1000 Subject: [PATCH 0282/1221] Use a seperate auto close task --- core/thread.py | 52 ++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/core/thread.py b/core/thread.py index fc8936f03f..17bd902a95 100644 --- a/core/thread.py +++ b/core/thread.py @@ -40,8 +40,8 @@ def __init__( self._channel = channel self.genesis_message = None self._ready_event = asyncio.Event() - self._close_task = None - self._auto_close_task = None + self.close_task = None + self.auto_close_task = None def __repr__(self): return ( @@ -57,14 +57,6 @@ async def wait_until_ready(self) -> None: def id(self) -> int: return self._id - @property - def close_task(self) -> asyncio.TimerHandle: - return self._close_task - - @close_task.setter - def close_task(self, val: asyncio.TimerHandle): - self._close_task = val - @property def channel(self) -> typing.Union[discord.TextChannel, discord.DMChannel]: return self._channel @@ -199,11 +191,12 @@ async def close( silent: bool = False, delete_channel: bool = True, message: str = None, + auto_close: bool = False ) -> None: """Close a thread now or after a set time in seconds""" # restarts the after timer - await self.cancel_closure() + await self.cancel_closure(auto_close) if after > 0: # TODO: Add somewhere to clean up broken closures @@ -221,9 +214,14 @@ async def close( self.bot.config.closures[str(self.id)] = items await self.bot.config.update() - self.close_task = self.bot.loop.call_later( + task = self.bot.loop.call_later( after, self._close_after, closer, silent, delete_channel, message ) + + if auto_close: + self.auto_close_task = task + else: + self.close_task = task else: await self._close(closer, silent, delete_channel, message) @@ -235,8 +233,6 @@ async def _close( await self.cancel_closure() # Cancel auto closing the thread if closed by any means. - if self._auto_close_task: - self._auto_close_task.cancel() if str(self.id) in self.bot.config.subscriptions: del self.bot.config.subscriptions[str(self.id)] @@ -337,10 +333,14 @@ async def _close( await asyncio.gather(*tasks) - async def cancel_closure(self) -> None: - if self.close_task is not None: + async def cancel_closure(self, auto_close: bool = False) -> None: + + if self.close_task is not None and not auto_close: self.close_task.cancel() self.close_task = None + elif self.auto_close_task is not None: + self.auto_close_task.cancel() + self.auto_close_task = None to_update = self.bot.config.closures.pop(str(self.id), None) if to_update is not None: @@ -415,7 +415,7 @@ async def _restart_close_timer(self): f" '{time_marker_regex}' to specify time." ) - await self.close(closer=self.bot.user, after=seconds, message=close_message) + await self.close(closer=self.bot.user, after=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( @@ -505,15 +505,16 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None # Cancel closing if a thread message is sent. - await self.cancel_closure() - tasks.append( - self.channel.send( - embed=discord.Embed( - color=discord.Color.red(), - description="Scheduled close has been cancelled.", + if self.close_task is not None: + await self.cancel_closure() + tasks.append( + self.channel.send( + embed=discord.Embed( + color=discord.Color.red(), + description="Scheduled close has been cancelled.", + ) ) ) - ) await self._restart_close_timer() @@ -547,9 +548,6 @@ async def send( if not from_mod and not note: self.bot.loop.create_task(self.bot.api.append_log(message, self.channel.id)) - # Cancel auto closing if we get a new message from user - if self._auto_close_task: - self._auto_close_task.cancel() destination = destination or self.channel From 288e13f3a1642e177ac16d80d9c1397b608d0d29 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 10:49:49 +1000 Subject: [PATCH 0283/1221] Fix place where auto_close_task gets created --- core/thread.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index 17bd902a95..c207ee47be 100644 --- a/core/thread.py +++ b/core/thread.py @@ -515,8 +515,6 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None ) ) ) - - await self._restart_close_timer() await asyncio.gather(*tasks) @@ -530,6 +528,9 @@ async def send( note: bool = False, anonymous: bool = False, ) -> None: + + self.bot.loop.create_task(self._restart_close_timer()) # Start or restart thread auto close + if self.close_task is not None: # cancel closing if a thread message is sent. self.bot.loop.create_task(self.cancel_closure()) From 97564255a017c19b7b11943b6d080f6323b6e597 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 10:55:01 +1000 Subject: [PATCH 0284/1221] Close cancel also cancels auto close --- cogs/modmail.py | 2 +- core/thread.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 15f01f99ac..09520d3a84 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -252,7 +252,7 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): if cancel: if thread.close_task is not None: - await thread.cancel_closure() + await thread.cancel_closure(all=True) embed = discord.Embed( color=discord.Color.red(), description="Scheduled close has been cancelled.", diff --git a/core/thread.py b/core/thread.py index c207ee47be..66bcb24855 100644 --- a/core/thread.py +++ b/core/thread.py @@ -102,18 +102,17 @@ 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 50 channel limit. del self.manager.cache[self.id] log_channel = self.bot.log_channel em = discord.Embed(color=discord.Color.red()) - em.title = 'Error while trying to create a thread' + em.title = "Error while trying to create a thread" em.description = e.message - em.add_field(name='Recipient', value=recipient.mention) + em.add_field(name="Recipient", value=recipient.mention) if log_channel is not None: return await log_channel.send(embed=em) - self._channel = channel @@ -191,7 +190,7 @@ async def close( silent: bool = False, delete_channel: bool = True, message: str = None, - auto_close: bool = False + auto_close: bool = False, ) -> None: """Close a thread now or after a set time in seconds""" @@ -333,12 +332,11 @@ async def _close( await asyncio.gather(*tasks) - async def cancel_closure(self, auto_close: bool = False) -> None: - - if self.close_task is not None and not auto_close: + 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 both: self.close_task.cancel() self.close_task = None - elif self.auto_close_task is not None: + if self.auto_close_task is not None and auto_close or both: self.auto_close_task.cancel() self.auto_close_task = None @@ -415,7 +413,9 @@ async def _restart_close_timer(self): f" '{time_marker_regex}' to specify time." ) - await self.close(closer=self.bot.user, after=seconds, message=close_message, auto_close=True) + await self.close( + closer=self.bot.user, after=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( @@ -503,7 +503,6 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None ) ) - # Cancel closing if a thread message is sent. if self.close_task is not None: await self.cancel_closure() @@ -529,7 +528,9 @@ async def send( anonymous: bool = False, ) -> None: - self.bot.loop.create_task(self._restart_close_timer()) # Start or restart thread auto close + self.bot.loop.create_task( + self._restart_close_timer() + ) # Start or restart thread auto close if self.close_task is not None: # cancel closing if a thread message is sent. @@ -549,7 +550,6 @@ async def send( if not from_mod and not note: self.bot.loop.create_task(self.bot.api.append_log(message, self.channel.id)) - destination = destination or self.channel author = message.author From 77e8718431e66dcc18183651907206919781ca51 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 10:55:10 +1000 Subject: [PATCH 0285/1221] Enforce black code style --- cogs/utility.py | 8 ++++---- core/decorators.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index c88339cf55..f4d50c5042 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -85,14 +85,14 @@ async def send_bot_help(self, cogs): cogs = list(filter(None, cogs)) bot = self.context.bot - + # always come first - default_cogs = [ + 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) for cog in default_cogs: @@ -404,7 +404,7 @@ async def debug_clear(self, ctx): color=self.bot.main_color, description="Cached logs are now cleared." ) ) - + @commands.command(aliases=["presence"]) @checks.has_permissions(PermissionLevel.ADMINISTRATOR) async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): diff --git a/core/decorators.py b/core/decorators.py index c87f34129b..6e27dd2292 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -10,4 +10,4 @@ async def wrapper(self, ctx: commands.Context, *args, **kwargs): await ctx.trigger_typing() return await func(self, ctx, *args, **kwargs) - return wrapper \ No newline at end of file + return wrapper From bea7ae2aaa434350303c6cb982be039c05aa5019 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 10:58:48 +1000 Subject: [PATCH 0286/1221] Fix name error --- core/thread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index 66bcb24855..ee8c853fd8 100644 --- a/core/thread.py +++ b/core/thread.py @@ -333,10 +333,10 @@ async def _close( await asyncio.gather(*tasks) 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 both: + if self.close_task is not None and not auto_close or all: self.close_task.cancel() self.close_task = None - if self.auto_close_task is not None and auto_close or both: + if self.auto_close_task is not None and auto_close or all: self.auto_close_task.cancel() self.auto_close_task = None From 5334d3ab02467a2c58d9c448d39b5b843ca75f91 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 11:01:10 +1000 Subject: [PATCH 0287/1221] Check for existence of auto_close_task --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 09520d3a84..0d770437b2 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -251,7 +251,7 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): if cancel: - if thread.close_task is not None: + if thread.close_task is not None or thread.auto_close_task is not None: await thread.cancel_closure(all=True) embed = discord.Embed( color=discord.Color.red(), From 3b39cf2fc4a74e2e5c6a0a8db448672698f3de1c Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 11:05:16 +1000 Subject: [PATCH 0288/1221] Add auto_close info in closure --- bot.py | 1 + core/thread.py | 1 + 2 files changed, 2 insertions(+) diff --git a/bot.py b/bot.py index 56a0c6a6aa..2f9c7e455f 100644 --- a/bot.py +++ b/bot.py @@ -398,6 +398,7 @@ async def on_ready(self): silent=items["silent"], delete_channel=items["delete_channel"], message=items["message"], + auto_close=items.get("auto_close", False) ) logger.info(LINE) diff --git a/core/thread.py b/core/thread.py index ee8c853fd8..d4da5a4321 100644 --- a/core/thread.py +++ b/core/thread.py @@ -209,6 +209,7 @@ async def close( "silent": silent, "delete_channel": delete_channel, "message": message, + "auto_close": auto_close } self.bot.config.closures[str(self.id)] = items await self.bot.config.update() From 7c60274f0ba4555f0270914ccb20cffe2eee40fb Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 11:09:21 +1000 Subject: [PATCH 0289/1221] Add brackets --- core/thread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index d4da5a4321..155cdde715 100644 --- a/core/thread.py +++ b/core/thread.py @@ -334,10 +334,10 @@ async def _close( await asyncio.gather(*tasks) 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: + if self.close_task is not None and (not auto_close or all): self.close_task.cancel() self.close_task = None - if self.auto_close_task is not None and auto_close or all: + if self.auto_close_task is not None and (auto_close or all): self.auto_close_task.cancel() self.auto_close_task = None From 5767ec37324183a5475575126e57642251772aeb Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 11:14:35 +1000 Subject: [PATCH 0290/1221] Cancel all tasks when finally closing --- CHANGELOG.md | 7 +++++++ core/thread.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acc2d15437..ac57fdbc86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v3.0.1 + +### Fixed + +- A lot of bugs with `thread_auto_close` 😅 + + # v3.0.0 ### Added diff --git a/core/thread.py b/core/thread.py index 155cdde715..76deb5995b 100644 --- a/core/thread.py +++ b/core/thread.py @@ -230,7 +230,7 @@ async def _close( ): del self.manager.cache[self.id] - await self.cancel_closure() + await self.cancel_closure(all=True) # Cancel auto closing the thread if closed by any means. From c0e44dc84c7ce92bfeac5a8e834816bd546324da Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 26 Jun 2019 16:54:07 +1000 Subject: [PATCH 0291/1221] Bump version --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 2f9c7e455f..b2ce3c3f79 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.0.0" +__version__ = "3.0.1" import asyncio import logging From afed28a43f4025c00d495f08288aded41ee50e87 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Wed, 26 Jun 2019 18:46:32 -0700 Subject: [PATCH 0292/1221] Update registry.json --- plugins/registry.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index b9151b2c69..5c3e6690bb 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -146,7 +146,7 @@ "backupdb": { "repository": "officialpiyush/modmail-plugins", "branch": "master", - "description": "Backup you're current modmail DB with a single command!\n\n**Requires `BACKUP_MONGO_URI` in either config.json or environment variables**", + "description": "Backup you're current Modmail DB with a single command!\n\n**Requires `BACKUP_MONGO_URI` in either config.json or environment variables**", "bot_version": "2.20.1", "title": "Backup Database (backupdb)", "icon_url": "https://images.ionadev.ml/b/nKAlOC4.jpg", @@ -160,11 +160,20 @@ "title": "Colors!!", "icon_url": "https://cdn1.iconfinder.com/data/icons/weather-19/32/rainbow-512.png", "thumbnail_url": "https://i.imgur.com/fSxnc9W.jpg" + }, + "logger": { + "repository": "Taaku18/modmail-plugins", + "branch": "master", + "description": "Moderation logger, logs message edits, message delete, channel changes, member join, etc.", + "bot_version": "2.20.1", + "title": "Server Activity Logger", + "icon_url": "https://i.imgur.com/dRqPq5M.png", + "thumbnail_url": "https://i.imgur.com/moQn7RC.png" }, "fun": { "repository": "TheKinG2149/modmail-plugins", "branch": "master", - "description": "Some fun commands like 8ball,dadjokes", + "description": "Some fun commands like 8ball, dadjokes", "bot_version": "2.24.1", "title": "Fun", "icon_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png", @@ -173,7 +182,7 @@ "stats": { "repository": "MiTonder/modmail-plugins", "branch": "master", - "description": "Get useful stats directly in an embed about either the ModMail bot, a user or the server.", + "description": "Get useful stats directly in an embed about either the Modmail bot, a user or the server.", "bot_version": "2.24.1", "title": "Get Stats", "icon_url": "https://image.flaticon.com/icons/png/512/117/117761.png", From d9c14cc40fcdb1d2b8409b30d6a89814c1e0b63e Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 30 Jun 2019 10:20:17 -0700 Subject: [PATCH 0293/1221] v3.0.2 --- bot.py | 25 +++++++++++++--- cogs/modmail.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++--- core/config.py | 2 ++ core/thread.py | 2 +- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/bot.py b/bot.py index b2ce3c3f79..0d99aafb19 100644 --- a/bot.py +++ b/bot.py @@ -398,7 +398,7 @@ async def on_ready(self): silent=items["silent"], delete_channel=items["delete_channel"], message=items["message"], - auto_close=items.get("auto_close", False) + auto_close=items.get("auto_close", False), ) logger.info(LINE) @@ -440,9 +440,22 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: return sent_emoji, blocked_emoji - async def process_modmail(self, message: discord.Message) -> None: - """Processes messages sent to the bot.""" + async def _process_blocked(self, message: discord.Message) -> bool: sent_emoji, blocked_emoji = await self.retrieve_emoji() + + if str(message.author.id) in self.config.blocked_whitelist: + if str(message.author.id) in self.blocked_users: + del self.config.blocked[str(message.author.id)] + await self.config.update() + + if sent_emoji != "disable": + try: + await message.add_reaction(sent_emoji) + except (discord.HTTPException, discord.InvalidArgument): + pass + + return False + now = datetime.utcnow() account_age = self.config.get("account_age") @@ -579,8 +592,12 @@ async def process_modmail(self, message: discord.Message) -> None: await message.add_reaction(reaction) except (discord.HTTPException, discord.InvalidArgument): pass + return str(message.author.id) in self.blocked_users - if str(message.author.id) not in self.blocked_users: + async def process_modmail(self, message: discord.Message) -> None: + """Processes messages sent to the bot.""" + blocked = await self._process_blocked(message) + if not blocked: thread = await self.threads.find_or_create(message.author) await thread.send(message) diff --git a/cogs/modmail.py b/cogs/modmail.py index 0d770437b2..db12961ec8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -759,7 +759,7 @@ async def contact( await ctx.send(embed=embed) - @commands.command() + @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing async def blocked(self, ctx): @@ -804,6 +804,63 @@ async def blocked(self, ctx): await PaginatorSession(ctx, *embeds).run() + @blocked.command(name="whitelist") + @checks.has_permissions(PermissionLevel.MODERATOR) + @trigger_typing + async def blocked_whitelist(self, ctx, *, user: User = None): + """ + Whitelist or un-whitelist a user from getting blocked. + + Useful for preventing users from getting blocked by account_age/guild_age restrictions. + """ + if user is None: + thread = ctx.thread + if thread: + user = thread.recipient + else: + return await ctx.send_help(ctx.command) + + mention = getattr(user, "mention", f"`{user.id}`") + msg = "" + + if str(user.id) in self.bot.config.blocked_whitelist: + embed = discord.Embed( + title="Success", + description=f"{mention} is no longer whitelisted.", + color=self.bot.main_color, + ) + self.bot.config.blocked_whitelist.remove(str(user.id)) + return await ctx.send(embed=embed) + + self.bot.config.blocked_whitelist.append(str(user.id)) + + if str(user.id) in self.bot.blocked_users: + msg = self.bot.blocked_users.get(str(user.id)) + if msg is None: + msg = "" + del self.bot.config.blocked[str(user.id)] + + await self.bot.config.update() + + if msg.startswith("System Message: "): + # If the user is blocked internally (for example: below minimum account age) + # Show an extended message stating the original internal message + reason = msg[16:].strip().rstrip(".") or "no reason" + embed = discord.Embed( + title="Success", + description=f"{mention} was previously blocked internally due to " + f'"{reason}". {mention} is now whitelisted.', + color=self.bot.main_color, + ) + else: + embed = discord.Embed( + title="Success", + color=self.bot.main_color, + description=f"{mention} is now whitelisted.", + ) + + return await ctx.send(embed=embed) + @commands.command() @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing @@ -832,6 +889,16 @@ async def block( else: raise commands.BadArgument(f'User "{after.arg}" not found') + mention = getattr(user, "mention", f"`{user.id}`") + + if str(user.id) in self.bot.config.blocked_whitelist: + embed = discord.Embed( + title="Error", + description=f"Cannot block {mention}, user is whitelisted.", + color=discord.Color.red(), + ) + return await ctx.send(embed=embed) + if after is not None: reason = after.arg if reason.startswith("System Message: "): @@ -846,8 +913,6 @@ async def block( if not reason: reason = None - mention = user.mention if hasattr(user, "mention") else f"`{user.id}`" - extend = f" for `{reason}`" if reason is not None else "" msg = self.bot.blocked_users.get(str(user.id), "") @@ -901,7 +966,7 @@ async def unblock(self, ctx, *, user: User = None): else: raise commands.MissingRequiredArgument(param(name="user")) - mention = user.mention if hasattr(user, "mention") else f"`{user.id}`" + mention = getattr(user, "mention", f"`{user.id}`") if str(user.id) in self.bot.blocked_users: msg = self.bot.blocked_users.get(str(user.id)) @@ -920,6 +985,11 @@ async def unblock(self, ctx, *, user: User = None): f'"{reason}". {mention} is no longer blocked.', color=self.bot.main_color, ) + embed.set_footer( + text="However, if the original system block reason still apply, " + f"{mention} will be automatically blocked again. Use " + f'"{self.bot.prefix}blocked whitelist {mention}" to whitelist the user.' + ) else: embed = discord.Embed( title="Success", diff --git a/core/config.py b/core/config.py index 325f31c9d3..32f9dacf79 100644 --- a/core/config.py +++ b/core/config.py @@ -62,6 +62,7 @@ class ConfigManager: "oauth_whitelist", # moderation "blocked", + "blocked_whitelist", "command_permissions", "level_permissions", # threads @@ -126,6 +127,7 @@ def populate_cache(self) -> dict: "plugins": [], "aliases": {}, "blocked": {}, + "blocked_whitelist": [], "oauth_whitelist": [], "command_permissions": {}, "level_permissions": {}, diff --git a/core/thread.py b/core/thread.py index 76deb5995b..ad37f4d05d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -209,7 +209,7 @@ async def close( "silent": silent, "delete_channel": delete_channel, "message": message, - "auto_close": auto_close + "auto_close": auto_close, } self.bot.config.closures[str(self.id)] = items await self.bot.config.update() From 50257af18b6f86f24b8f8bc77e6a055f79f08157 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 30 Jun 2019 10:27:23 -0700 Subject: [PATCH 0294/1221] Removed oauth aliases --- CHANGELOG.md | 18 ++++++++++++++---- bot.py | 2 +- cogs/utility.py | 4 +--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac57fdbc86..9f35ec46f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v3.0.2 + +### Added + +- New command, `?blocked whitelist `, this command prevents users from getting blocked by any means. + +### Changed + +- Removed some aliases from `?oauth`. + # v3.0.1 ### Fixed @@ -22,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking Changes -- Removed autoupdate functionality and the `update` command in favour of the [Pull app](https://github.com/apps/pull). +- Removed autoupdate functionality and the `?update` command in favour of the [Pull app](https://github.com/apps/pull). Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/updating) @@ -33,7 +43,7 @@ Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/u ### Fixed - Editing notes now works, minor bug with edit command is fixed. -- Bug in the `oauth` command where the response message fails to send when an ID is provided. +- Bug in the `?oauth` command where the response message fails to send when an ID is provided. - Plugin requirement installation now works in virtual environments @@ -41,13 +51,13 @@ Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/u ### Fixed -Fixed a bug with branches and `plugin update`. +Fixed a bug with branches and `?plugin update`. # v2.24.0 ### Added -Branch support for `plugin add` and in registry. Typically for developers. +Branch support for `?plugin add` and in registry. Typically for developers. # v2.23.0 diff --git a/bot.py b/bot.py index 0d99aafb19..6e84a6f9c4 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.0.1" +__version__ = "3.0.2" import asyncio import logging diff --git a/cogs/utility.py b/cogs/utility.py index f4d50c5042..1f10c8bbed 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1248,9 +1248,7 @@ def get_level(perm_level): p_session = PaginatorSession(ctx, *embeds) return await p_session.run() - @commands.group( - invoke_without_command=True, aliases=["oauth2", "auth", "authentication"] - ) + @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def oauth(self, ctx): """Commands relating to Logviewer oauth2 login authentication. From 255f7327b6524a45db30d72e4b0362a9e6d61813 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 30 Jun 2019 10:47:27 -0700 Subject: [PATCH 0295/1221] v3.0.3 snippets/alias edit --- CHANGELOG.md | 8 +++++++ bot.py | 8 +++++-- cogs/modmail.py | 59 +++++++++++++++++++++++++++++++++++++------------ cogs/utility.py | 56 +++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 109 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f35ec46f8..c764130b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +# v3.0.3 + +### Added + +- New commands, `?alias edit ` and `?snippets edit `. + - They can be used to edit aliases and snippets respectively. + # v3.0.2 ### Added diff --git a/bot.py b/bot.py index 6e84a6f9c4..d2e1ff82cc 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.0.2" +__version__ = "3.0.3" import asyncio import logging @@ -271,6 +271,10 @@ def main_category(self) -> typing.Optional[discord.TextChannel]: def blocked_users(self) -> typing.Dict[str, str]: return self.config.get("blocked", {}) + @property + def blocked_whitelisted_users(self) -> typing.List[str]: + return self.config.get("blocked_whitelist", []) + @property def prefix(self) -> str: return self.config.get("prefix", "?") @@ -443,7 +447,7 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: async def _process_blocked(self, message: discord.Message) -> bool: sent_emoji, blocked_emoji = await self.retrieve_emoji() - if str(message.author.id) in self.config.blocked_whitelist: + if str(message.author.id) in self.blocked_whitelisted_users: if str(message.author.id) in self.blocked_users: del self.config.blocked[str(message.author.id)] await self.config.update() diff --git a/cogs/modmail.py b/cogs/modmail.py index db12961ec8..2c6d9e6068 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -147,15 +147,21 @@ async def snippets_add(self, ctx, name: str.lower, *, value): {prefix}snippets add "two word" this is a two word snippet. ``` """ + if name in self.bot.config.snippets: + embed = discord.Embed( + title="Error", + color=discord.Color.red(), + description=f"Snippet `{name}` already exists.", + ) + else: + self.bot.config.snippets[name] = value + await self.bot.config.update() - self.bot.config.snippets[name] = value - await self.bot.config.update() - - embed = discord.Embed( - title="Added snippet", - color=self.bot.main_color, - description=f"`{name}` points to: {value}", - ) + embed = discord.Embed( + title="Added snippet", + color=self.bot.main_color, + description=f'`{name}` will now send "{value}".', + ) await ctx.send(embed=embed) @@ -164,7 +170,7 @@ async def snippets_add(self, ctx, name: str.lower, *, value): async def snippets_remove(self, ctx, *, name: str.lower): """Remove a snippet.""" - if self.bot.config.snippets.get(name): + if name in self.bot.config.snippets: embed = discord.Embed( title="Removed snippet", color=self.bot.main_color, @@ -182,6 +188,27 @@ async def snippets_remove(self, ctx, *, name: str.lower): await ctx.send(embed=embed) + @snippets.command(name="edit") + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def snippets_edit(self, ctx, name: str.lower, *, value): + if name in self.bot.config.snippets: + self.bot.config.snippets[name] = value + await self.bot.config.update() + + embed = discord.Embed( + title="Edited snippet", + color=self.bot.main_color, + description=f'`{name}` will now send "{value}".', + ) + + else: + embed = discord.Embed( + title="Error", + color=discord.Color.red(), + description=f"Snippet `{name}` does not exist.", + ) + await ctx.send(embed=embed) + @commands.command() @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() @@ -193,7 +220,11 @@ async def move(self, ctx, *, category: discord.CategoryChannel): """ thread = ctx.thread await thread.channel.edit(category=category, sync_permissions=True) - await ctx.message.add_reaction("✅") + sent_emoji, _ = await self.bot.retrieve_emoji() + try: + await ctx.message.add_reaction(sent_emoji) + except (discord.HTTPException, discord.InvalidArgument): + pass @staticmethod async def send_scheduled_close_message(ctx, after, silent=False): @@ -823,16 +854,16 @@ async def blocked_whitelist(self, ctx, *, user: User = None): mention = getattr(user, "mention", f"`{user.id}`") msg = "" - if str(user.id) in self.bot.config.blocked_whitelist: + if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( title="Success", description=f"{mention} is no longer whitelisted.", color=self.bot.main_color, ) - self.bot.config.blocked_whitelist.remove(str(user.id)) + self.bot.blocked_whitelisted_users.remove(str(user.id)) return await ctx.send(embed=embed) - self.bot.config.blocked_whitelist.append(str(user.id)) + self.bot.blocked_whitelisted_users.append(str(user.id)) if str(user.id) in self.bot.blocked_users: msg = self.bot.blocked_users.get(str(user.id)) @@ -891,7 +922,7 @@ async def block( mention = getattr(user, "mention", f"`{user.id}`") - if str(user.id) in self.bot.config.blocked_whitelist: + if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( title="Error", description=f"Cannot block {mention}, user is whitelisted.", diff --git a/cogs/utility.py b/cogs/utility.py index 1f10c8bbed..b1743f5eb3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -838,7 +838,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): if "aliases" not in self.bot.config.cache: self.bot.config["aliases"] = {} - if self.bot.get_command(name) or self.bot.config.aliases.get(name): + if self.bot.get_command(name) or name in self.bot.config.aliases: embed = Embed( title="Error", color=Color.red(), @@ -847,14 +847,14 @@ async def alias_add(self, ctx, name: str.lower, *, value): ) return await ctx.send(embed=embed) - if not self.bot.get_command(value.split()[0]): + linked_command = value.split()[0] + if not self.bot.get_command(linked_command): embed = Embed( title="Error", color=Color.red(), description="The command you are attempting to point " - f"to does not exist: `{value.split()[0]}`.", + f"to does not exist: `{linked_command}`.", ) - return await ctx.send(embed=embed) self.bot.config.aliases[name] = value await self.bot.config.update() @@ -862,7 +862,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed = Embed( title="Added alias", color=self.bot.main_color, - description=f"`{name}` points to: {value}", + description=f'`{name}` points to: "{value}".', ) return await ctx.send(embed=embed) @@ -875,7 +875,7 @@ async def alias_remove(self, ctx, *, name: str.lower): if "aliases" not in self.bot.config.cache: self.bot.config["aliases"] = {} - if self.bot.config.aliases.get(name): + if name in self.bot.config.aliases: del self.bot.config["aliases"][name] await self.bot.config.update() @@ -894,6 +894,50 @@ async def alias_remove(self, ctx, *, name: str.lower): return await ctx.send(embed=embed) + @alias.command(name="edit") + @checks.has_permissions(PermissionLevel.MODERATOR) + async def alias_edit(self, ctx, name: str.lower, *, value): + if "aliases" not in self.bot.config.cache: + self.bot.config["aliases"] = {} + + if name not in self.bot.config.aliases: + embed = Embed( + title="Error", + color=Color.red(), + description=f"Alias `{name}` does not exist.", + ) + + return await ctx.send(embed=embed) + + if self.bot.get_command(name): + embed = Embed( + title="Error", + color=Color.red(), + description="A command or alias already exists " + f"with the same name: `{name}`.", + ) + return await ctx.send(embed=embed) + + linked_command = value.split()[0] + if not self.bot.get_command(linked_command): + embed = Embed( + title="Error", + color=Color.red(), + description="The command you are attempting to point " + f"to does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) + + self.bot.config.aliases[name] = value + await self.bot.config.update() + + embed = Embed( + title="Edited alias", + color=self.bot.main_color, + description=f'`{name}` now points to: "{value}".', + ) + return await ctx.send(embed=embed) + @commands.group(aliases=["perms"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def permissions(self, ctx): From 288d52924091a885d0db95c1dff9026fb578a064 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 30 Jun 2019 15:00:13 -0700 Subject: [PATCH 0296/1221] Initial commit --- bot.py | 24 +++++++++++++----------- core/config.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- core/thread.py | 4 ++-- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/bot.py b/bot.py index d2e1ff82cc..06d72575b2 100644 --- a/bot.py +++ b/bot.py @@ -20,9 +20,7 @@ from colorama import init, Fore, Style from emoji import UNICODE_EMOJI from motor.motor_asyncio import AsyncIOMotorClient -from pkg_resources import parse_version -from core.changelog import Changelog from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import info, error, human_join @@ -68,9 +66,15 @@ def __init__(self): self._connected = asyncio.Event() self._configure_logging() - # TODO: Raise fatal error if mongo_uri or other essentials are not found - self._db = AsyncIOMotorClient(self.config.mongo_uri).modmail_bot - self._api = ApiClient(self) + + self.standalone = 'mongo_uri' not in self.config.cache + + if not self.standalone: + self._db = AsyncIOMotorClient(self.config.mongo_uri).modmail_bot + else: + self._db = None + + self.api = ApiClient(self) self.plugin_db = PluginDatabaseClient(self) self.metadata_task = self.loop.create_task(self.metadata_loop()) @@ -128,13 +132,11 @@ def version(self) -> str: return __version__ @property - def db(self) -> typing.Optional[AsyncIOMotorClient]: + def db(self) -> AsyncIOMotorClient: + if self._db is None: + raise ValueError('No database instance found.') return self._db - @property - def api(self) -> ApiClient: - return self._api - @property def config(self) -> ConfigManager: if self._config is None: @@ -743,7 +745,7 @@ async def on_raw_reaction_add(self, payload): ts = message.embeds[0].timestamp if message.embeds else None if thread and ts == thread.channel.created_at: # the reacted message is the corresponding thread creation embed - if not self.config.get("disable_recipient_thread_close"): + if self.config.get("recipient_thread_close"): await thread.close(closer=user) elif not isinstance(channel, discord.DMChannel): if not message.embeds: diff --git a/core/config.py b/core/config.py index 32f9dacf79..6b083d54f7 100644 --- a/core/config.py +++ b/core/config.py @@ -5,6 +5,7 @@ import isodate +import discord from discord.ext.commands import BadArgument from core._color_data import ALL_COLORS @@ -122,7 +123,7 @@ def cache(self, val: dict): self._cache = val def populate_cache(self) -> dict: - data = { + defaults = { "snippets": {}, "plugins": [], "aliases": {}, @@ -134,10 +135,51 @@ def populate_cache(self) -> dict: "notification_squad": {}, "subscriptions": {}, "closures": {}, + "twitch_url": 'https://www.twitch.tv/discord-modmail/', + "main_category_id": None, + "disable_autoupdates": False, + "prefix": '?', + "mention": None, + "main_color": discord.Color.blurple(), + "user_typing": False, + "mod_typing": False, + "account_age": None, + "guild_age": None, + "reply_without_command": False, + "log_channel_id": None, + "sent_emoji": '✅', + "blocked_emoji": '🚫', + "close_emoji": '🔒', + "recipient_thread_close": False, + "thread_auto_close": 0, + "thread_auto_close_response": None, + "thread_creation_response": "The staff team will get back to you as soon as possible.", + "thread_creation_footer": None, + "thread_creation_title": 'Thread Created', + "thread_close_footer": 'Replying will create a new thread', + "thread_close_title": 'Thread Closed', + "thread_close_response": '{closer.mention} has closed this Modmail thread.', + "thread_self_close_response": 'You have closed this Modmail thread.', + "recipient_color": discord.Color.gold(), + "mod_tag": None, + "mod_color": discord.Color.green(), + "anon_username": None, + "anon_avatar_url": None, + "anon_tag": 'Response', + "activity_message": None, + "activity_type": None, + "status": None, + "modmail_guild_id": None, + "guild_id": None, + "mongo_uri": None, + "owners": None, + "token": None, + "github_access_token": None, + "log_url": 'https://example.com/', "log_level": "INFO", } - data.update(os.environ) + defaults.update(os.environ) if os.path.exists("config.json"): with open("config.json") as f: diff --git a/core/thread.py b/core/thread.py index ad37f4d05d..107ec22773 100644 --- a/core/thread.py +++ b/core/thread.py @@ -163,7 +163,7 @@ async def send_genesis_message(): ) footer = "Your message has been sent" - if not self.bot.config.get("disable_recipient_thread_close"): + if self.bot.config.get("recipient_thread_close"): footer = "Click the lock to close the thread" footer = self.bot.config.get("thread_creation_footer", footer) @@ -172,7 +172,7 @@ async def send_genesis_message(): if creator is None: msg = await recipient.send(embed=embed) - if not self.bot.config.get("disable_recipient_thread_close"): + if self.bot.config.get("recipient_thread_close"): close_emoji = self.bot.config.get("close_emoji", "🔒") close_emoji = await self.bot.convert_emoji(close_emoji) await msg.add_reaction(close_emoji) From cf855062edf3ef969f51bdb12c1d074a9031c0dc Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 30 Jun 2019 15:15:29 -0700 Subject: [PATCH 0297/1221] Use dotenv to load .env when not ran from pipenv --- Pipfile | 3 ++ Pipfile.lock | 132 +++++++++++++++++++++++++++++++++++++++---------- core/config.py | 5 +- 3 files changed, 112 insertions(+), 28 deletions(-) diff --git a/Pipfile b/Pipfile index fda1d3a532..a37ea80cf9 100644 --- a/Pipfile +++ b/Pipfile @@ -4,6 +4,7 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] +black = "==19.3b0" [packages] colorama = ">=0.4.0" @@ -17,6 +18,8 @@ dnspython = "~=1.16.0" parsedatetime = "==2.4" aiohttp = "<3.6.0,>=3.3.0" pylint = "*" +python-dotenv = ">=0.10.3" +pipenv = "==2018.11.26" "discord.py" = "==1.1.1" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 8b4f831a3b..883013f5a8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f550e423d6429c852f502c5b8cee64f84edd89980e90db9fcee100495330a18c" + "sha256": "b80ac704c624f95f3d6e1e9a7c6e443cd545da71fefa14c822e6a00649ba4f5d" }, "pipfile-spec": 6, "requires": { @@ -65,6 +65,13 @@ ], "version": "==19.1.0" }, + "certifi": { + "hashes": [ + "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", + "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + ], + "version": "==2019.6.16" + }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", @@ -126,10 +133,10 @@ }, "isort": { "hashes": [ - "sha256:c40744b6bc5162bbb39c1257fe298b7a393861d50978b565f3ccd9cb9de0182a", - "sha256:f57abacd059dc3bd666258d1efb0377510a89777fda3e3274e3c01f7c03ae22d" + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" ], - "version": "==4.3.20" + "version": "==4.3.21" }, "lazy-object-proxy": { "hashes": [ @@ -218,6 +225,15 @@ "index": "pypi", "version": "==2.4" }, + "pipenv": { + "hashes": [ + "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330", + "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e", + "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846" + ], + "index": "pypi", + "version": "==2018.11.26" + }, "pylint": { "hashes": [ "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", @@ -267,6 +283,14 @@ "index": "pypi", "version": "==2.8.0" }, + "python-dotenv": { + "hashes": [ + "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093", + "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544" + ], + "index": "pypi", + "version": "==0.10.3" + }, "six": { "hashes": [ "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", @@ -276,28 +300,24 @@ }, "typed-ast": { "hashes": [ - "sha256:132eae51d6ef3ff4a8c47c393a4ef5ebf0d1aecc96880eb5d6c8ceab7017cc9b", - "sha256:18141c1484ab8784006c839be8b985cfc82a2e9725837b0ecfa0203f71c4e39d", - "sha256:2baf617f5bbbfe73fd8846463f5aeafc912b5ee247f410700245d68525ec584a", - "sha256:3d90063f2cbbe39177e9b4d888e45777012652d6110156845b828908c51ae462", - "sha256:4304b2218b842d610aa1a1d87e1dc9559597969acc62ce717ee4dfeaa44d7eee", - "sha256:4983ede548ffc3541bae49a82675996497348e55bafd1554dc4e4a5d6eda541a", - "sha256:5315f4509c1476718a4825f45a203b82d7fdf2a6f5f0c8f166435975b1c9f7d4", - "sha256:6cdfb1b49d5345f7c2b90d638822d16ba62dc82f7616e9b4caa10b72f3f16649", - "sha256:7b325f12635598c604690efd7a0197d0b94b7d7778498e76e0710cd582fd1c7a", - "sha256:8d3b0e3b8626615826f9a626548057c5275a9733512b137984a68ba1598d3d2f", - "sha256:8f8631160c79f53081bd23446525db0bc4c5616f78d04021e6e434b286493fd7", - "sha256:912de10965f3dc89da23936f1cc4ed60764f712e5fa603a09dd904f88c996760", - "sha256:b010c07b975fe853c65d7bbe9d4ac62f1c69086750a574f6292597763781ba18", - "sha256:c908c10505904c48081a5415a1e295d8403e353e0c14c42b6d67f8f97fae6616", - "sha256:c94dd3807c0c0610f7c76f078119f4ea48235a953512752b9175f9f98f5ae2bd", - "sha256:ce65dee7594a84c466e79d7fb7d3303e7295d16a83c22c7c4037071b059e2c21", - "sha256:eaa9cfcb221a8a4c2889be6f93da141ac777eb8819f077e1d09fb12d00a09a93", - "sha256:f3376bc31bad66d46d44b4e6522c5c21976bf9bca4ef5987bb2bf727f4506cbb", - "sha256:f9202fa138544e13a4ec1a6792c35834250a85958fde1251b6a22e07d1260ae7" + "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", + "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", + "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", + "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", + "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", + "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", + "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", + "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", + "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", + "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", + "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", + "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", + "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", + "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", + "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" ], "markers": "implementation_name == 'cpython'", - "version": "==1.3.5" + "version": "==1.4.0" }, "uvloop": { "hashes": [ @@ -316,6 +336,20 @@ "markers": "sys_platform != 'win32'", "version": "==0.12.2" }, + "virtualenv": { + "hashes": [ + "sha256:b7335cddd9260a3dd214b73a2521ffc09647bde3e9457fcca31dc3be3999d04a", + "sha256:d28ca64c0f3f125f59cabf13e0a150e1c68e5eea60983cc4395d88c584495783" + ], + "version": "==16.6.1" + }, + "virtualenv-clone": { + "hashes": [ + "sha256:532f789a5c88adf339506e3ca03326f20ee82fd08ee5586b44dc859b5b4468c5", + "sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557" + ], + "version": "==0.5.3" + }, "websockets": { "hashes": [ "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", @@ -344,9 +378,9 @@ }, "wrapt": { "hashes": [ - "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" ], - "version": "==1.11.1" + "version": "==1.11.2" }, "yarl": { "hashes": [ @@ -365,5 +399,49 @@ "version": "==1.3.0" } }, - "develop": {} + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "black": { + "hashes": [ + "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", + "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + ], + "index": "pypi", + "version": "==19.3b0" + }, + "certifi": { + "hashes": [ + "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", + "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + ], + "version": "==2019.6.16" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + } + } } diff --git a/core/config.py b/core/config.py index 32f9dacf79..c1ffe90adc 100644 --- a/core/config.py +++ b/core/config.py @@ -3,8 +3,8 @@ import os import typing +from dotenv import load_dotenv import isodate - from discord.ext.commands import BadArgument from core._color_data import ALL_COLORS @@ -12,6 +12,9 @@ from core.time import UserFriendlyTime +load_dotenv() + + class ConfigManager: allowed_to_change_in_command = { From cc2b22a4f498fb667adadc7a7c07c81ecd64a102 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 30 Jun 2019 16:25:25 -0700 Subject: [PATCH 0298/1221] Forgot to lock --- Pipfile.lock | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 883013f5a8..a39f536e5f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b80ac704c624f95f3d6e1e9a7c6e443cd545da71fefa14c822e6a00649ba4f5d" + "sha256": "e4f5169004c14f38445d36c1bb89baeeaacc7dd5f43aa88f51b91d0df7dac2de" }, "pipfile-spec": 6, "requires": { @@ -422,13 +422,6 @@ "index": "pypi", "version": "==19.3b0" }, - "certifi": { - "hashes": [ - "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", - "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" - ], - "version": "==2019.6.16" - }, "click": { "hashes": [ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", From aa29158914f1f820aa728af8282da68cbf873582 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 1 Jul 2019 13:35:47 -0700 Subject: [PATCH 0299/1221] Major commit --- CHANGELOG.md | 16 ++ bot.py | 358 ++++++++++++++++++++++++--------------------- cogs/modmail.py | 40 ++--- cogs/plugins.py | 28 ++-- cogs/utility.py | 85 +++++------ core/changelog.py | 9 +- core/checks.py | 4 +- core/clients.py | 214 +-------------------------- core/config.py | 281 ++++++++++++++--------------------- core/decorators.py | 1 - core/thread.py | 140 +++++++++--------- core/time.py | 2 +- core/utils.py | 7 + 13 files changed, 478 insertions(+), 707 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c764130b53..adcaf554e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# v3.1.0 + +### Breaking + +- `disable_recipient_thread_close` is removed, a new configuration variable `recipient_thread_close` replaces it which defaults to False. +- Truthy and falsy values for binary configuration variables are now interpreted respectfully. + + +### Changes + +- `thread_auto_close_response` has a configurable variable `{timeout}`. + +### Internal + +- Removed supporting code for GitHub interaction. + # v3.0.3 ### Added diff --git a/bot.py b/bot.py index 06d72575b2..406521dac4 100644 --- a/bot.py +++ b/bot.py @@ -23,7 +23,7 @@ from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import info, error, human_join +from core.utils import info, error, human_join, strtobool from core.models import PermissionLevel from core.thread import ThreadManager from core.time import human_timedelta @@ -58,22 +58,20 @@ def format(self, record): class ModmailBot(commands.Bot): def __init__(self): super().__init__(command_prefix=None) # implemented in `get_prefix` - self._threads = None - self._session = None - self._config = None - self._db = None + self._session = ClientSession(loop=self.loop) + self._config = ConfigManager(self) + self.start_time = datetime.utcnow() self._connected = asyncio.Event() self._configure_logging() - self.standalone = 'mongo_uri' not in self.config.cache - - if not self.standalone: - self._db = AsyncIOMotorClient(self.config.mongo_uri).modmail_bot - else: - self._db = None + mongo_uri = self.config['mongo_uri'] + if mongo_uri is None: + raise ValueError('A Mongo URI is necessary for the bot to function.') + self._db = AsyncIOMotorClient(mongo_uri).modmail_bot + self._threads = ThreadManager(self) self.api = ApiClient(self) self.plugin_db = PluginDatabaseClient(self) @@ -95,7 +93,7 @@ def uptime(self) -> str: return fmt.format(d=days, h=hours, m=minutes, s=seconds) def _configure_logging(self): - level_text = self.config.log_level.upper() + level_text = self.config['log_level'].upper() logging_levels = { "CRITICAL": logging.CRITICAL, "ERROR": logging.ERROR, @@ -104,7 +102,7 @@ def _configure_logging(self): "DEBUG": logging.DEBUG, } - log_file_name = self.config.token.split(".")[0] + log_file_name = self.token.split(".")[0] ch_debug = logging.FileHandler( os.path.join(temp_dir, f"{log_file_name}.log"), mode="a+" ) @@ -139,20 +137,14 @@ def db(self) -> AsyncIOMotorClient: @property def config(self) -> ConfigManager: - if self._config is None: - self._config = ConfigManager(self) return self._config @property def session(self) -> ClientSession: - if self._session is None: - self._session = ClientSession(loop=self.loop) return self._session @property def threads(self) -> ThreadManager: - if self._threads is None: - self._threads = ThreadManager(self) return self._threads async def get_prefix(self, message=None): @@ -183,8 +175,6 @@ def run(self, *args, **kwargs): self.loop.run_until_complete(self.start(self.token)) except discord.LoginFailure: logger.critical(error("Invalid token")) - except KeyboardInterrupt: - pass except Exception: logger.critical(error("Fatal exception"), exc_info=True) finally: @@ -192,7 +182,7 @@ def run(self, *args, **kwargs): self.metadata_task.cancel() self.loop.run_until_complete(self.metadata_task) except asyncio.CancelledError: - logger.debug(info("data_task has been cancelled.")) + logger.debug(info("metadata_task has been cancelled.")) self.loop.run_until_complete(self.logout()) for task in asyncio.Task.all_tasks(): @@ -207,37 +197,63 @@ def run(self, *args, **kwargs): logger.info(error(" - Shutting down bot - ")) async def is_owner(self, user: discord.User) -> bool: - raw = str(self.config.get("owners", "0")).split(",") - allowed = {int(x) for x in raw} - return (user.id in allowed) or await super().is_owner(user) + owners = self.config['owners'] + if owners is not None: + if user.id in set(map(int, str(owners).split(','))): + return True + return await super().is_owner(user) @property def log_channel(self) -> typing.Optional[discord.TextChannel]: - channel_id = self.config.get("log_channel_id") + channel_id = self.config["log_channel_id"] if channel_id is not None: - return self.get_channel(int(channel_id)) + channel = self.get_channel(int(channel_id)) + if channel is not None: + return channel + self.config.remove('log_channel_id') if self.main_category is not None: - return self.main_category.channels[0] - return None + try: + return self.main_category.channels[0] + except IndexError: + pass + logger.info(info(f'No log channel set, set one with `%ssetup` or ' + f'`%sconfig set log_channel_id `.'), self.prefix, self.prefix) + + @property + def is_connected(self) -> bool: + return self._connected.is_set() + + async def wait_for_connected(self) -> None: + await self.wait_until_ready() + await self._connected.wait() + await self.config.wait_until_ready() @property def snippets(self) -> typing.Dict[str, str]: - return {k: v for k, v in self.config.get("snippets", {}).items() if v} + return self.config["snippets"] @property def aliases(self) -> typing.Dict[str, str]: - return {k: v for k, v in self.config.get("aliases", {}).items() if v} + return self.config["aliases"] @property def token(self) -> str: - return self.config.token + token = self.config['token'] + if token is None: + raise ValueError('TOKEN must be set, this is your bot token.') + return token @property - def guild_id(self) -> int: - return int(self.config.guild_id) + def guild_id(self) -> typing.Optional[int]: + guild_id = self.config['guild_id'] + if guild_id is not None: + try: + return int(str(guild_id)) + except ValueError: + raise ValueError('Invalid guild_id set.') @property - def guild(self) -> discord.Guild: + def guild(self) -> typing.Optional[discord.Guild]: """ The guild that the bot is serving (the server where users message it from) @@ -245,90 +261,92 @@ def guild(self) -> discord.Guild: return discord.utils.get(self.guilds, id=self.guild_id) @property - def modmail_guild(self) -> discord.Guild: + def modmail_guild(self) -> typing.Optional[discord.Guild]: """ The guild that the bot is operating in (where the bot is creating threads) """ - modmail_guild_id = self.config.get("modmail_guild_id") - if not modmail_guild_id: + modmail_guild_id = self.config["modmail_guild_id"] + if modmail_guild_id is None: return self.guild - return discord.utils.get(self.guilds, id=int(modmail_guild_id)) + guild = discord.utils.get(self.guilds, id=int(modmail_guild_id)) + if guild is not None: + return guild + self.config.remove('modmail_guild_id') + logger.error(error('Invalid modmail_guild_id set.')) + return self.guild @property def using_multiple_server_setup(self) -> bool: return self.modmail_guild != self.guild @property - def main_category(self) -> typing.Optional[discord.TextChannel]: - category_id = self.config.get("main_category_id") - if category_id is not None: - return discord.utils.get(self.modmail_guild.categories, id=int(category_id)) - - if self.modmail_guild: + def main_category(self) -> typing.Optional[discord.CategoryChannel]: + if self.modmail_guild is not None: + category_id = self.config["main_category_id"] + if category_id is not None: + cat = discord.utils.get(self.modmail_guild.categories, id=int(category_id)) + if cat is not None: + return cat + self.config.remove("main_category_id") return discord.utils.get(self.modmail_guild.categories, name="Modmail") - return None @property def blocked_users(self) -> typing.Dict[str, str]: - return self.config.get("blocked", {}) + return self.config["blocked"] @property def blocked_whitelisted_users(self) -> typing.List[str]: - return self.config.get("blocked_whitelist", []) + return self.config["blocked_whitelist"] @property def prefix(self) -> str: - return self.config.get("prefix", "?") + return str(self.config["prefix"]) @property def mod_color(self) -> typing.Union[discord.Color, int]: color = self.config.get("mod_color") - if not color: - return discord.Color.green() + if isinstance(color, discord.Color): + return color try: - color = int(color.lstrip("#"), base=16) + return int(color.lstrip("#"), base=16) except ValueError: - logger.error(error("Invalid mod_color provided")) - return discord.Color.green() - else: - return color + logger.error(error("Invalid mod_color provided.")) + return self.config.remove('mod_color') @property def recipient_color(self) -> typing.Union[discord.Color, int]: color = self.config.get("recipient_color") - if not color: - return discord.Color.gold() + if isinstance(color, discord.Color): + return color try: - color = int(color.lstrip("#"), base=16) + return int(color.lstrip("#"), base=16) except ValueError: - logger.error(error("Invalid recipient_color provided")) - return discord.Color.gold() - else: - return color + logger.error(error("Invalid recipient_color provided.")) + return self.config.remove('recipient_color') @property def main_color(self) -> typing.Union[discord.Color, int]: color = self.config.get("main_color") - if not color: - return discord.Color.blurple() + if isinstance(color, discord.Color): + return color try: - color = int(color.lstrip("#"), base=16) + return int(color.lstrip("#"), base=16) except ValueError: - logger.error(error("Invalid main_color provided")) - return discord.Color.blurple() - else: - return color + logger.error(error("Invalid main_color provided.")) + return self.config.remove('main_color') async def on_connect(self): logger.info(LINE) - await self.validate_database_connection() + try: + await self.validate_database_connection() + except Exception: + return await self.logout() + logger.info(LINE) logger.info(info("Connected to gateway.")) - await self.config.refresh() - if self.db: - await self.setup_indexes() + await self.setup_indexes() self._connected.set() async def setup_indexes(self): @@ -357,30 +375,26 @@ async def setup_indexes(self): async def on_ready(self): """Bot startup, sets uptime.""" - await self._connected.wait() + + # Wait until config cache is populated with stuff from db and on_connect ran + await self.wait_for_connected() + logger.info(LINE) logger.info(info("Client ready.")) logger.info(LINE) logger.info(info(f"Logged in as: {self.user}")) logger.info(info(f"User ID: {self.user.id}")) logger.info(info(f"Prefix: {self.prefix}")) - logger.info(info(f"Guild Name: {self.guild.name if self.guild else 'None'}")) - logger.info(info(f"Guild ID: {self.guild.id if self.guild else 0}")) - + logger.info(info(f"Guild Name: {self.guild.name if self.guild else 'Invalid'}")) + logger.info(info(f"Guild ID: {self.guild.id if self.guild else 'Invalid'}")) logger.info(LINE) - if not self.guild: - logger.error(error("WARNING - The GUILD_ID " "provided does not exist!")) - else: - await self.threads.populate_cache() - - # Wait until config cache is populated with stuff from db - await self.config.wait_until_ready() + await self.threads.populate_cache() # closures - closures = self.config.closures.copy() + closures = self.config['closures'] logger.info( - info(f"There are {len(closures)} thread(s) " "pending to be closed.") + info(f"There are {len(closures)} thread(s) pending to be closed.") ) for recipient_id, items in closures.items(): @@ -394,7 +408,7 @@ async def on_ready(self): if not thread: # If the channel is deleted - self.config.closures.pop(str(recipient_id)) + self.config['closures'].pop(recipient_id) await self.config.update() continue @@ -423,27 +437,24 @@ async def convert_emoji(self, name: str) -> str: async def retrieve_emoji(self) -> typing.Tuple[str, str]: - sent_emoji = self.config.get("sent_emoji", "✅") - blocked_emoji = self.config.get("blocked_emoji", "🚫") + sent_emoji = self.config["sent_emoji"] + blocked_emoji = self.config["blocked_emoji"] if sent_emoji != "disable": try: sent_emoji = await self.convert_emoji(sent_emoji) except commands.BadArgument: - logger.warning(info("Removed sent emoji (%s)."), sent_emoji) - del self.config.cache["sent_emoji"] - await self.config.update() - sent_emoji = "✅" + logger.warning(error("Removed sent emoji (%s)."), sent_emoji) + sent_emoji = self.config.remove("sent_emoji") if blocked_emoji != "disable": try: blocked_emoji = await self.convert_emoji(blocked_emoji) except commands.BadArgument: - logger.warning(info("Removed blocked emoji (%s)."), blocked_emoji) - del self.config.cache["blocked_emoji"] - await self.config.update() - blocked_emoji = "🚫" + logger.warning(error("Removed blocked emoji (%s)."), blocked_emoji) + blocked_emoji = self.config.remove("blocked_emoji") + await self.config.update() return sent_emoji, blocked_emoji async def _process_blocked(self, message: discord.Message) -> bool: @@ -451,7 +462,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: if str(message.author.id) in self.blocked_whitelisted_users: if str(message.author.id) in self.blocked_users: - del self.config.blocked[str(message.author.id)] + self.blocked_users.pop(str(message.author.id)) await self.config.update() if sent_emoji != "disable": @@ -464,11 +475,10 @@ async def _process_blocked(self, message: discord.Message) -> bool: now = datetime.utcnow() - account_age = self.config.get("account_age") - guild_age = self.config.get("guild_age") - if account_age is None: - account_age = isodate.duration.Duration() - else: + account_age = self.config["account_age"] + guild_age = self.config["guild_age"] + + if not isinstance(account_age, isodate.Duration): try: account_age = isodate.parse_duration(account_age) except isodate.ISO8601Error: @@ -478,13 +488,9 @@ async def _process_blocked(self, message: discord.Message) -> bool: 'greater than 0 days, not "%s".', str(account_age), ) - del self.config.cache["account_age"] - await self.config.update() - account_age = isodate.duration.Duration() + account_age = self.config.remove("account_age") - if guild_age is None: - guild_age = isodate.duration.Duration() - else: + if not isinstance(guild_age, isodate.Duration): try: guild_age = isodate.parse_duration(guild_age) except isodate.ISO8601Error: @@ -494,33 +500,24 @@ async def _process_blocked(self, message: discord.Message) -> bool: 'greater than 0 days, not "%s".', str(guild_age), ) - del self.config.cache["guild_age"] - await self.config.update() - guild_age = isodate.duration.Duration() + guild_age = self.config.remove("guild_age") - reason = self.blocked_users.get(str(message.author.id)) - if reason is None: - reason = "" + reason = self.blocked_users.get(str(message.author.id)) or '' + min_guild_age = min_account_age = now try: min_account_age = message.author.created_at + account_age - except ValueError as exc: - logger.warning(exc.args[0]) - del self.config.cache["account_age"] - await self.config.update() - min_account_age = now + except ValueError: + logger.warning("Error with 'account_age'.", exc_info=True) + self.config.remove("account_age") try: - member = self.guild.get_member(message.author.id) - if member: - min_guild_age = member.joined_at + guild_age - else: - min_guild_age = now - except ValueError as exc: - logger.warning(exc.args[0]) - del self.config.cache["guild_age"] - await self.config.update() - min_guild_age = now + 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") if min_account_age > now: # User account has not reached the required time @@ -532,8 +529,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: new_reason = ( f"System Message: New Account. Required to wait for {delta}." ) - self.config.blocked[str(message.author.id)] = new_reason - await self.config.update() + self.blocked_users[str(message.author.id)] = new_reason changed = True if reason.startswith("System Message: New Account.") or changed: @@ -556,8 +552,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: new_reason = ( f"System Message: Recently Joined. Required to wait for {delta}." ) - self.config.blocked[str(message.author.id)] = new_reason - await self.config.update() + self.blocked_users[str(message.author.id)] = new_reason changed = True if reason.startswith("System Message: Recently Joined.") or changed: @@ -575,10 +570,9 @@ async def _process_blocked(self, message: discord.Message) -> bool: if reason.startswith("System Message: New Account.") or reason.startswith( "System Message: Recently Joined." ): - # Met the age limit already + # Met the age limit already, otherwise it would've been caught by the previous if's reaction = sent_emoji - del self.config.blocked[str(message.author.id)] - await self.config.update() + self.blocked_users.pop(str(message.author.id)) else: end_time = re.search(r"%(.+?)%$", reason) if end_time is not None: @@ -588,11 +582,11 @@ async def _process_blocked(self, message: discord.Message) -> bool: if after <= 0: # No longer blocked reaction = sent_emoji - del self.config.blocked[str(message.author.id)] - await self.config.update() + self.blocked_users.pop(str(message.author.id)) else: reaction = sent_emoji + await self.config.update() if reaction != "disable": try: await message.add_reaction(reaction) @@ -602,6 +596,8 @@ async def _process_blocked(self, message: discord.Message) -> bool: async def process_modmail(self, message: discord.Message) -> None: """Processes messages sent to the bot.""" + await self.wait_for_connected() + blocked = await self._process_blocked(message) if not blocked: thread = await self.threads.find_or_create(message.author) @@ -612,6 +608,7 @@ async def get_context(self, message, *, cls=commands.Context): Returns the invocation context from the message. Supports getting the prefix from database as well as command aliases. """ + await self.wait_for_connected() view = StringView(message.content) ctx = cls(prefix=None, view=view, bot=self, message=message) @@ -630,7 +627,7 @@ async def get_context(self, message, *, cls=commands.Context): invoker = view.get_word().lower() # Check if there is any aliases being called. - alias = self.config.get("aliases", {}).get(invoker) + alias = self.aliases.get(invoker) if alias is not None: ctx._alias_invoked = True len_ = len(f"{invoked_prefix}{invoker}") @@ -648,10 +645,10 @@ async def update_perms( self, name: typing.Union[PermissionLevel, str], value: int, add: bool = True ) -> None: if isinstance(name, PermissionLevel): - permissions = self.config.level_permissions + permissions = self.config['level_permissions'] name = name.name else: - permissions = self.config.command_permissions + permissions = self.config['command_permissions'] if name not in permissions: if add: permissions[name] = [value] @@ -666,6 +663,8 @@ async def update_perms( await self.config.update() async def on_message(self, message): + await self.wait_for_connected() + if message.type == discord.MessageType.pins_add and message.author == self.user: await message.delete() @@ -678,7 +677,7 @@ async def on_message(self, message): prefix = self.prefix if message.content.startswith(prefix): - cmd = message.content[len(prefix) :].strip() + cmd = message.content[len(prefix):].strip() if cmd in self.snippets: thread = await self.threads.find(channel=message.channel) snippet = self.snippets[cmd] @@ -692,7 +691,12 @@ async def on_message(self, message): thread = await self.threads.find(channel=ctx.channel) if thread is not None: - if self.config.get("reply_without_command"): + try: + reply_without_command = strtobool(self.config["reply_without_command"]) + except ValueError: + reply_without_command = self.config.remove('reply_without_command') + + if reply_without_command: await thread.reply(message) else: await self.api.append_log(message, type_="internal") @@ -703,30 +707,39 @@ async def on_message(self, message): self.dispatch("command_error", ctx, exc) async def on_typing(self, channel, user, _): + await self.wait_for_connected() + if user.bot: return if isinstance(channel, discord.DMChannel): - if not self.config.get("user_typing"): + try: + user_typing = strtobool(self.config["user_typing"]) + except ValueError: + user_typing = self.config.remove('user_typing') + if not user_typing: return + thread = await self.threads.find(recipient=user) if thread: await thread.channel.trigger_typing() else: - if not self.config.get("mod_typing"): + try: + mod_typing = strtobool(self.config["mod_typing"]) + except ValueError: + mod_typing = self.config.remove('mod_typing') + if not mod_typing: return + thread = await self.threads.find(channel=channel) - if thread and thread.recipient: + if thread is not None and thread.recipient: await thread.recipient.trigger_typing() async def on_raw_reaction_add(self, payload): - user = self.get_user(payload.user_id) - if user.bot: return channel = self.get_channel(payload.channel_id) - if not channel: # dm channel not in internal cache _thread = await self.threads.find(recipient=user) if not _thread: @@ -736,18 +749,21 @@ async def on_raw_reaction_add(self, payload): message = await channel.fetch_message(payload.message_id) reaction = payload.emoji - close_emoji = await self.convert_emoji(self.config.get("close_emoji", "🔒")) + close_emoji = await self.convert_emoji(self.config["close_emoji"]) - if isinstance(channel, discord.DMChannel) and str(reaction) == str( - close_emoji - ): # closing thread - thread = await self.threads.find(recipient=user) - ts = message.embeds[0].timestamp if message.embeds else None - if thread and ts == thread.channel.created_at: - # the reacted message is the corresponding thread creation embed - if self.config.get("recipient_thread_close"): - await thread.close(closer=user) - elif not isinstance(channel, discord.DMChannel): + if isinstance(channel, discord.DMChannel): + if str(reaction) == str(close_emoji): # closing thread + thread = await self.threads.find(recipient=user) + ts = message.embeds[0].timestamp if message.embeds else None + if thread and ts == thread.channel.created_at: + # the reacted message is the corresponding thread creation embed + try: + recipient_thread_close = strtobool(self.config["recipient_thread_close"]) + except ValueError: + recipient_thread_close = self.config.remove('recipient_thread_close') + if recipient_thread_close: + await thread.close(closer=user) + else: if not message.embeds: return message_id = str(message.embeds[0].author.url).split("/")[-1] @@ -771,13 +787,18 @@ async def on_guild_channel_delete(self, channel): if mod == self.user: return + if isinstance(channel, discord.CategoryChannel): + if self.main_category.id == channel.id: + self.config.remove('main_category_id') + await self.config.update() + return + if not isinstance(channel, discord.TextChannel): - if int(self.config.get("main_category_id")) == channel.id: - await self.config.update({"main_category_id": None}) return - if int(self.config.get("log_channel_id")) == channel.id: - await self.config.update({"log_channel_id": None}) + if self.log_channel is None or self.log_channel.id == channel.id: + self.config.remove('log_channel_id') + await self.config.update() return thread = await self.threads.find(channel=channel) @@ -918,12 +939,12 @@ async def validate_database_connection(self): ) ) - return await self.logout() + raise else: logger.info(info("Successfully connected to the database.")) async def metadata_loop(self): - await self.wait_until_ready() + await self.wait_for_connected() if not self.guild: return @@ -955,7 +976,6 @@ async def metadata_loop(self): if __name__ == "__main__": if os.name != "nt": import uvloop - uvloop.install() bot = ModmailBot() bot.run() diff --git a/cogs/modmail.py b/cogs/modmail.py index 2c6d9e6068..96d95ac096 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -35,9 +35,15 @@ async def setup(self, ctx): once after configuring Modmail. """ - if self.bot.main_category: + if self.bot.main_category is not None: return await ctx.send(f"{self.bot.modmail_guild} is already set up.") + if self.bot.modmail_guild is None: + embed = discord.Embed(title='Error', + description='Modmail functioning guild not found.', + color=discord.Color.red()) + return await ctx.send(embed=embed) + category = await self.bot.modmail_guild.create_category( name="Modmail", overwrites=self.bot.overwrites(ctx) ) @@ -65,7 +71,7 @@ async def setup(self, ctx): ) embed.set_footer( - text=f'Type "{self.bot.prefix}help" ' "for a complete list of commands." + text=f'Type "{self.bot.prefix}help" for a complete list of commands.' ) await log_channel.send(embed=embed) @@ -80,7 +86,7 @@ async def setup(self, ctx): f"Type `{self.bot.prefix}permissions` for more info." ) - if not self.bot.config.get("permissions"): + if not self.bot.config["permissions"]: await self.bot.update_perms(PermissionLevel.REGULAR, -1) await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id) @@ -147,14 +153,14 @@ async def snippets_add(self, ctx, name: str.lower, *, value): {prefix}snippets add "two word" this is a two word snippet. ``` """ - if name in self.bot.config.snippets: + if name in self.bot.snippets: embed = discord.Embed( title="Error", color=discord.Color.red(), description=f"Snippet `{name}` already exists.", ) else: - self.bot.config.snippets[name] = value + self.bot.snippets[name] = value await self.bot.config.update() embed = discord.Embed( @@ -170,13 +176,13 @@ async def snippets_add(self, ctx, name: str.lower, *, value): async def snippets_remove(self, ctx, *, name: str.lower): """Remove a snippet.""" - if name in self.bot.config.snippets: + if name in self.bot.snippets: embed = discord.Embed( title="Removed snippet", color=self.bot.main_color, description=f"`{name}` no longer exists.", ) - del self.bot.config["snippets"][name] + self.bot.snippets.pop(name) await self.bot.config.update() else: @@ -191,8 +197,8 @@ async def snippets_remove(self, ctx, *, name: str.lower): @snippets.command(name="edit") @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippets_edit(self, ctx, name: str.lower, *, value): - if name in self.bot.config.snippets: - self.bot.config.snippets[name] = value + if name in self.bot.snippets: + self.bot.snippets[name] = value await self.bot.config.update() embed = discord.Embed( @@ -337,7 +343,7 @@ async def notify( if mention in mentions: embed = discord.Embed( color=discord.Color.red(), - description=f"{mention} is already " "going to be mentioned.", + description=f"{mention} is already going to be mentioned.", ) else: mentions.append(mention) @@ -381,14 +387,14 @@ async def unnotify( if mention not in mentions: embed = discord.Embed( color=discord.Color.red(), - description=f"{mention} does not have a " "pending notification.", + description=f"{mention} does not have a pending notification.", ) else: mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=f"{mention} will no longer " "be notified.", + description=f"{mention} will no longer be notified.", ) return await ctx.send(embed=embed) @@ -510,11 +516,11 @@ def format_log_embeds(self, logs, avatar_url): created_at = parser.parse(entry["created_at"]) - prefix = os.getenv("LOG_URL_PREFIX", "/logs") + prefix = self.bot.config["log_url_prefix"] if prefix == "NONE": prefix = "" - log_url = self.bot.config.log_url.strip("/") + f"{prefix}/{key}" + log_url = self.bot.config['log_url'].strip("/") + f"{prefix}/{key}" username = entry["recipient"]["name"] + "#" username += entry["recipient"]["discriminator"] @@ -869,7 +875,7 @@ async def blocked_whitelist(self, ctx, *, user: User = None): msg = self.bot.blocked_users.get(str(user.id)) if msg is None: msg = "" - del self.bot.config.blocked[str(user.id)] + self.bot.blocked_users.pop(str(user.id)) await self.bot.config.update() @@ -967,7 +973,7 @@ async def block( color=self.bot.main_color, description=f"{mention} is now blocked{extend}.", ) - self.bot.config.blocked[str(user.id)] = reason + self.bot.blocked_users[str(user.id)] = reason await self.bot.config.update() else: embed = discord.Embed( @@ -1003,7 +1009,7 @@ async def unblock(self, ctx, *, user: User = None): msg = self.bot.blocked_users.get(str(user.id)) if msg is None: msg = "" - del self.bot.config.blocked[str(user.id)] + self.bot.blocked_users.pop(str(user.id)) await self.bot.config.update() if msg.startswith("System Message: "): diff --git a/cogs/plugins.py b/cogs/plugins.py index c138af960e..bcfd2a7dd5 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -13,7 +13,6 @@ import discord from discord.ext import commands -from discord.utils import async_all from pkg_resources import parse_version from core import checks @@ -58,7 +57,8 @@ def parse_plugin(name): # returns: (username, repo, plugin_name, branch) # default branch = master try: - result = name.split("/") + # when names are formatted with inline code + result = name.strip('`').split("/") result[2] = "/".join(result[2:]) if "@" in result[2]: # branch is specified @@ -75,9 +75,9 @@ def parse_plugin(name): return tuple(result) async def download_initial_plugins(self): - await self.bot._connected.wait() + await self.bot.wait_for_connected() - for i in self.bot.config.plugins: + for i in self.bot.config['plugins']: username, repo, name, branch = self.parse_plugin(i) try: @@ -187,7 +187,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): ) return await ctx.send(embed=embed) - if plugin_name in self.bot.config.plugins: + if plugin_name in self.bot.config['plugins']: embed = discord.Embed( description="This plugin is already installed.", color=self.bot.main_color, @@ -234,7 +234,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): # if it makes it here, it has passed all checks and should # be entered into the config - self.bot.config.plugins.append(plugin_name) + self.bot.config['plugins'].append(plugin_name) await self.bot.config.update() embed = discord.Embed( @@ -261,7 +261,7 @@ async def plugin_remove(self, ctx, *, plugin_name: str): details["repository"] + "/" + plugin_name + "@" + details["branch"] ) - if plugin_name in self.bot.config.plugins: + if plugin_name in self.bot.config['plugins']: try: username, repo, name, branch = self.parse_plugin(plugin_name) @@ -271,12 +271,12 @@ async def plugin_remove(self, ctx, *, plugin_name: str): except Exception: pass - self.bot.config.plugins.remove(plugin_name) + self.bot.config['plugins'].remove(plugin_name) try: - # BUG: Local variables 'username' and 'repo' might be referenced before assignment + # BUG: Local variables 'username', 'branch' and 'repo' might be referenced before assignment if not any( - i.startswith(f"{username}/{repo}") for i in self.bot.config.plugins + i.startswith(f"{username}/{repo}") for i in self.bot.config['plugins'] ): # if there are no more of such repos, delete the folder def onerror(func, path, exc_info): # pylint: disable=W0613 @@ -290,7 +290,7 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 ) except Exception as exc: logger.error(str(exc)) - self.bot.config.plugins.append(plugin_name) + self.bot.config['plugins'].append(plugin_name) logger.error(error(exc)) raise exc @@ -318,7 +318,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): details["repository"] + "/" + plugin_name + "@" + details["branch"] ) - if plugin_name not in self.bot.config.plugins: + if plugin_name not in self.bot.config['plugins']: embed = discord.Embed( description="That plugin is not installed.", color=self.bot.main_color ) @@ -368,8 +368,8 @@ async def plugin_update(self, ctx, *, plugin_name: str): async def plugin_enabled(self, ctx): """Shows a list of currently enabled plugins.""" - if self.bot.config.plugins: - msg = "```\n" + "\n".join(self.bot.config.plugins) + "\n```" + if self.bot.config['plugins']: + msg = "```\n" + "\n".join(self.bot.config['plugins']) + "\n```" embed = discord.Embed(description=msg, color=self.bot.main_color) await ctx.send(embed=embed) else: diff --git a/cogs/utility.py b/cogs/utility.py index b1743f5eb3..d1909df4da 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -296,7 +296,7 @@ async def sponsors(self, ctx): async def debug(self, ctx): """Shows the recent application-logs of the bot.""" - log_file_name = self.bot.config.token.split(".")[0] + log_file_name = self.bot.token.split(".")[0] with open( os.path.join( @@ -352,7 +352,7 @@ async def debug_hastebin(self, ctx): """Posts application-logs to Hastebin.""" haste_url = os.environ.get("HASTE_URL", "https://hasteb.in") - log_file_name = self.bot.config.token.split(".")[0] + log_file_name = self.bot.token.split(".")[0] with open( os.path.join( @@ -389,7 +389,7 @@ async def debug_hastebin(self, ctx): async def debug_clear(self, ctx): """Clears the locally cached logs.""" - log_file_name = self.bot.config.token.split(".")[0] + log_file_name = self.bot.token.split(".")[0] with open( os.path.join( @@ -428,8 +428,8 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): - `{prefix}activity clear` """ if activity_type == "clear": - self.bot.config["activity_type"] = None - self.bot.config["activity_message"] = None + self.bot.config.remove("activity_type") + self.bot.config.remove("activity_message") await self.bot.config.update() await self.set_presence() embed = Embed(title="Activity Removed", color=self.bot.main_color) @@ -474,7 +474,7 @@ async def status(self, ctx, *, status_type: str.lower): - `{prefix}status clear` """ if status_type == "clear": - self.bot.config["status"] = None + self.bot.config.remove("status") await self.bot.config.update() await self.set_presence() embed = Embed(title="Status Removed", color=self.bot.main_color) @@ -507,7 +507,7 @@ async def set_presence( activity = status = None if status_identifier is None: - status_identifier = self.bot.config.get("status", None) + status_identifier = self.bot.config["status"] status_by_key = False try: @@ -525,7 +525,7 @@ async def set_presence( raise ValueError( "activity_message must be None " "if activity_identifier is None." ) - activity_identifier = self.bot.config.get("activity_type", None) + activity_identifier = self.bot.config["activity_type"] activity_by_key = False try: @@ -540,7 +540,7 @@ async def set_presence( else: url = None activity_message = ( - activity_message or self.bot.config.get("activity_message", "") + activity_message or self.bot.config["activity_message"] ).strip() if activity_type == ActivityType.listening: @@ -549,9 +549,7 @@ async def set_presence( # discord automatically add the "to" activity_message = activity_message[3:].strip() elif activity_type == ActivityType.streaming: - url = self.bot.config.get( - "twitch_url", "https://www.twitch.tv/discord-modmail/" - ) + url = self.bot.config["twitch_url"] if activity_message: activity = Activity(type=activity_type, name=activity_message, url=url) @@ -578,14 +576,13 @@ async def set_presence( @commands.Cog.listener() async def on_ready(self): # Wait until config cache is populated with stuff from db - await self.bot.config.wait_until_ready() + await self.bot.wait_for_connected() logger.info(info(self.presence["activity"][1])) logger.info(info(self.presence["status"][1])) async def loop_presence(self): """Set presence to the configured value every hour.""" - await self.bot.config.wait_until_ready() - await self.bot.wait_until_ready() + await self.bot.wait_for_connected() while not self.bot.is_closed(): self.presence = await self.set_presence() await asyncio.sleep(600) @@ -610,7 +607,7 @@ async def mention(self, ctx, *, mention: str = None): Type only `{prefix}mention` to retrieve your current "mention" message. """ - current = self.bot.config.get("mention", "@here") + current = self.bot.config["mention"] if mention is None: embed = Embed( @@ -673,7 +670,7 @@ async def config(self, ctx): @checks.has_permissions(PermissionLevel.OWNER) async def config_options(self, ctx): """Return a list of valid configuration names you can change.""" - allowed = self.bot.config.allowed_to_change_in_command + allowed = self.bot.config.public_keys valid = ", ".join(f"`{k}`" for k in allowed) embed = Embed(title="Valid Keys", description=valid, color=self.bot.main_color) return await ctx.send(embed=embed) @@ -683,7 +680,7 @@ async def config_options(self, ctx): async def config_set(self, ctx, key: str.lower, *, value: str): """Set a configuration variable and its value.""" - keys = self.bot.config.allowed_to_change_in_command + keys = self.bot.config.public_keys if key in keys: try: @@ -691,7 +688,8 @@ async def config_set(self, ctx, key: str.lower, *, value: str): except InvalidConfigError as exc: embed = exc.embed else: - await self.bot.config.update({key: value}) + self.bot.config[key] = value + await self.bot.config.update() embed = Embed( title="Success", color=self.bot.main_color, @@ -714,16 +712,12 @@ async def config_remove(self, ctx, key: str.lower): """Delete a set configuration variable.""" keys = self.bot.config.allowed_to_change_in_command if key in keys: - try: - del self.bot.config.cache[key] - await self.bot.config.update() - except KeyError: - # when no values were set - pass + self.bot.config.remove(key) + await self.bot.config.update() embed = Embed( title="Success", color=self.bot.main_color, - description=f"`{key}` had been deleted from the config.", + description=f"`{key}` had been reset to default.", ) else: embed = Embed( @@ -744,11 +738,11 @@ async def config_get(self, ctx, key: str.lower = None): Leave `key` empty to show all currently set configuration variables. """ - keys = self.bot.config.allowed_to_change_in_command + keys = self.bot.config.public_keys if key: if key in keys: - desc = f"`{key}` is set to `{self.bot.config.get(key)}`" + desc = f"`{key}` is set to `{self.bot.config[key]}`" embed = Embed(color=self.bot.main_color, description=desc) embed.set_author( name="Config variable", icon_url=self.bot.user.avatar_url @@ -773,8 +767,8 @@ async def config_get(self, ctx, key: str.lower = None): config = { key: val - for key, val in self.bot.config.cache.items() - if val and key in keys + for key, val in self.bot.config.items() + if val != self.bot.config.defaults['key'] and key in keys } for name, value in reversed(list(config.items())): @@ -835,10 +829,7 @@ async def alias(self, ctx): @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_add(self, ctx, name: str.lower, *, value): """Add an alias.""" - if "aliases" not in self.bot.config.cache: - self.bot.config["aliases"] = {} - - if self.bot.get_command(name) or name in self.bot.config.aliases: + if self.bot.get_command(name) or name in self.bot.aliases: embed = Embed( title="Error", color=Color.red(), @@ -855,8 +846,9 @@ async def alias_add(self, ctx, name: str.lower, *, value): description="The command you are attempting to point " f"to does not exist: `{linked_command}`.", ) + return await ctx.send(embed=embed) - self.bot.config.aliases[name] = value + self.bot.aliases[name] = value await self.bot.config.update() embed = Embed( @@ -872,11 +864,8 @@ async def alias_add(self, ctx, name: str.lower, *, value): async def alias_remove(self, ctx, *, name: str.lower): """Remove an alias.""" - if "aliases" not in self.bot.config.cache: - self.bot.config["aliases"] = {} - - if name in self.bot.config.aliases: - del self.bot.config["aliases"][name] + if name in self.bot.aliases: + self.bot.aliases.pop(name) await self.bot.config.update() embed = Embed( @@ -897,10 +886,7 @@ async def alias_remove(self, ctx, *, name: str.lower): @alias.command(name="edit") @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_edit(self, ctx, name: str.lower, *, value): - if "aliases" not in self.bot.config.cache: - self.bot.config["aliases"] = {} - - if name not in self.bot.config.aliases: + if name not in self.bot.aliases: embed = Embed( title="Error", color=Color.red(), @@ -928,7 +914,7 @@ async def alias_edit(self, ctx, name: str.lower, *, value): ) return await ctx.send(embed=embed) - self.bot.config.aliases[name] = value + self.bot.aliases[name] = value await self.bot.config.update() embed = Embed( @@ -1141,11 +1127,11 @@ async def permissions_get(self, ctx, *, user_or_role: Union[User, Role, str]): cmds = [] levels = [] for cmd in self.bot.commands: - permissions = self.bot.config.command_permissions.get(cmd.name, []) + permissions = self.bot.config['command_permissions'].get(cmd.name, []) if value in permissions: cmds.append(cmd.name) for level in PermissionLevel: - permissions = self.bot.config.level_permissions.get(level.name, []) + permissions = self.bot.config['level_permissions'].get(level.name, []) if value in permissions: levels.append(level.name) mention = user_or_role.name if hasattr(user_or_role, "name") else user_or_role @@ -1181,7 +1167,7 @@ async def permissions_get_command(self, ctx, *, command: str = None): """View currently-set permissions for a command.""" def get_command(cmd): - permissions = self.bot.config.command_permissions.get(cmd.name, []) + permissions = self.bot.config['command_permissions'].get(cmd.name, []) if not permissions: embed = Embed( title=f"Permission entries for command `{cmd.name}`:", @@ -1239,7 +1225,7 @@ async def permissions_get_level(self, ctx, *, level: str = None): """View currently-set permissions for commands of a permission level.""" def get_level(perm_level): - permissions = self.bot.config.level_permissions.get(perm_level.name, []) + permissions = self.bot.config['level_permissions'].get(perm_level.name, []) if not permissions: embed = Embed( title="Permission entries for permission " @@ -1311,6 +1297,7 @@ async def oauth_whitelist(self, ctx, target: Union[User, Role]): """ whitelisted = self.bot.config["oauth_whitelist"] + # target.id is not int?? if target.id in whitelisted: whitelisted.remove(target.id) removed = True diff --git a/core/changelog.py b/core/changelog.py index 9a10b4dc35..e3f9424546 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -1,6 +1,6 @@ import re from collections import defaultdict -from typing import List +from typing import List, Union from discord import Embed, Color @@ -32,10 +32,13 @@ class Version: General description of the version. """ - def __init__(self, bot, version: str, lines: str): + def __init__(self, bot, version: str, lines: Union[List[str], str]): self.bot = bot self.version = version.lstrip("vV") - self.lines = [x for x in lines.splitlines() if x] + if isinstance(lines, list): + self.lines = lines + else: + self.lines = [x for x in lines.splitlines() if x] self.fields = defaultdict(str) self.description = "" self.parse() diff --git a/core/checks.py b/core/checks.py index 0779d3d617..72561b4a57 100644 --- a/core/checks.py +++ b/core/checks.py @@ -58,7 +58,7 @@ async def check_permissions(ctx, command_name, permission_level) -> bool: # Administrators have permission to all non-owner commands return True - command_permissions = ctx.bot.config.command_permissions + command_permissions = ctx.bot.config['command_permissions'] author_roles = ctx.author.roles if command_name in command_permissions: @@ -71,7 +71,7 @@ async def check_permissions(ctx, command_name, permission_level) -> bool: has_perm_id = ctx.author.id in command_permissions[command_name] return has_perm_role or has_perm_id - level_permissions = ctx.bot.config.level_permissions + level_permissions = ctx.bot.config['level_permissions'] for level in PermissionLevel: if level >= permission_level and level.name in level_permissions: diff --git a/core/clients.py b/core/clients.py index 4551af64f8..69b18c1dc9 100644 --- a/core/clients.py +++ b/core/clients.py @@ -1,4 +1,3 @@ -import os import logging import secrets from datetime import datetime @@ -14,10 +13,6 @@ logger = logging.getLogger("Modmail") -prefix = os.getenv("LOG_URL_PREFIX", "/logs") -if prefix == "NONE": - prefix = "" - class RequestClient: """ @@ -34,14 +29,11 @@ class RequestClient: The Modmail bot. session : ClientSession The bot's current running `ClientSession`. - headers : Dict[str, str] - The HTTP headers that will be sent along with the requiest. """ def __init__(self, bot): self.bot = bot self.session = bot.session - self.headers: dict = None async def request( self, @@ -76,10 +68,6 @@ async def request( `str` if the returned data is not a valid json data, the raw response. """ - if headers is not None: - headers.update(self.headers) - else: - headers = self.headers async with self.session.request( method, url, headers=headers, json=payload ) as resp: @@ -90,176 +78,8 @@ async def request( except (JSONDecodeError, ClientResponseError): return await resp.text() - def filter_valid(self, data): - """ - Filters configuration keys that are accepted. - - Parameters - ---------- - data : Dict[str, Any] - The data that needs to be cleaned. - - Returns - ------- - Dict[str, Any] - Filtered `data` to keep only the accepted pairs. - """ - valid_keys = self.bot.config.valid_keys.difference( - self.bot.config.protected_keys - ) - return {k: v for k, v in data.items() if k in valid_keys} - - -class GitHub(RequestClient): - """ - The client for interacting with GitHub API. - - Parameters - ---------- - bot : Bot - The Modmail bot. - access_token : str, optional - GitHub's access token. - username : str, optional - GitHub username. - avatar_url : str, optional - URL to the avatar in GitHub. - url : str, optional - URL to the GitHub profile. - - Attributes - ---------- - bot : Bot - The Modmail bot. - access_token : str - GitHub's access token. - username : str - GitHub username. - avatar_url : str - URL to the avatar in GitHub. - url : str - URL to the GitHub profile. - - Class Attributes - ---------------- - BASE : str - GitHub API base URL. - REPO : str - Modmail repo URL for GitHub API. - HEAD : str - Modmail HEAD URL for GitHub API. - MERGE_URL : str - URL for merging upstream to master. - FORK_URL : str - URL to fork Modmail. - STAR_URL : str - URL to star Modmail. - """ - - BASE = "https://api.github.com" - REPO = BASE + "/repos/kyb3r/modmail" - HEAD = REPO + "/git/refs/heads/master" - MERGE_URL = BASE + "/repos/{username}/modmail/merges" - FORK_URL = REPO + "/forks" - STAR_URL = BASE + "/user/starred/kyb3r/modmail" - - def __init__(self, bot, access_token: str = "", username: str = "", **kwargs): - super().__init__(bot) - self.access_token = access_token - self.username = username - self.avatar_url: str = kwargs.pop("avatar_url", "") - self.url: str = kwargs.pop("url", "") - if self.access_token: - self.headers = {"Authorization": "token " + str(access_token)} - - async def update_repository(self, sha: str = None) -> Optional[dict]: - """ - Update the repository from Modmail main repo. - - Parameters - ---------- - sha : Optional[str], optional - The commit SHA to update the repository. - - Returns - ------- - Optional[dict] - If the response is a dict. - """ - if not self.username: - raise commands.CommandInvokeError("Username not found.") - - if sha is None: - resp: dict = await self.request(self.HEAD) - sha = resp["object"]["sha"] - - payload = {"base": "master", "head": sha, "commit_message": "Updating bot"} - - merge_url = self.MERGE_URL.format(username=self.username) - - resp = await self.request(merge_url, method="POST", payload=payload) - if isinstance(resp, dict): - return resp - - async def fork_repository(self) -> None: - """ - Forks Modmail's repository. - """ - await self.request(self.FORK_URL, method="POST") - - async def has_starred(self) -> bool: - """ - Checks if shared Modmail. - - Returns - ------- - bool - `True`, if Modmail was starred. - Otherwise `False`. - """ - resp = await self.request(self.STAR_URL, return_response=True) - return resp.status == 204 - - async def star_repository(self) -> None: - """ - Stars Modmail's repository. - """ - await self.request(self.STAR_URL, method="PUT", headers={"Content-Length": "0"}) - - @classmethod - async def login(cls, bot) -> "GitHub": - """ - Logs in to GitHub with configuration variable information. - - Parameters - ---------- - bot : Bot - The Modmail bot. - - Returns - ------- - GitHub - The newly created `GitHub` object. - """ - self = cls(bot, bot.config.get("github_access_token")) - resp: dict = await self.request("https://api.github.com/user") - self.username: str = resp["login"] - self.avatar_url: str = resp["avatar_url"] - self.url: str = resp["html_url"] - logger.info(info(f"GitHub logged in to: {self.username}")) - return self - class ApiClient(RequestClient): - def __init__(self, bot): - super().__init__(bot) - if self.token: - self.headers = {"Authorization": "Bearer " + self.token} - - @property - def token(self) -> Optional[str]: - return self.bot.config.get("github_access_token") - @property def db(self): return self.bot.db @@ -279,7 +99,7 @@ async def get_log(self, channel_id: Union[str, int]) -> dict: async def get_log_link(self, channel_id: Union[str, int]) -> str: doc = await self.get_log(channel_id) - return f"{self.bot.config.log_url.strip('/')}{prefix}/{doc['key']}" + return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{doc['key']}" async def create_log_entry( self, recipient: Member, channel: TextChannel, creator: Member @@ -315,7 +135,7 @@ async def create_log_entry( } ) - return f"{self.bot.config.log_url.strip('/')}{prefix}/{key}" + return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{key}" async def get_config(self) -> dict: conf = await self.db.config.find_one({"bot_id": self.bot.user.id}) @@ -325,12 +145,8 @@ async def get_config(self) -> dict: return conf async def update_config(self, data: dict): - valid_keys = self.bot.config.valid_keys.difference( - self.bot.config.protected_keys - ) - - toset = {k: v for k, v in data.items() if k in valid_keys} - unset = {k: 1 for k in valid_keys if k not in data} + toset = self.bot.config.filter_valid(data) + unset = self.bot.config.filter_valid({k: 1 for k in self.bot.config.all_keys if k not in data}) return await self.db.config.update_one( {"bot_id": self.bot.user.id}, {"$set": toset, "$unset": unset} @@ -386,28 +202,6 @@ async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: return_document=True, ) - async def update_repository(self) -> dict: - user = await GitHub.login(self.bot) - data = await user.update_repository() - return { - "data": data, - "user": { - "username": user.username, - "avatar_url": user.avatar_url, - "url": user.url, - }, - } - - async def get_user_info(self) -> dict: - user = await GitHub.login(self.bot) - return { - "user": { - "username": user.username, - "avatar_url": user.avatar_url, - "url": user.url, - } - } - class PluginDatabaseClient: def __init__(self, bot): diff --git a/core/config.py b/core/config.py index 6b083d54f7..b3c0645576 100644 --- a/core/config.py +++ b/core/config.py @@ -2,6 +2,7 @@ import json import os import typing +from copy import deepcopy import isodate @@ -15,181 +16,113 @@ class ConfigManager: - allowed_to_change_in_command = { + public_keys = { # activity - "twitch_url", + "twitch_url": 'https://www.twitch.tv/discord-modmail/', # bot settings - "main_category_id", - "disable_autoupdates", - "prefix", - "mention", - "main_color", - "user_typing", - "mod_typing", - "account_age", - "guild_age", - "reply_without_command", + "main_category_id": None, + "prefix": '?', + "mention": '@here', + "main_color": discord.Color.blurple(), + "user_typing": False, + "mod_typing": False, + "account_age": isodate.Duration(), + "guild_age": isodate.Duration(), + "reply_without_command": False, # logging - "log_channel_id", + "log_channel_id": None, # threads - "sent_emoji", - "blocked_emoji", - "close_emoji", - "disable_recipient_thread_close", - "thread_auto_close", - "thread_auto_close_response", - "thread_creation_response", - "thread_creation_footer", - "thread_creation_title", - "thread_close_footer", - "thread_close_title", - "thread_close_response", - "thread_self_close_response", + "sent_emoji": '✅', + "blocked_emoji": '🚫', + "close_emoji": '🔒', + "recipient_thread_close": False, + "thread_auto_close": 0, + "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": None, + "thread_creation_title": 'Thread Created', + "thread_close_footer": 'Replying will create a new thread', + "thread_close_title": 'Thread Closed', + "thread_close_response": '{closer.mention} has closed this Modmail thread.', + "thread_self_close_response": 'You have closed this Modmail thread.', # moderation - "recipient_color", - "mod_tag", - "mod_color", + "recipient_color": discord.Color.gold(), + "mod_tag": None, + "mod_color": discord.Color.green(), # anonymous message - "anon_username", - "anon_avatar_url", - "anon_tag", + "anon_username": None, + "anon_avatar_url": None, + "anon_tag": 'Response', } - internal_keys = { + private_keys = { # bot presence - "activity_message", - "activity_type", - "status", - "oauth_whitelist", + "activity_message": '', + "activity_type": None, + "status": None, + "oauth_whitelist": [], # moderation - "blocked", - "blocked_whitelist", - "command_permissions", - "level_permissions", + "blocked": {}, + "blocked_whitelist": [], + "command_permissions": {}, + "level_permissions": {}, # threads - "snippets", - "notification_squad", - "subscriptions", - "closures", + "snippets": {}, + "notification_squad": {}, + "subscriptions": {}, + "closures": {}, # misc - "aliases", - "plugins", + "plugins": [], + "aliases": {}, } protected_keys = { # Modmail - "modmail_guild_id", - "guild_id", - "log_url", - "mongo_uri", - "owners", + "modmail_guild_id": None, + "guild_id": None, + "log_url": 'https://example.com/', + "log_url_prefix": '/logs', + "mongo_uri": None, + "owners": None, # bot - "token", - # GitHub - "github_access_token", + "token": None, # Logging - "log_level", + "log_level": "INFO", } colors = {"mod_color", "recipient_color", "main_color"} time_deltas = {"account_age", "guild_age", "thread_auto_close"} - valid_keys = allowed_to_change_in_command | internal_keys | protected_keys + defaults = {**public_keys, **private_keys, **protected_keys} + all_keys = set(defaults.keys()) def __init__(self, bot): self.bot = bot self._cache = {} - self._ready_event = asyncio.Event() + self.ready_event = asyncio.Event() self.populate_cache() def __repr__(self): - return repr(self.cache) + return repr(self._cache) @property def api(self): return self.bot.api - @property - def ready_event(self) -> asyncio.Event: - return self._ready_event - - @property - def cache(self) -> dict: - return self._cache - - @cache.setter - def cache(self, val: dict): - self._cache = val - def populate_cache(self) -> dict: - defaults = { - "snippets": {}, - "plugins": [], - "aliases": {}, - "blocked": {}, - "blocked_whitelist": [], - "oauth_whitelist": [], - "command_permissions": {}, - "level_permissions": {}, - "notification_squad": {}, - "subscriptions": {}, - "closures": {}, - "twitch_url": 'https://www.twitch.tv/discord-modmail/', - "main_category_id": None, - "disable_autoupdates": False, - "prefix": '?', - "mention": None, - "main_color": discord.Color.blurple(), - "user_typing": False, - "mod_typing": False, - "account_age": None, - "guild_age": None, - "reply_without_command": False, - "log_channel_id": None, - "sent_emoji": '✅', - "blocked_emoji": '🚫', - "close_emoji": '🔒', - "recipient_thread_close": False, - "thread_auto_close": 0, - "thread_auto_close_response": None, - "thread_creation_response": "The staff team will get back to you as soon as possible.", - "thread_creation_footer": None, - "thread_creation_title": 'Thread Created', - "thread_close_footer": 'Replying will create a new thread', - "thread_close_title": 'Thread Closed', - "thread_close_response": '{closer.mention} has closed this Modmail thread.', - "thread_self_close_response": 'You have closed this Modmail thread.', - "recipient_color": discord.Color.gold(), - "mod_tag": None, - "mod_color": discord.Color.green(), - "anon_username": None, - "anon_avatar_url": None, - "anon_tag": 'Response', - "activity_message": None, - "activity_type": None, - "status": None, - "modmail_guild_id": None, - "guild_id": None, - "mongo_uri": None, - "owners": None, - "token": None, - "github_access_token": None, - "log_url": 'https://example.com/', - "log_level": "INFO", - } - - defaults.update(os.environ) + 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}) if os.path.exists("config.json"): with open("config.json") as f: # Config json should override env vars - data.update(json.load(f)) + data.update({k.lower(): v for k, v in json.load(f).items() if k.lower() in self.all_keys}) - self.cache = { - k.lower(): v for k, v in data.items() if k.lower() in self.valid_keys - } - return self.cache + self._cache = data + return self._cache async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: value_text = val @@ -200,32 +133,17 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: hex_ = ALL_COLORS.get(val) if hex_ is None: - if not isinstance(val, str): + hex_ = str(hex_) + if hex_.startswith("#"): + hex_ = hex_[1:] + if len(hex_) == 3: + hex_ = ''.join(s for s in hex_ for _ in range(2)) + if len(hex_) != 6: raise InvalidConfigError("Invalid color name or hex.") - if val.startswith("#"): - val = val[1:] - if len(val) != 6: + try: + int(val, 16) + except ValueError: raise InvalidConfigError("Invalid color name or hex.") - for letter in val: - if letter not in { - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "a", - "b", - "c", - "d", - "e", - "f", - }: - raise InvalidConfigError("Invalid color name or hex.") clean_value = "#" + val value_text = clean_value else: @@ -253,31 +171,54 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: return clean_value, value_text - async def update(self, data: typing.Optional[dict] = None) -> dict: + async def update(self): """Updates the config with data from the cache""" - if data is not None: - self.cache.update(data) - await self.api.update_config(self.cache) - return self.cache + await self.api.update_config(self._cache) async def refresh(self) -> dict: """Refreshes internal cache with data from database""" data = await self.api.get_config() - self.cache.update(data) + self._cache.update(data) self.ready_event.set() - return self.cache + return self._cache async def wait_until_ready(self) -> None: await self.ready_event.wait() - def __getattr__(self, value: str) -> typing.Any: - return self.cache[value] - def __setitem__(self, key: str, item: typing.Any) -> None: - self.cache[key] = item + if key not in self.all_keys: + raise InvalidConfigError(f'Configuration "{key}" is invalid.') + self._cache[key] = item def __getitem__(self, key: str) -> typing.Any: - return self.cache[key] + if key not in self.all_keys: + raise InvalidConfigError(f'Configuration "{key}" is invalid.') + if key not in self._cache: + val = deepcopy(self.defaults[key]) + self._cache[key] = val + return self._cache[key] def get(self, key: str, default: typing.Any = None) -> typing.Any: - return self.cache.get(key, default) + if key not in self.all_keys: + raise InvalidConfigError(f'Configuration "{key}" is invalid.') + if key not in self._cache: + self._cache[key] = default + return self._cache[key] + + def set(self, key: str, item: typing.Any) -> None: + if key not in self.all_keys: + raise InvalidConfigError(f'Configuration "{key}" is invalid.') + self._cache[key] = item + + def remove(self, key: str) -> typing.Any: + if key not in self.all_keys: + raise InvalidConfigError(f'Configuration "{key}" is invalid.') + self._cache[key] = deepcopy(self.defaults[key]) + return self._cache[key] + + def items(self) -> typing.Iterable: + return self._cache.items() + + def filter_valid(self, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + return {k.lower(): v for k, v in data.items() + if k.lower() in self.public_keys or k.lower() in self.private_keys} diff --git a/core/decorators.py b/core/decorators.py index 6e27dd2292..b9e6f50067 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -1,6 +1,5 @@ import functools -from discord import Embed, Color from discord.ext import commands diff --git a/core/thread.py b/core/thread.py index 107ec22773..8c0033238c 100644 --- a/core/thread.py +++ b/core/thread.py @@ -12,8 +12,7 @@ from discord.ext.commands import MissingRequiredArgument, CommandError from core.time import human_timedelta -from core.utils import is_image_url, days, match_user_id -from core.utils import truncate, ignore, error +from core.utils import is_image_url, days, match_user_id, truncate, ignore, error, strtobool logger = logging.getLogger("Modmail") @@ -103,16 +102,16 @@ async def setup(self, *, creator=None, category=None): reason="Creating a thread channel", ) except discord.HTTPException as e: # Failed to create due to 50 channel limit. - del self.manager.cache[self.id] - log_channel = self.bot.log_channel + self.manager.cache.pop(self.id) - em = discord.Embed(color=discord.Color.red()) - em.title = "Error while trying to create a thread" - em.description = e.message - em.add_field(name="Recipient", value=recipient.mention) + embed = discord.Embed(color=discord.Color.red()) + embed.title = "Error while trying to create a thread" + embed.description = e.message + embed.add_field(name="Recipient", value=recipient.mention) - if log_channel is not None: - return await log_channel.send(embed=em) + if self.bot.log_channel is not None: + await self.bot.log_channel.send(embed=embed) + return self._channel = channel @@ -123,7 +122,7 @@ async def setup(self, *, creator=None, category=None): ) log_count = sum(1 for log in log_data if not log["open"]) - except: # Something went wrong with database? + except Exception: # Something went wrong with database? log_url = log_count = None # ensure core functionality still works @@ -131,7 +130,7 @@ async def setup(self, *, creator=None, category=None): if creator: mention = None else: - mention = self.bot.config.get("mention", "@here") + mention = self.bot.config["mention"] async def send_genesis_message(): info_embed = self.manager.format_info_embed( @@ -141,7 +140,7 @@ async def send_genesis_message(): msg = await channel.send(mention, embed=info_embed) self.bot.loop.create_task(msg.pin()) self.genesis_message = msg - except Exception as e: + except Exception: pass finally: self.ready = True @@ -151,10 +150,7 @@ async def send_genesis_message(): self.bot.loop.create_task(send_genesis_message()) # Once thread is ready, tell the recipient. - thread_creation_response = self.bot.config.get( - "thread_creation_response", - "The staff team will get back to you as soon as possible.", - ) + thread_creation_response = self.bot.config["thread_creation_response"] embed = discord.Embed( color=self.bot.mod_color, @@ -163,17 +159,26 @@ async def send_genesis_message(): ) footer = "Your message has been sent" - if self.bot.config.get("recipient_thread_close"): + try: + recipient_thread_close = strtobool(self.bot.config["recipient_thread_close"]) + except ValueError: + recipient_thread_close = self.bot.config.remove('recipient_thread_close') + + if recipient_thread_close: footer = "Click the lock to close the thread" - footer = self.bot.config.get("thread_creation_footer", footer) + _footer = self.bot.config["thread_creation_footer"] + if _footer is not None: + footer = _footer + embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) - embed.title = self.bot.config.get("thread_creation_title", "Thread Created") + embed.title = self.bot.config["thread_creation_title"] if creator is None: msg = await recipient.send(embed=embed) - if self.bot.config.get("recipient_thread_close"): - close_emoji = self.bot.config.get("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) @@ -211,7 +216,7 @@ async def close( "message": message, "auto_close": auto_close, } - self.bot.config.closures[str(self.id)] = items + self.bot.config['closures'][str(self.id)] = items await self.bot.config.update() task = self.bot.loop.call_later( @@ -228,14 +233,14 @@ async def close( async def _close( self, closer, silent=False, delete_channel=True, message=None, scheduled=False ): - del self.manager.cache[self.id] + self.manager.cache.pop(self.id) await self.cancel_closure(all=True) # Cancel auto closing the thread if closed by any means. - if str(self.id) in self.bot.config.subscriptions: - del self.bot.config.subscriptions[str(self.id)] + if str(self.id) in self.bot.config['subscriptions']: + self.bot.config['subscriptions'].pop(str(self.id)) # Logging log_data = await self.bot.api.post_log( @@ -255,10 +260,10 @@ async def _close( ) if log_data is not None and isinstance(log_data, dict): - prefix = os.getenv("LOG_URL_PREFIX", "/logs") + prefix = self.bot.config['log_url_prefix'] if prefix == "NONE": prefix = "" - log_url = f"{self.bot.config.log_url.strip('/')}{prefix}/{log_data['key']}" + log_url = f"{self.bot.config['log_url'].strip('/')}{prefix}/{log_data['key']}" if log_data["messages"]: content = str(log_data["messages"][0]["content"]) @@ -293,36 +298,27 @@ async def _close( tasks = [self.bot.config.update()] - try: + if self.bot.log_channel is not None: tasks.append(self.bot.log_channel.send(embed=embed)) - except (ValueError, AttributeError): - pass # Thread closed message embed = discord.Embed( - title=self.bot.config.get("thread_close_title", "Thread Closed"), + title=self.bot.config["thread_close_title"], color=discord.Color.red(), timestamp=datetime.utcnow(), ) if not message: if self.id == closer.id: - message = self.bot.config.get( - "thread_self_close_response", "You have closed this Modmail thread." - ) + message = self.bot.config["thread_self_close_response"] else: - message = self.bot.config.get( - "thread_close_response", - "{closer.mention} has closed this Modmail thread.", - ) + message = self.bot.config["thread_close_response"] message = message.format(closer=closer, loglink=log_url, logkey=log_data["key"]) embed.description = message - footer = self.bot.config.get( - "thread_close_footer", "Replying will create a new thread" - ) + footer = self.bot.config["thread_close_footer"] embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) if not silent and self.recipient is not None: @@ -341,7 +337,7 @@ async def cancel_closure(self, auto_close: bool = False, all: bool = False) -> N self.auto_close_task.cancel() self.auto_close_task = None - to_update = self.bot.config.closures.pop(str(self.id), None) + to_update = self.bot.config['closures'].pop(str(self.id), None) if to_update is not None: await self.bot.config.update() @@ -364,8 +360,8 @@ async def _fetch_timeout( :returns: None if no timeout is set. """ - timeout = self.bot.config.get("thread_auto_close") - if timeout is None: + timeout = self.bot.config["thread_auto_close"] + if not timeout: return timeout else: try: @@ -377,9 +373,9 @@ async def _fetch_timeout( 'greater than 0 days, not "%s".', str(timeout), ) - del self.bot.config.cache["thread_auto_close"] + timeout = self.bot.config.remove("thread_auto_close") await self.bot.config.update() - timeout = None + return timeout async def _restart_close_timer(self): @@ -390,7 +386,7 @@ async def _restart_close_timer(self): timeout = await self._fetch_timeout() # Exit if timeout was not set - if timeout is None: + if not timeout: return # Set timeout seconds @@ -400,22 +396,19 @@ async def _restart_close_timer(self): human_time = human_timedelta(dt=reset_time) # Grab message - close_message = self.bot.config.get( - "thread_auto_close_response", - f"This thread has been closed automatically due to inactivity " - f"after {human_time}.", - ) + close_message = self.bot.config["thread_auto_close_response"].format(timeout=human_time) + time_marker_regex = "%t" if len(re.findall(time_marker_regex, close_message)) == 1: close_message = re.sub(time_marker_regex, str(human_time), close_message) elif len(re.findall(time_marker_regex, close_message)) > 1: logger.warning( - "The thread_auto_close_response should only contain one" - f" '{time_marker_regex}' to specify time." + "The thread_auto_close_response should only contain one '%s' to specify time.", + time_marker_regex ) await self.close( - closer=self.bot.user, after=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: @@ -568,11 +561,15 @@ async def send( and not isinstance(destination, discord.TextChannel) ): # Anonymously sending to the user. - tag = self.bot.config.get("mod_tag", str(message.author.top_role)) - name = self.bot.config.get("anon_username", tag) - avatar_url = self.bot.config.get( - "anon_avatar_url", self.bot.guild.icon_url - ) + tag = self.bot.config['mod_tag'] + if tag is None: + tag = str(message.author.top_role) + name = self.bot.config['anon_username'] + if name is None: + name = tag + avatar_url = self.bot.config['anon_avatar_url'] + if avatar_url is None: + avatar_url = self.bot.guild.icon_url else: # Normal message name = str(author) @@ -597,7 +594,7 @@ async def send( image_links = [ (link, None) for link in re.findall( - r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", message.content, ) ] @@ -655,10 +652,12 @@ async def send( embed.set_footer(text="Anonymous Reply") # Normal messages elif not anonymous: - tag = self.bot.config.get("mod_tag", str(message.author.top_role)) - embed.set_footer(text=tag) # Normal messages + mod_tag = self.bot.config['mod_tag'] + if mod_tag is None: + mod_tag = str(message.author.top_role) + embed.set_footer(text=mod_tag) # Normal messages else: - embed.set_footer(text=self.bot.config.get("anon_tag", "Response")) + embed.set_footer(text=self.bot.config["anon_tag"]) elif note: # noinspection PyUnresolvedReferences,PyDunderSlots embed.color = discord.Color.blurple() # pylint: disable=E0237 @@ -687,16 +686,15 @@ async def send( return _msg def get_notifications(self) -> str: - config = self.bot.config key = str(self.id) mentions = [] - mentions.extend(config["subscriptions"].get(key, [])) + mentions.extend(self.bot.config["subscriptions"].get(key, [])) - if key in config["notification_squad"]: - mentions.extend(config["notification_squad"][key]) - del config["notification_squad"][key] - self.bot.loop.create_task(config.update()) + if key in self.bot.config["notification_squad"]: + mentions.extend(self.bot.config["notification_squad"][key]) + self.bot.config["notification_squad"].pop(key) + self.bot.loop.create_task(self.bot.config.update()) return " ".join(mentions) diff --git a/core/time.py b/core/time.py index c001bebcd1..a99efee78b 100644 --- a/core/time.py +++ b/core/time.py @@ -188,7 +188,7 @@ async def convert(self, ctx, argument): if not (end < len(argument) and argument[end] == '"'): raise BadArgument("If the time is quoted, you must unquote it.") - remaining = argument[end + 1 :].lstrip(" ,.!") + remaining = argument[end + 1:].lstrip(" ,.!") else: remaining = argument[end:].lstrip(" ,.!") elif len(argument) == end: diff --git a/core/utils.py b/core/utils.py index bf88af1bff..ca97c658cd 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,5 +1,6 @@ import re import typing +from distutils.util import strtobool as _stb from urllib import parse from discord import Object @@ -9,6 +10,12 @@ from core.models import PermissionLevel +def strtobool(val): + if isinstance(val, bool): + return val + return _stb(str(val)) + + def info(*msgs): return f'{Fore.CYAN}{" ".join(msgs)}{Style.RESET_ALL}' From 6c756a108f36825d96a1b5bc025bc018f59cc4b4 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 1 Jul 2019 22:22:55 -0700 Subject: [PATCH 0300/1221] Fixed a db problem and new config var --- CHANGELOG.md | 5 +++-- bot.py | 23 +++++++++++------------ cogs/modmail.py | 7 +++++++ core/clients.py | 15 ++++++++++++--- core/config.py | 13 +++++++------ core/thread.py | 9 +++------ 6 files changed, 43 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adcaf554e5..cdc1a393eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,21 +5,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# v3.1.0 +# [UNRELEASED] ### Breaking - `disable_recipient_thread_close` is removed, a new configuration variable `recipient_thread_close` replaces it which defaults to False. - Truthy and falsy values for binary configuration variables are now interpreted respectfully. - ### Changes - `thread_auto_close_response` has a configurable variable `{timeout}`. +- New configuration variable `thread_self_closable_creation_footer`, the footer when `recipient_thread_close` is enabled. ### Internal - Removed supporting code for GitHub interaction. +- All default values moved to `core/config.py`. # v3.0.3 diff --git a/bot.py b/bot.py index 406521dac4..990f826676 100644 --- a/bot.py +++ b/bot.py @@ -304,37 +304,31 @@ def prefix(self) -> str: return str(self.config["prefix"]) @property - def mod_color(self) -> typing.Union[discord.Color, int]: + def mod_color(self) -> int: color = self.config.get("mod_color") - if isinstance(color, discord.Color): - return color try: return int(color.lstrip("#"), base=16) except ValueError: logger.error(error("Invalid mod_color provided.")) - return self.config.remove('mod_color') + return int(self.config.remove('mod_color').lstrip("#"), base=16) @property - def recipient_color(self) -> typing.Union[discord.Color, int]: + def recipient_color(self) -> int: color = self.config.get("recipient_color") - if isinstance(color, discord.Color): - return color try: return int(color.lstrip("#"), base=16) except ValueError: logger.error(error("Invalid recipient_color provided.")) - return self.config.remove('recipient_color') + return int(self.config.remove('recipient_color').lstrip("#"), base=16) @property - def main_color(self) -> typing.Union[discord.Color, int]: + def main_color(self) -> int: color = self.config.get("main_color") - if isinstance(color, discord.Color): - return color try: return int(color.lstrip("#"), base=16) except ValueError: logger.error(error("Invalid main_color provided.")) - return self.config.remove('main_color') + return int(self.config.remove('main_color').lstrip("#"), base=16) async def on_connect(self): logger.info(LINE) @@ -478,6 +472,11 @@ async def _process_blocked(self, message: discord.Message) -> bool: account_age = self.config["account_age"] guild_age = self.config["guild_age"] + if account_age is None: + account_age = isodate.Duration() + if guild_age is None: + guild_age = isodate.Duration() + if not isinstance(account_age, isodate.Duration): try: account_age = isodate.parse_duration(account_age) diff --git a/cogs/modmail.py b/cogs/modmail.py index 96d95ac096..4d9f126510 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -197,6 +197,13 @@ async def snippets_remove(self, ctx, *, name: str.lower): @snippets.command(name="edit") @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippets_edit(self, ctx, name: str.lower, *, value): + """ + Edit a snippet. + + To edit a multi-word snippet name, use quotes: ``` + {prefix}snippets edit "two word" this is a new two word snippet. + ``` + """ if name in self.bot.snippets: self.bot.snippets[name] = value await self.bot.config.update() diff --git a/core/clients.py b/core/clients.py index 69b18c1dc9..48da5f8518 100644 --- a/core/clients.py +++ b/core/clients.py @@ -148,9 +148,18 @@ async def update_config(self, data: dict): toset = self.bot.config.filter_valid(data) unset = self.bot.config.filter_valid({k: 1 for k in self.bot.config.all_keys if k not in data}) - return await self.db.config.update_one( - {"bot_id": self.bot.user.id}, {"$set": toset, "$unset": unset} - ) + if toset and unset: + return await self.db.config.update_one( + {"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} + ) + if 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( diff --git a/core/config.py b/core/config.py index a745ae3231..fcd6ada080 100644 --- a/core/config.py +++ b/core/config.py @@ -27,11 +27,11 @@ class ConfigManager: "main_category_id": None, "prefix": '?', "mention": '@here', - "main_color": discord.Color.blurple(), + "main_color": '#7289da', "user_typing": False, "mod_typing": False, - "account_age": isodate.Duration(), - "guild_age": isodate.Duration(), + "account_age": None, + "guild_age": None, "reply_without_command": False, # logging "log_channel_id": None, @@ -43,16 +43,17 @@ class ConfigManager: "thread_auto_close": 0, "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": None, + "thread_creation_footer": 'Your message has been sent', + "thread_self_closable_creation_footer": "Click the lock to close the thread", "thread_creation_title": 'Thread Created', "thread_close_footer": 'Replying will create a new thread', "thread_close_title": 'Thread Closed', "thread_close_response": '{closer.mention} has closed this Modmail thread.', "thread_self_close_response": 'You have closed this Modmail thread.', # moderation - "recipient_color": discord.Color.gold(), + "recipient_color": '#f1c40f', "mod_tag": None, - "mod_color": discord.Color.green(), + "mod_color": '#2ecc71', # anonymous message "anon_username": None, "anon_avatar_url": None, diff --git a/core/thread.py b/core/thread.py index 8c0033238c..93cee5007b 100644 --- a/core/thread.py +++ b/core/thread.py @@ -158,18 +158,15 @@ async def send_genesis_message(): timestamp=channel.created_at, ) - footer = "Your message has been sent" try: recipient_thread_close = strtobool(self.bot.config["recipient_thread_close"]) except ValueError: recipient_thread_close = self.bot.config.remove('recipient_thread_close') if recipient_thread_close: - footer = "Click the lock to close the thread" - - _footer = self.bot.config["thread_creation_footer"] - if _footer is not None: - footer = _footer + 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"] From 863fdeb5bb537e812cec40f4911a52d1be8199b2 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 2 Jul 2019 12:29:24 -0700 Subject: [PATCH 0301/1221] Some fixes --- CHANGELOG.md | 4 +++- bot.py | 6 +++--- core/clients.py | 5 +---- core/config.py | 43 +++++++++++++++++++++++++++++-------------- core/models.py | 7 +++++++ 5 files changed, 43 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdc1a393eb..f09660b7b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Internal - Removed supporting code for GitHub interaction. -- All default values moved to `core/config.py`. +- All default config values moved to `core/config.py`. +- `config.cache` is no longer accessible, use `config['key']` for getting, `config['key'] = value` for setting, `config.remove('key')` for removing. +- Dynamic attribute for configs are removed, must use `config['key']` or `config.get('key')`. # v3.0.3 diff --git a/bot.py b/bot.py index 990f826676..dcff051e42 100644 --- a/bot.py +++ b/bot.py @@ -305,7 +305,7 @@ def prefix(self) -> str: @property def mod_color(self) -> int: - color = self.config.get("mod_color") + color = self.config["mod_color"] try: return int(color.lstrip("#"), base=16) except ValueError: @@ -314,7 +314,7 @@ def mod_color(self) -> int: @property def recipient_color(self) -> int: - color = self.config.get("recipient_color") + color = self.config["recipient_color"] try: return int(color.lstrip("#"), base=16) except ValueError: @@ -323,7 +323,7 @@ def recipient_color(self) -> int: @property def main_color(self) -> int: - color = self.config.get("main_color") + color = self.config["main_color"] try: return int(color.lstrip("#"), base=16) except ValueError: diff --git a/core/clients.py b/core/clients.py index 48da5f8518..2924c12488 100644 --- a/core/clients.py +++ b/core/clients.py @@ -2,15 +2,12 @@ import secrets from datetime import datetime from json import JSONDecodeError -from typing import Union, Optional +from typing import Union from discord import Member, DMChannel, TextChannel, Message -from discord.ext import commands from aiohttp import ClientResponseError, ClientResponse -from core.utils import info - logger = logging.getLogger("Modmail") diff --git a/core/config.py b/core/config.py index fcd6ada080..399f723f6f 100644 --- a/core/config.py +++ b/core/config.py @@ -1,5 +1,6 @@ import asyncio import json +import logging import os import typing from copy import deepcopy @@ -11,10 +12,10 @@ from discord.ext.commands import BadArgument from core._color_data import ALL_COLORS -from core.models import InvalidConfigError +from core.models import InvalidConfigError, Default from core.time import UserFriendlyTime - +logger = logging.getLogger("Modmail") load_dotenv() @@ -27,7 +28,7 @@ class ConfigManager: "main_category_id": None, "prefix": '?', "mention": '@here', - "main_color": '#7289da', + "main_color": str(discord.Color.blurple()), "user_typing": False, "mod_typing": False, "account_age": None, @@ -51,9 +52,9 @@ class ConfigManager: "thread_close_response": '{closer.mention} has closed this Modmail thread.', "thread_self_close_response": 'You have closed this Modmail thread.', # moderation - "recipient_color": '#f1c40f', + "recipient_color": str(discord.Color.gold()), "mod_tag": None, - "mod_color": '#2ecc71', + "mod_color": str(discord.Color.green()), # anonymous message "anon_username": None, "anon_avatar_url": None, @@ -178,19 +179,22 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: async def update(self): """Updates the config with data from the cache""" - await self.api.update_config(self._cache) + await self.api.update_config(self.filter_default(self._cache)) async def refresh(self) -> dict: """Refreshes internal cache with data from database""" - data = await self.api.get_config() + data = {k.lower(): v for k, v in (await self.api.get_config()).items() if k.lower() in self.all_keys} self._cache.update(data) - self.ready_event.set() + if not self.ready_event.is_set(): + self.ready_event.set() + logger.info('Config ready.') return self._cache async def wait_until_ready(self) -> None: await self.ready_event.wait() def __setitem__(self, key: str, item: typing.Any) -> None: + logger.info('Setting %s.', key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') self._cache[key] = item @@ -199,23 +203,29 @@ def __getitem__(self, key: str) -> typing.Any: if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') if key not in self._cache: - val = deepcopy(self.defaults[key]) - self._cache[key] = val + self._cache[key] = deepcopy(self.defaults[key]) return self._cache[key] - def get(self, key: str, default: typing.Any = None) -> typing.Any: + def get(self, key: str, default: typing.Any = Default) -> typing.Any: if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') if key not in self._cache: - self._cache[key] = default + self._cache[key] = deepcopy(self.defaults[key]) + if default is Default: + return self._cache[key] + return default + if self._cache[key] == self.defaults[key] and default is not Default: + return default return self._cache[key] def set(self, key: str, item: typing.Any) -> None: + logger.info('Setting %s.', key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') self._cache[key] = item def remove(self, key: str) -> typing.Any: + logger.info('Removing %s.', key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') self._cache[key] = deepcopy(self.defaults[key]) @@ -224,6 +234,11 @@ def remove(self, key: str) -> typing.Any: def items(self) -> typing.Iterable: return self._cache.items() - def filter_valid(self, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + @classmethod + 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() - if k.lower() in self.public_keys or k.lower() in self.private_keys} + if k.lower() in cls.public_keys or k.lower() in cls.private_keys} + + @classmethod + def filter_default(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + return {k.lower(): v for k, v in data.items() if v != cls.defaults[k.lower()]} diff --git a/core/models.py b/core/models.py index 3877ab23d6..36e480f809 100644 --- a/core/models.py +++ b/core/models.py @@ -24,3 +24,10 @@ def __init__(self, msg, *args): @property def embed(self): return Embed(title="Error", description=self.msg, color=Color.red()) + + +class _Default: + pass + + +Default = _Default() From f3afd990a7b3d5de548a425c7feafb1804f5c36a Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 3 Jul 2019 13:30:20 -0700 Subject: [PATCH 0302/1221] Some fixes + changelog --- CHANGELOG.md | 9 +++++++++ bot.py | 4 +++- cogs/modmail.py | 34 +++++++++++++++++++++------------- cogs/utility.py | 11 +++++++---- core/config.py | 1 + core/thread.py | 4 ++-- 6 files changed, 43 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f09660b7b9..c1969c6cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `thread_auto_close_response` has a configurable variable `{timeout}`. - New configuration variable `thread_self_closable_creation_footer`, the footer when `recipient_thread_close` is enabled. +- `?snippet` is now the default command name instead of `?snippets` (`?snippets` is still usable). This is to make this consistent with `?alias`/`?aliases`. + +### Fixes + +- `?notify` no longer carries over to the next thread. + +### Added + +- `?sfw`, mark a thread as "safe for work", undos `?nsfw`. ### Internal diff --git a/bot.py b/bot.py index dcff051e42..d6b89df692 100644 --- a/bot.py +++ b/bot.py @@ -173,6 +173,8 @@ def _load_extensions(self): def run(self, *args, **kwargs): try: self.loop.run_until_complete(self.start(self.token)) + except KeyboardInterrupt: + pass except discord.LoginFailure: logger.critical(error("Invalid token")) except Exception: @@ -391,7 +393,7 @@ async def on_ready(self): info(f"There are {len(closures)} thread(s) pending to be closed.") ) - for recipient_id, items in closures.items(): + for recipient_id, items in tuple(closures.items()): after = ( datetime.fromisoformat(items["time"]) - datetime.utcnow() ).total_seconds() diff --git a/cogs/modmail.py b/cogs/modmail.py index 4d9f126510..3d3450d7d0 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -90,19 +90,19 @@ async def setup(self, ctx): await self.bot.update_perms(PermissionLevel.REGULAR, -1) await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id) - @commands.group(invoke_without_command=True) + @commands.group(aliases=['snippets'], invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) - async def snippets(self, ctx): + async def snippet(self, ctx): """ Create pre-defined messages for use in threads. - When `{prefix}snippets` is used by itself, this will retrieve + When `{prefix}snippet` is used by itself, this will retrieve a list of snippets that are currently set. To use snippets: First create a snippet using: - - `{prefix}snippets add snippet-name A pre-defined text.` + - `{prefix}snippet add snippet-name A pre-defined text.` Afterwards, you can use your snippet in a thread channel with `{prefix}snippet-name`, the message "A pre-defined text." @@ -125,7 +125,7 @@ async def snippets(self, ctx): description="You dont have any snippets at the moment.", ) embed.set_footer( - text=f"Do {self.bot.prefix}help snippets for more commands." + text=f"Do {self.bot.prefix}help snippet for more commands." ) embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) @@ -143,14 +143,14 @@ async def snippets(self, ctx): session = PaginatorSession(ctx, *embeds) await session.run() - @snippets.command(name="add") + @snippet.command(name="add") @checks.has_permissions(PermissionLevel.SUPPORTER) - async def snippets_add(self, ctx, name: str.lower, *, value): + async def snippet_add(self, ctx, name: str.lower, *, value): """ Add a snippet. To add a multi-word snippet name, use quotes: ``` - {prefix}snippets add "two word" this is a two word snippet. + {prefix}snippet add "two word" this is a two word snippet. ``` """ if name in self.bot.snippets: @@ -171,9 +171,9 @@ async def snippets_add(self, ctx, name: str.lower, *, value): await ctx.send(embed=embed) - @snippets.command(name="remove", aliases=["del", "delete", "rm"]) + @snippet.command(name="remove", aliases=["del", "delete", "rm"]) @checks.has_permissions(PermissionLevel.SUPPORTER) - async def snippets_remove(self, ctx, *, name: str.lower): + async def snippet_remove(self, ctx, *, name: str.lower): """Remove a snippet.""" if name in self.bot.snippets: @@ -194,14 +194,14 @@ async def snippets_remove(self, ctx, *, name: str.lower): await ctx.send(embed=embed) - @snippets.command(name="edit") + @snippet.command(name="edit") @checks.has_permissions(PermissionLevel.SUPPORTER) - async def snippets_edit(self, ctx, name: str.lower, *, value): + async def snippet_edit(self, ctx, name: str.lower, *, value): """ Edit a snippet. To edit a multi-word snippet name, use quotes: ``` - {prefix}snippets edit "two word" this is a new two word snippet. + {prefix}snippet edit "two word" this is a new two word snippet. ``` """ if name in self.bot.snippets: @@ -502,6 +502,14 @@ async def nsfw(self, ctx): await ctx.channel.edit(nsfw=True) await ctx.message.add_reaction("✅") + @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def sfw(self, ctx): + """Flags a Modmail thread as SFW (safe for work).""" + await ctx.channel.edit(nsfw=False) + await ctx.message.add_reaction("✅") + @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() diff --git a/cogs/utility.py b/cogs/utility.py index d1909df4da..e271cfc0a3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -787,14 +787,14 @@ async def alias(self, ctx): To use alias: - First create a snippet using: + First create an alias using: - `{prefix}alias add alias-name other-command` For example: - `{prefix}alias add r reply` - Now you can use `{prefix}r` as an replacement for `{prefix}reply`. - See also `{prefix}snippets`. + See also `{prefix}snippet`. """ embeds = [] @@ -808,7 +808,7 @@ async def alias(self, ctx): description="You dont have any aliases at the moment.", ) embed.set_author(name="Command aliases", icon_url=ctx.guild.icon_url) - embed.set_footer(text=f"Do {self.bot.prefix}" "help aliases for more commands.") + embed.set_footer(text=f"Do {self.bot.prefix}help alias for more commands.") embeds.append(embed) for name, value in self.bot.aliases.items(): @@ -816,7 +816,7 @@ async def alias(self, ctx): embed = Embed(color=self.bot.main_color, description=desc) embed.set_author(name="Command aliases", icon_url=ctx.guild.icon_url) embed.set_footer( - text=f"Do {self.bot.prefix}help " "aliases for more commands." + text=f"Do {self.bot.prefix}help alias for more commands." ) embeds.append(embed) @@ -886,6 +886,9 @@ async def alias_remove(self, ctx, *, name: str.lower): @alias.command(name="edit") @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_edit(self, ctx, name: str.lower, *, value): + """ + Edit an alias. + """ if name not in self.bot.aliases: embed = Embed( title="Error", diff --git a/core/config.py b/core/config.py index 399f723f6f..2a774ae6eb 100644 --- a/core/config.py +++ b/core/config.py @@ -241,4 +241,5 @@ def filter_valid(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, ty @classmethod def filter_default(cls, data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + # TODO: use .get to prevent errors return {k.lower(): v for k, v in data.items() if v != cls.defaults[k.lower()]} diff --git a/core/thread.py b/core/thread.py index 93cee5007b..843acb9fd7 100644 --- a/core/thread.py +++ b/core/thread.py @@ -236,8 +236,8 @@ async def _close( # Cancel auto closing the thread if closed by any means. - if str(self.id) in self.bot.config['subscriptions']: - self.bot.config['subscriptions'].pop(str(self.id)) + self.bot.config['subscriptions'].pop(str(self.id), None) + self.bot.config['notification_squad'].pop(str(self.id), None) # Logging log_data = await self.bot.api.post_log( From 2bb545fd7d6dee70bfebbdd9973a7ac4ca126a9b Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 5 Jul 2019 19:10:11 -0700 Subject: [PATCH 0303/1221] Update - see changelog --- CHANGELOG.md | 3 +- bot.py | 196 ++++++++++++++++++++++-------------------------- cogs/modmail.py | 10 +-- cogs/plugins.py | 13 ++-- cogs/utility.py | 21 ++---- core/checks.py | 9 +-- core/config.py | 6 +- core/models.py | 42 +++++++++++ core/thread.py | 5 +- core/time.py | 4 +- core/utils.py | 9 --- 11 files changed, 161 insertions(+), 157 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1969c6cc9..7b37abfc87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixes - `?notify` no longer carries over to the next thread. - +- `discord.NotFound` errors for `on_raw_reaction_add`. ### Added - `?sfw`, mark a thread as "safe for work", undos `?nsfw`. @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - All default config values moved to `core/config.py`. - `config.cache` is no longer accessible, use `config['key']` for getting, `config['key'] = value` for setting, `config.remove('key')` for removing. - Dynamic attribute for configs are removed, must use `config['key']` or `config.get('key')`. +- Removed helper functions `info()` and `error()` for formatting logging, it's formatted automatically now. # v3.0.3 diff --git a/bot.py b/bot.py index d6b89df692..8726a9a7cf 100644 --- a/bot.py +++ b/bot.py @@ -17,20 +17,20 @@ import isodate from aiohttp import ClientSession -from colorama import init, Fore, Style from emoji import UNICODE_EMOJI from motor.motor_asyncio import AsyncIOMotorClient from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import info, error, human_join, strtobool -from core.models import PermissionLevel +from core.utils import human_join, strtobool +from core.models import PermissionLevel, ModmailLogger from core.thread import ThreadManager from core.time import human_timedelta -init() -logger = logging.getLogger("Modmail") +logger: ModmailLogger = logging.getLogger("Modmail") +logger.__class__ = ModmailLogger + logger.setLevel(logging.INFO) ch = logging.StreamHandler(stream=sys.stdout) @@ -52,17 +52,18 @@ def format(self, record): if not os.path.exists(temp_dir): os.mkdir(temp_dir) -LINE = Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL - class ModmailBot(commands.Bot): def __init__(self): super().__init__(command_prefix=None) # implemented in `get_prefix` - self._session = ClientSession(loop=self.loop) - self._config = ConfigManager(self) - - self.start_time = datetime.utcnow() + self._session = None + self._api = None self._connected = asyncio.Event() + self.start_time = datetime.utcnow() + + self.config = ConfigManager(self) + self.config.populate_cache() + self.threads = ThreadManager(self) self._configure_logging() @@ -70,9 +71,7 @@ def __init__(self): if mongo_uri is None: raise ValueError('A Mongo URI is necessary for the bot to function.') - self._db = AsyncIOMotorClient(mongo_uri).modmail_bot - self._threads = ThreadManager(self) - self.api = ApiClient(self) + self.db = AsyncIOMotorClient(mongo_uri).modmail_bot self.plugin_db = PluginDatabaseClient(self) self.metadata_task = self.loop.create_task(self.metadata_loop()) @@ -116,59 +115,56 @@ def _configure_logging(self): logger.addHandler(ch_debug) log_level = logging_levels.get(level_text) - logger.info(LINE) + if log_level is None: + log_level = self.config.remove('log_level') + + logger.line() if log_level is not None: logger.setLevel(log_level) ch.setLevel(log_level) - logger.info(info("Logging level: " + level_text)) + logger.info("Logging level: " + level_text) else: - logger.info(error("Invalid logging level set. ")) - logger.info(info("Using default logging level: INFO")) + logger.info("Invalid logging level set. ") + logger.warning("Using default logging level: INFO") @property def version(self) -> str: return __version__ - @property - def db(self) -> AsyncIOMotorClient: - if self._db is None: - raise ValueError('No database instance found.') - return self._db - - @property - def config(self) -> ConfigManager: - return self._config - @property def session(self) -> ClientSession: + if self._session is None: + self._session = ClientSession(loop=self.loop) return self._session @property - def threads(self) -> ThreadManager: - return self._threads + def api(self): + if self._api is None: + self._api = ApiClient(self) + return self._api async def get_prefix(self, message=None): return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] def _load_extensions(self): """Adds commands automatically""" - logger.info(LINE) - logger.info(info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬")) - logger.info(info("││││ │ │││││├─┤││")) - logger.info(info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘")) - logger.info(info(f"v{__version__}")) - logger.info(info("Authors: kyb3r, fourjr, Taaku18")) - logger.info(LINE) + logger.line() + logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") + logger.info("││││ │ │││││├─┤││") + logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") + logger.info(f"v{__version__}") + logger.info("Authors: kyb3r, fourjr, Taaku18") + logger.line() for file in os.listdir("cogs"): if not file.endswith(".py"): continue cog = f"cogs.{file[:-3]}" - logger.info(info(f"Loading {cog}")) + logger.info(f"Loading {cog}") try: self.load_extension(cog) except Exception: - logger.exception(error(f"Failed to load {cog}")) + logger.exception(f"Failed to load {cog}") def run(self, *args, **kwargs): try: @@ -176,27 +172,26 @@ def run(self, *args, **kwargs): except KeyboardInterrupt: pass except discord.LoginFailure: - logger.critical(error("Invalid token")) + logger.critical("Invalid token") except Exception: - logger.critical(error("Fatal exception"), exc_info=True) + logger.critical("Fatal exception", exc_info=True) finally: try: self.metadata_task.cancel() self.loop.run_until_complete(self.metadata_task) except asyncio.CancelledError: - logger.debug(info("metadata_task has been cancelled.")) + logger.debug("metadata_task has been cancelled.") self.loop.run_until_complete(self.logout()) - for task in asyncio.Task.all_tasks(): + for task in asyncio.all_tasks(self.loop): task.cancel() try: - self.loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks())) + self.loop.run_until_complete(asyncio.gather(*asyncio.all_tasks(self.loop))) except asyncio.CancelledError: - logger.debug(info("All pending tasks has been cancelled.")) + logger.debug("All pending tasks has been cancelled.") finally: self.loop.run_until_complete(self.session.close()) - self.loop.close() - logger.info(error(" - Shutting down bot - ")) + logger.error(" - Shutting down bot - ") async def is_owner(self, user: discord.User) -> bool: owners = self.config['owners'] @@ -218,8 +213,8 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: return self.main_category.channels[0] except IndexError: pass - logger.info(info(f'No log channel set, set one with `%ssetup` or ' - f'`%sconfig set log_channel_id `.'), self.prefix, self.prefix) + logger.info(f'No log channel set, set one with `%ssetup` or ' + f'`%sconfig set log_channel_id `.', self.prefix, self.prefix) @property def is_connected(self) -> bool: @@ -275,7 +270,7 @@ def modmail_guild(self) -> typing.Optional[discord.Guild]: if guild is not None: return guild self.config.remove('modmail_guild_id') - logger.error(error('Invalid modmail_guild_id set.')) + logger.error('Invalid modmail_guild_id set.') return self.guild @property @@ -311,7 +306,7 @@ def mod_color(self) -> int: try: return int(color.lstrip("#"), base=16) except ValueError: - logger.error(error("Invalid mod_color provided.")) + logger.error("Invalid mod_color provided.") return int(self.config.remove('mod_color').lstrip("#"), base=16) @property @@ -320,7 +315,7 @@ def recipient_color(self) -> int: try: return int(color.lstrip("#"), base=16) except ValueError: - logger.error(error("Invalid recipient_color provided.")) + logger.error("Invalid recipient_color provided.") return int(self.config.remove('recipient_color').lstrip("#"), base=16) @property @@ -329,18 +324,18 @@ def main_color(self) -> int: try: return int(color.lstrip("#"), base=16) except ValueError: - logger.error(error("Invalid main_color provided.")) + logger.error("Invalid main_color provided.") return int(self.config.remove('main_color').lstrip("#"), base=16) async def on_connect(self): - logger.info(LINE) + logger.line() try: await self.validate_database_connection() except Exception: return await self.logout() - logger.info(LINE) - logger.info(info("Connected to gateway.")) + logger.line() + logger.info("Connected to gateway.") await self.config.refresh() await self.setup_indexes() self._connected.set() @@ -355,12 +350,12 @@ async def setup_indexes(self): # Backwards compatibility old_index = "messages.content_text_messages.author.name_text" if old_index in index_info: - logger.info(info(f"Dropping old index: {old_index}")) + logger.info(f"Dropping old index: {old_index}") await coll.drop_index(old_index) if index_name not in index_info: - logger.info(info('Creating "text" index for logs collection.')) - logger.info(info("Name: " + index_name)) + logger.info('Creating "text" index for logs collection.') + logger.info("Name: " + index_name) await coll.create_index( [ ("messages.content", "text"), @@ -375,23 +370,21 @@ async def on_ready(self): # Wait until config cache is populated with stuff from db and on_connect ran await self.wait_for_connected() - logger.info(LINE) - logger.info(info("Client ready.")) - logger.info(LINE) - logger.info(info(f"Logged in as: {self.user}")) - logger.info(info(f"User ID: {self.user.id}")) - logger.info(info(f"Prefix: {self.prefix}")) - logger.info(info(f"Guild Name: {self.guild.name if self.guild else 'Invalid'}")) - logger.info(info(f"Guild ID: {self.guild.id if self.guild else 'Invalid'}")) - logger.info(LINE) + logger.line() + logger.info("Client ready.") + logger.line() + logger.info("Logged in as: %s", str(self.user)) + logger.info("User ID: %s", str(self.user.id)) + logger.info("Prefix: %s", str(self.prefix)) + logger.info("Guild Name: %s", self.guild.name if self.guild else 'Invalid') + logger.info("Guild ID: %s", self.guild.id if self.guild else 'Invalid') + logger.line() await self.threads.populate_cache() # closures closures = self.config['closures'] - logger.info( - info(f"There are {len(closures)} thread(s) pending to be closed.") - ) + logger.info("There are %d thread(s) pending to be closed.", len(closures)) for recipient_id, items in tuple(closures.items()): after = ( @@ -417,7 +410,7 @@ async def on_ready(self): auto_close=items.get("auto_close", False), ) - logger.info(LINE) + logger.line() async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) @@ -427,7 +420,7 @@ async def convert_emoji(self, name: str) -> str: try: name = await converter.convert(ctx, name.strip(":")) except commands.BadArgument: - logger.warning(info("%s is not a valid emoji."), name) + logger.warning("%s is not a valid emoji.", name) raise return name @@ -440,14 +433,14 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: try: sent_emoji = await self.convert_emoji(sent_emoji) except commands.BadArgument: - logger.warning(error("Removed sent emoji (%s)."), sent_emoji) + logger.warning("Removed sent emoji (%s).", sent_emoji) sent_emoji = self.config.remove("sent_emoji") if blocked_emoji != "disable": try: blocked_emoji = await self.convert_emoji(blocked_emoji) except commands.BadArgument: - logger.warning(error("Removed blocked emoji (%s)."), blocked_emoji) + logger.warning("Removed blocked emoji (%s).", blocked_emoji) blocked_emoji = self.config.remove("blocked_emoji") await self.config.update() @@ -660,7 +653,7 @@ async def update_perms( else: if value in permissions[name]: permissions[name].remove(value) - logger.info(info(f"Updating permissions for {name}, {value} (add={add}).")) + logger.info(f"Updating permissions for {name}, {value} (add={add}).") await self.config.update() async def on_message(self, message): @@ -747,7 +740,11 @@ async def on_raw_reaction_add(self, payload): return channel = await _thread.recipient.create_dm() - message = await channel.fetch_message(payload.message_id) + try: + message = await channel.fetch_message(payload.message_id) + except discord.NotFound: + return + reaction = payload.emoji close_emoji = await self.convert_emoji(self.config["close_emoji"]) @@ -859,8 +856,8 @@ async def on_message_edit(self, before, after): break async def on_error(self, event_method, *args, **kwargs): - logger.error(error("Ignoring exception in {}".format(event_method))) - logger.error(error("Unexpected exception:"), exc_info=sys.exc_info()) + logger.error("Ignoring exception in %s", str(event_method)) + logger.error("Unexpected exception:", exc_info=sys.exc_info()) async def on_command_error(self, context, exception): if isinstance(exception, commands.BadUnionArgument): @@ -880,7 +877,7 @@ async def on_command_error(self, context, exception): ) ) elif isinstance(exception, commands.CommandNotFound): - logger.warning(error("CommandNotFound: " + str(exception))) + logger.warning("CommandNotFound: %s", str(exception)) elif isinstance(exception, commands.MissingRequiredArgument): await context.send_help(context.command) elif isinstance(exception, commands.CheckFailure): @@ -891,9 +888,9 @@ async def on_command_error(self, context, exception): color=discord.Color.red(), description=check.fail_msg ) ) - logger.warning(error("CheckFailure: " + str(exception))) + logger.warning("CheckFailure: %s", str(exception)) else: - logger.error(error("Unexpected exception:"), exc_info=exception) + logger.error("Unexpected exception:", exc_info=exception) @staticmethod def overwrites(ctx: commands.Context) -> dict: @@ -912,37 +909,22 @@ async def validate_database_connection(self): try: await self.db.command("buildinfo") except Exception as exc: - logger.critical( - error("Something went wrong " "while connecting to the database.") - ) + logger.critical("Something went wrong " "while connecting to the database.") message = f"{type(exc).__name__}: {str(exc)}" - logger.critical(error(message)) + logger.critical(message) if "ServerSelectionTimeoutError" in message: - logger.critical( - error( - "This may have been caused by not whitelisting " - "IPs correctly. Make sure to whitelist all " - "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" - ) - ) + logger.critical("This may have been caused by not whitelisting " + "IPs correctly. Make sure to whitelist all " + "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png") if "OperationFailure" in message: - logger.critical( - error( - "This is due to having invalid credentials in your MONGO_URI." - ) - ) - logger.critical( - error( - "Recheck the username/password and make sure to url encode them. " - "https://www.urlencoder.io/" - ) - ) - + logger.critical("This is due to having invalid credentials in your MONGO_URI.") + logger.critical("Recheck the username/password and make sure to url encode them. " + "https://www.urlencoder.io/") raise else: - logger.info(info("Successfully connected to the database.")) + logger.info("Successfully connected to the database.") async def metadata_loop(self): await self.wait_for_connected() @@ -969,7 +951,7 @@ async def metadata_loop(self): } async with self.session.post("https://api.modmail.tk/metadata", json=data): - logger.debug(info("Uploading metadata to Modmail server.")) + logger.debug("Uploading metadata to Modmail server.") await asyncio.sleep(3600) diff --git a/cogs/modmail.py b/cogs/modmail.py index 3d3450d7d0..923927fc0c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,5 +1,4 @@ import asyncio -import os from datetime import datetime from typing import Optional, Union from types import SimpleNamespace as param @@ -966,7 +965,9 @@ async def block( reason = None extend = f" for `{reason}`" if reason is not None else "" - msg = self.bot.blocked_users.get(str(user.id), "") + msg = self.bot.blocked_users.get(str(user.id)) + if msg is None: + msg = '' if ( str(user.id) not in self.bot.blocked_users @@ -1021,10 +1022,7 @@ async def unblock(self, ctx, *, user: User = None): mention = getattr(user, "mention", f"`{user.id}`") if str(user.id) in self.bot.blocked_users: - msg = self.bot.blocked_users.get(str(user.id)) - if msg is None: - msg = "" - self.bot.blocked_users.pop(str(user.id)) + msg = self.bot.blocked_users.pop(str(user.id)) or '' await self.bot.config.update() if msg.startswith("System Message: "): diff --git a/cogs/plugins.py b/cogs/plugins.py index bcfd2a7dd5..c9ecf11d06 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -18,7 +18,6 @@ from core import checks from core.models import PermissionLevel from core.paginator import PaginatorSession -from core.utils import error, info logger = logging.getLogger("Modmail") @@ -84,13 +83,13 @@ async def download_initial_plugins(self): await self.download_plugin_repo(username, repo, branch) except DownloadError as exc: msg = f"{username}/{repo}@{branch} - {exc}" - logger.error(error(msg)) + logger.error(msg) else: try: await self.load_plugin(username, repo, name, branch) except DownloadError as exc: msg = f"{username}/{repo}@{branch}[{name}] - {exc}" - logger.error(error(msg)) + logger.error(msg) async def download_plugin_repo(self, username, repo, branch): try: @@ -140,7 +139,7 @@ async def load_plugin(self, username, repo, plugin_name, branch): if err: msg = f"Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]" - logger.error(error(msg)) + logger.error(msg) raise DownloadError( f"Unable to download requirements: ```\n{err}\n```" ) from exc @@ -155,11 +154,11 @@ async def load_plugin(self, username, repo, plugin_name, branch): self.bot.load_extension(ext) except commands.ExtensionError as exc: msg = f"Plugin Load Failure: {username}/{repo}@{branch}[{plugin_name}]" - logger.error(error(msg)) + logger.error(msg) raise DownloadError("Invalid plugin") from exc else: msg = f"Loaded Plugin: {username}/{repo}@{branch}[{plugin_name}]" - logger.info(info(msg)) + logger.info(msg) @commands.group(aliases=["plugins"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) @@ -291,7 +290,7 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 except Exception as exc: logger.error(str(exc)) self.bot.config['plugins'].append(plugin_name) - logger.error(error(exc)) + logger.error(exc) raise exc await self.bot.config.update() diff --git a/cogs/utility.py b/cogs/utility.py index e271cfc0a3..a5b8a71966 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -25,7 +25,7 @@ from core.decorators import trigger_typing from core.models import InvalidConfigError, PermissionLevel from core.paginator import PaginatorSession, MessagePaginatorSession -from core.utils import cleanup_code, info, error, User, get_perm_level +from core.utils import cleanup_code, User, get_perm_level logger = logging.getLogger("Modmail") @@ -169,7 +169,7 @@ async def send_group_help(self, group): await self.get_destination().send(embed=embed) async def send_error_message(self, msg): # pylint: disable=W0221 - logger.warning(error(f"CommandNotFound: {msg}")) + logger.warning("CommandNotFound: %s", str(msg)) embed = Embed(color=Color.red()) embed.set_footer( @@ -518,7 +518,7 @@ async def set_presence( except (KeyError, ValueError): if status_identifier is not None: msg = f"Invalid status type: {status_identifier}" - logger.warning(error(msg)) + logger.warning(msg) if activity_identifier is None: if activity_message is not None: @@ -536,7 +536,7 @@ async def set_presence( except (KeyError, ValueError): if activity_identifier is not None: msg = f"Invalid activity type: {activity_identifier}" - logger.warning(error(msg)) + logger.warning(msg) else: url = None activity_message = ( @@ -555,7 +555,7 @@ async def set_presence( activity = Activity(type=activity_type, name=activity_message, url=url) else: msg = "You must supply an activity message to use custom activity." - logger.warning(error(msg)) + logger.warning(msg) await self.bot.change_presence(activity=activity, status=status) @@ -577,8 +577,8 @@ async def set_presence( async def on_ready(self): # Wait until config cache is populated with stuff from db await self.bot.wait_for_connected() - logger.info(info(self.presence["activity"][1])) - logger.info(info(self.presence["status"][1])) + logger.info(self.presence["activity"][1]) + logger.info(self.presence["status"][1]) async def loop_presence(self): """Set presence to the configured value every hour.""" @@ -764,12 +764,7 @@ async def config_get(self, ctx, key: str.lower = None): "set configuration variables.", ) embed.set_author(name="Current config", icon_url=self.bot.user.avatar_url) - - config = { - key: val - for key, val in self.bot.config.items() - if val != self.bot.config.defaults['key'] and key in keys - } + config = self.bot.config.filter_defaults(self.bot.config.items()) for name, value in reversed(list(config.items())): embed.add_field(name=name, value=f"`{value}`", inline=False) diff --git a/core/checks.py b/core/checks.py index 72561b4a57..0c495bf747 100644 --- a/core/checks.py +++ b/core/checks.py @@ -3,7 +3,6 @@ from discord.ext import commands from core.models import PermissionLevel -from core.utils import error logger = logging.getLogger("Modmail") @@ -33,12 +32,8 @@ async def predicate(ctx): ) if not has_perm and ctx.command.qualified_name != "help": - logger.error( - error( - f"You does not have permission to use this command: " - f"`{ctx.command.qualified_name}` ({permission_level.name})." - ) - ) + logger.error("You does not have permission to use this command: `%s` (%s).", + str(ctx.command.qualified_name), str(permission_level.name)) return has_perm predicate.permission_level = permission_level diff --git a/core/config.py b/core/config.py index 2a774ae6eb..147ab9f89c 100644 --- a/core/config.py +++ b/core/config.py @@ -107,7 +107,6 @@ def __init__(self, bot): self.bot = bot self._cache = {} self.ready_event = asyncio.Event() - self.populate_cache() def __repr__(self): return repr(self._cache) @@ -194,12 +193,14 @@ async def wait_until_ready(self) -> None: await self.ready_event.wait() def __setitem__(self, key: str, item: typing.Any) -> None: + key = key.lower() logger.info('Setting %s.', key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') self._cache[key] = item def __getitem__(self, key: str) -> typing.Any: + key = key.lower() if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') if key not in self._cache: @@ -207,6 +208,7 @@ def __getitem__(self, key: str) -> typing.Any: return self._cache[key] def get(self, key: str, default: typing.Any = Default) -> typing.Any: + key = key.lower() if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') if key not in self._cache: @@ -219,12 +221,14 @@ def get(self, key: str, default: typing.Any = Default) -> typing.Any: return self._cache[key] def set(self, key: str, item: typing.Any) -> None: + key = key.lower() logger.info('Setting %s.', key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') self._cache[key] = item def remove(self, key: str) -> typing.Any: + key = key.lower() logger.info('Removing %s.', key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') diff --git a/core/models.py b/core/models.py index 36e480f809..1ac2929777 100644 --- a/core/models.py +++ b/core/models.py @@ -1,8 +1,11 @@ +import logging from enum import IntEnum from discord import Color, Embed from discord.ext import commands +from colorama import Fore, Style, init + class PermissionLevel(IntEnum): OWNER = 5 @@ -26,6 +29,45 @@ def embed(self): return Embed(title="Error", description=self.msg, color=Color.red()) +class ModmailLogger(logging.Logger): + def __init__(self, *args, **kwargs): + init() + super().__init__(*args, **kwargs) + + @staticmethod + def _debug_(*msgs): + return f'{Fore.CYAN}{" ".join(msgs)}{Style.RESET_ALL}' + + @staticmethod + def _info_(*msgs): + return f'{Fore.GREEN}{" ".join(msgs)}{Style.RESET_ALL}' + + @staticmethod + def _error_(*msgs): + return f'{Fore.RED}{" ".join(msgs)}{Style.RESET_ALL}' + + def debug(self, msg, *args, **kwargs): + return super().debug(self._debug_(msg), *args, **kwargs) + + def info(self, msg, *args, **kwargs): + return super().info(self._info_(msg), *args, **kwargs) + + def warning(self, msg, *args, **kwargs): + return super().warning(self._error_(msg), *args, **kwargs) + + def error(self, msg, *args, **kwargs): + return super().error(self._error_(msg), *args, **kwargs) + + def critical(self, msg, *args, **kwargs): + return super().critical(self._error_(msg), *args, **kwargs) + + def exception(self, msg, *args, exc_info=True, **kwargs): + return super().exception(self._error_(msg), *args, exc_info, **kwargs) + + def line(self): + super().info(Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL) + + class _Default: pass diff --git a/core/thread.py b/core/thread.py index 843acb9fd7..42f3a8faab 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1,6 +1,5 @@ import asyncio import logging -import os import re import string import typing @@ -12,7 +11,7 @@ from discord.ext.commands import MissingRequiredArgument, CommandError from core.time import human_timedelta -from core.utils import is_image_url, days, match_user_id, truncate, ignore, error, strtobool +from core.utils import is_image_url, days, match_user_id, truncate, ignore, strtobool logger = logging.getLogger("Modmail") @@ -463,7 +462,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None message, destination=self.recipient, from_mod=True, anonymous=anonymous ) except Exception: - logger.info(error("Message delivery failed:"), exc_info=True) + logger.error("Message delivery failed:", exc_info=True) tasks.append( message.channel.send( embed=discord.Embed( diff --git a/core/time.py b/core/time.py index a99efee78b..b4689cc518 100644 --- a/core/time.py +++ b/core/time.py @@ -12,8 +12,6 @@ import parsedatetime as pdt from dateutil.relativedelta import relativedelta -from core.utils import error - logger = logging.getLogger("Modmail") @@ -196,7 +194,7 @@ async def convert(self, ctx, argument): return await self.check_constraints(ctx, self.now, remaining) except Exception: - logger.exception(error("Something went wrong while parsing the time")) + logger.exception("Something went wrong while parsing the time.") raise diff --git a/core/utils.py b/core/utils.py index ca97c658cd..0d999b8f47 100644 --- a/core/utils.py +++ b/core/utils.py @@ -6,7 +6,6 @@ from discord import Object from discord.ext import commands -from colorama import Fore, Style from core.models import PermissionLevel @@ -16,14 +15,6 @@ def strtobool(val): return _stb(str(val)) -def info(*msgs): - return f'{Fore.CYAN}{" ".join(msgs)}{Style.RESET_ALL}' - - -def error(*msgs): - return f'{Fore.RED}{" ".join(msgs)}{Style.RESET_ALL}' - - class User(commands.IDConverter): """ A custom discord.py `Converter` that From ef678c0c59d24280457ee91d1b6d413284affe7c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 5 Jul 2019 19:22:03 -0700 Subject: [PATCH 0304/1221] Fixed some logging problems --- CHANGELOG.md | 1 + bot.py | 2 +- core/models.py | 17 +++++++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b37abfc87..568adf91b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `?notify` no longer carries over to the next thread. - `discord.NotFound` errors for `on_raw_reaction_add`. + ### Added - `?sfw`, mark a thread as "safe for work", undos `?nsfw`. diff --git a/bot.py b/bot.py index 8726a9a7cf..0874bd54a9 100644 --- a/bot.py +++ b/bot.py @@ -35,7 +35,7 @@ ch = logging.StreamHandler(stream=sys.stdout) ch.setLevel(logging.INFO) -formatter = logging.Formatter("%(filename)s - %(levelname)s: %(message)s") +formatter = logging.Formatter("%(filename)s[%(lineno)d] - %(levelname)s: %(message)s") ch.setFormatter(formatter) logger.addHandler(ch) diff --git a/core/models.py b/core/models.py index 1ac2929777..e27904b07c 100644 --- a/core/models.py +++ b/core/models.py @@ -50,22 +50,27 @@ def debug(self, msg, *args, **kwargs): return super().debug(self._debug_(msg), *args, **kwargs) def info(self, msg, *args, **kwargs): - return super().info(self._info_(msg), *args, **kwargs) + if self.isEnabledFor(logging.INFO): + self._log(logging.INFO, self._info_(msg), args, **kwargs) def warning(self, msg, *args, **kwargs): - return super().warning(self._error_(msg), *args, **kwargs) + if self.isEnabledFor(logging.WARNING): + self._log(logging.WARNING, self._error_(msg), args, **kwargs) def error(self, msg, *args, **kwargs): - return super().error(self._error_(msg), *args, **kwargs) + if self.isEnabledFor(logging.ERROR): + self._log(logging.ERROR, self._error_(msg), args, **kwargs) def critical(self, msg, *args, **kwargs): - return super().critical(self._error_(msg), *args, **kwargs) + if self.isEnabledFor(logging.CRITICAL): + self._log(logging.CRITICAL, self._error_(msg), args, **kwargs) def exception(self, msg, *args, exc_info=True, **kwargs): - return super().exception(self._error_(msg), *args, exc_info, **kwargs) + self.error(msg, *args, exc_info=exc_info, **kwargs) def line(self): - super().info(Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL) + if self.isEnabledFor(logging.INFO): + self._log(logging.INFO, Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL, []) class _Default: From f40de028770563b84e65a8e8594036b82270cd1c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 5 Jul 2019 19:48:39 -0700 Subject: [PATCH 0305/1221] Added config check --- bot.py | 2 +- cogs/utility.py | 29 ++++++++++++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/bot.py b/bot.py index 0874bd54a9..d83b34c908 100644 --- a/bot.py +++ b/bot.py @@ -108,7 +108,7 @@ def _configure_logging(self): ch_debug.setLevel(logging.DEBUG) formatter_debug = FileFormatter( - "%(asctime)s %(filename)s - " "%(levelname)s: %(message)s", + "%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) ch_debug.setFormatter(formatter_debug) diff --git a/cogs/utility.py b/cogs/utility.py index a5b8a71966..c72ec2a5a5 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -7,7 +7,7 @@ from contextlib import redirect_stdout from datetime import datetime from difflib import get_close_matches -from io import StringIO +from io import StringIO, BytesIO from typing import Union from types import SimpleNamespace as param from json import JSONDecodeError, loads @@ -359,21 +359,26 @@ async def debug_hastebin(self, ctx): os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log", ), - "r+", + "rb+", ) as f: - logs = f.read().strip() + logs = BytesIO(f.read().strip()) try: async with self.bot.session.post( haste_url + "/documents", data=logs ) as resp: - key = (await resp.json())["key"] + data = await resp.json() + try: + key = data["key"] + except KeyError: + logger.error(data['message']) + raise embed = Embed( title="Debug Logs", color=self.bot.main_color, description=f"{haste_url}/" + key, ) - except (JSONDecodeError, ClientResponseError, IndexError): + except (JSONDecodeError, ClientResponseError, IndexError, KeyError): embed = Embed( title="Debug Logs", color=self.bot.main_color, @@ -607,18 +612,19 @@ async def mention(self, ctx, *, mention: str = None): Type only `{prefix}mention` to retrieve your current "mention" message. """ + # TODO: ability to disable mention. current = self.bot.config["mention"] if mention is None: embed = Embed( - title="Current text", + title="Current mention:", color=self.bot.main_color, description=str(current), ) else: embed = Embed( title="Changed mention!", - description=f"On thread creation the bot now says {mention}.", + description=f'On thread creation the bot now says "{mention}".', color=self.bot.main_color, ) self.bot.config["mention"] = mention @@ -710,7 +716,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): @checks.has_permissions(PermissionLevel.OWNER) async def config_remove(self, ctx, key: str.lower): """Delete a set configuration variable.""" - keys = self.bot.config.allowed_to_change_in_command + keys = self.bot.config.public_keys if key in keys: self.bot.config.remove(key) await self.bot.config.update() @@ -764,10 +770,11 @@ async def config_get(self, ctx, key: str.lower = None): "set configuration variables.", ) embed.set_author(name="Current config", icon_url=self.bot.user.avatar_url) - config = self.bot.config.filter_defaults(self.bot.config.items()) + config = self.bot.config.filter_default(self.bot.config) - for name, value in reversed(list(config.items())): - embed.add_field(name=name, value=f"`{value}`", inline=False) + for name, value in config.items(): + if name in self.bot.config.public_keys: + embed.add_field(name=name, value=f"`{value}`", inline=False) return await ctx.send(embed=embed) From cfd1122db4089cd92d8855ba017e16cc1f9de9d2 Mon Sep 17 00:00:00 2001 From: Kyber Date: Mon, 8 Jul 2019 08:28:26 +1000 Subject: [PATCH 0306/1221] New sponsor (#322) --- README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index effaa06338..389840138d 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Modmail is similar to Reddit's Modmail both in functionality and purpose. It ser ## How does it work? When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. -All threads are logged and you can view previous threads through the corresponding generated log link. Here is an [**example**](https://logs.modmail.tk/example) +All threads are logged and you can view previous threads through the corresponding generated log link. [**Example**](https://logs.modmail.tk/example) ## Features @@ -45,7 +45,7 @@ All threads are logged and you can view previous threads through the correspondi * Interface elements (color, responses, reactions, etc.) * Snippets and *command aliases* * Minimum account/guild age in order to create a thread. -* **Thread logs** +* **Advanced Logging Functionality** * When you close a thread, a [log link](https://logs.modmail.tk/example) is generated and posted to your log channel. * Rendered in styled HTML like Discord. * Login in via Discord to protect your logs ([Patron only feature](https://patreon.com/kyber)). @@ -56,7 +56,7 @@ All threads are logged and you can view previous threads through the correspondi * Editing and deleting messages is synced on both ends. * Support for the full range of message content (mutliple images, files). * Paginated commands interfaces via reactions. - + This list is ever growing thanks to active development and our exceptional contributors. See a full list of documented commands by using the `help` command. ## Installation @@ -90,6 +90,18 @@ This bot can be hosted on heroku. Installation via Heroku is done in your web br You can also set up autoupdates. To do this, [install the Pull app in your fork](https://github.com/apps/pull). Then go to the Deploy tab in your Heroku account, select GitHub and connect your fork. Turn on auto-deploy for the master branch. + +## Sponsors + +Sepcial thanks to our sponsors for supporting the project. + + + + + +Become a [sponsor](https://patreon.com/kyber). + + ## Plugins Modmail supports the use of third party plugins to extend or add functionality to the bot. This allows the introduction of niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third party plugins can be found using the `plugins registry` command. To develop your own, check out the [documentation](https://github.com/kyb3r/modmail/wiki/Plugins) for plugins. From f387f34ae48709ebc59471dc03c6b4abc3d5470c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 8 Jul 2019 20:02:17 -0700 Subject: [PATCH 0307/1221] Some changes see cl --- CHANGELOG.md | 1 + bot.py | 29 +++++++++++++++++++++++++---- cogs/modmail.py | 7 ++++--- cogs/utility.py | 6 +++--- core/config.py | 15 +++++++++++++-- 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568adf91b3..9bdc482732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `?notify` no longer carries over to the next thread. - `discord.NotFound` errors for `on_raw_reaction_add`. +- `mod_typing` and `user_typing` will no longer show when user is blocked. ### Added diff --git a/bot.py b/bot.py index d83b34c908..77f5be4161 100644 --- a/bot.py +++ b/bot.py @@ -210,11 +210,14 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: self.config.remove('log_channel_id') if self.main_category is not None: try: - return self.main_category.channels[0] + channel = self.main_category.channels[0] + self.config['log_channel_id'] = channel.id + logger.debug('No log channel set, however, one was found. Setting...') + return channel except IndexError: pass - logger.info(f'No log channel set, set one with `%ssetup` or ' - f'`%sconfig set log_channel_id `.', self.prefix, self.prefix) + logger.info('No log channel set, set one with `%ssetup` or ' + '`%sconfig set log_channel_id `.', self.prefix, self.prefix) @property def is_connected(self) -> bool: @@ -286,7 +289,11 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]: if cat is not None: return cat self.config.remove("main_category_id") - return discord.utils.get(self.modmail_guild.categories, name="Modmail") + cat = discord.utils.get(self.modmail_guild.categories, name="Modmail") + if cat is not None: + self.config['main_category_id'] = cat.id + logger.debug('No main category set, however, one was found. Setting...') + return cat @property def blocked_users(self) -> typing.Dict[str, str]: @@ -705,7 +712,16 @@ async def on_typing(self, channel, user, _): if user.bot: return + + async def _(*args, **kwargs): + pass + if isinstance(channel, discord.DMChannel): + if await self._process_blocked(SimpleNamespace(author=user, + channel=SimpleNamespace(send=_), + add_reaction=_)): + return + try: user_typing = strtobool(self.config["user_typing"]) except ValueError: @@ -714,6 +730,7 @@ async def on_typing(self, channel, user, _): return thread = await self.threads.find(recipient=user) + if thread: await thread.channel.trigger_typing() else: @@ -726,6 +743,10 @@ async def on_typing(self, channel, user, _): 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=_), + add_reaction=_)): + return await thread.recipient.trigger_typing() async def on_raw_reaction_add(self, payload): diff --git a/cogs/modmail.py b/cogs/modmail.py index 923927fc0c..88dd7b2a55 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -85,7 +85,7 @@ async def setup(self, ctx): f"Type `{self.bot.prefix}permissions` for more info." ) - if not self.bot.config["permissions"]: + if not self.bot.config["command_permissions"] and not self.bot.config["level_permissions"]: await self.bot.update_perms(PermissionLevel.REGULAR, -1) await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id) @@ -1020,6 +1020,7 @@ async def unblock(self, ctx, *, user: User = None): raise commands.MissingRequiredArgument(param(name="user")) mention = getattr(user, "mention", f"`{user.id}`") + name = getattr(user, "name", f"`{user.id}`") if str(user.id) in self.bot.blocked_users: msg = self.bot.blocked_users.pop(str(user.id)) or '' @@ -1037,8 +1038,8 @@ async def unblock(self, ctx, *, user: User = None): ) embed.set_footer( text="However, if the original system block reason still apply, " - f"{mention} will be automatically blocked again. Use " - f'"{self.bot.prefix}blocked whitelist {mention}" to whitelist the user.' + f"{name} will be automatically blocked again. Use " + f'"{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' ) else: embed = discord.Embed( diff --git a/cogs/utility.py b/cogs/utility.py index c72ec2a5a5..49d5362cc3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -767,9 +767,9 @@ async def config_get(self, ctx, key: str.lower = None): embed = Embed( color=self.bot.main_color, description="Here is a list of currently " - "set configuration variables.", + "set configuration variable(s).", ) - embed.set_author(name="Current config", icon_url=self.bot.user.avatar_url) + 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(): @@ -809,7 +809,7 @@ async def alias(self, ctx): color=self.bot.main_color, description="You dont have any aliases at the moment.", ) - embed.set_author(name="Command aliases", icon_url=ctx.guild.icon_url) + embed.set_author(name="Command aliases:", icon_url=ctx.guild.icon_url) embed.set_footer(text=f"Do {self.bot.prefix}help alias for more commands.") embeds.append(embed) diff --git a/core/config.py b/core/config.py index 147ab9f89c..826dae8371 100644 --- a/core/config.py +++ b/core/config.py @@ -14,6 +14,7 @@ from core._color_data import ALL_COLORS from core.models import InvalidConfigError, Default from core.time import UserFriendlyTime +from core.utils import strtobool logger = logging.getLogger("Modmail") load_dotenv() @@ -100,6 +101,8 @@ class ConfigManager: time_deltas = {"account_age", "guild_age", "thread_auto_close"} + booleans = {"user_typing", "mod_typing", "reply_without_command", "recipient_thread_close"} + defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) @@ -174,6 +177,12 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: clean_value = isodate.duration_isoformat(time.dt - converter.now) value_text = f"{val} ({clean_value})" + elif key in self.booleans: + try: + clean_value = value_text = strtobool(val) + except ValueError: + raise InvalidConfigError("Must be a yes/no value.") + return clean_value, value_text async def update(self): @@ -182,8 +191,10 @@ async def update(self): async def refresh(self) -> dict: """Refreshes internal cache with data from database""" - data = {k.lower(): v for k, v in (await self.api.get_config()).items() if k.lower() in self.all_keys} - self._cache.update(data) + for k, v in (await self.api.get_config()).items(): + k = k.lower() + if k in self.all_keys: + self._cache[k] = v if not self.ready_event.is_set(): self.ready_event.set() logger.info('Config ready.') From 41a0d8de4eab8a6b02885adb3eb6a0529fa61be6 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 10 Jul 2019 20:17:57 +1000 Subject: [PATCH 0308/1221] Add version argument to jump straight to specified version. --- CHANGELOG.md | 5 +++++ cogs/utility.py | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c764130b53..8f02bd6516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [UNRELEASED] + +### Changed + +- Changelog command can now take a version argument to jump straight to specified version. # v3.0.3 diff --git a/cogs/utility.py b/cogs/utility.py index b1743f5eb3..460f0afec7 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -224,11 +224,24 @@ def cog_unload(self): @commands.command() @checks.has_permissions(PermissionLevel.REGULAR) @trigger_typing - async def changelog(self, ctx): + async def changelog(self, ctx, version=None): """Shows the changelog of the Modmail.""" changelog = await Changelog.from_url(self.bot) + version = version.lstrip("vV") if version else changelog.latest_version.version + + try: + index = [v.version for v in changelog.versions].index(version) + except ValueError: + return await ctx.send( + embed=Embed( + color=Color.red(), + description=f"The specified version `{version}` could not be found.", + ) + ) + try: paginator = PaginatorSession(ctx, *changelog.embeds) + paginator.current = index await paginator.run() except: await ctx.send(changelog.CHANGELOG_URL) From 043fd5a0778cead7a16830cb20836856cfa66756 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 10 Jul 2019 20:27:46 +1000 Subject: [PATCH 0309/1221] Fix bug when logkey cant be resolved --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index ad37f4d05d..7c4ff86621 100644 --- a/core/thread.py +++ b/core/thread.py @@ -317,7 +317,7 @@ async def _close( "{closer.mention} has closed this Modmail thread.", ) - message = message.format(closer=closer, loglink=log_url, logkey=log_data["key"]) + message = message.format(closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None) embed.description = message footer = self.bot.config.get( From 986b744b67e22fe1a9b722ac2ac401953a9e3e9d Mon Sep 17 00:00:00 2001 From: kyb3r Date: Wed, 10 Jul 2019 23:10:13 +1000 Subject: [PATCH 0310/1221] Remove searching through message history to find threads --- core/thread.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/core/thread.py b/core/thread.py index 7c4ff86621..87627dbaf7 100644 --- a/core/thread.py +++ b/core/thread.py @@ -735,7 +735,7 @@ async def find( ) -> Thread: """Finds a thread from cache or from discord channel topics.""" if recipient is None and channel is not None: - return await self._find_from_channel(channel) + return self._find_from_channel(channel) thread = None @@ -761,7 +761,7 @@ async def find( thread.ready = True return thread - async def _find_from_channel(self, channel): + def _find_from_channel(self, channel): """ Tries to find a thread from a channel channel topic, if channel topic doesnt exist for some reason, falls back to @@ -773,23 +773,6 @@ async def _find_from_channel(self, channel): if channel.topic: user_id = match_user_id(channel.topic) - # BUG: When discord fails to create channel topic. - # search through message history - elif channel.topic is None: - try: - async for message in channel.history(limit=100): - if message.author != self.bot.user: - continue - if message.embeds: - embed = message.embeds[0] - if embed.footer.text: - user_id = match_user_id(embed.footer.text) - if user_id != -1: - break - except discord.NotFound: - # When the channel's deleted. - pass - if user_id != -1: if user_id in self.cache: return self.cache[user_id] From 12c414745ce17e1bf6d4c19789c8661ba61f8dff Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 10 Jul 2019 20:06:46 -0700 Subject: [PATCH 0311/1221] Use black format --- bot.py | 131 ++++++++++++++++++++++++++++-------------------- cogs/modmail.py | 21 +++++--- cogs/plugins.py | 23 +++++---- cogs/utility.py | 14 +++--- core/checks.py | 11 ++-- core/clients.py | 4 +- core/config.py | 76 +++++++++++++++++----------- core/models.py | 9 +++- core/thread.py | 39 ++++++++------ core/time.py | 2 +- 10 files changed, 202 insertions(+), 128 deletions(-) diff --git a/bot.py b/bot.py index 77f5be4161..bbe810cd44 100644 --- a/bot.py +++ b/bot.py @@ -67,9 +67,9 @@ def __init__(self): self._configure_logging() - mongo_uri = self.config['mongo_uri'] + mongo_uri = self.config["mongo_uri"] if mongo_uri is None: - raise ValueError('A Mongo URI is necessary for the bot to function.') + raise ValueError("A Mongo URI is necessary for the bot to function.") self.db = AsyncIOMotorClient(mongo_uri).modmail_bot self.plugin_db = PluginDatabaseClient(self) @@ -92,7 +92,7 @@ def uptime(self) -> str: return fmt.format(d=days, h=hours, m=minutes, s=seconds) def _configure_logging(self): - level_text = self.config['log_level'].upper() + level_text = self.config["log_level"].upper() logging_levels = { "CRITICAL": logging.CRITICAL, "ERROR": logging.ERROR, @@ -116,7 +116,7 @@ def _configure_logging(self): log_level = logging_levels.get(level_text) if log_level is None: - log_level = self.config.remove('log_level') + log_level = self.config.remove("log_level") logger.line() if log_level is not None: @@ -186,7 +186,9 @@ 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: @@ -194,9 +196,9 @@ def run(self, *args, **kwargs): logger.error(" - Shutting down bot - ") async def is_owner(self, user: discord.User) -> bool: - owners = self.config['owners'] + owners = self.config["owners"] if owners is not None: - if user.id in set(map(int, str(owners).split(','))): + if user.id in set(map(int, str(owners).split(","))): return True return await super().is_owner(user) @@ -207,17 +209,21 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: channel = self.get_channel(int(channel_id)) if channel is not None: return channel - self.config.remove('log_channel_id') + self.config.remove("log_channel_id") if self.main_category is not None: try: channel = self.main_category.channels[0] - self.config['log_channel_id'] = channel.id - logger.debug('No log channel set, however, one was found. Setting...') + self.config["log_channel_id"] = channel.id + logger.debug("No log channel set, however, one was found. Setting...") return channel except IndexError: pass - logger.info('No log channel set, set one with `%ssetup` or ' - '`%sconfig set log_channel_id `.', self.prefix, self.prefix) + logger.info( + "No log channel set, set one with `%ssetup` or " + "`%sconfig set log_channel_id `.", + self.prefix, + self.prefix, + ) @property def is_connected(self) -> bool: @@ -238,19 +244,19 @@ def aliases(self) -> typing.Dict[str, str]: @property def token(self) -> str: - token = self.config['token'] + token = self.config["token"] if token is None: - raise ValueError('TOKEN must be set, this is your bot token.') + raise ValueError("TOKEN must be set, this is your bot token.") return token @property def guild_id(self) -> typing.Optional[int]: - guild_id = self.config['guild_id'] + guild_id = self.config["guild_id"] if guild_id is not None: try: return int(str(guild_id)) except ValueError: - raise ValueError('Invalid guild_id set.') + raise ValueError("Invalid guild_id set.") @property def guild(self) -> typing.Optional[discord.Guild]: @@ -272,8 +278,8 @@ def modmail_guild(self) -> typing.Optional[discord.Guild]: guild = discord.utils.get(self.guilds, id=int(modmail_guild_id)) if guild is not None: return guild - self.config.remove('modmail_guild_id') - logger.error('Invalid modmail_guild_id set.') + self.config.remove("modmail_guild_id") + logger.error("Invalid modmail_guild_id set.") return self.guild @property @@ -285,14 +291,16 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]: if self.modmail_guild is not None: category_id = self.config["main_category_id"] if category_id is not None: - 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 self.config.remove("main_category_id") cat = discord.utils.get(self.modmail_guild.categories, name="Modmail") if cat is not None: - self.config['main_category_id'] = cat.id - logger.debug('No main category set, however, one was found. Setting...') + self.config["main_category_id"] = cat.id + logger.debug("No main category set, however, one was found. Setting...") return cat @property @@ -314,7 +322,7 @@ def mod_color(self) -> int: return int(color.lstrip("#"), base=16) except ValueError: logger.error("Invalid mod_color provided.") - return int(self.config.remove('mod_color').lstrip("#"), base=16) + return int(self.config.remove("mod_color").lstrip("#"), base=16) @property def recipient_color(self) -> int: @@ -323,7 +331,7 @@ def recipient_color(self) -> int: return int(color.lstrip("#"), base=16) except ValueError: logger.error("Invalid recipient_color provided.") - return int(self.config.remove('recipient_color').lstrip("#"), base=16) + return int(self.config.remove("recipient_color").lstrip("#"), base=16) @property def main_color(self) -> int: @@ -332,7 +340,7 @@ def main_color(self) -> int: return int(color.lstrip("#"), base=16) except ValueError: logger.error("Invalid main_color provided.") - return int(self.config.remove('main_color').lstrip("#"), base=16) + return int(self.config.remove("main_color").lstrip("#"), base=16) async def on_connect(self): logger.line() @@ -383,14 +391,14 @@ async def on_ready(self): logger.info("Logged in as: %s", str(self.user)) logger.info("User ID: %s", str(self.user.id)) logger.info("Prefix: %s", str(self.prefix)) - logger.info("Guild Name: %s", self.guild.name if self.guild else 'Invalid') - logger.info("Guild ID: %s", self.guild.id if self.guild else 'Invalid') + logger.info("Guild Name: %s", self.guild.name if self.guild else "Invalid") + logger.info("Guild ID: %s", self.guild.id if self.guild else "Invalid") logger.line() await self.threads.populate_cache() # closures - closures = self.config['closures'] + closures = self.config["closures"] logger.info("There are %d thread(s) pending to be closed.", len(closures)) for recipient_id, items in tuple(closures.items()): @@ -404,7 +412,7 @@ async def on_ready(self): if not thread: # If the channel is deleted - self.config['closures'].pop(recipient_id) + self.config["closures"].pop(recipient_id) await self.config.update() continue @@ -503,7 +511,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: ) guild_age = self.config.remove("guild_age") - reason = self.blocked_users.get(str(message.author.id)) or '' + reason = self.blocked_users.get(str(message.author.id)) or "" min_guild_age = min_account_age = now try: @@ -513,7 +521,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: self.config.remove("account_age") try: - joined_at = getattr(message.author, 'joined_at', None) + joined_at = getattr(message.author, "joined_at", None) if joined_at is not None: min_guild_age = joined_at + guild_age except ValueError: @@ -646,10 +654,10 @@ async def update_perms( self, name: typing.Union[PermissionLevel, str], value: int, add: bool = True ) -> None: if isinstance(name, PermissionLevel): - permissions = self.config['level_permissions'] + permissions = self.config["level_permissions"] name = name.name else: - permissions = self.config['command_permissions'] + permissions = self.config["command_permissions"] if name not in permissions: if add: permissions[name] = [value] @@ -678,7 +686,7 @@ async def on_message(self, message): prefix = self.prefix if message.content.startswith(prefix): - cmd = message.content[len(prefix):].strip() + cmd = message.content[len(prefix) :].strip() if cmd in self.snippets: thread = await self.threads.find(channel=message.channel) snippet = self.snippets[cmd] @@ -695,7 +703,7 @@ async def on_message(self, message): try: reply_without_command = strtobool(self.config["reply_without_command"]) except ValueError: - reply_without_command = self.config.remove('reply_without_command') + reply_without_command = self.config.remove("reply_without_command") if reply_without_command: await thread.reply(message) @@ -713,19 +721,21 @@ async def on_typing(self, channel, user, _): if user.bot: return - async def _(*args, **kwargs): + async def _void(*_args, **_kwargs): pass if isinstance(channel, discord.DMChannel): - if await self._process_blocked(SimpleNamespace(author=user, - channel=SimpleNamespace(send=_), - add_reaction=_)): + if await self._process_blocked( + SimpleNamespace( + author=user, channel=SimpleNamespace(send=_void), add_reaction=_void + ) + ): return try: user_typing = strtobool(self.config["user_typing"]) except ValueError: - user_typing = self.config.remove('user_typing') + user_typing = self.config.remove("user_typing") if not user_typing: return @@ -737,15 +747,19 @@ async def _(*args, **kwargs): try: mod_typing = strtobool(self.config["mod_typing"]) except ValueError: - mod_typing = self.config.remove('mod_typing') + mod_typing = self.config.remove("mod_typing") if not mod_typing: return 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=_), - add_reaction=_)): + if await self._process_blocked( + SimpleNamespace( + author=thread.recipient, + channel=SimpleNamespace(send=_void), + add_reaction=_void, + ) + ): return await thread.recipient.trigger_typing() @@ -777,9 +791,13 @@ async def on_raw_reaction_add(self, payload): if thread and ts == thread.channel.created_at: # the reacted message is the corresponding thread creation embed try: - recipient_thread_close = strtobool(self.config["recipient_thread_close"]) + recipient_thread_close = strtobool( + self.config["recipient_thread_close"] + ) except ValueError: - recipient_thread_close = self.config.remove('recipient_thread_close') + recipient_thread_close = self.config.remove( + "recipient_thread_close" + ) if recipient_thread_close: await thread.close(closer=user) else: @@ -808,7 +826,7 @@ async def on_guild_channel_delete(self, channel): if isinstance(channel, discord.CategoryChannel): if self.main_category.id == channel.id: - self.config.remove('main_category_id') + self.config.remove("main_category_id") await self.config.update() return @@ -816,7 +834,7 @@ async def on_guild_channel_delete(self, channel): return if self.log_channel is None or self.log_channel.id == channel.id: - self.config.remove('log_channel_id') + self.config.remove("log_channel_id") await self.config.update() return @@ -935,14 +953,20 @@ async def validate_database_connection(self): logger.critical(message) if "ServerSelectionTimeoutError" in message: - logger.critical("This may have been caused by not whitelisting " - "IPs correctly. Make sure to whitelist all " - "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png") + logger.critical( + "This may have been caused by not whitelisting " + "IPs correctly. Make sure to whitelist all " + "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" + ) if "OperationFailure" in message: - logger.critical("This is due to having invalid credentials in your MONGO_URI.") - logger.critical("Recheck the username/password and make sure to url encode them. " - "https://www.urlencoder.io/") + logger.critical( + "This is due to having invalid credentials in your MONGO_URI." + ) + logger.critical( + "Recheck the username/password and make sure to url encode them. " + "https://www.urlencoder.io/" + ) raise else: logger.info("Successfully connected to the database.") @@ -980,6 +1004,7 @@ async def metadata_loop(self): if __name__ == "__main__": if os.name != "nt": import uvloop + uvloop.install() bot = ModmailBot() bot.run() diff --git a/cogs/modmail.py b/cogs/modmail.py index 88dd7b2a55..9e4228d4c5 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -38,9 +38,11 @@ async def setup(self, ctx): return await ctx.send(f"{self.bot.modmail_guild} is already set up.") if self.bot.modmail_guild is None: - embed = discord.Embed(title='Error', - description='Modmail functioning guild not found.', - color=discord.Color.red()) + embed = discord.Embed( + title="Error", + description="Modmail functioning guild not found.", + color=discord.Color.red(), + ) return await ctx.send(embed=embed) category = await self.bot.modmail_guild.create_category( @@ -85,11 +87,14 @@ async def setup(self, ctx): f"Type `{self.bot.prefix}permissions` for more info." ) - 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) await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id) - @commands.group(aliases=['snippets'], invoke_without_command=True) + @commands.group(aliases=["snippets"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippet(self, ctx): """ @@ -534,7 +539,7 @@ def format_log_embeds(self, logs, avatar_url): if prefix == "NONE": prefix = "" - log_url = self.bot.config['log_url'].strip("/") + f"{prefix}/{key}" + log_url = self.bot.config["log_url"].strip("/") + f"{prefix}/{key}" username = entry["recipient"]["name"] + "#" username += entry["recipient"]["discriminator"] @@ -967,7 +972,7 @@ async def block( extend = f" for `{reason}`" if reason is not None else "" msg = self.bot.blocked_users.get(str(user.id)) if msg is None: - msg = '' + msg = "" if ( str(user.id) not in self.bot.blocked_users @@ -1023,7 +1028,7 @@ async def unblock(self, ctx, *, user: User = None): name = getattr(user, "name", f"`{user.id}`") if str(user.id) in self.bot.blocked_users: - msg = self.bot.blocked_users.pop(str(user.id)) or '' + msg = self.bot.blocked_users.pop(str(user.id)) or "" await self.bot.config.update() if msg.startswith("System Message: "): diff --git a/cogs/plugins.py b/cogs/plugins.py index c9ecf11d06..ddef4c9e77 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -57,7 +57,7 @@ def parse_plugin(name): # default branch = master try: # when names are formatted with inline code - result = name.strip('`').split("/") + result = name.strip("`").split("/") result[2] = "/".join(result[2:]) if "@" in result[2]: # branch is specified @@ -76,7 +76,7 @@ def parse_plugin(name): async def download_initial_plugins(self): await self.bot.wait_for_connected() - for i in self.bot.config['plugins']: + for i in self.bot.config["plugins"]: username, repo, name, branch = self.parse_plugin(i) try: @@ -186,7 +186,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): ) return await ctx.send(embed=embed) - if plugin_name in self.bot.config['plugins']: + if plugin_name in self.bot.config["plugins"]: embed = discord.Embed( description="This plugin is already installed.", color=self.bot.main_color, @@ -233,7 +233,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): # if it makes it here, it has passed all checks and should # be entered into the config - self.bot.config['plugins'].append(plugin_name) + self.bot.config["plugins"].append(plugin_name) await self.bot.config.update() embed = discord.Embed( @@ -260,7 +260,7 @@ async def plugin_remove(self, ctx, *, plugin_name: str): details["repository"] + "/" + plugin_name + "@" + details["branch"] ) - if plugin_name in self.bot.config['plugins']: + if plugin_name in self.bot.config["plugins"]: try: username, repo, name, branch = self.parse_plugin(plugin_name) @@ -270,12 +270,13 @@ async def plugin_remove(self, ctx, *, plugin_name: str): except Exception: pass - self.bot.config['plugins'].remove(plugin_name) + self.bot.config["plugins"].remove(plugin_name) try: # BUG: Local variables 'username', 'branch' and 'repo' might be referenced before assignment if not any( - i.startswith(f"{username}/{repo}") for i in self.bot.config['plugins'] + i.startswith(f"{username}/{repo}") + for i in self.bot.config["plugins"] ): # if there are no more of such repos, delete the folder def onerror(func, path, exc_info): # pylint: disable=W0613 @@ -289,7 +290,7 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 ) except Exception as exc: logger.error(str(exc)) - self.bot.config['plugins'].append(plugin_name) + self.bot.config["plugins"].append(plugin_name) logger.error(exc) raise exc @@ -317,7 +318,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): details["repository"] + "/" + plugin_name + "@" + details["branch"] ) - if plugin_name not in self.bot.config['plugins']: + if plugin_name not in self.bot.config["plugins"]: embed = discord.Embed( description="That plugin is not installed.", color=self.bot.main_color ) @@ -367,8 +368,8 @@ async def plugin_update(self, ctx, *, plugin_name: str): async def plugin_enabled(self, ctx): """Shows a list of currently enabled plugins.""" - if self.bot.config['plugins']: - msg = "```\n" + "\n".join(self.bot.config['plugins']) + "\n```" + if self.bot.config["plugins"]: + msg = "```\n" + "\n".join(self.bot.config["plugins"]) + "\n```" embed = discord.Embed(description=msg, color=self.bot.main_color) await ctx.send(embed=embed) else: diff --git a/cogs/utility.py b/cogs/utility.py index 49d5362cc3..79fc0c213b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -371,7 +371,7 @@ async def debug_hastebin(self, ctx): try: key = data["key"] except KeyError: - logger.error(data['message']) + logger.error(data["message"]) raise embed = Embed( title="Debug Logs", @@ -769,7 +769,9 @@ async def config_get(self, ctx, key: str.lower = None): 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) + 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(): @@ -1132,11 +1134,11 @@ async def permissions_get(self, ctx, *, user_or_role: Union[User, Role, str]): cmds = [] levels = [] for cmd in self.bot.commands: - permissions = self.bot.config['command_permissions'].get(cmd.name, []) + permissions = self.bot.config["command_permissions"].get(cmd.name, []) if value in permissions: cmds.append(cmd.name) for level in PermissionLevel: - permissions = self.bot.config['level_permissions'].get(level.name, []) + permissions = self.bot.config["level_permissions"].get(level.name, []) if value in permissions: levels.append(level.name) mention = user_or_role.name if hasattr(user_or_role, "name") else user_or_role @@ -1172,7 +1174,7 @@ async def permissions_get_command(self, ctx, *, command: str = None): """View currently-set permissions for a command.""" def get_command(cmd): - permissions = self.bot.config['command_permissions'].get(cmd.name, []) + permissions = self.bot.config["command_permissions"].get(cmd.name, []) if not permissions: embed = Embed( title=f"Permission entries for command `{cmd.name}`:", @@ -1230,7 +1232,7 @@ async def permissions_get_level(self, ctx, *, level: str = None): """View currently-set permissions for commands of a permission level.""" def get_level(perm_level): - permissions = self.bot.config['level_permissions'].get(perm_level.name, []) + permissions = self.bot.config["level_permissions"].get(perm_level.name, []) if not permissions: embed = Embed( title="Permission entries for permission " diff --git a/core/checks.py b/core/checks.py index 0c495bf747..b706d2bea3 100644 --- a/core/checks.py +++ b/core/checks.py @@ -32,8 +32,11 @@ async def predicate(ctx): ) if not has_perm and ctx.command.qualified_name != "help": - logger.error("You does not have permission to use this command: `%s` (%s).", - str(ctx.command.qualified_name), str(permission_level.name)) + logger.error( + "You does not have permission to use this command: `%s` (%s).", + str(ctx.command.qualified_name), + str(permission_level.name), + ) return has_perm predicate.permission_level = permission_level @@ -53,7 +56,7 @@ async def check_permissions(ctx, command_name, permission_level) -> bool: # Administrators have permission to all non-owner commands return True - command_permissions = ctx.bot.config['command_permissions'] + command_permissions = ctx.bot.config["command_permissions"] author_roles = ctx.author.roles if command_name in command_permissions: @@ -66,7 +69,7 @@ async def check_permissions(ctx, command_name, permission_level) -> bool: has_perm_id = ctx.author.id in command_permissions[command_name] return has_perm_role or has_perm_id - level_permissions = ctx.bot.config['level_permissions'] + level_permissions = ctx.bot.config["level_permissions"] for level in PermissionLevel: if level >= permission_level and level.name in level_permissions: diff --git a/core/clients.py b/core/clients.py index 2924c12488..88ffbd8743 100644 --- a/core/clients.py +++ b/core/clients.py @@ -143,7 +143,9 @@ async def get_config(self) -> dict: async def update_config(self, data: dict): toset = self.bot.config.filter_valid(data) - unset = self.bot.config.filter_valid({k: 1 for k in self.bot.config.all_keys if k not in data}) + unset = self.bot.config.filter_valid( + {k: 1 for k in self.bot.config.all_keys if k not in data} + ) if toset and unset: return await self.db.config.update_one( diff --git a/core/config.py b/core/config.py index 826dae8371..8d29a3c2b9 100644 --- a/core/config.py +++ b/core/config.py @@ -24,11 +24,11 @@ class ConfigManager: public_keys = { # activity - "twitch_url": 'https://www.twitch.tv/discord-modmail/', + "twitch_url": "https://www.twitch.tv/discord-modmail/", # bot settings "main_category_id": None, - "prefix": '?', - "mention": '@here', + "prefix": "?", + "mention": "@here", "main_color": str(discord.Color.blurple()), "user_typing": False, "mod_typing": False, @@ -38,20 +38,20 @@ class ConfigManager: # logging "log_channel_id": None, # threads - "sent_emoji": '✅', - "blocked_emoji": '🚫', - "close_emoji": '🔒', + "sent_emoji": "✅", + "blocked_emoji": "🚫", + "close_emoji": "🔒", "recipient_thread_close": False, "thread_auto_close": 0, "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', + "thread_creation_footer": "Your message has been sent", "thread_self_closable_creation_footer": "Click the lock to close the thread", - "thread_creation_title": 'Thread Created', - "thread_close_footer": 'Replying will create a new thread', - "thread_close_title": 'Thread Closed', - "thread_close_response": '{closer.mention} has closed this Modmail thread.', - "thread_self_close_response": 'You have closed this Modmail thread.', + "thread_creation_title": "Thread Created", + "thread_close_footer": "Replying will create a new thread", + "thread_close_title": "Thread Closed", + "thread_close_response": "{closer.mention} has closed this Modmail thread.", + "thread_self_close_response": "You have closed this Modmail thread.", # moderation "recipient_color": str(discord.Color.gold()), "mod_tag": None, @@ -59,12 +59,12 @@ class ConfigManager: # anonymous message "anon_username": None, "anon_avatar_url": None, - "anon_tag": 'Response', + "anon_tag": "Response", } private_keys = { # bot presence - "activity_message": '', + "activity_message": "", "activity_type": None, "status": None, "oauth_whitelist": [], @@ -87,8 +87,8 @@ class ConfigManager: # Modmail "modmail_guild_id": None, "guild_id": None, - "log_url": 'https://example.com/', - "log_url_prefix": '/logs', + "log_url": "https://example.com/", + "log_url_prefix": "/logs", "mongo_uri": None, "owners": None, # bot @@ -101,7 +101,12 @@ class ConfigManager: time_deltas = {"account_age", "guild_age", "thread_auto_close"} - booleans = {"user_typing", "mod_typing", "reply_without_command", "recipient_thread_close"} + booleans = { + "user_typing", + "mod_typing", + "reply_without_command", + "recipient_thread_close", + } defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) @@ -122,12 +127,20 @@ 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} + ) if os.path.exists("config.json"): with open("config.json") as f: # Config json should override env vars - data.update({k.lower(): v for k, v in json.load(f).items() if k.lower() in self.all_keys}) + data.update( + { + k.lower(): v + for k, v in json.load(f).items() + if k.lower() in self.all_keys + } + ) self._cache = data return self._cache @@ -145,7 +158,7 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: if hex_.startswith("#"): hex_ = hex_[1:] if len(hex_) == 3: - hex_ = ''.join(s for s in hex_ for _ in range(2)) + hex_ = "".join(s for s in hex_ for _ in range(2)) if len(hex_) != 6: raise InvalidConfigError("Invalid color name or hex.") try: @@ -197,7 +210,7 @@ async def refresh(self) -> dict: self._cache[k] = v if not self.ready_event.is_set(): self.ready_event.set() - logger.info('Config ready.') + logger.info("Config ready.") return self._cache async def wait_until_ready(self) -> None: @@ -205,7 +218,7 @@ async def wait_until_ready(self) -> None: def __setitem__(self, key: str, item: typing.Any) -> None: key = key.lower() - logger.info('Setting %s.', key) + logger.info("Setting %s.", key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') self._cache[key] = item @@ -233,14 +246,14 @@ def get(self, key: str, default: typing.Any = Default) -> typing.Any: def set(self, key: str, item: typing.Any) -> None: key = key.lower() - logger.info('Setting %s.', key) + logger.info("Setting %s.", key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') self._cache[key] = item def remove(self, key: str) -> typing.Any: key = key.lower() - logger.info('Removing %s.', key) + logger.info("Removing %s.", key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') self._cache[key] = deepcopy(self.defaults[key]) @@ -250,11 +263,18 @@ 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]: - return {k.lower(): v for k, v in data.items() - if k.lower() in cls.public_keys or k.lower() in cls.private_keys} + 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() + if k.lower() in cls.public_keys or k.lower() in cls.private_keys + } @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 return {k.lower(): v for k, v in data.items() if v != cls.defaults[k.lower()]} diff --git a/core/models.py b/core/models.py index e27904b07c..4ba3964231 100644 --- a/core/models.py +++ b/core/models.py @@ -70,7 +70,14 @@ def exception(self, msg, *args, exc_info=True, **kwargs): def line(self): if self.isEnabledFor(logging.INFO): - self._log(logging.INFO, Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL, []) + self._log( + logging.INFO, + Fore.BLACK + + Style.BRIGHT + + "-------------------------" + + Style.RESET_ALL, + [], + ) class _Default: diff --git a/core/thread.py b/core/thread.py index 42f3a8faab..03aa06302a 100644 --- a/core/thread.py +++ b/core/thread.py @@ -158,9 +158,11 @@ async def send_genesis_message(): ) try: - recipient_thread_close = strtobool(self.bot.config["recipient_thread_close"]) + recipient_thread_close = strtobool( + self.bot.config["recipient_thread_close"] + ) except ValueError: - recipient_thread_close = self.bot.config.remove('recipient_thread_close') + recipient_thread_close = self.bot.config.remove("recipient_thread_close") if recipient_thread_close: footer = self.bot.config["thread_self_closable_creation_footer"] @@ -212,7 +214,7 @@ async def close( "message": message, "auto_close": auto_close, } - self.bot.config['closures'][str(self.id)] = items + self.bot.config["closures"][str(self.id)] = items await self.bot.config.update() task = self.bot.loop.call_later( @@ -235,8 +237,8 @@ async def _close( # Cancel auto closing the thread if closed by any means. - self.bot.config['subscriptions'].pop(str(self.id), None) - self.bot.config['notification_squad'].pop(str(self.id), None) + self.bot.config["subscriptions"].pop(str(self.id), None) + self.bot.config["notification_squad"].pop(str(self.id), None) # Logging log_data = await self.bot.api.post_log( @@ -256,10 +258,12 @@ async def _close( ) if log_data is not None and isinstance(log_data, dict): - prefix = self.bot.config['log_url_prefix'] + prefix = self.bot.config["log_url_prefix"] if prefix == "NONE": prefix = "" - log_url = f"{self.bot.config['log_url'].strip('/')}{prefix}/{log_data['key']}" + log_url = ( + f"{self.bot.config['log_url'].strip('/')}{prefix}/{log_data['key']}" + ) if log_data["messages"]: content = str(log_data["messages"][0]["content"]) @@ -333,7 +337,7 @@ async def cancel_closure(self, auto_close: bool = False, all: bool = False) -> N self.auto_close_task.cancel() self.auto_close_task = None - to_update = self.bot.config['closures'].pop(str(self.id), None) + to_update = self.bot.config["closures"].pop(str(self.id), None) if to_update is not None: await self.bot.config.update() @@ -392,7 +396,9 @@ async def _restart_close_timer(self): human_time = human_timedelta(dt=reset_time) # Grab message - close_message = self.bot.config["thread_auto_close_response"].format(timeout=human_time) + close_message = self.bot.config["thread_auto_close_response"].format( + timeout=human_time + ) time_marker_regex = "%t" if len(re.findall(time_marker_regex, close_message)) == 1: @@ -400,11 +406,14 @@ async def _restart_close_timer(self): elif len(re.findall(time_marker_regex, close_message)) > 1: logger.warning( "The thread_auto_close_response should only contain one '%s' to specify time.", - time_marker_regex + time_marker_regex, ) 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,13 +566,13 @@ async def send( and not isinstance(destination, discord.TextChannel) ): # Anonymously sending to the user. - tag = self.bot.config['mod_tag'] + tag = self.bot.config["mod_tag"] if tag is None: tag = str(message.author.top_role) - name = self.bot.config['anon_username'] + name = self.bot.config["anon_username"] if name is None: name = tag - avatar_url = self.bot.config['anon_avatar_url'] + avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: avatar_url = self.bot.guild.icon_url else: @@ -648,7 +657,7 @@ async def send( embed.set_footer(text="Anonymous Reply") # Normal messages elif not anonymous: - mod_tag = self.bot.config['mod_tag'] + mod_tag = self.bot.config["mod_tag"] if mod_tag is None: mod_tag = str(message.author.top_role) embed.set_footer(text=mod_tag) # Normal messages diff --git a/core/time.py b/core/time.py index b4689cc518..da9bdafcb4 100644 --- a/core/time.py +++ b/core/time.py @@ -186,7 +186,7 @@ async def convert(self, ctx, argument): if not (end < len(argument) and argument[end] == '"'): raise BadArgument("If the time is quoted, you must unquote it.") - remaining = argument[end + 1:].lstrip(" ,.!") + remaining = argument[end + 1 :].lstrip(" ,.!") else: remaining = argument[end:].lstrip(" ,.!") elif len(argument) == end: From 24f9a15dbc7383ccd9cf8b2e09197739349798c4 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 11 Jul 2019 20:36:41 -0700 Subject: [PATCH 0312/1221] Formatting --- .pylintrc | 3 ++- bot.py | 26 +++++++++++++------------- cogs/plugins.py | 7 +++---- cogs/utility.py | 23 +++++++++++++++++------ core/thread.py | 13 ++++++------- 5 files changed, 41 insertions(+), 31 deletions(-) diff --git a/.pylintrc b/.pylintrc index 60d8d81041..55e09c1a93 100644 --- a/.pylintrc +++ b/.pylintrc @@ -82,7 +82,8 @@ disable=raw-checker-failed, too-many-lines, line-too-long, bad-continuation, - invalid-name + invalid-name, + logging-too-many-args # Enable the message, report, category or checker with the given id(s). You can diff --git a/bot.py b/bot.py index bbe810cd44..2a7611a332 100644 --- a/bot.py +++ b/bot.py @@ -122,10 +122,10 @@ def _configure_logging(self): if log_level is not None: logger.setLevel(log_level) ch.setLevel(log_level) - logger.info("Logging level: " + level_text) + logger.info("Logging level: %s", level_text) else: - logger.info("Invalid logging level set. ") - logger.warning("Using default logging level: INFO") + logger.info("Invalid logging level set.") + logger.warning("Using default logging level: INFO.") @property def version(self) -> str: @@ -152,7 +152,7 @@ def _load_extensions(self): logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") logger.info("││││ │ │││││├─┤││") logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") - logger.info(f"v{__version__}") + logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") logger.line() @@ -160,11 +160,11 @@ def _load_extensions(self): if not file.endswith(".py"): continue cog = f"cogs.{file[:-3]}" - logger.info(f"Loading {cog}") + logger.info("Loading %s.", cog) try: self.load_extension(cog) except Exception: - logger.exception(f"Failed to load {cog}") + logger.exception("Failed to load %s.", cog) def run(self, *args, **kwargs): try: @@ -388,9 +388,9 @@ async def on_ready(self): logger.line() logger.info("Client ready.") logger.line() - logger.info("Logged in as: %s", str(self.user)) - logger.info("User ID: %s", str(self.user.id)) - logger.info("Prefix: %s", str(self.prefix)) + logger.info("Logged in as: %s", self.user) + logger.info("User ID: %s", self.user.id) + logger.info("Prefix: %s", self.prefix) logger.info("Guild Name: %s", self.guild.name if self.guild else "Invalid") logger.info("Guild ID: %s", self.guild.id if self.guild else "Invalid") logger.line() @@ -668,7 +668,7 @@ async def update_perms( else: if value in permissions[name]: permissions[name].remove(value) - logger.info(f"Updating permissions for {name}, {value} (add={add}).") + logger.info("Updating permissions for %s, %s (add=%s).", name, value, add) await self.config.update() async def on_message(self, message): @@ -895,7 +895,7 @@ async def on_message_edit(self, before, after): break async def on_error(self, event_method, *args, **kwargs): - logger.error("Ignoring exception in %s", str(event_method)) + logger.error("Ignoring exception in %s.", event_method) logger.error("Unexpected exception:", exc_info=sys.exc_info()) async def on_command_error(self, context, exception): @@ -916,7 +916,7 @@ async def on_command_error(self, context, exception): ) ) elif isinstance(exception, commands.CommandNotFound): - logger.warning("CommandNotFound: %s", str(exception)) + logger.warning("CommandNotFound: %s", exception) elif isinstance(exception, commands.MissingRequiredArgument): await context.send_help(context.command) elif isinstance(exception, commands.CheckFailure): @@ -927,7 +927,7 @@ async def on_command_error(self, context, exception): color=discord.Color.red(), description=check.fail_msg ) ) - logger.warning("CheckFailure: %s", str(exception)) + logger.warning("CheckFailure: %s", exception) else: logger.error("Unexpected exception:", exc_info=exception) diff --git a/cogs/plugins.py b/cogs/plugins.py index ddef4c9e77..b59e645616 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -397,10 +397,9 @@ def find_index(find_name): if find_name == n: return i - index = 0 - if plugin_name in self.registry: - index = find_index(plugin_name) - elif plugin_name is not None: + index = next((i for i, (n, _) in enumerate(registry) if plugin_name == n), None) + + if index is None: embed = discord.Embed( color=discord.Color.red(), description=f'Could not find a plugin with name "{plugin_name}" within the registry.', diff --git a/cogs/utility.py b/cogs/utility.py index 79fc0c213b..e05fd8600f 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -78,11 +78,11 @@ async def format_cog_help(self, cog): def process_help_msg(self, help_: str): return help_.format(prefix=self.clean_prefix) if help_ else "No help message." - async def send_bot_help(self, cogs): + async def send_bot_help(self, mapping): embeds = [] # TODO: Implement for no cog commands - cogs = list(filter(None, cogs)) + cogs = list(filter(None, mapping)) bot = self.context.bot @@ -230,7 +230,7 @@ async def changelog(self, ctx): try: paginator = PaginatorSession(ctx, *changelog.embeds) await paginator.run() - except: + except Exception: await ctx.send(changelog.CHANGELOG_URL) @commands.command(aliases=["bot", "info"]) @@ -239,7 +239,11 @@ async def changelog(self, ctx): async def about(self, ctx): """Shows information about this bot.""" embed = Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) - embed.set_author(name="Modmail - About", icon_url=self.bot.user.avatar_url) + embed.set_author( + name="Modmail - About", + icon_url=self.bot.user.avatar_url, + url="https://discord.gg/F34cRU8", + ) embed.set_thumbnail(url=self.bot.user.avatar_url) desc = "This is an open source Discord bot that serves as a means for " @@ -264,7 +268,14 @@ async def about(self, ctx): name="GitHub", value="https://github.com/kyb3r/modmail", inline=False ) - embed.add_field(name="Donate", value="[Patreon](https://patreon.com/kyber)") + embed.add_field( + name="Discord Server", value="https://discord.gg/F34cRU8", inline=False + ) + + embed.add_field( + name="Donate", + value="Support this bot on [`Patreon`](https://patreon.com/kyber).", + ) embed.set_footer(text=footer) await ctx.send(embed=embed) @@ -1289,7 +1300,7 @@ def get_level(perm_level): @checks.has_permissions(PermissionLevel.OWNER) async def oauth(self, ctx): """Commands relating to Logviewer oauth2 login authentication. - + This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature. """ await ctx.send_help(ctx.command) diff --git a/core/thread.py b/core/thread.py index 03aa06302a..a05b67fb42 100644 --- a/core/thread.py +++ b/core/thread.py @@ -6,8 +6,9 @@ from datetime import datetime, timedelta from types import SimpleNamespace as param -import discord import isodate + +import discord from discord.ext.commands import MissingRequiredArgument, CommandError from core.time import human_timedelta @@ -98,14 +99,14 @@ async def setup(self, *, creator=None, category=None): name=self.manager.format_channel_name(recipient), category=category, overwrites=overwrites, - reason="Creating a thread channel", + reason="Creating a thread channel.", ) except discord.HTTPException as e: # Failed to create due to 50 channel limit. self.manager.cache.pop(self.id) embed = discord.Embed(color=discord.Color.red()) - embed.title = "Error while trying to create a thread" - embed.description = e.message + embed.title = "Error while trying to create a thread." + embed.description = str(e) embed.add_field(name="Recipient", value=recipient.mention) if self.bot.log_channel is not None: @@ -361,9 +362,7 @@ async def _fetch_timeout( :returns: None if no timeout is set. """ timeout = self.bot.config["thread_auto_close"] - if not timeout: - return timeout - else: + if timeout: try: timeout = isodate.parse_duration(timeout) except isodate.ISO8601Error: From e68b99704209070a32c8b413840416a8310b9498 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 11 Jul 2019 21:16:35 -0700 Subject: [PATCH 0313/1221] Lint + black --- .pylintrc | 2 +- bot.py | 9 +++++--- cogs/plugins.py | 7 +------ cogs/utility.py | 21 +++++++++++-------- core/checks.py | 7 ++++--- core/thread.py | 56 ++++++++++++++++++++++++++----------------------- core/utils.py | 6 +++--- 7 files changed, 57 insertions(+), 51 deletions(-) diff --git a/.pylintrc b/.pylintrc index 55e09c1a93..1bed8185c7 100644 --- a/.pylintrc +++ b/.pylintrc @@ -508,4 +508,4 @@ min-public-methods=2 # Exceptions that will emit a warning when being caught. Defaults to # "Exception". -overgeneral-exceptions=Exception +overgeneral-exceptions=BaseException diff --git a/bot.py b/bot.py index 2a7611a332..32960b4336 100644 --- a/bot.py +++ b/bot.py @@ -224,6 +224,7 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: self.prefix, self.prefix, ) + return None @property def is_connected(self) -> bool: @@ -257,6 +258,7 @@ def guild_id(self) -> typing.Optional[int]: return int(str(guild_id)) except ValueError: raise ValueError("Invalid guild_id set.") + return None @property def guild(self) -> typing.Optional[discord.Guild]: @@ -302,6 +304,7 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]: self.config["main_category_id"] = cat.id logger.debug("No main category set, however, one was found. Setting...") return cat + return None @property def blocked_users(self) -> typing.Dict[str, str]: @@ -365,12 +368,12 @@ async def setup_indexes(self): # Backwards compatibility old_index = "messages.content_text_messages.author.name_text" if old_index in index_info: - logger.info(f"Dropping old index: {old_index}") + logger.info("Dropping old index: %s", old_index) await coll.drop_index(old_index) if index_name not in index_info: logger.info('Creating "text" index for logs collection.') - logger.info("Name: " + index_name) + logger.info("Name: %s", index_name) await coll.create_index( [ ("messages.content", "text"), @@ -638,7 +641,7 @@ async def get_context(self, message, *, cls=commands.Context): # Check if there is any aliases being called. alias = self.aliases.get(invoker) if alias is not None: - ctx._alias_invoked = True + ctx._alias_invoked = True # pylint: disable=W0212 len_ = len(f"{invoked_prefix}{invoker}") view = StringView(f"{alias}{ctx.message.content[len_:]}") ctx.view = view diff --git a/cogs/plugins.py b/cogs/plugins.py index b59e645616..7f2dcec871 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -336,7 +336,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): err = exc.stderr.decode("utf8").strip() embed = discord.Embed( - description=f"An error occured while updating: {err}.", + description=f"An error occurred while updating: {err}.", color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -392,11 +392,6 @@ async def plugin_registry(self, ctx, *, plugin_name: str = None): registry = list(self.registry.items()) random.shuffle(registry) - def find_index(find_name): - for i, (n, _) in enumerate(registry): - if find_name == n: - return i - index = next((i for i, (n, _) in enumerate(registry) if plugin_name == n), None) if index is None: diff --git a/cogs/utility.py b/cogs/utility.py index e05fd8600f..89bb079267 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -8,10 +8,10 @@ from datetime import datetime from difflib import get_close_matches from io import StringIO, BytesIO -from typing import Union -from types import SimpleNamespace as param from json import JSONDecodeError, loads from textwrap import indent +from types import SimpleNamespace as param +from typing import Union from discord import Embed, Color, Activity, Role from discord.enums import ActivityType, Status @@ -206,9 +206,12 @@ def __init__(self, bot): verify_checks=False, command_attrs={"help": "Shows this help message."} ) # Looks a bit ugly - self.bot.help_command._command_impl = checks.has_permissions( + # noinspection PyProtectedMember + self.bot.help_command._command_impl = checks.has_permissions( # pylint: disable=W0212 PermissionLevel.REGULAR - )(self.bot.help_command._command_impl) + )( + self.bot.help_command._command_impl # pylint: disable=W0212 + ) self.bot.help_command.cog = self @@ -1299,7 +1302,7 @@ def get_level(perm_level): @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def oauth(self, ctx): - """Commands relating to Logviewer oauth2 login authentication. + """Commands relating to logviewer oauth2 login authentication. This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature. """ @@ -1407,7 +1410,7 @@ def paginate(text: str): try: exec(to_compile, env) # pylint: disable=exec-used - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: await ctx.send(f"```py\n{exc.__class__.__name__}: {exc}\n```") return await ctx.message.add_reaction("\u2049") @@ -1415,7 +1418,7 @@ def paginate(text: str): try: with redirect_stdout(stdout): ret = await func() - except Exception: # pylint: disable=broad-except + except Exception: value = stdout.getvalue() await ctx.send(f"```py\n{value}{traceback.format_exc()}\n```") return await ctx.message.add_reaction("\u2049") @@ -1426,7 +1429,7 @@ def paginate(text: str): if value: try: await ctx.send(f"```py\n{value}\n```") - except Exception: # pylint: disable=broad-except + except Exception: paginated_text = paginate(value) for page in paginated_text: if page == paginated_text[-1]: @@ -1436,7 +1439,7 @@ def paginate(text: str): else: try: await ctx.send(f"```py\n{value}{ret}\n```") - except Exception: # pylint: disable=broad-except + except Exception: paginated_text = paginate(f"{value}{ret}") for page in paginated_text: if page == paginated_text[-1]: diff --git a/core/checks.py b/core/checks.py index b706d2bea3..42da8cf4b8 100644 --- a/core/checks.py +++ b/core/checks.py @@ -43,7 +43,9 @@ async def predicate(ctx): return commands.check(predicate) -async def check_permissions(ctx, command_name, permission_level) -> bool: +async def check_permissions( # pylint: disable=R0911 + ctx, command_name, permission_level +) -> bool: """Logic for checking permissions for a command for a user""" if await ctx.bot.is_owner(ctx.author): # Direct bot owner (creator) has absolute power over the bot @@ -79,8 +81,7 @@ async def check_permissions(ctx, command_name, permission_level) -> bool: role.id in level_permissions[level.name] for role in author_roles ) has_perm_id = ctx.author.id in level_permissions[level.name] - if has_perm_role or has_perm_id: - return True + return has_perm_role or has_perm_id return False diff --git a/core/thread.py b/core/thread.py index a05b67fb42..c2796a342f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -330,7 +330,9 @@ async def _close( await asyncio.gather(*tasks) - async def cancel_closure(self, auto_close: bool = False, all: bool = False) -> None: + async def cancel_closure( + self, auto_close: bool = False, all: bool = False # pylint: disable=W0622 + ) -> None: if self.close_task is not None and (not auto_close or all): self.close_task.cancel() self.close_task = None @@ -590,21 +592,23 @@ async def send( delete_message = not bool(message.attachments) - attachments = [(a.url, a.filename) for a in message.attachments] + ext = [(a.url, a.filename) for a in message.attachments] - images = [x for x in attachments if is_image_url(*x)] - attachments = [x for x in attachments if not is_image_url(*x)] + images = [] + attachments = [] + for attachment in ext: + if is_image_url(attachment[0]): + images.append(attachment) + else: + attachments.append(attachment) - image_links = [ - (link, None) - for link in re.findall( - r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", - message.content, - ) - ] + image_urls = re.findall( + r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + message.content, + ) - image_links = [x for x in image_links if is_image_url(*x)] - images.extend(image_links) + image_urls = [(url, None) for url in image_urls if is_image_url(url)] + images.extend(image_urls) embedded_image = False @@ -613,15 +617,15 @@ async def send( additional_images = [] additional_count = 1 - for att in images: + for url, filename in images: if not prioritize_uploads or ( - is_image_url(*att) and not embedded_image and att[1] + is_image_url(url) and not embedded_image and filename ): - embed.set_image(url=att[0]) - if att[1]: - embed.add_field(name="Image", value=f"[{att[1]}]({att[0]})") + embed.set_image(url=url) + if filename: + embed.add_field(name="Image", value=f"[{filename}]({url})") embedded_image = True - elif att[1] is not None: + elif filename is not None: if note: color = discord.Color.blurple() elif from_mod: @@ -630,9 +634,9 @@ async def send( color = self.bot.recipient_color img_embed = discord.Embed(color=color) - img_embed.set_image(url=att[0]) - img_embed.title = att[1] - img_embed.url = att[0] + 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})" ) @@ -642,9 +646,9 @@ async def send( file_upload_count = 1 - for att in attachments: + for url, filename in attachments: embed.add_field( - name=f"File upload ({file_upload_count})", value=f"[{att[1]}]({att[0]})" + name=f"File upload ({file_upload_count})", value=f"[{filename}]({url})" ) file_upload_count += 1 @@ -677,7 +681,7 @@ async def send( else: mentions = None - _msg = await destination.send(mentions, embed=embed) + msg = await destination.send(mentions, embed=embed) if additional_images: self.ready = False @@ -687,7 +691,7 @@ async def send( if delete_message: self.bot.loop.create_task(ignore(message.delete())) - return _msg + return msg def get_notifications(self) -> str: key = str(self.id) diff --git a/core/utils.py b/core/utils.py index 0d999b8f47..4f76d6d4d6 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,6 +1,6 @@ import re import typing -from distutils.util import strtobool as _stb +from distutils.util import strtobool as _stb # pylint: disable=E0401 from urllib import parse from discord import Object @@ -37,7 +37,7 @@ async def convert(self, ctx, argument): return Object(int(match.group(1))) -def truncate(text: str, max: int = 50) -> str: +def truncate(text: str, max: int = 50) -> str: # pylint: disable=W0622 """ Reduces the string to `max` length, by trimming the message into "...". @@ -85,7 +85,7 @@ def format_preview(messages: typing.List[typing.Dict[str, typing.Any]]): return out or "No Messages" -def is_image_url(url: str, _=None) -> bool: +def is_image_url(url: str) -> bool: """ Check if the URL is pointing to an image. From 3c917b4bbbb5a629e4156cd0b25e27b76a83fe67 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 11 Jul 2019 21:33:02 -0700 Subject: [PATCH 0314/1221] Updated pipfile --- .travis.yml | 6 +- Pipfile | 5 +- Pipfile.lock | 167 +++++++++++++++++++++++++++------------------------ Procfile | 2 +- bot.py | 8 ++- 5 files changed, 100 insertions(+), 88 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7458b1db96..14f9928d1b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ matrix: dist: xenial install: - - pipenv install - - pipenv install pylint + - pipenv install -d -script: python .lint.py +script: + - pipenv run python .lint.py diff --git a/Pipfile b/Pipfile index a37ea80cf9..095270dc6c 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,7 @@ verify_ssl = true [dev-packages] black = "==19.3b0" +pylint = "*" [packages] colorama = ">=0.4.0" @@ -17,10 +18,12 @@ isodate = ">=0.6.0" dnspython = "~=1.16.0" parsedatetime = "==2.4" aiohttp = "<3.6.0,>=3.3.0" -pylint = "*" python-dotenv = ">=0.10.3" pipenv = "==2018.11.26" "discord.py" = "==1.1.1" [requires] python_version = "3.7" + +[scripts] +bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index a39f536e5f..c845f3a614 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e4f5169004c14f38445d36c1bb89baeeaacc7dd5f43aa88f51b91d0df7dac2de" + "sha256": "4c7d21c64125daa0eea6dfb5499eef9d589d2164d6955689e845c83e37d115ae" }, "pipfile-spec": 6, "requires": { @@ -44,13 +44,6 @@ "index": "pypi", "version": "==3.5.4" }, - "astroid": { - "hashes": [ - "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", - "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" - ], - "version": "==2.2.5" - }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", @@ -131,43 +124,6 @@ "index": "pypi", "version": "==0.6.0" }, - "isort": { - "hashes": [ - "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", - "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" - ], - "version": "==4.3.21" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", - "sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", - "sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", - "sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", - "sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", - "sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", - "sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", - "sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", - "sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", - "sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", - "sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", - "sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", - "sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", - "sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", - "sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", - "sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", - "sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", - "sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1" - ], - "version": "==1.4.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, "motor": { "hashes": [ "sha256:462fbb824f4289481c158227a2579d6adaf1ec7c70cf7ebe60ed6ceb321e5869", @@ -234,14 +190,6 @@ "index": "pypi", "version": "==2018.11.26" }, - "pylint": { - "hashes": [ - "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", - "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" - ], - "index": "pypi", - "version": "==2.3.1" - }, "pymongo": { "hashes": [ "sha256:32421df60d06f479d71b6b539642e410ece3006e8910688e68df962c8eb40a21", @@ -298,27 +246,6 @@ ], "version": "==1.12.0" }, - "typed-ast": { - "hashes": [ - "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", - "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", - "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", - "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", - "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", - "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", - "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", - "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", - "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", - "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", - "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", - "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", - "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", - "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", - "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" - ], - "markers": "implementation_name == 'cpython'", - "version": "==1.4.0" - }, "uvloop": { "hashes": [ "sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573", @@ -376,12 +303,6 @@ ], "version": "==6.0" }, - "wrapt": { - "hashes": [ - "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" - ], - "version": "==1.11.2" - }, "yarl": { "hashes": [ "sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9", @@ -407,6 +328,13 @@ ], "version": "==1.4.3" }, + "astroid": { + "hashes": [ + "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", + "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" + ], + "version": "==2.2.5" + }, "attrs": { "hashes": [ "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", @@ -429,12 +357,91 @@ ], "version": "==7.0" }, + "isort": { + "hashes": [ + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + ], + "version": "==4.3.21" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", + "sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", + "sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", + "sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", + "sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", + "sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", + "sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", + "sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", + "sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", + "sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", + "sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", + "sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", + "sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", + "sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", + "sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", + "sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", + "sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", + "sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1" + ], + "version": "==1.4.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", + "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" + ], + "index": "pypi", + "version": "==2.3.1" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, "toml": { "hashes": [ "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" ], "version": "==0.10.0" + }, + "typed-ast": { + "hashes": [ + "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", + "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", + "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", + "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", + "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", + "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", + "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", + "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", + "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", + "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", + "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", + "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", + "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", + "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", + "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" + ], + "markers": "implementation_name == 'cpython'", + "version": "==1.4.0" + }, + "wrapt": { + "hashes": [ + "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + ], + "version": "==1.11.2" } } } diff --git a/Procfile b/Procfile index 29cff6d9d1..5ae4640def 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -worker: python bot.py +worker: pipenv run bot diff --git a/bot.py b/bot.py index 32960b4336..c83d76ac50 100644 --- a/bot.py +++ b/bot.py @@ -1006,8 +1006,10 @@ async def metadata_loop(self): if __name__ == "__main__": if os.name != "nt": - import uvloop - - uvloop.install() + try: + import uvloop + uvloop.install() + except ImportError: + pass bot = ModmailBot() bot.run() From 1dce4750296fa2311ecbf6900c7b9b8020f46b16 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 11 Jul 2019 21:50:08 -0700 Subject: [PATCH 0315/1221] A minimal version of req.txt --- requirements.txt.min | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 requirements.txt.min diff --git a/requirements.txt.min b/requirements.txt.min new file mode 100644 index 0000000000..27849727ad --- /dev/null +++ b/requirements.txt.min @@ -0,0 +1,15 @@ +# Generated as of July 11, 2019 +# This is the bare minimum requirements.txt for running Modmail. +# To install requirements.txt run: pip install -r requirements.txt.min + +aiohttp==3.5.4 +colorama==0.4.1 +discord.py==1.1.1 +dnspython==1.16.0 +emoji==0.5.2 +isodate==0.6.0 +motor==2.0.0 +natural==0.2.0 +parsedatetime==2.4 +python-dateutil==2.8.0 +python-dotenv==0.10.3 From f9781c319f71831018e71bb0265ba11b0a8f44ae Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 12 Jul 2019 17:04:45 -0700 Subject: [PATCH 0316/1221] Remove requirement for colorama --- bot.py | 6 ++++++ core/models.py | 9 ++++----- core/thread.py | 1 + requirements.txt.min => requirements.min.txt | 3 +-- 4 files changed, 12 insertions(+), 7 deletions(-) rename requirements.txt.min => requirements.min.txt (75%) diff --git a/bot.py b/bot.py index c83d76ac50..6e95cc0ba7 100644 --- a/bot.py +++ b/bot.py @@ -20,6 +20,12 @@ from emoji import UNICODE_EMOJI from motor.motor_asyncio import AsyncIOMotorClient +try: + from colorama import init + init() +except ImportError: + pass + from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import human_join, strtobool diff --git a/core/models.py b/core/models.py index 4ba3964231..98780a3002 100644 --- a/core/models.py +++ b/core/models.py @@ -4,7 +4,10 @@ from discord import Color, Embed from discord.ext import commands -from colorama import Fore, Style, init +try: + from colorama import Fore, Style +except ImportError: + Fore = Style = type('Dummy', (object,), {'__getattr__': lambda self, item: ''})() class PermissionLevel(IntEnum): @@ -30,10 +33,6 @@ def embed(self): class ModmailLogger(logging.Logger): - def __init__(self, *args, **kwargs): - init() - super().__init__(*args, **kwargs) - @staticmethod def _debug_(*msgs): return f'{Fore.CYAN}{" ".join(msgs)}{Style.RESET_ALL}' diff --git a/core/thread.py b/core/thread.py index c2796a342f..11521d74ac 100644 --- a/core/thread.py +++ b/core/thread.py @@ -808,6 +808,7 @@ async def _find_from_channel(self, channel): thread.ready = True return thread + return None def create( self, diff --git a/requirements.txt.min b/requirements.min.txt similarity index 75% rename from requirements.txt.min rename to requirements.min.txt index 27849727ad..70a2c8ae59 100644 --- a/requirements.txt.min +++ b/requirements.min.txt @@ -1,9 +1,8 @@ # Generated as of July 11, 2019 # This is the bare minimum requirements.txt for running Modmail. -# To install requirements.txt run: pip install -r requirements.txt.min +# To install requirements.txt run: pip install -r requirements.min.txt aiohttp==3.5.4 -colorama==0.4.1 discord.py==1.1.1 dnspython==1.16.0 emoji==0.5.2 From a2787e89ea82afabf78ff4aef3ac3e33ef3256f9 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 12 Jul 2019 17:44:22 -0700 Subject: [PATCH 0317/1221] thread_auto_close_silently + some fixes --- CHANGELOG.md | 13 ++++++++----- cogs/modmail.py | 4 ++-- core/config.py | 2 ++ core/thread.py | 27 +++++++++++++++++++++++++-- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bdc482732..41c690dbf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `disable_recipient_thread_close` is removed, a new configuration variable `recipient_thread_close` replaces it which defaults to False. - Truthy and falsy values for binary configuration variables are now interpreted respectfully. +### Added + +- `?sfw`, mark a thread as "safe for work", undos `?nsfw`. +- New config variable, `thread_auto_close_silently`, when set to a truthy value, no message will be sent when thread is auto-closed. +- New configuration variable `thread_self_closable_creation_footer` — the footer when `recipient_thread_close` is enabled. + ### Changes - `thread_auto_close_response` has a configurable variable `{timeout}`. -- New configuration variable `thread_self_closable_creation_footer`, the footer when `recipient_thread_close` is enabled. - `?snippet` is now the default command name instead of `?snippets` (`?snippets` is still usable). This is to make this consistent with `?alias`/`?aliases`. ### Fixes @@ -23,10 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `?notify` no longer carries over to the next thread. - `discord.NotFound` errors for `on_raw_reaction_add`. - `mod_typing` and `user_typing` will no longer show when user is blocked. - -### Added - -- `?sfw`, mark a thread as "safe for work", undos `?nsfw`. +- Better `?block` usage message. +- Resolves errors when message was sent by mods after thread is closed somehow. ### Internal diff --git a/cogs/modmail.py b/cogs/modmail.py index 9e4228d4c5..dc4e92e87b 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -917,7 +917,7 @@ async def blocked_whitelist(self, ctx, *, user: User = None): return await ctx.send(embed=embed) - @commands.command() + @commands.command(usage="[user] [duration] [close message]") @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing async def block( @@ -931,7 +931,7 @@ async def block( Leave `user` blank when this command is used within a thread channel to block the current recipient. `user` may be a user ID, mention, or name. - `after` may be a simple "human-readable" time text. See `{prefix}help close` for examples. + `duration` may be a simple "human-readable" time text. See `{prefix}help close` for examples. """ reason = "" diff --git a/core/config.py b/core/config.py index 8d29a3c2b9..f5f3b9c50b 100644 --- a/core/config.py +++ b/core/config.py @@ -42,6 +42,7 @@ class ConfigManager: "blocked_emoji": "🚫", "close_emoji": "🔒", "recipient_thread_close": False, + "thread_auto_close_silently": False, "thread_auto_close": 0, "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.", @@ -106,6 +107,7 @@ class ConfigManager: "mod_typing", "reply_without_command", "recipient_thread_close", + "thread_auto_close_silently" } defaults = {**public_keys, **private_keys, **protected_keys} diff --git a/core/thread.py b/core/thread.py index 11521d74ac..696afb16f7 100644 --- a/core/thread.py +++ b/core/thread.py @@ -232,7 +232,11 @@ async def close( async def _close( self, closer, silent=False, delete_channel=True, message=None, scheduled=False ): - self.manager.cache.pop(self.id) + try: + self.manager.cache.pop(self.id) + except KeyError: + logger.warning('Thread already closed.', exc_info=True) + return await self.cancel_closure(all=True) @@ -396,6 +400,21 @@ async def _restart_close_timer(self): reset_time = datetime.utcnow() + timedelta(seconds=seconds) human_time = human_timedelta(dt=reset_time) + try: + thread_auto_close_silently = strtobool( + self.bot.config["thread_auto_close_silently"] + ) + except ValueError: + thread_auto_close_silently = self.bot.config.remove("thread_auto_close_silently") + + if thread_auto_close_silently: + return await self.close( + closer=self.bot.user, + silent=True, + after=int(seconds), + auto_close=True, + ) + # Grab message close_message = self.bot.config["thread_auto_close_response"].format( timeout=human_time @@ -674,7 +693,11 @@ async def send( # noinspection PyUnresolvedReferences,PyDunderSlots embed.color = self.bot.recipient_color # pylint: disable=E0237 - await destination.trigger_typing() + try: + await destination.trigger_typing() + except discord.NotFound: + logger.warning('Channel not found.', exc_info=True) + return if not from_mod and not note: mentions = self.get_notifications() From 8969a79cba9007069ff8d1581fff5a9b4275d351 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 12 Jul 2019 18:03:00 -0700 Subject: [PATCH 0318/1221] Black formatting --- bot.py | 2 ++ core/config.py | 4 ++-- core/models.py | 2 +- core/thread.py | 13 ++++++------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/bot.py b/bot.py index 6e95cc0ba7..fba3490fea 100644 --- a/bot.py +++ b/bot.py @@ -22,6 +22,7 @@ try: from colorama import init + init() except ImportError: pass @@ -1014,6 +1015,7 @@ async def metadata_loop(self): if os.name != "nt": try: import uvloop + uvloop.install() except ImportError: pass diff --git a/core/config.py b/core/config.py index f5f3b9c50b..5f7eda1ea1 100644 --- a/core/config.py +++ b/core/config.py @@ -43,7 +43,7 @@ class ConfigManager: "close_emoji": "🔒", "recipient_thread_close": False, "thread_auto_close_silently": False, - "thread_auto_close": 0, + "thread_auto_close": None, "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", @@ -107,7 +107,7 @@ class ConfigManager: "mod_typing", "reply_without_command", "recipient_thread_close", - "thread_auto_close_silently" + "thread_auto_close_silently", } defaults = {**public_keys, **private_keys, **protected_keys} diff --git a/core/models.py b/core/models.py index 98780a3002..34c79da1c5 100644 --- a/core/models.py +++ b/core/models.py @@ -7,7 +7,7 @@ try: from colorama import Fore, Style except ImportError: - Fore = Style = type('Dummy', (object,), {'__getattr__': lambda self, item: ''})() + Fore = Style = type("Dummy", (object,), {"__getattr__": lambda self, item: ""})() class PermissionLevel(IntEnum): diff --git a/core/thread.py b/core/thread.py index 696afb16f7..e85a734bd6 100644 --- a/core/thread.py +++ b/core/thread.py @@ -235,7 +235,7 @@ async def _close( try: self.manager.cache.pop(self.id) except KeyError: - logger.warning('Thread already closed.', exc_info=True) + logger.warning("Thread already closed.", exc_info=True) return await self.cancel_closure(all=True) @@ -405,14 +405,13 @@ async def _restart_close_timer(self): self.bot.config["thread_auto_close_silently"] ) except ValueError: - thread_auto_close_silently = self.bot.config.remove("thread_auto_close_silently") + thread_auto_close_silently = self.bot.config.remove( + "thread_auto_close_silently" + ) if thread_auto_close_silently: return await self.close( - closer=self.bot.user, - silent=True, - after=int(seconds), - auto_close=True, + closer=self.bot.user, silent=True, after=int(seconds), auto_close=True ) # Grab message @@ -696,7 +695,7 @@ async def send( try: await destination.trigger_typing() except discord.NotFound: - logger.warning('Channel not found.', exc_info=True) + logger.warning("Channel not found.", exc_info=True) return if not from_mod and not note: From 41f7207513c5eb6100a742efaaf88dc316ff2d2e Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 12 Jul 2019 18:06:58 -0700 Subject: [PATCH 0319/1221] Bump: v3.1.0 --- CHANGELOG.md | 2 +- bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c690dbf4..6c50478373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -# [UNRELEASED] +# v3.1.0 ### Breaking diff --git a/bot.py b/bot.py index fba3490fea..4b3f30464d 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.0.3" +__version__ = "3.1.0" import asyncio import logging From 40fee5c5c6bf1b897cacc03d8ddea93d090a5e2b Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 12 Jul 2019 18:16:00 -0700 Subject: [PATCH 0320/1221] Updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c50478373..e4a095d418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,11 +17,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `?sfw`, mark a thread as "safe for work", undos `?nsfw`. - New config variable, `thread_auto_close_silently`, when set to a truthy value, no message will be sent when thread is auto-closed. - New configuration variable `thread_self_closable_creation_footer` — the footer when `recipient_thread_close` is enabled. +- Added a minimalistic version of requirements.txt (named requirements.min.txt) that contains only the absolute minimum of Modmail. + - For users having trouble with pipenv or any other reason. ### Changes - `thread_auto_close_response` has a configurable variable `{timeout}`. - `?snippet` is now the default command name instead of `?snippets` (`?snippets` is still usable). This is to make this consistent with `?alias`/`?aliases`. +- Colorama is no longer a necessity, this is due to some unsupported OS. ### Fixes From e585b23f7bd2e500882f837395ac4bc91958b05f Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 12 Jul 2019 18:46:40 -0700 Subject: [PATCH 0321/1221] Update README.md --- README.md | 88 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index effaa06338..72e382be11 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

- A feature rich Modmail bot for Discord. + A feature-rich Modmail bot for Discord.

@@ -32,70 +32,86 @@ Modmail is similar to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. +This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included! + ## How does it work? When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. -All threads are logged and you can view previous threads through the corresponding generated log link. Here is an [**example**](https://logs.modmail.tk/example) +All threads are logged and you can view previous threads through the corresponding generated log link. Here is an [**example**](https://logs.modmail.tk/example). ## Features -* **Highly Customisable** +* **Highly Customisable:** * Bot activity, prefix, category, log channel, etc. - * Fully customisable command permission system. - * Interface elements (color, responses, reactions, etc.) - * Snippets and *command aliases* - * Minimum account/guild age in order to create a thread. -* **Thread logs** + * Command permission system. + * Interface elements (color, responses, reactions, etc). + * Snippets and *command aliases*. + * Minimum duration for accounts to be created before allowed to contact Modmail (`account_age`). + * Minimum duration for member to be in the guild allowed to contact Modmail (`guild_age`). +* **Thread logs:** * When you close a thread, a [log link](https://logs.modmail.tk/example) is generated and posted to your log channel. - * Rendered in styled HTML like Discord. - * Login in via Discord to protect your logs ([Patron only feature](https://patreon.com/kyber)). - * See past logs of a user with `?logs` - * Searchable by text queries using `?logs search` -* **Robust implementation** + * Native Discord dark-mode feel. + * Markdown/formatting support. + * Login via Discord to protect your logs ([premium Patreon feature](https://patreon.com/kyber)). + * See past logs of a user with `?logs`. + * Searchable by text queries using `?logs search`. +* **Robust implementation:** * Scheduled tasks in human time, e.g. `?close in 2 hours silently`. - * Editing and deleting messages is synced on both ends. - * Support for the full range of message content (mutliple images, files). + * Editing and deleting messages are synced on all channels. + * Support for the full range of message content (multiple images, files). * Paginated commands interfaces via reactions. -This list is ever growing thanks to active development and our exceptional contributors. See a full list of documented commands by using the `help` command. +This list is ever-growing thanks to active development and our exceptional contributors. See a full list of documented commands by using the `?help` command. ## Installation -### Locally -Installation locally for development reasons or otherwise is as follows, you will need `python 3.7`. +### Heroku + +This bot can be hosted on Heroku. + +Installation via Heroku is possible with only your web browser. +The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. + +You can also set up auto-update. To do this: + - [Fork the repo](https://github.com/kyb3r/modmail/fork). + - [Install the Pull app for your fork](https://github.com/apps/pull). + - Then go to the Deploy tab in your Heroku account, select GitHub and connect your fork. + - Turn on auto-deploy for the `master` branch. + +### Hosting for patrons + +If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintenance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! + +### Locally + +Installation locally for development reasons or otherwise is as follows, you will need [`python 3.7`](https://www.python.org/downloads/). +Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying Heroku apps. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. + +Clone the repo: -Clone the repo ```console $ git clone https://github.com/kyb3r/modmail $ cd modmail ``` -Install dependancies -```console -$ pipenv install -``` +Install dependencies: -Rename the `.env.example` to `.env` and fill out the fields. -And finally, run the bot. ```console -$ pipenv run python3 bot.py +$ pipenv install ``` -### Hosting for patrons - -If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! +Rename the `.env.example` to `.env` and fill out the fields. If `.env.example` is hidden, create a text file named `.env` and copy the contents of [`.env.example`](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) then modify the values. -### Heroku -This bot can be hosted on heroku. Installation via Heroku is done in your web browser. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. +Finally, run the bot. -You can also set up autoupdates. To do this, [install the Pull app in your fork](https://github.com/apps/pull). Then go to the Deploy tab in your Heroku account, select GitHub and connect your fork. Turn on auto-deploy for the master branch. +```console +$ pipenv run bot +``` ## Plugins -Modmail supports the use of third party plugins to extend or add functionality to the bot. This allows the introduction of niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third party plugins can be found using the `plugins registry` command. To develop your own, check out the [documentation](https://github.com/kyb3r/modmail/wiki/Plugins) for plugins. +Modmail supports the use of third-party plugins to extend or add functionality to the bot. This allows the introduction of niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third party plugins can be found using the `plugins registry` command. To develop your own, check out the [documentation](https://github.com/kyb3r/modmail/wiki/Plugins) for plugins. ## Contributing -Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. - -This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, here's the link for our **[Patreon](https://www.patreon.com/kyber)**. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. From 0ecccca82b6542a25134e616d7aa12fb7926b040 Mon Sep 17 00:00:00 2001 From: Kyber Date: Mon, 15 Jul 2019 08:42:19 +1000 Subject: [PATCH 0322/1221] New sponsor (#324) * Update README.md * Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 389840138d..5776675b28 100644 --- a/README.md +++ b/README.md @@ -95,10 +95,16 @@ You can also set up autoupdates. To do this, [install the Pull app in your fork] Sepcial thanks to our sponsors for supporting the project. + + + + + + Become a [sponsor](https://patreon.com/kyber). From ecaac569837d3e81ea2bd92452fefbd293bef369 Mon Sep 17 00:00:00 2001 From: Kyber Date: Mon, 15 Jul 2019 09:13:14 +1000 Subject: [PATCH 0323/1221] Update SPONSORS.json --- SPONSORS.json | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index 29011d0adc..d4b968999c 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -1,26 +1,22 @@ [{ "embed": { - "title": "You can have a custom page for yourself here.", - "description": "Generate your own embed from [here](https://leovoel.github.io/embed-visualizer/) and make a pull request editing the `SPONSORS.json` file on [GitHub](https://github.com/kyb3r/modmail/blob/master/SPONSORS.json).", - "url": "https://patreon.com/kyber", - "color": 6071775, + "description": "This is a youtube channel that provides high quality reddit content. Videos are uploaded regularly so you are never out of funny, interesting or even creepy content!", + "color": 15114050, "footer": { - "icon_url": "https://vignette.wikia.nocookie.net/smashtopia/images/c/c9/Insert_Image_Here.jpg/revision/latest?cb=20170827054712", - "text": "footer text" + "icon_url": "https://i.imgur.com/fvNKUks.png", + "text": "I am a robot, son of Daniel (UK)" }, "thumbnail": { - "url": "https://vignette.wikia.nocookie.net/smashtopia/images/c/c9/Insert_Image_Here.jpg/revision/latest?cb=20170827054712" + "url": "https://i.imgur.com/WyzaPKY.png" }, "author": { - "name": "Nothing here yet!", - "url": "https://patreon.com/kyber", - "icon_url": "https://vignette.wikia.nocookie.net/smashtopia/images/c/c9/Insert_Image_Here.jpg/revision/latest?cb=20170827054712" + "name": "Sir Reddit", + "url": "https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1", + "icon_url": "https://i.imgur.com/WyzaPKY.png" }, "fields": [ - { - "name": "🤔", - "value": "This command lists the sponsors of this project on [**Patreon**](https://patreon.com/kyber)" - } + {"name": "Subscribe!", + "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)"} ] } }] From 6005d01fce96c228e57edcc7c132bce828b7ca83 Mon Sep 17 00:00:00 2001 From: Kyber Date: Mon, 15 Jul 2019 23:40:04 +1000 Subject: [PATCH 0324/1221] Update SPONSORS.json --- SPONSORS.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index d4b968999c..989f1dfdfd 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -15,8 +15,14 @@ "icon_url": "https://i.imgur.com/WyzaPKY.png" }, "fields": [ - {"name": "Subscribe!", - "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)"} + { + "name": "Subscribe!", + "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)" + }, + { + "name": "Discord Server!", + "value": "[**Click Here**](https://discord.gg/s8Ddphx)" + } ] } }] From ef2c10b9901b14b969d17c4d800e0e058edaebfa Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 17 Jul 2019 18:51:44 -0700 Subject: [PATCH 0325/1221] Add fallback method of searching through cache when channel topic is invalid --- core/thread.py | 11 ++++++++--- core/utils.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/thread.py b/core/thread.py index 87627dbaf7..d857c91df2 100644 --- a/core/thread.py +++ b/core/thread.py @@ -127,7 +127,6 @@ async def setup(self, *, creator=None, category=None): log_url = log_count = None # ensure core functionality still works - topic = f"User ID: {recipient.id}" if creator: mention = None else: @@ -147,7 +146,7 @@ async def send_genesis_message(): self.ready = True self.bot.dispatch("thread_ready", self) - await channel.edit(topic=topic) + await channel.edit(topic=f"User ID: {recipient.id}") self.bot.loop.create_task(send_genesis_message()) # Once thread is ready, tell the recipient. @@ -735,7 +734,13 @@ async def find( ) -> Thread: """Finds a thread from cache or from discord channel topics.""" if recipient is None and channel is not None: - return self._find_from_channel(channel) + thread = self._find_from_channel(channel) + if thread is None: + id, thread = next(((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.') + await channel.edit(topic=f"User ID: {id}") + return thread thread = None diff --git a/core/utils.py b/core/utils.py index bf88af1bff..411cbe3e65 100644 --- a/core/utils.py +++ b/core/utils.py @@ -188,7 +188,7 @@ def match_user_id(text: str) -> int: int The user ID if found. Otherwise, -1. """ - match = re.match(r"^User ID: (\d+)$", text) + match = re.search(r"\bUser ID: (\d+)\b", text) if match is not None: return int(match.group(1)) return -1 From 83ebd43051f19dce8f22fbfbfaa3467ca1d6c6e3 Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Thu, 18 Jul 2019 14:18:04 -0400 Subject: [PATCH 0326/1221] Remove Moderation Plugin (#327) Removed the Moderation Plugin Per Request of the Developer. --- plugins/registry.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 5c3e6690bb..8822612d5d 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -107,15 +107,6 @@ "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, - "moderation": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Rule offenders in your server? Lift the ban hammer or warn them! Moderation plugin to the rescue :tada:", - "bot_version": "2.20.1", - "title": "Moderation Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, "translator": { "repository": "officialpiyush/modmail-plugins", "branch": "master", From 226b3dc2ac52d5c25643f21cec7c8233be2357a9 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 18 Jul 2019 16:26:21 -0700 Subject: [PATCH 0327/1221] Bump discord.py version to 1.2.3 --- CHANGELOG.md | 1 + Pipfile | 2 +- Pipfile.lock | 12 ++++++------ cogs/utility.py | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23b77febb3..995e6a104a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `config.cache` is no longer accessible, use `config['key']` for getting, `config['key'] = value` for setting, `config.remove('key')` for removing. - Dynamic attribute for configs are removed, must use `config['key']` or `config.get('key')`. - Removed helper functions `info()` and `error()` for formatting logging, it's formatted automatically now. +- Bumped discord.py version to 1.2.3. # v3.0.3 diff --git a/Pipfile b/Pipfile index 095270dc6c..0b541bd697 100644 --- a/Pipfile +++ b/Pipfile @@ -20,7 +20,7 @@ parsedatetime = "==2.4" aiohttp = "<3.6.0,>=3.3.0" python-dotenv = ">=0.10.3" pipenv = "==2018.11.26" -"discord.py" = "==1.1.1" +"discord.py" = "==1.2.3" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index c845f3a614..5fbfe85392 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4c7d21c64125daa0eea6dfb5499eef9d589d2164d6955689e845c83e37d115ae" + "sha256": "46092f8b1032f2ac529f9b0df1aa725a4c6135d04826cd4cc4844af01879a4c2" }, "pipfile-spec": 6, "requires": { @@ -82,10 +82,10 @@ }, "discord.py": { "hashes": [ - "sha256:d0ab22f1fee1fcc02ac50a67ff49a5d1f6d7bc7eba77e34e35bd160b3ad3d7e8" + "sha256:4684733fa137cc7def18087ae935af615212e423e3dbbe3e84ef01d7ae8ed17d" ], "index": "pypi", - "version": "==1.1.1" + "version": "==1.2.3" }, "dnspython": { "hashes": [ @@ -265,10 +265,10 @@ }, "virtualenv": { "hashes": [ - "sha256:b7335cddd9260a3dd214b73a2521ffc09647bde3e9457fcca31dc3be3999d04a", - "sha256:d28ca64c0f3f125f59cabf13e0a150e1c68e5eea60983cc4395d88c584495783" + "sha256:861bbce3a418110346c70f5c7a696fdcf23a261424e1d28aa4f9362fc2ccbc19", + "sha256:ba8ce6a961d842320681fb90a3d564d0e5134f41dacd0e2bae7f02441dde2d52" ], - "version": "==16.6.1" + "version": "==16.6.2" }, "virtualenv-clone": { "hashes": [ diff --git a/cogs/utility.py b/cogs/utility.py index 1cbbc96b3f..315aff0c94 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -546,7 +546,7 @@ async def set_presence( if status_by_key: status = Status[status_identifier] else: - status = Status(status_identifier) + status = Status.try_value(status_identifier) except (KeyError, ValueError): if status_identifier is not None: msg = f"Invalid status type: {status_identifier}" @@ -564,7 +564,7 @@ async def set_presence( if activity_by_key: activity_type = ActivityType[activity_identifier] else: - activity_type = ActivityType(activity_identifier) + activity_type = ActivityType.try_value(activity_identifier) except (KeyError, ValueError): if activity_identifier is not None: msg = f"Invalid activity type: {activity_identifier}" From cbdf61fc981a1242530a30674cf9ec0989cd776f Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 18 Jul 2019 17:35:44 -0700 Subject: [PATCH 0328/1221] Use tasks --- CHANGELOG.md | 1 + bot.py | 90 +++++++++++++++++++++++++++---------------------- cogs/utility.py | 2 +- core/thread.py | 11 ++++-- 4 files changed, 60 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 995e6a104a..2e520bb1f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dynamic attribute for configs are removed, must use `config['key']` or `config.get('key')`. - Removed helper functions `info()` and `error()` for formatting logging, it's formatted automatically now. - Bumped discord.py version to 1.2.3. +- Use discord tasks for metadata loop. # v3.0.3 diff --git a/bot.py b/bot.py index 4b3f30464d..5043c0681d 100644 --- a/bot.py +++ b/bot.py @@ -11,7 +11,7 @@ from types import SimpleNamespace import discord -from discord.ext import commands +from discord.ext import commands, tasks from discord.ext.commands.view import StringView import isodate @@ -70,6 +70,7 @@ def __init__(self): self.config = ConfigManager(self) self.config.populate_cache() + self.threads = ThreadManager(self) self._configure_logging() @@ -81,7 +82,8 @@ def __init__(self): self.db = AsyncIOMotorClient(mongo_uri).modmail_bot self.plugin_db = PluginDatabaseClient(self) - self.metadata_task = self.loop.create_task(self.metadata_loop()) + self.metadata_loop = None + self._load_extensions() @property @@ -183,12 +185,6 @@ def run(self, *args, **kwargs): except Exception: logger.critical("Fatal exception", exc_info=True) finally: - try: - self.metadata_task.cancel() - self.loop.run_until_complete(self.metadata_task) - except asyncio.CancelledError: - logger.debug("metadata_task has been cancelled.") - self.loop.run_until_complete(self.logout()) for task in asyncio.all_tasks(self.loop): task.cancel() @@ -437,6 +433,19 @@ async def on_ready(self): logger.line() + self.metadata_loop = tasks.Loop( + self.post_metadata, + seconds=0, + minutes=0, + hours=1, + count=None, + reconnect=True, + loop=None, + ) + self.metadata_loop.before_loop(self.before_post_metadata) + self.metadata_loop.after_loop(self.after_post_metadata) + self.metadata_loop.start() + async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() @@ -958,7 +967,7 @@ async def validate_database_connection(self): try: await self.db.command("buildinfo") except Exception as exc: - logger.critical("Something went wrong " "while connecting to the database.") + logger.critical("Something went wrong while connecting to the database.") message = f"{type(exc).__name__}: {str(exc)}" logger.critical(message) @@ -981,43 +990,44 @@ async def validate_database_connection(self): else: logger.info("Successfully connected to the database.") - async def metadata_loop(self): + async def post_metadata(self): + owner = (await self.application_info()).owner + data = { + "owner_name": str(owner), + "owner_id": owner.id, + "bot_id": self.user.id, + "bot_name": str(self.user), + "avatar_url": str(self.user.avatar_url), + "guild_id": self.guild_id, + "guild_name": self.guild.name, + "member_count": len(self.guild.members), + "uptime": (datetime.utcnow() - self.start_time).total_seconds(), + "latency": f"{self.ws.latency * 1000:.4f}", + "version": self.version, + "selfhosted": True, + "last_updated": str(datetime.utcnow()), + } + + async with self.session.post("https://api.modmail.tk/metadata", json=data): + logger.debug("Uploading metadata to Modmail server.") + + async def before_post_metadata(self): + logger.info("Starting metadata loop.") await self.wait_for_connected() if not self.guild: - return - - owner = (await self.application_info()).owner + self.metadata_loop.cancel() - while not self.is_closed(): - data = { - "owner_name": str(owner), - "owner_id": owner.id, - "bot_id": self.user.id, - "bot_name": str(self.user), - "avatar_url": str(self.user.avatar_url), - "guild_id": self.guild_id, - "guild_name": self.guild.name, - "member_count": len(self.guild.members), - "uptime": (datetime.utcnow() - self.start_time).total_seconds(), - "latency": f"{self.ws.latency * 1000:.4f}", - "version": self.version, - "selfhosted": True, - "last_updated": str(datetime.utcnow()), - } - - async with self.session.post("https://api.modmail.tk/metadata", json=data): - logger.debug("Uploading metadata to Modmail server.") - - await asyncio.sleep(3600) + async def after_post_metadata(self): + logger.info("Metadata loop has been cancelled.") if __name__ == "__main__": - if os.name != "nt": - try: - import uvloop + try: + import uvloop + + uvloop.install() + except ImportError: + pass - uvloop.install() - except ImportError: - pass bot = ModmailBot() bot.run() diff --git a/cogs/utility.py b/cogs/utility.py index 315aff0c94..ee7b7c9493 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -227,7 +227,7 @@ def cog_unload(self): @commands.command() @checks.has_permissions(PermissionLevel.REGULAR) @trigger_typing - async def changelog(self, ctx, version: str.lower = ''): + 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 diff --git a/core/thread.py b/core/thread.py index bd693b02e3..04e506f7ae 100644 --- a/core/thread.py +++ b/core/thread.py @@ -319,7 +319,9 @@ async def _close( else: message = self.bot.config["thread_close_response"] - message = message.format(closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None) + message = message.format( + closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None + ) embed.description = message footer = self.bot.config["thread_close_footer"] @@ -764,9 +766,12 @@ async def find( if recipient is None and channel is not None: thread = self._find_from_channel(channel) if thread is None: - id, thread = next(((k, v) for k, v in self.cache.items() if v.channel == channel), (-1, None)) + id, thread = next( + ((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.') + logger.debug("Found thread with tempered ID.") await channel.edit(topic=f"User ID: {id}") return thread From 0bc17a88af49c8ab99e729484385b15c04367d90 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 21 Jul 2019 12:02:19 -0700 Subject: [PATCH 0329/1221] Misc --- CHANGELOG.md | 4 ++ bot.py | 126 +++++++++++++++++++++++------------- cogs/modmail.py | 38 ++++++++--- cogs/plugins.py | 86 +++++++++++++++---------- cogs/utility.py | 2 +- core/changelog.py | 6 +- core/clients.py | 8 ++- core/config.py | 49 ++++++++------ core/models.py | 3 +- core/paginator.py | 8 ++- core/thread.py | 159 +++++++++++++++++++++++----------------------- core/time.py | 6 +- 12 files changed, 297 insertions(+), 198 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e520bb1f0..bf4775ff87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `?snippet` is now the default command name instead of `?snippets` (`?snippets` is still usable). This is to make this consistent with `?alias`/`?aliases`. - `colorama` is no longer a necessity, this is due to some unsupported OS. - Changelog command can now take a version argument to jump straight to specified version. +- `?plugin enabled` results are now sorted alphabetically. +- `?plugin registry` results are now sorted alphabetically, helps user find plugins more easily. +- `?plugin registry page-number` plugin registry can specify a page number for quick access. ### Fixes @@ -44,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed helper functions `info()` and `error()` for formatting logging, it's formatted automatically now. - Bumped discord.py version to 1.2.3. - Use discord tasks for metadata loop. +- More debug based logging. # v3.0.3 diff --git a/bot.py b/bot.py index 5043c0681d..b6d9d39ebe 100644 --- a/bot.py +++ b/bot.py @@ -19,6 +19,7 @@ from aiohttp import ClientSession from emoji import UNICODE_EMOJI from motor.motor_asyncio import AsyncIOMotorClient +from pymongo.errors import ConfigurationError try: from colorama import init @@ -65,6 +66,8 @@ def __init__(self): super().__init__(command_prefix=None) # implemented in `get_prefix` self._session = None self._api = None + self.metadata_loop = None + self._connected = asyncio.Event() self.start_time = datetime.utcnow() @@ -77,13 +80,17 @@ def __init__(self): mongo_uri = self.config["mongo_uri"] if mongo_uri is None: - raise ValueError("A Mongo URI is necessary for the bot to function.") - - self.db = AsyncIOMotorClient(mongo_uri).modmail_bot - self.plugin_db = PluginDatabaseClient(self) + logger.critical("A Mongo URI is necessary for the bot to function.") + raise RuntimeError - self.metadata_loop = None + try: + self.db = AsyncIOMotorClient(mongo_uri).modmail_bot + except ConfigurationError as e: + logger.critical("Your MONGO_URI is copied wrong, try re-copying from the source again.") + logger.critical(str(e)) + sys.exit(0) + self.plugin_db = PluginDatabaseClient(self) self._load_extensions() @property @@ -134,7 +141,8 @@ def _configure_logging(self): logger.info("Logging level: %s", level_text) else: logger.info("Invalid logging level set.") - logger.warning("Using default logging level: INFO.") + logger.warning("Using default logging level: %s.", level_text) + logger.debug("Successfully configured logging.") @property def version(self) -> str: @@ -198,11 +206,21 @@ def run(self, *args, **kwargs): self.loop.run_until_complete(self.session.close()) logger.error(" - Shutting down bot - ") + @property + def owner_ids(self): + owner_ids = self.config["owners"] + if owner_ids is not None: + 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, []) + for perm in permissions: + owner_ids.add(int(perm)) + return owner_ids + async def is_owner(self, user: discord.User) -> bool: - owners = self.config["owners"] - if owners is not None: - if user.id in set(map(int, str(owners).split(","))): - return True + if user.id in self.owner_ids: + return True return await super().is_owner(user) @property @@ -212,18 +230,18 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: channel = self.get_channel(int(channel_id)) if channel is not None: return channel + logger.debug('LOG_CHANNEL_ID was invalid, removed.') self.config.remove("log_channel_id") if self.main_category is not None: try: channel = self.main_category.channels[0] self.config["log_channel_id"] = channel.id - logger.debug("No log channel set, however, one was found. Setting...") + logger.warning("No log channel set, setting #%s to be the log channel.", channel.name) return channel except IndexError: pass - logger.info( - "No log channel set, set one with `%ssetup` or " - "`%sconfig set log_channel_id `.", + logger.warning( + "No log channel set, set one with `%ssetup` or `%sconfig set log_channel_id `.", self.prefix, self.prefix, ) @@ -250,7 +268,8 @@ def aliases(self) -> typing.Dict[str, str]: def token(self) -> str: token = self.config["token"] if token is None: - raise ValueError("TOKEN must be set, this is your bot token.") + logger.critical("TOKEN must be set, set this as bot token found on the Discord Dev Portal.") + sys.exit(0) return token @property @@ -260,7 +279,7 @@ def guild_id(self) -> typing.Optional[int]: try: return int(str(guild_id)) except ValueError: - raise ValueError("Invalid guild_id set.") + logger.critical("Invalid GUILD_ID set.") return None @property @@ -284,7 +303,7 @@ def modmail_guild(self) -> typing.Optional[discord.Guild]: if guild is not None: return guild self.config.remove("modmail_guild_id") - logger.error("Invalid modmail_guild_id set.") + logger.critical("Invalid MODMAIL_GUILD_ID set.") return self.guild @property @@ -302,10 +321,11 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]: if cat is not None: return cat self.config.remove("main_category_id") + logger.debug('MAIN_CATEGORY_ID was invalid, removed.') cat = discord.utils.get(self.modmail_guild.categories, name="Modmail") if cat is not None: self.config["main_category_id"] = cat.id - logger.debug("No main category set, however, one was found. Setting...") + logger.debug("No main category set explicitly, setting category \"Modmail\" as the main category.") return cat return None @@ -384,6 +404,7 @@ async def setup_indexes(self): ("key", "text"), ] ) + logger.debug('Successfully set up database indexes.') async def on_ready(self): """Bot startup, sets uptime.""" @@ -391,14 +412,18 @@ async def on_ready(self): # Wait until config cache is populated with stuff from db and on_connect ran await self.wait_for_connected() + if self.guild is None: + logger.debug('Logging out due to invalid GUILD_ID.') + return await self.logout() + logger.line() logger.info("Client ready.") logger.line() logger.info("Logged in as: %s", self.user) logger.info("User ID: %s", self.user.id) logger.info("Prefix: %s", self.prefix) - logger.info("Guild Name: %s", self.guild.name if self.guild else "Invalid") - logger.info("Guild ID: %s", self.guild.id if self.guild else "Invalid") + logger.info("Guild Name: %s", self.guild.name) + logger.info("Guild ID: %s", self.guild.id) logger.line() await self.threads.populate_cache() @@ -406,6 +431,7 @@ async def on_ready(self): # closures closures = self.config["closures"] logger.info("There are %d thread(s) pending to be closed.", len(closures)) + logger.line() for recipient_id, items in tuple(closures.items()): after = ( @@ -418,10 +444,13 @@ async def on_ready(self): if not thread: # If the channel is deleted + logger.debug('Failed to close thread for recipient %s.', recipient_id) self.config["closures"].pop(recipient_id) 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, @@ -431,8 +460,6 @@ async def on_ready(self): auto_close=items.get("auto_close", False), ) - logger.line() - self.metadata_loop = tasks.Loop( self.post_metadata, seconds=0, @@ -552,6 +579,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: reaction = blocked_emoji changed = False delta = human_timedelta(min_account_age) + logger.debug('Blocked due to account age, user %s.', message.author.name) if str(message.author.id) not in self.blocked_users: new_reason = ( @@ -565,7 +593,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: embed=discord.Embed( title="Message not sent!", description=f"Your must wait for {delta} " - f"before you can contact {self.user.mention}.", + f"before you can contact me.", color=discord.Color.red(), ) ) @@ -575,6 +603,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: reaction = blocked_emoji changed = False delta = human_timedelta(min_guild_age) + logger.debug('Blocked due to guild age, user %s.', message.author.name) if str(message.author.id) not in self.blocked_users: new_reason = ( @@ -588,22 +617,24 @@ async def _process_blocked(self, message: discord.Message) -> bool: embed=discord.Embed( title="Message not sent!", description=f"Your must wait for {delta} " - f"before you can contact {self.user.mention}.", + f"before you can contact me.", color=discord.Color.red(), ) ) elif str(message.author.id) in self.blocked_users: - reaction = blocked_emoji 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 end_time = re.search(r"%(.+?)%$", reason) if end_time is not None: + logger.debug('No longer blocked, user %s.', message.author.name) after = ( datetime.fromisoformat(end_time.group(1)) - now ).total_seconds() @@ -611,6 +642,8 @@ async def _process_blocked(self, message: discord.Message) -> bool: # No longer blocked reaction = sent_emoji self.blocked_users.pop(str(message.author.id)) + else: + logger.debug('User blocked, user %s.', message.author.name) else: reaction = sent_emoji @@ -619,7 +652,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: try: await message.add_reaction(reaction) except (discord.HTTPException, discord.InvalidArgument): - pass + logger.warning('Failed to add reaction %s.', reaction, exc_info=True) return str(message.author.id) in self.blocked_users async def process_modmail(self, message: discord.Message) -> None: @@ -805,20 +838,17 @@ async def on_raw_reaction_add(self, payload): if isinstance(channel, discord.DMChannel): if str(reaction) == str(close_emoji): # closing thread + try: + recipient_thread_close = strtobool(self.config["recipient_thread_close"]) + except ValueError: + recipient_thread_close = self.config.remove("recipient_thread_close") + if not recipient_thread_close: + return thread = await self.threads.find(recipient=user) ts = message.embeds[0].timestamp if message.embeds else None if thread and ts == thread.channel.created_at: # the reacted message is the corresponding thread creation embed - try: - recipient_thread_close = strtobool( - self.config["recipient_thread_close"] - ) - except ValueError: - recipient_thread_close = self.config.remove( - "recipient_thread_close" - ) - if recipient_thread_close: - await thread.close(closer=user) + await thread.close(closer=user) else: if not message.embeds: return @@ -845,6 +875,7 @@ async def on_guild_channel_delete(self, channel): if isinstance(channel, discord.CategoryChannel): if self.main_category.id == channel.id: + logger.debug('Main category was deleted.') self.config.remove("main_category_id") await self.config.update() return @@ -853,17 +884,19 @@ async def on_guild_channel_delete(self, channel): return if self.log_channel is None or self.log_channel.id == channel.id: + logger.info('Log channel deleted.') self.config.remove("log_channel_id") await self.config.update() return thread = await self.threads.find(channel=channel) - if not thread: - return - - await thread.close(closer=mod, silent=True, delete_channel=False) + if thread: + logger.debug('Manually closed channel %s.', channel.name) + await thread.close(closer=mod, silent=True, delete_channel=False) async def on_member_remove(self, member): + if member.guild != self.guild: + return thread = await self.threads.find(recipient=member) if thread: embed = discord.Embed( @@ -873,6 +906,8 @@ async def on_member_remove(self, member): await thread.channel.send(embed=embed) async def on_member_join(self, member): + if member.guild != self.guild: + return thread = await self.threads.find(recipient=member) if thread: embed = discord.Embed( @@ -980,11 +1015,14 @@ async def validate_database_connection(self): if "OperationFailure" in message: logger.critical( - "This is due to having invalid credentials in your MONGO_URI." + "This is due to having invalid credentials in your MONGO_URI. " + "Remember you need to substitute `` with your actual password." ) logger.critical( - "Recheck the username/password and make sure to url encode them. " - "https://www.urlencoder.io/" + "Be sure to URL encode your username and password (not the entire URL!!), " + "https://www.urlencoder.io/, if this issue persists, try changing your username and password " + "to only include alphanumeric characters, no symbols." + "" ) raise else: @@ -1024,7 +1062,7 @@ async def after_post_metadata(self): if __name__ == "__main__": try: import uvloop - + logger.debug('Setting up with uvloop.') uvloop.install() except ImportError: pass diff --git a/cogs/modmail.py b/cogs/modmail.py index dc4e92e87b..b8ebaf1adf 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,4 +1,5 @@ import asyncio +import logging from datetime import datetime from typing import Optional, Union from types import SimpleNamespace as param @@ -16,6 +17,8 @@ from core.time import UserFriendlyTime, human_timedelta from core.utils import format_preview, User +logger = logging.getLogger("Modmail") + class Modmail(commands.Cog): """Commands directly related to Modmail functionality.""" @@ -35,6 +38,7 @@ async def setup(self, ctx): """ if self.bot.main_category is not None: + logger.debug("Can't re-setup server, main_category is found.") return await ctx.send(f"{self.bot.modmail_guild} is already set up.") if self.bot.modmail_guild is None: @@ -58,9 +62,8 @@ async def setup(self, ctx): embed = discord.Embed( title="Friendly Reminder", description=f"You may use the `{self.bot.prefix}config set log_channel_id " - "` command to set up a custom log channel" - ", then you can delete the default " - f"{log_channel.mention} channel.", + "` command to set up a custom log channel, then you can delete this default " + f"{log_channel.mention} log channel.", color=self.bot.main_color, ) @@ -92,7 +95,8 @@ async def setup(self, ctx): and not self.bot.config["level_permissions"] ): await self.bot.update_perms(PermissionLevel.REGULAR, -1) - await self.bot.update_perms(PermissionLevel.OWNER, ctx.author.id) + for owner_ids in self.bot.owner_ids: + await self.bot.update_perms(PermissionLevel.OWNER, owner_ids) @commands.group(aliases=["snippets"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -504,7 +508,11 @@ async def unsubscribe( async def nsfw(self, ctx): """Flags a Modmail thread as NSFW (not safe for work).""" await ctx.channel.edit(nsfw=True) - await ctx.message.add_reaction("✅") + sent_emoji, _ = await self.bot.retrieve_emoji() + try: + await ctx.message.add_reaction(sent_emoji) + except (discord.HTTPException, discord.InvalidArgument): + pass @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -512,7 +520,11 @@ async def nsfw(self, ctx): async def sfw(self, ctx): """Flags a Modmail thread as SFW (safe for work).""" await ctx.channel.edit(nsfw=False) - await ctx.message.add_reaction("✅") + sent_emoji, _ = await self.bot.retrieve_emoji() + try: + await ctx.message.add_reaction(sent_emoji) + except (discord.HTTPException, discord.InvalidArgument): + pass @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -662,7 +674,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): if not embeds: embed = discord.Embed( color=discord.Color.red(), - 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) @@ -764,7 +776,11 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): self.bot.api.edit_message(linked_message_id, message), ) - await ctx.message.add_reaction("✅") + sent_emoji, _ = await self.bot.retrieve_emoji() + try: + await ctx.message.add_reaction(sent_emoji) + except (discord.HTTPException, discord.InvalidArgument): + pass @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1093,7 +1109,11 @@ async def delete(self, ctx, message_id: Optional[int] = None): ) await thread.delete_message(linked_message_id) - await ctx.message.add_reaction("✅") + sent_emoji, _ = await self.bot.retrieve_emoji() + try: + await ctx.message.add_reaction(sent_emoji) + except (discord.HTTPException, discord.InvalidArgument): + pass def setup(bot): diff --git a/cogs/plugins.py b/cogs/plugins.py index 7f2dcec871..5f58c697f7 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -3,12 +3,12 @@ import json import logging import os -import random import shutil import site import stat import subprocess import sys +import typing from difflib import get_close_matches import discord @@ -57,7 +57,7 @@ def parse_plugin(name): # default branch = master try: # when names are formatted with inline code - result = name.strip("`").split("/") + result = name.split("/") result[2] = "/".join(result[2:]) if "@" in result[2]: # branch is specified @@ -83,13 +83,13 @@ async def download_initial_plugins(self): await self.download_plugin_repo(username, repo, branch) except DownloadError as exc: msg = f"{username}/{repo}@{branch} - {exc}" - logger.error(msg) + logger.error(msg, exc_info=True) else: try: await self.load_plugin(username, repo, name, branch) except DownloadError as exc: msg = f"{username}/{repo}@{branch}[{name}] - {exc}" - logger.error(msg) + logger.error(msg, exc_info=True) async def download_plugin_repo(self, username, repo, branch): try: @@ -136,7 +136,7 @@ async def load_plugin(self, username, repo, plugin_name, branch): # so there's no terminal output unless there's an error except subprocess.CalledProcessError as exc: err = exc.stderr.decode("utf8").strip() - + logger.error('Error.', exc_info=True) if err: msg = f"Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]" logger.error(msg) @@ -154,7 +154,7 @@ async def load_plugin(self, username, repo, plugin_name, branch): self.bot.load_extension(ext) except commands.ExtensionError as exc: msg = f"Plugin Load Failure: {username}/{repo}@{branch}[{plugin_name}]" - logger.error(msg) + logger.error(msg, exc_info=True) raise DownloadError("Invalid plugin") from exc else: msg = f"Loaded Plugin: {username}/{repo}@{branch}[{plugin_name}]" @@ -174,9 +174,8 @@ async def plugin_add(self, ctx, *, plugin_name: str): if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = ( - details["repository"] + "/" + plugin_name + "@" + details["branch"] - ) + plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"] + required_version = details["bot_version"] if parse_version(self.bot.version) < parse_version(required_version): @@ -212,7 +211,9 @@ async def plugin_add(self, ctx, *, plugin_name: str): try: await self.download_plugin_repo(username, repo, branch) - except DownloadError as exc: + except Exception as exc: + if not isinstance(exc, DownloadError): + logger.error('Unknown error when adding a plugin:', exc_info=True) embed = discord.Embed( description=f"Unable to fetch this plugin from Github: `{exc}`.", color=self.bot.main_color, @@ -224,6 +225,8 @@ async def plugin_add(self, ctx, *, plugin_name: str): try: await self.load_plugin(username, repo, name, branch) except Exception as exc: + if not isinstance(exc, DownloadError): + logger.error('Unknown error when adding a plugin:', exc_info=True) embed = discord.Embed( description=f"Unable to load this plugin: `{exc}`.", color=self.bot.main_color, @@ -238,13 +241,15 @@ async def plugin_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description="The plugin is installed.\n" - "*Please note: Any plugin that you install is at your **own risk***", + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*", color=self.bot.main_color, ) await ctx.send(embed=embed) else: embed = discord.Embed( - description="Invalid plugin name format: use username/repo/plugin.", + description="Invalid plugin name format: use plugin-name or " + "username/repo/plugin or username/repo/plugin@branch.", color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -261,19 +266,17 @@ async def plugin_remove(self, ctx, *, plugin_name: str): ) if plugin_name in self.bot.config["plugins"]: + username, repo, name, branch = self.parse_plugin(plugin_name) try: - username, repo, name, branch = self.parse_plugin(plugin_name) - self.bot.unload_extension( f"plugins.{username}-{repo}-{branch}.{name}.{name}" ) - except Exception: - pass + except commands.ExtensionNotLoaded: + logger.error('Plugin was never loaded.') self.bot.config["plugins"].remove(plugin_name) try: - # BUG: Local variables 'username', 'branch' and 'repo' might be referenced before assignment if not any( i.startswith(f"{username}/{repo}") for i in self.bot.config["plugins"] @@ -289,10 +292,9 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 f"plugins/{username}-{repo}-{branch}", onerror=onerror ) except Exception as exc: - logger.error(str(exc)) + logger.error('Failed to remove plugin %s.', plugin_name, exc_info=True) self.bot.config["plugins"].append(plugin_name) - logger.error(exc) - raise exc + return await self.bot.config.update() @@ -314,9 +316,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = ( - details["repository"] + "/" + plugin_name + "@" + details["branch"] - ) + plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"] if plugin_name not in self.bot.config["plugins"]: embed = discord.Embed( @@ -328,7 +328,8 @@ async def plugin_update(self, ctx, *, plugin_name: str): username, repo, name, branch = self.parse_plugin(plugin_name) try: - cmd = f"cd plugins/{username}-{repo}-{branch} && git reset --hard origin/{branch} && git fetch --all && git pull" + cmd = f"cd plugins/{username}-{repo}-{branch} && " \ + f"git reset --hard origin/{branch} && git fetch --all && git pull" cmd = await self.bot.loop.run_in_executor( None, self._asubprocess_run, cmd ) @@ -339,6 +340,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): description=f"An error occurred while updating: {err}.", color=self.bot.main_color, ) + logger.error('An error occurred while updating plugin:', exc_info=True) await ctx.send(embed=embed) else: @@ -357,11 +359,12 @@ async def plugin_update(self, ctx, *, plugin_name: str): try: await self.load_plugin(username, repo, name, branch) except DownloadError as exc: - em = discord.Embed( + embed = discord.Embed( description=f"Unable to start the plugin: `{exc}`.", color=self.bot.main_color, ) - await ctx.send(embed=em) + logger.error('An error occurred while updating plugin:', exc_info=True) + await ctx.send(embed=embed) @plugin.command(name="enabled", aliases=["installed"]) @checks.has_permissions(PermissionLevel.OWNER) @@ -369,7 +372,7 @@ async def plugin_enabled(self, ctx): """Shows a list of currently enabled plugins.""" if self.bot.config["plugins"]: - msg = "```\n" + "\n".join(self.bot.config["plugins"]) + "\n```" + msg = "```\n" + "\n".join(sorted(self.bot.config["plugins"])) + "\n```" embed = discord.Embed(description=msg, color=self.bot.main_color) await ctx.send(embed=embed) else: @@ -382,19 +385,32 @@ async def plugin_enabled(self, ctx): invoke_without_command=True, name="registry", aliases=["list", "info"] ) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_registry(self, ctx, *, plugin_name: str = None): - """Shows a list of all approved plugins.""" + async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = None): + """ + Shows a list of all approved plugins. + + Usage: + `{prefix}plugin registry` Details about all plugins. + `{prefix}plugin registry plugin-name` Details about the indicated plugin. + `{prefix}plugin registry page-number` Jump to a page in the registry. + """ await self.populate_registry() embeds = [] - registry = list(self.registry.items()) - random.shuffle(registry) + registry = sorted(self.registry.items(), key=lambda elem: elem[0]) - index = next((i for i, (n, _) in enumerate(registry) if plugin_name == n), None) + if isinstance(plugin_name, int): + index = plugin_name - 1 + if index < 0: + index = 0 + if index >= len(registry): + index = len(registry) - 1 + else: + index = next((i for i, (n, _) in enumerate(registry) if plugin_name == n), 0) - if index is None: + if not index and plugin_name is not None: embed = discord.Embed( color=discord.Color.red(), description=f'Could not find a plugin with name "{plugin_name}" within the registry.', @@ -440,13 +456,13 @@ async def plugin_registry(self, ctx, *, plugin_name: str = None): await paginator.run() @plugin_registry.command(name="compact") + @checks.has_permissions(PermissionLevel.OWNER) async def plugin_registry_compact(self, ctx): """Shows a compact view of all plugins within the registry.""" await self.populate_registry() - registry = list(self.registry.items()) - registry.sort(key=lambda elem: elem[0]) + registry = sorted(self.registry.items(), key=lambda elem: elem[0]) pages = [""] diff --git a/cogs/utility.py b/cogs/utility.py index ee7b7c9493..9f07d68e57 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -555,7 +555,7 @@ async def set_presence( if activity_identifier is None: if activity_message is not None: raise ValueError( - "activity_message must be None " "if activity_identifier is None." + "activity_message must be None if activity_identifier is None." ) activity_identifier = self.bot.config["activity_type"] activity_by_key = False diff --git a/core/changelog.py b/core/changelog.py index e3f9424546..ec12a89838 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -1,9 +1,12 @@ +import logging import re from collections import defaultdict from typing import List, Union from discord import Embed, Color +logger = logging.getLogger("Modmail") + class Version: """ @@ -117,11 +120,12 @@ class Changelog: "https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md" ) CHANGELOG_URL = "https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md" - VERSION_REGEX = re.compile(r"# (v\d+\.\d+\.\d+)([\S\s]*?(?=# v|$))") + VERSION_REGEX = re.compile(r"# ([vV]\d+\.\d+(?:\.\d+)?)(.*?(?=# (?:[vV]\d+\.\d+(?:\.\d+)?)|$))", flags=re.DOTALL) def __init__(self, bot, text: str): self.bot = bot self.text = text + logger.debug('Fetching changelog from GitHub.') self.versions = [Version(bot, *m) for m in self.VERSION_REGEX.findall(text)] @property diff --git a/core/clients.py b/core/clients.py index 88ffbd8743..4b8b578cc4 100644 --- a/core/clients.py +++ b/core/clients.py @@ -87,15 +87,18 @@ def logs(self): async def get_user_logs(self, user_id: Union[str, int]) -> list: query = {"recipient.id": str(user_id), "guild_id": str(self.bot.guild_id)} - projection = {"messages": {"$slice": 5}} + logger.debug('Retrieving user %s logs.', user_id) + return await self.logs.find(query, projection).to_list(None) async def get_log(self, channel_id: Union[str, int]) -> dict: + logger.debug('Retrieving channel %s logs.', channel_id) return await self.logs.find_one({"channel_id": str(channel_id)}) async def get_log_link(self, channel_id: Union[str, int]) -> str: doc = await self.get_log(channel_id) + logger.debug('Retrieving log link for channel %s.', channel_id) return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{doc['key']}" async def create_log_entry( @@ -131,12 +134,13 @@ async def create_log_entry( "messages": [], } ) - + logger.debug('Created a log entry, key %s.', key) return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{key}" async def get_config(self) -> dict: conf = await self.db.config.find_one({"bot_id": self.bot.user.id}) if conf is None: + logger.debug('Creating a new config entry for bot %s.', self.bot.user.id) await self.db.config.insert_one({"bot_id": self.bot.user.id}) return {"bot_id": self.bot.user.id} return conf diff --git a/core/config.py b/core/config.py index 5f7eda1ea1..c54165dc93 100644 --- a/core/config.py +++ b/core/config.py @@ -115,16 +115,13 @@ class ConfigManager: def __init__(self, bot): self.bot = bot + self.api = self.bot.api self._cache = {} self.ready_event = asyncio.Event() def __repr__(self): return repr(self._cache) - @property - def api(self): - return self.bot.api - def populate_cache(self) -> dict: data = deepcopy(self.defaults) @@ -132,17 +129,21 @@ def populate_cache(self) -> dict: data.update( {k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys} ) - - if os.path.exists("config.json"): - with open("config.json") as f: + configjson = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") + if os.path.exists(configjson): + logger.debug('Loading envs from config.json.') + with open(configjson, 'r') as f: # Config json should override env vars - data.update( - { - k.lower(): v - for k, v in json.load(f).items() - if k.lower() in self.all_keys - } - ) + try: + data.update( + { + k.lower(): v + for k, v in json.load(f).items() + if k.lower() in self.all_keys + } + ) + except json.JSONDecodeError: + logger.critical('Failed to load config.json env values.', exc_info=True) self._cache = data return self._cache @@ -212,7 +213,7 @@ async def refresh(self) -> dict: self._cache[k] = v if not self.ready_event.is_set(): self.ready_event.set() - logger.info("Config ready.") + logger.info("Successfully fetched configurations from database.") return self._cache async def wait_until_ready(self) -> None: @@ -238,12 +239,10 @@ def get(self, key: str, default: typing.Any = Default) -> typing.Any: if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') if key not in self._cache: - self._cache[key] = deepcopy(self.defaults[key]) if default is Default: - return self._cache[key] - return default - if self._cache[key] == self.defaults[key] and default is not Default: - return default + self._cache[key] = deepcopy(self.defaults[key]) + if default is not Default and self._cache[key] == self.defaults[key]: + self._cache[key] = default return self._cache[key] def set(self, key: str, item: typing.Any) -> None: @@ -279,4 +278,12 @@ def filter_default( cls, data: typing.Dict[str, typing.Any] ) -> typing.Dict[str, typing.Any]: # TODO: use .get to prevent errors - return {k.lower(): v for k, v in data.items() if v != cls.defaults[k.lower()]} + filtered = {} + for k, v in data.items(): + default = cls.defaults.get(k.lower(), Default) + if default is Default: + logger.error('Unexpected configuration detected: %s.', k) + continue + if v != default: + filtered[k.lower()] = v + return filtered diff --git a/core/models.py b/core/models.py index 34c79da1c5..58ef344115 100644 --- a/core/models.py +++ b/core/models.py @@ -46,7 +46,8 @@ def _error_(*msgs): return f'{Fore.RED}{" ".join(msgs)}{Style.RESET_ALL}' def debug(self, msg, *args, **kwargs): - return super().debug(self._debug_(msg), *args, **kwargs) + if self.isEnabledFor(logging.DEBUG): + self._log(logging.DEBUG, self._debug_(msg), args, **kwargs) def info(self, msg, *args, **kwargs): if self.isEnabledFor(logging.INFO): diff --git a/core/paginator.py b/core/paginator.py index 58fdcaea20..2fb72da8ff 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -194,8 +194,6 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: """ self.running = False - self.ctx.bot.loop.create_task(self.ctx.message.add_reaction("✅")) - if delete: return await self.base.delete() @@ -204,6 +202,12 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: except HTTPException: pass + sent_emoji, _ = await self.ctx.bot.retrieve_emoji() + try: + await self.ctx.message.add_reaction(sent_emoji) + except (HTTPException, InvalidArgument): + pass + async def first_page(self) -> None: """ Go to the first page. diff --git a/core/thread.py b/core/thread.py index 04e506f7ae..73248d915f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -72,6 +72,7 @@ def ready(self) -> bool: def ready(self, flag: bool): if flag: self._ready_event.set() + self.bot.dispatch("thread_ready", self) else: self._ready_event.clear() @@ -102,6 +103,7 @@ async def setup(self, *, creator=None, category=None): reason="Creating a thread channel.", ) except discord.HTTPException as e: # Failed to create due to 50 channel limit. + logger.critical('An error occurred while creating a thread.', exc_info=True) self.manager.cache.pop(self.id) embed = discord.Embed(color=discord.Color.red()) @@ -122,7 +124,8 @@ async def setup(self, *, creator=None, category=None): ) log_count = sum(1 for log in log_data if not log["open"]) - except Exception: # Something went wrong with database? + except Exception: + logger.error('An error occurred while posting logs to the database.', exc_info=True) log_url = log_count = None # ensure core functionality still works @@ -132,18 +135,17 @@ async def setup(self, *, creator=None, category=None): mention = self.bot.config["mention"] async def send_genesis_message(): - info_embed = self.manager.format_info_embed( + info_embed = self._format_info_embed( recipient, log_url, log_count, discord.Color.green() ) try: msg = await channel.send(mention, embed=info_embed) self.bot.loop.create_task(msg.pin()) self.genesis_message = msg - except Exception: - pass + except Exception as e: + logger.error(str(e)) finally: self.ready = True - self.bot.dispatch("thread_ready", self) await channel.edit(topic=f"User ID: {recipient.id}") self.bot.loop.create_task(send_genesis_message()) @@ -180,6 +182,75 @@ async def send_genesis_message(): close_emoji = await self.bot.convert_emoji(close_emoji) await msg.add_reaction(close_emoji) + def _format_info_embed(self, user, log_url, log_count, color): + """Get information about a member of a server + supports users from the guild or not.""" + member = self.bot.guild.get_member(user.id) + time = datetime.utcnow() + + # key = log_url.split('/')[-1] + + role_names = "" + if member is None: + sep_server = self.bot.using_multiple_server_setup + separator = ", " if sep_server else " " + + roles = [] + + for role in sorted(member.roles, key=lambda r: r.position): + if role.name == "@everyone": + continue + + fmt = role.name if sep_server else role.mention + roles.append(fmt) + + if len(separator.join(roles)) > 1024: + roles.append("...") + while len(separator.join(roles)) > 1024: + roles.pop(-2) + break + + role_names = separator.join(roles) + + created = str((time - user.created_at).days) + embed = discord.Embed(color=color, description=f"{user.mention} was created {days(created)}", timestamp=time) + + # if not role_names: + # embed.add_field(name='Mention', value=user.mention) + # embed.add_field(name='Registered', value=created + days(created)) + + footer = "User ID: " + str(user.id) + embed.set_author(name=str(user), icon_url=user.avatar_url, url=log_url) + # embed.set_thumbnail(url=avi) + + if member is None: + joined = str((time - member.joined_at).days) + # embed.add_field(name='Joined', value=joined + days(joined)) + embed.description += f", joined {days(joined)}" + + if member.nick: + embed.add_field(name="Nickname", value=member.nick, inline=True) + if role_names: + embed.add_field(name="Roles", value=role_names, inline=True) + embed.set_footer(text=footer) + else: + 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}') + thread = "thread" if log_count == 1 else "threads" + embed.description += f" with **{log_count or 'no'}** past {thread}." + else: + embed.description += "." + + mutual_guilds = [g for g in self.bot.guilds if user in g.members] + if member is None or len(mutual_guilds) > 1: + embed.add_field( + name="Mutual Server(s)", value=", ".join(g.name for g in mutual_guilds) + ) + + return embed + def _close_after(self, closer, silent, delete_channel, message): return self.bot.loop.create_task( self._close(closer, silent, delete_channel, message, True) @@ -203,7 +274,6 @@ async def close( if after > 0: # TODO: Add somewhere to clean up broken closures # (when channel is already deleted) - await self.bot.config.update() now = datetime.utcnow() items = { # 'initiation_time': now.isoformat(), @@ -261,7 +331,7 @@ async def _close( }, ) - if log_data is not None and isinstance(log_data, dict): + if isinstance(log_data, dict): prefix = self.bot.config["log_url_prefix"] if prefix == "NONE": prefix = "" @@ -475,7 +545,7 @@ async def note(self, message: discord.Message) -> None: async def reply(self, message: discord.Message, anonymous: bool = False) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(param(name="msg")) - if all(not g.get_member(self.id) for g in self.bot.guilds): + if not any(g.get_member(self.id) for g in self.bot.guilds): return await message.channel.send( embed=discord.Embed( color=discord.Color.red(), @@ -853,77 +923,8 @@ def format_channel_name(self, author): ) new_name += f"-{author.discriminator}" + counter = 1 while new_name in [c.name for c in self.bot.modmail_guild.text_channels]: - new_name += "-x" # two channels with same name + new_name += f'-{counter}' # two channels with same name return new_name - - def format_info_embed(self, user, log_url, log_count, color): - """Get information about a member of a server - supports users from the guild or not.""" - member = self.bot.guild.get_member(user.id) - time = datetime.utcnow() - - # key = log_url.split('/')[-1] - - role_names = "" - if member: - sep_server = self.bot.using_multiple_server_setup - separator = ", " if sep_server else " " - - roles = [] - - for role in sorted(member.roles, key=lambda r: r.position): - if role.name == "@everyone": - continue - - fmt = role.name if sep_server else role.mention - roles.append(fmt) - - if len(separator.join(roles)) > 1024: - roles.append("...") - while len(separator.join(roles)) > 1024: - roles.pop(-2) - break - - role_names = separator.join(roles) - - embed = discord.Embed(color=color, description=user.mention, timestamp=time) - - created = str((time - user.created_at).days) - # if not role_names: - # embed.add_field(name='Mention', value=user.mention) - # embed.add_field(name='Registered', value=created + days(created)) - embed.description += f" was created {days(created)}" - - footer = "User ID: " + str(user.id) - embed.set_footer(text=footer) - embed.set_author(name=str(user), icon_url=user.avatar_url, url=log_url) - # embed.set_thumbnail(url=avi) - - if member: - joined = str((time - member.joined_at).days) - # embed.add_field(name='Joined', value=joined + days(joined)) - embed.description += f", joined {days(joined)}" - - if member.nick: - embed.add_field(name="Nickname", value=member.nick, inline=True) - if role_names: - embed.add_field(name="Roles", value=role_names, inline=True) - else: - embed.set_footer(text=f"{footer} • (not in main server)") - - if 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}** past {thread}." - else: - embed.description += "." - - mutual_guilds = [g for g in self.bot.guilds if user in g.members] - if user not in self.bot.guild.members or len(mutual_guilds) > 1: - embed.add_field( - name="Mutual Servers", value=", ".join(g.name for g in mutual_guilds) - ) - - return embed diff --git a/core/time.py b/core/time.py index da9bdafcb4..5e8049567a 100644 --- a/core/time.py +++ b/core/time.py @@ -32,7 +32,7 @@ class ShortTime: def __init__(self, argument): match = self.compiled.fullmatch(argument) if match is None or not match.group(0): - raise BadArgument("invalid time provided") + raise BadArgument("Invalid time provided.") data = {k: int(v) for k, v in match.groupdict(default="0").items()} now = datetime.utcnow() @@ -52,7 +52,7 @@ def __init__(self, argument): now = datetime.utcnow() dt, status = self.calendar.parseDT(argument, sourceTime=now) if not status.hasDateOrTime: - raise BadArgument('invalid time provided, try e.g. "tomorrow" or "3 days"') + raise BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".') if not status.hasTime: # replace it with the current time @@ -83,7 +83,7 @@ def __init__(self, argument): super().__init__(argument) if self._past: - raise BadArgument("this time is in the past") + raise BadArgument("The time is in the past.") class UserFriendlyTime(Converter): From 8f7d020021c7217a0f11deb11936bd2986d68fa0 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 21 Jul 2019 12:15:41 -0700 Subject: [PATCH 0330/1221] Black/lint --- bot.py | 64 ++++++++++++++++++++++++++++++----------------- cogs/plugins.py | 40 +++++++++++++++++++---------- cogs/utility.py | 2 ++ core/changelog.py | 7 ++++-- core/clients.py | 10 ++++---- core/config.py | 14 +++++++---- core/thread.py | 18 ++++++++----- 7 files changed, 101 insertions(+), 54 deletions(-) diff --git a/bot.py b/bot.py index b6d9d39ebe..132ae4c913 100644 --- a/bot.py +++ b/bot.py @@ -86,7 +86,9 @@ def __init__(self): try: self.db = AsyncIOMotorClient(mongo_uri).modmail_bot except ConfigurationError as e: - logger.critical("Your MONGO_URI is copied wrong, try re-copying from the source again.") + logger.critical( + "Your MONGO_URI is copied wrong, try re-copying from the source again." + ) logger.critical(str(e)) sys.exit(0) @@ -213,7 +215,9 @@ 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 @@ -230,13 +234,16 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: channel = self.get_channel(int(channel_id)) if channel is not None: return channel - logger.debug('LOG_CHANNEL_ID was invalid, removed.') + logger.debug("LOG_CHANNEL_ID was invalid, removed.") self.config.remove("log_channel_id") if self.main_category is not None: try: 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) + logger.warning( + "No log channel set, setting #%s to be the log channel.", + channel.name, + ) return channel except IndexError: pass @@ -268,7 +275,9 @@ def aliases(self) -> typing.Dict[str, str]: def token(self) -> str: token = self.config["token"] if token is None: - logger.critical("TOKEN must be set, set this as bot token found on the Discord Dev Portal.") + logger.critical( + "TOKEN must be set, set this as bot token found on the Discord Dev Portal." + ) sys.exit(0) return token @@ -321,11 +330,13 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]: if cat is not None: return cat self.config.remove("main_category_id") - logger.debug('MAIN_CATEGORY_ID was invalid, removed.') + logger.debug("MAIN_CATEGORY_ID was invalid, removed.") cat = discord.utils.get(self.modmail_guild.categories, name="Modmail") if cat is not None: self.config["main_category_id"] = cat.id - logger.debug("No main category set explicitly, setting category \"Modmail\" as the main category.") + logger.debug( + 'No main category set explicitly, setting category "Modmail" as the main category.' + ) return cat return None @@ -404,7 +415,7 @@ async def setup_indexes(self): ("key", "text"), ] ) - logger.debug('Successfully set up database indexes.') + logger.debug("Successfully set up database indexes.") async def on_ready(self): """Bot startup, sets uptime.""" @@ -413,7 +424,7 @@ async def on_ready(self): await self.wait_for_connected() if self.guild is None: - logger.debug('Logging out due to invalid GUILD_ID.') + logger.debug("Logging out due to invalid GUILD_ID.") return await self.logout() logger.line() @@ -444,12 +455,12 @@ async def on_ready(self): if not thread: # If the channel is deleted - logger.debug('Failed to close thread for recipient %s.', recipient_id) + logger.debug("Failed to close thread for recipient %s.", recipient_id) self.config["closures"].pop(recipient_id) await self.config.update() continue - logger.debug('Closing thread for recipient %s.', recipient_id) + logger.debug("Closing thread for recipient %s.", recipient_id) await thread.close( closer=self.get_user(items["closer_id"]), @@ -579,7 +590,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: 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.", message.author.name) if str(message.author.id) not in self.blocked_users: new_reason = ( @@ -603,7 +614,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: 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.", message.author.name) if str(message.author.id) not in self.blocked_users: new_reason = ( @@ -628,13 +639,15 @@ async def _process_blocked(self, message: discord.Message) -> bool: ): # 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 end_time = re.search(r"%(.+?)%$", reason) if end_time is not None: - logger.debug('No longer blocked, user %s.', message.author.name) + logger.debug("No longer blocked, user %s.", message.author.name) after = ( datetime.fromisoformat(end_time.group(1)) - now ).total_seconds() @@ -643,7 +656,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: reaction = sent_emoji self.blocked_users.pop(str(message.author.id)) else: - logger.debug('User blocked, user %s.', message.author.name) + logger.debug("User blocked, user %s.", message.author.name) else: reaction = sent_emoji @@ -652,7 +665,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: try: await message.add_reaction(reaction) except (discord.HTTPException, discord.InvalidArgument): - logger.warning('Failed to add reaction %s.', reaction, exc_info=True) + logger.warning("Failed to add reaction %s.", reaction, exc_info=True) return str(message.author.id) in self.blocked_users async def process_modmail(self, message: discord.Message) -> None: @@ -839,9 +852,13 @@ async def on_raw_reaction_add(self, payload): if isinstance(channel, discord.DMChannel): if str(reaction) == str(close_emoji): # closing thread try: - recipient_thread_close = strtobool(self.config["recipient_thread_close"]) + recipient_thread_close = strtobool( + self.config["recipient_thread_close"] + ) except ValueError: - recipient_thread_close = self.config.remove("recipient_thread_close") + recipient_thread_close = self.config.remove( + "recipient_thread_close" + ) if not recipient_thread_close: return thread = await self.threads.find(recipient=user) @@ -875,7 +892,7 @@ async def on_guild_channel_delete(self, channel): if isinstance(channel, discord.CategoryChannel): if self.main_category.id == channel.id: - logger.debug('Main category was deleted.') + logger.debug("Main category was deleted.") self.config.remove("main_category_id") await self.config.update() return @@ -884,14 +901,14 @@ async def on_guild_channel_delete(self, channel): return if self.log_channel is None or self.log_channel.id == channel.id: - logger.info('Log channel deleted.') + 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: - logger.debug('Manually closed channel %s.', channel.name) + logger.debug("Manually closed channel %s.", channel.name) await thread.close(closer=mod, silent=True, delete_channel=False) async def on_member_remove(self, member): @@ -1062,7 +1079,8 @@ async def after_post_metadata(self): if __name__ == "__main__": try: import uvloop - logger.debug('Setting up with uvloop.') + + logger.debug("Setting up with uvloop.") uvloop.install() except ImportError: pass diff --git a/cogs/plugins.py b/cogs/plugins.py index 5f58c697f7..4f204ef10e 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -136,7 +136,7 @@ async def load_plugin(self, username, repo, plugin_name, branch): # so there's no terminal output unless there's an error except subprocess.CalledProcessError as exc: err = exc.stderr.decode("utf8").strip() - logger.error('Error.', exc_info=True) + logger.error("Error.", exc_info=True) if err: msg = f"Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]" logger.error(msg) @@ -174,7 +174,9 @@ async def plugin_add(self, ctx, *, plugin_name: str): if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"] + plugin_name = ( + details["repository"] + "/" + plugin_name + "@" + details["branch"] + ) required_version = details["bot_version"] @@ -213,7 +215,9 @@ async def plugin_add(self, ctx, *, plugin_name: str): await self.download_plugin_repo(username, repo, branch) except Exception as exc: if not isinstance(exc, DownloadError): - logger.error('Unknown error when adding a plugin:', exc_info=True) + logger.error( + "Unknown error when adding a plugin:", exc_info=True + ) embed = discord.Embed( description=f"Unable to fetch this plugin from Github: `{exc}`.", color=self.bot.main_color, @@ -226,7 +230,9 @@ async def plugin_add(self, ctx, *, plugin_name: str): await self.load_plugin(username, repo, name, branch) except Exception as exc: if not isinstance(exc, DownloadError): - logger.error('Unknown error when adding a plugin:', exc_info=True) + logger.error( + "Unknown error when adding a plugin:", exc_info=True + ) embed = discord.Embed( description=f"Unable to load this plugin: `{exc}`.", color=self.bot.main_color, @@ -249,7 +255,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): else: embed = discord.Embed( description="Invalid plugin name format: use plugin-name or " - "username/repo/plugin or username/repo/plugin@branch.", + "username/repo/plugin or username/repo/plugin@branch.", color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -272,7 +278,7 @@ async def plugin_remove(self, ctx, *, plugin_name: str): f"plugins.{username}-{repo}-{branch}.{name}.{name}" ) except commands.ExtensionNotLoaded: - logger.error('Plugin was never loaded.') + logger.error("Plugin was never loaded.") self.bot.config["plugins"].remove(plugin_name) @@ -291,8 +297,8 @@ def onerror(func, path, exc_info): # pylint: disable=W0613 shutil.rmtree( f"plugins/{username}-{repo}-{branch}", onerror=onerror ) - except Exception as exc: - logger.error('Failed to remove plugin %s.', plugin_name, exc_info=True) + except Exception: + logger.error("Failed to remove plugin %s.", plugin_name, exc_info=True) self.bot.config["plugins"].append(plugin_name) return @@ -316,7 +322,9 @@ async def plugin_update(self, ctx, *, plugin_name: str): if plugin_name in self.registry: details = self.registry[plugin_name] - plugin_name = details["repository"] + "/" + plugin_name + "@" + details["branch"] + plugin_name = ( + details["repository"] + "/" + plugin_name + "@" + details["branch"] + ) if plugin_name not in self.bot.config["plugins"]: embed = discord.Embed( @@ -328,8 +336,10 @@ async def plugin_update(self, ctx, *, plugin_name: str): username, repo, name, branch = self.parse_plugin(plugin_name) try: - cmd = f"cd plugins/{username}-{repo}-{branch} && " \ + cmd = ( + f"cd plugins/{username}-{repo}-{branch} && " f"git reset --hard origin/{branch} && git fetch --all && git pull" + ) cmd = await self.bot.loop.run_in_executor( None, self._asubprocess_run, cmd ) @@ -340,7 +350,7 @@ async def plugin_update(self, ctx, *, plugin_name: str): description=f"An error occurred while updating: {err}.", color=self.bot.main_color, ) - logger.error('An error occurred while updating plugin:', exc_info=True) + logger.error("An error occurred while updating plugin:", exc_info=True) await ctx.send(embed=embed) else: @@ -363,7 +373,9 @@ async def plugin_update(self, ctx, *, plugin_name: str): description=f"Unable to start the plugin: `{exc}`.", color=self.bot.main_color, ) - logger.error('An error occurred while updating plugin:', exc_info=True) + logger.error( + "An error occurred while updating plugin:", exc_info=True + ) await ctx.send(embed=embed) @plugin.command(name="enabled", aliases=["installed"]) @@ -408,7 +420,9 @@ async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = No 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( diff --git a/cogs/utility.py b/cogs/utility.py index 9f07d68e57..36057ee294 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -246,6 +246,8 @@ async def changelog(self, ctx, version: str.lower = ""): paginator = PaginatorSession(ctx, *changelog.embeds) paginator.current = index await paginator.run() + except asyncio.CancelledError: + pass except Exception: await ctx.send(changelog.CHANGELOG_URL) diff --git a/core/changelog.py b/core/changelog.py index ec12a89838..386baf176f 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -120,12 +120,15 @@ class Changelog: "https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md" ) CHANGELOG_URL = "https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md" - VERSION_REGEX = re.compile(r"# ([vV]\d+\.\d+(?:\.\d+)?)(.*?(?=# (?:[vV]\d+\.\d+(?:\.\d+)?)|$))", flags=re.DOTALL) + VERSION_REGEX = re.compile( + r"# ([vV]\d+\.\d+(?:\.\d+)?)(.*?(?=# (?:[vV]\d+\.\d+(?:\.\d+)?)|$))", + flags=re.DOTALL, + ) def __init__(self, bot, text: str): self.bot = bot self.text = text - logger.debug('Fetching changelog from GitHub.') + logger.debug("Fetching changelog from GitHub.") self.versions = [Version(bot, *m) for m in self.VERSION_REGEX.findall(text)] @property diff --git a/core/clients.py b/core/clients.py index 4b8b578cc4..a4ed847622 100644 --- a/core/clients.py +++ b/core/clients.py @@ -88,17 +88,17 @@ def logs(self): async def get_user_logs(self, user_id: Union[str, int]) -> list: query = {"recipient.id": str(user_id), "guild_id": str(self.bot.guild_id)} projection = {"messages": {"$slice": 5}} - logger.debug('Retrieving user %s logs.', user_id) + logger.debug("Retrieving user %s logs.", user_id) return await self.logs.find(query, projection).to_list(None) async def get_log(self, channel_id: Union[str, int]) -> dict: - logger.debug('Retrieving channel %s logs.', channel_id) + logger.debug("Retrieving channel %s logs.", channel_id) return await self.logs.find_one({"channel_id": str(channel_id)}) async def get_log_link(self, channel_id: Union[str, int]) -> str: doc = await self.get_log(channel_id) - logger.debug('Retrieving log link for channel %s.', channel_id) + logger.debug("Retrieving log link for channel %s.", channel_id) return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{doc['key']}" async def create_log_entry( @@ -134,13 +134,13 @@ async def create_log_entry( "messages": [], } ) - logger.debug('Created a log entry, key %s.', key) + logger.debug("Created a log entry, key %s.", key) return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{key}" async def get_config(self) -> dict: conf = await self.db.config.find_one({"bot_id": self.bot.user.id}) if conf is None: - logger.debug('Creating a new config entry for bot %s.', self.bot.user.id) + logger.debug("Creating a new config entry for bot %s.", self.bot.user.id) await self.db.config.insert_one({"bot_id": self.bot.user.id}) return {"bot_id": self.bot.user.id} return conf diff --git a/core/config.py b/core/config.py index c54165dc93..301ab69f61 100644 --- a/core/config.py +++ b/core/config.py @@ -129,10 +129,12 @@ def populate_cache(self) -> dict: data.update( {k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys} ) - configjson = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") + configjson = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "config.json" + ) if os.path.exists(configjson): - logger.debug('Loading envs from config.json.') - with open(configjson, 'r') as f: + logger.debug("Loading envs from config.json.") + with open(configjson, "r") as f: # Config json should override env vars try: data.update( @@ -143,7 +145,9 @@ 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 return self._cache @@ -282,7 +286,7 @@ def filter_default( for k, v in data.items(): default = cls.defaults.get(k.lower(), Default) if default is Default: - logger.error('Unexpected configuration detected: %s.', k) + logger.error("Unexpected configuration detected: %s.", k) continue if v != default: filtered[k.lower()] = v diff --git a/core/thread.py b/core/thread.py index 73248d915f..d4213a0c93 100644 --- a/core/thread.py +++ b/core/thread.py @@ -103,7 +103,7 @@ async def setup(self, *, creator=None, category=None): reason="Creating a thread channel.", ) except discord.HTTPException as e: # Failed to create due to 50 channel limit. - logger.critical('An error occurred while creating a thread.', exc_info=True) + logger.critical("An error occurred while creating a thread.", exc_info=True) self.manager.cache.pop(self.id) embed = discord.Embed(color=discord.Color.red()) @@ -125,7 +125,9 @@ 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 @@ -213,7 +215,11 @@ def _format_info_embed(self, user, log_url, log_count, color): role_names = separator.join(roles) created = str((time - user.created_at).days) - embed = discord.Embed(color=color, description=f"{user.mention} was created {days(created)}", timestamp=time) + embed = discord.Embed( + color=color, + description=f"{user.mention} was created {days(created)}", + timestamp=time, + ) # if not role_names: # embed.add_field(name='Mention', value=user.mention) @@ -836,13 +842,13 @@ async def find( if recipient is None and channel is not None: thread = self._find_from_channel(channel) if thread is None: - id, thread = next( + user_id, thread = next( ((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.") - await channel.edit(topic=f"User ID: {id}") + await channel.edit(topic=f"User ID: {user_id}") return thread thread = None @@ -925,6 +931,6 @@ def format_channel_name(self, author): 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 + new_name += f"-{counter}" # two channels with same name return new_name From 263f6c95e2735c351af001383f3ffe7774169f5f Mon Sep 17 00:00:00 2001 From: xPolar <50601857+xPolar@users.noreply.github.com> Date: Sun, 21 Jul 2019 20:40:00 -0700 Subject: [PATCH 0331/1221] Moderation plugin PR (#328) * Moderation plugin PR * fixed moderation plugin syntax error * Update registry.json --- plugins/registry.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 8822612d5d..cf46dd72be 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -178,5 +178,14 @@ "title": "Get Stats", "icon_url": "https://image.flaticon.com/icons/png/512/117/117761.png", "thumbnail_url": "http://www.pngmart.com/files/7/Statistics-PNG-Clipart.png" - } + }, + "moderation":{ + "repository": "xPolar/modmail-plugins/Moderation/", + "branch": "master", + "description": "Moderate your server with Modmail, bring the Mod to Modmail!", + "bot_version": "3.0.3", + "title": "Moderate your server", + "icon_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png", + "thumbnail_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png" + } } From fce6de818305d689196f7d22f463540072bb224e Mon Sep 17 00:00:00 2001 From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com> Date: Mon, 22 Jul 2019 16:22:20 +0600 Subject: [PATCH 0332/1221] Fix moderation plugin in registry (#329) --- plugins/registry.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index cf46dd72be..fc1dd11a2e 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -179,8 +179,8 @@ "icon_url": "https://image.flaticon.com/icons/png/512/117/117761.png", "thumbnail_url": "http://www.pngmart.com/files/7/Statistics-PNG-Clipart.png" }, - "moderation":{ - "repository": "xPolar/modmail-plugins/Moderation/", + "moderation": { + "repository": "xPolar/modmail-plugins", "branch": "master", "description": "Moderate your server with Modmail, bring the Mod to Modmail!", "bot_version": "3.0.3", From 9e15f683ab9c417b36a542f6f9e8a2f735de2140 Mon Sep 17 00:00:00 2001 From: kyb3r Date: Mon, 22 Jul 2019 23:44:20 +1000 Subject: [PATCH 0333/1221] Fix bug in error handler in thread setup --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 87627dbaf7..669edb9723 100644 --- a/core/thread.py +++ b/core/thread.py @@ -108,7 +108,7 @@ async def setup(self, *, creator=None, category=None): em = discord.Embed(color=discord.Color.red()) em.title = "Error while trying to create a thread" - em.description = e.message + em.description = str(e) em.add_field(name="Recipient", value=recipient.mention) if log_channel is not None: From b3e8f62e993f718b98ee32367022f8324c108a1c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 26 Jul 2019 23:59:51 -0700 Subject: [PATCH 0334/1221] Snippets/aliases --- CHANGELOG.md | 4 ++ bot.py | 3 +- cogs/modmail.py | 115 ++++++++++++++++++++++++++++-------------------- cogs/utility.py | 107 ++++++++++++++++++++++++-------------------- core/utils.py | 18 ++++++-- 5 files changed, 149 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4775ff87..3872d519a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `?plugin enabled` results are now sorted alphabetically. - `?plugin registry` results are now sorted alphabetically, helps user find plugins more easily. - `?plugin registry page-number` plugin registry can specify a page number for quick access. +- A reworked interface for `?snippet` and `?alias`. + - Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown). ### Fixes @@ -37,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `mod_typing` and `user_typing` will no longer show when user is blocked. - Better `?block` usage message. - Resolves errors when message was sent by mods after thread is closed somehow. +- Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`. +- When creating snippets and aliases, it now checks if another snippets/aliases with the same name exists. ### Internal diff --git a/bot.py b/bot.py index 132ae4c913..7c9e18528c 100644 --- a/bot.py +++ b/bot.py @@ -87,7 +87,8 @@ def __init__(self): self.db = AsyncIOMotorClient(mongo_uri).modmail_bot except ConfigurationError as e: logger.critical( - "Your MONGO_URI is copied wrong, try re-copying from the source again." + "Your MONGO_URI might be copied wrong, try re-copying from the source again. " + "Otherwise noted in the following message:" ) logger.critical(str(e)) sys.exit(0) diff --git a/cogs/modmail.py b/cogs/modmail.py index 1f396cdc65..d8c8304fbb 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,11 +1,13 @@ import asyncio import logging from datetime import datetime +from itertools import zip_longest, takewhile from typing import Optional, Union from types import SimpleNamespace as param import discord from discord.ext import commands +from discord.utils import escape_markdown, escape_mentions from dateutil import parser from natural.date import duration @@ -15,7 +17,7 @@ from core.models import PermissionLevel from core.paginator import PaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User +from core.utils import format_preview, User, create_not_found_embed logger = logging.getLogger("Modmail") @@ -100,34 +102,36 @@ async def setup(self, ctx): @commands.group(aliases=["snippets"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) - async def snippet(self, ctx): + async def snippet(self, ctx, *, name: str.lower = None): """ Create pre-defined messages for use in threads. When `{prefix}snippet` is used by itself, this will retrieve - a list of snippets that are currently set. + a list of snippets that are currently set. `{prefix}snippet-name` will show what the + snippet point to. - To use snippets: - - First create a snippet using: + To create a snippet: - `{prefix}snippet add snippet-name A pre-defined text.` - Afterwards, you can use your snippet in a thread channel + You can use your snippet in a thread channel with `{prefix}snippet-name`, the message "A pre-defined text." will be sent to the recipient. + Currently, there is not a default anonymous snippet command; however, a workaround + is available using `{prefix}alias`. Here is how: + - `{prefix}alias add snippet-name anonreply A pre-defined anonymous text.` + See also `{prefix}alias`. """ - embeds = [] + 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') + return await ctx.send(embed=embed) + return await ctx.send(escape_mentions(val)) - if self.bot.snippets: - embed = discord.Embed( - color=self.bot.main_color, - description="Here is a list of snippets " - "that are currently configured.", - ) - else: + if not self.bot.snippets: embed = discord.Embed( color=discord.Color.red(), description="You dont have any snippets at the moment.", @@ -135,25 +139,37 @@ async def snippet(self, ctx): 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) - embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) - embeds.append(embed) + embeds = [] - for name, value in self.bot.snippets.items(): - if len(embed.fields) == 5: - embed = discord.Embed( - color=self.bot.main_color, description=embed.description + for names in zip_longest(*(iter(sorted(self.bot.snippets)),) * 15): + description = "\n".join( + ": ".join((str(a), b)) + for a, b in enumerate( + takewhile(lambda x: x is not None, names), start=1 ) - embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) - embeds.append(embed) - embed.add_field(name=name, value=value, inline=False) + ) + embed = discord.Embed(color=self.bot.main_color, description=description) + embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) + embeds.append(embed) session = PaginatorSession(ctx, *embeds) await session.run() + @snippet.command(name="raw") + @checks.has_permissions(PermissionLevel.SUPPORTER) + 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('<', '\\<')) + @snippet.command(name="add") @checks.has_permissions(PermissionLevel.SUPPORTER) - async def snippet_add(self, ctx, name: str.lower, *, value): + async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_content): """ Add a snippet. @@ -167,19 +183,35 @@ async def snippet_add(self, ctx, name: str.lower, *, value): color=discord.Color.red(), description=f"Snippet `{name}` already exists.", ) - else: - self.bot.snippets[name] = value - await self.bot.config.update() + return await ctx.send(embed=embed) + if name in self.bot.aliases: embed = discord.Embed( - title="Added snippet", - color=self.bot.main_color, - description=f'`{name}` will now send "{value}".', + title="Error", + color=discord.Color.red(), + description=f"An alias with the same name already exists: `{name}`.", + ) + return await ctx.send(embed=embed) + + if len(name) > 120: + embed = discord.Embed( + title="Error", + color=discord.Color.red(), + description=f"Snippet names cannot be longer than 120 characters.", ) + return await ctx.send(embed=embed) - await ctx.send(embed=embed) + self.bot.snippets[name] = value + await self.bot.config.update() + + embed = discord.Embed( + title="Added snippet", + color=self.bot.main_color, + description=f'Successfully created snippet.', + ) + return await ctx.send(embed=embed) - @snippet.command(name="remove", aliases=["del", "delete", "rm"]) + @snippet.command(name="remove", aliases=["del", "delete"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippet_remove(self, ctx, *, name: str.lower): """Remove a snippet.""" @@ -188,18 +220,12 @@ async def snippet_remove(self, ctx, *, name: str.lower): embed = discord.Embed( title="Removed snippet", color=self.bot.main_color, - description=f"`{name}` no longer exists.", + description=f"Snippet `{name}` is now deleted.", ) self.bot.snippets.pop(name) await self.bot.config.update() - else: - embed = discord.Embed( - title="Error", - color=discord.Color.red(), - description=f"Snippet `{name}` does not exist.", - ) - + embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet') await ctx.send(embed=embed) @snippet.command(name="edit") @@ -221,13 +247,8 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): color=self.bot.main_color, description=f'`{name}` will now send "{value}".', ) - else: - embed = discord.Embed( - title="Error", - color=discord.Color.red(), - description=f"Snippet `{name}` does not exist.", - ) + embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet') await ctx.send(embed=embed) @commands.command() diff --git a/cogs/utility.py b/cogs/utility.py index c1fb34369f..182f836864 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -8,6 +8,7 @@ from datetime import datetime from difflib import get_close_matches from io import StringIO, BytesIO +from itertools import zip_longest, takewhile from json import JSONDecodeError, loads from textwrap import indent from types import SimpleNamespace as param @@ -16,6 +17,7 @@ from discord import Embed, Color, Activity, Role from discord.enums import ActivityType, Status from discord.ext import commands +from discord.utils import escape_mentions from aiohttp import ClientResponseError from pkg_resources import parse_version @@ -25,7 +27,7 @@ from core.decorators import trigger_typing from core.models import InvalidConfigError, PermissionLevel from core.paginator import PaginatorSession, MessagePaginatorSession -from core.utils import cleanup_code, User, get_perm_level +from core.utils import cleanup_code, User, get_perm_level, create_not_found_embed logger = logging.getLogger("Modmail") @@ -811,12 +813,13 @@ async def config_get(self, ctx, key: str.lower = None): @commands.group(aliases=["aliases"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) - async def alias(self, ctx): + async def alias(self, ctx, *, name: str.lower = None): """ Create shortcuts to bot commands. When `{prefix}alias` is used by itself, this will retrieve - a list of alias that are currently set. + a list of alias that are currently set. `{prefix}alias-name` will show what the + alias point to. To use alias: @@ -830,44 +833,73 @@ async def alias(self, ctx): See also `{prefix}snippet`. """ - embeds = [] - desc = "Here is a list of aliases that are currently configured." + if name is not None: + val = self.bot.aliases.get(name) + if val is None: + embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias') + return await ctx.send(embed=embed) + return await ctx.send(escape_mentions(val)) - if self.bot.aliases: - embed = Embed(color=self.bot.main_color, description=desc) - else: + if not self.bot.aliases: embed = Embed( - color=self.bot.main_color, + color=Color.red(), description="You dont have any aliases at the moment.", ) - embed.set_author(name="Command aliases:", icon_url=ctx.guild.icon_url) - embed.set_footer(text=f"Do {self.bot.prefix}help alias for more commands.") - embeds.append(embed) + 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) - for name, value in self.bot.aliases.items(): - if len(embed.fields) == 5: - embed = Embed(color=self.bot.main_color, description=desc) - embed.set_author(name="Command aliases", icon_url=ctx.guild.icon_url) - embed.set_footer( - text=f"Do {self.bot.prefix}help alias for more commands." - ) + embeds = [] - embeds.append(embed) - embed.add_field(name=name, value=value, inline=False) + for names in zip_longest(*(iter(sorted(self.bot.aliases)),) * 15): + description = "\n".join( + ": ".join((str(a), b)) + for a, b in enumerate( + takewhile(lambda x: x is not None, names), start=1 + ) + ) + embed = Embed(color=self.bot.main_color, description=description) + embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) + embeds.append(embed) session = PaginatorSession(ctx, *embeds) - return await session.run() + await session.run() @alias.command(name="add") @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_add(self, ctx, name: str.lower, *, value): """Add an alias.""" - if self.bot.get_command(name) or name in self.bot.aliases: + if self.bot.get_command(name): embed = Embed( title="Error", color=Color.red(), - description="A command or alias already exists " - f"with the same name: `{name}`.", + description=f"A command with the same name already exists: `{name}`.", + ) + return await ctx.send(embed=embed) + + if name in self.bot.aliases: + embed = Embed( + title="Error", + color=Color.red(), + description=f"Another alias with the same name already exists: `{name}`.", + ) + return await ctx.send(embed=embed) + + if name in self.bot.snippets: + embed = Embed( + title="Error", + color=Color.red(), + description=f"A snippet with the same name already exists: `{name}`.", + ) + return await ctx.send(embed=embed) + + if len(name) > 120: + embed = Embed( + title="Error", + color=Color.red(), + description=f"Alias names cannot be longer than 120 characters.", ) return await ctx.send(embed=embed) @@ -892,7 +924,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) - @alias.command(name="remove", aliases=["del", "delete", "rm"]) + @alias.command(name="remove", aliases=["del", "delete"]) @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_remove(self, ctx, *, name: str.lower): """Remove an alias.""" @@ -906,13 +938,8 @@ async def alias_remove(self, ctx, *, name: str.lower): color=self.bot.main_color, description=f"`{name}` no longer exists.", ) - else: - embed = Embed( - title="Error", - color=Color.red(), - description=f"Alias `{name}` does not exist.", - ) + embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias') return await ctx.send(embed=embed) @@ -923,21 +950,7 @@ async def alias_edit(self, ctx, name: str.lower, *, value): Edit an alias. """ if name not in self.bot.aliases: - embed = Embed( - title="Error", - color=Color.red(), - description=f"Alias `{name}` does not exist.", - ) - - return await ctx.send(embed=embed) - - if self.bot.get_command(name): - embed = Embed( - title="Error", - color=Color.red(), - description="A command or alias already exists " - f"with the same name: `{name}`.", - ) + embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias') return await ctx.send(embed=embed) linked_command = value.split()[0] diff --git a/core/utils.py b/core/utils.py index 8f5e6c2589..76c1864a27 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,9 +1,10 @@ import re import typing +from difflib import get_close_matches from distutils.util import strtobool as _stb # pylint: disable=E0401 from urllib import parse -from discord import Object +import discord from discord.ext import commands from core.models import PermissionLevel @@ -34,7 +35,7 @@ async def convert(self, ctx, argument): match = self._get_id_match(argument) if match is None: raise commands.BadArgument('User "{}" not found'.format(argument)) - return Object(int(match.group(1))) + return discord.Object(int(match.group(1))) def truncate(text: str, max: int = 50) -> str: # pylint: disable=W0622 @@ -186,7 +187,7 @@ def match_user_id(text: str) -> int: int The user ID if found. Otherwise, -1. """ - match = re.search(r"\bUser ID: (\d+)\b", text) + match = re.search(r"\bUser ID: (\d{17,21})\b", text) if match is not None: return int(match.group(1)) return -1 @@ -208,3 +209,14 @@ async def ignore(coro): await coro except Exception: pass + + +def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discord.Embed: + embed = discord.Embed( + color=discord.Color.red(), + description=f"**{name.capitalize()} `{word}` cannot be found.**", + ) + val = get_close_matches(word, possibilities, n=n, cutoff=cutoff) + if val: + embed.description += '\nHowever, perhaps you meant...\n' + '\n'.join(val) + return embed From 1564e1f7e4c9e3d2cdd9a5766aa69027cb0295f4 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 27 Jul 2019 15:05:15 -0700 Subject: [PATCH 0335/1221] Multistep alias --- .gitignore | 4 ++ CHANGELOG.md | 1 + bot.py | 131 +++++++++++++++++++++++++------------ cogs/modmail.py | 14 ++-- cogs/utility.py | 170 ++++++++++++++++++++++++++++++++++++++---------- core/utils.py | 35 +++++++++- 6 files changed, 273 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 0c953c5e8e..8e80af7c26 100644 --- a/.gitignore +++ b/.gitignore @@ -124,6 +124,10 @@ dmypy.json # VS Code .vscode/ +# Node +package-lock.json +node_modules/ + # Modmail config.json plugins/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3872d519a6..707fbd1207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New configuration variable `thread_self_closable_creation_footer` — the footer when `recipient_thread_close` is enabled. - Added a minimalistic version of requirements.txt (named requirements.min.txt) that contains only the absolute minimum of Modmail. - For users having trouble with pipenv or any other reason. +- Multi-step alias, see `?help alias add`. Public beta testing, might be unstable. ### Changes diff --git a/bot.py b/bot.py index 7c9e18528c..f92630faec 100644 --- a/bot.py +++ b/bot.py @@ -6,8 +6,8 @@ import re import sys import typing - from datetime import datetime +from itertools import zip_longest from types import SimpleNamespace import discord @@ -30,7 +30,7 @@ from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import human_join, strtobool +from core.utils import human_join, strtobool, parse_alias from core.models import PermissionLevel, ModmailLogger from core.thread import ThreadManager from core.time import human_timedelta @@ -669,24 +669,76 @@ async def _process_blocked(self, message: discord.Message) -> bool: logger.warning("Failed to add reaction %s.", reaction, exc_info=True) return str(message.author.id) in self.blocked_users - async def process_modmail(self, message: discord.Message) -> None: + async def process_dm_modmail(self, message: discord.Message) -> None: """Processes messages sent to the bot.""" - await self.wait_for_connected() - blocked = await self._process_blocked(message) if not blocked: thread = await self.threads.find_or_create(message.author) await thread.send(message) + async def get_contexts(self, message, *, cls=commands.Context): + """ + Returns all invocation contexts from the message. + Supports getting the prefix from database as well as command aliases. + """ + + view = StringView(message.content) + ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) + ctx.thread = await self.threads.find(channel=ctx.channel) + + if self._skip_check(message.author.id, self.user.id): + return [ctx] + + prefixes = await self.get_prefix() + + invoked_prefix = discord.utils.find(view.skip_string, prefixes) + if invoked_prefix is None: + return [ctx] + + invoker = view.get_word().lower() + + # Check if there is any aliases being called. + alias = self.aliases.get(invoker) + if alias is not None: + aliases = parse_alias(alias) + 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 + + ctx.invoked_with = invoker + ctx.command = self.all_commands.get(invoker) + return [ctx] + async def get_context(self, message, *, cls=commands.Context): """ Returns the invocation context from the message. - Supports getting the prefix from database as well as command aliases. + Supports getting the prefix from database. """ - await self.wait_for_connected() view = StringView(message.content) - ctx = cls(prefix=None, view=view, bot=self, message=message) + ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) if self._skip_check(message.author.id, self.user.id): return ctx @@ -701,17 +753,7 @@ async def get_context(self, message, *, cls=commands.Context): invoker = view.get_word().lower() - # Check if there is any aliases being called. - alias = self.aliases.get(invoker) - if alias is not None: - ctx._alias_invoked = True # pylint: disable=W0212 - len_ = len(f"{invoked_prefix}{invoker}") - view = StringView(f"{alias}{ctx.message.content[len_:]}") - ctx.view = view - invoker = view.get_word() - ctx.invoked_with = invoker - ctx.prefix = self.prefix # Sane prefix (No mentions) ctx.command = self.all_commands.get(invoker) return ctx @@ -739,47 +781,52 @@ async def update_perms( async def on_message(self, message): await self.wait_for_connected() - if message.type == discord.MessageType.pins_add and message.author == self.user: await message.delete() + await self.process_commands(message) + async def process_commands(self, message): if message.author.bot: return if isinstance(message.channel, discord.DMChannel): - return await self.process_modmail(message) + return await self.process_dm_modmail(message) - prefix = self.prefix + if message.content.startswith(self.prefix): + cmd = message.content[len(self.prefix) :].strip() - if message.content.startswith(prefix): - cmd = message.content[len(prefix) :].strip() + # Process snippets if cmd in self.snippets: thread = await self.threads.find(channel=message.channel) snippet = self.snippets[cmd] if thread: snippet = snippet.format(recipient=thread.recipient) - message.content = f"{prefix}reply {snippet}" + message.content = f"{self.prefix}reply {snippet}" - ctx = await self.get_context(message) - if ctx.command: - return await self.invoke(ctx) + ctxs = await self.get_contexts(message) + for ctx in ctxs: + if ctx.command: + await self.invoke(ctx) + continue - thread = await self.threads.find(channel=ctx.channel) - if thread is not None: - try: - reply_without_command = strtobool(self.config["reply_without_command"]) - except ValueError: - reply_without_command = self.config.remove("reply_without_command") + thread = await self.threads.find(channel=ctx.channel) + if thread is not None: + try: + reply_without_command = strtobool( + self.config["reply_without_command"] + ) + except ValueError: + reply_without_command = self.config.remove("reply_without_command") - if reply_without_command: - await thread.reply(message) - else: - await self.api.append_log(message, type_="internal") - elif ctx.invoked_with: - exc = commands.CommandNotFound( - 'Command "{}" is not found'.format(ctx.invoked_with) - ) - self.dispatch("command_error", ctx, exc) + if reply_without_command: + await thread.reply(message) + else: + await self.api.append_log(message, type_="internal") + elif ctx.invoked_with: + exc = commands.CommandNotFound( + 'Command "{}" is not found'.format(ctx.invoked_with) + ) + self.dispatch("command_error", ctx, exc) async def on_typing(self, channel, user, _): await self.wait_for_connected() diff --git a/cogs/modmail.py b/cogs/modmail.py index d8c8304fbb..4eae4037e3 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -127,7 +127,9 @@ 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)) @@ -163,9 +165,9 @@ async def snippet(self, ctx, *, name: str.lower = None): 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') + 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('<', '\\<')) + return await ctx.send(escape_markdown(escape_mentions(val)).replace("<", "\\<")) @snippet.command(name="add") @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -207,7 +209,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=f"Successfully created snippet.", ) return await ctx.send(embed=embed) @@ -225,7 +227,7 @@ async def snippet_remove(self, ctx, *, name: str.lower): self.bot.snippets.pop(name) await self.bot.config.update() else: - embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet') + embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") await ctx.send(embed=embed) @snippet.command(name="edit") @@ -248,7 +250,7 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): description=f'`{name}` will now send "{value}".', ) else: - embed = create_not_found_embed(name, self.bot.snippets.keys(), 'Snippet') + embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") await ctx.send(embed=embed) @commands.command() diff --git a/cogs/utility.py b/cogs/utility.py index 182f836864..a5adde8345 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -17,7 +17,7 @@ from discord import Embed, Color, Activity, Role from discord.enums import ActivityType, Status from discord.ext import commands -from discord.utils import escape_mentions +from discord.utils import escape_markdown from aiohttp import ClientResponseError from pkg_resources import parse_version @@ -27,7 +27,13 @@ from core.decorators import trigger_typing from core.models import InvalidConfigError, PermissionLevel from core.paginator import PaginatorSession, MessagePaginatorSession -from core.utils import cleanup_code, User, get_perm_level, create_not_found_embed +from core.utils import ( + cleanup_code, + User, + get_perm_level, + create_not_found_embed, + parse_alias, +) logger = logging.getLogger("Modmail") @@ -836,18 +842,43 @@ 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 = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias') + embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias") return await ctx.send(embed=embed) - return await ctx.send(escape_mentions(val)) + + values = parse_alias(val) + + if not values: + embed = Embed( + title="Error", + color=Color.red(), + description=f"Alias `{name}` is invalid, it used to be `{escape_markdown(val)}`. " + f"This alias will now be deleted.", + ) + self.bot.aliases.pop(name) + await self.bot.config.update() + return await ctx.send(embed=embed) + + if len(values) == 1: + embed = Embed( + color=self.bot.main_color, + description=f"`{name}` points to `{escape_markdown(values[0])}`.", + ) + else: + embed = 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)}" + + return await ctx.send(embed=embed) if not self.bot.aliases: embed = Embed( color=Color.red(), 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) @@ -870,7 +901,19 @@ async def alias(self, ctx, *, name: str.lower = None): @alias.command(name="add") @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_add(self, ctx, name: str.lower, *, value): - """Add an alias.""" + """ + 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"` + """ if self.bot.get_command(name): embed = Embed( title="Error", @@ -903,24 +946,55 @@ async def alias_add(self, ctx, name: str.lower, *, value): ) return await ctx.send(embed=embed) - linked_command = value.split()[0] - if not self.bot.get_command(linked_command): + values = parse_alias(value) + + if not values: embed = Embed( title="Error", color=Color.red(), - description="The command you are attempting to point " - f"to does not exist: `{linked_command}`.", + 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) - self.bot.aliases[name] = value - await self.bot.config.update() + if len(values) == 1: + linked_command = values[0].split()[0] + if not self.bot.get_command(linked_command): + embed = Embed( + title="Error", + color=Color.red(), + description="The command you are attempting to point " + f"to does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) - embed = Embed( - title="Added alias", - color=self.bot.main_color, - description=f'`{name}` points to: "{value}".', - ) + embed = Embed( + title="Added alias", + color=self.bot.main_color, + description=f'`{name}` points to: "{values[0]}".', + ) + + else: + embed = Embed( + title="Added 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 = val.split()[0] + if not self.bot.get_command(linked_command): + embed = Embed( + title="Error", + color=Color.red(), + description="The command you are attempting to point " + f"to on step {i} does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) + embed.description += f"\n{i}: {val}" + + self.bot.aliases[name] = "&&".join(values) + await self.bot.config.update() return await ctx.send(embed=embed) @@ -936,10 +1010,10 @@ async def alias_remove(self, ctx, *, name: str.lower): embed = Embed( title="Removed alias", color=self.bot.main_color, - description=f"`{name}` no longer exists.", + description=f"Successfully deleted `{name}`.", ) else: - embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias') + embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias") return await ctx.send(embed=embed) @@ -950,27 +1024,57 @@ async def alias_edit(self, ctx, name: str.lower, *, value): Edit an alias. """ if name not in self.bot.aliases: - embed = create_not_found_embed(name, self.bot.aliases.keys(), 'Alias') + embed = create_not_found_embed(name, self.bot.aliases.keys(), "Alias") return await ctx.send(embed=embed) - linked_command = value.split()[0] - if not self.bot.get_command(linked_command): + values = parse_alias(value) + + if not values: embed = Embed( title="Error", color=Color.red(), - description="The command you are attempting to point " - f"to does not exist: `{linked_command}`.", + 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) - self.bot.aliases[name] = value - await self.bot.config.update() + if len(values) == 1: + linked_command = values[0].split()[0] + if not self.bot.get_command(linked_command): + embed = Embed( + title="Error", + color=Color.red(), + description="The command you are attempting to point " + f"to does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) + embed = Embed( + title="Edited alias", + color=self.bot.main_color, + description=f'`{name}` now points to: "{values[0]}".', + ) - embed = Embed( - title="Edited alias", - color=self.bot.main_color, - description=f'`{name}` now points to: "{value}".', - ) + else: + embed = 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 = val.split()[0] + if not self.bot.get_command(linked_command): + embed = Embed( + title="Error", + color=Color.red(), + description="The command you are attempting to point " + f"to on step {i} does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) + embed.description += f"\n{i}: {val}" + + self.bot.aliases[name] = "&&".join(values) + await self.bot.config.update() return await ctx.send(embed=embed) @commands.group(aliases=["perms"], invoke_without_command=True) diff --git a/core/utils.py b/core/utils.py index 76c1864a27..16fe1675b0 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,4 +1,5 @@ import re +import shlex import typing from difflib import get_close_matches from distutils.util import strtobool as _stb # pylint: disable=E0401 @@ -218,5 +219,37 @@ def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discor ) val = get_close_matches(word, possibilities, n=n, cutoff=cutoff) if val: - embed.description += '\nHowever, perhaps you meant...\n' + '\n'.join(val) + embed.description += "\nHowever, perhaps you meant...\n" + "\n".join(val) return embed + + +def parse_alias(alias): + if "&&" not in alias: + if alias.startswith('"') and alias.endswith('"'): + return [alias[1:-1]] + return [alias] + + 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 [] + + buffer = buffer.strip() + if buffer.startswith('"') and buffer.endswith('"'): + buffer = buffer[1:-1] + cmd += [buffer] + + if not all(cmd): + return [] + return cmd From 60b5eb13412885b8cbb20e1580a550383ba46f06 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 27 Jul 2019 15:25:59 -0700 Subject: [PATCH 0336/1221] Address semvar stuff --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 707fbd1207..c17564c901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Changelog All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); +however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). # v3.1.0 From d22093721f68519cf819c20c0305ffa05d21db40 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 27 Jul 2019 18:54:57 -0700 Subject: [PATCH 0337/1221] misc help and return user_typing --- CHANGELOG.md | 3 ++- bot.py | 7 ------- cogs/utility.py | 18 +++++++++++------- core/config.py | 5 ++--- core/utils.py | 6 +++--- 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c17564c901..66c4d7cf5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Added a minimalistic version of requirements.txt (named requirements.min.txt) that contains only the absolute minimum of Modmail. - For users having trouble with pipenv or any other reason. - Multi-step alias, see `?help alias add`. Public beta testing, might be unstable. +- Misc commands without cogs are now displayed in `?help`. ### Changes @@ -38,7 +39,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - `?notify` no longer carries over to the next thread. - `discord.NotFound` errors for `on_raw_reaction_add`. -- `mod_typing` and `user_typing` will no longer show when user is blocked. +- `mod_typing` ~~and `user_typing`~~ (`user_typing` is by-design to show now) will no longer show when user is blocked. - Better `?block` usage message. - Resolves errors when message was sent by mods after thread is closed somehow. - Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`. diff --git a/bot.py b/bot.py index f92630faec..6031fd3574 100644 --- a/bot.py +++ b/bot.py @@ -838,13 +838,6 @@ async def _void(*_args, **_kwargs): pass if isinstance(channel, discord.DMChannel): - if await self._process_blocked( - SimpleNamespace( - author=user, channel=SimpleNamespace(send=_void), add_reaction=_void - ) - ): - return - try: user_typing = strtobool(self.config["user_typing"]) except ValueError: diff --git a/cogs/utility.py b/cogs/utility.py index a5adde8345..51d5baa1a5 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -39,13 +39,13 @@ class ModmailHelpCommand(commands.HelpCommand): - async def format_cog_help(self, cog): + async def format_cog_help(self, cog, *, no_cog=False): bot = self.context.bot prefix = self.clean_prefix formats = [""] for cmd in await self.filter_commands( - cog.get_commands(), sort=True, key=get_perm_level + cog.get_commands() if not no_cog else cog, sort=True, key=get_perm_level ): perm_level = get_perm_level(cmd) if perm_level is PermissionLevel.INVALID: @@ -63,16 +63,19 @@ async def format_cog_help(self, cog): embeds = [] for format_ in formats: + description = cog.description or "No description." \ + if not no_cog else "Miscellaneous commands without a category." embed = Embed( - description=f'*{cog.description or "No description."}*', + description=f'*{description}*', color=bot.main_color, ) 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" embed.set_author( - name=cog.qualified_name + " - Help" + continued, + name=name + continued, icon_url=bot.user.avatar_url, ) @@ -88,9 +91,8 @@ def process_help_msg(self, help_: str): async def send_bot_help(self, mapping): embeds = [] - # TODO: Implement for no cog commands - - cogs = list(filter(None, mapping)) + no_cog_commands = sorted(mapping.pop(None), key=lambda c: c.qualified_name) + cogs = sorted(mapping, key=lambda c: c.qualified_name) bot = self.context.bot @@ -105,6 +107,8 @@ async def send_bot_help(self, mapping): for cog in default_cogs: embeds.extend(await self.format_cog_help(cog)) + if no_cog_commands: + embeds.extend(await self.format_cog_help(no_cog_commands, no_cog=True)) p_session = PaginatorSession( self.context, *embeds, destination=self.get_destination() diff --git a/core/config.py b/core/config.py index 301ab69f61..d99d762307 100644 --- a/core/config.py +++ b/core/config.py @@ -115,7 +115,6 @@ class ConfigManager: def __init__(self, bot): self.bot = bot - self.api = self.bot.api self._cache = {} self.ready_event = asyncio.Event() @@ -207,11 +206,11 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: async def update(self): """Updates the config with data from the cache""" - await self.api.update_config(self.filter_default(self._cache)) + await self.bot.api.update_config(self.filter_default(self._cache)) async def refresh(self) -> dict: """Refreshes internal cache with data from database""" - for k, v in (await self.api.get_config()).items(): + for k, v in (await self.bot.api.get_config()).items(): k = k.lower() if k in self.all_keys: self._cache[k] = v diff --git a/core/utils.py b/core/utils.py index 16fe1675b0..024c6e0f03 100644 --- a/core/utils.py +++ b/core/utils.py @@ -8,8 +8,6 @@ import discord from discord.ext import commands -from core.models import PermissionLevel - def strtobool(val): if isinstance(val, bool): @@ -194,7 +192,9 @@ def match_user_id(text: str) -> int: return -1 -def get_perm_level(cmd) -> PermissionLevel: +def get_perm_level(cmd): + from core.models import PermissionLevel + for check in cmd.checks: perm = getattr(check, "permission_level", None) if perm is not None: From 1f8d2d0a7a1cde45f0c25e0ef34148908e288876 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 27 Jul 2019 19:25:37 -0700 Subject: [PATCH 0338/1221] Snippet/alias help msg --- CHANGELOG.md | 3 ++- cogs/utility.py | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66c4d7cf5f..b58143ce87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - For users having trouble with pipenv or any other reason. - Multi-step alias, see `?help alias add`. Public beta testing, might be unstable. - Misc commands without cogs are now displayed in `?help`. +- `?help` works for alias and snippets. ### Changes @@ -39,7 +40,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - `?notify` no longer carries over to the next thread. - `discord.NotFound` errors for `on_raw_reaction_add`. -- `mod_typing` ~~and `user_typing`~~ (`user_typing` is by-design to show now) will no longer show when user is blocked. +- `mod_typing` ~~and `user_typing`~~ (`user_typing` is now by-design to show) will no longer show when user is blocked. - Better `?block` usage message. - Resolves errors when message was sent by mods after thread is closed somehow. - Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`. diff --git a/cogs/utility.py b/cogs/utility.py index 51d5baa1a5..a929fc02a2 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -17,7 +17,7 @@ from discord import Embed, Color, Activity, Role from discord.enums import ActivityType, Status from discord.ext import commands -from discord.utils import escape_markdown +from discord.utils import escape_markdown, escape_mentions from aiohttp import ClientResponseError from pkg_resources import parse_version @@ -180,12 +180,40 @@ async def send_group_help(self, group): await self.get_destination().send(embed=embed) - async def send_error_message(self, msg): # pylint: disable=W0221 - logger.warning("CommandNotFound: %s", str(msg)) + 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, ' + f'content:**\n\n{val}')) + + val = self.context.bot.aliases.get(command) + if val is not None: + values = parse_alias(val) + + if len(values) == 1: + embed = Embed( + title=f"{command} is an alias.", + color=self.context.bot.main_color, + description=f"`{command}` points to `{escape_markdown(values[0])}`.", + ) + else: + embed = Embed( + 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)}" + embed.set_footer(text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" for more ' + 'details on aliases.') + return await self.get_destination().send(embed=embed) + + logger.warning("CommandNotFound: %s", str(error)) embed = Embed(color=Color.red()) embed.set_footer( - text=f'Command/Category "{self.context.kwargs.get("command")}" not found.' + text=f'Command/Category "{command}" not found.' ) choices = set() @@ -193,7 +221,7 @@ async def send_error_message(self, msg): # pylint: disable=W0221 for name, cmd in self.context.bot.all_commands.items(): if not cmd.hidden: choices.add(name) - command = self.context.kwargs.get("command") + closest = get_close_matches(command, choices) if closest: embed.add_field( @@ -870,7 +898,7 @@ async def alias(self, ctx, *, name: str.lower = None): else: embed = Embed( color=self.bot.main_color, - description=f"`{name}` points to the following steps:", + description=f"**`{name}` points to the following steps:**", ) for i, val in enumerate(values, start=1): embed.description += f"\n{i}: {escape_markdown(val)}" From 310f65210a211c4b94a350d0b01c9ecd5b5ec88e Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Jul 2019 12:55:10 -0700 Subject: [PATCH 0339/1221] Config help --- CHANGELOG.md | 3 + cogs/utility.py | 53 +++++- core/config.py | 14 +- core/config_help.json | 374 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 438 insertions(+), 6 deletions(-) create mode 100644 core/config_help.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b58143ce87..c473256b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Multi-step alias, see `?help alias add`. Public beta testing, might be unstable. - Misc commands without cogs are now displayed in `?help`. - `?help` works for alias and snippets. +- `?config help ` shows a help embed for the configuration. ### Changes @@ -35,6 +36,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - `?plugin registry page-number` plugin registry can specify a page number for quick access. - A reworked interface for `?snippet` and `?alias`. - Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown). +- The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/. ### Fixes @@ -45,6 +47,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Resolves errors when message was sent by mods after thread is closed somehow. - Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`. - When creating snippets and aliases, it now checks if another snippets/aliases with the same name exists. +- Was looking for `config.json` in the wrong directory. ### Internal diff --git a/cogs/utility.py b/cogs/utility.py index a929fc02a2..4914663b2b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -733,11 +733,14 @@ async def config(self, ctx): Type `{prefix}config options` to view a list of valid configuration variables. + Type `{prefix}config help config-name` for info + on a config. + To set a configuration variable: - - `{prefix}config set varname value here` + - `{prefix}config set config-name value here` To remove a configuration variable: - - `{prefix}config remove varname` + - `{prefix}config remove config-name` """ await ctx.send_help(ctx.command) @@ -849,6 +852,52 @@ async def config_get(self, ctx, key: str.lower = None): return await ctx.send(embed=embed) + @config.command(name="help", aliases=["info"]) + @checks.has_permissions(PermissionLevel.OWNER) + async def config_help(self, ctx, key: str.lower): + """ + Show information on a specified configuration. + """ + if key not in self.bot.config.public_keys: + embed = Embed( + title="Error", + color=Color.red(), + description=f"`{key}` is an invalid key.", + ) + return await ctx.send(embed=embed) + + config_help = self.bot.config.config_help + info = config_help.get(key) + + if info is None: + embed = Embed( + title="Error", + color=Color.red(), + description=f"No help details found for `{key}`.", + ) + return await ctx.send(embed=embed) + + def fmt(val): + return val.format(prefix=self.bot.prefix, bot=self.bot) + + embed = Embed( + title=f"Configuration description on {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) + example_text = '' + for example in info['examples']: + example_text += f'- {fmt(example)}\n' + embed.add_field(name='Example(s):', value=example_text, inline=False) + + note_text = '' + for note in info['notes']: + note_text += f'- {fmt(note)}\n' + if note_text: + embed.add_field(name='Notes(s):', value=note_text, inline=False) + return await ctx.send(embed=embed) + @commands.group(aliases=["aliases"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) async def alias(self, ctx, *, name: str.lower = None): diff --git a/core/config.py b/core/config.py index d99d762307..33e493eb0b 100644 --- a/core/config.py +++ b/core/config.py @@ -24,7 +24,7 @@ class ConfigManager: public_keys = { # activity - "twitch_url": "https://www.twitch.tv/discord-modmail/", + "twitch_url": "https://www.twitch.tv/discordmodmail/", # bot settings "main_category_id": None, "prefix": "?", @@ -55,8 +55,8 @@ class ConfigManager: "thread_self_close_response": "You have closed this Modmail thread.", # moderation "recipient_color": str(discord.Color.gold()), - "mod_tag": None, "mod_color": str(discord.Color.green()), + "mod_tag": None, # anonymous message "anon_username": None, "anon_avatar_url": None, @@ -117,6 +117,7 @@ def __init__(self, bot): self.bot = bot self._cache = {} self.ready_event = asyncio.Event() + self.config_help = {} def __repr__(self): return repr(self._cache) @@ -129,7 +130,7 @@ def populate_cache(self) -> dict: {k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys} ) configjson = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "config.json" + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json" ) if os.path.exists(configjson): logger.debug("Loading envs from config.json.") @@ -147,8 +148,13 @@ def populate_cache(self) -> dict: logger.critical( "Failed to load config.json env values.", exc_info=True ) - self._cache = data + + confighelpjson = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "config_help.json" + ) + with open(confighelpjson, "r") as f: + self.config_help = json.load(f) return self._cache async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: diff --git a/core/config_help.json b/core/config_help.json new file mode 100644 index 0000000000..a5b385b431 --- /dev/null +++ b/core/config_help.json @@ -0,0 +1,374 @@ +{ + "twitch_url": { + "default": "`https://www.twitch.tv/discordmodmail/`", + "description": "This channel dictates the linked Twitch channel when the activity is set to \"Streaming\".", + "examples": [ + "`{prefix}config set twitch_url https://www.twitch.tv/yourchannelname/`" + ], + "notes": [ + "This has no effect when the activity is not set to \"Streaming\".", + "See also: `{prefix}help activity`." + ] + }, + "main_category_id": { + "default": "`Modmail` (created with `{prefix}setup`)", + "description": "This is the category where all new threads will be created.\n\nTo change the Modmail category, you will need to find the [category’s ID](https://support.discordapp.com/hc/en-us/articles/206346498).", + "examples": [ + "`{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." + ] + }, + "prefix": { + "default": "`?`", + "description": "The prefix of the bot.", + "examples": [ + "`{prefix}prefix !`", + "`{prefix}config set prefix !`" + ], + "notes": [ + "If you forgot the bot prefix, Modmail will always respond to its mention (ping)." + ] + }, + "mention": { + "default": "@here", + "description": "This is the message above user information for when a new thread is created in the channel.", + "examples": [ + "`{prefix}config set mention Yo~ Here's a new thread for ya!`", + "`{prefix}mention Yo~ Here's a new thread for ya!`" + ], + "notes": [ + "Unfortunately, its not currently possible to disable mention." + ] + }, + "main_color": { + "default": "Discord Blurple ([#7289DA](https://placehold.it/100/7289da?text=+))", + "description": "This is the main color for Modmail (help/about/ping embed messages, subscribe, move, etc.).", + "examples": [ + "`{prefix}config set main_color olive green`", + "`{prefix}config set main_color 12de3a`", + "`{prefix}config set main_color #12de3a`", + "`{prefix}config set main_color fff`" + ], + "notes": [ + "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", + "See also: `mod_color`, `recipient_color`." + ] + }, + "user_typing": { + "default": "No", + "description": "When this is set to `yes`, whenever the recipient user starts to type in their DM channel, the moderator will see “{bot.user.display_name} is typing…” in the thread channel.", + "examples": [ + "`{prefix}config set user_typing yes`", + "`{prefix}config set user_typing no`" + ], + "notes": [ + "See also: `mod_typing`." + ] + }, + "mod_typing": { + "default": "No", + "description": "When this is set to `yes`, whenever a moderator starts to type in the thread channel, the recipient user will see \"{bot.user.display_name} is typing…\" in their DM channel.", + "examples": [ + "`{prefix}config set mod_typing yes`", + "`{prefix}config set mod_typing no`" + ], + "notes": [ + "See also: `mod_typing`." + ] + }, + "account_age": { + "default": "No age threshold", + "description": "The creation date of the recipient user account must be greater than the number of days, hours, minutes or any time-interval specified by this configuration.", + "examples": [ + "`{prefix}config set account_age P12DT3H` (stands for 12 days and 3 hours in [ISO-8601 Duration Format](https://en.wikipedia.org/wiki/ISO_8601#Durations))", + "`{prefix}config set account_age 3 days and 5 hours` (accepted readable time)" + ], + "notes": [ + "To remove this restriction, do `{prefix}config del account_age`.", + "See also: `guild_age`." + ] + }, + "guild_age": { + "default": "No age threshold", + "description": "The join date of the recipient user into this server must be greater than the number of days, hours, minutes or any time-interval specified by this configuration.", + "examples": [ + "`{prefix}config set guild_age P12DT3H` (stands for 12 days and 3 hours in [ISO-8601 Duration Format](https://en.wikipedia.org/wiki/ISO_8601#Durations))", + "`{prefix}config set guild_age 3 days and 5 hours` (accepted readable time)" + ], + "notes": [ + "To remove this restriction, do `{prefix}config del guild_age`.", + "See also: `account_age`." + ] + }, + "reply_without_command": { + "default": "Disabled", + "description": "Setting this configuration will make all non-command messages sent in the thread channel to be forwarded to the recipient without the need of `{prefix}reply`.", + "examples": [ + "`{prefix}config set reply_without_command yes`", + "`{prefix}config set reply_without_command no`" + ], + "notes": [ + "Unfortunately, anonymous `reply_without_command` is currently not possible." + ] + }, + "log_channel_id": { + "default": "`#bot-logs` (created with `{prefix}setup`)", + "description": "This is the channel where all log messages will be sent (ie. thread close message, update message, etc.).\n\nTo change the log channel, you will need to find the [channel’s ID](https://support.discordapp.com/hc/en-us/articles/206346498). The channel doesn’t necessary have to be under the `main_category`.", + "examples": [ + "`{prefix}config set log_channel_id 9234932582312` (9234932582312 is the channel ID)" + ], + "notes": [ + "If the Modmail logging channel ended up being non-existent/invalid, no logs will be sent." + ] + }, + "sent_emoji": { + "default": "✅", + "description": "This is the emoji added to the message when when a Modmail action is invoked successfully (ie. DM Modmail, edit message, etc.).", + "examples": [ + "`{prefix}config set sent_emoji ✨`" + ], + "notes": [ + "You can disable `sent_emoji` with `{prefix}config set sent_emoji disable`.", + "Custom/animated emojis are also supported, however, the emoji must be added to the server.", + "See also: `blocked_emoji`." + ] + }, + "blocked_emoji": { + "default": "🚫", + "description": "This is the emoji added to the message when when a Modmail action is invoked unsuccessfully (ie. DM Modmail when blocked, failed to reply, etc.).", + "examples": [ + "`{prefix}config set blocked_emoji 🙅‍`" + ], + "notes": [ + "You can disable `blocked_emoji` with `{prefix}config set blocked_emoji disable`.", + "Custom/animated emojis are also supported, however, the emoji must be added to the server.", + "See also: `sent_emoji`." + ] + }, + "close_emoji": { + "default": "🔒", + "description": "This is the emoji the recipient can click to close a thread themselves. The emoji is automatically added to the `thread_creation_response` embed.", + "examples": [ + "`{prefix}config set close_emoji 👍‍`" + ], + "notes": [ + "This will only have an effect when `recipient_thread_close` is enabled.", + "See also: `recipient_thread_close`." + ] + }, + "recipient_thread_close": { + "default": "No", + "description": "Setting this configuration will allow recipients to use the `close_emoji` to close the thread themselves.", + "examples": [ + "`{prefix}config set reply_without_command yes`", + "`{prefix}config set reply_without_command no`" + ], + "notes": [ + "The close emoji is dictated by the configuration `close_emoji`.", + "See also: `close_emoji`." + ] + }, + "thread_auto_close_silently": { + "default": "No", + "description": "Setting this configuration will close silently when the thread auto-closes.", + "examples": [ + "`{prefix}config set thread_auto_close_silently yes`", + "`{prefix}config set thread_auto_close_silently no`" + ], + "notes": [ + "This will only have an effect when `thread_auto_close` is set.", + "See also: `thread_auto_close`." + ] + }, + "thread_auto_close": { + "default": "Never", + "description": "Setting this configuration will close threads automatically after the number of days, hours, minutes or any time-interval specified by this configuration.", + "examples": [ + "`{prefix}config set thread_auto_close 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_auto_close 3 days and 5 hours` (accepted readable time)" + ], + "notes": [ + "To disable auto close, do `{prefix}config del thread_auto_close`.", + "To prevent a thread from auto-closing, do `{prefix}close cancel`.", + "See also: `thread_auto_close_silently`, `thread_auto_close_response`." + ] + }, + "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.", + "examples": [ + "`{prefix}config set thread_auto_close_response Your close message here.`" + ], + "notes": [ + "Its possible to use `{{timeout}}` as a placeholder for a formatted timeout text.", + "This will not have an effect when `thread_auto_close_silently` is enabled.", + "Discord flavoured markdown is fully supported in `thread_auto_close_response`.", + "See also: `thread_auto_close`, `thread_auto_close_silently`." + ] + }, + "thread_creation_response": { + "default": "\"The staff team will get back to you as soon as possible.\"", + "description": "This is the message embed content sent to the recipient upon the creation of a new thread.", + "examples": [ + "`{prefix}config set thread_creation_response You will be contacted shortly.`" + ], + "notes": [ + "Discord flavoured markdown is fully supported in `thread_creation_response`.", + "See also: `thread_creation_title`, `thread_creation_footer`, `thread_close_response`." + ] + }, + "thread_creation_footer": { + "default": "\"Your message has been sent\"", + "description": "This is the message embed footer sent to the recipient upon the creation of a new thread.", + "examples": [ + "`{prefix}config set thread_creation_footer Please Hold...`" + ], + "notes": [ + "This is used in place of `thread_self_closable_creation_footer` when `recipient_thread_close` is enabled.", + "See also: `thread_creation_title`, `thread_creation_response`, `thread_self_closable_creation_footer`, `thread_close_footer`." + ] + }, + "thread_self_closable_creation_footer": { + "default": "\"Click the lock to close the thread\"", + "description": "This is the message embed footer sent to the recipient upon the creation of a new thread.", + "examples": [ + "`{prefix}config set thread_self_closable_creation_footer Please Hold...`" + ], + "notes": [ + "This is used in place of `thread_creation_footer` when `recipient_thread_close` is disabled.", + "See also: `thread_creation_title`, `thread_creation_response`, `thread_creation_footer`." + ] + }, + "thread_creation_title": { + "default": "\"Thread Created\"", + "description": "This is the message embed title sent to the recipient upon the creation of a new thread.", + "examples": [ + "`{prefix}config set thread_creation_title Hello!`" + ], + "notes": [ + "See also: `thread_creation_response`, `thread_creation_footer`, `thread_close_title`." + ] + }, + "thread_close_footer": { + "default": "\"Replying will create a new thread\"", + "description": "This is the message embed footer sent to the recipient upon the closure of a thread.", + "examples": [ + "`{prefix}config set thread_close_footer Bye!`" + ], + "notes": [ + "See also: `thread_close_title`, `thread_close_response`, `thread_creation_footer`." + ] + }, + "thread_close_title": { + "default": "\"Thread Closed\"", + "description": "This is the message embed title sent to the recipient upon the closure of a thread.", + "examples": [ + "`{prefix}config set thread_close_title Farewell!`" + ], + "notes": [ + "See also: `thread_close_response`, `thread_close_footer`, `thread_creation_title`." + ] + }, + "thread_close_response": { + "default": "\"{{closer.mention}} has closed this Modmail thread\"", + "description": "This is the message embed content sent to the recipient upon the closure of a thread.", + "examples": [ + "`{prefix}config set thread_close_response Your message is appreciated!`" + ], + "notes": [ + "When `recipient_thread_close` is enabled and the recipient closed their own thread, `thread_self_close_response` is used instead of this configuration.", + "You may use the `{{closer}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that closed the thread.", + "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. 3ifdm9204) of the log.", + "Discord flavoured markdown is fully supported in `thread_close_response`.", + "See also: `thread_close_title`, `thread_close_footer`, `thread_self_close_response`, `thread_creation_response`." + ] + }, + "thread_self_close_response": { + "default": "\"You have closed this Modmail thread.\"", + "description": "This is the message embed content sent to the recipient upon the closure of a their own thread.", + "examples": [ + "`{prefix}config set thread_self_close_response You have closed your own thread...`" + ], + "notes": [ + "When `recipient_thread_close` is disabled or the thread wasn't closed by the recipient, `thread_close_response` is used instead of this configuration.", + "You may use the `{{closer}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that closed the thread.", + "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. 3ifdm9204) of the log.", + "Discord flavoured markdown is fully supported in `thread_self_close_response`.", + "See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`." + ] + }, + "recipient_color": { + "default": "Discord Gold ([#F1C40F](https://placehold.it/100/f1c40f?text=+))", + "description": "This is the color of the messages sent by the recipient, this applies to messages received in the thread channel.", + "examples": [ + "`{prefix}config set recipient_color dark beige`", + "`{prefix}config set recipient_color cb7723`", + "`{prefix}config set recipient_color #cb7723`", + "`{prefix}config set recipient_color c4k`" + ], + "notes": [ + "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", + "See also: `main_color`, `mod_color`." + ] + }, + "mod_color": { + "default": "Discord Green ([#2ECC71](https://placehold.it/100/2ecc71?text=+))", + "description": "This is the color of the messages sent by the moderators, this applies to messages within in the thread channel and the DM thread messages received by the recipient.", + "examples": [ + "`{prefix}config set mod_color dark beige`", + "`{prefix}config set mod_color cb7723`", + "`{prefix}config set mod_color #cb7723`", + "`{prefix}config set mod_color c4k`" + ], + "notes": [ + "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", + "See also: `main_color`, `recipient_color`." + ] + }, + "mod_tag": { + "default": "The moderator's highest role", + "description": "This is the name tag in the “footer” section of the embeds sent by moderators in the recipient DM and thread channel.", + "examples": [ + "`{prefix}config set mod_tag Moderator`" + ], + "notes": [ + "When the message is sent anonymously, `anon_tag` is used instead.", + "See also: `anon_tag`." + ] + }, + "anon_username": { + "default": "Fallback on `mod_tag`", + "description": "This is the name in the “author” section of the embeds sent by anonymous moderators in the recipient DM.", + "examples": [ + "`{prefix}config set anon_username Incognito Mod`" + ], + "notes": [ + "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.", + "See also: `anon_avatar_url`, `anon_tag`." + ] + }, + "anon_avatar_url": { + "default": "Server avatar", + "description": "This is the avatar of the embeds sent by anonymous moderators in the recipient DM.", + "examples": [ + "`{prefix}config set anon_avatar_url https://path.to/your/avatar.png` (you will need to upload the avatar to somewhere)" + ], + "notes": [ + "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.", + "See also: `anon_username`, `anon_tag`." + ] + }, + "anon_tag": { + "default": "\"Response\"", + "description": "This is the name tag in the “footer” section of the embeds sent by anonymous moderators in the recipient DM.", + "examples": [ + "`{prefix}config set anon_tag Support Agent`" + ], + "notes": [ + "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.", + "See also: `anon_avatar_url`, `anon_username`, `mod_tag`." + ] + } +} \ No newline at end of file From afa3c9ad3bcea4ea480cf1943fe60b2c5316661b Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Jul 2019 14:23:41 -0700 Subject: [PATCH 0340/1221] I'm dumb --- core/checks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/checks.py b/core/checks.py index 42da8cf4b8..3c7f8b701f 100644 --- a/core/checks.py +++ b/core/checks.py @@ -81,8 +81,8 @@ async def check_permissions( # pylint: disable=R0911 role.id in level_permissions[level.name] for role in author_roles ) has_perm_id = ctx.author.id in level_permissions[level.name] - return has_perm_role or has_perm_id - + if has_perm_role or has_perm_id: + return True return False From 3afa23e9e53c773cfba75806db8b2e6fa394a3a2 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Jul 2019 14:27:22 -0700 Subject: [PATCH 0341/1221] More fluent English --- core/config_help.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/config_help.json b/core/config_help.json index a5b385b431..1cf4776c8f 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -57,7 +57,7 @@ ] }, "user_typing": { - "default": "No", + "default": "Disabled", "description": "When this is set to `yes`, whenever the recipient user starts to type in their DM channel, the moderator will see “{bot.user.display_name} is typing…” in the thread channel.", "examples": [ "`{prefix}config set user_typing yes`", @@ -68,7 +68,7 @@ ] }, "mod_typing": { - "default": "No", + "default": "Disabled", "description": "When this is set to `yes`, whenever a moderator starts to type in the thread channel, the recipient user will see \"{bot.user.display_name} is typing…\" in their DM channel.", "examples": [ "`{prefix}config set mod_typing yes`", @@ -159,7 +159,7 @@ ] }, "recipient_thread_close": { - "default": "No", + "default": "Disabled", "description": "Setting this configuration will allow recipients to use the `close_emoji` to close the thread themselves.", "examples": [ "`{prefix}config set reply_without_command yes`", From 894ad2fc0272935515b8706fd09facfa4541a5d8 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Jul 2019 14:39:00 -0700 Subject: [PATCH 0342/1221] Numbers carries over in snippet/alias now --- cogs/modmail.py | 4 ++-- cogs/utility.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 4eae4037e3..1e8f3f3f95 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -146,9 +146,9 @@ async def snippet(self, ctx, *, name: str.lower = None): embeds = [] - for names in zip_longest(*(iter(sorted(self.bot.snippets)),) * 15): + for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.snippets)),) * 15)): description = "\n".join( - ": ".join((str(a), b)) + ": ".join((str(a + i * 15), b)) for a, b in enumerate( takewhile(lambda x: x is not None, names), start=1 ) diff --git a/cogs/utility.py b/cogs/utility.py index 4914663b2b..2f9d0f60d9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -965,9 +965,9 @@ async def alias(self, ctx, *, name: str.lower = None): embeds = [] - for names in zip_longest(*(iter(sorted(self.bot.aliases)),) * 15): + for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = "\n".join( - ": ".join((str(a), b)) + ": ".join((str(a + i * 15), b)) for a, b in enumerate( takewhile(lambda x: x is not None, names), start=1 ) From 7f6992f70c7259543ec5948f22cbc2e48eef9551 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Jul 2019 15:41:42 -0700 Subject: [PATCH 0343/1221] Displays image/thumbnail for config help --- cogs/utility.py | 9 ++++++++- core/config_help.json | 29 ++++++++++++++++------------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 2f9d0f60d9..86f1866796 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -895,7 +895,14 @@ def fmt(val): for note in info['notes']: note_text += f'- {fmt(note)}\n' if note_text: - embed.add_field(name='Notes(s):', value=note_text, inline=False) + embed.add_field(name='Note(s):', value=note_text, inline=False) + + if info.get('image') is not None: + embed.set_image(url=fmt(info['image'])) + + if info.get('thumbnail') is not None: + embed.set_thumbnail(url=fmt(info['thumbnail'])) + return await ctx.send(embed=embed) @commands.group(aliases=["aliases"], invoke_without_command=True) diff --git a/core/config_help.json b/core/config_help.json index 1cf4776c8f..37106f286a 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -43,7 +43,7 @@ ] }, "main_color": { - "default": "Discord Blurple ([#7289DA](https://placehold.it/100/7289da?text=+))", + "default": "Discord Blurple [#7289DA](https://placehold.it/100/7289da?text=+)", "description": "This is the main color for Modmail (help/about/ping embed messages, subscribe, move, etc.).", "examples": [ "`{prefix}config set main_color olive green`", @@ -54,7 +54,8 @@ "notes": [ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", "See also: `mod_color`, `recipient_color`." - ] + ], + "thumbnail": "https://placehold.it/100/7289da?text=+" }, "user_typing": { "default": "Disabled", @@ -285,7 +286,7 @@ "See also: `thread_close_title`, `thread_close_footer`, `thread_self_close_response`, `thread_creation_response`." ] }, - "thread_self_close_response": { + "thread_self_close_response": { "default": "\"You have closed this Modmail thread.\"", "description": "This is the message embed content sent to the recipient upon the closure of a their own thread.", "examples": [ @@ -300,7 +301,7 @@ ] }, "recipient_color": { - "default": "Discord Gold ([#F1C40F](https://placehold.it/100/f1c40f?text=+))", + "default": "Discord Gold [#F1C40F](https://placehold.it/100/f1c40f?text=+)", "description": "This is the color of the messages sent by the recipient, this applies to messages received in the thread channel.", "examples": [ "`{prefix}config set recipient_color dark beige`", @@ -311,10 +312,11 @@ "notes": [ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", "See also: `main_color`, `mod_color`." - ] + ], + "thumbnail": "https://placehold.it/100/f1c40f?text=+" }, "mod_color": { - "default": "Discord Green ([#2ECC71](https://placehold.it/100/2ecc71?text=+))", + "default": "Discord Green [#2ECC71](https://placehold.it/100/2ecc71?text=+)", "description": "This is the color of the messages sent by the moderators, this applies to messages within in the thread channel and the DM thread messages received by the recipient.", "examples": [ "`{prefix}config set mod_color dark beige`", @@ -325,7 +327,8 @@ "notes": [ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", "See also: `main_color`, `recipient_color`." - ] + ], + "thumbnail": "https://placehold.it/100/2ecc71?text=+" }, "mod_tag": { "default": "The moderator's highest role", @@ -345,9 +348,9 @@ "`{prefix}config set anon_username Incognito Mod`" ], "notes": [ - "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.", "See also: `anon_avatar_url`, `anon_tag`." - ] + ], + "image": "https://i.imgur.com/SKOC42Z.png" }, "anon_avatar_url": { "default": "Server avatar", @@ -356,9 +359,9 @@ "`{prefix}config set anon_avatar_url https://path.to/your/avatar.png` (you will need to upload the avatar to somewhere)" ], "notes": [ - "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.", "See also: `anon_username`, `anon_tag`." - ] + ], + "image": "https://i.imgur.com/SKOC42Z.png" }, "anon_tag": { "default": "\"Response\"", @@ -367,8 +370,8 @@ "`{prefix}config set anon_tag Support Agent`" ], "notes": [ - "A detailed labeling of anonymous message configurations: https://i.imgur.com/SKOC42Z.png.", "See also: `anon_avatar_url`, `anon_username`, `mod_tag`." - ] + ], + "image": "https://i.imgur.com/SKOC42Z.png" } } \ No newline at end of file From 106d4a6fcbda21d24c11f4e16cfa5f4676a9d2e0 Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sun, 28 Jul 2019 22:06:53 -0400 Subject: [PATCH 0344/1221] Fix Expired Invite (#334) --- SPONSORS.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 989f1dfdfd..5f79ff80ea 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -21,7 +21,7 @@ }, { "name": "Discord Server!", - "value": "[**Click Here**](https://discord.gg/s8Ddphx)" + "value": "[**Click Here**](https://discord.gg/V8ErqHb)" } ] } From b4b9ec56d25c583de78196b2640d980d1d7d147d Mon Sep 17 00:00:00 2001 From: Kyber Date: Mon, 29 Jul 2019 13:39:08 +1000 Subject: [PATCH 0345/1221] Add dockerfile --- Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..d33f146c9e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM kennethreitz/pipenv + +COPY . /app + +CMD python3 bot.py From bdb2bcf71da23176f16b65e42b0410e1054bc3c4 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Jul 2019 21:49:09 -0700 Subject: [PATCH 0346/1221] Paginate config options and patched a thread open bug --- CHANGELOG.md | 2 + cogs/modmail.py | 4 +- cogs/plugins.py | 2 +- cogs/utility.py | 111 ++++++++++++++++++++++++++++-------------------- core/config.py | 15 ++++--- core/thread.py | 2 +- 6 files changed, 81 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c473256b57..b0c5110ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - A reworked interface for `?snippet` and `?alias`. - Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown). - The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/. +- Removed unclear `rm` alias for some `remove` commands. +- Paginate `?config options`. ### Fixes diff --git a/cogs/modmail.py b/cogs/modmail.py index 1e8f3f3f95..7949688577 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -146,7 +146,9 @@ async def snippet(self, ctx, *, name: str.lower = None): 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 = "\n".join( ": ".join((str(a + i * 15), b)) for a, b in enumerate( diff --git a/cogs/plugins.py b/cogs/plugins.py index 4f204ef10e..6ed1dccf1b 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -260,7 +260,7 @@ async def plugin_add(self, ctx, *, plugin_name: str): ) await ctx.send(embed=embed) - @plugin.command(name="remove", aliases=["del", "delete", "rm"]) + @plugin.command(name="remove", aliases=["del", "delete"]) @checks.has_permissions(PermissionLevel.OWNER) async def plugin_remove(self, ctx, *, plugin_name: str): """Remove a plugin.""" diff --git a/cogs/utility.py b/cogs/utility.py index 86f1866796..d90bd390b5 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -63,21 +63,22 @@ async def format_cog_help(self, cog, *, no_cog=False): embeds = [] for format_ in formats: - description = cog.description or "No description." \ - if not no_cog else "Miscellaneous commands without a category." - embed = Embed( - description=f'*{description}*', - color=bot.main_color, + description = ( + cog.description or "No description." + if not no_cog + else "Miscellaneous commands without a category." ) + embed = Embed(description=f"*{description}*", color=bot.main_color) 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" - embed.set_author( - name=name + continued, - icon_url=bot.user.avatar_url, + 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( text=f'Type "{prefix}{self.command_attrs["name"]} command" ' @@ -184,8 +185,9 @@ 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, ' - f'content:**\n\n{val}')) + return await self.get_destination().send( + escape_mentions(f"**`{command}` is a snippet, " f"content:**\n\n{val}") + ) val = self.context.bot.aliases.get(command) if val is not None: @@ -205,16 +207,16 @@ 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.') + embed.set_footer( + text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" for more ' + "details on aliases." + ) return await self.get_destination().send(embed=embed) logger.warning("CommandNotFound: %s", str(error)) embed = Embed(color=Color.red()) - embed.set_footer( - text=f'Command/Category "{command}" not found.' - ) + embed.set_footer(text=f'Command/Category "{command}" not found.') choices = set() @@ -748,10 +750,20 @@ async def config(self, ctx): @checks.has_permissions(PermissionLevel.OWNER) async def config_options(self, ctx): """Return a list of valid configuration names you can change.""" - allowed = self.bot.config.public_keys - valid = ", ".join(f"`{k}`" for k in allowed) - embed = Embed(title="Valid Keys", description=valid, color=self.bot.main_color) - return await ctx.send(embed=embed) + embeds = [] + for names in zip_longest(*(iter(sorted(self.bot.config.public_keys)),) * 15): + description = "\n".join( + f"`{name}`" for name in takewhile(lambda x: x is not None, names) + ) + embed = Embed( + title="Available configuration keys:", + color=self.bot.main_color, + description=description, + ) + embeds.append(embed) + + session = PaginatorSession(ctx, *embeds) + await session.run() @config.command(name="set", aliases=["add"]) @checks.has_permissions(PermissionLevel.OWNER) @@ -784,7 +796,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): return await ctx.send(embed=embed) - @config.command(name="remove", aliases=["del", "delete", "rm"]) + @config.command(name="remove", aliases=["del", "delete"]) @checks.has_permissions(PermissionLevel.OWNER) async def config_remove(self, ctx, key: str.lower): """Delete a set configuration variable.""" @@ -867,9 +879,8 @@ async def config_help(self, ctx, key: str.lower): return await ctx.send(embed=embed) config_help = self.bot.config.config_help - info = config_help.get(key) - if info is None: + if key not in config_help: embed = Embed( title="Error", color=Color.red(), @@ -880,30 +891,40 @@ async def config_help(self, ctx, key: str.lower): def fmt(val): return val.format(prefix=self.bot.prefix, bot=self.bot) - embed = Embed( - title=f"Configuration description on {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) - example_text = '' - for example in info['examples']: - example_text += f'- {fmt(example)}\n' - embed.add_field(name='Example(s):', value=example_text, inline=False) + index = 0 + embeds = [] + for i, (current_key, info) in enumerate(config_help.items()): + if current_key == key: + index = i + embed = Embed( + 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 + ) + example_text = "" + for example in info["examples"]: + example_text += f"- {fmt(example)}\n" + embed.add_field(name="Example(s):", value=example_text, inline=False) - note_text = '' - for note in info['notes']: - note_text += f'- {fmt(note)}\n' - if note_text: - embed.add_field(name='Note(s):', value=note_text, inline=False) + note_text = "" + for note in info["notes"]: + note_text += f"- {fmt(note)}\n" + if note_text: + embed.add_field(name="Note(s):", value=note_text, inline=False) - if info.get('image') is not None: - embed.set_image(url=fmt(info['image'])) + if info.get("image") is not None: + embed.set_image(url=fmt(info["image"])) - if info.get('thumbnail') is not None: - embed.set_thumbnail(url=fmt(info['thumbnail'])) + if info.get("thumbnail") is not None: + embed.set_thumbnail(url=fmt(info["thumbnail"])) + embeds += [embed] - return await ctx.send(embed=embed) + paginator = PaginatorSession(ctx, *embeds) + paginator.current = index + await paginator.run() @commands.group(aliases=["aliases"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) @@ -1265,9 +1286,7 @@ async def permissions_add_level( return await ctx.send(embed=embed) @permissions.group( - name="remove", - aliases=["del", "delete", "rm", "revoke"], - invoke_without_command=True, + name="remove", aliases=["del", "delete", "revoke"], invoke_without_command=True ) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove(self, ctx): diff --git a/core/config.py b/core/config.py index 33e493eb0b..ae0a1f0a3e 100644 --- a/core/config.py +++ b/core/config.py @@ -3,6 +3,7 @@ import logging import os import typing +from collections import namedtuple from copy import deepcopy from dotenv import load_dotenv @@ -129,12 +130,12 @@ def populate_cache(self) -> dict: data.update( {k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys} ) - configjson = os.path.join( + config_json = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json" ) - if os.path.exists(configjson): + if os.path.exists(config_json): logger.debug("Loading envs from config.json.") - with open(configjson, "r") as f: + with open(config_json, "r") as f: # Config json should override env vars try: data.update( @@ -150,11 +151,13 @@ def populate_cache(self) -> dict: ) self._cache = data - confighelpjson = os.path.join( + config_help_json = os.path.join( os.path.dirname(os.path.abspath(__file__)), "config_help.json" ) - with open(confighelpjson, "r") as f: - self.config_help = json.load(f) + with open(config_help_json, "r") as f: + Entry = namedtuple("Entry", ["index", "embed"]) + self.config_help = dict(sorted(json.load(f).items())) + return self._cache async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: diff --git a/core/thread.py b/core/thread.py index d4213a0c93..bb2f244926 100644 --- a/core/thread.py +++ b/core/thread.py @@ -229,7 +229,7 @@ def _format_info_embed(self, user, log_url, log_count, color): embed.set_author(name=str(user), icon_url=user.avatar_url, url=log_url) # embed.set_thumbnail(url=avi) - if member is None: + if member is not None: joined = str((time - member.joined_at).days) # embed.add_field(name='Joined', value=joined + days(joined)) embed.description += f", joined {days(joined)}" From c7dd2fe3590f7688143d7761ca3d664eb329f93c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 28 Jul 2019 21:55:42 -0700 Subject: [PATCH 0347/1221] Another bug... --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index bb2f244926..12bb71ab3c 100644 --- a/core/thread.py +++ b/core/thread.py @@ -193,7 +193,7 @@ def _format_info_embed(self, user, log_url, log_count, color): # key = log_url.split('/')[-1] role_names = "" - if member is None: + if member is not None: sep_server = self.bot.using_multiple_server_setup separator = ", " if sep_server else " " From 0ce91d036b2bd1af40e0ce2f9d0d5edbc81f1b82 Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sun, 28 Jul 2019 22:06:53 -0400 Subject: [PATCH 0348/1221] Fix Expired Invite (#334) --- SPONSORS.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 989f1dfdfd..5f79ff80ea 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -21,7 +21,7 @@ }, { "name": "Discord Server!", - "value": "[**Click Here**](https://discord.gg/s8Ddphx)" + "value": "[**Click Here**](https://discord.gg/V8ErqHb)" } ] } From fb257d4810f9c0fdb5a3f77509625682ca072c8b Mon Sep 17 00:00:00 2001 From: Kyber Date: Mon, 29 Jul 2019 13:39:08 +1000 Subject: [PATCH 0349/1221] Add dockerfile --- Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..d33f146c9e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM kennethreitz/pipenv + +COPY . /app + +CMD python3 bot.py From 31c116489c1a9d63e99e2041c52200c4592a2461 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 29 Jul 2019 02:19:35 -0700 Subject: [PATCH 0350/1221] perms stuff --- CHANGELOG.md | 4 +- cogs/utility.py | 458 +++++++++++++++++++++--------------------------- 2 files changed, 198 insertions(+), 264 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c5110ce5..47934e28ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Misc commands without cogs are now displayed in `?help`. - `?help` works for alias and snippets. - `?config help ` shows a help embed for the configuration. +- Support setting permissions for sub commands. ### Changes @@ -61,7 +62,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Bumped discord.py version to 1.2.3. - Use discord tasks for metadata loop. - More debug based logging. - +- Reduce redundancies in `?perms` sub commands. + # v3.0.3 ### Added diff --git a/cogs/utility.py b/cogs/utility.py index d90bd390b5..1252341d89 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1209,331 +1209,263 @@ async def permissions(self, ctx): """ await ctx.send_help(ctx.command) - @permissions.group(name="add", invoke_without_command=True) - @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_add(self, ctx): - """Add a permission to a command or a permission level.""" - await ctx.send_help(ctx.command) - - @permissions_add.command(name="command") - @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_add_command( - self, ctx, command: str, *, user_or_role: Union[User, Role, str] - ): - """ - Add a user, role, or everyone permission to use a command. - - Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, - `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". - """ - if command not in self.bot.all_commands: - embed = Embed( - title="Error", - color=Color.red(), - description="The command you are attempting to point " - f"to does not exist: `{command}`.", - ) - return await ctx.send(embed=embed) - + @staticmethod + def _verify_user_or_role(user_or_role): if hasattr(user_or_role, "id"): - value = user_or_role.id + return user_or_role.id elif user_or_role in {"everyone", "all"}: - value = -1 + return -1 else: raise commands.BadArgument(f'User or Role "{user_or_role}" not found') - await self.bot.update_perms(self.bot.all_commands[command].name, value) - embed = Embed( - title="Success", - color=self.bot.main_color, - description=f"Permission for {command} was successfully updated.", - ) - return await ctx.send(embed=embed) - - @permissions_add.command(name="level", aliases=["group"]) + @permissions.command(name="add", usage="[command/level] [name] [user_or_role]") @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_add_level( - self, ctx, level: str, *, user_or_role: Union[User, Role, str] + async def permissions_add( + self, ctx, type_: str.lower, name: str, *, user_or_role: Union[User, Role, str] ): """ - Add a user, role, or everyone permission to use commands of a permission level. + Add a permission to a command or a permission level. + + For sub commands, wrap the complete command name with quotes. + To find a list of permission levels, see `{prefix}help perms`. + + Examples: + - `{prefix}perms add level REGULAR everyone` + - `{prefix}perms add command reply @user` + - `{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, - `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". """ - if level.upper() not in PermissionLevel.__members__: + + if type_ not in {"command", "level"}: + return await ctx.send_help(ctx.command) + + command = level = None + if type_ == "command": + command = self.bot.get_command(name.lower()) + check = command is not None + else: + check = name.upper() in PermissionLevel.__members__ + level = PermissionLevel[name.upper()] if check else None + + if not check: embed = Embed( title="Error", color=Color.red(), - description="The permission level you are attempting to point " - f"to does not exist: `{level}`.", + description=f"The referenced {type_} does not exist: `{name}`.", ) return await ctx.send(embed=embed) - if hasattr(user_or_role, "id"): - value = user_or_role.id - elif user_or_role in {"everyone", "all"}: - value = -1 + value = self._verify_user_or_role(user_or_role) + if type_ == "command": + await self.bot.update_perms(command.qualified_name, value) + name = command.qualified_name else: - raise commands.BadArgument(f'User or Role "{user_or_role}" not found') + await self.bot.update_perms(level, value) + name = level.name - await self.bot.update_perms(PermissionLevel[level.upper()], value) embed = Embed( title="Success", color=self.bot.main_color, - description=f"Permission for {level} was successfully updated.", + description=f"Permission for `{name}` was successfully updated.", ) return await ctx.send(embed=embed) - @permissions.group( - name="remove", aliases=["del", "delete", "revoke"], invoke_without_command=True + @permissions.command( + name="remove", + aliases=["del", "delete", "revoke"], + usage="[command/level] [name] [user_or_role]", ) @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_remove(self, ctx): - """Remove permission to use a command or permission level.""" - await ctx.send_help(ctx.command) - - @permissions_remove.command(name="command") - @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_remove_command( - self, ctx, command: str, *, user_or_role: Union[User, Role, str] + async def permissions_remove( + self, ctx, type_: str.lower, name: str, *, user_or_role: Union[User, Role, str] ): """ - Remove a user, role, or everyone permission to use a command. + Remove permission to use a command or permission level. + + For sub commands, wrap the complete command name with quotes. + To find a list of permission levels, see `{prefix}help perms`. + + Examples: + - `{prefix}perms remove level REGULAR everyone` + - `{prefix}perms remove command reply @user` + - `{prefix}perms remove command "plugin enabled" @role` + - `{prefix}perms remove command help 984301093849028` Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, - `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". """ - if command not in self.bot.all_commands: - embed = Embed( - title="Error", - color=Color.red(), - description="The command you are attempting to point " - f"to does not exist: `{command}`.", - ) - return await ctx.send(embed=embed) + if type_ not in {"command", "level"}: + return await ctx.send_help(ctx.command) - if hasattr(user_or_role, "id"): - value = user_or_role.id - elif user_or_role in {"everyone", "all"}: - value = -1 + level = None + if type_ == "command": + command = self.bot.get_command(name.lower()) + name = command.qualified_name if command is not None else name else: - raise commands.BadArgument(f'User or Role "{user_or_role}" not found') + if name.upper() not in PermissionLevel.__members__: + embed = Embed( + title="Error", + color=Color.red(), + description=f"The referenced {type_} does not exist: `{name}`.", + ) + return await ctx.send(embed=embed) + level = PermissionLevel[name.upper()] + name = level.name + + value = self._verify_user_or_role(user_or_role) + await self.bot.update_perms(level or name, value, add=False) - await self.bot.update_perms( - self.bot.all_commands[command].name, value, add=False - ) embed = Embed( title="Success", color=self.bot.main_color, - description=f"Permission for {command} was successfully updated.", + description=f"Permission for `{name}` was successfully updated.", ) return await ctx.send(embed=embed) - @permissions_remove.command(name="level", aliases=["group"]) - @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_remove_level( - self, ctx, level: str, *, user_or_role: Union[User, Role, str] - ): - """ - Remove a user, role, or everyone permission to use commands of a permission level. - - Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, - `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". - """ - if level.upper() not in PermissionLevel.__members__: + def _get_perm(self, ctx, name, type_): + if type_ == "command": + permissions = self.bot.config["command_permissions"].get(name, []) + else: + permissions = self.bot.config["level_permissions"].get(name, []) + if not permissions: embed = Embed( - title="Error", - color=Color.red(), - description="The permission level you are attempting to point " - f"to does not exist: `{level}`.", + title=f"Permission entries for {type_} `{name}`:", + description="No permission entries found.", + color=self.bot.main_color, ) - return await ctx.send(embed=embed) - - if hasattr(user_or_role, "id"): - value = user_or_role.id - elif user_or_role in {"everyone", "all"}: - value = -1 else: - raise commands.BadArgument(f'User or Role "{user_or_role}" not found') + values = [] + for perm in permissions: + if perm == -1: + values.insert(0, "**everyone**") + continue + member = ctx.guild.get_member(perm) + if member is not None: + values.append(member.mention) + continue + user = self.bot.get_user(perm) + if user is not None: + values.append(user.mention) + continue + role = ctx.guild.get_role(perm) + if role is not None: + values.append(role.mention) + else: + values.append(str(perm)) - await self.bot.update_perms(PermissionLevel[level.upper()], value, add=False) - embed = Embed( - title="Success", - color=self.bot.main_color, - description=f"Permission for {level} was successfully updated.", - ) - return await ctx.send(embed=embed) + embed = Embed( + title=f"Permission entries for {type_} `{name}`:", + description=", ".join(values), + color=self.bot.main_color, + ) + return embed - @permissions.group(name="get", invoke_without_command=True) + @permissions.command(name="get", usage="[@user] or [command/level] [name]") @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_get(self, ctx, *, user_or_role: Union[User, Role, str]): + async def permissions_get( + self, ctx, user_or_role: Union[User, Role, str], *, name: str = None + ): """ View the currently-set permissions. - You can specify `user_or_role` as an alternative to get-by-command or get-by-level. + To find a list of permission levels, see `{prefix}help perms`. + + Examples: + - `{prefix}perms get @user` + - `{prefix}perms get 984301093849028` + - `{prefix}perms get command reply` + - `{prefix}perms get command plugin remove` + - `{prefix}perms get level SUPPORTER` Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, - `user_or_role` may be a role ID, name, mention, user ID, name, mention, "all", or "everyone". """ - if hasattr(user_or_role, "id"): - value = user_or_role.id - elif user_or_role in {"everyone", "all"}: - value = -1 - else: - raise commands.BadArgument(f'User or Role "{user_or_role}" not found') + if name is None and user_or_role not in {"command", "level"}: + value = self._verify_user_or_role(user_or_role) - cmds = [] - levels = [] - for cmd in self.bot.commands: - permissions = self.bot.config["command_permissions"].get(cmd.name, []) - if value in permissions: - cmds.append(cmd.name) - for level in PermissionLevel: - permissions = self.bot.config["level_permissions"].get(level.name, []) - if value in permissions: - levels.append(level.name) - mention = user_or_role.name if hasattr(user_or_role, "name") else user_or_role - desc_cmd = ( - ", ".join(map(lambda x: f"`{x}`", cmds)) - if cmds - else "No permission entries found." - ) - desc_level = ( - ", ".join(map(lambda x: f"`{x}`", levels)) - if levels - else "No permission entries found." - ) + cmds = [] + levels = [] - embeds = [ - Embed( - title=f"{mention} has permission with the following commands:", - description=desc_cmd, - color=self.bot.main_color, - ), - Embed( - title=f"{mention} has permission with the following permission groups:", - description=desc_level, - color=self.bot.main_color, - ), - ] - p_session = PaginatorSession(ctx, *embeds) - return await p_session.run() - - @permissions_get.command(name="command") - @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_get_command(self, ctx, *, command: str = None): - """View currently-set permissions for a command.""" + done = set() + for _, command in self.bot.all_commands.items(): + if command not in done: + done.add(command) + permissions = self.bot.config["command_permissions"].get( + command.qualified_name, [] + ) + if value in permissions: + cmds.append(command.qualified_name) + + for level in PermissionLevel: + permissions = self.bot.config["level_permissions"].get(level.name, []) + if value in permissions: + levels.append(level.name) + + mention = getattr(user_or_role, "name", user_or_role) + desc_cmd = ( + ", ".join(map(lambda x: f"`{x}`", cmds)) + if cmds + else "No permission entries found." + ) + desc_level = ( + ", ".join(map(lambda x: f"`{x}`", levels)) + if levels + else "No permission entries found." + ) - def get_command(cmd): - permissions = self.bot.config["command_permissions"].get(cmd.name, []) - if not permissions: - embed = Embed( - title=f"Permission entries for command `{cmd.name}`:", - description="No permission entries found.", + embeds = [ + Embed( + title=f"{mention} has permission with the following commands:", + description=desc_cmd, color=self.bot.main_color, - ) - else: - values = [] - for perm in permissions: - if perm == -1: - values.insert(0, "**everyone**") - continue - member = ctx.guild.get_member(perm) - if member is not None: - values.append(member.mention) - continue - user = self.bot.get_user(perm) - if user is not None: - values.append(user.mention) - continue - role = ctx.guild.get_role(perm) - if role is not None: - values.append(role.mention) - else: - values.append(str(perm)) - - embed = Embed( - title=f"Permission entries for command `{cmd.name}`:", - description=", ".join(values), + ), + Embed( + title=f"{mention} has permission with the following permission groups:", + description=desc_level, color=self.bot.main_color, - ) - return embed - - embeds = [] - if command is not None: - if command not in self.bot.all_commands: - embed = Embed( - title="Error", - color=Color.red(), - description="The command you are attempting to point " - f"to does not exist: `{command}`.", - ) - return await ctx.send(embed=embed) - embeds.append(get_command(self.bot.all_commands[command])) + ), + ] else: - for cmd in self.bot.commands: - embeds.append(get_command(cmd)) - - p_session = PaginatorSession(ctx, *embeds) - return await p_session.run() - - @permissions_get.command(name="level", aliases=["group"]) - @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_get_level(self, ctx, *, level: str = None): - """View currently-set permissions for commands of a permission level.""" + if user_or_role not in {"command", "level"}: + return await ctx.send_help(ctx.command) + embeds = [] + if name is not None: + name = name.strip('"') + command = level = None + if user_or_role == "command": + command = self.bot.get_command(name.lower()) + check = command is not None + else: + check = name.upper() in PermissionLevel.__members__ + level = PermissionLevel[name.upper()] if check else None + + if not check: + embed = Embed( + title="Error", + color=Color.red(), + description=f"The referenced {user_or_role} does not exist: `{name}`.", + ) + return await ctx.send(embed=embed) - def get_level(perm_level): - permissions = self.bot.config["level_permissions"].get(perm_level.name, []) - if not permissions: - embed = Embed( - title="Permission entries for permission " - f"level `{perm_level.name}`:", - description="No permission entries found.", - color=self.bot.main_color, - ) + if user_or_role == "command": + embeds.append( + self._get_perm(ctx, command.qualified_name, "command") + ) + else: + embeds.append(self._get_perm(ctx, level.name, "level")) else: - values = [] - for perm in permissions: - if perm == -1: - values.insert(0, "**everyone**") - continue - member = ctx.guild.get_member(perm) - if member is not None: - values.append(member.mention) - continue - user = self.bot.get_user(perm) - if user is not None: - values.append(user.mention) - continue - role = ctx.guild.get_role(perm) - if role is not None: - values.append(role.mention) - else: - values.append(str(perm)) - - embed = Embed( - title=f"Permission entries for permission level `{perm_level.name}`:", - description=", ".join(values), - color=self.bot.main_color, - ) - return embed - - embeds = [] - if level is not None: - if level.upper() not in PermissionLevel.__members__: - embed = Embed( - title="Error", - color=Color.red(), - description="The permission level you are attempting to point " - f"to does not exist: `{level}`.", - ) - return await ctx.send(embed=embed) - embeds.append(get_level(PermissionLevel[level.upper()])) - else: - for perm_level in PermissionLevel: - embeds.append(get_level(perm_level)) + if user_or_role == "command": + done = set() + for _, command in self.bot.all_commands.items(): + if command not in done: + done.add(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")) p_session = PaginatorSession(ctx, *embeds) return await p_session.run() From 52b5b2b4b2502ba102dcb7df43747ac410b1e95f Mon Sep 17 00:00:00 2001 From: KongGal Date: Mon, 29 Jul 2019 13:57:44 +0200 Subject: [PATCH 0351/1221] Updated Dockerfile to allow for automated building (#335) --- Dockerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index d33f146c9e..773031d731 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ -FROM kennethreitz/pipenv +FROM library/python:latest +RUN apt update && apt install -y pipenv +RUN mkdir -p /bot && cd /bot && git clone https://github.com/kyb3r/modmail . +WORKDIR /bot +RUN pipenv install -COPY . /app - -CMD python3 bot.py +CMD ["pipenv", "run", "python3", "bot.py"] From af904218d129beb5167b3ed0f3ac0669c2cf5814 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 29 Jul 2019 12:09:27 -0700 Subject: [PATCH 0352/1221] Fix stuff with viewing perm --- CHANGELOG.md | 4 ++-- cogs/modmail.py | 2 +- cogs/utility.py | 19 +++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47934e28ff..8d57ffdc9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -225,7 +225,7 @@ Un-deprecated the `OWNERS` config variable to support discord developer team acc ### New Permissions System - A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. -- There are five permission groups/levels: +- There are five permission levels: - Owner [5] - Administrator [4] - Moderator [3] @@ -247,7 +247,7 @@ The same applies to individual commands permissions: To revoke permission, use `remove` instead of `add`. -To view all roles and users with permission for a permission group or command do: +To view all roles and users with permission for a permission level or command do: - `?permissions get command command-name` - `?permissions get level owner` diff --git a/cogs/modmail.py b/cogs/modmail.py index 7949688577..c29cfec308 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -87,7 +87,7 @@ async def setup(self, ctx): await self.bot.config.update() await ctx.send( "Successfully set up server.\n" - "Consider setting permission groups to give access " + "Consider setting permission levels to give access " "to roles or users the ability to use Modmail.\n" f"Type `{self.bot.prefix}permissions` for more info." ) diff --git a/cogs/utility.py b/cogs/utility.py index 1252341d89..4f7d17a502 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -220,9 +220,9 @@ async def send_error_message(self, error): choices = set() - for name, cmd in self.context.bot.all_commands.items(): + for cmd in self.context.bot.walk_commands(): if not cmd.hidden: - choices.add(name) + choices.add(cmd.name) closest = get_close_matches(command, choices) if closest: @@ -1221,7 +1221,7 @@ def _verify_user_or_role(user_or_role): @permissions.command(name="add", usage="[command/level] [name] [user_or_role]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add( - self, ctx, type_: str.lower, name: str, *, user_or_role: Union[User, Role, str] + self, ctx, type_: str.lower, name: str, *, user_or_role: Union[Role, User, str] ): """ Add a permission to a command or a permission level. @@ -1279,7 +1279,7 @@ async def permissions_add( ) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove( - self, ctx, type_: str.lower, name: str, *, user_or_role: Union[User, Role, str] + self, ctx, type_: str.lower, name: str, *, user_or_role: Union[Role, User, str] ): """ Remove permission to use a command or permission level. @@ -1300,8 +1300,7 @@ async def permissions_remove( level = None if type_ == "command": - command = self.bot.get_command(name.lower()) - name = command.qualified_name if command is not None else name + name = getattr(self.bot.get_command(name.lower()), "qualified_name", name) else: if name.upper() not in PermissionLevel.__members__: embed = Embed( @@ -1364,7 +1363,7 @@ def _get_perm(self, ctx, name, type_): @permissions.command(name="get", usage="[@user] or [command/level] [name]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get( - self, ctx, user_or_role: Union[User, Role, str], *, name: str = None + self, ctx, user_or_role: Union[Role, User, str], *, name: str = None ): """ View the currently-set permissions. @@ -1388,7 +1387,7 @@ async def permissions_get( levels = [] done = set() - for _, command in self.bot.all_commands.items(): + for command in self.bot.walk_commands(): if command not in done: done.add(command) permissions = self.bot.config["command_permissions"].get( @@ -1421,7 +1420,7 @@ async def permissions_get( color=self.bot.main_color, ), Embed( - title=f"{mention} has permission with the following permission groups:", + title=f"{mention} has permission with the following permission levels:", description=desc_level, color=self.bot.main_color, ), @@ -1457,7 +1456,7 @@ async def permissions_get( else: if user_or_role == "command": done = set() - for _, command in self.bot.all_commands.items(): + for command in self.bot.walk_commands(): if command not in done: done.add(command) embeds.append( From c596f1c48b8c6f21e61c64b4446d9625fd54deb7 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 29 Jul 2019 12:50:22 -0700 Subject: [PATCH 0353/1221] 1-5 as level names --- CHANGELOG.md | 1 + cogs/utility.py | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d57ffdc9d..ee45406fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - `?help` works for alias and snippets. - `?config help ` shows a help embed for the configuration. - Support setting permissions for sub commands. +- Support numbers (1-5) as substitutes for Permission Level REGULAR - OWNER in `?perms` sub commands. ### Changes diff --git a/cogs/utility.py b/cogs/utility.py index 4f7d17a502..62d02ebeba 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1218,6 +1218,22 @@ def _verify_user_or_role(user_or_role): else: raise commands.BadArgument(f'User or Role "{user_or_role}" not found') + @staticmethod + def _parse_level(name): + name = name.upper() + try: + return PermissionLevel[name] + except KeyError: + pass + transform = { + "1": PermissionLevel.REGULAR, + "2": PermissionLevel.SUPPORTER, + "3": PermissionLevel.MODERATOR, + "4": PermissionLevel.ADMINISTRATOR, + "5": PermissionLevel.OWNER, + } + return transform.get(name) + @permissions.command(name="add", usage="[command/level] [name] [user_or_role]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add( @@ -1246,8 +1262,8 @@ async def permissions_add( command = self.bot.get_command(name.lower()) check = command is not None else: - check = name.upper() in PermissionLevel.__members__ - level = PermissionLevel[name.upper()] if check else None + level = self._parse_level(name) + check = level is not None if not check: embed = Embed( @@ -1302,14 +1318,14 @@ async def permissions_remove( if type_ == "command": name = getattr(self.bot.get_command(name.lower()), "qualified_name", name) else: - if name.upper() not in PermissionLevel.__members__: + level = self._parse_level(name) + if level is None: embed = Embed( title="Error", color=Color.red(), description=f"The referenced {type_} does not exist: `{name}`.", ) return await ctx.send(embed=embed) - level = PermissionLevel[name.upper()] name = level.name value = self._verify_user_or_role(user_or_role) @@ -1370,9 +1386,15 @@ async def permissions_get( To find a list of permission levels, see `{prefix}help perms`. + To view all command and level permissions: + Examples: - `{prefix}perms get @user` - `{prefix}perms get 984301093849028` + + To view all users and roles of a command or level permission: + + Examples: - `{prefix}perms get command reply` - `{prefix}perms get command plugin remove` - `{prefix}perms get level SUPPORTER` @@ -1401,7 +1423,9 @@ async def permissions_get( if value in permissions: levels.append(level.name) - mention = getattr(user_or_role, "name", 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 @@ -1436,8 +1460,8 @@ async def permissions_get( command = self.bot.get_command(name.lower()) check = command is not None else: - check = name.upper() in PermissionLevel.__members__ - level = PermissionLevel[name.upper()] if check else None + level = self._parse_level(name) + check = level is not None if not check: embed = Embed( From 3a07329b79941832bb1370b21dc0233f478ffe7c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 30 Jul 2019 17:25:19 -0700 Subject: [PATCH 0354/1221] Add doc for snippet raw --- cogs/modmail.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index c29cfec308..32d716ec89 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -117,7 +117,7 @@ async def snippet(self, ctx, *, name: str.lower = None): with `{prefix}snippet-name`, the message "A pre-defined text." will be sent to the recipient. - Currently, there is not a default anonymous snippet command; however, a workaround + Currently, there is not a built-in anonymous snippet command; however, a workaround is available using `{prefix}alias`. Here is how: - `{prefix}alias add snippet-name anonreply A pre-defined anonymous text.` @@ -165,6 +165,9 @@ async def snippet(self, ctx, *, name: str.lower = None): @snippet.command(name="raw") @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippet_raw(self, ctx, *, name: str.lower): + """ + View the raw content of a snippet. + """ val = self.bot.snippets.get(name) if val is None: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") From 92fad07af76c6d89c2b8a81ec0b6d078df04cb38 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 30 Jul 2019 17:39:27 -0700 Subject: [PATCH 0355/1221] Fix log url prefix Signed-off-by: Taaku18 <45324516+Taaku18@users.noreply.github.com> --- cogs/modmail.py | 12 ++++-------- core/clients.py | 10 ++++++++-- core/thread.py | 10 ++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 32d716ec89..dc4368160c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -570,16 +570,12 @@ def format_log_embeds(self, logs, avatar_url): title = f"Total Results Found ({len(logs)})" for entry in logs: - - key = entry["key"] - created_at = parser.parse(entry["created_at"]) - prefix = self.bot.config["log_url_prefix"] - if prefix == "NONE": - prefix = "" - - log_url = self.bot.config["log_url"].strip("/") + f"{prefix}/{key}" + prefix = self.bot.config['log_url_prefix'].strip('/') + if prefix == 'NONE': + prefix = '' + log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{entry['key']}" username = entry["recipient"]["name"] + "#" username += entry["recipient"]["discriminator"] diff --git a/core/clients.py b/core/clients.py index a4ed847622..31a73be390 100644 --- a/core/clients.py +++ b/core/clients.py @@ -99,7 +99,10 @@ async def get_log(self, channel_id: Union[str, int]) -> dict: async def get_log_link(self, channel_id: Union[str, int]) -> str: doc = await self.get_log(channel_id) logger.debug("Retrieving log link for channel %s.", channel_id) - return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{doc['key']}" + 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']}" async def create_log_entry( self, recipient: Member, channel: TextChannel, creator: Member @@ -135,7 +138,10 @@ async def create_log_entry( } ) logger.debug("Created a log entry, key %s.", key) - return f"{self.bot.config['log_url'].strip('/')}{self.bot.config['log_url_prefix']}/{key}" + prefix = self.bot.config['log_url_prefix'].strip('/') + if prefix == 'NONE': + prefix = '' + return f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{key}" async def get_config(self) -> dict: conf = await self.db.config.find_one({"bot_id": self.bot.user.id}) diff --git a/core/thread.py b/core/thread.py index 12bb71ab3c..7e8a2574db 100644 --- a/core/thread.py +++ b/core/thread.py @@ -338,12 +338,10 @@ async def _close( ) if isinstance(log_data, dict): - prefix = self.bot.config["log_url_prefix"] - if prefix == "NONE": - prefix = "" - log_url = ( - f"{self.bot.config['log_url'].strip('/')}{prefix}/{log_data['key']}" - ) + prefix = self.bot.config['log_url_prefix'].strip('/') + if prefix == 'NONE': + prefix = '' + log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{log_data['key']}" if log_data["messages"]: content = str(log_data["messages"][0]["content"]) From 5a26c28900616ca2958376c4f0e7bbf723957472 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 30 Jul 2019 22:07:52 -0700 Subject: [PATCH 0356/1221] Config help on protected keys --- CHANGELOG.md | 1 + cogs/modmail.py | 6 ++-- cogs/utility.py | 18 ++++++----- core/config_help.json | 72 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee45406fb2..49d41d382f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - `disable_recipient_thread_close` is removed, a new configuration variable `recipient_thread_close` replaces it which defaults to False. - Truthy and falsy values for binary configuration variables are now interpreted respectfully. +- `LOG_URL_PREFIX` cannot be set to "NONE" to specify no additional path in the future, "/" is the new method. ### Added diff --git a/cogs/modmail.py b/cogs/modmail.py index dc4368160c..693adafb18 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -572,9 +572,9 @@ def format_log_embeds(self, logs, avatar_url): for entry in logs: created_at = parser.parse(entry["created_at"]) - prefix = self.bot.config['log_url_prefix'].strip('/') - if prefix == 'NONE': - prefix = '' + prefix = self.bot.config["log_url_prefix"].strip("/") + if prefix == "NONE": + prefix = "" log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{entry['key']}" username = entry["recipient"]["name"] + "#" diff --git a/cogs/utility.py b/cogs/utility.py index 62d02ebeba..672d8f3448 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -844,8 +844,9 @@ async def config_get(self, ctx, key: str.lower = None): color=Color.red(), description=f"`{key}` is an invalid key.", ) - valid_keys = [f"`{k}`" for k in keys] - embed.add_field(name="Valid keys", value=", ".join(valid_keys)) + embed.set_footer( + text=f'Type "{self.bot.prefix}config options" for a list of config variables.' + ) else: embed = Embed( @@ -870,7 +871,9 @@ async def config_help(self, ctx, key: str.lower): """ Show information on a specified configuration. """ - if key not in self.bot.config.public_keys: + if not ( + key in self.bot.config.public_keys or key in self.bot.config.protected_keys + ): embed = Embed( title="Error", color=Color.red(), @@ -904,10 +907,11 @@ def fmt(val): embed.add_field( name="Information:", value=fmt(info["description"]), inline=False ) - example_text = "" - for example in info["examples"]: - example_text += f"- {fmt(example)}\n" - embed.add_field(name="Example(s):", value=example_text, inline=False) + if info["examples"]: + example_text = "" + for example in info["examples"]: + example_text += f"- {fmt(example)}\n" + embed.add_field(name="Example(s):", value=example_text, inline=False) note_text = "" for note in info["notes"]: diff --git a/core/config_help.json b/core/config_help.json index 37106f286a..f4e8eae36f 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -373,5 +373,77 @@ "See also: `anon_avatar_url`, `anon_username`, `mod_tag`." ], "image": "https://i.imgur.com/SKOC42Z.png" + }, + "modmail_guild_id": { + "default": "Fallback on `GUILD_ID`", + "description": "The ID of the discord server where the threads channels should be created (receiving server).", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] + }, + "guild_id": { + "default": "None, required", + "description": "The ID of the discord server where recipient users reside (users server).", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] + }, + "log_url": { + "default": "https://example.com/", + "description": "The base log viewer URL link, leave this as-is to not configure a log viewer.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] + }, + "log_url_prefix": { + "default": "`/logs`", + "description": "The path to your log viewer extending from your `LOG_URL`, set this to `/` to specify no extra path to the log viewer.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] + }, + "mongo_uri": { + "default": "None, required", + "description": "A MongoDB SRV connection string.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] + }, + "owners": { + "default": "None, required", + "description": "A list of definite bot owners, use `{prefix}perms add level OWNER @user` to set flexible bot owners.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables." + ] + }, + "token": { + "default": "None, required", + "description": "Your bot token as found in the Discord Developer Portal.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables." + ] + }, + "log_level": { + "default": "INFO", + "description": "The logging level for logging to stdout.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables." + ] } } \ No newline at end of file From 30a6c7f6bc6547f25450ec09209df60e2251f53c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 31 Jul 2019 01:51:03 -0700 Subject: [PATCH 0357/1221] Added raw alias --- .pylintrc | 2 +- CHANGELOG.md | 1 + app.json | 4 ---- cogs/modmail.py | 11 +++-------- cogs/plugins.py | 2 +- cogs/utility.py | 44 +++++++++++++++++++++++++------------------- core/checks.py | 4 ++-- core/config.py | 2 -- core/thread.py | 19 +++++++++---------- core/utils.py | 12 ++++++++++-- 10 files changed, 52 insertions(+), 49 deletions(-) diff --git a/.pylintrc b/.pylintrc index 1bed8185c7..a45837fa82 100644 --- a/.pylintrc +++ b/.pylintrc @@ -19,7 +19,7 @@ ignore-patterns= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. -jobs=1 +jobs=0 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d41d382f..f904043075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - `?plugin registry page-number` plugin registry can specify a page number for quick access. - A reworked interface for `?snippet` and `?alias`. - Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown). + - Add an `?alias raw ` command for viewing the raw content of a alias (escaped markdown). - The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/. - Removed unclear `rm` alias for some `remove` commands. - Paginate `?config options`. diff --git a/app.json b/app.json index 8d16cd75c2..e4f5032e81 100644 --- a/app.json +++ b/app.json @@ -15,10 +15,6 @@ "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval and update).", "required": true }, - "GITHUB_ACCESS_TOKEN": { - "description": "Your personal access token for GitHub, adding this gives you the ability to use the 'update' command, which will sync your fork with the main repo.", - "required": false - }, "MONGO_URI": { "description": "Mongo DB connection URI for self-hosting your data.", "required": true diff --git a/cogs/modmail.py b/cogs/modmail.py index 693adafb18..081385d202 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,7 +1,7 @@ import asyncio import logging from datetime import datetime -from itertools import zip_longest, takewhile +from itertools import zip_longest from typing import Optional, Union from types import SimpleNamespace as param @@ -17,7 +17,7 @@ from core.models import PermissionLevel from core.paginator import PaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User, create_not_found_embed +from core.utils import format_preview, User, create_not_found_embed, format_description logger = logging.getLogger("Modmail") @@ -149,12 +149,7 @@ async def snippet(self, ctx, *, name: str.lower = None): for i, names in enumerate( zip_longest(*(iter(sorted(self.bot.snippets)),) * 15) ): - description = "\n".join( - ": ".join((str(a + i * 15), b)) - for a, b in enumerate( - takewhile(lambda x: x is not None, names), start=1 - ) - ) + 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) embeds.append(embed) diff --git a/cogs/plugins.py b/cogs/plugins.py index 6ed1dccf1b..ebb9a72241 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -288,7 +288,7 @@ async def plugin_remove(self, ctx, *, plugin_name: str): for i in self.bot.config["plugins"] ): # if there are no more of such repos, delete the folder - def onerror(func, path, exc_info): # pylint: disable=W0613 + def onerror(func, path, _): if not os.access(path, os.W_OK): # Is the error an access error? os.chmod(path, stat.S_IWUSR) diff --git a/cogs/utility.py b/cogs/utility.py index 672d8f3448..b1fe7aa7b3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -33,6 +33,7 @@ get_perm_level, create_not_found_embed, parse_alias, + format_description, ) logger = logging.getLogger("Modmail") @@ -248,11 +249,10 @@ def __init__(self, bot): verify_checks=False, command_attrs={"help": "Shows this help message."} ) # Looks a bit ugly - # noinspection PyProtectedMember - self.bot.help_command._command_impl = checks.has_permissions( # pylint: disable=W0212 + self.bot.help_command._command_impl = checks.has_permissions( # pylint: disable=protected-access PermissionLevel.REGULAR )( - self.bot.help_command._command_impl # pylint: disable=W0212 + self.bot.help_command._command_impl # pylint: disable=protected-access ) self.bot.help_command.cog = self @@ -998,12 +998,7 @@ async def alias(self, ctx, *, name: str.lower = None): embeds = [] for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): - description = "\n".join( - ": ".join((str(a + i * 15), b)) - for a, b in enumerate( - takewhile(lambda x: x is not None, names), start=1 - ) - ) + description = format_description(i, names) embed = Embed(color=self.bot.main_color, description=description) embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) embeds.append(embed) @@ -1011,6 +1006,18 @@ async def alias(self, ctx, *, name: str.lower = None): session = PaginatorSession(ctx, *embeds) await session.run() + @alias.command(name="raw") + @checks.has_permissions(PermissionLevel.MODERATOR) + async def alias_raw(self, ctx, *, name: str.lower): + """ + View the raw content of an alias. + """ + val = self.bot.aliases.get(name) + if val is None: + embed = 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): @@ -1027,36 +1034,36 @@ async def alias_add(self, ctx, name: str.lower, *, value): - 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 = Embed( title="Error", color=Color.red(), description=f"A command with the same name already exists: `{name}`.", ) - return await ctx.send(embed=embed) - if name in self.bot.aliases: + elif name in self.bot.aliases: embed = Embed( title="Error", color=Color.red(), description=f"Another alias with the same name already exists: `{name}`.", ) - return await ctx.send(embed=embed) - if name in self.bot.snippets: + elif name in self.bot.snippets: embed = Embed( title="Error", color=Color.red(), description=f"A snippet with the same name already exists: `{name}`.", ) - return await ctx.send(embed=embed) - if len(name) > 120: + elif len(name) > 120: embed = Embed( title="Error", color=Color.red(), description=f"Alias names cannot be longer than 120 characters.", ) + + if embed is not None: return await ctx.send(embed=embed) values = parse_alias(value) @@ -1106,7 +1113,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) embed.description += f"\n{i}: {val}" - self.bot.aliases[name] = "&&".join(values) + self.bot.aliases[name] = " && ".join(values) await self.bot.config.update() return await ctx.send(embed=embed) @@ -1217,10 +1224,9 @@ async def permissions(self, ctx): def _verify_user_or_role(user_or_role): if hasattr(user_or_role, "id"): return user_or_role.id - elif user_or_role in {"everyone", "all"}: + if user_or_role in {"everyone", "all"}: return -1 - else: - raise commands.BadArgument(f'User or Role "{user_or_role}" not found') + raise commands.BadArgument(f'User or Role "{user_or_role}" not found') @staticmethod def _parse_level(name): diff --git a/core/checks.py b/core/checks.py index 3c7f8b701f..503877b3a7 100644 --- a/core/checks.py +++ b/core/checks.py @@ -33,7 +33,7 @@ async def predicate(ctx): if not has_perm and ctx.command.qualified_name != "help": logger.error( - "You does not have permission to use this command: `%s` (%s).", + "You do not have permission to use this command: `%s` (%s).", str(ctx.command.qualified_name), str(permission_level.name), ) @@ -43,7 +43,7 @@ async def predicate(ctx): return commands.check(predicate) -async def check_permissions( # pylint: disable=R0911 +async def check_permissions( # pylint: disable=too-many-return-statements ctx, command_name, permission_level ) -> bool: """Logic for checking permissions for a command for a user""" diff --git a/core/config.py b/core/config.py index ae0a1f0a3e..e7776cb39d 100644 --- a/core/config.py +++ b/core/config.py @@ -3,7 +3,6 @@ import logging import os import typing -from collections import namedtuple from copy import deepcopy from dotenv import load_dotenv @@ -155,7 +154,6 @@ def populate_cache(self) -> dict: os.path.dirname(os.path.abspath(__file__)), "config_help.json" ) with open(config_help_json, "r") as f: - Entry = namedtuple("Entry", ["index", "embed"]) self.config_help = dict(sorted(json.load(f).items())) return self._cache diff --git a/core/thread.py b/core/thread.py index 7e8a2574db..95eeadc3dd 100644 --- a/core/thread.py +++ b/core/thread.py @@ -338,9 +338,9 @@ async def _close( ) if isinstance(log_data, dict): - prefix = self.bot.config['log_url_prefix'].strip('/') - if prefix == 'NONE': - prefix = '' + prefix = self.bot.config["log_url_prefix"].strip("/") + if prefix == "NONE": + prefix = "" log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{log_data['key']}" if log_data["messages"]: @@ -410,7 +410,9 @@ async def _close( await asyncio.gather(*tasks) async def cancel_closure( - self, auto_close: bool = False, all: bool = False # pylint: disable=W0622 + 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() @@ -746,8 +748,7 @@ async def send( file_upload_count += 1 if from_mod: - # noinspection PyUnresolvedReferences,PyDunderSlots - embed.color = self.bot.mod_color # pylint: disable=E0237 + embed.colour = self.bot.mod_color # Anonymous reply sent in thread channel if anonymous and isinstance(destination, discord.TextChannel): embed.set_footer(text="Anonymous Reply") @@ -760,12 +761,10 @@ async def send( else: embed.set_footer(text=self.bot.config["anon_tag"]) elif note: - # noinspection PyUnresolvedReferences,PyDunderSlots - embed.color = discord.Color.blurple() # pylint: disable=E0237 + embed.colour = discord.Color.blurple() else: embed.set_footer(text=f"Recipient") - # noinspection PyUnresolvedReferences,PyDunderSlots - embed.color = self.bot.recipient_color # pylint: disable=E0237 + embed.colour = self.bot.recipient_color try: await destination.trigger_typing() diff --git a/core/utils.py b/core/utils.py index 024c6e0f03..89f9b55f81 100644 --- a/core/utils.py +++ b/core/utils.py @@ -2,7 +2,8 @@ import shlex import typing from difflib import get_close_matches -from distutils.util import strtobool as _stb # pylint: disable=E0401 +from distutils.util import strtobool as _stb +from itertools import takewhile from urllib import parse import discord @@ -37,7 +38,7 @@ async def convert(self, ctx, argument): return discord.Object(int(match.group(1))) -def truncate(text: str, max: int = 50) -> str: # pylint: disable=W0622 +def truncate(text: str, max: int = 50) -> str: # pylint: disable=redefined-builtin """ Reduces the string to `max` length, by trimming the message into "...". @@ -253,3 +254,10 @@ def parse_alias(alias): if not all(cmd): return [] return cmd + + +def format_description(i, names): + return "\n".join( + ": ".join((str(a + i * 15), b)) + for a, b in enumerate(takewhile(lambda x: x is not None, names), start=1) + ) From c89ce1495f2e7ce5bdae2167347722f11c7afb7d Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 31 Jul 2019 23:48:00 -0600 Subject: [PATCH 0358/1221] Emoji wasn't showing cuz im dumb --- core/paginator.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/core/paginator.py b/core/paginator.py index 2fb72da8ff..a19db604b7 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -194,6 +194,12 @@ 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 + if delete: return await self.base.delete() @@ -202,12 +208,6 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: except HTTPException: pass - sent_emoji, _ = await self.ctx.bot.retrieve_emoji() - try: - await self.ctx.message.add_reaction(sent_emoji) - except (HTTPException, InvalidArgument): - pass - async def first_page(self) -> None: """ Go to the first page. @@ -391,7 +391,11 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: """ self.running = False - self.ctx.bot.loop.create_task(self.ctx.message.add_reaction("✅")) + sent_emoji, _ = await self.ctx.bot.retrieve_emoji() + try: + await self.ctx.message.add_reaction(sent_emoji) + except (HTTPException, InvalidArgument): + pass if delete: return await self.base.delete() From 2cb9fdcb296bb20a7c8a273b19fd94db40b59c89 Mon Sep 17 00:00:00 2001 From: Kyber Date: Fri, 2 Aug 2019 18:53:49 +1000 Subject: [PATCH 0359/1221] Remove sponsor --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 5776675b28..d887601646 100644 --- a/README.md +++ b/README.md @@ -99,12 +99,6 @@ Sepcial thanks to our sponsors for supporting the project. - - - - - - Become a [sponsor](https://patreon.com/kyber). From bef1437930140dd1944e46bd1fee65abde05829c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 2 Aug 2019 21:41:34 -0600 Subject: [PATCH 0360/1221] Paginator change --- CHANGELOG.md | 3 +- cogs/modmail.py | 13 ++- cogs/plugins.py | 6 +- cogs/utility.py | 24 ++-- core/paginator.py | 274 ++++++++++------------------------------------ 5 files changed, 81 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f904043075..da6784d12c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Use discord tasks for metadata loop. - More debug based logging. - Reduce redundancies in `?perms` sub commands. - +- paginator been split into `EmbedPaginatorSession` and `MessagePaginatorSession`, both subclassing `PaginatorSession`. + # v3.0.3 ### Added diff --git a/cogs/modmail.py b/cogs/modmail.py index 081385d202..c3750d7900 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -15,7 +15,7 @@ from core import checks from core.decorators import trigger_typing from core.models import PermissionLevel -from core.paginator import PaginatorSession +from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta from core.utils import format_preview, User, create_not_found_embed, format_description @@ -154,7 +154,7 @@ async def snippet(self, ctx, *, name: str.lower = None): embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) embeds.append(embed) - session = PaginatorSession(ctx, *embeds) + session = EmbedPaginatorSession(ctx, *embeds) await session.run() @snippet.command(name="raw") @@ -631,7 +631,7 @@ async def logs(self, ctx, *, user: User = None): embeds = self.format_log_embeds(logs, avatar_url=icon_url) - session = PaginatorSession(ctx, *embeds) + session = EmbedPaginatorSession(ctx, *embeds) await session.run() @logs.command(name="closed-by", aliases=["closeby"]) @@ -664,7 +664,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): ) return await ctx.send(embed=embed) - session = PaginatorSession(ctx, *embeds) + session = EmbedPaginatorSession(ctx, *embeds) await session.run() @logs.command(name="search", aliases=["find"]) @@ -697,7 +697,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): ) return await ctx.send(embed=embed) - session = PaginatorSession(ctx, *embeds) + session = EmbedPaginatorSession(ctx, *embeds) await session.run() @commands.command() @@ -893,7 +893,8 @@ async def blocked(self, ctx): else: embeds[-1].description = "Currently there are no blocked users." - await PaginatorSession(ctx, *embeds).run() + session = EmbedPaginatorSession(ctx, *embeds) + await session.run() @blocked.command(name="whitelist") @checks.has_permissions(PermissionLevel.MODERATOR) diff --git a/cogs/plugins.py b/cogs/plugins.py index ebb9a72241..f871209bb6 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -17,7 +17,7 @@ from core import checks from core.models import PermissionLevel -from core.paginator import PaginatorSession +from core.paginator import EmbedPaginatorSession logger = logging.getLogger("Modmail") @@ -465,7 +465,7 @@ async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = No embeds.append(embed) - paginator = PaginatorSession(ctx, *embeds) + paginator = EmbedPaginatorSession(ctx, *embeds) paginator.current = index await paginator.run() @@ -499,7 +499,7 @@ async def plugin_registry_compact(self, ctx): embed.set_author(name="Plugin Registry", icon_url=self.bot.user.avatar_url) embeds.append(embed) - paginator = PaginatorSession(ctx, *embeds) + paginator = EmbedPaginatorSession(ctx, *embeds) await paginator.run() diff --git a/cogs/utility.py b/cogs/utility.py index b1fe7aa7b3..f661cecb40 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -26,7 +26,7 @@ from core.changelog import Changelog from core.decorators import trigger_typing from core.models import InvalidConfigError, PermissionLevel -from core.paginator import PaginatorSession, MessagePaginatorSession +from core.paginator import EmbedPaginatorSession, MessagePaginatorSession from core.utils import ( cleanup_code, User, @@ -112,17 +112,17 @@ async def send_bot_help(self, mapping): if no_cog_commands: embeds.extend(await self.format_cog_help(no_cog_commands, no_cog=True)) - p_session = PaginatorSession( + session = EmbedPaginatorSession( self.context, *embeds, destination=self.get_destination() ) - return await p_session.run() + return await session.run() async def send_cog_help(self, cog): embeds = await self.format_cog_help(cog) - p_session = PaginatorSession( + session = EmbedPaginatorSession( self.context, *embeds, destination=self.get_destination() ) - return await p_session.run() + return await session.run() async def send_command_help(self, command): if not await self.filter_commands([command]): @@ -285,7 +285,7 @@ async def changelog(self, ctx, version: str.lower = ""): ) try: - paginator = PaginatorSession(ctx, *changelog.embeds) + paginator = EmbedPaginatorSession(ctx, *changelog.embeds) paginator.current = index await paginator.run() except asyncio.CancelledError: @@ -358,7 +358,7 @@ async def sponsors(self, ctx): random.shuffle(embeds) - session = PaginatorSession(ctx, *embeds) + session = EmbedPaginatorSession(ctx, *embeds) await session.run() @commands.group(invoke_without_command=True) @@ -762,7 +762,7 @@ async def config_options(self, ctx): ) embeds.append(embed) - session = PaginatorSession(ctx, *embeds) + session = EmbedPaginatorSession(ctx, *embeds) await session.run() @config.command(name="set", aliases=["add"]) @@ -926,7 +926,7 @@ def fmt(val): embed.set_thumbnail(url=fmt(info["thumbnail"])) embeds += [embed] - paginator = PaginatorSession(ctx, *embeds) + paginator = EmbedPaginatorSession(ctx, *embeds) paginator.current = index await paginator.run() @@ -1003,7 +1003,7 @@ async def alias(self, ctx, *, name: str.lower = None): embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) embeds.append(embed) - session = PaginatorSession(ctx, *embeds) + session = EmbedPaginatorSession(ctx, *embeds) await session.run() @alias.command(name="raw") @@ -1500,8 +1500,8 @@ async def permissions_get( for perm_level in PermissionLevel: embeds.append(self._get_perm(ctx, perm_level.name, "level")) - p_session = PaginatorSession(ctx, *embeds) - return await p_session.run() + session = EmbedPaginatorSession(ctx, *embeds) + return await session.run() @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) diff --git a/core/paginator.py b/core/paginator.py index a19db604b7..0a8aa8b814 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -8,7 +8,7 @@ class PaginatorSession: """ - Class that interactively paginates a list of `Embed`. + Class that interactively paginates something. Parameters ---------- @@ -16,11 +16,8 @@ class PaginatorSession: The context of the command. timeout : float How long to wait for before the session closes. - embeds : List[Embed] + pages : List[Any] A list of entries to paginate. - edit_footer : bool, optional - Whether to set the footer. - Defaults to `True`. Attributes ---------- @@ -28,7 +25,7 @@ class PaginatorSession: The context of the command. timeout : float How long to wait for before the session closes. - embeds : List[Embed] + pages : List[Any] A list of entries to paginate. running : bool Whether the paginate session is running. @@ -36,18 +33,17 @@ class PaginatorSession: The `Message` of the `Embed`. current : int The current page number. - reaction_map : Dict[str, meth] + reaction_map : Dict[str, method] A mapping for reaction to method. - """ - def __init__(self, ctx: commands.Context, *embeds, **options): + def __init__(self, ctx: commands.Context, *pages, **options): self.ctx = ctx self.timeout: int = options.get("timeout", 210) - self.embeds: typing.List[Embed] = list(embeds) self.running = False self.base: Message = None self.current = 0 + self.pages = list(pages) self.destination = options.get("destination", ctx) self.reaction_map = { "⏮": self.first_page, @@ -57,48 +53,31 @@ def __init__(self, ctx: commands.Context, *embeds, **options): "🛑": self.close, } - if options.get("edit_footer", True) and len(self.embeds) > 1: - for i, embed in enumerate(self.embeds): - footer_text = f"Page {i + 1} of {len(self.embeds)}" - if embed.footer.text: - 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: + def add_page(self, item) -> None: """ - Add a `Embed` page. - - Parameters - ---------- - embed : Embed - The `Embed` to add. + Add a page. """ - if isinstance(embed, Embed): - self.embeds.append(embed) - else: - raise TypeError("Page must be an Embed object.") + raise NotImplementedError - async def create_base(self, embed: Embed) -> None: + async def create_base(self, item) -> None: """ Create a base `Message`. - - Parameters - ---------- - embed : Embed - The `Embed` to fill the base `Message`. """ - self.base = await self.destination.send(embed=embed) + await self._create_base(item) - if len(self.embeds) == 1: + if len(self.pages) == 1: self.running = False return self.running = True for reaction in self.reaction_map: - if len(self.embeds) == 2 and reaction in "⏮⏭": + if len(self.pages) == 2 and reaction in "⏮⏭": continue await self.base.add_reaction(reaction) + async def _create_base(self, item) -> None: + raise NotImplementedError + async def show_page(self, index: int) -> None: """ Show a page by page number. @@ -108,17 +87,20 @@ async def show_page(self, index: int) -> None: index : int The index of the page. """ - if not 0 <= index < len(self.embeds): + if not 0 <= index < len(self.pages): return self.current = index - page = self.embeds[index] + page = self.pages[index] if self.running: - await self.base.edit(embed=page) + return await self._show_page(page) else: await self.create_base(page) + async def _show_page(self, page): + raise NotImplementedError + def react_check(self, reaction: Reaction, user: User) -> bool: """ @@ -218,201 +200,59 @@ async def last_page(self) -> None: """ Go to the last page. """ - await self.show_page(len(self.embeds) - 1) + await self.show_page(len(self.pages) - 1) -class MessagePaginatorSession: +class EmbedPaginatorSession(PaginatorSession): + def __init__(self, ctx: commands.Context, *embeds, **options): + super().__init__(ctx, *embeds, **options) - # TODO: Subclass MessagePaginatorSession from PaginatorSession + if len(self.pages) > 1: + for i, embed in enumerate(self.pages): + footer_text = f"Page {i + 1} of {len(self.pages)}" + if embed.footer.text: + 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) + 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 _show_page(self, page): + await self.base.edit(embed=page) + + +class MessagePaginatorSession(PaginatorSession): def __init__( self, ctx: commands.Context, *messages, embed: Embed = None, **options ): - self.ctx = ctx - self.timeout: int = options.get("timeout", 180) - self.messages: typing.List[str] = list(messages) - - self.running = False - self.base: Message = None self.embed = embed - if embed is not None: - self.footer_text = self.embed.footer.text - else: - self.footer_text = None - - self.current = 0 - self.reaction_map = { - "⏮": self.first_page, - "◀": self.previous_page, - "▶": self.next_page, - "⏭": self.last_page, - "🛑": self.close, - } + 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: - """ - Add a message page. - - Parameters - ---------- - msg : str - The message to add. - """ if isinstance(msg, str): - self.messages.append(msg) + self.pages.append(msg) else: raise TypeError("Page must be a str object.") - async def create_base(self, msg: str) -> None: - """ - Create a base `Message`. - - Parameters - ---------- - msg : str - The message content to fill the base `Message`. - """ + def _set_footer(self): if self.embed is not None: - footer_text = f"Page {self.current+1} of {len(self.messages)}" + footer_text = f"Page {self.current+1} of {len(self.pages)}" if self.footer_text: 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: + self._set_footer() self.base = await self.ctx.send(content=msg, embed=self.embed) - if len(self.messages) == 1: - self.running = False - return - - self.running = True - for reaction in self.reaction_map: - if len(self.messages) == 2 and reaction in "⏮⏭": - continue - await self.base.add_reaction(reaction) - - async def show_page(self, index: int) -> None: - """ - Show a page by page number. - - Parameters - ---------- - index : int - The index of the page. - """ - if not 0 <= index < len(self.messages): - return - - self.current = index - page = self.messages[index] - - if self.embed is not None: - footer_text = f"Page {self.current + 1} of {len(self.messages)}" - if self.footer_text: - footer_text = footer_text + " • " + self.footer_text - self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url) - - if self.running: - await self.base.edit(content=page, embed=self.embed) - else: - await self.create_base(page) - - def react_check(self, reaction: Reaction, user: User) -> bool: - """ - - Parameters - ---------- - reaction : Reaction - The `Reaction` object of the reaction. - user : User - The `User` or `Member` object of who sent the reaction. - - Returns - ------- - bool - """ - return ( - reaction.message.id == self.base.id - and user.id == self.ctx.author.id - and reaction.emoji in self.reaction_map.keys() - ) - - async def run(self) -> typing.Optional[Message]: - """ - Starts the pagination session. - - Returns - ------- - Optional[Message] - If it's closed before running ends. - """ - if not self.running: - await self.show_page(self.current) - while self.running: - try: - reaction, user = await self.ctx.bot.wait_for( - "reaction_add", check=self.react_check, timeout=self.timeout - ) - except asyncio.TimeoutError: - return await self.close(delete=False) - else: - action = self.reaction_map.get(reaction.emoji) - await action() - try: - await self.base.remove_reaction(reaction, user) - except (HTTPException, InvalidArgument): - pass - - async def previous_page(self) -> None: - """ - Go to the previous page. - """ - await self.show_page(self.current - 1) - - async def next_page(self) -> None: - """ - Go to the next page. - """ - await self.show_page(self.current + 1) - - async def close(self, delete: bool = True) -> typing.Optional[Message]: - """ - Closes the pagination session. - - Parameters - ---------- - delete : bool, optional - Whether or delete the message upon closure. - Defaults to `True`. - - Returns - ------- - Optional[Message] - If `delete` is `True`. - """ - self.running = False - - sent_emoji, _ = await self.ctx.bot.retrieve_emoji() - try: - await self.ctx.message.add_reaction(sent_emoji) - except (HTTPException, InvalidArgument): - pass - - if delete: - return await self.base.delete() - - try: - await self.base.clear_reactions() - except HTTPException: - pass - - async def first_page(self) -> None: - """ - Go to the first page. - """ - await self.show_page(0) - - async def last_page(self) -> None: - """ - Go to the last page. - """ - await self.show_page(len(self.messages) - 1) + async def _show_page(self, page) -> None: + self._set_footer() + await self.base.edit(content=page, embed=self.embed) From e0d6c07828b992e59a104e90f73f6470492dd437 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 5 Aug 2019 09:25:55 -0600 Subject: [PATCH 0361/1221] Add pull.yml --- .github/pull.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/pull.yml diff --git a/.github/pull.yml b/.github/pull.yml new file mode 100644 index 0000000000..2fec8bbda0 --- /dev/null +++ b/.github/pull.yml @@ -0,0 +1,8 @@ +version: "1" +rules: + - base: master + upstream: kyb3r:master + mergeMethod: hardreset + - base: development + upstream: kyb3r:development + mergeMethod: hardreset \ No newline at end of file From c11593d184a529b9d732ae9cc0f1b7d221698c8f Mon Sep 17 00:00:00 2001 From: Agent-Fennec <12753219+Agent-Fennec@users.noreply.github.com> Date: Tue, 6 Aug 2019 02:42:07 -0400 Subject: [PATCH 0362/1221] A few style fixes and customisations (#342) --- README.md | 50 ++++++++++++++++++++++++++++++++++---------------- core/thread.py | 11 ++++++++--- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d887601646..afc5f1f51c 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,39 @@ A feature rich Modmail bot for Discord.

- + - + - - Bot instances + + + Bot instances + - Support + Support - + - Python 3.7 + Python 3.7 + + + + Made with Python 3.7 - + + + + + + + + + + + + + MIT License @@ -33,7 +51,7 @@ Modmail is similar to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. ## How does it work? -When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. +When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. All threads are logged and you can view previous threads through the corresponding generated log link. [**Example**](https://logs.modmail.tk/example) @@ -61,7 +79,7 @@ This list is ever growing thanks to active development and our exceptional contr ## Installation -### Locally +### Locally Installation locally for development reasons or otherwise is as follows, you will need `python 3.7`. Clone the repo @@ -75,7 +93,7 @@ Install dependancies $ pipenv install ``` -Rename the `.env.example` to `.env` and fill out the fields. +Rename the `.env.example` to `.env` and fill out the fields. And finally, run the bot. ```console $ pipenv run python3 bot.py @@ -83,7 +101,7 @@ $ pipenv run python3 bot.py ### Hosting for patrons -If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! +If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintainance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! ### Heroku This bot can be hosted on heroku. Installation via Heroku is done in your web browser. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. @@ -95,8 +113,8 @@ You can also set up autoupdates. To do this, [install the Pull app in your fork] Sepcial thanks to our sponsors for supporting the project. - - + + Become a [sponsor](https://patreon.com/kyber). @@ -108,6 +126,6 @@ Modmail supports the use of third party plugins to extend or add functionality t ## Contributing -Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. -This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, here's the link for our **[Patreon](https://www.patreon.com/kyber)**. +This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, here's the link for our **[Patreon](https://www.patreon.com/kyber)**. diff --git a/core/thread.py b/core/thread.py index b307a0dc24..6590d93fb8 100644 --- a/core/thread.py +++ b/core/thread.py @@ -316,7 +316,9 @@ async def _close( "{closer.mention} has closed this Modmail thread.", ) - message = message.format(closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None) + message = message.format( + closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None + ) embed.description = message footer = self.bot.config.get( @@ -736,9 +738,12 @@ async def find( if recipient is None and channel is not None: thread = self._find_from_channel(channel) if thread is None: - id, thread = next(((k, v) for k, v in self.cache.items() if v.channel == channel), (-1, None)) + id, thread = next( + ((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.') + logger.debug("Found thread with tempered ID.") await channel.edit(topic=f"User ID: {id}") return thread From 56cef8dd7b16d5f1d7e8b542aeb036a780516640 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 6 Aug 2019 00:35:17 -0700 Subject: [PATCH 0363/1221] Formatting and changelog change --- CHANGELOG.md | 3 + bot.py | 111 ++++++++++++++--------------- cogs/modmail.py | 48 ++++++++++--- cogs/plugins.py | 6 +- cogs/utility.py | 161 ++++++++++++++++++++++++++---------------- core/checks.py | 10 +++ core/clients.py | 12 ++-- core/config_help.json | 4 +- core/paginator.py | 1 - core/thread.py | 10 +-- 10 files changed, 220 insertions(+), 146 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da6784d12c..22dc37c980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,9 @@ however, insignificant breaking changes does not guarantee a major version bump, - The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/. - Removed unclear `rm` alias for some `remove` commands. - Paginate `?config options`. +- All users configured with a permission level greater than REGULAR has access to the main Modmail category. + - Category overrides also changes when a level is removed or added to a user or role. +- `@everyone` is now accepted for `?perms add`. ### Fixes diff --git a/bot.py b/bot.py index 6031fd3574..46f1081e3b 100644 --- a/bot.py +++ b/bot.py @@ -22,6 +22,7 @@ from pymongo.errors import ConfigurationError try: + # noinspection PyUnresolvedReferences from colorama import init init() @@ -94,7 +95,17 @@ def __init__(self): sys.exit(0) self.plugin_db = PluginDatabaseClient(self) + + logger.line() + logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") + logger.info("││││ │ │││││├─┤││") + logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") + logger.info("v%s", __version__) + logger.info("Authors: kyb3r, fourjr, Taaku18") + logger.line() + self._load_extensions() + logger.line() @property def uptime(self) -> str: @@ -168,14 +179,6 @@ async def get_prefix(self, message=None): def _load_extensions(self): """Adds commands automatically""" - logger.line() - logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") - logger.info("││││ │ │││││├─┤││") - logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") - logger.info("v%s", __version__) - logger.info("Authors: kyb3r, fourjr, Taaku18") - logger.line() - for file in os.listdir("cogs"): if not file.endswith(".py"): continue @@ -232,9 +235,12 @@ async def is_owner(self, user: discord.User) -> bool: def log_channel(self) -> typing.Optional[discord.TextChannel]: channel_id = self.config["log_channel_id"] if channel_id is not None: - channel = self.get_channel(int(channel_id)) - if channel is not None: - return channel + try: + channel = self.get_channel(int(channel_id)) + if channel is not None: + return channel + except ValueError: + pass logger.debug("LOG_CHANNEL_ID was invalid, removed.") self.config.remove("log_channel_id") if self.main_category is not None: @@ -255,10 +261,6 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: ) return None - @property - def is_connected(self) -> bool: - return self._connected.is_set() - async def wait_for_connected(self) -> None: await self.wait_until_ready() await self._connected.wait() @@ -277,7 +279,7 @@ def token(self) -> str: token = self.config["token"] if token is None: logger.critical( - "TOKEN must be set, set this as bot token found on the Discord Dev Portal." + "TOKEN must be set, set this as bot token found on the Discord Developer Portal." ) sys.exit(0) return token @@ -289,6 +291,7 @@ def guild_id(self) -> typing.Optional[int]: try: return int(str(guild_id)) except ValueError: + self.config.remove("guild_id") logger.critical("Invalid GUILD_ID set.") return None @@ -309,9 +312,12 @@ def modmail_guild(self) -> typing.Optional[discord.Guild]: modmail_guild_id = self.config["modmail_guild_id"] if modmail_guild_id is None: return self.guild - guild = discord.utils.get(self.guilds, id=int(modmail_guild_id)) - if guild is not None: - return guild + try: + guild = discord.utils.get(self.guilds, id=int(modmail_guild_id)) + if guild is not None: + return guild + except ValueError: + pass self.config.remove("modmail_guild_id") logger.critical("Invalid MODMAIL_GUILD_ID set.") return self.guild @@ -325,11 +331,14 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]: if self.modmail_guild is not None: category_id = self.config["main_category_id"] if category_id is not None: - cat = discord.utils.get( - self.modmail_guild.categories, id=int(category_id) - ) - if cat is not None: - return cat + try: + cat = discord.utils.get( + self.modmail_guild.categories, id=int(category_id) + ) + if cat is not None: + return cat + except ValueError: + pass self.config.remove("main_category_id") logger.debug("MAIN_CATEGORY_ID was invalid, removed.") cat = discord.utils.get(self.modmail_guild.categories, name="Modmail") @@ -353,41 +362,34 @@ def blocked_whitelisted_users(self) -> typing.List[str]: def prefix(self) -> str: return str(self.config["prefix"]) - @property - def mod_color(self) -> int: - color = self.config["mod_color"] + def _parse_color(self, conf_name): + color = self.config[conf_name] try: return int(color.lstrip("#"), base=16) except ValueError: - logger.error("Invalid mod_color provided.") - return int(self.config.remove("mod_color").lstrip("#"), base=16) + logger.error("Invalid %s provided.", conf_name) + return int(self.config.remove(conf_name).lstrip("#"), base=16) + + @property + def mod_color(self) -> int: + return self._parse_color("mod_color") @property def recipient_color(self) -> int: - color = self.config["recipient_color"] - try: - return int(color.lstrip("#"), base=16) - except ValueError: - logger.error("Invalid recipient_color provided.") - return int(self.config.remove("recipient_color").lstrip("#"), base=16) + return self._parse_color("recipient_color") @property def main_color(self) -> int: - color = self.config["main_color"] - try: - return int(color.lstrip("#"), base=16) - except ValueError: - logger.error("Invalid main_color provided.") - return int(self.config.remove("main_color").lstrip("#"), base=16) + return self._parse_color("main_color") async def on_connect(self): logger.line() try: await self.validate_database_connection() except Exception: + logger.debug("Logging out due to failed database connection.") return await self.logout() - logger.line() logger.info("Connected to gateway.") await self.config.refresh() await self.setup_indexes() @@ -436,6 +438,8 @@ async def on_ready(self): logger.info("Prefix: %s", self.prefix) logger.info("Guild Name: %s", self.guild.name) logger.info("Guild ID: %s", self.guild.id) + if self.using_multiple_server_setup: + logger.info("Receiving guild ID: %s", self.modmail_guild.id) logger.line() await self.threads.populate_cache() @@ -508,6 +512,7 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: except commands.BadArgument: logger.warning("Removed sent emoji (%s).", sent_emoji) sent_emoji = self.config.remove("sent_emoji") + await self.config.update() if blocked_emoji != "disable": try: @@ -515,8 +520,8 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: except commands.BadArgument: logger.warning("Removed blocked emoji (%s).", blocked_emoji) blocked_emoji = self.config.remove("blocked_emoji") + await self.config.update() - await self.config.update() return sent_emoji, blocked_emoji async def _process_blocked(self, message: discord.Message) -> bool: @@ -531,7 +536,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: try: await message.add_reaction(sent_emoji) except (discord.HTTPException, discord.InvalidArgument): - pass + logger.warning("Failed to add sent_emoji.", exc_info=True) return False @@ -761,6 +766,7 @@ async def get_context(self, message, *, cls=commands.Context): async def update_perms( self, name: typing.Union[PermissionLevel, str], value: int, add: bool = True ) -> None: + value = int(value) if isinstance(name, PermissionLevel): permissions = self.config["level_permissions"] name = name.name @@ -1043,19 +1049,6 @@ async def on_command_error(self, context, exception): else: logger.error("Unexpected exception:", exc_info=exception) - @staticmethod - def overwrites(ctx: commands.Context) -> dict: - """Permission overwrites for the guild.""" - overwrites = { - ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), - ctx.guild.me: discord.PermissionOverwrite(read_messages=True), - } - - for role in ctx.guild.roles: - if role.permissions.administrator: - overwrites[role] = discord.PermissionOverwrite(read_messages=True) - return overwrites - async def validate_database_connection(self): try: await self.db.command("buildinfo") @@ -1113,12 +1106,14 @@ async def before_post_metadata(self): if not self.guild: self.metadata_loop.cancel() - async def after_post_metadata(self): + @staticmethod + async def after_post_metadata(): logger.info("Metadata loop has been cancelled.") if __name__ == "__main__": try: + # noinspection PyUnresolvedReferences import uvloop logger.debug("Setting up with uvloop.") diff --git a/cogs/modmail.py b/cogs/modmail.py index c3750d7900..486ddb303b 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -3,7 +3,7 @@ from datetime import datetime from itertools import zip_longest from typing import Optional, Union -from types import SimpleNamespace as param +from types import SimpleNamespace import discord from discord.ext import commands @@ -39,6 +39,11 @@ async def setup(self, ctx): once after configuring Modmail. """ + if ctx.guild != self.bot.modmail_guild: + return await ctx.send( + f"You can only setup in the Modmail guild: {self.bot.modmail_guild}." + ) + if self.bot.main_category is not None: logger.debug("Can't re-setup server, main_category is found.") return await ctx.send(f"{self.bot.modmail_guild} is already set up.") @@ -51,8 +56,31 @@ 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.me: discord.PermissionOverwrite(read_messages=True), + } + + for level in PermissionLevel: + if level <= PermissionLevel.REGULAR: + continue + permissions = self.bot.config["level_permissions"].get(level.name, []) + for perm in permissions: + perm = int(perm) + if perm == -1: + key = self.bot.modmail_guild.default_role + else: + key = self.bot.modmail_guild.get_member(perm) + if key is None: + key = self.bot.modmail_guild.get_role(perm) + if key is not None: + logger.info("Granting %s access to Modmail category.", key.name) + overwrites[key] = discord.PermissionOverwrite(read_messages=True) + category = await self.bot.modmail_guild.create_category( - name="Modmail", overwrites=self.bot.overwrites(ctx) + name="Modmail", overwrites=overwrites ) await category.edit(position=0) @@ -86,10 +114,12 @@ 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" - f"Type `{self.bot.prefix}permissions` for more info." + "**Successfully set up server.**\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." ) if ( @@ -612,7 +642,7 @@ async def logs(self, ctx, *, user: User = None): if not user: thread = ctx.thread if not thread: - raise commands.MissingRequiredArgument(param(name="member")) + raise commands.MissingRequiredArgument(SimpleNamespace(name="member")) user = thread.recipient default_avatar = "https://cdn.discordapp.com/embed/avatars/0.png" @@ -977,7 +1007,7 @@ async def block( if thread: user = thread.recipient elif after is None: - raise commands.MissingRequiredArgument(param(name="user")) + raise commands.MissingRequiredArgument(SimpleNamespace(name="user")) else: raise commands.BadArgument(f'User "{after.arg}" not found') @@ -1058,7 +1088,7 @@ async def unblock(self, ctx, *, user: User = None): if thread: user = thread.recipient else: - raise commands.MissingRequiredArgument(param(name="user")) + raise commands.MissingRequiredArgument(SimpleNamespace(name="user")) mention = getattr(user, "mention", f"`{user.id}`") name = getattr(user, "name", f"`{user.id}`") diff --git a/cogs/plugins.py b/cogs/plugins.py index f871209bb6..4ea76503be 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -62,9 +62,9 @@ def parse_plugin(name): if "@" in result[2]: # branch is specified # for example, fourjr/modmail-plugins/welcomer@develop is a valid name - branchsplit_result = result[2].split("@") - result.append(branchsplit_result[-1]) - result[2] = "@".join(branchsplit_result[:-1]) + branch_split_result = result[2].split("@") + result.append(branch_split_result[-1]) + result[2] = "@".join(branch_split_result[:-1]) else: result.append("master") diff --git a/cogs/utility.py b/cogs/utility.py index f661cecb40..188da898bf 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -11,12 +11,12 @@ from itertools import zip_longest, takewhile from json import JSONDecodeError, loads from textwrap import indent -from types import SimpleNamespace as param +from types import SimpleNamespace from typing import Union from discord import Embed, Color, Activity, Role from discord.enums import ActivityType, Status -from discord.ext import commands +from discord.ext import commands, tasks from discord.utils import escape_markdown, escape_mentions from aiohttp import ClientResponseError @@ -27,14 +27,7 @@ from core.decorators import trigger_typing from core.models import InvalidConfigError, PermissionLevel from core.paginator import EmbedPaginatorSession, MessagePaginatorSession -from core.utils import ( - cleanup_code, - User, - get_perm_level, - create_not_found_embed, - parse_alias, - format_description, -) +from core import utils logger = logging.getLogger("Modmail") @@ -46,9 +39,11 @@ async def format_cog_help(self, cog, *, no_cog=False): formats = [""] for cmd in await self.filter_commands( - cog.get_commands() if not no_cog else cog, sort=True, key=get_perm_level + cog.get_commands() if not no_cog else cog, + sort=True, + key=utils.get_perm_level, ): - perm_level = get_perm_level(cmd) + perm_level = utils.get_perm_level(cmd) if perm_level is PermissionLevel.INVALID: format_ = f"`{prefix + cmd.qualified_name}` " else: @@ -127,7 +122,7 @@ async def send_cog_help(self, cog): async def send_command_help(self, command): if not await self.filter_commands([command]): return - perm_level = get_perm_level(command) + perm_level = utils.get_perm_level(command) if perm_level is not PermissionLevel.INVALID: perm_level = f"{perm_level.name} [{perm_level}]" else: @@ -145,7 +140,7 @@ async def send_group_help(self, group): if not await self.filter_commands([group]): return - perm_level = get_perm_level(group) + perm_level = utils.get_perm_level(group) if perm_level is not PermissionLevel.INVALID: perm_level = f"{perm_level.name} [{perm_level}]" else: @@ -192,7 +187,7 @@ async def send_error_message(self, error): val = self.context.bot.aliases.get(command) if val is not None: - values = parse_alias(val) + values = utils.parse_alias(val) if len(values) == 1: embed = Embed( @@ -246,22 +241,14 @@ def __init__(self, bot): self.bot = bot self._original_help_command = bot.help_command self.bot.help_command = ModmailHelpCommand( - verify_checks=False, command_attrs={"help": "Shows this help message."} + verify_checks=False, + command_attrs={ + "help": "Shows this help message.", + "checks": [checks.has_permissions_help(PermissionLevel.REGULAR)], + }, ) - # Looks a bit ugly - self.bot.help_command._command_impl = checks.has_permissions( # pylint: disable=protected-access - PermissionLevel.REGULAR - )( - self.bot.help_command._command_impl # pylint: disable=protected-access - ) - self.bot.help_command.cog = self - - # Class Variables - self.presence = None - - # Tasks - self.presence_task = self.bot.loop.create_task(self.loop_presence()) + self.loop_presence.start() def cog_unload(self): self.bot.help_command = self._original_help_command @@ -512,7 +499,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): return await ctx.send(embed=embed) if not message: - raise commands.MissingRequiredArgument(param(name="message")) + raise commands.MissingRequiredArgument(SimpleNamespace(name="message")) activity, msg = ( await self.set_presence( @@ -522,7 +509,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): ) )["activity"] if activity is None: - raise commands.MissingRequiredArgument(param(name="activity")) + raise commands.MissingRequiredArgument(SimpleNamespace(name="activity")) self.bot.config["activity_type"] = activity.type.value self.bot.config["activity_message"] = message @@ -561,7 +548,7 @@ async def status(self, ctx, *, status_type: str.lower): await self.set_presence(status_identifier=status_type, status_by_key=True) )["status"] if status is None: - raise commands.MissingRequiredArgument(param(name="status")) + raise commands.MissingRequiredArgument(SimpleNamespace(name="status")) self.bot.config["status"] = status.value await self.bot.config.update() @@ -631,7 +618,7 @@ async def set_presence( activity = Activity(type=activity_type, name=activity_message, url=url) else: msg = "You must supply an activity message to use custom activity." - logger.warning(msg) + logger.debug(msg) await self.bot.change_presence(activity=activity, status=status) @@ -649,19 +636,18 @@ async def set_presence( presence["status"] = (status, msg) return presence - @commands.Cog.listener() - async def on_ready(self): - # Wait until config cache is populated with stuff from db - await self.bot.wait_for_connected() - logger.info(self.presence["activity"][1]) - logger.info(self.presence["status"][1]) - + @tasks.loop(minutes=45) async def loop_presence(self): - """Set presence to the configured value every hour.""" + """Set presence to the configured value every 45 minutes.""" + presence = await self.set_presence() + logger.line() + logger.info(presence["activity"][1]) + logger.info(presence["status"][1]) + + @loop_presence.before_loop + async def before_loop_presence(self): + logger.info("Starting metadata loop.") await self.bot.wait_for_connected() - while not self.bot.is_closed(): - self.presence = await self.set_presence() - await asyncio.sleep(600) @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) @@ -867,11 +853,11 @@ async def config_get(self, ctx, key: str.lower = None): @config.command(name="help", aliases=["info"]) @checks.has_permissions(PermissionLevel.OWNER) - async def config_help(self, ctx, key: str.lower): + async def config_help(self, ctx, key: str.lower = None): """ Show information on a specified configuration. """ - if not ( + if key is not None and not ( key in self.bot.config.public_keys or key in self.bot.config.protected_keys ): embed = Embed( @@ -883,7 +869,7 @@ async def config_help(self, ctx, key: str.lower): config_help = self.bot.config.config_help - if key not in config_help: + if key is not None and key not in config_help: embed = Embed( title="Error", color=Color.red(), @@ -955,10 +941,12 @@ 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 = 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 = parse_alias(val) + values = utils.parse_alias(val) if not values: embed = Embed( @@ -998,7 +986,7 @@ async def alias(self, ctx, *, name: str.lower = None): embeds = [] for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): - description = format_description(i, names) + description = utils.format_description(i, names) embed = Embed(color=self.bot.main_color, description=description) embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) embeds.append(embed) @@ -1014,7 +1002,7 @@ async def alias_raw(self, ctx, *, name: str.lower): """ val = self.bot.aliases.get(name) if val is None: - embed = 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) return await ctx.send(escape_markdown(escape_mentions(val)).replace("<", "\\<")) @@ -1066,7 +1054,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): if embed is not None: return await ctx.send(embed=embed) - values = parse_alias(value) + values = utils.parse_alias(value) if not values: embed = Embed( @@ -1133,7 +1121,7 @@ async def alias_remove(self, ctx, *, name: str.lower): description=f"Successfully deleted `{name}`.", ) else: - embed = 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) @@ -1144,10 +1132,10 @@ async def alias_edit(self, ctx, name: str.lower, *, value): Edit an alias. """ if name not in self.bot.aliases: - embed = 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 = parse_alias(value) + values = utils.parse_alias(value) if not values: embed = Embed( @@ -1222,10 +1210,13 @@ async def permissions(self, ctx): @staticmethod def _verify_user_or_role(user_or_role): + if isinstance(user_or_role, Role): + if user_or_role.is_default(): + return -1 + elif user_or_role in {"everyone", "all"}: + return -1 if hasattr(user_or_role, "id"): return user_or_role.id - if user_or_role in {"everyone", "all"}: - return -1 raise commands.BadArgument(f'User or Role "{user_or_role}" not found') @staticmethod @@ -1247,7 +1238,12 @@ def _parse_level(name): @permissions.command(name="add", usage="[command/level] [name] [user_or_role]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add( - self, ctx, type_: str.lower, name: str, *, user_or_role: Union[Role, User, str] + self, + ctx, + type_: str.lower, + name: str, + *, + user_or_role: Union[Role, utils.User, str], ): """ Add a permission to a command or a permission level. @@ -1290,6 +1286,18 @@ async def permissions_add( else: await self.bot.update_perms(level, value) name = level.name + if level > PermissionLevel.REGULAR: + if value == -1: + key = self.bot.modmail_guild.default_role + elif isinstance(user_or_role, Role): + key = user_or_role + else: + 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 + ) embed = Embed( title="Success", @@ -1305,7 +1313,12 @@ async def permissions_add( ) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove( - self, ctx, type_: str.lower, name: str, *, user_or_role: Union[Role, User, str] + self, + ctx, + type_: str.lower, + name: str, + *, + user_or_role: Union[Role, utils.User, str], ): """ Remove permission to use a command or permission level. @@ -1341,6 +1354,30 @@ async def permissions_remove( value = self._verify_user_or_role(user_or_role) await self.bot.update_perms(level or name, value, add=False) + if type_ == "level": + if level > PermissionLevel.REGULAR: + if value == -1: + logger.info("Denying @everyone access to Modmail category.") + await self.bot.main_category.set_permissions( + self.bot.modmail_guild.default_role, read_messages=False + ) + elif isinstance(user_or_role, 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 + ) + 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 + ) + embed = Embed( title="Success", color=self.bot.main_color, @@ -1389,7 +1426,7 @@ def _get_perm(self, ctx, name, type_): @permissions.command(name="get", usage="[@user] or [command/level] [name]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get( - self, ctx, user_or_role: Union[Role, User, str], *, name: str = None + self, ctx, user_or_role: Union[Role, utils.User, str], *, name: str = None ): """ View the currently-set permissions. @@ -1514,7 +1551,7 @@ async def oauth(self, ctx): @oauth.command(name="whitelist") @checks.has_permissions(PermissionLevel.OWNER) - async def oauth_whitelist(self, ctx, target: Union[User, Role]): + async def oauth_whitelist(self, ctx, target: Union[Role, utils.User]): """ Whitelist or un-whitelist a user or role to have access to logs. @@ -1593,7 +1630,7 @@ async def eval_(self, ctx, *, body: str): env.update(globals()) - body = cleanup_code(body) + body = utils.cleanup_code(body) stdout = StringIO() to_compile = f'async def func():\n{indent(body, " ")}' diff --git a/core/checks.py b/core/checks.py index 503877b3a7..17d2b4353b 100644 --- a/core/checks.py +++ b/core/checks.py @@ -7,6 +7,16 @@ logger = logging.getLogger("Modmail") +def has_permissions_help(permission_level: PermissionLevel = PermissionLevel.REGULAR): + async def predicate(ctx): + return await check_permissions( + ctx, ctx.command.qualified_name, permission_level + ) + + predicate.permission_level = permission_level + return predicate + + def has_permissions(permission_level: PermissionLevel = PermissionLevel.REGULAR): """ A decorator that checks if the author has the required permissions. diff --git a/core/clients.py b/core/clients.py index 31a73be390..a8bb006e6d 100644 --- a/core/clients.py +++ b/core/clients.py @@ -99,9 +99,9 @@ async def get_log(self, channel_id: Union[str, int]) -> dict: async def get_log_link(self, channel_id: Union[str, int]) -> str: doc = await self.get_log(channel_id) logger.debug("Retrieving log link for channel %s.", channel_id) - prefix = self.bot.config['log_url_prefix'].strip('/') - if prefix == 'NONE': - prefix = '' + 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']}" async def create_log_entry( @@ -138,9 +138,9 @@ async def create_log_entry( } ) logger.debug("Created a log entry, key %s.", key) - prefix = self.bot.config['log_url_prefix'].strip('/') - if prefix == 'NONE': - prefix = '' + prefix = self.bot.config["log_url_prefix"].strip("/") + if prefix == "NONE": + prefix = "" return f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{key}" async def get_config(self) -> dict: diff --git a/core/config_help.json b/core/config_help.json index f4e8eae36f..be499bb230 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -281,7 +281,7 @@ "notes": [ "When `recipient_thread_close` is enabled and the recipient closed their own thread, `thread_self_close_response` is used instead of this configuration.", "You may use the `{{closer}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that closed the thread.", - "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. 3ifdm9204) of the log.", + "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. s3kf91a) of the log.", "Discord flavoured markdown is fully supported in `thread_close_response`.", "See also: `thread_close_title`, `thread_close_footer`, `thread_self_close_response`, `thread_creation_response`." ] @@ -295,7 +295,7 @@ "notes": [ "When `recipient_thread_close` is disabled or the thread wasn't closed by the recipient, `thread_close_response` is used instead of this configuration.", "You may use the `{{closer}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that closed the thread.", - "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. 3ifdm9204) of the log.", + "`{{loglink}}` can be used as a placeholder substitute for the full URL linked to the thread in the log viewer and `{{loglink}}` for the unique key (ie. s3kf91a) of the log.", "Discord flavoured markdown is fully supported in `thread_self_close_response`.", "See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`." ] diff --git a/core/paginator.py b/core/paginator.py index 0a8aa8b814..c2bed558ed 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -228,7 +228,6 @@ async def _show_page(self, page): class MessagePaginatorSession(PaginatorSession): - def __init__( self, ctx: commands.Context, *messages, embed: Embed = None, **options ): diff --git a/core/thread.py b/core/thread.py index 95eeadc3dd..00aa24c9c7 100644 --- a/core/thread.py +++ b/core/thread.py @@ -4,7 +4,7 @@ import string import typing from datetime import datetime, timedelta -from types import SimpleNamespace as param +from types import SimpleNamespace import isodate @@ -144,8 +144,8 @@ async def send_genesis_message(): msg = await channel.send(mention, embed=info_embed) self.bot.loop.create_task(msg.pin()) self.genesis_message = msg - except Exception as e: - logger.error(str(e)) + except Exception: + logger.error("Failed unexpectedly:", exc_info=True) finally: self.ready = True @@ -539,7 +539,7 @@ async def delete_message(self, message_id): async def note(self, message: discord.Message) -> None: if not message.content and not message.attachments: - raise MissingRequiredArgument(param(name="msg")) + raise MissingRequiredArgument(SimpleNamespace(name="msg")) _, msg = await asyncio.gather( self.bot.api.append_log(message, self.channel.id, type_="system"), @@ -550,7 +550,7 @@ async def note(self, message: discord.Message) -> None: async def reply(self, message: discord.Message, anonymous: bool = False) -> None: if not message.content and not message.attachments: - raise MissingRequiredArgument(param(name="msg")) + raise MissingRequiredArgument(SimpleNamespace(name="msg")) if not any(g.get_member(self.id) for g in self.bot.guilds): return await message.channel.send( embed=discord.Embed( From 2c046227970d145b2b01fe687b5d76c90646c85e Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 6 Aug 2019 22:21:35 -0700 Subject: [PATCH 0364/1221] Updated README.md --- README.md | 60 +++++++++++++++++++++++++---------------------- core/paginator.py | 2 +- core/utils.py | 2 +- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 514d0719ba..e9aabe9a76 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,6 @@ Made with Python 3.7 - - - - @@ -53,9 +49,10 @@ Modmail is similar to Reddit's Modmail both in functionality and purpose. It ser This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included! ## How does it work? -When a member sends a direct message to the bot, a channel or "thread" is created within an isolated category for that member. This channel is where messages will be relayed and where any available staff member can respond to that user. -All threads are logged and you can view previous threads through the corresponding generated log link. Here is an [**example**](https://logs.modmail.tk/example). +When a member sends a direct message to the bot, Modmail will create a channel or "thread" within an isolated category. All further DM messages will automatically relay to that channel, for any available staff can respond within the channel. + +All threads are logged and you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.modmail.tk/example). ## Features @@ -65,46 +62,51 @@ All threads are logged and you can view previous threads through the correspondi * Interface elements (color, responses, reactions, etc). * Snippets and *command aliases*. * Minimum duration for accounts to be created before allowed to contact Modmail (`account_age`). - * Minimum duration for member to be in the guild allowed to contact Modmail (`guild_age`). + * Minimum duration for members to be in the guild before allowed to contact Modmail (`guild_age`). + * **Advanced Logging Functionality:** - * When you close a thread, a [log link](https://logs.modmail.tk/example) is generated and posted to your log channel. + * When you close a thread, Modmail will generate a [log link](https://logs.modmail.tk/example) and post it to your log channel. * Native Discord dark-mode feel. * Markdown/formatting support. * Login via Discord to protect your logs ([premium Patreon feature](https://patreon.com/kyber)). * See past logs of a user with `?logs`. * Searchable by text queries using `?logs search`. + * **Robust implementation:** - * Scheduled tasks in human time, e.g. `?close in 2 hours silently`. - * Editing and deleting messages are synced on all channels. - * Support for the full range of message content (multiple images, files). + * Schedule tasks in human time, e.g. `?close in 2 hours silently`. + * Editing and deleting messages are synced. + * Support for the diverse range of message contents (multiple images, files). * Paginated commands interfaces via reactions. - + This list is ever-growing thanks to active development and our exceptional contributors. See a full list of documented commands by using the `?help` command. ## Installation -<<<<<<< HEAD +Where is the Modmail bot invite link? Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time): + ### Heroku -This bot can be hosted on Heroku. +This bot can be hosted on Heroku. -Installation via Heroku is possible with only your web browser. -The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) will guide you through the entire installation process. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. +Installation via Heroku is possible with your web browser alone. +The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. -You can also set up auto-update. To do this: +To configure automatic updates: + - Login to [GitHub](https://github.com/) and verify your account. - [Fork the repo](https://github.com/kyb3r/modmail/fork). - - [Install the Pull app for your fork](https://github.com/apps/pull). - - Then go to the Deploy tab in your Heroku account, select GitHub and connect your fork. + - Install the [Pull app](https://github.com/apps/pull) for your fork. + - Then go to the Deploy tab in your [Heroku account](https://dashboard.heroku.com/apps) of your bot app, select GitHub and connect your fork (usually by typing "Modmail"). - Turn on auto-deploy for the `master` branch. ### Hosting for patrons -If you don't want to go through the trouble of setting up your own bot, and want to support this project as well, we offer installation, hosting and maintenance for Modmail bots for [**Patrons**](https://patreon.com/kyber). Join the support server for more info! +If you don't want to go through the trouble of setting up your very own Modmail bot, and/or want to support this project, we offer the all inclusive installation, hosting and maintenance of your Modmail with [**Patron**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! ### Locally -Installation locally for development reasons or otherwise is as follows, you will need [`python 3.7`](https://www.python.org/downloads/). -Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying Heroku apps. If you run into any problems, join the [development server](https://discord.gg/etJNHCQ) for help and support. +Local hosting of Modmail is also possible, first you will need [`python 3.7`](https://www.python.org/downloads/). + +Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. Clone the repo: @@ -119,9 +121,9 @@ Install dependencies: $ pipenv install ``` -Rename the `.env.example` to `.env` and fill out the fields. If `.env.example` is hidden, create a text file named `.env` and copy the contents of [`.env.example`](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) then modify the values. +Rename the `.env.example` to `.env` and fill out the fields. If `.env.example` is nonexistent (hidden), create a text file named `.env` and copy the contents of [`.env.example`](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) then modify the values. -Finally, run the bot. +Finally, start Modmail. ```console $ pipenv run bot @@ -135,14 +137,16 @@ Special thanks to our sponsors for supporting the project. -Become a [sponsor](https://patreon.com/kyber). +Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins -Modmail supports the use of third-party plugins to extend or add functionality to the bot. This allows the introduction of niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third party plugins can be found using the `plugins registry` command. To develop your own, check out the [documentation](https://github.com/kyb3r/modmail/wiki/Plugins) for plugins. +Modmail supports the use of third-party plugins to extend or add functionalities to the bot. This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third-party plugins can be found using the `plugins registry` command. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). + +Plugins requests and support is available in our (Modmail Plugins Server)[https://discord.gg/4JE4XSW]. ## Contributing -Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. -This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, here's the link to our **[Patreon Page](https://www.patreon.com/kyber)**! \ No newline at end of file +If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! \ No newline at end of file diff --git a/core/paginator.py b/core/paginator.py index c2bed558ed..835337dbed 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -94,7 +94,7 @@ async def show_page(self, index: int) -> None: page = self.pages[index] if self.running: - return await self._show_page(page) + await self._show_page(page) else: await self.create_base(page) diff --git a/core/utils.py b/core/utils.py index 89f9b55f81..585c0fba3d 100644 --- a/core/utils.py +++ b/core/utils.py @@ -2,7 +2,7 @@ import shlex import typing from difflib import get_close_matches -from distutils.util import strtobool as _stb +from distutils.util import strtobool as _stb # pylint: disable=import-error from itertools import takewhile from urllib import parse From 6a7a60410b09650ae0c1c19e17208adf7e94210c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Aug 2019 10:28:54 -0700 Subject: [PATCH 0365/1221] Fix a blocked bug --- cogs/modmail.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 486ddb303b..7f2bb4ce56 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -906,22 +906,21 @@ async def blocked(self, ctx): pass if users: - embed = embeds[-1] + embed = embeds[0] for mention, reason in users: line = mention + f" - `{reason or 'No reason provided'}`\n" if len(embed.description) + len(line) > 2048: - embeds.append( - discord.Embed( + embed = discord.Embed( title="Blocked Users (Continued)", color=self.bot.main_color, description=line, ) - ) + embeds.append(embed) else: embed.description += line else: - embeds[-1].description = "Currently there are no blocked users." + embeds[0].description = "Currently there are no blocked users." session = EmbedPaginatorSession(ctx, *embeds) await session.run() From b85faf2a7d43b3f3cb826dc86941c63bf175f5dd Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 9 Aug 2019 08:15:19 -0700 Subject: [PATCH 0366/1221] Fixed MD --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e9aabe9a76..b43079e850 100644 --- a/README.md +++ b/README.md @@ -143,10 +143,10 @@ Become a sponsor on [Patreon](https://patreon.com/kyber). Modmail supports the use of third-party plugins to extend or add functionalities to the bot. This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third-party plugins can be found using the `plugins registry` command. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). -Plugins requests and support is available in our (Modmail Plugins Server)[https://discord.gg/4JE4XSW]. +Plugins requests and support is available in our [Modmail Plugins Server](https://discord.gg/4JE4XSW). ## Contributing Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. -If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! \ No newline at end of file +If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! From a99a0917cf5a8f4c3b0cdba9787f34cfc67ed65d Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 9 Aug 2019 08:15:19 -0700 Subject: [PATCH 0367/1221] Fix open issue on Windows --- README.md | 4 ++-- core/config.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e9aabe9a76..b43079e850 100644 --- a/README.md +++ b/README.md @@ -143,10 +143,10 @@ Become a sponsor on [Patreon](https://patreon.com/kyber). Modmail supports the use of third-party plugins to extend or add functionalities to the bot. This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third-party plugins can be found using the `plugins registry` command. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). -Plugins requests and support is available in our (Modmail Plugins Server)[https://discord.gg/4JE4XSW]. +Plugins requests and support is available in our [Modmail Plugins Server](https://discord.gg/4JE4XSW). ## Contributing Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. -If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! \ No newline at end of file +If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! diff --git a/core/config.py b/core/config.py index e7776cb39d..b682f32e04 100644 --- a/core/config.py +++ b/core/config.py @@ -153,7 +153,7 @@ def populate_cache(self) -> dict: config_help_json = os.path.join( os.path.dirname(os.path.abspath(__file__)), "config_help.json" ) - with open(config_help_json, "r") as f: + with open(config_help_json, "r", encoding="utf-8") as f: self.config_help = dict(sorted(json.load(f).items())) return self._cache From 2d89a854cdbb59dd3d02a77d69350ba7da8b50f5 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 9 Aug 2019 08:15:19 -0700 Subject: [PATCH 0368/1221] Bump version with fixes for windows issue --- CHANGELOG.md | 5 +++++ README.md | 4 ++-- bot.py | 2 +- core/config.py | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22dc37c980..fca34438db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). +# v3.1.1 + +### Fixed + +- An issue when reading `config_help.json` for Windows users due to an encoding problem. # v3.1.0 diff --git a/README.md b/README.md index e9aabe9a76..b43079e850 100644 --- a/README.md +++ b/README.md @@ -143,10 +143,10 @@ Become a sponsor on [Patreon](https://patreon.com/kyber). Modmail supports the use of third-party plugins to extend or add functionalities to the bot. This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third-party plugins can be found using the `plugins registry` command. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). -Plugins requests and support is available in our (Modmail Plugins Server)[https://discord.gg/4JE4XSW]. +Plugins requests and support is available in our [Modmail Plugins Server](https://discord.gg/4JE4XSW). ## Contributing Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. -If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! \ No newline at end of file +If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! diff --git a/bot.py b/bot.py index 46f1081e3b..50b8351474 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.1.0" +__version__ = "3.1.1" import asyncio import logging diff --git a/core/config.py b/core/config.py index e7776cb39d..b682f32e04 100644 --- a/core/config.py +++ b/core/config.py @@ -153,7 +153,7 @@ def populate_cache(self) -> dict: config_help_json = os.path.join( os.path.dirname(os.path.abspath(__file__)), "config_help.json" ) - with open(config_help_json, "r") as f: + with open(config_help_json, "r", encoding="utf-8") as f: self.config_help = dict(sorted(json.load(f).items())) return self._cache From e18af492fdaa34d09e3c93b1cf7369753940d899 Mon Sep 17 00:00:00 2001 From: Matrix <49132750+matrix2113@users.noreply.github.com> Date: Mon, 12 Aug 2019 19:30:46 -0400 Subject: [PATCH 0369/1221] Removed "update" --- app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.json b/app.json index e4f5032e81..76fe320a30 100644 --- a/app.json +++ b/app.json @@ -12,7 +12,7 @@ "required": true }, "OWNERS": { - "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval and update).", + "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", "required": true }, "MONGO_URI": { From 9d4fb27ef296e38318e37b1a3b3a12a747612bd3 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 17 Aug 2019 00:19:40 -1000 Subject: [PATCH 0370/1221] v3.2.0 dev --- CHANGELOG.md | 18 ++++ README.md | 6 +- bot.py | 51 ++++++++-- cogs/modmail.py | 8 +- cogs/utility.py | 230 +++++++++++++++++++++++++++++++++++------- core/changelog.py | 60 ++++++----- core/checks.py | 58 ++++------- core/config.py | 1 + core/config_help.json | 8 +- core/utils.py | 16 +-- 10 files changed, 331 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fca34438db..09998321ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,28 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). +# v3.2.0 + +### Added + +- Ability to change permission levels of individual commands. + - See `?permissions override` for more information. + +### Fixed + +- `?help `, will return `Perhaps you meant: `, now its fixed. + - For example, `?help add` used to return `Perhaps you meant: add`, now it wouldn't do this. +- Aliases and Permissions command names are always saved lowercase now. + +### Internal + +- Use regex to parse Changes, Added, Fixed, etc and description. + # v3.1.1 ### Fixed diff --git a/README.md b/README.md index b43079e850..46e41697e9 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,9 @@ This list is ever-growing thanks to active development and our exceptional contr ## Installation -Where is the Modmail bot invite link? Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time): +Where can I find the Modmail bot invite link? + +Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time)... ### Heroku @@ -104,7 +106,7 @@ If you don't want to go through the trouble of setting up your very own Modmail ### Locally -Local hosting of Modmail is also possible, first you will need [`python 3.7`](https://www.python.org/downloads/). +Local hosting of Modmail is also possible, first you will need [`Python 3.7`](https://www.python.org/downloads/). Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. diff --git a/bot.py b/bot.py index 50b8351474..3bb205c991 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.1.1" +__version__ = "3.2.0" import asyncio import logging @@ -382,6 +382,34 @@ def recipient_color(self) -> int: def main_color(self) -> int: return self._parse_color("main_color") + def command_perm(self, command_name: str) -> PermissionLevel: + level = self.config["override_command_level"].get(command_name) + if level is not None: + try: + return PermissionLevel[level.upper()] + except KeyError: + logger.warning( + "Invalid override_command_level for command %s.", command_name + ) + self.config["override_command_level"].pop(command_name) + + command = self.get_command(command_name) + if command is None: + logger.debug("Command %s not found.", command_name) + return PermissionLevel.INVALID + level = next( + ( + check.permission_level + for check in command.checks + if hasattr(check, "permission_level") + ), + None, + ) + if level is None: + logger.debug("Command %s does not have a permission level.", command_name) + return PermissionLevel.INVALID + return level + async def on_connect(self): logger.line() try: @@ -1039,12 +1067,23 @@ async def on_command_error(self, context, exception): await context.send_help(context.command) elif isinstance(exception, commands.CheckFailure): for check in context.command.checks: - if not await check(context) and hasattr(check, "fail_msg"): - await context.send( - embed=discord.Embed( - color=discord.Color.red(), description=check.fail_msg + if not await check(context): + if hasattr(check, "fail_msg"): + await context.send( + embed=discord.Embed( + color=discord.Color.red(), description=check.fail_msg + ) + ) + if hasattr(check, 'permission_level'): + corrected_permission_level = self.command_perm( + context.command.qualified_name + ) + logger.warning( + "User %s does not have permission to use this command: `%s` (%s).", + context.author.name, + context.command.qualified_name, + corrected_permission_level.name, ) - ) logger.warning("CheckFailure: %s", exception) else: logger.error("Unexpected exception:", exc_info=exception) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7f2bb4ce56..e3f57ffb7d 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -912,10 +912,10 @@ async def blocked(self, ctx): line = mention + f" - `{reason or 'No reason provided'}`\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title="Blocked Users (Continued)", - color=self.bot.main_color, - description=line, - ) + title="Blocked Users (Continued)", + color=self.bot.main_color, + description=line, + ) embeds.append(embed) else: embed.description += line diff --git a/cogs/utility.py b/cogs/utility.py index 188da898bf..00963b88b2 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -41,9 +41,9 @@ async def format_cog_help(self, cog, *, no_cog=False): for cmd in await self.filter_commands( cog.get_commands() if not no_cog else cog, sort=True, - key=utils.get_perm_level, + key=lambda c: bot.command_perm(c.qualified_name), ): - perm_level = utils.get_perm_level(cmd) + perm_level = bot.command_perm(cmd.qualified_name) if perm_level is PermissionLevel.INVALID: format_ = f"`{prefix + cmd.qualified_name}` " else: @@ -122,11 +122,11 @@ async def send_cog_help(self, cog): async def send_command_help(self, command): if not await self.filter_commands([command]): return - perm_level = utils.get_perm_level(command) + perm_level = self.context.bot.command_perm(command.qualified_name) if perm_level is not PermissionLevel.INVALID: perm_level = f"{perm_level.name} [{perm_level}]" else: - perm_level = "" + perm_level = "NONE" embed = Embed( title=f"`{self.get_command_signature(command)}`", @@ -140,11 +140,11 @@ async def send_group_help(self, group): if not await self.filter_commands([group]): return - perm_level = utils.get_perm_level(group) + perm_level = self.context.bot.command_perm(group.qualified_name) if perm_level is not PermissionLevel.INVALID: perm_level = f"{perm_level.name} [{perm_level}]" else: - perm_level = "" + perm_level = "NONE" embed = Embed( title=f"`{self.get_command_signature(group)}`", @@ -153,7 +153,7 @@ async def send_group_help(self, group): ) if perm_level: - embed.add_field(name="Permission level", value=perm_level, inline=False) + embed.add_field(name="Permission Level", value=perm_level, inline=False) format_ = "" length = len(group.commands) @@ -169,7 +169,7 @@ async def send_group_help(self, group): branch = "├─" format_ += f"`{branch} {command.name}` - {command.short_doc}\n" - embed.add_field(name="Sub Commands", value=format_[:1024], inline=False) + embed.add_field(name="Sub Command(s)", value=format_[:1024], inline=False) embed.set_footer( text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' "for more info on a command." @@ -218,7 +218,7 @@ async def send_error_message(self, error): for cmd in self.context.bot.walk_commands(): if not cmd.hidden: - choices.add(cmd.name) + choices.add(cmd.qualified_name) closest = get_close_matches(command, choices) if closest: @@ -244,7 +244,7 @@ def __init__(self, bot): verify_checks=False, command_attrs={ "help": "Shows this help message.", - "checks": [checks.has_permissions_help(PermissionLevel.REGULAR)], + "checks": [checks.has_permissions_predicate(PermissionLevel.REGULAR)], }, ) self.bot.help_command.cog = self @@ -271,14 +271,21 @@ async def changelog(self, ctx, version: str.lower = ""): ) ) + paginator = EmbedPaginatorSession(ctx, *changelog.embeds) try: - paginator = EmbedPaginatorSession(ctx, *changelog.embeds) paginator.current = index await paginator.run() except asyncio.CancelledError: pass except Exception: - await ctx.send(changelog.CHANGELOG_URL) + try: + await paginator.close() + except Exception: + pass + logger.warning("Failed to display changelog.", exc_info=True) + await ctx.send( + f"View the changelog here: {changelog.CHANGELOG_URL}#v{version[::2]}" + ) @commands.command(aliases=["bot", "info"]) @checks.has_permissions(PermissionLevel.REGULAR) @@ -640,14 +647,17 @@ async def set_presence( async def loop_presence(self): """Set presence to the configured value every 45 minutes.""" presence = await self.set_presence() - logger.line() - logger.info(presence["activity"][1]) - logger.info(presence["status"][1]) + logger.debug(f'{presence["activity"][1]} {presence["status"][1]}') @loop_presence.before_loop async def before_loop_presence(self): logger.info("Starting metadata loop.") await self.bot.wait_for_connected() + logger.line() + presence = await self.set_presence() + logger.info(presence["activity"][1]) + logger.info(presence["status"][1]) + await asyncio.sleep(2700) @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) @@ -1066,7 +1076,7 @@ async def alias_add(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) if len(values) == 1: - linked_command = values[0].split()[0] + linked_command = values[0].split()[0].lower() if not self.bot.get_command(linked_command): embed = Embed( title="Error", @@ -1147,7 +1157,7 @@ async def alias_edit(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) if len(values) == 1: - linked_command = values[0].split()[0] + linked_command = values[0].split()[0].lower() if not self.bot.get_command(linked_command): embed = Embed( title="Error", @@ -1203,6 +1213,9 @@ async def permissions(self, ctx): By default, owner is set to the absolute bot owner and regular is `@everyone`. + To set permissions, see `{prefix}help permissions add`; and to change permission level for specific + commands see `{prefix}help permissions override`. + Note: You will still have to manually give/take permission to the Modmail category to users/roles. """ @@ -1233,9 +1246,64 @@ def _parse_level(name): "4": PermissionLevel.ADMINISTRATOR, "5": PermissionLevel.OWNER, } - return transform.get(name) + return transform.get(name, PermissionLevel.INVALID) + + @permissions.command(name="override") + @checks.has_permissions(PermissionLevel.OWNER) + async def permissions_override( + self, ctx, command_name: str.lower, *, level_name: str + ): + """ + Change a permission level for a specific command. + + Examples: + - `{prefix}perms override reply administrator` + - `{prefix}perms override "plugin enabled" moderator` + + To undo a permission override, see `{prefix}help permissions remove`. + + Example: + - `{prefix}perms remove override reply` + - `{prefix}perms remove override plugin enabled` + + You can retrieve a single or all command level override(s), see`{prefix}help permissions get`. + """ + + command = self.bot.get_command(command_name) + if command is None: + embed = Embed( + title="Error", + color=Color.red(), + description=f"The referenced command does not exist: `{command_name}`.", + ) + return await ctx.send(embed=embed) + + level = self._parse_level(level_name) + if level is PermissionLevel.INVALID: + embed = Embed( + title="Error", + color=Color.red(), + description=f"The referenced level does not exist: `{level_name}`.", + ) + else: + logger.info( + "Updated command permission level for `%s` to `%s`.", + command.qualified_name, + level.name, + ) + self.bot.config["override_command_level"][ + command.qualified_name + ] = level.name + await self.bot.config.update() + embed = Embed( + title="Success", + color=self.bot.main_color, + description="Successfully set command permission level for " + f"`{command.qualified_name}` to `{level.name}`.", + ) + return await ctx.send(embed=embed) - @permissions.command(name="add", usage="[command/level] [name] [user_or_role]") + @permissions.command(name="add", usage="[command/level] [name] [user/role]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add( self, @@ -1265,11 +1333,12 @@ async def permissions_add( command = level = None if type_ == "command": - command = self.bot.get_command(name.lower()) + name = name.lower() + command = self.bot.get_command(name) check = command is not None else: level = self._parse_level(name) - check = level is not None + check = level is not PermissionLevel.INVALID if not check: embed = Embed( @@ -1281,8 +1350,8 @@ async def permissions_add( value = self._verify_user_or_role(user_or_role) if type_ == "command": - await self.bot.update_perms(command.qualified_name, value) name = command.qualified_name + await self.bot.update_perms(name, value) else: await self.bot.update_perms(level, value) name = level.name @@ -1309,7 +1378,7 @@ async def permissions_add( @permissions.command( name="remove", aliases=["del", "delete", "revoke"], - usage="[command/level] [name] [user_or_role]", + usage="[command/level] [name] [user/role] or [override] [command name]", ) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove( @@ -1318,10 +1387,10 @@ async def permissions_remove( type_: str.lower, name: str, *, - user_or_role: Union[Role, utils.User, str], + user_or_role: Union[Role, utils.User, str] = None, ): """ - Remove permission to use a command or permission level. + Remove permission to use a command, permission level, or command level override. For sub commands, wrap the complete command name with quotes. To find a list of permission levels, see `{prefix}help perms`. @@ -1331,22 +1400,54 @@ async def permissions_remove( - `{prefix}perms remove command reply @user` - `{prefix}perms remove command "plugin enabled" @role` - `{prefix}perms remove command help 984301093849028` + - `{prefix}perms remove override block` + - `{prefix}perms remove override "snippet add"` Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, """ - if type_ not in {"command", "level"}: + if type_ not in {"command", "level", "override"} or ( + type_ != "override" and user_or_role is None + ): return await ctx.send_help(ctx.command) + if type_ == "override": + extension = ctx.kwargs["user_or_role"] + if extension is not None: + name += f" {extension}" + name = name.lower() + name = getattr(self.bot.get_command(name), "qualified_name", name) + level = self.bot.config["override_command_level"].get(name) + if level is None: + perm = self.bot.command_perm(name) + embed = Embed( + title="Error", + color=Color.red(), + description=f"The command permission level was never overridden: `{name}`, " + f"current permission level is {perm.name}.", + ) + else: + logger.info("Restored command permission level for `%s`.", name) + self.bot.config["override_command_level"].pop(name) + await self.bot.config.update() + perm = self.bot.command_perm(name) + embed = Embed( + title="Success", + color=self.bot.main_color, + description=f"Command permission level for `{name}` was successfully restored to {perm.name}.", + ) + return await ctx.send(embed=embed) + level = None if type_ == "command": - name = getattr(self.bot.get_command(name.lower()), "qualified_name", name) + name = name.lower() + name = getattr(self.bot.get_command(name), "qualified_name", name) else: level = self._parse_level(name) - if level is None: + if level is PermissionLevel.INVALID: embed = Embed( title="Error", color=Color.red(), - description=f"The referenced {type_} does not exist: `{name}`.", + description=f"The referenced level does not exist: `{name}`.", ) return await ctx.send(embed=embed) name = level.name @@ -1423,7 +1524,7 @@ def _get_perm(self, ctx, name, type_): ) return embed - @permissions.command(name="get", usage="[@user] or [command/level] [name]") + @permissions.command(name="get", usage="[@user] or [command/level/override] [name]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get( self, ctx, user_or_role: Union[Role, utils.User, str], *, name: str = None @@ -1446,10 +1547,16 @@ async def permissions_get( - `{prefix}perms get command plugin remove` - `{prefix}perms get level SUPPORTER` + To view command level overrides: + + Examples: + - `{prefix}perms get override block` + - `{prefix}perms get override permissions add` + Do not ping `@everyone` for granting permission to everyone, use "everyone" or "all" instead, """ - if name is None and user_or_role not in {"command", "level"}: + if name is None and user_or_role not in {"command", "level", "override"}: value = self._verify_user_or_role(user_or_role) cmds = [] @@ -1497,6 +1604,59 @@ async def permissions_get( ), ] else: + user_or_role = (user_or_role or "").lower() + if user_or_role == "override": + if name is None: + done = set() + + overrides = {} + for command in self.bot.walk_commands(): + if command not in done: + done.add(command) + level = self.bot.config["override_command_level"].get(command.qualified_name) + if level is not None: + overrides[command.qualified_name] = level + + embeds = [] + if not overrides: + embeds.append(Embed( + title="Permission Overrides", + description="You don't have any command level overrides at the moment.", + color=Color.red() + )) + else: + for items in zip_longest(*(iter(sorted(overrides.items())),) * 15): + description = "\n".join( + ": ".join((f"`{name}`", level)) + for name, level in takewhile(lambda x: x is not None, items) + ) + embed = Embed(color=self.bot.main_color, description=description) + embed.set_author(name="Permission Overrides", icon_url=ctx.guild.icon_url) + embeds.append(embed) + + session = EmbedPaginatorSession(ctx, *embeds) + return await session.run() + + name = name.lower() + name = getattr(self.bot.get_command(name), "qualified_name", name) + level = self.bot.config["override_command_level"].get(name) + perm = self.bot.command_perm(name) + if level is None: + embed = Embed( + title="Error", + color=Color.red(), + description=f"The command permission level was never overridden: `{name}`, " + f"current permission level is {perm.name}.", + ) + else: + embed = Embed( + title="Success", + color=self.bot.main_color, + description=f'Permission override for command "{name}" is "{perm.name}".', + ) + + return await ctx.send(embed=embed) + if user_or_role not in {"command", "level"}: return await ctx.send_help(ctx.command) embeds = [] @@ -1504,11 +1664,12 @@ async def permissions_get( name = name.strip('"') command = level = None if user_or_role == "command": - command = self.bot.get_command(name.lower()) + name = name.lower() + command = self.bot.get_command(name) check = command is not None else: level = self._parse_level(name) - check = level is not None + check = level is not PermissionLevel.INVALID if not check: embed = Embed( @@ -1543,7 +1704,8 @@ async def permissions_get( @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def oauth(self, ctx): - """Commands relating to logviewer oauth2 login authentication. + """ + Commands relating to logviewer oauth2 login authentication. This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature. """ diff --git a/core/changelog.py b/core/changelog.py index 386baf176f..3bf4ad865b 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -1,9 +1,10 @@ import logging import re -from collections import defaultdict -from typing import List, Union +from typing import List -from discord import Embed, Color +from discord import Embed + +from core.utils import truncate logger = logging.getLogger("Modmail") @@ -27,22 +28,29 @@ class Version: The Modmail bot. version : str The version string (ie. "v2.12.0"). - lines : List[str] + lines : str A list of lines of changelog messages for this version. - fields : defaultdict[str, str] + fields : Dict[str, str] A dict of fields separated by "Fixed", "Changed", etc sections. description : str General description of the version. + + Class Attributes + ---------------- + ACTION_REGEX : str + The regex used to parse the actions. + DESCRIPTION_REGEX: str + The regex used to parse the description. """ - def __init__(self, bot, version: str, lines: Union[List[str], str]): + ACTION_REGEX = r"###\s*(.+?)\s*\n(.*?)(?=###\s*.+?|$)" + DESCRIPTION_REGEX = r"^(.*?)(?=###\s*.+?|$)" + + def __init__(self, bot, version: str, lines: str): self.bot = bot self.version = version.lstrip("vV") - if isinstance(lines, list): - self.lines = lines - else: - self.lines = [x for x in lines.splitlines() if x] - self.fields = defaultdict(str) + self.lines = lines.strip() + self.fields = {} self.description = "" self.parse() @@ -53,26 +61,32 @@ def parse(self) -> None: """ Parse the lines and split them into `description` and `fields`. """ - curr_action = None + self.description = re.match(self.DESCRIPTION_REGEX, self.lines, re.DOTALL) + self.description = ( + self.description.group(1).strip() if self.description is not None else "" + ) - for line in self.lines: - if line.startswith("### "): - curr_action = line[4:] - elif curr_action is None: - self.description += line + "\n" - else: - self.fields[curr_action] += line + "\n" + matches = re.finditer(self.ACTION_REGEX, self.lines, re.DOTALL) + for m in matches: + try: + self.fields[m.group(1).strip()] = m.group(2).strip() + except AttributeError: + logger.error( + "Something went wrong when parsing the changelog for version %s.", + self.version, + exc_info=True, + ) @property def url(self) -> str: - return Changelog.CHANGELOG_URL + "#v" + self.version.replace(".", "") + return f"{Changelog.CHANGELOG_URL}#v{self.version[::2]}" @property def embed(self) -> Embed: """ Embed: the formatted `Embed` of this `Version`. """ - embed = Embed(color=Color.green(), description=self.description) + embed = Embed(color=self.bot.main_color, description=self.description) embed.set_author( name=f"v{self.version} - Changelog", icon_url=self.bot.user.avatar_url, @@ -80,7 +94,7 @@ def embed(self) -> Embed: ) for name, value in self.fields.items(): - embed.add_field(name=name, value=value) + embed.add_field(name=name, value=truncate(value, 1024)) embed.set_footer(text=f"Current version: v{self.bot.version}") embed.set_thumbnail(url=self.bot.user.avatar_url) return embed @@ -121,7 +135,7 @@ class Changelog: ) CHANGELOG_URL = "https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md" VERSION_REGEX = re.compile( - r"# ([vV]\d+\.\d+(?:\.\d+)?)(.*?(?=# (?:[vV]\d+\.\d+(?:\.\d+)?)|$))", + r"#\s*([vV]\d+\.\d+(?:\.\d+)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)?|$)", flags=re.DOTALL, ) diff --git a/core/checks.py b/core/checks.py index 17d2b4353b..2e3dbe58d9 100644 --- a/core/checks.py +++ b/core/checks.py @@ -7,11 +7,9 @@ logger = logging.getLogger("Modmail") -def has_permissions_help(permission_level: PermissionLevel = PermissionLevel.REGULAR): +def has_permissions_predicate(permission_level: PermissionLevel = PermissionLevel.REGULAR): async def predicate(ctx): - return await check_permissions( - ctx, ctx.command.qualified_name, permission_level - ) + return await check_permissions(ctx, ctx.command.qualified_name) predicate.permission_level = permission_level return predicate @@ -36,62 +34,46 @@ async def setup(ctx): print("Success") """ - async def predicate(ctx): - has_perm = await check_permissions( - ctx, ctx.command.qualified_name, permission_level - ) - - if not has_perm and ctx.command.qualified_name != "help": - logger.error( - "You do not have permission to use this command: `%s` (%s).", - str(ctx.command.qualified_name), - str(permission_level.name), - ) - return has_perm + return commands.check(has_permissions_predicate(permission_level)) - predicate.permission_level = permission_level - return commands.check(predicate) - -async def check_permissions( # pylint: disable=too-many-return-statements - ctx, command_name, permission_level -) -> bool: +async def check_permissions(ctx, command_name) -> bool: """Logic for checking permissions for a command for a user""" if await ctx.bot.is_owner(ctx.author): - # Direct bot owner (creator) has absolute power over the bot + # Bot owner(s) (and creator) has absolute power over the bot + return True + + permission_level = ctx.bot.command_perm(command_name) + + if permission_level is PermissionLevel.INVALID: + logger.warning("Invalid permission level for command %s.", command_name) return True if ( - permission_level != PermissionLevel.OWNER + permission_level is not PermissionLevel.OWNER and ctx.channel.permissions_for(ctx.author).administrator ): # Administrators have permission to all non-owner commands + logger.debug("Allowed due to administrator.") return True command_permissions = ctx.bot.config["command_permissions"] - author_roles = ctx.author.roles + checkables = {*ctx.author.roles, ctx.author} if command_name in command_permissions: # -1 is for @everyone - if -1 in command_permissions[command_name]: - return True - has_perm_role = any( - role.id in command_permissions[command_name] for role in author_roles + return -1 in command_permissions[command_name] or any( + check.id in command_permissions[command_name] for check in checkables ) - has_perm_id = ctx.author.id in command_permissions[command_name] - return has_perm_role or has_perm_id level_permissions = ctx.bot.config["level_permissions"] for level in PermissionLevel: if level >= permission_level and level.name in level_permissions: - if -1 in level_permissions[level.name]: - return True - has_perm_role = any( - role.id in level_permissions[level.name] for role in author_roles - ) - has_perm_id = ctx.author.id in level_permissions[level.name] - if has_perm_role or has_perm_id: + # -1 is for @everyone + if -1 in level_permissions[level.name] or any( + check.id in level_permissions[level.name] for check in checkables + ): return True return False diff --git a/core/config.py b/core/config.py index b682f32e04..d07b87ea3b 100644 --- a/core/config.py +++ b/core/config.py @@ -74,6 +74,7 @@ class ConfigManager: "blocked_whitelist": [], "command_permissions": {}, "level_permissions": {}, + "override_command_level": {}, # threads "snippets": {}, "notification_squad": {}, diff --git a/core/config_help.json b/core/config_help.json index be499bb230..2d9a9d68e2 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -412,7 +412,7 @@ }, "mongo_uri": { "default": "None, required", - "description": "A MongoDB SRV connection string.", + "description": "A MongoDB connection string.", "examples": [ ], "notes": [ @@ -425,7 +425,7 @@ "examples": [ ], "notes": [ - "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables." + "This configuration can only to be set through `.env` file or environment (config) variables." ] }, "token": { @@ -434,7 +434,7 @@ "examples": [ ], "notes": [ - "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables." + "This configuration can only to be set through `.env` file or environment (config) variables." ] }, "log_level": { @@ -443,7 +443,7 @@ "examples": [ ], "notes": [ - "This configuration can only to be set through `.env` file, ~~`config.json` file~~ (removed), or environment (config) variables." + "This configuration can only to be set through `.env` file or environment (config) variables." ] } } \ No newline at end of file diff --git a/core/utils.py b/core/utils.py index 585c0fba3d..de17e18419 100644 --- a/core/utils.py +++ b/core/utils.py @@ -55,6 +55,7 @@ def truncate(text: str, max: int = 50) -> str: # pylint: disable=redefined-buil str The truncated text. """ + text = text.strip() return text[: max - 3].strip() + "..." if len(text) > max else text @@ -75,7 +76,7 @@ def format_preview(messages: typing.List[typing.Dict[str, typing.Any]]): messages = messages[:3] out = "" for message in messages: - if message.get("type") in ("note", "internal"): + if message.get("type") in {"note", "internal"}: continue author = message["author"] content = str(message["content"]).replace("\n", " ") @@ -193,19 +194,6 @@ def match_user_id(text: str) -> int: return -1 -def get_perm_level(cmd): - from core.models import PermissionLevel - - for check in cmd.checks: - perm = getattr(check, "permission_level", None) - if perm is not None: - return perm - for check in cmd.checks: - if "is_owner" in str(check): - return PermissionLevel.OWNER - return PermissionLevel.INVALID - - async def ignore(coro): try: await coro From 259ba09954aeda73dc5d4eef7727c7c75f1ff2e5 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 17 Aug 2019 10:43:08 -1000 Subject: [PATCH 0371/1221] Add checks when changing perm level --- cogs/utility.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cogs/utility.py b/cogs/utility.py index 00963b88b2..f3a543336e 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1294,6 +1294,11 @@ async def permissions_override( self.bot.config["override_command_level"][ command.qualified_name ] = level.name + + if not any(check for check in command.checks if hasattr(check, "permission_level")): + logger.debug('Command %s has no permissions check, adding invalid.', command.qualified_name) + checks.has_permissions(PermissionLevel.INVALID)(command) + await self.bot.config.update() embed = Embed( title="Success", From f066b32e14c8f158ba5822e01255665552274cb4 Mon Sep 17 00:00:00 2001 From: Lasse Niermann Date: Tue, 20 Aug 2019 00:11:43 +0200 Subject: [PATCH 0372/1221] Added Moderator to blocking reason (#349) Co-Authored-By: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> --- cogs/modmail.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7f2bb4ce56..3bedd23477 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1012,6 +1012,8 @@ async def block( mention = getattr(user, "mention", f"`{user.id}`") + moderator = ctx.author.name + if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( title="Error", @@ -1021,7 +1023,7 @@ async def block( return await ctx.send(embed=embed) if after is not None: - reason = after.arg + reason = f"{after.arg} by {moderator}" if reason.startswith("System Message: "): raise commands.BadArgument( "The reason cannot start with `System Message:`." @@ -1029,10 +1031,10 @@ async def block( if "%" in reason: raise commands.BadArgument('The reason contains illegal character "%".') if after.dt > after.now: - reason = f"{reason} %{after.dt.isoformat()}%" + reason = f"{reason} %{after.dt.isoformat()}% by {moderator}" if not reason: - reason = None + reason = f"Blocked by {moderator}" extend = f" for `{reason}`" if reason is not None else "" msg = self.bot.blocked_users.get(str(user.id)) @@ -1046,7 +1048,7 @@ async def block( ): if str(user.id) in self.bot.blocked_users: - old_reason = msg.strip().rstrip(".") or "no reason" + old_reason = msg.strip().rstrip(".") or f"Blocked by {moderator}" embed = discord.Embed( title="Success", description=f"{mention} was previously blocked for " From 0032a9dda426db2e3e586ab04ce3d5fb2cc38112 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 23 Aug 2019 02:23:50 -0700 Subject: [PATCH 0373/1221] Fix override --- CHANGELOG.md | 1 + bot.py | 14 +++++++++++++- cogs/utility.py | 37 +++++++++++++++++++++++-------------- core/checks.py | 4 +++- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09998321ce..8d89d2d73b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ however, insignificant breaking changes does not guarantee a major version bump, ### Internal - Use regex to parse Changes, Added, Fixed, etc and description. +- Adds `PermissionLevel.INVALID` when commands doesn't have a permission level. # v3.1.1 diff --git a/bot.py b/bot.py index 3bb205c991..62803d525d 100644 --- a/bot.py +++ b/bot.py @@ -29,6 +29,7 @@ except ImportError: pass +from core import checks from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import human_join, strtobool, parse_alias @@ -840,6 +841,17 @@ async def process_commands(self, message): ctxs = await self.get_contexts(message) for ctx in ctxs: if ctx.command: + if not any( + 1 + for check in ctx.command.checks + if hasattr(check, "permission_level") + ): + logger.debug( + "Command %s has no permissions check, adding invalid level.", + ctx.command.qualified_name, + ) + checks.has_permissions(PermissionLevel.INVALID)(ctx.command) + await self.invoke(ctx) continue @@ -1074,7 +1086,7 @@ async def on_command_error(self, context, exception): color=discord.Color.red(), description=check.fail_msg ) ) - if hasattr(check, 'permission_level'): + if hasattr(check, "permission_level"): corrected_permission_level = self.command_perm( context.command.qualified_name ) diff --git a/cogs/utility.py b/cogs/utility.py index f3a543336e..c10f845e72 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -646,6 +646,7 @@ async def set_presence( @tasks.loop(minutes=45) async def loop_presence(self): """Set presence to the configured value every 45 minutes.""" + # TODO: Does this even work? presence = await self.set_presence() logger.debug(f'{presence["activity"][1]} {presence["status"][1]}') @@ -1295,10 +1296,6 @@ async def permissions_override( command.qualified_name ] = level.name - if not any(check for check in command.checks if hasattr(check, "permission_level")): - logger.debug('Command %s has no permissions check, adding invalid.', command.qualified_name) - checks.has_permissions(PermissionLevel.INVALID)(command) - await self.bot.config.update() embed = Embed( title="Success", @@ -1618,25 +1615,37 @@ async def permissions_get( for command in self.bot.walk_commands(): if command not in done: done.add(command) - level = self.bot.config["override_command_level"].get(command.qualified_name) + level = self.bot.config["override_command_level"].get( + command.qualified_name + ) if level is not None: overrides[command.qualified_name] = level embeds = [] if not overrides: - embeds.append(Embed( - title="Permission Overrides", - description="You don't have any command level overrides at the moment.", - color=Color.red() - )) + embeds.append( + Embed( + title="Permission Overrides", + description="You don't have any command level overrides at the moment.", + color=Color.red(), + ) + ) else: - for items in zip_longest(*(iter(sorted(overrides.items())),) * 15): + for items in zip_longest( + *(iter(sorted(overrides.items())),) * 15 + ): description = "\n".join( ": ".join((f"`{name}`", level)) - for name, level in takewhile(lambda x: x is not None, items) + for name, level in takewhile( + lambda x: x is not None, items + ) + ) + embed = Embed( + color=self.bot.main_color, description=description + ) + embed.set_author( + name="Permission Overrides", icon_url=ctx.guild.icon_url ) - embed = Embed(color=self.bot.main_color, description=description) - embed.set_author(name="Permission Overrides", icon_url=ctx.guild.icon_url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) diff --git a/core/checks.py b/core/checks.py index 2e3dbe58d9..6ccf1647b8 100644 --- a/core/checks.py +++ b/core/checks.py @@ -7,7 +7,9 @@ logger = logging.getLogger("Modmail") -def has_permissions_predicate(permission_level: PermissionLevel = PermissionLevel.REGULAR): +def has_permissions_predicate( + permission_level: PermissionLevel = PermissionLevel.REGULAR +): async def predicate(ctx): return await check_permissions(ctx, ctx.command.qualified_name) From 244dd9490d1ee73c6987b4830d9f3ce2116cf293 Mon Sep 17 00:00:00 2001 From: weirdllamas Date: Fri, 23 Aug 2019 20:18:57 -0700 Subject: [PATCH 0374/1221] Added to Config: thread_move_notify and thread_move_response Closes Issue #189 --- cogs/modmail.py | 7 +++++++ core/config.py | 3 +++ core/config_help.json | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/cogs/modmail.py b/cogs/modmail.py index e3f57ffb7d..78f847d7f9 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -294,6 +294,13 @@ async def move(self, ctx, *, category: discord.CategoryChannel): """ thread = ctx.thread await thread.channel.edit(category=category, sync_permissions=True) + if self.bot.config["thread_move_notify"]: + embed = discord.Embed( + title="Thread Moved", + description=self.bot.config["thread_move_response"], + color=discord.Color.red()) + + await thread.recipient.send(embed=embed) sent_emoji, _ = await self.bot.retrieve_emoji() try: await ctx.message.add_reaction(sent_emoji) diff --git a/core/config.py b/core/config.py index d07b87ea3b..b2109a94da 100644 --- a/core/config.py +++ b/core/config.py @@ -53,6 +53,8 @@ class ConfigManager: "thread_close_title": "Thread Closed", "thread_close_response": "{closer.mention} has closed this Modmail thread.", "thread_self_close_response": "You have closed this Modmail thread.", + "thread_move_notify": False, + "thread_move_response": "This thread has been moved.", # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), @@ -109,6 +111,7 @@ class ConfigManager: "reply_without_command", "recipient_thread_close", "thread_auto_close_silently", + "thread_move_notify", } defaults = {**public_keys, **private_keys, **protected_keys} diff --git a/core/config_help.json b/core/config_help.json index 2d9a9d68e2..fd8649859c 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -300,6 +300,27 @@ "See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`." ] }, + "thread_move_notify": { + "default": "No", + "description": "Notify the recipient if the thread was moved.", + "examples": [ + "`{prefix}config set thread_move_notify yes`", + "`{prefix}config set thread_move_notify no`" + ], + "notes": [ + "See also: `thread_move_response`" + ] + }, + "thread_move_response": { + "default": "This thread has been moved.", + "description": "This is the message to display to the user when the thread is moved.", + "examples": [ + "`{prefix}config set thread_move_response This thread has been moved to another category for review!`" + ], + "notes": [ + "Only has an effect when thread_move_notify is on." + ] + }, "recipient_color": { "default": "Discord Gold [#F1C40F](https://placehold.it/100/f1c40f?text=+)", "description": "This is the color of the messages sent by the recipient, this applies to messages received in the thread channel.", From 8c5ff588812b3ce31f4b01b169fb295167e34b69 Mon Sep 17 00:00:00 2001 From: weirdllamas Date: Fri, 23 Aug 2019 20:25:41 -0700 Subject: [PATCH 0375/1221] Show Message ID Updated CHANGELOG.md Closes issue #344 --- CHANGELOG.md | 2 ++ core/thread.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09998321ce..c3aae3cefd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Ability to change permission levels of individual commands. - See `?permissions override` for more information. +- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved +- IDs of messages sent to Modmail are now viewable ### Fixed diff --git a/core/thread.py b/core/thread.py index 00aa24c9c7..b1ad46086d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -763,7 +763,7 @@ async def send( elif note: embed.colour = discord.Color.blurple() else: - embed.set_footer(text=f"Recipient") + embed.set_footer(text=f"Message ID: {message.id}") embed.colour = self.bot.recipient_color try: From 85fdadd3a8d427fbcae7252cc29011cf54732123 Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sat, 24 Aug 2019 02:16:06 -0400 Subject: [PATCH 0376/1221] Update recipient_thread_close --- core/config_help.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/config_help.json b/core/config_help.json index 2d9a9d68e2..c24b5868fa 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -39,7 +39,7 @@ "`{prefix}mention Yo~ Here's a new thread for ya!`" ], "notes": [ - "Unfortunately, its not currently possible to disable mention." + "Unfortunately, its not currently possible to disable mention. You do not have to include a mention." ] }, "main_color": { @@ -163,8 +163,8 @@ "default": "Disabled", "description": "Setting this configuration will allow recipients to use the `close_emoji` to close the thread themselves.", "examples": [ - "`{prefix}config set reply_without_command yes`", - "`{prefix}config set reply_without_command no`" + "`{prefix}config set recipient_thread_close yes`", + "`{prefix}config set recipient_thread_close no`" ], "notes": [ "The close emoji is dictated by the configuration `close_emoji`.", @@ -446,4 +446,4 @@ "This configuration can only to be set through `.env` file or environment (config) variables." ] } -} \ No newline at end of file +} From 45c42a012a16229d92a7615022c376dd6935f68c Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Sat, 24 Aug 2019 21:21:41 -0600 Subject: [PATCH 0377/1221] Update CONTRIBUTING.md (#360) * Update CONTRIBUTING.md Fixed the bit about Git Flow Fixed the license name and link Just some general stuff --- CONTRIBUTING.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d001f9004..5d7078e485 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,20 +13,25 @@ If you are proposing new features, please discuss them with us in the [developme ## We Develop with Github We use github to host code, to track issues and feature requests, as well as accept pull requests. -## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests -Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: +## We Use [Git Flow](https://atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) +![Simple Image Of A Git Flow Workflow](https://nvie.com/img/hotfix-branches@2x.png) +When contributing to this project please make sure you follow this and name your branches appropriately! -1. Fork the repo and create your branch from `master`. +## All Code Changes Happen Through Pull Requests +Make sure you know how Git Flow works before contributing! +Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: + +1. Fork the repo and create your branch from `master` or `development` according to Git Flow. 2. If you've added code that should be tested, add tests. 3. If you've changed APIs, update the documentation. 4. Ensure the test suite passes. 5. Make sure your code lints. 6. Issue that pull request! -## Any contributions you make will be under the MIT Software License -In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. +## Any contributions you make will be under the GNU Affero General Public License v3.0 +In short, when you submit code changes, your submissions are understood to be under the same [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project. Feel free to contact the maintainers if that's a concern. -## Report bugs using Github's [issues](https://github.com/kyb3r/modmail/issues) +## Report bugs using [Github Issues](https://github.com/kyb3r/modmail/issues) We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! ## Write bug reports with detail, background, and sample code From 562214eea21e1065b930caccd9a84a1bdae0f0fb Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Sun, 25 Aug 2019 17:50:44 -0600 Subject: [PATCH 0378/1221] Update cogs/modmail.py Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> --- cogs/modmail.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 78f847d7f9..92e4b14c84 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -294,7 +294,11 @@ async def move(self, ctx, *, category: discord.CategoryChannel): """ thread = ctx.thread await thread.channel.edit(category=category, sync_permissions=True) - if self.bot.config["thread_move_notify"]: + try: + thread_move_notify = strtobool(self.config["thread_move_notify"]) + except ValueError: + thread_move_notify = self.config.remove("thread_move_notify") + if thread_move_notify: embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], From 223c33f4148931e708cabb6ec4962f9a12fe25a8 Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Sun, 25 Aug 2019 17:51:16 -0600 Subject: [PATCH 0379/1221] Update core/config_help.json Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> --- core/config_help.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/config_help.json b/core/config_help.json index fd8649859c..44f211003b 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -308,7 +308,7 @@ "`{prefix}config set thread_move_notify no`" ], "notes": [ - "See also: `thread_move_response`" + "See also: `thread_move_response`." ] }, "thread_move_response": { @@ -467,4 +467,4 @@ "This configuration can only to be set through `.env` file or environment (config) variables." ] } -} \ No newline at end of file +} From bd6bb708d77b0f25772a128d47597f889efa512b Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Sun, 25 Aug 2019 17:51:26 -0600 Subject: [PATCH 0380/1221] Update core/config_help.json Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> --- core/config_help.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/config_help.json b/core/config_help.json index 44f211003b..50d0a9fad9 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -318,7 +318,8 @@ "`{prefix}config set thread_move_response This thread has been moved to another category for review!`" ], "notes": [ - "Only has an effect when thread_move_notify is on." + "Only has an effect when `thread_move_notify` is on.", + "See also: `thread_move_notify`." ] }, "recipient_color": { From 6bf83c8bee13ce4f70f7f651a711764a39d2b30b Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Sun, 25 Aug 2019 17:53:29 -0600 Subject: [PATCH 0381/1221] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3aae3cefd..4ba3ebf67a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Ability to change permission levels of individual commands. - See `?permissions override` for more information. -- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved -- IDs of messages sent to Modmail are now viewable +- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved. +- IDs of messages sent to Modmail are now viewable. ### Fixed From 5f77f8e18f0f90ad861ee064b263bc2756028d40 Mon Sep 17 00:00:00 2001 From: weirdllamas Date: Sun, 25 Aug 2019 17:39:41 -0700 Subject: [PATCH 0382/1221] Updating modmail.move Added specifics to arguments of move, allowing for an easily expandable set of specifics for the command Added a specific for silent moving for modmail.move --- cogs/modmail.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 92e4b14c84..f390ace1c7 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -17,7 +17,7 @@ from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User, create_not_found_embed, format_description +from core.utils import format_preview, User, create_not_found_embed, format_description, strtobool logger = logging.getLogger("Modmail") @@ -286,25 +286,34 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): @commands.command() @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() - async def move(self, ctx, *, category: discord.CategoryChannel): + async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None): """ Move a thread to another category. `category` may be a category ID, mention, or name. """ thread = ctx.thread + silent = False + + if specifics: + silent_words = ['silent', 'quiet'] + silent = any(word in specifics for word in silent_words) + await thread.channel.edit(category=category, sync_permissions=True) + try: - thread_move_notify = strtobool(self.config["thread_move_notify"]) + thread_move_notify = strtobool(self.bot.config["thread_move_notify"]) except ValueError: - thread_move_notify = self.config.remove("thread_move_notify") - if thread_move_notify: + thread_move_notify = self.bot.config.remove("thread_move_notify") + + if thread_move_notify and not silent: embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], color=discord.Color.red()) await thread.recipient.send(embed=embed) + sent_emoji, _ = await self.bot.retrieve_emoji() try: await ctx.message.add_reaction(sent_emoji) From c32f44a2796e0aca54af60482c45c9edc3079710 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 25 Aug 2019 19:35:54 -0700 Subject: [PATCH 0383/1221] Updated docker --- .dockerignore | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 13 ++--- README.md | 35 ++++++++++++ 3 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..78f469063d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,154 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# PyCharm +.idea/ + +# MacOS +.DS_Store + +# VS Code +.vscode/ + +# Node +package-lock.json +node_modules/ + +# Modmail +config.json +plugins/ +!plugins/registry.json +temp/ +test.py + +# Other stuff +.env.example +.gitignore +.lint.py +.pylintrc +.travis.yml +app.json +CHANGELOG.md +CODE_OF_CONDUCT.md +CONTRIBUTING.md +Pipfile +Pipfile.lock +Procfile +pyproject.toml +README.md +runtime.txt +SPONSORS.json diff --git a/Dockerfile b/Dockerfile index 851c65b4cc..9735f92d08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ -FROM library/python:latest -RUN apt update && apt install -y pipenv -RUN mkdir -p /bot && cd /bot && git clone https://github.com/kyb3r/modmail . -WORKDIR /bot -RUN pipenv install - -CMD ["pipenv", "run", "bot"] \ No newline at end of file +FROM python:3.7.4-alpine +RUN apk add --no-cache git +WORKDIR /modmailbot +COPY . /modmailbot +RUN pip install --no-cache-dir -r requirements.min.txt +CMD ["python", "bot.py"] \ No newline at end of file diff --git a/README.md b/README.md index 46e41697e9..71b17e8e30 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,41 @@ Finally, start Modmail. $ pipenv run bot ``` +#### Docker + +This repo supplies a Dockerfile for simplified deployment. + +You can build your own Docker image: + +```console +$ docker build . --tag=modmail +``` + +or run directly from a pre-built version from https://hub.docker.com/. Currently there are two community release of Modmail: + +- Kyber's: + +```console +$ docker pull kyb3rr/modmail +``` + +- Taku's: + +```console +$ docker pull taaku18/modmail +# You can also choose one of the following: +$ docker pull taaku18/modmail:dev +$ docker pull taaku18/modmail: ( ex: 3.2.0, 3.2, etc.) +``` + +And to run your docker image: + +```console +$ docker run --env-file .env user/modmail +``` +- Replace `user/modmail` with `kyb3rr/modmail`, `taaku18/modmail`, `taaku18/modmail:3.2`, etc as above. +- `.env` should be the path to your env file, you can also supply a path: `/path/to/.env`. + ## Sponsors Special thanks to our sponsors for supporting the project. From 2e53b7c7c38727fcb4bbded3fb1ac50c8dff9757 Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Mon, 26 Aug 2019 14:30:02 -0600 Subject: [PATCH 0384/1221] Update modmail.py --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index f390ace1c7..42ce97f5a3 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -310,7 +310,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], - color=discord.Color.red()) + color=self.bot.main_color await thread.recipient.send(embed=embed) From 133a565ba76428b5d0db2f4134064cd4dedc2cbb Mon Sep 17 00:00:00 2001 From: Weirdllamas <51797295+Weirdllamas@users.noreply.github.com> Date: Mon, 26 Aug 2019 14:55:35 -0600 Subject: [PATCH 0385/1221] Update modmail.py --- cogs/modmail.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 42ce97f5a3..d63a9e8b59 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -291,13 +291,14 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = Move a thread to another category. `category` may be a category ID, mention, or name. + `specifics` is a string which takes in arguments on how to perform the move. Ex: "silently" """ thread = ctx.thread silent = False if specifics: - silent_words = ['silent', 'quiet'] - silent = any(word in specifics for word in silent_words) + silent_words = ['silent', 'silently'] + silent = any(word in silent_words for word in specifics.split()) await thread.channel.edit(category=category, sync_permissions=True) From d3bab1ac8caa7f75c8bcf3d96f823d2355a239d1 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 26 Aug 2019 14:12:15 -0700 Subject: [PATCH 0386/1221] Syntax error in previous commit --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index d63a9e8b59..a55b637ddb 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -312,7 +312,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = title="Thread Moved", description=self.bot.config["thread_move_response"], color=self.bot.main_color - + ) await thread.recipient.send(embed=embed) sent_emoji, _ = await self.bot.retrieve_emoji() From 872d3044a0f73ce59b0d5784799a9bf9cd9f2bce Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 27 Aug 2019 11:08:57 -0700 Subject: [PATCH 0387/1221] Add attribution --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83a9ab37e..6129498119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,20 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.2.0 +# UNRELEASED ### Added - Ability to change permission levels of individual commands. - See `?permissions override` for more information. -- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved. -- IDs of messages sent to Modmail are now viewable. +- `thread_move_notify` and `thread_move_response` to notify recipients if a thread is moved. (Thanks to Flufster PR#360) +- IDs of messages sent to Modmail are now viewable. (Thanks to Flufster PR#360) ### Fixed - `?help `, will return `Perhaps you meant: `, now its fixed. - For example, `?help add` used to return `Perhaps you meant: add`, now it wouldn't do this. - Aliases and Permissions command names are always saved lowercase now. +- An improved Dockerfile. ### Internal From 4968de4093db7862dae62e1cd469ed3deb5e40f1 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sun, 8 Sep 2019 02:02:56 +0200 Subject: [PATCH 0388/1221] sponsor.json update (#365) --- SPONSORS.json | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/SPONSORS.json b/SPONSORS.json index 5f79ff80ea..4f65673165 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -1,28 +1,131 @@ [{ + "embed": { + "description": "This is a youtube channel that provides high quality reddit content. Videos are uploaded regularly so you are never out of funny, interesting or even creepy content!", + "color": 15114050, + "footer": { + "icon_url": "https://i.imgur.com/fvNKUks.png", + "text": "I am a robot, son of Daniel (UK)" + }, + "thumbnail": { + "url": "https://i.imgur.com/WyzaPKY.png" + }, + "author": { + "name": "Sir Reddit", + "url": "https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1", + "icon_url": "https://i.imgur.com/WyzaPKY.png" + }, + "fields": [ + { + "name": "Subscribe!", + "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)" + }, + { + "name": "Discord Server!", + "value": "[**Click Here**](https://discord.gg/V8ErqHb)" + } + ] + } + +},{ + + "embed": { + + "title":"Hey there! ", + + "description": "Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/djwMSQ)", + + "url": "https://discord.gg/djwMSQ", + + "color": 13003681, + + "timestamp": "2019-09-07T14:09:41.072Z", + + "footer": { + + "icon_url": "https://imgur.com/Mrc9pLd.gif", + + "text": "footer text" + + }, + + "thumbnail": { + + "url": "https://imgur.com/Mrc9pLd.gif" + + }, + + "image": { + + "url": "https://imgur.com/ZUFiL6b.gif" + + }, + + "author": { + + "name": "Roomieofficial", + + "url": "https://discord.gg/djwMSQ", + + "icon_url": "https://imgur.com/6hBkt7Z.png" + + }, + + "fields": [ + + { + + "name": "What is all this about🤔 ", + + "value": "We are mainly focused on everything that has anything to do with music or singing! " + + }, + + + + { + + "name": "Youtube🙄", + + "value": "U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads" + + },{ + + "name": "Discord😁", + + "value": "Make sure to join our [Discord](https://discord.gg/djwMSQ) We have weekly events, 24/7 chats and more! " + + + + } + + ] + + } + }] From 5cabada96ee4e54b7fe55bf5965625f6cbb5933f Mon Sep 17 00:00:00 2001 From: Kyber Date: Sun, 8 Sep 2019 10:11:54 +1000 Subject: [PATCH 0389/1221] Update SPONSORS.json --- SPONSORS.json | 194 +++++++++++++++++--------------------------------- 1 file changed, 65 insertions(+), 129 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index 4f65673165..9b9aabcbe7 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -1,131 +1,67 @@ -[{ - - "embed": { - - "description": "This is a youtube channel that provides high quality reddit content. Videos are uploaded regularly so you are never out of funny, interesting or even creepy content!", - - "color": 15114050, - - "footer": { - - "icon_url": "https://i.imgur.com/fvNKUks.png", - - "text": "I am a robot, son of Daniel (UK)" - - }, - - "thumbnail": { - - "url": "https://i.imgur.com/WyzaPKY.png" - - }, - - "author": { - - "name": "Sir Reddit", - - "url": "https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1", - - "icon_url": "https://i.imgur.com/WyzaPKY.png" - - }, - - "fields": [ - - { - - "name": "Subscribe!", - - "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)" - - }, - - { - - "name": "Discord Server!", - - "value": "[**Click Here**](https://discord.gg/V8ErqHb)" - +[ + { + "embed":{ + "description":"This is a youtube channel that provides high quality reddit content. Videos are uploaded regularly so you are never out of funny, interesting or even creepy content!", + "color":15114050, + "footer":{ + "icon_url":"https://i.imgur.com/fvNKUks.png", + "text":"I am a robot, son of Daniel (UK)" + }, + "thumbnail":{ + "url":"https://i.imgur.com/WyzaPKY.png" + }, + "author":{ + "name":"Sir Reddit", + "url":"https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1", + "icon_url":"https://i.imgur.com/WyzaPKY.png" + }, + "fields":[ + { + "name":"Subscribe!", + "value":"[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)" + }, + { + "name":"Discord Server!", + "value":"[**Click Here**](https://discord.gg/V8ErqHb)" + } + ] } - - ] - - } - -},{ - - "embed": { - - "title":"Hey there! ", - - "description": "Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/djwMSQ)", - - "url": "https://discord.gg/djwMSQ", - - "color": 13003681, - - "timestamp": "2019-09-07T14:09:41.072Z", - - "footer": { - - "icon_url": "https://imgur.com/Mrc9pLd.gif", - - "text": "footer text" - - }, - - "thumbnail": { - - "url": "https://imgur.com/Mrc9pLd.gif" - - }, - - "image": { - - "url": "https://imgur.com/ZUFiL6b.gif" - - }, - - "author": { - - "name": "Roomieofficial", - - "url": "https://discord.gg/djwMSQ", - - "icon_url": "https://imgur.com/6hBkt7Z.png" - - }, - - "fields": [ - - { - - "name": "What is all this about🤔 ", - - "value": "We are mainly focused on everything that has anything to do with music or singing! " - - }, - - - - { - - "name": "Youtube🙄", - - "value": "U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads" - - },{ - - "name": "Discord😁", - - "value": "Make sure to join our [Discord](https://discord.gg/djwMSQ) We have weekly events, 24/7 chats and more! " - - - + }, + { + "embed":{ + "title":"Hey there! ", + "description":"Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/djwMSQ)", + "url":"https://discord.gg/djwMSQ", + "color":13003681, + "footer":{ + "icon_url":"https://imgur.com/Mrc9pLd.gif", + "text":"footer text" + }, + "thumbnail":{ + "url":"https://imgur.com/Mrc9pLd.gif" + }, + "image":{ + "url":"https://imgur.com/ZUFiL6b.gif" + }, + "author":{ + "name":"Roomieofficial", + "url":"https://discord.gg/djwMSQ", + "icon_url":"https://imgur.com/6hBkt7Z.png" + }, + "fields":[ + { + "name":"What is all this about🤔 ", + "value":"We are mainly focused on everything that has anything to do with music or singing! " + }, + { + "name":"Youtube🙄", + "value":"U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads" + }, + { + "name":"Discord😁", + "value":"Make sure to join our [Discord](https://discord.gg/djwMSQ) We have weekly events, 24/7 chats and more! " + } + ] } - - ] - - } - -}] + } +] From 7ac20f7309300dde8fa6471c280cc23c9b072554 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sun, 8 Sep 2019 02:22:10 +0200 Subject: [PATCH 0390/1221] Update SPONSORS.json (#366) --- SPONSORS.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index 9b9aabcbe7..6a4b439907 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -30,8 +30,8 @@ { "embed":{ "title":"Hey there! ", - "description":"Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/djwMSQ)", - "url":"https://discord.gg/djwMSQ", + "description":"Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/qdujXC8)", + "url":"https://discord.gg/qdujXC8", "color":13003681, "footer":{ "icon_url":"https://imgur.com/Mrc9pLd.gif", @@ -45,7 +45,7 @@ }, "author":{ "name":"Roomieofficial", - "url":"https://discord.gg/djwMSQ", + "url":"https://discord.gg/qdujXC8", "icon_url":"https://imgur.com/6hBkt7Z.png" }, "fields":[ @@ -59,7 +59,7 @@ }, { "name":"Discord😁", - "value":"Make sure to join our [Discord](https://discord.gg/djwMSQ) We have weekly events, 24/7 chats and more! " + "value":"Make sure to join our [Discord](https://discord.gg/qdujXC8) We have weekly events, 24/7 chats and more! " } ] } From 2b73044ae316adbd1035d456d7dceff8b26da8c9 Mon Sep 17 00:00:00 2001 From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com> Date: Thu, 12 Sep 2019 23:24:15 +0600 Subject: [PATCH 0391/1221] Use Python 3.7.4 --- runtime.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime.txt b/runtime.txt index 6f651a3b1c..42731f2fbe 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.3 +python-3.7.4 From ff9554ed401ffb762dd742b39672d65abfd4ddd1 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Sep 2019 20:31:01 -0700 Subject: [PATCH 0392/1221] Merge --- .dockerignore | 1 + .gitignore | 1 + Pipfile | 2 +- Pipfile.lock | 132 +++++++++++++++++++++++------------------------- bot.py | 3 +- cogs/utility.py | 2 +- 6 files changed, 70 insertions(+), 71 deletions(-) diff --git a/.dockerignore b/.dockerignore index 78f469063d..57b6d8718c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -152,3 +152,4 @@ pyproject.toml README.md runtime.txt SPONSORS.json +stack.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8e80af7c26..a7fb960a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,4 @@ plugins/ !plugins/registry.json temp/ test.py +stack.yml \ No newline at end of file diff --git a/Pipfile b/Pipfile index 0b541bd697..cfb4f8ba59 100644 --- a/Pipfile +++ b/Pipfile @@ -19,7 +19,7 @@ dnspython = "~=1.16.0" parsedatetime = "==2.4" aiohttp = "<3.6.0,>=3.3.0" python-dotenv = ">=0.10.3" -pipenv = "==2018.11.26" +pipenv = "*" "discord.py" = "==1.2.3" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 5fbfe85392..ee4681eb19 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "46092f8b1032f2ac529f9b0df1aa725a4c6135d04826cd4cc4844af01879a4c2" + "sha256": "564773a120ff7ba8214e5efd98fab8daf98230eb4b27f8af2dd63afcad39994c" }, "pipfile-spec": 6, "requires": { @@ -97,11 +97,10 @@ }, "emoji": { "hashes": [ - "sha256:9ae01495fc3fcc04e9136ca1af8cae58726ec5dfaaa92f61f0732cbae9a12fa9", - "sha256:cecf8ed7cc535c999dc96e3fe7e7aacd176deb9ef62b3ef022c535e155084c44" + "sha256:b68112d40482a05e5da5d53da33d0aba3cce96891282c9c179cc340003c6c64e" ], "index": "pypi", - "version": "==0.5.2" + "version": "==0.5.3" }, "future": { "hashes": [ @@ -192,36 +191,36 @@ }, "pymongo": { "hashes": [ - "sha256:32421df60d06f479d71b6b539642e410ece3006e8910688e68df962c8eb40a21", - "sha256:324b22a8443e11faca44c96b20e7ec8a9e59a1e664457edeeb4f796080b31cde", - "sha256:4505ff8b7923dd7a8bed1bf25c9c4d0df5ab0b8b2821f2296533f2149a55f401", - "sha256:460b224681ea711e48e3638d15be2249024031b7dcb9622ba19c2e85bd5a26cc", - "sha256:47473b70c5f3cd5ddd2c49ab3b9ceafdafbbed5bc963f147df22a9343d7978f5", - "sha256:49375839af76834e9c5c3cc78c78386873fd0b2ad9a0860a7dc4ec9fe73af9dd", - "sha256:4a65f0f71ece86c860d30a1436b646db8ea32aec518845ef2903ca569faec32e", - "sha256:530621906c5dd6d27305b39c4e017701e5f4299aa68b93cde70eb985f94ca26f", - "sha256:54f4770b5810e8dc3cbeed675874195f02bb2bc4e95a9d665068edfb3baff4f7", - "sha256:5ed9382410e938b0ff76041c34018210504729a83bcf4f6a70c7092c28169f6f", - "sha256:61cad83637ae12c1c825130d7f9325cd6c162e3a64e8747a8144866020be3ff4", - "sha256:61e8e1c58b4fdf47ab79b7c7db8bb022c1e40b3b5fcbbaeea5fc94dc5c75638d", - "sha256:6e04e496af7d156b66cce70460011c621ecbadf5dcdce325c7acbb3cd6ea245d", - "sha256:7ef89ec435e89da902451dde6845066fe2770befaf0301fe2a1ac426b51fced3", - "sha256:854e8425e5eb775ccfffad04ecd094c99923d60a2c2d49babb5c435e836a91fa", - "sha256:9569796d48498e4db4e1d56284b626a8ed15f641ce3a8b2085f06bb03f4c2c88", - "sha256:9d50c99c6388863cbfdc5db9bad62e3a7c2e5fc151554a07c7f3c2530334a34f", - "sha256:9ea016c2c011df21f77c1f806ce45129a344ba2d414bd50f9e065b13a4a134be", - "sha256:a8421f0823174888fb12a5fa675322e756499d71e77ff712b4412d4b8f3c6503", - "sha256:aef7d88384ada699976350a285c7a333f96ebc959e98e7d2c98589f47bbf3b7f", - "sha256:b4d7ff9957ee770cf03bd7156a68a2f2e838e60712d9608eadc8741c15d01e72", - "sha256:c1db85c39e6a60588f855dbc7bd68fb0dab796096148ab5aa4abecaff19e1c6e", - "sha256:cee2fc0b94e66e7230da12fc4b3d34793c49957e16ee04f6468a94e264a1e41d", - "sha256:cf1dea28379a16b23e47db312883f07b3ba8d9d6abc1c59e51d4c8ae1820ab43", - "sha256:d1cd175df7c8b5fc976bade78bf4d9fb5aa7ab465c0f59931e380bbe188ef8fc", - "sha256:d48a94edf3cdd34524936a72ea01b352682b337f33a42db10ba29a96c37147d3", - "sha256:d9cc103a4e97f78bc77a1d72759ab3722f6cdf0374ad4fb4b0c53bd3238bdf98", - "sha256:fcb9ae8aa9158106c5d98a4349ec0d90b68f052d620b2d24622ba03b91e4d81d" - ], - "version": "==3.8.0" + "sha256:09f8196e1cb081713aa3face08d1806dc0a5dd64cb9f67fefc568519253a7ff2", + "sha256:1be549c0ce2ba8242c149156ae2064b12a5d4704448d49f630b4910606efd474", + "sha256:1f9fe869e289210250cba4ea20fbd169905b1793e1cd2737f423e107061afa98", + "sha256:3653cea82d1e35edd0a2355150daf8a27ebf12cf55182d5ad1046bfa288f5140", + "sha256:4249c6ba45587b959292a727532826c5032d59171f923f7f823788f413c2a5a3", + "sha256:4ff8f5e7c0a78983c1ee07894fff1b21c0e0ad3a122d9786cc3745fd60e4a2ce", + "sha256:56b29c638ab924716b48a3e94e3d7ac00b04acec1daa8190c36d61fc714c3629", + "sha256:56ec9358bbfe5ae3b25e785f8a14619d6799c855a44734c9098bb457174019bf", + "sha256:5dca250cbf1183c3e7b7b18c882c2b2199bfb20c74c4c68dbf11596808a296da", + "sha256:61101d1cc92881fac1f9ac7e99b033062f4c210178dc33193c8f5567feecb069", + "sha256:86624c0205a403fb4fbfedef79c5b4ab27e21fd018fdb6a27cf03b3c32a9e2b9", + "sha256:88ac09e1b197c3b4531e43054d49c022a3ea1281431b2f4980abafa35d2a5ce2", + "sha256:8b0339809b12ea292d468524dd1777f1a9637d9bdc0353a9261b88f82537d606", + "sha256:93dbf7388f6bf9af48dbb32f265b75b3dbc743a7a2ce98e44c88c049c58d85d3", + "sha256:9b705daec636c560dd2d63935f428a6b3cddfe903fffc0f349e0e91007c893d6", + "sha256:a090a819fe6fefadc2901d3911c07c76c0935ec5c790a50e9f3c3c47bacd5978", + "sha256:a102b346f1921237eaa9a31ee89eda57ad3c3973d79be3a456d92524e7df8fec", + "sha256:a13363869f2f36291d6367069c65d51d7b8d1b2fb410266b0b6b1f3c90d6deb0", + "sha256:a409a43c76da50881b70cc9ee70a1744f882848e8e93a68fb434254379777fa3", + "sha256:a76475834a978058425b0163f1bad35a5f70e45929a543075633c3fc1df564c5", + "sha256:ad474e93525baa6c58d75d63a73143af24c9f93c8e26e8d382f32c4da637901a", + "sha256:b268c7fa03ac77a8662fab3b2ab0be4beecb82f60f4c24b584e69565691a107f", + "sha256:cca4e1ab5ba0cd7877d3938167ee8ae9c2986cc0e10d3dcc3243d664d3a83fec", + "sha256:cef61de3f0f4441ec40266ff2ab42e5c16eaba1dc1fc6e1036f274621c52adc1", + "sha256:e28153b5d5ca33d4ba0c3bbc0e1ff161b9016e5e5f3f8ca10d6fa49106eb9e04", + "sha256:f30d7b37804daf0bab1143abc71666c630d7e270f5c14c5a7c300a6699c21108", + "sha256:f70f0133301cccf9bfd68fd20f67184ef991be578b646e78441106f9e27cc44d", + "sha256:fa75c21c1d82f20cce62f6fc4a68c2b0f33572ab406df1b17cd77a947d0b2993" + ], + "version": "==3.9.0" }, "python-dateutil": { "hashes": [ @@ -248,27 +247,24 @@ }, "uvloop": { "hashes": [ - "sha256:0fcd894f6fc3226a962ee7ad895c4f52e3f5c3c55098e21efb17c071849a0573", - "sha256:2f31de1742c059c96cb76b91c5275b22b22b965c886ee1fced093fa27dde9e64", - "sha256:459e4649fcd5ff719523de33964aa284898e55df62761e7773d088823ccbd3e0", - "sha256:67867aafd6e0bc2c30a079603a85d83b94f23c5593b3cc08ec7e58ac18bf48e5", - "sha256:8c200457e6847f28d8bb91c5e5039d301716f5f2fce25646f5fb3fd65eda4a26", - "sha256:958906b9ca39eb158414fbb7d6b8ef1b7aee4db5c8e8e5d00fcbb69a1ce9dca7", - "sha256:ac1dca3d8f3ef52806059e81042ee397ac939e5a86c8a3cea55d6b087db66115", - "sha256:b284c22d8938866318e3b9d178142b8be316c52d16fcfe1560685a686718a021", - "sha256:c48692bf4587ce281d641087658eca275a5ad3b63c78297bbded96570ae9ce8f", - "sha256:fefc3b2b947c99737c348887db2c32e539160dcbeb7af9aa6b53db7a283538fe" + "sha256:0deb6c97c5807c792dd9024bab90e6ca49e981862103cb2ea37b430c1ca0a267", + "sha256:155b34d513655e753d07f499a7e811970e2d397f240dfcbec0b32a9587159c99", + "sha256:1df3ddfa280206e9999ae1c777a20836eb895bcec6dc9fae2cbb6eecfafb099e", + "sha256:5b19361c8767e1dc61f6367f948d4f3dc5504b9f2eba488641b3d26ec14498ba", + "sha256:942cd07035510b149d6160796f4e972137130ae953871b6a98c2cf5d5ab68c2e", + "sha256:c63b6c0bf33144c604dd72f7eecf2d3a3ac7405c503c67bd98128cf306efe18a", + "sha256:e698a20a3b4ccb380d207f9d491d4085d7c38d364f6a0bae98684a1612a9607a" ], "index": "pypi", "markers": "sys_platform != 'win32'", - "version": "==0.12.2" + "version": "==0.13.0" }, "virtualenv": { "hashes": [ - "sha256:861bbce3a418110346c70f5c7a696fdcf23a261424e1d28aa4f9362fc2ccbc19", - "sha256:ba8ce6a961d842320681fb90a3d564d0e5134f41dacd0e2bae7f02441dde2d52" + "sha256:94a6898293d07f84a98add34c4df900f8ec64a570292279f6d91c781d37fd305", + "sha256:f6fc312c031f2d2344f885de114f1cb029dfcffd26aa6e57d2ee2296935c4e7d" ], - "version": "==16.6.2" + "version": "==16.7.4" }, "virtualenv-clone": { "hashes": [ @@ -366,26 +362,26 @@ }, "lazy-object-proxy": { "hashes": [ - "sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", - "sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", - "sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", - "sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", - "sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", - "sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", - "sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", - "sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", - "sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", - "sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", - "sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", - "sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", - "sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", - "sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", - "sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", - "sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", - "sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", - "sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1" - ], - "version": "==1.4.1" + "sha256:02b260c8deb80db09325b99edf62ae344ce9bc64d68b7a634410b8e9a568edbf", + "sha256:18f9c401083a4ba6e162355873f906315332ea7035803d0fd8166051e3d402e3", + "sha256:1f2c6209a8917c525c1e2b55a716135ca4658a3042b5122d4e3413a4030c26ce", + "sha256:2f06d97f0ca0f414f6b707c974aaf8829c2292c1c497642f63824119d770226f", + "sha256:616c94f8176808f4018b39f9638080ed86f96b55370b5a9463b2ee5c926f6c5f", + "sha256:63b91e30ef47ef68a30f0c3c278fbfe9822319c15f34b7538a829515b84ca2a0", + "sha256:77b454f03860b844f758c5d5c6e5f18d27de899a3db367f4af06bec2e6013a8e", + "sha256:83fe27ba321e4cfac466178606147d3c0aa18e8087507caec78ed5a966a64905", + "sha256:84742532d39f72df959d237912344d8a1764c2d03fe58beba96a87bfa11a76d8", + "sha256:874ebf3caaf55a020aeb08acead813baf5a305927a71ce88c9377970fe7ad3c2", + "sha256:9f5caf2c7436d44f3cec97c2fa7791f8a675170badbfa86e1992ca1b84c37009", + "sha256:a0c8758d01fcdfe7ae8e4b4017b13552efa7f1197dd7358dc9da0576f9d0328a", + "sha256:a4def978d9d28cda2d960c279318d46b327632686d82b4917516c36d4c274512", + "sha256:ad4f4be843dace866af5fc142509e9b9817ca0c59342fdb176ab6ad552c927f5", + "sha256:ae33dd198f772f714420c5ab698ff05ff900150486c648d29951e9c70694338e", + "sha256:b4a2b782b8a8c5522ad35c93e04d60e2ba7f7dcb9271ec8e8c3e08239be6c7b4", + "sha256:c462eb33f6abca3b34cdedbe84d761f31a60b814e173b98ede3c81bb48967c4f", + "sha256:fd135b8d35dfdcdb984828c84d695937e58cc5f49e1c854eb311c4d6aa03f4f1" + ], + "version": "==1.4.2" }, "mccabe": { "hashes": [ diff --git a/bot.py b/bot.py index 62803d525d..bea7adadbb 100644 --- a/bot.py +++ b/bot.py @@ -1152,8 +1152,9 @@ async def post_metadata(self): logger.debug("Uploading metadata to Modmail server.") async def before_post_metadata(self): - logger.info("Starting metadata loop.") await self.wait_for_connected() + logger.debug("Starting metadata loop.") + logger.line() if not self.guild: self.metadata_loop.cancel() diff --git a/cogs/utility.py b/cogs/utility.py index c10f845e72..8f0986f06b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -652,8 +652,8 @@ async def loop_presence(self): @loop_presence.before_loop async def before_loop_presence(self): - logger.info("Starting metadata loop.") await self.bot.wait_for_connected() + logger.debug("Starting metadata loop.") logger.line() presence = await self.set_presence() logger.info(presence["activity"][1]) From cd063609edf21853e2208d712ea895e9f6c28d6c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Sep 2019 20:38:50 -0700 Subject: [PATCH 0393/1221] Bump version v3.2.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6129498119..bf0820128e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# UNRELEASED +# v3.2.0 ### Added From 56da633a255b6db80ffe0df7727d5c0b7fdc1305 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Sep 2019 21:29:09 -0700 Subject: [PATCH 0394/1221] Fixed color hex issue --- CHANGELOG.md | 10 ++++++++++ bot.py | 2 +- core/_color_data.py | 32 ++++++++++++++++++++++++++++++++ core/config.py | 25 +++++++++++++++++-------- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf0820128e..7d601f839e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). +# v3.2.1 + +### Fixed + +- Can't set hex for main_color, recipient_color, etc. + +### Added + +- Discord colors by default when addressing them by names. + # v3.2.0 ### Added diff --git a/bot.py b/bot.py index bea7adadbb..afe7a547cc 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.2.0" +__version__ = "3.2.1" import asyncio import logging diff --git a/core/_color_data.py b/core/_color_data.py index 13346e903e..997a6a1d25 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -15,6 +15,36 @@ "w": "#ffffff", } +# Discord native colors +DISCORD_COLORS = { + "default": "#000000", + "teal": "#1abc9c", + "dark teal": "#11806a", + "green": "#2ecc71", + "dark green": "#1f8b4c", + "blue": "#3498db", + "dark blue": "#206694", + "purple": "#9b59b6", + "dark purple": "#71368a", + "magenta": "#e91e63", + "dark magenta": "#ad1457", + "gold": "#f1c40f", + "dark gold": "#c27c0e", + "orange": "#e67e22", + "dark orange": "#a84300", + "red": "#e74c3c", + "dark red": "#992d22", + "lighter gray": "#95a5a6", + "darker gray": "#546e7a", + "light gray": "#979c9f", + "dark gray": "#607d8b", + "blurple": "#7289da", + "grayple": "#99aab5" +} + +# Normalize name to "discord:" to avoid name collisions. +DISCORD_COLORS_NORM = {"discord:" + name: value for name, value in DISCORD_COLORS.items()} + # These colors are from Tableau TABLEAU_COLORS = { @@ -1160,3 +1190,5 @@ ALL_COLORS.update(XKCD_COLORS_NORM) ALL_COLORS.update(TABLEAU_COLORS) ALL_COLORS.update(TABLEAU_COLORS_NORM) +ALL_COLORS.update(DISCORD_COLORS) +ALL_COLORS.update(DISCORD_COLORS_NORM) diff --git a/core/config.py b/core/config.py index b2109a94da..3c8393d732 100644 --- a/core/config.py +++ b/core/config.py @@ -2,6 +2,7 @@ import json import logging import os +import re import typing from copy import deepcopy @@ -168,10 +169,9 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: # when setting a color if key in self.colors: - hex_ = ALL_COLORS.get(val) + try: + hex_ = str(val) - if hex_ is None: - hex_ = str(hex_) if hex_.startswith("#"): hex_ = hex_[1:] if len(hex_) == 3: @@ -179,14 +179,23 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: if len(hex_) != 6: raise InvalidConfigError("Invalid color name or hex.") try: - int(val, 16) + int(hex_, 16) except ValueError: raise InvalidConfigError("Invalid color name or hex.") - clean_value = "#" + val - value_text = clean_value - else: + hex_ = "#" + hex_ + value_text = clean_value = hex_ + + except InvalidConfigError: + name = re.sub(r"[\-. ]+", " ", str(val)) + hex_ = ALL_COLORS.get(name) + if hex_ is None: + name = re.sub(r"\W", "", str(val)) + hex_ = ALL_COLORS.get(name) + if hex_ is None: + raise + clean_value = hex_ - value_text = f"{val} ({clean_value})" + value_text = f"{hex_} ({clean_value})" elif key in self.time_deltas: try: From f94a81285071bc48c1117f440f56cbdd372d8256 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Sep 2019 21:32:21 -0700 Subject: [PATCH 0395/1221] Small typo --- core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.py b/core/config.py index 3c8393d732..57944c15e6 100644 --- a/core/config.py +++ b/core/config.py @@ -195,7 +195,7 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: raise clean_value = hex_ - value_text = f"{hex_} ({clean_value})" + value_text = f"{name} ({clean_value})" elif key in self.time_deltas: try: From 62847444dca539c6f25c59e88d63df90b3f76d31 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 13 Sep 2019 21:37:04 -0700 Subject: [PATCH 0396/1221] Small typo --- core/config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/config.py b/core/config.py index 57944c15e6..ad1a42d2f9 100644 --- a/core/config.py +++ b/core/config.py @@ -186,10 +186,11 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: value_text = clean_value = hex_ except InvalidConfigError: - name = re.sub(r"[\-. ]+", " ", str(val)) + name = str(val).lower() + name = re.sub(r"[\-+|. ]+", " ", name) hex_ = ALL_COLORS.get(name) if hex_ is None: - name = re.sub(r"\W", "", str(val)) + name = re.sub(r"[\-+|. ]+", "", name) hex_ = ALL_COLORS.get(name) if hex_ is None: raise From 6faf4c310a321509b2ce063bbeb5d6e3c540b6a8 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 14 Sep 2019 00:29:08 -0700 Subject: [PATCH 0397/1221] Added utf 8 encoding to config --- bot.py | 4 +++- core/config.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index afe7a547cc..9642cce2b3 100644 --- a/bot.py +++ b/bot.py @@ -294,6 +294,8 @@ def guild_id(self) -> typing.Optional[int]: except ValueError: self.config.remove("guild_id") logger.critical("Invalid GUILD_ID set.") + else: + logger.debug("No GUILD_ID set.") return None @property @@ -456,7 +458,7 @@ async def on_ready(self): await self.wait_for_connected() if self.guild is None: - logger.debug("Logging out due to invalid GUILD_ID.") + logger.error("Logging out due to invalid GUILD_ID.") return await self.logout() logger.line() diff --git a/core/config.py b/core/config.py index ad1a42d2f9..ed9adead23 100644 --- a/core/config.py +++ b/core/config.py @@ -139,7 +139,7 @@ def populate_cache(self) -> dict: ) if os.path.exists(config_json): logger.debug("Loading envs from config.json.") - with open(config_json, "r") as f: + with open(config_json, "r", encoding="utf-8") as f: # Config json should override env vars try: data.update( From e3d0f3e9db9aaa593c3120d0feff6dbf7abb1496 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 15 Sep 2019 22:33:53 -0700 Subject: [PATCH 0398/1221] Update security 3.2.2 --- CHANGELOG.md | 12 ++++++++++++ bot.py | 9 +++++---- core/models.py | 32 ++++++++++++++++++++++++++++++++ core/thread.py | 7 ++++--- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d601f839e..d9532ce987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). +# v3.2.2 + +Security update! + +### Important + +- Supporter permission users used to be able to "hack" snippets to reveal all your config vars, including your token and MongoURI. +- Implemented some changes to address this bug: + - All customizable variables used in snippets, close messages, etc, using the `{}` syntax, now forbids chaining 2 or more attributes and attributes that starts with `_`. +- It is advised to update to this version. +- If you felt your credentials have been leaked, consider changing your bot token / mongo uri. + # v3.2.1 ### Fixed diff --git a/bot.py b/bot.py index 9642cce2b3..a25777160d 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.2.1" +__version__ = "3.2.2" import asyncio import logging @@ -33,7 +33,7 @@ from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import human_join, strtobool, parse_alias -from core.models import PermissionLevel, ModmailLogger +from core.models import PermissionLevel, ModmailLogger, SafeFormatter from core.thread import ThreadManager from core.time import human_timedelta @@ -69,6 +69,7 @@ def __init__(self): self._session = None self._api = None self.metadata_loop = None + self.formatter = SafeFormatter() self._connected = asyncio.Event() self.start_time = datetime.utcnow() @@ -120,7 +121,7 @@ def uptime(self) -> str: if days: fmt = "{d}d " + fmt - return fmt.format(d=days, h=hours, m=minutes, s=seconds) + return self.formatter.format(fmt, d=days, h=hours, m=minutes, s=seconds) def _configure_logging(self): level_text = self.config["log_level"].upper() @@ -837,7 +838,7 @@ async def process_commands(self, message): thread = await self.threads.find(channel=message.channel) snippet = self.snippets[cmd] if thread: - snippet = snippet.format(recipient=thread.recipient) + snippet = self.formatter.format(snippet, recipient=thread.recipient) message.content = f"{self.prefix}reply {snippet}" ctxs = await self.get_contexts(message) diff --git a/core/models.py b/core/models.py index 58ef344115..8b21c10eb0 100644 --- a/core/models.py +++ b/core/models.py @@ -1,5 +1,7 @@ +import _string import logging from enum import IntEnum +from string import Formatter from discord import Color, Embed from discord.ext import commands @@ -85,3 +87,33 @@ class _Default: Default = _Default() + + +class SafeFormatter(Formatter): + + def get_field(self, field_name, args, kwargs): + first, rest = _string.formatter_field_name_split(field_name) + + try: + obj = self.get_value(first, args, kwargs) + except (IndexError, KeyError): + return "", first + + # loop through the rest of the field_name, doing + # getattr or getitem as needed + # stops when reaches the depth of 2 or starts with _. + try: + for n, (is_attr, i) in enumerate(rest): + if n >= 2: + break + if is_attr: + if str(i).startswith('_'): + break + obj = getattr(obj, i) + else: + obj = obj[i] + else: + return obj, first + except (IndexError, KeyError): + pass + return "", first diff --git a/core/thread.py b/core/thread.py index b1ad46086d..bdf0bc7aa2 100644 --- a/core/thread.py +++ b/core/thread.py @@ -393,8 +393,8 @@ async def _close( else: message = self.bot.config["thread_close_response"] - message = message.format( - closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None + message = self.bot.formatter.format( + message, closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None ) embed.description = message @@ -492,7 +492,8 @@ async def _restart_close_timer(self): ) # Grab message - close_message = self.bot.config["thread_auto_close_response"].format( + close_message = self.bot.formatter.format( + self.bot.config["thread_auto_close_response"], timeout=human_time ) From 9ec5cb79c2d154bf0b827894556435db509655ec Mon Sep 17 00:00:00 2001 From: DAzVise <52792999+DAzVise@users.noreply.github.com> Date: Tue, 17 Sep 2019 14:04:16 +0300 Subject: [PATCH 0399/1221] Update modmail.py --- cogs/modmail.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index f8246e17e2..a407232182 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -894,12 +894,16 @@ async def contact( await thread.wait_until_ready() embed = discord.Embed( title="Created thread", - description=f"Thread started in {thread.channel.mention} " + description=f"Thread started by {ctx.author.mention} " f"for {user.mention}.", color=self.bot.main_color, ) - await ctx.send(embed=embed) + try: + await thread.channel.send(embed=embed) + except: + await ctx.send(embed=embed) + await ctx.message.delete() @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) From 1340d733d51c7c0f57f8091d1a587d2aa794aa2c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 17 Sep 2019 20:36:55 -0700 Subject: [PATCH 0400/1221] Added changelog for v3.2.3-pre and slighty changed contact logic --- CHANGELOG.md | 7 +++++++ bot.py | 2 +- cogs/modmail.py | 11 ++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9532ce987..1f6c4c92da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). + +# [UNRELEASED] + +### Changed + +- `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) + # v3.2.2 Security update! diff --git a/bot.py b/bot.py index a25777160d..4d7014c4d2 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.2.2" +__version__ = "3.2.3-pre" import asyncio import logging diff --git a/cogs/modmail.py b/cogs/modmail.py index a407232182..1ccdcc4a5a 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -886,24 +886,21 @@ async def contact( description="A thread for this user already " f"exists in {exists.channel.mention}.", ) + await ctx.channel.send(embed=embed) else: thread = self.bot.threads.create( user, creator=ctx.author, category=category ) - await thread.wait_until_ready() embed = discord.Embed( - title="Created thread", + title="Created Thread", description=f"Thread started by {ctx.author.mention} " f"for {user.mention}.", color=self.bot.main_color, ) - - try: + await thread.wait_until_ready() await thread.channel.send(embed=embed) - except: - await ctx.send(embed=embed) - await ctx.message.delete() + await ctx.message.delete() @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) From c236b609ec59b108139a57cacf24696f207c2e4c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 19 Sep 2019 21:14:35 -0700 Subject: [PATCH 0401/1221] 3.3.0-dev --- CHANGELOG.md | 13 + Dockerfile | 1 - bot.py | 132 +-- cogs/modmail.py | 79 +- cogs/plugins.py | 688 +++++++------ cogs/utility.py | 78 +- core/_color_data.py | 2276 ++++++++++++++++++++--------------------- core/changelog.py | 27 +- core/config.py | 157 +-- core/config_help.json | 9 + core/thread.py | 35 +- plugins/registry.json | 2 +- 12 files changed, 1753 insertions(+), 1744 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f6c4c92da..738f7a3927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,19 @@ however, insignificant breaking changes does not guarantee a major version bump, ### Changed - `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) +- Plugins update (mostly internal). + - `git` is no longer used to install plugins, it now downloads through zip files. + - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. + - Reorganised plugins folder structure. + - Logging / plugin-related messages changes. + - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). +- Help command is in alphabetical order grouped by permissions. + +### Internal + +- Reworked `config.get` and `config.set`, it feeds through the converters before setting/getting. + - To get/set the raw value, access through `config[]`. +- Prerelease naming scheme is now `x.x.x-dev`. # v3.2.2 diff --git a/Dockerfile b/Dockerfile index 9735f92d08..eadf05e4e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ FROM python:3.7.4-alpine -RUN apk add --no-cache git WORKDIR /modmailbot COPY . /modmailbot RUN pip install --no-cache-dir -r requirements.min.txt diff --git a/bot.py b/bot.py index 4d7014c4d2..9108ddf583 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.2.3-pre" +__version__ = "3.3.0-dev" import asyncio import logging @@ -19,6 +19,7 @@ from aiohttp import ClientSession from emoji import UNICODE_EMOJI from motor.motor_asyncio import AsyncIOMotorClient +from pkg_resources import parse_version from pymongo.errors import ConfigurationError try: @@ -32,7 +33,7 @@ from core import checks from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import human_join, strtobool, parse_alias +from core.utils import human_join, parse_alias from core.models import PermissionLevel, ModmailLogger, SafeFormatter from core.thread import ThreadManager from core.time import human_timedelta @@ -70,6 +71,7 @@ def __init__(self): self._api = None self.metadata_loop = None self.formatter = SafeFormatter() + self.loaded_cogs = ['cogs.modmail', 'cogs.plugins', 'cogs.utility'] self._connected = asyncio.Event() self.start_time = datetime.utcnow() @@ -97,17 +99,7 @@ def __init__(self): sys.exit(0) self.plugin_db = PluginDatabaseClient(self) - - logger.line() - logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") - logger.info("││││ │ │││││├─┤││") - logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") - logger.info("v%s", __version__) - logger.info("Authors: kyb3r, fourjr, Taaku18") - logger.line() - - self._load_extensions() - logger.line() + self.startup() @property def uptime(self) -> str: @@ -123,6 +115,24 @@ def uptime(self) -> str: return self.formatter.format(fmt, d=days, h=hours, m=minutes, s=seconds) + def startup(self): + logger.line() + logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") + logger.info("││││ │ │││││├─┤││") + logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") + logger.info("v%s", __version__) + logger.info("Authors: kyb3r, fourjr, Taaku18") + logger.line() + + for cog in self.loaded_cogs: + logger.info("Loading %s.", cog) + try: + self.load_extension(cog) + logger.info("Successfully loaded %s.", cog) + except Exception: + logger.exception("Failed to load %s.", cog) + logger.line() + def _configure_logging(self): level_text = self.config["log_level"].upper() logging_levels = { @@ -161,8 +171,8 @@ def _configure_logging(self): logger.debug("Successfully configured logging.") @property - def version(self) -> str: - return __version__ + def version(self): + return parse_version(__version__) @property def session(self) -> ClientSession: @@ -179,18 +189,6 @@ def api(self): async def get_prefix(self, message=None): return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] - def _load_extensions(self): - """Adds commands automatically""" - for file in os.listdir("cogs"): - if not file.endswith(".py"): - continue - cog = f"cogs.{file[:-3]}" - logger.info("Loading %s.", cog) - try: - self.load_extension(cog) - except Exception: - logger.exception("Failed to load %s.", cog) - def run(self, *args, **kwargs): try: self.loop.run_until_complete(self.start(self.token)) @@ -366,25 +364,17 @@ def blocked_whitelisted_users(self) -> typing.List[str]: def prefix(self) -> str: return str(self.config["prefix"]) - def _parse_color(self, conf_name): - color = self.config[conf_name] - try: - return int(color.lstrip("#"), base=16) - except ValueError: - logger.error("Invalid %s provided.", conf_name) - return int(self.config.remove(conf_name).lstrip("#"), base=16) - @property def mod_color(self) -> int: - return self._parse_color("mod_color") + return self.config.get("mod_color") @property def recipient_color(self) -> int: - return self._parse_color("recipient_color") + return self.config.get("recipient_color") @property def main_color(self) -> int: - return self._parse_color("main_color") + return self.config.get("main_color") def command_perm(self, command_name: str) -> PermissionLevel: level = self.config["override_command_level"].get(command_name) @@ -518,7 +508,6 @@ async def on_ready(self): loop=None, ) self.metadata_loop.before_loop(self.before_post_metadata) - self.metadata_loop.after_loop(self.after_post_metadata) self.metadata_loop.start() async def convert_emoji(self, name: str) -> str: @@ -574,38 +563,14 @@ async def _process_blocked(self, message: discord.Message) -> bool: now = datetime.utcnow() - account_age = self.config["account_age"] - guild_age = self.config["guild_age"] + account_age = self.config.get("account_age") + guild_age = self.config.get("guild_age") if account_age is None: account_age = isodate.Duration() if guild_age is None: guild_age = isodate.Duration() - if not isinstance(account_age, isodate.Duration): - try: - account_age = isodate.parse_duration(account_age) - except isodate.ISO8601Error: - logger.warning( - "The account age limit needs to be a " - "ISO-8601 duration formatted duration string " - 'greater than 0 days, not "%s".', - str(account_age), - ) - account_age = self.config.remove("account_age") - - if not isinstance(guild_age, isodate.Duration): - try: - guild_age = isodate.parse_duration(guild_age) - except isodate.ISO8601Error: - logger.warning( - "The guild join age limit needs to be a " - "ISO-8601 duration formatted duration string " - 'greater than 0 days, not "%s".', - str(guild_age), - ) - guild_age = self.config.remove("guild_age") - reason = self.blocked_users.get(str(message.author.id)) or "" min_guild_age = min_account_age = now @@ -860,14 +825,7 @@ async def process_commands(self, message): thread = await self.threads.find(channel=ctx.channel) if thread is not None: - try: - reply_without_command = strtobool( - self.config["reply_without_command"] - ) - except ValueError: - reply_without_command = self.config.remove("reply_without_command") - - if reply_without_command: + if self.config.get('reply_without_command'): await thread.reply(message) else: await self.api.append_log(message, type_="internal") @@ -887,11 +845,7 @@ async def _void(*_args, **_kwargs): pass if isinstance(channel, discord.DMChannel): - try: - user_typing = strtobool(self.config["user_typing"]) - except ValueError: - user_typing = self.config.remove("user_typing") - if not user_typing: + if not self.config.get("user_typing"): return thread = await self.threads.find(recipient=user) @@ -899,11 +853,7 @@ async def _void(*_args, **_kwargs): if thread: await thread.channel.trigger_typing() else: - try: - mod_typing = strtobool(self.config["mod_typing"]) - except ValueError: - mod_typing = self.config.remove("mod_typing") - if not mod_typing: + if not self.config.get('mod_typing'): return thread = await self.threads.find(channel=channel) @@ -941,15 +891,7 @@ async def on_raw_reaction_add(self, payload): if isinstance(channel, discord.DMChannel): if str(reaction) == str(close_emoji): # closing thread - try: - recipient_thread_close = strtobool( - self.config["recipient_thread_close"] - ) - except ValueError: - recipient_thread_close = self.config.remove( - "recipient_thread_close" - ) - if not recipient_thread_close: + if not self.config.get('recipient_thread_close'): return thread = await self.threads.find(recipient=user) ts = message.embeds[0].timestamp if message.embeds else None @@ -1146,7 +1088,7 @@ async def post_metadata(self): "member_count": len(self.guild.members), "uptime": (datetime.utcnow() - self.start_time).total_seconds(), "latency": f"{self.ws.latency * 1000:.4f}", - "version": self.version, + "version": str(self.version), "selfhosted": True, "last_updated": str(datetime.utcnow()), } @@ -1161,10 +1103,6 @@ async def before_post_metadata(self): if not self.guild: self.metadata_loop.cancel() - @staticmethod - async def after_post_metadata(): - logger.info("Metadata loop has been cancelled.") - if __name__ == "__main__": try: diff --git a/cogs/modmail.py b/cogs/modmail.py index 1ccdcc4a5a..f51c131b67 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -17,7 +17,7 @@ from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User, create_not_found_embed, format_description, strtobool +from core.utils import format_preview, User, create_not_found_embed, format_description logger = logging.getLogger("Modmail") @@ -302,12 +302,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = await thread.channel.edit(category=category, sync_permissions=True) - try: - thread_move_notify = strtobool(self.bot.config["thread_move_notify"]) - except ValueError: - thread_move_notify = self.bot.config.remove("thread_move_notify") - - if thread_move_notify and not silent: + if self.bot.config('thread_move_notify') and not silent: embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], @@ -398,6 +393,17 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): closer=ctx.author, after=close_after, message=message, silent=silent ) + @staticmethod + def parse_user_or_role(ctx, user_or_role): + mention = None + if user_or_role is None: + mention = ctx.author.mention + elif hasattr(user_or_role, "mention"): + mention = user_or_role.mention + elif user_or_role in {"here", "everyone", "@here", "@everyone"}: + mention = "@" + user_or_role.lstrip("@") + return mention + @commands.command(aliases=["alert"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @@ -413,17 +419,12 @@ async def notify( `@here` and `@everyone` can be substituted with `here` and `everyone`. `user_or_role` may be a user ID, mention, name. role ID, mention, name, "everyone", or "here". """ - thread = ctx.thread - - if user_or_role is None: - mention = ctx.author.mention - elif hasattr(user_or_role, "mention"): - mention = user_or_role.mention - elif user_or_role in {"here", "everyone", "@here", "@everyone"}: - mention = "@" + user_or_role.lstrip("@") - else: + mention = self.parse_user_or_role(ctx, user_or_role) + if mention is None: raise commands.BadArgument(f"{user_or_role} is not a valid role.") + thread = ctx.thread + if str(thread.id) not in self.bot.config["notification_squad"]: self.bot.config["notification_squad"][str(thread.id)] = [] @@ -457,17 +458,12 @@ async def unnotify( `@here` and `@everyone` can be substituted with `here` and `everyone`. `user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here". """ - thread = ctx.thread - - if user_or_role is None: - mention = ctx.author.mention - elif hasattr(user_or_role, "mention"): - mention = user_or_role.mention - elif user_or_role in {"here", "everyone", "@here", "@everyone"}: - mention = "@" + user_or_role.lstrip("@") - else: + mention = self.parse_user_or_role(ctx, user_or_role) + if mention is None: mention = f"`{user_or_role}`" + thread = ctx.thread + if str(thread.id) not in self.bot.config["notification_squad"]: self.bot.config["notification_squad"][str(thread.id)] = [] @@ -502,17 +498,12 @@ async def subscribe( `@here` and `@everyone` can be substituted with `here` and `everyone`. `user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here". """ - thread = ctx.thread - - if user_or_role is None: - mention = ctx.author.mention - elif hasattr(user_or_role, "mention"): - mention = user_or_role.mention - elif user_or_role in {"here", "everyone", "@here", "@everyone"}: - mention = "@" + user_or_role.lstrip("@") - else: + mention = self.parse_user_or_role(ctx, user_or_role) + if mention is None: raise commands.BadArgument(f"{user_or_role} is not a valid role.") + thread = ctx.thread + if str(thread.id) not in self.bot.config["subscriptions"]: self.bot.config["subscriptions"][str(thread.id)] = [] @@ -546,17 +537,12 @@ async def unsubscribe( `@here` and `@everyone` can be substituted with `here` and `everyone`. `user_or_role` may be a user ID, mention, name, role ID, mention, name, "everyone", or "here". """ - thread = ctx.thread - - if user_or_role is None: - mention = ctx.author.mention - elif hasattr(user_or_role, "mention"): - mention = user_or_role.mention - elif user_or_role in {"here", "everyone", "@here", "@everyone"}: - mention = "@" + user_or_role.lstrip("@") - else: + mention = self.parse_user_or_role(ctx, user_or_role) + if mention is None: mention = f"`{user_or_role}`" + thread = ctx.thread + if str(thread.id) not in self.bot.config["subscriptions"]: self.bot.config["subscriptions"][str(thread.id)] = [] @@ -854,7 +840,6 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) - @trigger_typing async def contact( self, ctx, @@ -900,6 +885,12 @@ async def contact( ) await thread.wait_until_ready() await thread.channel.send(embed=embed) + sent_emoji, _ = await self.bot.retrieve_emoji() + try: + await ctx.message.add_reaction(sent_emoji) + except (discord.HTTPException, discord.InvalidArgument): + pass + await asyncio.sleep(3) await ctx.message.delete() @commands.group(invoke_without_command=True) diff --git a/cogs/plugins.py b/cogs/plugins.py index 4ea76503be..e15b47e29b 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -1,15 +1,19 @@ import asyncio -import importlib +import io import json import logging import os import shutil -import site -import stat -import subprocess import sys import typing +import zipfile + +from importlib import invalidate_caches from difflib import get_close_matches +from pathlib import Path, PurePath +from re import match +from site import USER_SITE +from subprocess import PIPE import discord from discord.ext import commands @@ -18,14 +22,68 @@ from core import checks from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession +from core.utils import truncate logger = logging.getLogger("Modmail") -class DownloadError(Exception): +class InvalidPluginError(commands.BadArgument): pass +class Plugin: + def __init__(self, user, repo, name, branch=None): + self.user = user + self.repo = repo + self.name = name + self.branch = branch if branch is not None else 'master' + self.url = f"https://github.com/{user}/{repo}/archive/{self.branch}.zip" + self.link = f"https://github.com/{user}/{repo}/tree/{self.branch}/{name}" + + @property + def path(self): + return PurePath('plugins') / self.user / self.repo / f'{self.name}-{self.branch}' + + @property + def abs_path(self): + return Path(__file__).absolute().parent.parent / self.path + + @property + def cache_path(self): + return Path(__file__).absolute().parent.parent / 'temp' / \ + 'plugins-cache' / f'{self.user}-{self.repo}-{self.branch}.zip' + + @property + def ext_string(self): + return f'plugins.{self.user}.{self.repo}.{self.name}-{self.branch}.{self.name}' + + def __str__(self): + return f'{self.user}/{self.repo}/{self.name}@{self.branch}' + + def __lt__(self, other): + return self.name.lower() < other.name.lower() + + @classmethod + def from_string(cls, s, strict=False): + if not strict: + m = match(r'^(.+?)/(.+?)/(.+?)(?:@(.+?))?$', s) + else: + m = match(r'^(.+?)/(.+?)/(.+?)@(.+?)$', s) + if m is not None: + return Plugin(*m.groups()) + else: + raise InvalidPluginError('Cannot decipher %s.', s) + + def __hash__(self): + return hash((self.user, self.repo, self.name, self.branch)) + + def __repr__(self): + return f'' + + def __eq__(self, other): + return isinstance(other, Plugin) and self.__str__() == other.__str__() + + class Plugins(commands.Cog): """ Plugins expand Modmail functionality by allowing third-party addons. @@ -39,7 +97,10 @@ class Plugins(commands.Cog): def __init__(self, bot): self.bot = bot self.registry = {} - self.bot.loop.create_task(self.download_initial_plugins()) + self.loaded_plugins = set() + self._ready_event = asyncio.Event() + + self.bot.loop.create_task(self.initial_load_plugins()) self.bot.loop.create_task(self.populate_registry()) async def populate_registry(self): @@ -47,357 +108,344 @@ async def populate_registry(self): async with self.bot.session.get(url) as resp: self.registry = json.loads(await resp.text()) - @staticmethod - def _asubprocess_run(cmd): - return subprocess.run(cmd, shell=True, check=True, capture_output=True) + async def initial_load_plugins(self): + await self.bot.wait_for_connected() - @staticmethod - def parse_plugin(name): - # returns: (username, repo, plugin_name, branch) - # default branch = master - try: - # when names are formatted with inline code - result = name.split("/") - result[2] = "/".join(result[2:]) - if "@" in result[2]: - # branch is specified - # for example, fourjr/modmail-plugins/welcomer@develop is a valid name - branch_split_result = result[2].split("@") - result.append(branch_split_result[-1]) - result[2] = "@".join(branch_split_result[:-1]) - else: - result.append("master") + for plugin_name in list(self.bot.config["plugins"]): + try: + plugin = Plugin.from_string(plugin_name, strict=True) + except InvalidPluginError: + self.bot.config["plugins"].remove(plugin_name) + try: + # For backwards compat + plugin = Plugin.from_string(plugin_name) + except InvalidPluginError: + logger.error("Failed to parse plugin name: %s.", plugin_name, exc_info=True) + continue - except IndexError: - return None + logger.info("Migrated legacy plugin name: %s, now %s.", plugin_name, str(plugin)) + self.bot.config["plugins"].append(str(plugin)) - return tuple(result) + try: + await self.download_plugin(plugin) + await self.load_plugin(plugin) + except Exception: + logger.error("Error when loading plugin %s.", plugin, exc_info=True) + continue - async def download_initial_plugins(self): - await self.bot.wait_for_connected() + logger.info("Finished loading all plugins.") + self._ready_event.set() + await self.bot.config.update() - for i in self.bot.config["plugins"]: - username, repo, name, branch = self.parse_plugin(i) + async def download_plugin(self, plugin, force=False): + if plugin.abs_path.exists() and not force: + return - try: - await self.download_plugin_repo(username, repo, branch) - except DownloadError as exc: - msg = f"{username}/{repo}@{branch} - {exc}" - logger.error(msg, exc_info=True) - else: - try: - await self.load_plugin(username, repo, name, branch) - except DownloadError as exc: - msg = f"{username}/{repo}@{branch}[{name}] - {exc}" - logger.error(msg, exc_info=True) + plugin.abs_path.mkdir(parents=True, exist_ok=True) - async def download_plugin_repo(self, username, repo, branch): - try: - cmd = f"git clone https://github.com/{username}/{repo} " - cmd += f"plugins/{username}-{repo}-{branch} " - cmd += f"-b {branch} -q" - - await self.bot.loop.run_in_executor(None, self._asubprocess_run, cmd) - # -q (quiet) so there's no terminal output unless there's an error - except subprocess.CalledProcessError as exc: - err = exc.stderr.decode("utf-8").strip() - - if not err.endswith("already exists and is not an empty directory."): - # don't raise error if the plugin folder exists - msg = f"Download Error: {username}/{repo}@{branch}" - logger.error(msg) - raise DownloadError(err) from exc - - async def load_plugin(self, username, repo, plugin_name, branch): - ext = f"plugins.{username}-{repo}-{branch}.{plugin_name}.{plugin_name}" - dirname = f"plugins/{username}-{repo}-{branch}/{plugin_name}" - - if "requirements.txt" in os.listdir(dirname): + if plugin.cache_path.exists() and not force: + plugin_io = plugin.cache_path.open('rb') + logger.debug('Loading cached %s.', plugin.cache_path) + + else: + async with self.bot.session.get(plugin.url) as resp: + logger.debug('Downloading %s.', plugin.url) + raw = await resp.read() + plugin_io = io.BytesIO(raw) + if not plugin.cache_path.parent.exists(): + plugin.cache_path.parent.mkdir(parents=True) + + with plugin.cache_path.open('wb') as f: + f.write(raw) + + with zipfile.ZipFile(plugin_io) as zipf: + for member in zipf.namelist(): + path = PurePath(member) + if len(path.parts) >= 3 and path.parts[1] == plugin.name: + with zipf.open(member) as src, (plugin.abs_path / Path(*path.parts[2:])).open("wb") as dst: + shutil.copyfileobj(src, dst) + + plugin_io.close() + + async def load_plugin(self, plugin): + if not (plugin.abs_path / f'{plugin.name}.py').exists(): + raise InvalidPluginError(f'{plugin.name}.py not found.') + + req_txt = plugin.abs_path / 'requirements.txt' + + if req_txt.exists(): # Install PIP requirements venv = hasattr(sys, "real_prefix") # in a virtual env - user_install = "--user" if not venv else "" + user_install = ' --user' if not venv else '' + proc = await asyncio.create_subprocess_shell( + f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", + stderr=PIPE, + stdout=PIPE + ) - try: - if os.name == "nt": # Windows - await self.bot.loop.run_in_executor( - None, - self._asubprocess_run, - f"pip install -r {dirname}/requirements.txt {user_install} -q -q", - ) - else: - await self.bot.loop.run_in_executor( - None, - self._asubprocess_run, - f"python3 -m pip install -U -r {dirname}/" - f"requirements.txt {user_install} -q -q", - ) - # -q -q (quiet) - # so there's no terminal output unless there's an error - except subprocess.CalledProcessError as exc: - err = exc.stderr.decode("utf8").strip() - logger.error("Error.", exc_info=True) - if err: - msg = f"Requirements Download Error: {username}/{repo}@{branch}[{plugin_name}]" - logger.error(msg) - raise DownloadError( - f"Unable to download requirements: ```\n{err}\n```" - ) from exc - else: - if not os.path.exists(site.USER_SITE): - os.makedirs(site.USER_SITE) + logger.debug('Downloading requirements for %s.', plugin.ext_string) + + stdout, stderr = await proc.communicate() - sys.path.insert(0, site.USER_SITE) + if stdout: + logger.debug('[stdout]\n%s.', stdout.decode()) + + if stderr: + logger.debug('[stderr]\n%s.', stderr.decode()) + logger.error("Failed to download requirements for %s.", plugin.ext_string, exc_info=True) + raise InvalidPluginError( + f"Unable to download requirements: ```\n{stderr.decode()}\n```" + ) + + if os.path.exists(USER_SITE): + sys.path.insert(0, USER_SITE) - await asyncio.sleep(0.5) try: - self.bot.load_extension(ext) + self.bot.load_extension(plugin.ext_string) + logger.info('Loaded plugin: %s', plugin.ext_string) + self.loaded_plugins.add(plugin) + except commands.ExtensionError as exc: - msg = f"Plugin Load Failure: {username}/{repo}@{branch}[{plugin_name}]" - logger.error(msg, exc_info=True) - raise DownloadError("Invalid plugin") from exc + logger.error('Plugin load failure: %s', plugin.ext_string, exc_info=True) + raise InvalidPluginError("Cannot load extension, plugin invalid.") from exc + + async def parse_user_input(self, ctx, plugin_name, check_version=False): + + if plugin_name in self.registry: + details = self.registry[plugin_name] + user, repo = details["repository"].split('/', maxsplit=1) + branch = details.get("branch") + + if check_version: + required_version = details.get("bot_version", False) + + if required_version and self.bot.version < parse_version(required_version): + embed = discord.Embed( + description="Your bot's version is too low. " + f"This plugin requires version `{required_version}`.", + color=discord.Color.red(), + ) + await ctx.send(embed=embed) + return + + plugin = Plugin(user, repo, plugin_name, branch) + else: - msg = f"Loaded Plugin: {username}/{repo}@{branch}[{plugin_name}]" - logger.info(msg) + try: + plugin = Plugin.from_string(plugin_name) + except InvalidPluginError: + embed = discord.Embed( + description="Invalid plugin name, double check the plugin name " + "or use one of the following formats: " + "username/repo/plugin, username/repo/plugin@branch.", + color=discord.Color.red(), + ) + await ctx.send(embed=embed) + return + return plugin - @commands.group(aliases=["plugins"], invoke_without_command=True) + @commands.group(aliases=["plugin"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin(self, ctx): - """Plugin handler. Controls the plugins in the bot.""" + async def plugins(self, ctx): + """ + Manage plugins for Modmail. + """ await ctx.send_help(ctx.command) - @plugin.command(name="add", aliases=["install"]) + @plugins.command(name="add", aliases=["install", "load"]) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_add(self, ctx, *, plugin_name: str): - """Add a plugin.""" - - if plugin_name in self.registry: - details = self.registry[plugin_name] - plugin_name = ( - details["repository"] + "/" + plugin_name + "@" + details["branch"] - ) + async def plugins_add(self, ctx, *, plugin_name: str): + """ + Install a new plugin for the bot. - required_version = details["bot_version"] + `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + """ - if parse_version(self.bot.version) < parse_version(required_version): - embed = discord.Embed( - description=f"Your bot's version is too low. This plugin requires version `{required_version}`.", - color=self.bot.main_color, - ) - return await ctx.send(embed=embed) + plugin = await self.parse_user_input(ctx, plugin_name, check_version=True) + if plugin is None: + return - if plugin_name in self.bot.config["plugins"]: + if str(plugin) in self.bot.config["plugins"]: embed = discord.Embed( description="This plugin is already installed.", - color=self.bot.main_color, + color=discord.Color.red(), ) return await ctx.send(embed=embed) - if plugin_name in self.bot.cogs.keys(): + if plugin.name in self.bot.cogs: # another class with the same name embed = discord.Embed( - description="There's another cog installed with the same name.", - color=self.bot.main_color, + description="Cannot install this plugin (dupe cog name).", + color=discord.Color.red(), ) return await ctx.send(embed=embed) embed = discord.Embed( - description="Downloading this plugin...", color=self.bot.main_color + description=f"Starting to download plugin from {plugin.link}...", + color=self.bot.main_color ) - await ctx.send(embed=embed) + msg = await ctx.send(embed=embed) async with ctx.typing(): - if len(plugin_name.split("/")) >= 3: - username, repo, name, branch = self.parse_plugin(plugin_name) - - try: - await self.download_plugin_repo(username, repo, branch) - except Exception as exc: - if not isinstance(exc, DownloadError): - logger.error( - "Unknown error when adding a plugin:", exc_info=True - ) - embed = discord.Embed( - description=f"Unable to fetch this plugin from Github: `{exc}`.", - color=self.bot.main_color, - ) - return await ctx.send(embed=embed) + try: + await self.download_plugin(plugin, force=True) + except Exception: + logger.warning(f"Unable to download plugin %s.", plugin, exc_info=True) - importlib.invalidate_caches() + embed = discord.Embed( + description=f"Failed to download plugin, check logs for error.", + color=discord.Color.red() + ) - try: - await self.load_plugin(username, repo, name, branch) - except Exception as exc: - if not isinstance(exc, DownloadError): - logger.error( - "Unknown error when adding a plugin:", exc_info=True - ) - embed = discord.Embed( - description=f"Unable to load this plugin: `{exc}`.", - color=self.bot.main_color, - ) - return await ctx.send(embed=embed) + return await msg.edit(embed=embed) - # if it makes it here, it has passed all checks and should - # be entered into the config + invalidate_caches() - self.bot.config["plugins"].append(plugin_name) - await self.bot.config.update() + try: + await self.load_plugin(plugin) + except Exception: + logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description="The plugin is installed.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*", - color=self.bot.main_color, - ) - await ctx.send(embed=embed) - else: - embed = discord.Embed( - description="Invalid plugin name format: use plugin-name or " - "username/repo/plugin or username/repo/plugin@branch.", - color=self.bot.main_color, + description=f"Failed to download plugin, check logs for error.", + color=discord.Color.red() ) - await ctx.send(embed=embed) - @plugin.command(name="remove", aliases=["del", "delete"]) + return await msg.edit(embed=embed) + + self.bot.config["plugins"].append(str(plugin)) + await self.bot.config.update() + + embed = discord.Embed( + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*", + color=self.bot.main_color, + ) + return await msg.edit(embed=embed) + + @plugins.command(name="remove", aliases=["del", "delete"]) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_remove(self, ctx, *, plugin_name: str): - """Remove a plugin.""" + async def plugins_remove(self, ctx, *, plugin_name: str): + """ + Remove an installed plugin of the bot. - if plugin_name in self.registry: - details = self.registry[plugin_name] - plugin_name = ( - details["repository"] + "/" + plugin_name + "@" + details["branch"] + `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference + to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + """ + plugin = await self.parse_user_input(ctx, plugin_name) + if plugin is None: + return + + if str(plugin) not in self.bot.config["plugins"]: + embed = discord.Embed( + description="Plugin is not installed.", + color=discord.Color.red(), ) + return await ctx.send(embed=embed) - if plugin_name in self.bot.config["plugins"]: - username, repo, name, branch = self.parse_plugin(plugin_name) - try: - self.bot.unload_extension( - f"plugins.{username}-{repo}-{branch}.{name}.{name}" - ) - except commands.ExtensionNotLoaded: - logger.error("Plugin was never loaded.") + try: + self.bot.unload_extension(plugin.ext_string) + self.loaded_plugins.remove(plugin) + except (commands.ExtensionNotLoaded, KeyError): + logger.warning("Plugin was never loaded.") - self.bot.config["plugins"].remove(plugin_name) + self.bot.config["plugins"].remove(str(plugin)) + await self.bot.config.update() - try: - if not any( - i.startswith(f"{username}/{repo}") - for i in self.bot.config["plugins"] - ): - # if there are no more of such repos, delete the folder - def onerror(func, path, _): - if not os.access(path, os.W_OK): - # Is the error an access error? - os.chmod(path, stat.S_IWUSR) - func(path) - - shutil.rmtree( - f"plugins/{username}-{repo}-{branch}", onerror=onerror - ) - except Exception: - logger.error("Failed to remove plugin %s.", plugin_name, exc_info=True) - self.bot.config["plugins"].append(plugin_name) - return + embed = discord.Embed( + description="The plugin is uninstalled and all its data is erased.", + color=self.bot.main_color, + ) + await ctx.send(embed=embed) - await self.bot.config.update() + @plugins.command(name="update") + @checks.has_permissions(PermissionLevel.OWNER) + async def plugins_update(self, ctx, *, plugin_name: str): + """ + Update a plugin for the bot. + + `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference + to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + """ + + plugin = await self.parse_user_input(ctx, plugin_name, check_version=True) + if plugin is None: + return + if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description="The plugin is uninstalled and all its data is erased.", - color=self.bot.main_color, + description="Plugin is not installed.", + color=discord.Color.red(), ) - await ctx.send(embed=embed) - else: + return await ctx.send(embed=embed) + + async with ctx.typing(): + await self.download_plugin(plugin, force=True) + try: + self.bot.unload_extension(plugin.ext_string) + except commands.ExtensionError: + logger.warning("Plugin unload fail.", exc_info=True) + await self.load_plugin(plugin) + embed = discord.Embed( - description="That plugin is not installed.", color=self.bot.main_color + description=f"Successfully updated {plugin.name}.", + color=self.bot.main_color, ) - await ctx.send(embed=embed) + return await ctx.send(embed=embed) - @plugin.command(name="update") + @plugins.command(name="loaded", aliases=["enabled", "installed"]) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_update(self, ctx, *, plugin_name: str): - """Update a plugin.""" + async def plugins_loaded(self, ctx): + """ + Show a list of currently loaded plugins. + """ - if plugin_name in self.registry: - details = self.registry[plugin_name] - plugin_name = ( - details["repository"] + "/" + plugin_name + "@" + details["branch"] + if not self._ready_event.is_set(): + embed = discord.Embed( + description="Plugins are still loading, please try again later.", + color=self.bot.main_color ) + return await ctx.send(embed=embed) - if plugin_name not in self.bot.config["plugins"]: + if not self.loaded_plugins: embed = discord.Embed( - description="That plugin is not installed.", color=self.bot.main_color + description="There are no plugins currently loaded.", + color=discord.Color.red() ) return await ctx.send(embed=embed) - async with ctx.typing(): - username, repo, name, branch = self.parse_plugin(plugin_name) - - try: - cmd = ( - f"cd plugins/{username}-{repo}-{branch} && " - f"git reset --hard origin/{branch} && git fetch --all && git pull" - ) - cmd = await self.bot.loop.run_in_executor( - None, self._asubprocess_run, cmd - ) - except subprocess.CalledProcessError as exc: - err = exc.stderr.decode("utf8").strip() - - embed = discord.Embed( - description=f"An error occurred while updating: {err}.", - color=self.bot.main_color, - ) - logger.error("An error occurred while updating plugin:", exc_info=True) - await ctx.send(embed=embed) - + loaded_plugins = map(str, sorted(self.loaded_plugins)) + pages = ['```\n'] + for plugin in loaded_plugins: + msg = str(plugin) + '\n' + if len(msg) + len(pages[-1]) + 3 <= 2048: + pages[-1] += msg else: - output = cmd.stdout.decode("utf8").strip() + pages[-1] += '```' + pages.append(f'```\n{msg}') - embed = discord.Embed( - description=f"```\n{output}\n```", color=self.bot.main_color - ) - await ctx.send(embed=embed) - - if output != "Already up to date.": - # repo was updated locally, now perform the cog reload - ext = f"plugins.{username}-{repo}-{branch}.{name}.{name}" - self.bot.unload_extension(ext) - - try: - await self.load_plugin(username, repo, name, branch) - except DownloadError as exc: - embed = discord.Embed( - description=f"Unable to start the plugin: `{exc}`.", - color=self.bot.main_color, - ) - logger.error( - "An error occurred while updating plugin:", exc_info=True - ) - await ctx.send(embed=embed) - - @plugin.command(name="enabled", aliases=["installed"]) - @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_enabled(self, ctx): - """Shows a list of currently enabled plugins.""" + if pages[-1][-3:] != '```': + pages[-1] += '```' - if self.bot.config["plugins"]: - msg = "```\n" + "\n".join(sorted(self.bot.config["plugins"])) + "\n```" - embed = discord.Embed(description=msg, color=self.bot.main_color) - await ctx.send(embed=embed) - else: + embeds = [] + for page in pages: embed = discord.Embed( - description="There are no plugins installed.", color=self.bot.main_color + title="Loaded plugins:", + description=page, + color=self.bot.main_color ) - await ctx.send(embed=embed) + embeds.append(embed) + paginator = EmbedPaginatorSession(ctx, *embeds) + await paginator.run() - @plugin.group( + @plugins.group( invoke_without_command=True, name="registry", aliases=["list", "info"] ) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = None): + async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = None): """ Shows a list of all approved plugins. @@ -440,57 +488,95 @@ async def plugin_registry(self, ctx, *, plugin_name: typing.Union[int, str] = No return await ctx.send(embed=embed) - for name, details in registry: - repo = f"https://github.com/{details['repository']}" - url = f"{repo}/tree/master/{name}" + for plugin_name, details in registry: + details = self.registry[plugin_name] + user, repo = details["repository"].split('/', maxsplit=1) + branch = details.get("branch") + + plugin = Plugin(user, repo, plugin_name, branch) embed = discord.Embed( color=self.bot.main_color, description=details["description"], - url=repo, + url=plugin.link, title=details["repository"], ) embed.add_field( - name="Installation", value=f"```{self.bot.prefix}plugins add {name}```" + name="Installation", value=f"```{self.bot.prefix}plugins add {plugin_name}```" ) embed.set_author( - name=details["title"], icon_url=details.get("icon_url"), url=url + name=details["title"], + icon_url=details.get("icon_url"), + url=plugin.link ) + if details.get("thumbnail_url"): embed.set_thumbnail(url=details.get("thumbnail_url")) + if details.get("image_url"): embed.set_image(url=details.get("image_url")) + if plugin in self.loaded_plugins: + embed.set_footer(text="This plugin is currently loaded.") + else: + required_version = details.get("bot_version", False) + if required_version and self.bot.version < parse_version(required_version): + embed.set_footer( + text=f"Your bot is unable to install this plugin, " + f"minimum required version is v{required_version}.") + else: + embed.set_footer(text="Your bot is able to install this plugin.") + embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) paginator.current = index await paginator.run() - @plugin_registry.command(name="compact") + @plugins_registry.command(name="compact", aliases=["slim"]) @checks.has_permissions(PermissionLevel.OWNER) - async def plugin_registry_compact(self, ctx): - """Shows a compact view of all plugins within the registry.""" + async def plugins_registry_compact(self, ctx): + """ + Shows a compact view of all plugins within the registry. + """ await self.populate_registry() registry = sorted(self.registry.items(), key=lambda elem: elem[0]) - pages = [""] - - for name, details in registry: - repo = f"https://github.com/{details['repository']}" - url = f"{repo}/tree/{details['branch']}/{name}" - desc = details["description"].replace("\n", "") - fmt = f"[`{name}`]({url}) - {desc}" - length = len(fmt) - len(url) - 4 - fmt = fmt[: 75 + len(url)].strip() + "..." if length > 75 else fmt - if len(fmt) + len(pages[-1]) >= 2048: - pages.append(fmt + "\n") + pages = [''] + + for plugin_name, details in registry: + details = self.registry[plugin_name] + user, repo = details["repository"].split('/', maxsplit=1) + branch = details.get("branch") + + plugin = Plugin(user, repo, plugin_name, branch) + + desc = discord.utils.escape_markdown(details["description"].replace("\n", "")) + + name = f"[`{plugin.name}`]({plugin.link})" + fmt = f"{name} - {desc}" + + if plugin_name in self.loaded_plugins: + limit = 75 - len(plugin_name) - 4 - 8 + len(name) + if limit < 0: + fmt = plugin.name + limit = 75 + fmt = truncate(fmt, limit) + '[loaded]\n' + else: + limit = 75 - len(plugin_name) - 4 + len(name) + if limit < 0: + fmt = plugin.name + limit = 75 + fmt = truncate(fmt, limit) + '\n' + + if len(fmt) + len(pages[-1]) <= 2048: + pages[-1] += fmt else: - pages[-1] += fmt + "\n" + pages.append(fmt) embeds = [] diff --git a/cogs/utility.py b/cogs/utility.py index 8f0986f06b..d54ad362b9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -41,7 +41,7 @@ async def format_cog_help(self, cog, *, no_cog=False): for cmd in await self.filter_commands( cog.get_commands() if not no_cog else cog, sort=True, - key=lambda c: bot.command_perm(c.qualified_name), + key=lambda c: (bot.command_perm(c.qualified_name), c.qualified_name), ): perm_level = bot.command_perm(cmd.qualified_name) if perm_level is PermissionLevel.INVALID: @@ -119,41 +119,34 @@ async def send_cog_help(self, cog): ) return await session.run() - async def send_command_help(self, command): - if not await self.filter_commands([command]): + async def _get_help_embed(self, topic): + if not await self.filter_commands([topic]): return - perm_level = self.context.bot.command_perm(command.qualified_name) + perm_level = self.context.bot.command_perm(topic.qualified_name) if perm_level is not PermissionLevel.INVALID: perm_level = f"{perm_level.name} [{perm_level}]" else: perm_level = "NONE" embed = Embed( - title=f"`{self.get_command_signature(command)}`", + title=f"`{self.get_command_signature(topic)}`", color=self.context.bot.main_color, - description=self.process_help_msg(command.help), + description=self.process_help_msg(topic.help), ) - embed.set_footer(text=f"Permission level: {perm_level}") - await self.get_destination().send(embed=embed) + return embed, perm_level + + async def send_command_help(self, command): + topic = await self._get_help_embed(command) + if topic is not None: + topic[0].set_footer(text=f"Permission level: {topic[1]}") + await self.get_destination().send(embed=topic[0]) async def send_group_help(self, group): - if not await self.filter_commands([group]): + topic = await self._get_help_embed(group) + if topic is None: return - - perm_level = self.context.bot.command_perm(group.qualified_name) - if perm_level is not PermissionLevel.INVALID: - perm_level = f"{perm_level.name} [{perm_level}]" - else: - perm_level = "NONE" - - embed = Embed( - title=f"`{self.get_command_signature(group)}`", - color=self.context.bot.main_color, - description=self.process_help_msg(group.help), - ) - - if perm_level: - embed.add_field(name="Permission Level", value=perm_level, inline=False) + embed = topic[0] + embed.add_field(name="Permission Level", value=topic[1], inline=False) format_ = "" length = len(group.commands) @@ -284,7 +277,7 @@ async def changelog(self, ctx, version: str.lower = ""): pass logger.warning("Failed to display changelog.", exc_info=True) await ctx.send( - f"View the changelog here: {changelog.CHANGELOG_URL}#v{version[::2]}" + f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}" ) @commands.command(aliases=["bot", "info"]) @@ -308,27 +301,27 @@ async def about(self, ctx): embed.add_field(name="Uptime", value=self.bot.uptime) embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms") embed.add_field(name="Version", value=f"`{self.bot.version}`") - embed.add_field(name="Author", value="[`kyb3r`](https://github.com/kyb3r)") + embed.add_field(name="Author", value="[`kyb3r`, `Taki`, `4jr`](https://github.com/kyb3r)") changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version - if parse_version(self.bot.version) < parse_version(latest.version): + if self.bot.version < parse_version(latest.version): footer = f"A newer version is available v{latest.version}" else: footer = "You are up to date with the latest version." embed.add_field( - name="GitHub", value="https://github.com/kyb3r/modmail", inline=False - ) - - embed.add_field( - name="Discord Server", value="https://discord.gg/F34cRU8", inline=False + name="Want Modmail in Your Server?", + value="Installation guide on GitHub (https://github.com/kyb3r/modmail) " + "and join our discord server (https://discord.gg/F34cRU8)!", inline=False ) embed.add_field( - name="Donate", - value="Support this bot on [`Patreon`](https://patreon.com/kyber).", + name="Support the Developers", + value="This bot is completely free for everyone. We rely on kind individuals " + "like you to support us on [`Patreon`](https://patreon.com/kyber) (includes perks!) " + "to keep this bot free forever!", ) embed.set_footer(text=footer) @@ -648,12 +641,11 @@ async def loop_presence(self): """Set presence to the configured value every 45 minutes.""" # TODO: Does this even work? presence = await self.set_presence() - logger.debug(f'{presence["activity"][1]} {presence["status"][1]}') + logger.debug('Loop... %s - %s', presence["activity"][1], presence["status"][1]) @loop_presence.before_loop async def before_loop_presence(self): await self.bot.wait_for_connected() - logger.debug("Starting metadata loop.") logger.line() presence = await self.set_presence() logger.info(presence["activity"][1]) @@ -771,17 +763,15 @@ async def config_set(self, ctx, key: str.lower, *, value: str): if key in keys: try: - value, value_text = await self.bot.config.clean_data(key, value) - except InvalidConfigError as exc: - embed = exc.embed - else: - self.bot.config[key] = value + self.bot.config.set(key, value) await self.bot.config.update() embed = Embed( title="Success", color=self.bot.main_color, - description=f"Set `{key}` to `{value_text}`", + description=f"Set `{key}` to `{self.bot.config[key]}`.", ) + except InvalidConfigError as exc: + embed = exc.embed else: embed = Embed( title="Error", @@ -795,7 +785,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): @config.command(name="remove", aliases=["del", "delete"]) @checks.has_permissions(PermissionLevel.OWNER) - async def config_remove(self, ctx, key: str.lower): + async def config_remove(self, ctx, *, key: str.lower): """Delete a set configuration variable.""" keys = self.bot.config.public_keys if key in keys: @@ -819,7 +809,7 @@ async def config_remove(self, ctx, key: str.lower): @config.command(name="get") @checks.has_permissions(PermissionLevel.OWNER) - async def config_get(self, ctx, key: str.lower = None): + async def config_get(self, ctx, *, key: str.lower = None): """ Show the configuration variables that are currently set. diff --git a/core/_color_data.py b/core/_color_data.py index 997a6a1d25..acfd834505 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -5,41 +5,41 @@ BASE_COLORS = { - "b": "#0000ff", - "g": "#007f00", - "r": "#ff0000", - "c": "#00bfbf", - "m": "#bf00bf", - "y": "#bfbf00", - "k": "#000000", - "w": "#ffffff", + "b": "0000ff", + "g": "007f00", + "r": "ff0000", + "c": "00bfbf", + "m": "bf00bf", + "y": "bfbf00", + "k": "000000", + "w": "ffffff", } # Discord native colors DISCORD_COLORS = { - "default": "#000000", - "teal": "#1abc9c", - "dark teal": "#11806a", - "green": "#2ecc71", - "dark green": "#1f8b4c", - "blue": "#3498db", - "dark blue": "#206694", - "purple": "#9b59b6", - "dark purple": "#71368a", - "magenta": "#e91e63", - "dark magenta": "#ad1457", - "gold": "#f1c40f", - "dark gold": "#c27c0e", - "orange": "#e67e22", - "dark orange": "#a84300", - "red": "#e74c3c", - "dark red": "#992d22", - "lighter gray": "#95a5a6", - "darker gray": "#546e7a", - "light gray": "#979c9f", - "dark gray": "#607d8b", - "blurple": "#7289da", - "grayple": "#99aab5" + "default": "000000", + "teal": "1abc9c", + "dark teal": "11806a", + "green": "2ecc71", + "dark green": "1f8b4c", + "blue": "3498db", + "dark blue": "206694", + "purple": "9b59b6", + "dark purple": "71368a", + "magenta": "e91e63", + "dark magenta": "ad1457", + "gold": "f1c40f", + "dark gold": "c27c0e", + "orange": "e67e22", + "dark orange": "a84300", + "red": "e74c3c", + "dark red": "992d22", + "lighter gray": "95a5a6", + "darker gray": "546e7a", + "light gray": "979c9f", + "dark gray": "607d8b", + "blurple": "7289da", + "grayple": "99aab5" } # Normalize name to "discord:" to avoid name collisions. @@ -48,16 +48,16 @@ # These colors are from Tableau TABLEAU_COLORS = { - "blue": "#1f77b4", - "orange": "#ff7f0e", - "green": "#2ca02c", - "red": "#d62728", - "purple": "#9467bd", - "brown": "#8c564b", - "pink": "#e377c2", - "gray": "#7f7f7f", - "olive": "#bcbd22", - "cyan": "#17becf", + "blue": "1f77b4", + "orange": "ff7f0e", + "green": "2ca02c", + "red": "d62728", + "purple": "9467bd", + "brown": "8c564b", + "pink": "e377c2", + "gray": "7f7f7f", + "olive": "bcbd22", + "cyan": "17becf", } # Normalize name to "tab:" to avoid name collisions. @@ -72,955 +72,955 @@ # # License: http://creativecommons.org/publicdomain/zero/1.0/ XKCD_COLORS = { - "cloudy blue": "#acc2d9", - "dark pastel green": "#56ae57", - "dust": "#b2996e", - "electric lime": "#a8ff04", - "fresh green": "#69d84f", - "light eggplant": "#894585", - "nasty green": "#70b23f", - "really light blue": "#d4ffff", - "tea": "#65ab7c", - "warm purple": "#952e8f", - "yellowish tan": "#fcfc81", - "cement": "#a5a391", - "dark grass green": "#388004", - "dusty teal": "#4c9085", - "grey teal": "#5e9b8a", - "macaroni and cheese": "#efb435", - "pinkish tan": "#d99b82", - "spruce": "#0a5f38", - "strong blue": "#0c06f7", - "toxic green": "#61de2a", - "windows blue": "#3778bf", - "blue blue": "#2242c7", - "blue with a hint of purple": "#533cc6", - "booger": "#9bb53c", - "bright sea green": "#05ffa6", - "dark green blue": "#1f6357", - "deep turquoise": "#017374", - "green teal": "#0cb577", - "strong pink": "#ff0789", - "bland": "#afa88b", - "deep aqua": "#08787f", - "lavender pink": "#dd85d7", - "light moss green": "#a6c875", - "light seafoam green": "#a7ffb5", - "olive yellow": "#c2b709", - "pig pink": "#e78ea5", - "deep lilac": "#966ebd", - "desert": "#ccad60", - "dusty lavender": "#ac86a8", - "purpley grey": "#947e94", - "purply": "#983fb2", - "candy pink": "#ff63e9", - "light pastel green": "#b2fba5", - "boring green": "#63b365", - "kiwi green": "#8ee53f", - "light grey green": "#b7e1a1", - "orange pink": "#ff6f52", - "tea green": "#bdf8a3", - "very light brown": "#d3b683", - "egg shell": "#fffcc4", - "eggplant purple": "#430541", - "powder pink": "#ffb2d0", - "reddish grey": "#997570", - "baby shit brown": "#ad900d", - "liliac": "#c48efd", - "stormy blue": "#507b9c", - "ugly brown": "#7d7103", - "custard": "#fffd78", - "darkish pink": "#da467d", - "deep brown": "#410200", - "greenish beige": "#c9d179", - "manilla": "#fffa86", - "off blue": "#5684ae", - "battleship grey": "#6b7c85", - "browny green": "#6f6c0a", - "bruise": "#7e4071", - "kelley green": "#009337", - "sickly yellow": "#d0e429", - "sunny yellow": "#fff917", - "azul": "#1d5dec", - "darkgreen": "#054907", - "green/yellow": "#b5ce08", - "lichen": "#8fb67b", - "light light green": "#c8ffb0", - "pale gold": "#fdde6c", - "sun yellow": "#ffdf22", - "tan green": "#a9be70", - "burple": "#6832e3", - "butterscotch": "#fdb147", - "toupe": "#c7ac7d", - "dark cream": "#fff39a", - "indian red": "#850e04", - "light lavendar": "#efc0fe", - "poison green": "#40fd14", - "baby puke green": "#b6c406", - "bright yellow green": "#9dff00", - "charcoal grey": "#3c4142", - "squash": "#f2ab15", - "cinnamon": "#ac4f06", - "light pea green": "#c4fe82", - "radioactive green": "#2cfa1f", - "raw sienna": "#9a6200", - "baby purple": "#ca9bf7", - "cocoa": "#875f42", - "light royal blue": "#3a2efe", - "orangeish": "#fd8d49", - "rust brown": "#8b3103", - "sand brown": "#cba560", - "swamp": "#698339", - "tealish green": "#0cdc73", - "burnt siena": "#b75203", - "camo": "#7f8f4e", - "dusk blue": "#26538d", - "fern": "#63a950", - "old rose": "#c87f89", - "pale light green": "#b1fc99", - "peachy pink": "#ff9a8a", - "rosy pink": "#f6688e", - "light bluish green": "#76fda8", - "light bright green": "#53fe5c", - "light neon green": "#4efd54", - "light seafoam": "#a0febf", - "tiffany blue": "#7bf2da", - "washed out green": "#bcf5a6", - "browny orange": "#ca6b02", - "nice blue": "#107ab0", - "sapphire": "#2138ab", - "greyish teal": "#719f91", - "orangey yellow": "#fdb915", - "parchment": "#fefcaf", - "straw": "#fcf679", - "very dark brown": "#1d0200", - "terracota": "#cb6843", - "ugly blue": "#31668a", - "clear blue": "#247afd", - "creme": "#ffffb6", - "foam green": "#90fda9", - "grey/green": "#86a17d", - "light gold": "#fddc5c", - "seafoam blue": "#78d1b6", - "topaz": "#13bbaf", - "violet pink": "#fb5ffc", - "wintergreen": "#20f986", - "yellow tan": "#ffe36e", - "dark fuchsia": "#9d0759", - "indigo blue": "#3a18b1", - "light yellowish green": "#c2ff89", - "pale magenta": "#d767ad", - "rich purple": "#720058", - "sunflower yellow": "#ffda03", - "green/blue": "#01c08d", - "leather": "#ac7434", - "racing green": "#014600", - "vivid purple": "#9900fa", - "dark royal blue": "#02066f", - "hazel": "#8e7618", - "muted pink": "#d1768f", - "booger green": "#96b403", - "canary": "#fdff63", - "cool grey": "#95a3a6", - "dark taupe": "#7f684e", - "darkish purple": "#751973", - "true green": "#089404", - "coral pink": "#ff6163", - "dark sage": "#598556", - "dark slate blue": "#214761", - "flat blue": "#3c73a8", - "mushroom": "#ba9e88", - "rich blue": "#021bf9", - "dirty purple": "#734a65", - "greenblue": "#23c48b", - "icky green": "#8fae22", - "light khaki": "#e6f2a2", - "warm blue": "#4b57db", - "dark hot pink": "#d90166", - "deep sea blue": "#015482", - "carmine": "#9d0216", - "dark yellow green": "#728f02", - "pale peach": "#ffe5ad", - "plum purple": "#4e0550", - "golden rod": "#f9bc08", - "neon red": "#ff073a", - "old pink": "#c77986", - "very pale blue": "#d6fffe", - "blood orange": "#fe4b03", - "grapefruit": "#fd5956", - "sand yellow": "#fce166", - "clay brown": "#b2713d", - "dark blue grey": "#1f3b4d", - "flat green": "#699d4c", - "light green blue": "#56fca2", - "warm pink": "#fb5581", - "dodger blue": "#3e82fc", - "gross green": "#a0bf16", - "ice": "#d6fffa", - "metallic blue": "#4f738e", - "pale salmon": "#ffb19a", - "sap green": "#5c8b15", - "algae": "#54ac68", - "bluey grey": "#89a0b0", - "greeny grey": "#7ea07a", - "highlighter green": "#1bfc06", - "light light blue": "#cafffb", - "light mint": "#b6ffbb", - "raw umber": "#a75e09", - "vivid blue": "#152eff", - "deep lavender": "#8d5eb7", - "dull teal": "#5f9e8f", - "light greenish blue": "#63f7b4", - "mud green": "#606602", - "pinky": "#fc86aa", - "red wine": "#8c0034", - "shit green": "#758000", - "tan brown": "#ab7e4c", - "darkblue": "#030764", - "rosa": "#fe86a4", - "lipstick": "#d5174e", - "pale mauve": "#fed0fc", - "claret": "#680018", - "dandelion": "#fedf08", - "orangered": "#fe420f", - "poop green": "#6f7c00", - "ruby": "#ca0147", - "dark": "#1b2431", - "greenish turquoise": "#00fbb0", - "pastel red": "#db5856", - "piss yellow": "#ddd618", - "bright cyan": "#41fdfe", - "dark coral": "#cf524e", - "algae green": "#21c36f", - "darkish red": "#a90308", - "reddy brown": "#6e1005", - "blush pink": "#fe828c", - "camouflage green": "#4b6113", - "lawn green": "#4da409", - "putty": "#beae8a", - "vibrant blue": "#0339f8", - "dark sand": "#a88f59", - "purple/blue": "#5d21d0", - "saffron": "#feb209", - "twilight": "#4e518b", - "warm brown": "#964e02", - "bluegrey": "#85a3b2", - "bubble gum pink": "#ff69af", - "duck egg blue": "#c3fbf4", - "greenish cyan": "#2afeb7", - "petrol": "#005f6a", - "royal": "#0c1793", - "butter": "#ffff81", - "dusty orange": "#f0833a", - "off yellow": "#f1f33f", - "pale olive green": "#b1d27b", - "orangish": "#fc824a", - "leaf": "#71aa34", - "light blue grey": "#b7c9e2", - "dried blood": "#4b0101", - "lightish purple": "#a552e6", - "rusty red": "#af2f0d", - "lavender blue": "#8b88f8", - "light grass green": "#9af764", - "light mint green": "#a6fbb2", - "sunflower": "#ffc512", - "velvet": "#750851", - "brick orange": "#c14a09", - "lightish red": "#fe2f4a", - "pure blue": "#0203e2", - "twilight blue": "#0a437a", - "violet red": "#a50055", - "yellowy brown": "#ae8b0c", - "carnation": "#fd798f", - "muddy yellow": "#bfac05", - "dark seafoam green": "#3eaf76", - "deep rose": "#c74767", - "dusty red": "#b9484e", - "grey/blue": "#647d8e", - "lemon lime": "#bffe28", - "purple/pink": "#d725de", - "brown yellow": "#b29705", - "purple brown": "#673a3f", - "wisteria": "#a87dc2", - "banana yellow": "#fafe4b", - "lipstick red": "#c0022f", - "water blue": "#0e87cc", - "brown grey": "#8d8468", - "vibrant purple": "#ad03de", - "baby green": "#8cff9e", - "barf green": "#94ac02", - "eggshell blue": "#c4fff7", - "sandy yellow": "#fdee73", - "cool green": "#33b864", - "pale": "#fff9d0", - "blue/grey": "#758da3", - "hot magenta": "#f504c9", - "greyblue": "#77a1b5", - "purpley": "#8756e4", - "baby shit green": "#889717", - "brownish pink": "#c27e79", - "dark aquamarine": "#017371", - "diarrhea": "#9f8303", - "light mustard": "#f7d560", - "pale sky blue": "#bdf6fe", - "turtle green": "#75b84f", - "bright olive": "#9cbb04", - "dark grey blue": "#29465b", - "greeny brown": "#696006", - "lemon green": "#adf802", - "light periwinkle": "#c1c6fc", - "seaweed green": "#35ad6b", - "sunshine yellow": "#fffd37", - "ugly purple": "#a442a0", - "medium pink": "#f36196", - "puke brown": "#947706", - "very light pink": "#fff4f2", - "viridian": "#1e9167", - "bile": "#b5c306", - "faded yellow": "#feff7f", - "very pale green": "#cffdbc", - "vibrant green": "#0add08", - "bright lime": "#87fd05", - "spearmint": "#1ef876", - "light aquamarine": "#7bfdc7", - "light sage": "#bcecac", - "yellowgreen": "#bbf90f", - "baby poo": "#ab9004", - "dark seafoam": "#1fb57a", - "deep teal": "#00555a", - "heather": "#a484ac", - "rust orange": "#c45508", - "dirty blue": "#3f829d", - "fern green": "#548d44", - "bright lilac": "#c95efb", - "weird green": "#3ae57f", - "peacock blue": "#016795", - "avocado green": "#87a922", - "faded orange": "#f0944d", - "grape purple": "#5d1451", - "hot green": "#25ff29", - "lime yellow": "#d0fe1d", - "mango": "#ffa62b", - "shamrock": "#01b44c", - "bubblegum": "#ff6cb5", - "purplish brown": "#6b4247", - "vomit yellow": "#c7c10c", - "pale cyan": "#b7fffa", - "key lime": "#aeff6e", - "tomato red": "#ec2d01", - "lightgreen": "#76ff7b", - "merlot": "#730039", - "night blue": "#040348", - "purpleish pink": "#df4ec8", - "apple": "#6ecb3c", - "baby poop green": "#8f9805", - "green apple": "#5edc1f", - "heliotrope": "#d94ff5", - "yellow/green": "#c8fd3d", - "almost black": "#070d0d", - "cool blue": "#4984b8", - "leafy green": "#51b73b", - "mustard brown": "#ac7e04", - "dusk": "#4e5481", - "dull brown": "#876e4b", - "frog green": "#58bc08", - "vivid green": "#2fef10", - "bright light green": "#2dfe54", - "fluro green": "#0aff02", - "kiwi": "#9cef43", - "seaweed": "#18d17b", - "navy green": "#35530a", - "ultramarine blue": "#1805db", - "iris": "#6258c4", - "pastel orange": "#ff964f", - "yellowish orange": "#ffab0f", - "perrywinkle": "#8f8ce7", - "tealish": "#24bca8", - "dark plum": "#3f012c", - "pear": "#cbf85f", - "pinkish orange": "#ff724c", - "midnight purple": "#280137", - "light urple": "#b36ff6", - "dark mint": "#48c072", - "greenish tan": "#bccb7a", - "light burgundy": "#a8415b", - "turquoise blue": "#06b1c4", - "ugly pink": "#cd7584", - "sandy": "#f1da7a", - "electric pink": "#ff0490", - "muted purple": "#805b87", - "mid green": "#50a747", - "greyish": "#a8a495", - "neon yellow": "#cfff04", - "banana": "#ffff7e", - "carnation pink": "#ff7fa7", - "tomato": "#ef4026", - "sea": "#3c9992", - "muddy brown": "#886806", - "turquoise green": "#04f489", - "buff": "#fef69e", - "fawn": "#cfaf7b", - "muted blue": "#3b719f", - "pale rose": "#fdc1c5", - "dark mint green": "#20c073", - "amethyst": "#9b5fc0", - "blue/green": "#0f9b8e", - "chestnut": "#742802", - "sick green": "#9db92c", - "pea": "#a4bf20", - "rusty orange": "#cd5909", - "stone": "#ada587", - "rose red": "#be013c", - "pale aqua": "#b8ffeb", - "deep orange": "#dc4d01", - "earth": "#a2653e", - "mossy green": "#638b27", - "grassy green": "#419c03", - "pale lime green": "#b1ff65", - "light grey blue": "#9dbcd4", - "pale grey": "#fdfdfe", - "asparagus": "#77ab56", - "blueberry": "#464196", - "purple red": "#990147", - "pale lime": "#befd73", - "greenish teal": "#32bf84", - "caramel": "#af6f09", - "deep magenta": "#a0025c", - "light peach": "#ffd8b1", - "milk chocolate": "#7f4e1e", - "ocher": "#bf9b0c", - "off green": "#6ba353", - "purply pink": "#f075e6", - "lightblue": "#7bc8f6", - "dusky blue": "#475f94", - "golden": "#f5bf03", - "light beige": "#fffeb6", - "butter yellow": "#fffd74", - "dusky purple": "#895b7b", - "french blue": "#436bad", - "ugly yellow": "#d0c101", - "greeny yellow": "#c6f808", - "orangish red": "#f43605", - "shamrock green": "#02c14d", - "orangish brown": "#b25f03", - "tree green": "#2a7e19", - "deep violet": "#490648", - "gunmetal": "#536267", - "blue/purple": "#5a06ef", - "cherry": "#cf0234", - "sandy brown": "#c4a661", - "warm grey": "#978a84", - "dark indigo": "#1f0954", - "midnight": "#03012d", - "bluey green": "#2bb179", - "grey pink": "#c3909b", - "soft purple": "#a66fb5", - "blood": "#770001", - "brown red": "#922b05", - "medium grey": "#7d7f7c", - "berry": "#990f4b", - "poo": "#8f7303", - "purpley pink": "#c83cb9", - "light salmon": "#fea993", - "snot": "#acbb0d", - "easter purple": "#c071fe", - "light yellow green": "#ccfd7f", - "dark navy blue": "#00022e", - "drab": "#828344", - "light rose": "#ffc5cb", - "rouge": "#ab1239", - "purplish red": "#b0054b", - "slime green": "#99cc04", - "baby poop": "#937c00", - "irish green": "#019529", - "pink/purple": "#ef1de7", - "dark navy": "#000435", - "greeny blue": "#42b395", - "light plum": "#9d5783", - "pinkish grey": "#c8aca9", - "dirty orange": "#c87606", - "rust red": "#aa2704", - "pale lilac": "#e4cbff", - "orangey red": "#fa4224", - "primary blue": "#0804f9", - "kermit green": "#5cb200", - "brownish purple": "#76424e", - "murky green": "#6c7a0e", - "wheat": "#fbdd7e", - "very dark purple": "#2a0134", - "bottle green": "#044a05", - "watermelon": "#fd4659", - "deep sky blue": "#0d75f8", - "fire engine red": "#fe0002", - "yellow ochre": "#cb9d06", - "pumpkin orange": "#fb7d07", - "pale olive": "#b9cc81", - "light lilac": "#edc8ff", - "lightish green": "#61e160", - "carolina blue": "#8ab8fe", - "mulberry": "#920a4e", - "shocking pink": "#fe02a2", - "auburn": "#9a3001", - "bright lime green": "#65fe08", - "celadon": "#befdb7", - "pinkish brown": "#b17261", - "poo brown": "#885f01", - "bright sky blue": "#02ccfe", - "celery": "#c1fd95", - "dirt brown": "#836539", - "strawberry": "#fb2943", - "dark lime": "#84b701", - "copper": "#b66325", - "medium brown": "#7f5112", - "muted green": "#5fa052", - "robin's egg": "#6dedfd", - "bright aqua": "#0bf9ea", - "bright lavender": "#c760ff", - "ivory": "#ffffcb", - "very light purple": "#f6cefc", - "light navy": "#155084", - "pink red": "#f5054f", - "olive brown": "#645403", - "poop brown": "#7a5901", - "mustard green": "#a8b504", - "ocean green": "#3d9973", - "very dark blue": "#000133", - "dusty green": "#76a973", - "light navy blue": "#2e5a88", - "minty green": "#0bf77d", - "adobe": "#bd6c48", - "barney": "#ac1db8", - "jade green": "#2baf6a", - "bright light blue": "#26f7fd", - "light lime": "#aefd6c", - "dark khaki": "#9b8f55", - "orange yellow": "#ffad01", - "ocre": "#c69c04", - "maize": "#f4d054", - "faded pink": "#de9dac", - "british racing green": "#05480d", - "sandstone": "#c9ae74", - "mud brown": "#60460f", - "light sea green": "#98f6b0", - "robin egg blue": "#8af1fe", - "aqua marine": "#2ee8bb", - "dark sea green": "#11875d", - "soft pink": "#fdb0c0", - "orangey brown": "#b16002", - "cherry red": "#f7022a", - "burnt yellow": "#d5ab09", - "brownish grey": "#86775f", - "camel": "#c69f59", - "purplish grey": "#7a687f", - "marine": "#042e60", - "greyish pink": "#c88d94", - "pale turquoise": "#a5fbd5", - "pastel yellow": "#fffe71", - "bluey purple": "#6241c7", - "canary yellow": "#fffe40", - "faded red": "#d3494e", - "sepia": "#985e2b", - "coffee": "#a6814c", - "bright magenta": "#ff08e8", - "mocha": "#9d7651", - "ecru": "#feffca", - "purpleish": "#98568d", - "cranberry": "#9e003a", - "darkish green": "#287c37", - "brown orange": "#b96902", - "dusky rose": "#ba6873", - "melon": "#ff7855", - "sickly green": "#94b21c", - "silver": "#c5c9c7", - "purply blue": "#661aee", - "purpleish blue": "#6140ef", - "hospital green": "#9be5aa", - "shit brown": "#7b5804", - "mid blue": "#276ab3", - "amber": "#feb308", - "easter green": "#8cfd7e", - "soft blue": "#6488ea", - "cerulean blue": "#056eee", - "golden brown": "#b27a01", - "bright turquoise": "#0ffef9", - "red pink": "#fa2a55", - "red purple": "#820747", - "greyish brown": "#7a6a4f", - "vermillion": "#f4320c", - "russet": "#a13905", - "steel grey": "#6f828a", - "lighter purple": "#a55af4", - "bright violet": "#ad0afd", - "prussian blue": "#004577", - "slate green": "#658d6d", - "dirty pink": "#ca7b80", - "dark blue green": "#005249", - "pine": "#2b5d34", - "yellowy green": "#bff128", - "dark gold": "#b59410", - "bluish": "#2976bb", - "darkish blue": "#014182", - "dull red": "#bb3f3f", - "pinky red": "#fc2647", - "bronze": "#a87900", - "pale teal": "#82cbb2", - "military green": "#667c3e", - "barbie pink": "#fe46a5", - "bubblegum pink": "#fe83cc", - "pea soup green": "#94a617", - "dark mustard": "#a88905", - "shit": "#7f5f00", - "medium purple": "#9e43a2", - "very dark green": "#062e03", - "dirt": "#8a6e45", - "dusky pink": "#cc7a8b", - "red violet": "#9e0168", - "lemon yellow": "#fdff38", - "pistachio": "#c0fa8b", - "dull yellow": "#eedc5b", - "dark lime green": "#7ebd01", - "denim blue": "#3b5b92", - "teal blue": "#01889f", - "lightish blue": "#3d7afd", - "purpley blue": "#5f34e7", - "light indigo": "#6d5acf", - "swamp green": "#748500", - "brown green": "#706c11", - "dark maroon": "#3c0008", - "hot purple": "#cb00f5", - "dark forest green": "#002d04", - "faded blue": "#658cbb", - "drab green": "#749551", - "light lime green": "#b9ff66", - "snot green": "#9dc100", - "yellowish": "#faee66", - "light blue green": "#7efbb3", - "bordeaux": "#7b002c", - "light mauve": "#c292a1", - "ocean": "#017b92", - "marigold": "#fcc006", - "muddy green": "#657432", - "dull orange": "#d8863b", - "steel": "#738595", - "electric purple": "#aa23ff", - "fluorescent green": "#08ff08", - "yellowish brown": "#9b7a01", - "blush": "#f29e8e", - "soft green": "#6fc276", - "bright orange": "#ff5b00", - "lemon": "#fdff52", - "purple grey": "#866f85", - "acid green": "#8ffe09", - "pale lavender": "#eecffe", - "violet blue": "#510ac9", - "light forest green": "#4f9153", - "burnt red": "#9f2305", - "khaki green": "#728639", - "cerise": "#de0c62", - "faded purple": "#916e99", - "apricot": "#ffb16d", - "dark olive green": "#3c4d03", - "grey brown": "#7f7053", - "green grey": "#77926f", - "true blue": "#010fcc", - "pale violet": "#ceaefa", - "periwinkle blue": "#8f99fb", - "light sky blue": "#c6fcff", - "blurple": "#5539cc", - "green brown": "#544e03", - "bluegreen": "#017a79", - "bright teal": "#01f9c6", - "brownish yellow": "#c9b003", - "pea soup": "#929901", - "forest": "#0b5509", - "barney purple": "#a00498", - "ultramarine": "#2000b1", - "purplish": "#94568c", - "puke yellow": "#c2be0e", - "bluish grey": "#748b97", - "dark periwinkle": "#665fd1", - "dark lilac": "#9c6da5", - "reddish": "#c44240", - "light maroon": "#a24857", - "dusty purple": "#825f87", - "terra cotta": "#c9643b", - "avocado": "#90b134", - "marine blue": "#01386a", - "teal green": "#25a36f", - "slate grey": "#59656d", - "lighter green": "#75fd63", - "electric green": "#21fc0d", - "dusty blue": "#5a86ad", - "golden yellow": "#fec615", - "bright yellow": "#fffd01", - "light lavender": "#dfc5fe", - "umber": "#b26400", - "poop": "#7f5e00", - "dark peach": "#de7e5d", - "jungle green": "#048243", - "eggshell": "#ffffd4", - "denim": "#3b638c", - "yellow brown": "#b79400", - "dull purple": "#84597e", - "chocolate brown": "#411900", - "wine red": "#7b0323", - "neon blue": "#04d9ff", - "dirty green": "#667e2c", - "light tan": "#fbeeac", - "ice blue": "#d7fffe", - "cadet blue": "#4e7496", - "dark mauve": "#874c62", - "very light blue": "#d5ffff", - "grey purple": "#826d8c", - "pastel pink": "#ffbacd", - "very light green": "#d1ffbd", - "dark sky blue": "#448ee4", - "evergreen": "#05472a", - "dull pink": "#d5869d", - "aubergine": "#3d0734", - "mahogany": "#4a0100", - "reddish orange": "#f8481c", - "deep green": "#02590f", - "vomit green": "#89a203", - "purple pink": "#e03fd8", - "dusty pink": "#d58a94", - "faded green": "#7bb274", - "camo green": "#526525", - "pinky purple": "#c94cbe", - "pink purple": "#db4bda", - "brownish red": "#9e3623", - "dark rose": "#b5485d", - "mud": "#735c12", - "brownish": "#9c6d57", - "emerald green": "#028f1e", - "pale brown": "#b1916e", - "dull blue": "#49759c", - "burnt umber": "#a0450e", - "medium green": "#39ad48", - "clay": "#b66a50", - "light aqua": "#8cffdb", - "light olive green": "#a4be5c", - "brownish orange": "#cb7723", - "dark aqua": "#05696b", - "purplish pink": "#ce5dae", - "dark salmon": "#c85a53", - "greenish grey": "#96ae8d", - "jade": "#1fa774", - "ugly green": "#7a9703", - "dark beige": "#ac9362", - "emerald": "#01a049", - "pale red": "#d9544d", - "light magenta": "#fa5ff7", - "sky": "#82cafc", - "light cyan": "#acfffc", - "yellow orange": "#fcb001", - "reddish purple": "#910951", - "reddish pink": "#fe2c54", - "orchid": "#c875c4", - "dirty yellow": "#cdc50a", - "orange red": "#fd411e", - "deep red": "#9a0200", - "orange brown": "#be6400", - "cobalt blue": "#030aa7", - "neon pink": "#fe019a", - "rose pink": "#f7879a", - "greyish purple": "#887191", - "raspberry": "#b00149", - "aqua green": "#12e193", - "salmon pink": "#fe7b7c", - "tangerine": "#ff9408", - "brownish green": "#6a6e09", - "red brown": "#8b2e16", - "greenish brown": "#696112", - "pumpkin": "#e17701", - "pine green": "#0a481e", - "charcoal": "#343837", - "baby pink": "#ffb7ce", - "cornflower": "#6a79f7", - "blue violet": "#5d06e9", - "chocolate": "#3d1c02", - "greyish green": "#82a67d", - "scarlet": "#be0119", - "green yellow": "#c9ff27", - "dark olive": "#373e02", - "sienna": "#a9561e", - "pastel purple": "#caa0ff", - "terracotta": "#ca6641", - "aqua blue": "#02d8e9", - "sage green": "#88b378", - "blood red": "#980002", - "deep pink": "#cb0162", - "grass": "#5cac2d", - "moss": "#769958", - "pastel blue": "#a2bffe", - "bluish green": "#10a674", - "green blue": "#06b48b", - "dark tan": "#af884a", - "greenish blue": "#0b8b87", - "pale orange": "#ffa756", - "vomit": "#a2a415", - "forrest green": "#154406", - "dark lavender": "#856798", - "dark violet": "#34013f", - "purple blue": "#632de9", - "dark cyan": "#0a888a", - "olive drab": "#6f7632", - "pinkish": "#d46a7e", - "cobalt": "#1e488f", - "neon purple": "#bc13fe", - "light turquoise": "#7ef4cc", - "apple green": "#76cd26", - "dull green": "#74a662", - "wine": "#80013f", - "powder blue": "#b1d1fc", - "off white": "#ffffe4", - "electric blue": "#0652ff", - "dark turquoise": "#045c5a", - "blue purple": "#5729ce", - "azure": "#069af3", - "bright red": "#ff000d", - "pinkish red": "#f10c45", - "cornflower blue": "#5170d7", - "light olive": "#acbf69", - "grape": "#6c3461", - "greyish blue": "#5e819d", - "purplish blue": "#601ef9", - "yellowish green": "#b0dd16", - "greenish yellow": "#cdfd02", - "medium blue": "#2c6fbb", - "dusty rose": "#c0737a", - "light violet": "#d6b4fc", - "midnight blue": "#020035", - "bluish purple": "#703be7", - "red orange": "#fd3c06", - "dark magenta": "#960056", - "greenish": "#40a368", - "ocean blue": "#03719c", - "coral": "#fc5a50", - "cream": "#ffffc2", - "reddish brown": "#7f2b0a", - "burnt sienna": "#b04e0f", - "brick": "#a03623", - "sage": "#87ae73", - "grey green": "#789b73", - "white": "#ffffff", - "robin's egg blue": "#98eff9", - "moss green": "#658b38", - "steel blue": "#5a7d9a", - "eggplant": "#380835", - "light yellow": "#fffe7a", - "leaf green": "#5ca904", - "light grey": "#d8dcd6", - "puke": "#a5a502", - "pinkish purple": "#d648d7", - "sea blue": "#047495", - "pale purple": "#b790d4", - "slate blue": "#5b7c99", - "blue grey": "#607c8e", - "hunter green": "#0b4008", - "fuchsia": "#ed0dd9", - "crimson": "#8c000f", - "pale yellow": "#ffff84", - "ochre": "#bf9005", - "mustard yellow": "#d2bd0a", - "light red": "#ff474c", - "cerulean": "#0485d1", - "pale pink": "#ffcfdc", - "deep blue": "#040273", - "rust": "#a83c09", - "light teal": "#90e4c1", - "slate": "#516572", - "goldenrod": "#fac205", - "dark yellow": "#d5b60a", - "dark grey": "#363737", - "army green": "#4b5d16", - "grey blue": "#6b8ba4", - "seafoam": "#80f9ad", - "puce": "#a57e52", - "spring green": "#a9f971", - "dark orange": "#c65102", - "sand": "#e2ca76", - "pastel green": "#b0ff9d", - "mint": "#9ffeb0", - "light orange": "#fdaa48", - "bright pink": "#fe01b1", - "chartreuse": "#c1f80a", - "deep purple": "#36013f", - "dark brown": "#341c02", - "taupe": "#b9a281", - "pea green": "#8eab12", - "puke green": "#9aae07", - "kelly green": "#02ab2e", - "seafoam green": "#7af9ab", - "blue green": "#137e6d", - "khaki": "#aaa662", - "burgundy": "#610023", - "dark teal": "#014d4e", - "brick red": "#8f1402", - "royal purple": "#4b006e", - "plum": "#580f41", - "mint green": "#8fff9f", - "gold": "#dbb40c", - "baby blue": "#a2cffe", - "yellow green": "#c0fb2d", - "bright purple": "#be03fd", - "dark red": "#840000", - "pale blue": "#d0fefe", - "grass green": "#3f9b0b", - "navy": "#01153e", - "aquamarine": "#04d8b2", - "burnt orange": "#c04e01", - "neon green": "#0cff0c", - "bright blue": "#0165fc", - "rose": "#cf6275", - "light pink": "#ffd1df", - "mustard": "#ceb301", - "indigo": "#380282", - "lime": "#aaff32", - "sea green": "#53fca1", - "periwinkle": "#8e82fe", - "dark pink": "#cb416b", - "olive green": "#677a04", - "peach": "#ffb07c", - "pale green": "#c7fdb5", - "light brown": "#ad8150", - "hot pink": "#ff028d", - "black": "#000000", - "lilac": "#cea2fd", - "navy blue": "#001146", - "royal blue": "#0504aa", - "beige": "#e6daa6", - "salmon": "#ff796c", - "olive": "#6e750e", - "maroon": "#650021", - "bright green": "#01ff07", - "dark purple": "#35063e", - "mauve": "#ae7181", - "forest green": "#06470c", - "aqua": "#13eac9", - "cyan": "#00ffff", - "tan": "#d1b26f", - "dark blue": "#00035b", - "lavender": "#c79fef", - "turquoise": "#06c2ac", - "dark green": "#033500", - "violet": "#9a0eea", - "light purple": "#bf77f6", - "lime green": "#89fe05", - "grey": "#929591", - "sky blue": "#75bbfd", - "yellow": "#ffff14", - "magenta": "#c20078", - "light green": "#96f97b", - "orange": "#f97306", - "teal": "#029386", - "light blue": "#95d0fc", - "red": "#e50000", - "brown": "#653700", - "pink": "#ff81c0", - "blue": "#0343df", - "green": "#15b01a", - "purple": "#7e1e9c", + "cloudy blue": "acc2d9", + "dark pastel green": "56ae57", + "dust": "b2996e", + "electric lime": "a8ff04", + "fresh green": "69d84f", + "light eggplant": "894585", + "nasty green": "70b23f", + "really light blue": "d4ffff", + "tea": "65ab7c", + "warm purple": "952e8f", + "yellowish tan": "fcfc81", + "cement": "a5a391", + "dark grass green": "388004", + "dusty teal": "4c9085", + "grey teal": "5e9b8a", + "macaroni and cheese": "efb435", + "pinkish tan": "d99b82", + "spruce": "0a5f38", + "strong blue": "0c06f7", + "toxic green": "61de2a", + "windows blue": "3778bf", + "blue blue": "2242c7", + "blue with a hint of purple": "533cc6", + "booger": "9bb53c", + "bright sea green": "05ffa6", + "dark green blue": "1f6357", + "deep turquoise": "017374", + "green teal": "0cb577", + "strong pink": "ff0789", + "bland": "afa88b", + "deep aqua": "08787f", + "lavender pink": "dd85d7", + "light moss green": "a6c875", + "light seafoam green": "a7ffb5", + "olive yellow": "c2b709", + "pig pink": "e78ea5", + "deep lilac": "966ebd", + "desert": "ccad60", + "dusty lavender": "ac86a8", + "purpley grey": "947e94", + "purply": "983fb2", + "candy pink": "ff63e9", + "light pastel green": "b2fba5", + "boring green": "63b365", + "kiwi green": "8ee53f", + "light grey green": "b7e1a1", + "orange pink": "ff6f52", + "tea green": "bdf8a3", + "very light brown": "d3b683", + "egg shell": "fffcc4", + "eggplant purple": "430541", + "powder pink": "ffb2d0", + "reddish grey": "997570", + "baby shit brown": "ad900d", + "liliac": "c48efd", + "stormy blue": "507b9c", + "ugly brown": "7d7103", + "custard": "fffd78", + "darkish pink": "da467d", + "deep brown": "410200", + "greenish beige": "c9d179", + "manilla": "fffa86", + "off blue": "5684ae", + "battleship grey": "6b7c85", + "browny green": "6f6c0a", + "bruise": "7e4071", + "kelley green": "009337", + "sickly yellow": "d0e429", + "sunny yellow": "fff917", + "azul": "1d5dec", + "darkgreen": "054907", + "green/yellow": "b5ce08", + "lichen": "8fb67b", + "light light green": "c8ffb0", + "pale gold": "fdde6c", + "sun yellow": "ffdf22", + "tan green": "a9be70", + "burple": "6832e3", + "butterscotch": "fdb147", + "toupe": "c7ac7d", + "dark cream": "fff39a", + "indian red": "850e04", + "light lavendar": "efc0fe", + "poison green": "40fd14", + "baby puke green": "b6c406", + "bright yellow green": "9dff00", + "charcoal grey": "3c4142", + "squash": "f2ab15", + "cinnamon": "ac4f06", + "light pea green": "c4fe82", + "radioactive green": "2cfa1f", + "raw sienna": "9a6200", + "baby purple": "ca9bf7", + "cocoa": "875f42", + "light royal blue": "3a2efe", + "orangeish": "fd8d49", + "rust brown": "8b3103", + "sand brown": "cba560", + "swamp": "698339", + "tealish green": "0cdc73", + "burnt siena": "b75203", + "camo": "7f8f4e", + "dusk blue": "26538d", + "fern": "63a950", + "old rose": "c87f89", + "pale light green": "b1fc99", + "peachy pink": "ff9a8a", + "rosy pink": "f6688e", + "light bluish green": "76fda8", + "light bright green": "53fe5c", + "light neon green": "4efd54", + "light seafoam": "a0febf", + "tiffany blue": "7bf2da", + "washed out green": "bcf5a6", + "browny orange": "ca6b02", + "nice blue": "107ab0", + "sapphire": "2138ab", + "greyish teal": "719f91", + "orangey yellow": "fdb915", + "parchment": "fefcaf", + "straw": "fcf679", + "very dark brown": "1d0200", + "terracota": "cb6843", + "ugly blue": "31668a", + "clear blue": "247afd", + "creme": "ffffb6", + "foam green": "90fda9", + "grey/green": "86a17d", + "light gold": "fddc5c", + "seafoam blue": "78d1b6", + "topaz": "13bbaf", + "violet pink": "fb5ffc", + "wintergreen": "20f986", + "yellow tan": "ffe36e", + "dark fuchsia": "9d0759", + "indigo blue": "3a18b1", + "light yellowish green": "c2ff89", + "pale magenta": "d767ad", + "rich purple": "720058", + "sunflower yellow": "ffda03", + "green/blue": "01c08d", + "leather": "ac7434", + "racing green": "014600", + "vivid purple": "9900fa", + "dark royal blue": "02066f", + "hazel": "8e7618", + "muted pink": "d1768f", + "booger green": "96b403", + "canary": "fdff63", + "cool grey": "95a3a6", + "dark taupe": "7f684e", + "darkish purple": "751973", + "true green": "089404", + "coral pink": "ff6163", + "dark sage": "598556", + "dark slate blue": "214761", + "flat blue": "3c73a8", + "mushroom": "ba9e88", + "rich blue": "021bf9", + "dirty purple": "734a65", + "greenblue": "23c48b", + "icky green": "8fae22", + "light khaki": "e6f2a2", + "warm blue": "4b57db", + "dark hot pink": "d90166", + "deep sea blue": "015482", + "carmine": "9d0216", + "dark yellow green": "728f02", + "pale peach": "ffe5ad", + "plum purple": "4e0550", + "golden rod": "f9bc08", + "neon red": "ff073a", + "old pink": "c77986", + "very pale blue": "d6fffe", + "blood orange": "fe4b03", + "grapefruit": "fd5956", + "sand yellow": "fce166", + "clay brown": "b2713d", + "dark blue grey": "1f3b4d", + "flat green": "699d4c", + "light green blue": "56fca2", + "warm pink": "fb5581", + "dodger blue": "3e82fc", + "gross green": "a0bf16", + "ice": "d6fffa", + "metallic blue": "4f738e", + "pale salmon": "ffb19a", + "sap green": "5c8b15", + "algae": "54ac68", + "bluey grey": "89a0b0", + "greeny grey": "7ea07a", + "highlighter green": "1bfc06", + "light light blue": "cafffb", + "light mint": "b6ffbb", + "raw umber": "a75e09", + "vivid blue": "152eff", + "deep lavender": "8d5eb7", + "dull teal": "5f9e8f", + "light greenish blue": "63f7b4", + "mud green": "606602", + "pinky": "fc86aa", + "red wine": "8c0034", + "shit green": "758000", + "tan brown": "ab7e4c", + "darkblue": "030764", + "rosa": "fe86a4", + "lipstick": "d5174e", + "pale mauve": "fed0fc", + "claret": "680018", + "dandelion": "fedf08", + "orangered": "fe420f", + "poop green": "6f7c00", + "ruby": "ca0147", + "dark": "1b2431", + "greenish turquoise": "00fbb0", + "pastel red": "db5856", + "piss yellow": "ddd618", + "bright cyan": "41fdfe", + "dark coral": "cf524e", + "algae green": "21c36f", + "darkish red": "a90308", + "reddy brown": "6e1005", + "blush pink": "fe828c", + "camouflage green": "4b6113", + "lawn green": "4da409", + "putty": "beae8a", + "vibrant blue": "0339f8", + "dark sand": "a88f59", + "purple/blue": "5d21d0", + "saffron": "feb209", + "twilight": "4e518b", + "warm brown": "964e02", + "bluegrey": "85a3b2", + "bubble gum pink": "ff69af", + "duck egg blue": "c3fbf4", + "greenish cyan": "2afeb7", + "petrol": "005f6a", + "royal": "0c1793", + "butter": "ffff81", + "dusty orange": "f0833a", + "off yellow": "f1f33f", + "pale olive green": "b1d27b", + "orangish": "fc824a", + "leaf": "71aa34", + "light blue grey": "b7c9e2", + "dried blood": "4b0101", + "lightish purple": "a552e6", + "rusty red": "af2f0d", + "lavender blue": "8b88f8", + "light grass green": "9af764", + "light mint green": "a6fbb2", + "sunflower": "ffc512", + "velvet": "750851", + "brick orange": "c14a09", + "lightish red": "fe2f4a", + "pure blue": "0203e2", + "twilight blue": "0a437a", + "violet red": "a50055", + "yellowy brown": "ae8b0c", + "carnation": "fd798f", + "muddy yellow": "bfac05", + "dark seafoam green": "3eaf76", + "deep rose": "c74767", + "dusty red": "b9484e", + "grey/blue": "647d8e", + "lemon lime": "bffe28", + "purple/pink": "d725de", + "brown yellow": "b29705", + "purple brown": "673a3f", + "wisteria": "a87dc2", + "banana yellow": "fafe4b", + "lipstick red": "c0022f", + "water blue": "0e87cc", + "brown grey": "8d8468", + "vibrant purple": "ad03de", + "baby green": "8cff9e", + "barf green": "94ac02", + "eggshell blue": "c4fff7", + "sandy yellow": "fdee73", + "cool green": "33b864", + "pale": "fff9d0", + "blue/grey": "758da3", + "hot magenta": "f504c9", + "greyblue": "77a1b5", + "purpley": "8756e4", + "baby shit green": "889717", + "brownish pink": "c27e79", + "dark aquamarine": "017371", + "diarrhea": "9f8303", + "light mustard": "f7d560", + "pale sky blue": "bdf6fe", + "turtle green": "75b84f", + "bright olive": "9cbb04", + "dark grey blue": "29465b", + "greeny brown": "696006", + "lemon green": "adf802", + "light periwinkle": "c1c6fc", + "seaweed green": "35ad6b", + "sunshine yellow": "fffd37", + "ugly purple": "a442a0", + "medium pink": "f36196", + "puke brown": "947706", + "very light pink": "fff4f2", + "viridian": "1e9167", + "bile": "b5c306", + "faded yellow": "feff7f", + "very pale green": "cffdbc", + "vibrant green": "0add08", + "bright lime": "87fd05", + "spearmint": "1ef876", + "light aquamarine": "7bfdc7", + "light sage": "bcecac", + "yellowgreen": "bbf90f", + "baby poo": "ab9004", + "dark seafoam": "1fb57a", + "deep teal": "00555a", + "heather": "a484ac", + "rust orange": "c45508", + "dirty blue": "3f829d", + "fern green": "548d44", + "bright lilac": "c95efb", + "weird green": "3ae57f", + "peacock blue": "016795", + "avocado green": "87a922", + "faded orange": "f0944d", + "grape purple": "5d1451", + "hot green": "25ff29", + "lime yellow": "d0fe1d", + "mango": "ffa62b", + "shamrock": "01b44c", + "bubblegum": "ff6cb5", + "purplish brown": "6b4247", + "vomit yellow": "c7c10c", + "pale cyan": "b7fffa", + "key lime": "aeff6e", + "tomato red": "ec2d01", + "lightgreen": "76ff7b", + "merlot": "730039", + "night blue": "040348", + "purpleish pink": "df4ec8", + "apple": "6ecb3c", + "baby poop green": "8f9805", + "green apple": "5edc1f", + "heliotrope": "d94ff5", + "yellow/green": "c8fd3d", + "almost black": "070d0d", + "cool blue": "4984b8", + "leafy green": "51b73b", + "mustard brown": "ac7e04", + "dusk": "4e5481", + "dull brown": "876e4b", + "frog green": "58bc08", + "vivid green": "2fef10", + "bright light green": "2dfe54", + "fluro green": "0aff02", + "kiwi": "9cef43", + "seaweed": "18d17b", + "navy green": "35530a", + "ultramarine blue": "1805db", + "iris": "6258c4", + "pastel orange": "ff964f", + "yellowish orange": "ffab0f", + "perrywinkle": "8f8ce7", + "tealish": "24bca8", + "dark plum": "3f012c", + "pear": "cbf85f", + "pinkish orange": "ff724c", + "midnight purple": "280137", + "light urple": "b36ff6", + "dark mint": "48c072", + "greenish tan": "bccb7a", + "light burgundy": "a8415b", + "turquoise blue": "06b1c4", + "ugly pink": "cd7584", + "sandy": "f1da7a", + "electric pink": "ff0490", + "muted purple": "805b87", + "mid green": "50a747", + "greyish": "a8a495", + "neon yellow": "cfff04", + "banana": "ffff7e", + "carnation pink": "ff7fa7", + "tomato": "ef4026", + "sea": "3c9992", + "muddy brown": "886806", + "turquoise green": "04f489", + "buff": "fef69e", + "fawn": "cfaf7b", + "muted blue": "3b719f", + "pale rose": "fdc1c5", + "dark mint green": "20c073", + "amethyst": "9b5fc0", + "blue/green": "0f9b8e", + "chestnut": "742802", + "sick green": "9db92c", + "pea": "a4bf20", + "rusty orange": "cd5909", + "stone": "ada587", + "rose red": "be013c", + "pale aqua": "b8ffeb", + "deep orange": "dc4d01", + "earth": "a2653e", + "mossy green": "638b27", + "grassy green": "419c03", + "pale lime green": "b1ff65", + "light grey blue": "9dbcd4", + "pale grey": "fdfdfe", + "asparagus": "77ab56", + "blueberry": "464196", + "purple red": "990147", + "pale lime": "befd73", + "greenish teal": "32bf84", + "caramel": "af6f09", + "deep magenta": "a0025c", + "light peach": "ffd8b1", + "milk chocolate": "7f4e1e", + "ocher": "bf9b0c", + "off green": "6ba353", + "purply pink": "f075e6", + "lightblue": "7bc8f6", + "dusky blue": "475f94", + "golden": "f5bf03", + "light beige": "fffeb6", + "butter yellow": "fffd74", + "dusky purple": "895b7b", + "french blue": "436bad", + "ugly yellow": "d0c101", + "greeny yellow": "c6f808", + "orangish red": "f43605", + "shamrock green": "02c14d", + "orangish brown": "b25f03", + "tree green": "2a7e19", + "deep violet": "490648", + "gunmetal": "536267", + "blue/purple": "5a06ef", + "cherry": "cf0234", + "sandy brown": "c4a661", + "warm grey": "978a84", + "dark indigo": "1f0954", + "midnight": "03012d", + "bluey green": "2bb179", + "grey pink": "c3909b", + "soft purple": "a66fb5", + "blood": "770001", + "brown red": "922b05", + "medium grey": "7d7f7c", + "berry": "990f4b", + "poo": "8f7303", + "purpley pink": "c83cb9", + "light salmon": "fea993", + "snot": "acbb0d", + "easter purple": "c071fe", + "light yellow green": "ccfd7f", + "dark navy blue": "00022e", + "drab": "828344", + "light rose": "ffc5cb", + "rouge": "ab1239", + "purplish red": "b0054b", + "slime green": "99cc04", + "baby poop": "937c00", + "irish green": "019529", + "pink/purple": "ef1de7", + "dark navy": "000435", + "greeny blue": "42b395", + "light plum": "9d5783", + "pinkish grey": "c8aca9", + "dirty orange": "c87606", + "rust red": "aa2704", + "pale lilac": "e4cbff", + "orangey red": "fa4224", + "primary blue": "0804f9", + "kermit green": "5cb200", + "brownish purple": "76424e", + "murky green": "6c7a0e", + "wheat": "fbdd7e", + "very dark purple": "2a0134", + "bottle green": "044a05", + "watermelon": "fd4659", + "deep sky blue": "0d75f8", + "fire engine red": "fe0002", + "yellow ochre": "cb9d06", + "pumpkin orange": "fb7d07", + "pale olive": "b9cc81", + "light lilac": "edc8ff", + "lightish green": "61e160", + "carolina blue": "8ab8fe", + "mulberry": "920a4e", + "shocking pink": "fe02a2", + "auburn": "9a3001", + "bright lime green": "65fe08", + "celadon": "befdb7", + "pinkish brown": "b17261", + "poo brown": "885f01", + "bright sky blue": "02ccfe", + "celery": "c1fd95", + "dirt brown": "836539", + "strawberry": "fb2943", + "dark lime": "84b701", + "copper": "b66325", + "medium brown": "7f5112", + "muted green": "5fa052", + "robin's egg": "6dedfd", + "bright aqua": "0bf9ea", + "bright lavender": "c760ff", + "ivory": "ffffcb", + "very light purple": "f6cefc", + "light navy": "155084", + "pink red": "f5054f", + "olive brown": "645403", + "poop brown": "7a5901", + "mustard green": "a8b504", + "ocean green": "3d9973", + "very dark blue": "000133", + "dusty green": "76a973", + "light navy blue": "2e5a88", + "minty green": "0bf77d", + "adobe": "bd6c48", + "barney": "ac1db8", + "jade green": "2baf6a", + "bright light blue": "26f7fd", + "light lime": "aefd6c", + "dark khaki": "9b8f55", + "orange yellow": "ffad01", + "ocre": "c69c04", + "maize": "f4d054", + "faded pink": "de9dac", + "british racing green": "05480d", + "sandstone": "c9ae74", + "mud brown": "60460f", + "light sea green": "98f6b0", + "robin egg blue": "8af1fe", + "aqua marine": "2ee8bb", + "dark sea green": "11875d", + "soft pink": "fdb0c0", + "orangey brown": "b16002", + "cherry red": "f7022a", + "burnt yellow": "d5ab09", + "brownish grey": "86775f", + "camel": "c69f59", + "purplish grey": "7a687f", + "marine": "042e60", + "greyish pink": "c88d94", + "pale turquoise": "a5fbd5", + "pastel yellow": "fffe71", + "bluey purple": "6241c7", + "canary yellow": "fffe40", + "faded red": "d3494e", + "sepia": "985e2b", + "coffee": "a6814c", + "bright magenta": "ff08e8", + "mocha": "9d7651", + "ecru": "feffca", + "purpleish": "98568d", + "cranberry": "9e003a", + "darkish green": "287c37", + "brown orange": "b96902", + "dusky rose": "ba6873", + "melon": "ff7855", + "sickly green": "94b21c", + "silver": "c5c9c7", + "purply blue": "661aee", + "purpleish blue": "6140ef", + "hospital green": "9be5aa", + "shit brown": "7b5804", + "mid blue": "276ab3", + "amber": "feb308", + "easter green": "8cfd7e", + "soft blue": "6488ea", + "cerulean blue": "056eee", + "golden brown": "b27a01", + "bright turquoise": "0ffef9", + "red pink": "fa2a55", + "red purple": "820747", + "greyish brown": "7a6a4f", + "vermillion": "f4320c", + "russet": "a13905", + "steel grey": "6f828a", + "lighter purple": "a55af4", + "bright violet": "ad0afd", + "prussian blue": "004577", + "slate green": "658d6d", + "dirty pink": "ca7b80", + "dark blue green": "005249", + "pine": "2b5d34", + "yellowy green": "bff128", + "dark gold": "b59410", + "bluish": "2976bb", + "darkish blue": "014182", + "dull red": "bb3f3f", + "pinky red": "fc2647", + "bronze": "a87900", + "pale teal": "82cbb2", + "military green": "667c3e", + "barbie pink": "fe46a5", + "bubblegum pink": "fe83cc", + "pea soup green": "94a617", + "dark mustard": "a88905", + "shit": "7f5f00", + "medium purple": "9e43a2", + "very dark green": "062e03", + "dirt": "8a6e45", + "dusky pink": "cc7a8b", + "red violet": "9e0168", + "lemon yellow": "fdff38", + "pistachio": "c0fa8b", + "dull yellow": "eedc5b", + "dark lime green": "7ebd01", + "denim blue": "3b5b92", + "teal blue": "01889f", + "lightish blue": "3d7afd", + "purpley blue": "5f34e7", + "light indigo": "6d5acf", + "swamp green": "748500", + "brown green": "706c11", + "dark maroon": "3c0008", + "hot purple": "cb00f5", + "dark forest green": "002d04", + "faded blue": "658cbb", + "drab green": "749551", + "light lime green": "b9ff66", + "snot green": "9dc100", + "yellowish": "faee66", + "light blue green": "7efbb3", + "bordeaux": "7b002c", + "light mauve": "c292a1", + "ocean": "017b92", + "marigold": "fcc006", + "muddy green": "657432", + "dull orange": "d8863b", + "steel": "738595", + "electric purple": "aa23ff", + "fluorescent green": "08ff08", + "yellowish brown": "9b7a01", + "blush": "f29e8e", + "soft green": "6fc276", + "bright orange": "ff5b00", + "lemon": "fdff52", + "purple grey": "866f85", + "acid green": "8ffe09", + "pale lavender": "eecffe", + "violet blue": "510ac9", + "light forest green": "4f9153", + "burnt red": "9f2305", + "khaki green": "728639", + "cerise": "de0c62", + "faded purple": "916e99", + "apricot": "ffb16d", + "dark olive green": "3c4d03", + "grey brown": "7f7053", + "green grey": "77926f", + "true blue": "010fcc", + "pale violet": "ceaefa", + "periwinkle blue": "8f99fb", + "light sky blue": "c6fcff", + "blurple": "5539cc", + "green brown": "544e03", + "bluegreen": "017a79", + "bright teal": "01f9c6", + "brownish yellow": "c9b003", + "pea soup": "929901", + "forest": "0b5509", + "barney purple": "a00498", + "ultramarine": "2000b1", + "purplish": "94568c", + "puke yellow": "c2be0e", + "bluish grey": "748b97", + "dark periwinkle": "665fd1", + "dark lilac": "9c6da5", + "reddish": "c44240", + "light maroon": "a24857", + "dusty purple": "825f87", + "terra cotta": "c9643b", + "avocado": "90b134", + "marine blue": "01386a", + "teal green": "25a36f", + "slate grey": "59656d", + "lighter green": "75fd63", + "electric green": "21fc0d", + "dusty blue": "5a86ad", + "golden yellow": "fec615", + "bright yellow": "fffd01", + "light lavender": "dfc5fe", + "umber": "b26400", + "poop": "7f5e00", + "dark peach": "de7e5d", + "jungle green": "048243", + "eggshell": "ffffd4", + "denim": "3b638c", + "yellow brown": "b79400", + "dull purple": "84597e", + "chocolate brown": "411900", + "wine red": "7b0323", + "neon blue": "04d9ff", + "dirty green": "667e2c", + "light tan": "fbeeac", + "ice blue": "d7fffe", + "cadet blue": "4e7496", + "dark mauve": "874c62", + "very light blue": "d5ffff", + "grey purple": "826d8c", + "pastel pink": "ffbacd", + "very light green": "d1ffbd", + "dark sky blue": "448ee4", + "evergreen": "05472a", + "dull pink": "d5869d", + "aubergine": "3d0734", + "mahogany": "4a0100", + "reddish orange": "f8481c", + "deep green": "02590f", + "vomit green": "89a203", + "purple pink": "e03fd8", + "dusty pink": "d58a94", + "faded green": "7bb274", + "camo green": "526525", + "pinky purple": "c94cbe", + "pink purple": "db4bda", + "brownish red": "9e3623", + "dark rose": "b5485d", + "mud": "735c12", + "brownish": "9c6d57", + "emerald green": "028f1e", + "pale brown": "b1916e", + "dull blue": "49759c", + "burnt umber": "a0450e", + "medium green": "39ad48", + "clay": "b66a50", + "light aqua": "8cffdb", + "light olive green": "a4be5c", + "brownish orange": "cb7723", + "dark aqua": "05696b", + "purplish pink": "ce5dae", + "dark salmon": "c85a53", + "greenish grey": "96ae8d", + "jade": "1fa774", + "ugly green": "7a9703", + "dark beige": "ac9362", + "emerald": "01a049", + "pale red": "d9544d", + "light magenta": "fa5ff7", + "sky": "82cafc", + "light cyan": "acfffc", + "yellow orange": "fcb001", + "reddish purple": "910951", + "reddish pink": "fe2c54", + "orchid": "c875c4", + "dirty yellow": "cdc50a", + "orange red": "fd411e", + "deep red": "9a0200", + "orange brown": "be6400", + "cobalt blue": "030aa7", + "neon pink": "fe019a", + "rose pink": "f7879a", + "greyish purple": "887191", + "raspberry": "b00149", + "aqua green": "12e193", + "salmon pink": "fe7b7c", + "tangerine": "ff9408", + "brownish green": "6a6e09", + "red brown": "8b2e16", + "greenish brown": "696112", + "pumpkin": "e17701", + "pine green": "0a481e", + "charcoal": "343837", + "baby pink": "ffb7ce", + "cornflower": "6a79f7", + "blue violet": "5d06e9", + "chocolate": "3d1c02", + "greyish green": "82a67d", + "scarlet": "be0119", + "green yellow": "c9ff27", + "dark olive": "373e02", + "sienna": "a9561e", + "pastel purple": "caa0ff", + "terracotta": "ca6641", + "aqua blue": "02d8e9", + "sage green": "88b378", + "blood red": "980002", + "deep pink": "cb0162", + "grass": "5cac2d", + "moss": "769958", + "pastel blue": "a2bffe", + "bluish green": "10a674", + "green blue": "06b48b", + "dark tan": "af884a", + "greenish blue": "0b8b87", + "pale orange": "ffa756", + "vomit": "a2a415", + "forrest green": "154406", + "dark lavender": "856798", + "dark violet": "34013f", + "purple blue": "632de9", + "dark cyan": "0a888a", + "olive drab": "6f7632", + "pinkish": "d46a7e", + "cobalt": "1e488f", + "neon purple": "bc13fe", + "light turquoise": "7ef4cc", + "apple green": "76cd26", + "dull green": "74a662", + "wine": "80013f", + "powder blue": "b1d1fc", + "off white": "ffffe4", + "electric blue": "0652ff", + "dark turquoise": "045c5a", + "blue purple": "5729ce", + "azure": "069af3", + "bright red": "ff000d", + "pinkish red": "f10c45", + "cornflower blue": "5170d7", + "light olive": "acbf69", + "grape": "6c3461", + "greyish blue": "5e819d", + "purplish blue": "601ef9", + "yellowish green": "b0dd16", + "greenish yellow": "cdfd02", + "medium blue": "2c6fbb", + "dusty rose": "c0737a", + "light violet": "d6b4fc", + "midnight blue": "020035", + "bluish purple": "703be7", + "red orange": "fd3c06", + "dark magenta": "960056", + "greenish": "40a368", + "ocean blue": "03719c", + "coral": "fc5a50", + "cream": "ffffc2", + "reddish brown": "7f2b0a", + "burnt sienna": "b04e0f", + "brick": "a03623", + "sage": "87ae73", + "grey green": "789b73", + "white": "ffffff", + "robin's egg blue": "98eff9", + "moss green": "658b38", + "steel blue": "5a7d9a", + "eggplant": "380835", + "light yellow": "fffe7a", + "leaf green": "5ca904", + "light grey": "d8dcd6", + "puke": "a5a502", + "pinkish purple": "d648d7", + "sea blue": "047495", + "pale purple": "b790d4", + "slate blue": "5b7c99", + "blue grey": "607c8e", + "hunter green": "0b4008", + "fuchsia": "ed0dd9", + "crimson": "8c000f", + "pale yellow": "ffff84", + "ochre": "bf9005", + "mustard yellow": "d2bd0a", + "light red": "ff474c", + "cerulean": "0485d1", + "pale pink": "ffcfdc", + "deep blue": "040273", + "rust": "a83c09", + "light teal": "90e4c1", + "slate": "516572", + "goldenrod": "fac205", + "dark yellow": "d5b60a", + "dark grey": "363737", + "army green": "4b5d16", + "grey blue": "6b8ba4", + "seafoam": "80f9ad", + "puce": "a57e52", + "spring green": "a9f971", + "dark orange": "c65102", + "sand": "e2ca76", + "pastel green": "b0ff9d", + "mint": "9ffeb0", + "light orange": "fdaa48", + "bright pink": "fe01b1", + "chartreuse": "c1f80a", + "deep purple": "36013f", + "dark brown": "341c02", + "taupe": "b9a281", + "pea green": "8eab12", + "puke green": "9aae07", + "kelly green": "02ab2e", + "seafoam green": "7af9ab", + "blue green": "137e6d", + "khaki": "aaa662", + "burgundy": "610023", + "dark teal": "014d4e", + "brick red": "8f1402", + "royal purple": "4b006e", + "plum": "580f41", + "mint green": "8fff9f", + "gold": "dbb40c", + "baby blue": "a2cffe", + "yellow green": "c0fb2d", + "bright purple": "be03fd", + "dark red": "840000", + "pale blue": "d0fefe", + "grass green": "3f9b0b", + "navy": "01153e", + "aquamarine": "04d8b2", + "burnt orange": "c04e01", + "neon green": "0cff0c", + "bright blue": "0165fc", + "rose": "cf6275", + "light pink": "ffd1df", + "mustard": "ceb301", + "indigo": "380282", + "lime": "aaff32", + "sea green": "53fca1", + "periwinkle": "8e82fe", + "dark pink": "cb416b", + "olive green": "677a04", + "peach": "ffb07c", + "pale green": "c7fdb5", + "light brown": "ad8150", + "hot pink": "ff028d", + "black": "000000", + "lilac": "cea2fd", + "navy blue": "001146", + "royal blue": "0504aa", + "beige": "e6daa6", + "salmon": "ff796c", + "olive": "6e750e", + "maroon": "650021", + "bright green": "01ff07", + "dark purple": "35063e", + "mauve": "ae7181", + "forest green": "06470c", + "aqua": "13eac9", + "cyan": "00ffff", + "tan": "d1b26f", + "dark blue": "00035b", + "lavender": "c79fef", + "turquoise": "06c2ac", + "dark green": "033500", + "violet": "9a0eea", + "light purple": "bf77f6", + "lime green": "89fe05", + "grey": "929591", + "sky blue": "75bbfd", + "yellow": "ffff14", + "magenta": "c20078", + "light green": "96f97b", + "orange": "f97306", + "teal": "029386", + "light blue": "95d0fc", + "red": "e50000", + "brown": "653700", + "pink": "ff81c0", + "blue": "0343df", + "green": "15b01a", + "purple": "7e1e9c", } # Normalize name to "xkcd:" to avoid name collisions. @@ -1029,154 +1029,154 @@ # https://drafts.csswg.org/css-color-4/#named-colors CSS4_COLORS = { - "aliceblue": "#F0F8FF", - "antiquewhite": "#FAEBD7", - "aqua": "#00FFFF", - "aquamarine": "#7FFFD4", - "azure": "#F0FFFF", - "beige": "#F5F5DC", - "bisque": "#FFE4C4", - "black": "#000000", - "blanchedalmond": "#FFEBCD", - "blue": "#0000FF", - "blueviolet": "#8A2BE2", - "brown": "#A52A2A", - "burlywood": "#DEB887", - "cadetblue": "#5F9EA0", - "chartreuse": "#7FFF00", - "chocolate": "#D2691E", - "coral": "#FF7F50", - "cornflowerblue": "#6495ED", - "cornsilk": "#FFF8DC", - "crimson": "#DC143C", - "cyan": "#00FFFF", - "darkblue": "#00008B", - "darkcyan": "#008B8B", - "darkgoldenrod": "#B8860B", - "darkgray": "#A9A9A9", - "darkgreen": "#006400", - "darkgrey": "#A9A9A9", - "darkkhaki": "#BDB76B", - "darkmagenta": "#8B008B", - "darkolivegreen": "#556B2F", - "darkorange": "#FF8C00", - "darkorchid": "#9932CC", - "darkred": "#8B0000", - "darksalmon": "#E9967A", - "darkseagreen": "#8FBC8F", - "darkslateblue": "#483D8B", - "darkslategray": "#2F4F4F", - "darkslategrey": "#2F4F4F", - "darkturquoise": "#00CED1", - "darkviolet": "#9400D3", - "deeppink": "#FF1493", - "deepskyblue": "#00BFFF", - "dimgray": "#696969", - "dimgrey": "#696969", - "dodgerblue": "#1E90FF", - "firebrick": "#B22222", - "floralwhite": "#FFFAF0", - "forestgreen": "#228B22", - "fuchsia": "#FF00FF", - "gainsboro": "#DCDCDC", - "ghostwhite": "#F8F8FF", - "gold": "#FFD700", - "goldenrod": "#DAA520", - "gray": "#808080", - "green": "#008000", - "greenyellow": "#ADFF2F", - "grey": "#808080", - "honeydew": "#F0FFF0", - "hotpink": "#FF69B4", - "indianred": "#CD5C5C", - "indigo": "#4B0082", - "ivory": "#FFFFF0", - "khaki": "#F0E68C", - "lavender": "#E6E6FA", - "lavenderblush": "#FFF0F5", - "lawngreen": "#7CFC00", - "lemonchiffon": "#FFFACD", - "lightblue": "#ADD8E6", - "lightcoral": "#F08080", - "lightcyan": "#E0FFFF", - "lightgoldenrodyellow": "#FAFAD2", - "lightgray": "#D3D3D3", - "lightgreen": "#90EE90", - "lightgrey": "#D3D3D3", - "lightpink": "#FFB6C1", - "lightsalmon": "#FFA07A", - "lightseagreen": "#20B2AA", - "lightskyblue": "#87CEFA", - "lightslategray": "#778899", - "lightslategrey": "#778899", - "lightsteelblue": "#B0C4DE", - "lightyellow": "#FFFFE0", - "lime": "#00FF00", - "limegreen": "#32CD32", - "linen": "#FAF0E6", - "magenta": "#FF00FF", - "maroon": "#800000", - "mediumaquamarine": "#66CDAA", - "mediumblue": "#0000CD", - "mediumorchid": "#BA55D3", - "mediumpurple": "#9370DB", - "mediumseagreen": "#3CB371", - "mediumslateblue": "#7B68EE", - "mediumspringgreen": "#00FA9A", - "mediumturquoise": "#48D1CC", - "mediumvioletred": "#C71585", - "midnightblue": "#191970", - "mintcream": "#F5FFFA", - "mistyrose": "#FFE4E1", - "moccasin": "#FFE4B5", - "navajowhite": "#FFDEAD", - "navy": "#000080", - "oldlace": "#FDF5E6", - "olive": "#808000", - "olivedrab": "#6B8E23", - "orange": "#FFA500", - "orangered": "#FF4500", - "orchid": "#DA70D6", - "palegoldenrod": "#EEE8AA", - "palegreen": "#98FB98", - "paleturquoise": "#AFEEEE", - "palevioletred": "#DB7093", - "papayawhip": "#FFEFD5", - "peachpuff": "#FFDAB9", - "peru": "#CD853F", - "pink": "#FFC0CB", - "plum": "#DDA0DD", - "powderblue": "#B0E0E6", - "purple": "#800080", - "rebeccapurple": "#663399", - "red": "#FF0000", - "rosybrown": "#BC8F8F", - "royalblue": "#4169E1", - "saddlebrown": "#8B4513", - "salmon": "#FA8072", - "sandybrown": "#F4A460", - "seagreen": "#2E8B57", - "seashell": "#FFF5EE", - "sienna": "#A0522D", - "silver": "#C0C0C0", - "skyblue": "#87CEEB", - "slateblue": "#6A5ACD", - "slategray": "#708090", - "slategrey": "#708090", - "snow": "#FFFAFA", - "springgreen": "#00FF7F", - "steelblue": "#4682B4", - "tan": "#D2B48C", - "teal": "#008080", - "thistle": "#D8BFD8", - "tomato": "#FF6347", - "turquoise": "#40E0D0", - "violet": "#EE82EE", - "wheat": "#F5DEB3", - "white": "#FFFFFF", - "whitesmoke": "#F5F5F5", - "yellow": "#FFFF00", - "yellowgreen": "#9ACD32", + "aliceblue": "F0F8FF", + "antiquewhite": "FAEBD7", + "aqua": "00FFFF", + "aquamarine": "7FFFD4", + "azure": "F0FFFF", + "beige": "F5F5DC", + "bisque": "FFE4C4", + "black": "000000", + "blanchedalmond": "FFEBCD", + "blue": "0000FF", + "blueviolet": "8A2BE2", + "brown": "A52A2A", + "burlywood": "DEB887", + "cadetblue": "5F9EA0", + "chartreuse": "7FFF00", + "chocolate": "D2691E", + "coral": "FF7F50", + "cornflowerblue": "6495ED", + "cornsilk": "FFF8DC", + "crimson": "DC143C", + "cyan": "00FFFF", + "darkblue": "00008B", + "darkcyan": "008B8B", + "darkgoldenrod": "B8860B", + "darkgray": "A9A9A9", + "darkgreen": "006400", + "darkgrey": "A9A9A9", + "darkkhaki": "BDB76B", + "darkmagenta": "8B008B", + "darkolivegreen": "556B2F", + "darkorange": "FF8C00", + "darkorchid": "9932CC", + "darkred": "8B0000", + "darksalmon": "E9967A", + "darkseagreen": "8FBC8F", + "darkslateblue": "483D8B", + "darkslategray": "2F4F4F", + "darkslategrey": "2F4F4F", + "darkturquoise": "00CED1", + "darkviolet": "9400D3", + "deeppink": "FF1493", + "deepskyblue": "00BFFF", + "dimgray": "696969", + "dimgrey": "696969", + "dodgerblue": "1E90FF", + "firebrick": "B22222", + "floralwhite": "FFFAF0", + "forestgreen": "228B22", + "fuchsia": "FF00FF", + "gainsboro": "DCDCDC", + "ghostwhite": "F8F8FF", + "gold": "FFD700", + "goldenrod": "DAA520", + "gray": "808080", + "green": "008000", + "greenyellow": "ADFF2F", + "grey": "808080", + "honeydew": "F0FFF0", + "hotpink": "FF69B4", + "indianred": "CD5C5C", + "indigo": "4B0082", + "ivory": "FFFFF0", + "khaki": "F0E68C", + "lavender": "E6E6FA", + "lavenderblush": "FFF0F5", + "lawngreen": "7CFC00", + "lemonchiffon": "FFFACD", + "lightblue": "ADD8E6", + "lightcoral": "F08080", + "lightcyan": "E0FFFF", + "lightgoldenrodyellow": "FAFAD2", + "lightgray": "D3D3D3", + "lightgreen": "90EE90", + "lightgrey": "D3D3D3", + "lightpink": "FFB6C1", + "lightsalmon": "FFA07A", + "lightseagreen": "20B2AA", + "lightskyblue": "87CEFA", + "lightslategray": "778899", + "lightslategrey": "778899", + "lightsteelblue": "B0C4DE", + "lightyellow": "FFFFE0", + "lime": "00FF00", + "limegreen": "32CD32", + "linen": "FAF0E6", + "magenta": "FF00FF", + "maroon": "800000", + "mediumaquamarine": "66CDAA", + "mediumblue": "0000CD", + "mediumorchid": "BA55D3", + "mediumpurple": "9370DB", + "mediumseagreen": "3CB371", + "mediumslateblue": "7B68EE", + "mediumspringgreen": "00FA9A", + "mediumturquoise": "48D1CC", + "mediumvioletred": "C71585", + "midnightblue": "191970", + "mintcream": "F5FFFA", + "mistyrose": "FFE4E1", + "moccasin": "FFE4B5", + "navajowhite": "FFDEAD", + "navy": "000080", + "oldlace": "FDF5E6", + "olive": "808000", + "olivedrab": "6B8E23", + "orange": "FFA500", + "orangered": "FF4500", + "orchid": "DA70D6", + "palegoldenrod": "EEE8AA", + "palegreen": "98FB98", + "paleturquoise": "AFEEEE", + "palevioletred": "DB7093", + "papayawhip": "FFEFD5", + "peachpuff": "FFDAB9", + "peru": "CD853F", + "pink": "FFC0CB", + "plum": "DDA0DD", + "powderblue": "B0E0E6", + "purple": "800080", + "rebeccapurple": "663399", + "red": "FF0000", + "rosybrown": "BC8F8F", + "royalblue": "4169E1", + "saddlebrown": "8B4513", + "salmon": "FA8072", + "sandybrown": "F4A460", + "seagreen": "2E8B57", + "seashell": "FFF5EE", + "sienna": "A0522D", + "silver": "C0C0C0", + "skyblue": "87CEEB", + "slateblue": "6A5ACD", + "slategray": "708090", + "slategrey": "708090", + "snow": "FFFAFA", + "springgreen": "00FF7F", + "steelblue": "4682B4", + "tan": "D2B48C", + "teal": "008080", + "thistle": "D8BFD8", + "tomato": "FF6347", + "turquoise": "40E0D0", + "violet": "EE82EE", + "wheat": "F5DEB3", + "white": "FFFFFF", + "whitesmoke": "F5F5F5", + "yellow": "FFFF00", + "yellowgreen": "9ACD32", } # Normalize name to "tab:" to avoid name collisions. diff --git a/core/changelog.py b/core/changelog.py index 3bf4ad865b..f687cfa57a 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -46,11 +46,12 @@ class Version: ACTION_REGEX = r"###\s*(.+?)\s*\n(.*?)(?=###\s*.+?|$)" DESCRIPTION_REGEX = r"^(.*?)(?=###\s*.+?|$)" - def __init__(self, bot, version: str, lines: str): + def __init__(self, bot, branch: str, version: str, lines: str): self.bot = bot self.version = version.lstrip("vV") self.lines = lines.strip() self.fields = {} + self.changelog_url = f"https://github.com/kyb3r/modmail/blob/{branch}/CHANGELOG.md" self.description = "" self.parse() @@ -79,7 +80,7 @@ def parse(self) -> None: @property def url(self) -> str: - return f"{Changelog.CHANGELOG_URL}#v{self.version[::2]}" + return f"{self.changelog_url}#v{self.version[::2]}" @property def embed(self) -> Embed: @@ -122,28 +123,19 @@ class Changelog: Class Attributes ---------------- - RAW_CHANGELOG_URL : str - The URL to Modmail changelog. - CHANGELOG_URL : str - The URL to Modmail changelog directly from in GitHub. VERSION_REGEX : re.Pattern The regex used to parse the versions. """ - - RAW_CHANGELOG_URL = ( - "https://raw.githubusercontent.com/kyb3r/modmail/master/CHANGELOG.md" - ) - CHANGELOG_URL = "https://github.com/kyb3r/modmail/blob/master/CHANGELOG.md" VERSION_REGEX = re.compile( r"#\s*([vV]\d+\.\d+(?:\.\d+)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)?|$)", flags=re.DOTALL, ) - def __init__(self, bot, text: str): + def __init__(self, bot, branch: str, text: str): self.bot = bot self.text = text logger.debug("Fetching changelog from GitHub.") - self.versions = [Version(bot, *m) for m in self.VERSION_REGEX.findall(text)] + self.versions = [Version(bot, branch, *m) for m in self.VERSION_REGEX.findall(text)] @property def latest_version(self) -> Version: @@ -169,7 +161,6 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": bot : Bot The Modmail bot. url : str, optional - Defaults to `RAW_CHANGELOG_URL`. The URL to the changelog. Returns @@ -177,6 +168,8 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": Changelog The newly created `Changelog` parsed from the `url`. """ - url = url or cls.RAW_CHANGELOG_URL - resp = await bot.session.get(url) - return cls(bot, await resp.text()) + branch = 'master' if not bot.version.is_prerelease else 'development' + url = url or f"https://raw.githubusercontent.com/kyb3r/modmail/{branch}/CHANGELOG.md" + + async with await bot.session.get(url) as resp: + return cls(bot, branch, await resp.text()) diff --git a/core/config.py b/core/config.py index ed9adead23..f20049e10a 100644 --- a/core/config.py +++ b/core/config.py @@ -100,6 +100,7 @@ class ConfigManager: "token": None, # Logging "log_level": "INFO", + "load_plugins": True, } colors = {"mod_color", "recipient_color", "main_color"} @@ -113,6 +114,7 @@ class ConfigManager: "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", + "load_plugins" } defaults = {**public_keys, **private_keys, **protected_keys} @@ -163,15 +165,83 @@ def populate_cache(self) -> dict: return self._cache - async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: - value_text = val - clean_value = val + async def update(self): + """Updates the config with data from the cache""" + await self.bot.api.update_config(self.filter_default(self._cache)) + + async def refresh(self) -> dict: + """Refreshes internal cache with data from database""" + for k, v in (await self.bot.api.get_config()).items(): + k = k.lower() + if k in self.all_keys: + self._cache[k] = v + if not self.ready_event.is_set(): + self.ready_event.set() + logger.info("Successfully fetched configurations from database.") + return self._cache + + async def wait_until_ready(self) -> None: + await self.ready_event.wait() + + def __setitem__(self, key: str, item: typing.Any) -> None: + key = key.lower() + logger.info("Setting %s.", key) + if key not in self.all_keys: + raise InvalidConfigError(f'Configuration "{key}" is invalid.') + self._cache[key] = item + + def __getitem__(self, key: str) -> typing.Any: + key = key.lower() + if key not in self.all_keys: + raise InvalidConfigError(f'Configuration "{key}" is invalid.') + if key not in self._cache: + self._cache[key] = deepcopy(self.defaults[key]) + return self._cache[key] + + def __delitem__(self, key: str) -> None: + return self.remove(key) + + def get(self, key: str, convert=True) -> typing.Any: + value = self.__getitem__(key) + + if not convert: + return value - # when setting a color if key in self.colors: try: - hex_ = str(val) + return int(value.lstrip("#"), base=16) + except ValueError: + logger.error("Invalid %s provided.", key) + value = int(self.remove(key).lstrip("#"), base=16) + + elif key in self.time_deltas: + if value is None: + return + try: + value = isodate.parse_duration(value) + except isodate.ISO8601Error: + logger.warning( + "The {account} age limit needs to be a " + 'ISO-8601 duration formatted duration, not "%s".', + value, + ) + value = self.remove(key) + + elif key in self.booleans: + try: + value = strtobool(value) + except ValueError: + value = self.remove(key) + + return value + + def set(self, key: str, item: typing.Any, convert=True) -> None: + if not convert: + return self.__setitem__(key, item) + if key in self.colors: + try: + hex_ = str(item) if hex_.startswith("#"): hex_ = hex_[1:] if len(hex_) == 3: @@ -182,11 +252,9 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: int(hex_, 16) except ValueError: raise InvalidConfigError("Invalid color name or hex.") - hex_ = "#" + hex_ - value_text = clean_value = hex_ except InvalidConfigError: - name = str(val).lower() + name = str(item).lower() name = re.sub(r"[\-+|. ]+", " ", name) hex_ = ALL_COLORS.get(name) if hex_ is None: @@ -194,17 +262,15 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: hex_ = ALL_COLORS.get(name) if hex_ is None: raise + return self.__setitem__(key, '#' + hex_) - clean_value = hex_ - value_text = f"{name} ({clean_value})" - - elif key in self.time_deltas: + if key in self.time_deltas: try: - isodate.parse_duration(val) + isodate.parse_duration(item) except isodate.ISO8601Error: try: converter = UserFriendlyTime() - time = await converter.convert(None, val) + time = self.bot.loop.run_until_complete(converter.convert(None, item)) if time.arg: raise ValueError except BadArgument as exc: @@ -214,73 +280,24 @@ async def clean_data(self, key: str, val: typing.Any) -> typing.Tuple[str, str]: "Unrecognized time, please use ISO-8601 duration format " 'string or a simpler "human readable" time.' ) - clean_value = isodate.duration_isoformat(time.dt - converter.now) - value_text = f"{val} ({clean_value})" + item = isodate.duration_isoformat(time.dt - converter.now) + return self.__setitem__(key, item) - elif key in self.booleans: + if key in self.booleans: try: - clean_value = value_text = strtobool(val) + return self.__setitem__(key, strtobool(item)) except ValueError: raise InvalidConfigError("Must be a yes/no value.") - return clean_value, value_text - - async def update(self): - """Updates the config with data from the cache""" - await self.bot.api.update_config(self.filter_default(self._cache)) - - async def refresh(self) -> dict: - """Refreshes internal cache with data from database""" - for k, v in (await self.bot.api.get_config()).items(): - k = k.lower() - if k in self.all_keys: - self._cache[k] = v - if not self.ready_event.is_set(): - self.ready_event.set() - logger.info("Successfully fetched configurations from database.") - return self._cache - - async def wait_until_ready(self) -> None: - await self.ready_event.wait() - - def __setitem__(self, key: str, item: typing.Any) -> None: - key = key.lower() - logger.info("Setting %s.", key) - if key not in self.all_keys: - raise InvalidConfigError(f'Configuration "{key}" is invalid.') - self._cache[key] = item - - def __getitem__(self, key: str) -> typing.Any: - key = key.lower() - if key not in self.all_keys: - raise InvalidConfigError(f'Configuration "{key}" is invalid.') - if key not in self._cache: - self._cache[key] = deepcopy(self.defaults[key]) - return self._cache[key] - - def get(self, key: str, default: typing.Any = Default) -> typing.Any: - key = key.lower() - if key not in self.all_keys: - raise InvalidConfigError(f'Configuration "{key}" is invalid.') - if key not in self._cache: - if default is Default: - self._cache[key] = deepcopy(self.defaults[key]) - if default is not Default and self._cache[key] == self.defaults[key]: - self._cache[key] = default - return self._cache[key] - - def set(self, key: str, item: typing.Any) -> None: - key = key.lower() - logger.info("Setting %s.", key) - if key not in self.all_keys: - raise InvalidConfigError(f'Configuration "{key}" is invalid.') - self._cache[key] = item + return self.__setitem__(key, item) def remove(self, key: str) -> typing.Any: key = key.lower() logger.info("Removing %s.", key) if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') + if key in self._cache: + del self._cache[key] self._cache[key] = deepcopy(self.defaults[key]) return self._cache[key] diff --git a/core/config_help.json b/core/config_help.json index 50d0a9fad9..3b5028ab28 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -467,5 +467,14 @@ "notes": [ "This configuration can only to be set through `.env` file or environment (config) variables." ] + }, + "load_plugins": { + "default": "Yes", + "description": "Whether plugins should be loaded into Modmail.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] } } diff --git a/core/thread.py b/core/thread.py index bdf0bc7aa2..9686e5ecb1 100644 --- a/core/thread.py +++ b/core/thread.py @@ -12,7 +12,7 @@ from discord.ext.commands import MissingRequiredArgument, CommandError from core.time import human_timedelta -from core.utils import is_image_url, days, match_user_id, truncate, ignore, strtobool +from core.utils import is_image_url, days, match_user_id, truncate, ignore logger = logging.getLogger("Modmail") @@ -161,12 +161,7 @@ async def send_genesis_message(): timestamp=channel.created_at, ) - try: - recipient_thread_close = strtobool( - self.bot.config["recipient_thread_close"] - ) - except ValueError: - recipient_thread_close = self.bot.config.remove("recipient_thread_close") + recipient_thread_close = self.bot.config.get("recipient_thread_close") if recipient_thread_close: footer = self.bot.config["thread_self_closable_creation_footer"] @@ -444,20 +439,7 @@ async def _fetch_timeout( :returns: None if no timeout is set. """ - timeout = self.bot.config["thread_auto_close"] - if timeout: - try: - timeout = isodate.parse_duration(timeout) - except isodate.ISO8601Error: - logger.warning( - "The auto_close_thread limit needs to be a " - "ISO-8601 duration formatted duration string " - 'greater than 0 days, not "%s".', - str(timeout), - ) - timeout = self.bot.config.remove("thread_auto_close") - await self.bot.config.update() - + timeout = self.bot.config.get("thread_auto_close") return timeout async def _restart_close_timer(self): @@ -477,16 +459,7 @@ async def _restart_close_timer(self): reset_time = datetime.utcnow() + timedelta(seconds=seconds) human_time = human_timedelta(dt=reset_time) - try: - thread_auto_close_silently = strtobool( - self.bot.config["thread_auto_close_silently"] - ) - except ValueError: - thread_auto_close_silently = self.bot.config.remove( - "thread_auto_close_silently" - ) - - if thread_auto_close_silently: + if self.bot.config.get('thread_auto_close_silently'): return await self.close( closer=self.bot.user, silent=True, after=int(seconds), auto_close=True ) diff --git a/plugins/registry.json b/plugins/registry.json index fc1dd11a2e..a7d699fa5a 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -187,5 +187,5 @@ "title": "Moderate your server", "icon_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png" - } + } } From 08ab537c13a61200c5751665d983833f51886538 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 20 Sep 2019 01:08:42 -0700 Subject: [PATCH 0402/1221] v3.3.0-dev1 --- CHANGELOG.md | 9 +- bot.py | 18 ++-- cogs/modmail.py | 53 +++++----- cogs/plugins.py | 105 +++++++++++-------- cogs/utility.py | 230 +++++++++++++++++++++--------------------- core/config.py | 7 +- core/config_help.json | 25 ++++- core/models.py | 5 +- core/thread.py | 20 ++-- core/utils.py | 1 + 10 files changed, 262 insertions(+), 211 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 738f7a3927..dde8659b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,13 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# [UNRELEASED] +# v3.3.0-dev1 + +### Added + +- Two new config vars: + - `ENABLE_PLUGINS` (yes/no default yes), when set to no, plugins will not be loaded into the bot. + - `ERROR_COLOR` (color format, defaults discord red), the color of error messages. ### Changed @@ -19,6 +25,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Logging / plugin-related messages changes. - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). - Help command is in alphabetical order grouped by permissions. +- Notes are no longer always blurple, its set to `MAIN_COLOR` now. ### Internal diff --git a/bot.py b/bot.py index 9108ddf583..f4ff2c1ec4 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev" +__version__ = "3.3.0-dev1" import asyncio import logging @@ -376,6 +376,10 @@ def recipient_color(self) -> int: def main_color(self) -> int: return self.config.get("main_color") + @property + def error_color(self) -> int: + return self.config.get("error_color") + def command_perm(self, command_name: str) -> PermissionLevel: level = self.config["override_command_level"].get(command_name) if level is not None: @@ -608,7 +612,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: title="Message not sent!", description=f"Your must wait for {delta} " f"before you can contact me.", - color=discord.Color.red(), + color=self.error_color, ) ) @@ -632,7 +636,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: title="Message not sent!", description=f"Your must wait for {delta} " f"before you can contact me.", - color=discord.Color.red(), + color=self.error_color, ) ) @@ -950,7 +954,7 @@ async def on_member_remove(self, member): if thread: embed = discord.Embed( description="The recipient has left the server.", - color=discord.Color.red(), + color=self.error_color, ) await thread.channel.send(embed=embed) @@ -1008,14 +1012,14 @@ async def on_command_error(self, context, exception): ) await context.trigger_typing() await context.send( - embed=discord.Embed(color=discord.Color.red(), description=msg) + embed=discord.Embed(color=self.error_color, description=msg) ) elif isinstance(exception, commands.BadArgument): await context.trigger_typing() await context.send( embed=discord.Embed( - color=discord.Color.red(), description=str(exception) + color=self.error_color, description=str(exception) ) ) elif isinstance(exception, commands.CommandNotFound): @@ -1028,7 +1032,7 @@ async def on_command_error(self, context, exception): if hasattr(check, "fail_msg"): await context.send( embed=discord.Embed( - color=discord.Color.red(), description=check.fail_msg + color=self.error_color, description=check.fail_msg ) ) if hasattr(check, "permission_level"): diff --git a/cogs/modmail.py b/cogs/modmail.py index f51c131b67..33d7f28457 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -52,7 +52,7 @@ async def setup(self, ctx): embed = discord.Embed( title="Error", description="Modmail functioning guild not found.", - color=discord.Color.red(), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -165,7 +165,7 @@ async def snippet(self, ctx, *, name: str.lower = None): if not self.bot.snippets: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="You dont have any snippets at the moment.", ) embed.set_footer( @@ -212,7 +212,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte if name in self.bot.snippets: embed = discord.Embed( title="Error", - color=discord.Color.red(), + color=self.bot.error_color, description=f"Snippet `{name}` already exists.", ) return await ctx.send(embed=embed) @@ -220,7 +220,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte if name in self.bot.aliases: embed = discord.Embed( title="Error", - color=discord.Color.red(), + color=self.bot.error_color, description=f"An alias with the same name already exists: `{name}`.", ) return await ctx.send(embed=embed) @@ -228,7 +228,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte if len(name) > 120: embed = discord.Embed( title="Error", - color=discord.Color.red(), + color=self.bot.error_color, description=f"Snippet names cannot be longer than 120 characters.", ) return await ctx.send(embed=embed) @@ -316,8 +316,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = except (discord.HTTPException, discord.InvalidArgument): pass - @staticmethod - async def send_scheduled_close_message(ctx, after, silent=False): + async def send_scheduled_close_message(self, ctx, after, silent=False): human_delta = human_timedelta(after.dt) silent = "*silently* " if silent else "" @@ -325,7 +324,7 @@ async def send_scheduled_close_message(ctx, after, silent=False): embed = discord.Embed( title="Scheduled close", description=f"This thread will close {silent}in {human_delta}.", - color=discord.Color.red(), + color=self.bot.error_color, ) if after.arg and not silent: @@ -375,12 +374,12 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): if thread.close_task is not None or thread.auto_close_task is not None: await thread.cancel_closure(all=True) embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Scheduled close has been cancelled.", ) else: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="This thread has not already been scheduled to close.", ) @@ -432,7 +431,7 @@ async def notify( if mention in mentions: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f"{mention} is already going to be mentioned.", ) else: @@ -471,7 +470,7 @@ async def unnotify( if mention not in mentions: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f"{mention} does not have a pending notification.", ) else: @@ -511,7 +510,7 @@ async def subscribe( if mention in mentions: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f"{mention} is already " "subscribed to this thread.", ) else: @@ -550,7 +549,7 @@ async def unsubscribe( if mention not in mentions: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f"{mention} is not already " "subscribed to this thread.", ) else: @@ -659,7 +658,7 @@ async def logs(self, ctx, *, user: User = None): if not any(not log["open"] for log in logs): embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="This user does not " "have any previous logs.", ) return await ctx.send(embed=embed) @@ -696,7 +695,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="No log entries have been found for that query", ) return await ctx.send(embed=embed) @@ -729,7 +728,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): if not embeds: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="No log entries have been found for that query.", ) return await ctx.send(embed=embed) @@ -788,11 +787,7 @@ async def find_linked_message(self, ctx, message_id): async for msg in ctx.channel.history(): if message_id is None and msg.embeds: embed = msg.embeds[0] - if isinstance(self.bot.mod_color, discord.Color): - mod_color = self.bot.mod_color.value - else: - mod_color = self.bot.mod_color - if embed.color.value != mod_color or not embed.author.url: + if embed.color.value != self.bot.mod_color or not embed.author.url: continue # TODO: use regex to find the linked message id linked_message_id = str(embed.author.url).split("/")[-1] @@ -823,7 +818,7 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): embed=discord.Embed( title="Failed", description="Cannot find a message to edit.", - color=discord.Color.red(), + color=self.bot.error_color, ) ) @@ -859,7 +854,7 @@ async def contact( if user.bot: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Cannot start a thread with a bot.", ) return await ctx.send(embed=embed) @@ -867,7 +862,7 @@ async def contact( exists = await self.bot.threads.find(recipient=user) if exists: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="A thread for this user already " f"exists in {exists.channel.mention}.", ) @@ -1031,7 +1026,7 @@ async def block( embed = discord.Embed( title="Error", description=f"Cannot block {mention}, user is whitelisted.", - color=discord.Color.red(), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -1079,7 +1074,7 @@ async def block( else: embed = discord.Embed( title="Error", - color=discord.Color.red(), + color=self.bot.error_color, description=f"{mention} is already blocked.", ) @@ -1136,7 +1131,7 @@ async def unblock(self, ctx, *, user: User = None): embed = discord.Embed( title="Error", description=f"{mention} is not blocked.", - color=discord.Color.red(), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -1168,7 +1163,7 @@ async def delete(self, ctx, message_id: Optional[int] = None): embed=discord.Embed( title="Failed", description="Cannot find a message to delete.", - color=discord.Color.red(), + color=self.bot.error_color, ) ) diff --git a/cogs/plugins.py b/cogs/plugins.py index e15b47e29b..556983aaf0 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -7,7 +7,6 @@ import sys import typing import zipfile - from importlib import invalidate_caches from difflib import get_close_matches from pathlib import Path, PurePath @@ -17,6 +16,7 @@ import discord from discord.ext import commands + from pkg_resources import parse_version from core import checks @@ -100,9 +100,13 @@ def __init__(self, bot): self.loaded_plugins = set() self._ready_event = asyncio.Event() - self.bot.loop.create_task(self.initial_load_plugins()) self.bot.loop.create_task(self.populate_registry()) + if self.bot.config.get('enable_plugins'): + self.bot.loop.create_task(self.initial_load_plugins()) + else: + logger.info('Plugins not loaded since ENABLE_PLUGINS=false.') + async def populate_registry(self): url = "https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json" async with self.bot.session.get(url) as resp: @@ -224,7 +228,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): embed = discord.Embed( description="Your bot's version is too low. " f"This plugin requires version `{required_version}`.", - color=discord.Color.red(), + color=self.bot.error_color, ) await ctx.send(embed=embed) return @@ -239,7 +243,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): description="Invalid plugin name, double check the plugin name " "or use one of the following formats: " "username/repo/plugin, username/repo/plugin@branch.", - color=discord.Color.red(), + color=self.bot.error_color, ) await ctx.send(embed=embed) return @@ -270,7 +274,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): if str(plugin) in self.bot.config["plugins"]: embed = discord.Embed( description="This plugin is already installed.", - color=discord.Color.red(), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -278,7 +282,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): # another class with the same name embed = discord.Embed( description="Cannot install this plugin (dupe cog name).", - color=discord.Color.red(), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -296,34 +300,45 @@ async def plugins_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description=f"Failed to download plugin, check logs for error.", - color=discord.Color.red() + color=self.bot.error_color ) return await msg.edit(embed=embed) - invalidate_caches() + self.bot.config["plugins"].append(str(plugin)) + await self.bot.config.update() - try: - await self.load_plugin(plugin) - except Exception: - logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) + if self.bot.config.get('enable_plugins'): - embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.", - color=discord.Color.red() - ) + invalidate_caches() - return await msg.edit(embed=embed) + try: + await self.load_plugin(plugin) + except Exception: + logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) - self.bot.config["plugins"].append(str(plugin)) - await self.bot.config.update() + embed = discord.Embed( + description=f"Failed to download plugin, check logs for error.", + color=self.bot.error_color + ) - embed = discord.Embed( - description="Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*", - color=self.bot.main_color, - ) + return await msg.edit(embed=embed) + + embed = discord.Embed( + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*", + color=self.bot.main_color, + ) + else: + embed = discord.Embed( + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*\n\n" + 'This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, ' + 'to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.', + color=self.bot.main_color, + ) return await msg.edit(embed=embed) @plugins.command(name="remove", aliases=["del", "delete"]) @@ -342,21 +357,22 @@ async def plugins_remove(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( description="Plugin is not installed.", - color=discord.Color.red(), + color=self.bot.error_color ) return await ctx.send(embed=embed) - try: - self.bot.unload_extension(plugin.ext_string) - self.loaded_plugins.remove(plugin) - except (commands.ExtensionNotLoaded, KeyError): - logger.warning("Plugin was never loaded.") + if self.bot.config.get('enable_plugins'): + try: + self.bot.unload_extension(plugin.ext_string) + self.loaded_plugins.remove(plugin) + except (commands.ExtensionNotLoaded, KeyError): + logger.warning("Plugin was never loaded.") self.bot.config["plugins"].remove(str(plugin)) await self.bot.config.update() embed = discord.Embed( - description="The plugin is uninstalled and all its data is erased.", + description="The plugin is successfully uninstalled.", color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -378,17 +394,18 @@ async def plugins_update(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( description="Plugin is not installed.", - color=discord.Color.red(), + color=self.bot.error_color ) return await ctx.send(embed=embed) async with ctx.typing(): await self.download_plugin(plugin, force=True) - try: - self.bot.unload_extension(plugin.ext_string) - except commands.ExtensionError: - logger.warning("Plugin unload fail.", exc_info=True) - await self.load_plugin(plugin) + if self.bot.config.get('enable_plugins'): + try: + self.bot.unload_extension(plugin.ext_string) + except commands.ExtensionError: + logger.warning("Plugin unload fail.", exc_info=True) + await self.load_plugin(plugin) embed = discord.Embed( description=f"Successfully updated {plugin.name}.", @@ -403,6 +420,14 @@ async def plugins_loaded(self, ctx): Show a list of currently loaded plugins. """ + if not self.bot.config.get('enable_plugins'): + embed = discord.Embed( + description="No plugins are loaded due to `ENABLE_PLUGINS=false`, " + 'to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.', + color=self.bot.error_color + ) + return await ctx.send(embed=embed) + if not self._ready_event.is_set(): embed = discord.Embed( description="Plugins are still loading, please try again later.", @@ -413,7 +438,7 @@ async def plugins_loaded(self, ctx): if not self.loaded_plugins: embed = discord.Embed( description="There are no plugins currently loaded.", - color=discord.Color.red() + color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -474,7 +499,7 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N if not index and plugin_name is not None: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f'Could not find a plugin with name "{plugin_name}" within the registry.', ) diff --git a/cogs/utility.py b/cogs/utility.py index d54ad362b9..b62cfb722e 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -14,7 +14,7 @@ from types import SimpleNamespace from typing import Union -from discord import Embed, Color, Activity, Role +import discord from discord.enums import ActivityType, Status from discord.ext import commands, tasks from discord.utils import escape_markdown, escape_mentions @@ -64,7 +64,7 @@ async def format_cog_help(self, cog, *, no_cog=False): if not no_cog else "Miscellaneous commands without a category." ) - embed = Embed(description=f"*{description}*", color=bot.main_color) + embed = discord.Embed(description=f"*{description}*", color=bot.main_color) embed.add_field(name="Commands", value=format_ or "No commands.") @@ -128,7 +128,7 @@ async def _get_help_embed(self, topic): else: perm_level = "NONE" - embed = Embed( + embed = discord.Embed( title=f"`{self.get_command_signature(topic)}`", color=self.context.bot.main_color, description=self.process_help_msg(topic.help), @@ -183,13 +183,13 @@ async def send_error_message(self, error): values = utils.parse_alias(val) if len(values) == 1: - embed = Embed( + embed = discord.Embed( title=f"{command} is an alias.", color=self.context.bot.main_color, description=f"`{command}` points to `{escape_markdown(values[0])}`.", ) else: - embed = Embed( + embed = discord.Embed( title=f"{command} is an alias.", color=self.context.bot.main_color, description=f"**`{command}` points to the following steps:**", @@ -204,7 +204,7 @@ async def send_error_message(self, error): logger.warning("CommandNotFound: %s", str(error)) - embed = Embed(color=Color.red()) + embed = discord.Embed(color=self.context.bot.error_color) embed.set_footer(text=f'Command/Category "{command}" not found.') choices = set() @@ -258,8 +258,8 @@ async def changelog(self, ctx, version: str.lower = ""): index = [v.version for v in changelog.versions].index(version) except ValueError: return await ctx.send( - embed=Embed( - color=Color.red(), + embed=discord.Embed( + color=self.bot.error_color, description=f"The specified version `{version}` could not be found.", ) ) @@ -285,7 +285,7 @@ async def changelog(self, ctx, version: str.lower = ""): @trigger_typing async def about(self, ctx): """Shows information about this bot.""" - embed = Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) + embed = discord.Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) embed.set_author( name="Modmail - About", icon_url=self.bot.user.avatar_url, @@ -301,27 +301,29 @@ async def about(self, ctx): embed.add_field(name="Uptime", value=self.bot.uptime) embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms") embed.add_field(name="Version", value=f"`{self.bot.version}`") - embed.add_field(name="Author", value="[`kyb3r`, `Taki`, `4jr`](https://github.com/kyb3r)") + embed.add_field(name="Authors", value="`kyb3r`, `Taki`, `fourjr`") changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version if self.bot.version < parse_version(latest.version): - footer = f"A newer version is available v{latest.version}" + footer = f"A newer version is available v{latest.version}." else: footer = "You are up to date with the latest version." embed.add_field( name="Want Modmail in Your Server?", - value="Installation guide on GitHub (https://github.com/kyb3r/modmail) " - "and join our discord server (https://discord.gg/F34cRU8)!", inline=False + value="Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " + "and join our [Discord server](https://discord.gg/F34cRU8/)!", + inline=False ) embed.add_field( name="Support the Developers", value="This bot is completely free for everyone. We rely on kind individuals " - "like you to support us on [`Patreon`](https://patreon.com/kyber) (includes perks!) " + "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " "to keep this bot free forever!", + inline=False ) embed.set_footer(text=footer) @@ -340,7 +342,7 @@ async def sponsors(self, ctx): embeds = [] for elem in data: - embed = Embed.from_dict(elem["embed"]) + embed = discord.Embed.from_dict(elem["embed"]) embeds.append(embed) random.shuffle(embeds) @@ -366,7 +368,7 @@ async def debug(self, ctx): logs = f.read().strip() if not logs: - embed = Embed( + embed = discord.Embed( color=self.bot.main_color, title="Debug Logs:", description="You don't have any logs at the moment.", @@ -396,7 +398,7 @@ async def debug(self, ctx): msg += "```" messages.append(msg) - embed = Embed(color=self.bot.main_color) + embed = discord.Embed(color=self.bot.main_color) embed.set_footer(text="Debug logs - Navigate using the reactions below.") session = MessagePaginatorSession(ctx, *messages, embed=embed) @@ -431,13 +433,13 @@ async def debug_hastebin(self, ctx): except KeyError: logger.error(data["message"]) raise - embed = Embed( + embed = discord.Embed( title="Debug Logs", color=self.bot.main_color, description=f"{haste_url}/" + key, ) except (JSONDecodeError, ClientResponseError, IndexError, KeyError): - embed = Embed( + embed = discord.Embed( title="Debug Logs", color=self.bot.main_color, description="Something's wrong. " @@ -463,7 +465,7 @@ async def debug_clear(self, ctx): ): pass await ctx.send( - embed=Embed( + embed=discord.Embed( color=self.bot.main_color, description="Cached logs are now cleared." ) ) @@ -495,7 +497,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): self.bot.config.remove("activity_message") await self.bot.config.update() await self.set_presence() - embed = Embed(title="Activity Removed", color=self.bot.main_color) + embed = discord.Embed(title="Activity Removed", color=self.bot.main_color) return await ctx.send(embed=embed) if not message: @@ -515,7 +517,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): self.bot.config["activity_message"] = message await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Activity Changed", description=msg, color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -540,7 +542,7 @@ async def status(self, ctx, *, status_type: str.lower): self.bot.config.remove("status") await self.bot.config.update() await self.set_presence() - embed = Embed(title="Status Removed", color=self.bot.main_color) + embed = discord.Embed(title="Status Removed", color=self.bot.main_color) return await ctx.send(embed=embed) status_type = status_type.replace(" ", "_") @@ -553,7 +555,7 @@ async def status(self, ctx, *, status_type: str.lower): self.bot.config["status"] = status.value await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Status Changed", description=msg, color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -615,7 +617,7 @@ async def set_presence( url = self.bot.config["twitch_url"] if activity_message: - activity = Activity(type=activity_type, name=activity_message, url=url) + activity = discord.Activity(type=activity_type, name=activity_message, url=url) else: msg = "You must supply an activity message to use custom activity." logger.debug(msg) @@ -657,7 +659,7 @@ async def before_loop_presence(self): @trigger_typing async def ping(self, ctx): """Pong! Returns your websocket latency.""" - embed = Embed( + embed = discord.Embed( title="Pong! Websocket Latency:", description=f"{self.bot.ws.latency * 1000:.4f} ms", color=self.bot.main_color, @@ -676,13 +678,13 @@ async def mention(self, ctx, *, mention: str = None): current = self.bot.config["mention"] if mention is None: - embed = Embed( + embed = discord.Embed( title="Current mention:", color=self.bot.main_color, description=str(current), ) else: - embed = Embed( + embed = discord.Embed( title="Changed mention!", description=f'On thread creation the bot now says "{mention}".', color=self.bot.main_color, @@ -702,7 +704,7 @@ async def prefix(self, ctx, *, prefix=None): """ current = self.bot.prefix - embed = Embed( + embed = discord.Embed( title="Current prefix", color=self.bot.main_color, description=f"{current}" ) @@ -744,7 +746,7 @@ async def config_options(self, ctx): description = "\n".join( f"`{name}`" for name in takewhile(lambda x: x is not None, names) ) - embed = Embed( + embed = discord.Embed( title="Available configuration keys:", color=self.bot.main_color, description=description, @@ -765,7 +767,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): try: self.bot.config.set(key, value) await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f"Set `{key}` to `{self.bot.config[key]}`.", @@ -773,12 +775,12 @@ async def config_set(self, ctx, key: str.lower, *, value: str): except InvalidConfigError as exc: embed = exc.embed else: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"{key} is an invalid key.", ) - valid_keys = [f"`{k}`" for k in keys] + valid_keys = [f"`{k}`" for k in sorted(keys)] embed.add_field(name="Valid keys", value=", ".join(valid_keys)) return await ctx.send(embed=embed) @@ -791,18 +793,18 @@ async def config_remove(self, ctx, *, key: str.lower): if key in keys: self.bot.config.remove(key) await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f"`{key}` had been reset to default.", ) else: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"{key} is an invalid key.", ) - valid_keys = [f"`{k}`" for k in keys] + valid_keys = [f"`{k}`" for k in sorted(keys)] embed.add_field(name="Valid keys", value=", ".join(valid_keys)) return await ctx.send(embed=embed) @@ -820,15 +822,15 @@ async def config_get(self, ctx, *, key: str.lower = None): if key: if key in keys: desc = f"`{key}` is set to `{self.bot.config[key]}`" - embed = Embed(color=self.bot.main_color, description=desc) + embed = discord.Embed(color=self.bot.main_color, description=desc) embed.set_author( name="Config variable", icon_url=self.bot.user.avatar_url ) else: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"`{key}` is an invalid key.", ) embed.set_footer( @@ -836,7 +838,7 @@ async def config_get(self, ctx, *, key: str.lower = None): ) else: - embed = Embed( + embed = discord.Embed( color=self.bot.main_color, description="Here is a list of currently " "set configuration variable(s).", @@ -861,9 +863,9 @@ async def config_help(self, ctx, key: str.lower = None): if key is not None and not ( key in self.bot.config.public_keys or key in self.bot.config.protected_keys ): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"`{key}` is an invalid key.", ) return await ctx.send(embed=embed) @@ -871,9 +873,9 @@ async def config_help(self, ctx, key: str.lower = None): config_help = self.bot.config.config_help if key is not None and key not in config_help: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"No help details found for `{key}`.", ) return await ctx.send(embed=embed) @@ -886,7 +888,7 @@ def fmt(val): for i, (current_key, info) in enumerate(config_help.items()): if current_key == key: index = i - embed = Embed( + embed = discord.Embed( title=f"Configuration description on {current_key}:", color=self.bot.main_color, ) @@ -950,9 +952,9 @@ async def alias(self, ctx, *, name: str.lower = None): values = utils.parse_alias(val) if not values: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"Alias `{name}` is invalid, it used to be `{escape_markdown(val)}`. " f"This alias will now be deleted.", ) @@ -961,12 +963,12 @@ async def alias(self, ctx, *, name: str.lower = None): return await ctx.send(embed=embed) if len(values) == 1: - embed = Embed( + embed = discord.Embed( color=self.bot.main_color, description=f"`{name}` points to `{escape_markdown(values[0])}`.", ) else: - embed = Embed( + embed = discord.Embed( color=self.bot.main_color, description=f"**`{name}` points to the following steps:**", ) @@ -976,8 +978,8 @@ async def alias(self, ctx, *, name: str.lower = None): return await ctx.send(embed=embed) if not self.bot.aliases: - embed = Embed( - color=Color.red(), + embed = discord.Embed( + color=self.bot.error_color, description="You dont have any aliases at the moment.", ) embed.set_footer(text=f"Do {self.bot.prefix}help alias for more commands.") @@ -988,7 +990,7 @@ async def alias(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = utils.format_description(i, names) - embed = Embed(color=self.bot.main_color, description=description) + embed =discord.Embed(color=self.bot.main_color, description=description) embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) embeds.append(embed) @@ -1025,30 +1027,30 @@ async def alias_add(self, ctx, name: str.lower, *, value): """ embed = None if self.bot.get_command(name): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"A command with the same name already exists: `{name}`.", ) elif name in self.bot.aliases: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"Another alias with the same name already exists: `{name}`.", ) elif name in self.bot.snippets: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"A snippet with the same name already exists: `{name}`.", ) elif len(name) > 120: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"Alias names cannot be longer than 120 characters.", ) @@ -1058,9 +1060,9 @@ async def alias_add(self, ctx, name: str.lower, *, value): values = utils.parse_alias(value) if not values: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="Invalid multi-step alias, try wrapping each steps in quotes.", ) embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.') @@ -1069,22 +1071,22 @@ async def alias_add(self, ctx, name: str.lower, *, value): if len(values) == 1: linked_command = values[0].split()[0].lower() if not self.bot.get_command(linked_command): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="The command you are attempting to point " f"to does not exist: `{linked_command}`.", ) return await ctx.send(embed=embed) - embed = Embed( + embed = discord.Embed( title="Added alias", color=self.bot.main_color, description=f'`{name}` points to: "{values[0]}".', ) else: - embed = Embed( + embed = discord.Embed( title="Added alias", color=self.bot.main_color, description=f"`{name}` now points to the following steps:", @@ -1093,9 +1095,9 @@ async def alias_add(self, ctx, name: str.lower, *, value): for i, val in enumerate(values, start=1): linked_command = val.split()[0] if not self.bot.get_command(linked_command): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="The command you are attempting to point " f"to on step {i} does not exist: `{linked_command}`.", ) @@ -1116,7 +1118,7 @@ async def alias_remove(self, ctx, *, name: str.lower): self.bot.aliases.pop(name) await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Removed alias", color=self.bot.main_color, description=f"Successfully deleted `{name}`.", @@ -1139,9 +1141,9 @@ async def alias_edit(self, ctx, name: str.lower, *, value): values = utils.parse_alias(value) if not values: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="Invalid multi-step alias, try wrapping each steps in quotes.", ) embed.set_footer(text=f'See "{self.bot.prefix}alias add" for more details.') @@ -1150,21 +1152,21 @@ async def alias_edit(self, ctx, name: str.lower, *, value): if len(values) == 1: linked_command = values[0].split()[0].lower() if not self.bot.get_command(linked_command): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="The command you are attempting to point " f"to does not exist: `{linked_command}`.", ) return await ctx.send(embed=embed) - embed = Embed( + embed = discord.Embed( title="Edited alias", color=self.bot.main_color, description=f'`{name}` now points to: "{values[0]}".', ) else: - embed = Embed( + embed = discord.Embed( title="Edited alias", color=self.bot.main_color, description=f"`{name}` now points to the following steps:", @@ -1173,9 +1175,9 @@ async def alias_edit(self, ctx, name: str.lower, *, value): for i, val in enumerate(values, start=1): linked_command = val.split()[0] if not self.bot.get_command(linked_command): - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description="The command you are attempting to point " f"to on step {i} does not exist: `{linked_command}`.", ) @@ -1214,7 +1216,7 @@ async def permissions(self, ctx): @staticmethod def _verify_user_or_role(user_or_role): - if isinstance(user_or_role, Role): + if isinstance(user_or_role, discord.Role): if user_or_role.is_default(): return -1 elif user_or_role in {"everyone", "all"}: @@ -1262,18 +1264,18 @@ async def permissions_override( command = self.bot.get_command(command_name) if command is None: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The referenced command does not exist: `{command_name}`.", ) return await ctx.send(embed=embed) level = self._parse_level(level_name) if level is PermissionLevel.INVALID: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The referenced level does not exist: `{level_name}`.", ) else: @@ -1287,7 +1289,7 @@ async def permissions_override( ] = level.name await self.bot.config.update() - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description="Successfully set command permission level for " @@ -1303,7 +1305,7 @@ async def permissions_add( type_: str.lower, name: str, *, - user_or_role: Union[Role, utils.User, str], + user_or_role: Union[discord.Role, utils.User, str], ): """ Add a permission to a command or a permission level. @@ -1333,9 +1335,9 @@ async def permissions_add( check = level is not PermissionLevel.INVALID if not check: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The referenced {type_} does not exist: `{name}`.", ) return await ctx.send(embed=embed) @@ -1350,7 +1352,7 @@ async def permissions_add( if level > PermissionLevel.REGULAR: if value == -1: key = self.bot.modmail_guild.default_role - elif isinstance(user_or_role, Role): + elif isinstance(user_or_role, discord.Role): key = user_or_role else: key = self.bot.modmail_guild.get_member(value) @@ -1360,7 +1362,7 @@ async def permissions_add( key, read_messages=True ) - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f"Permission for `{name}` was successfully updated.", @@ -1379,7 +1381,7 @@ async def permissions_remove( type_: str.lower, name: str, *, - user_or_role: Union[Role, utils.User, str] = None, + user_or_role: Union[discord.Role, utils.User, str] = None, ): """ Remove permission to use a command, permission level, or command level override. @@ -1411,9 +1413,9 @@ async def permissions_remove( level = self.bot.config["override_command_level"].get(name) if level is None: perm = self.bot.command_perm(name) - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The command permission level was never overridden: `{name}`, " f"current permission level is {perm.name}.", ) @@ -1422,7 +1424,7 @@ async def permissions_remove( self.bot.config["override_command_level"].pop(name) await self.bot.config.update() perm = self.bot.command_perm(name) - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f"Command permission level for `{name}` was successfully restored to {perm.name}.", @@ -1436,9 +1438,9 @@ async def permissions_remove( else: level = self._parse_level(name) if level is PermissionLevel.INVALID: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The referenced level does not exist: `{name}`.", ) return await ctx.send(embed=embed) @@ -1454,7 +1456,7 @@ async def permissions_remove( await self.bot.main_category.set_permissions( self.bot.modmail_guild.default_role, read_messages=False ) - elif isinstance(user_or_role, Role): + elif isinstance(user_or_role, discord.Role): logger.info( "Denying %s access to Modmail category.", user_or_role.name ) @@ -1471,7 +1473,7 @@ async def permissions_remove( member, overwrite=None ) - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f"Permission for `{name}` was successfully updated.", @@ -1484,7 +1486,7 @@ def _get_perm(self, ctx, name, type_): else: permissions = self.bot.config["level_permissions"].get(name, []) if not permissions: - embed = Embed( + embed = discord.Embed( title=f"Permission entries for {type_} `{name}`:", description="No permission entries found.", color=self.bot.main_color, @@ -1509,7 +1511,7 @@ def _get_perm(self, ctx, name, type_): else: values.append(str(perm)) - embed = Embed( + embed = discord.Embed( title=f"Permission entries for {type_} `{name}`:", description=", ".join(values), color=self.bot.main_color, @@ -1519,7 +1521,7 @@ def _get_perm(self, ctx, name, type_): @permissions.command(name="get", usage="[@user] or [command/level/override] [name]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get( - self, ctx, user_or_role: Union[Role, utils.User, str], *, name: str = None + self, ctx, user_or_role: Union[discord.Role, utils.User, str], *, name: str = None ): """ View the currently-set permissions. @@ -1584,12 +1586,12 @@ async def permissions_get( ) embeds = [ - Embed( + discord.Embed( title=f"{mention} has permission with the following commands:", description=desc_cmd, color=self.bot.main_color, ), - Embed( + discord.Embed( title=f"{mention} has permission with the following permission levels:", description=desc_level, color=self.bot.main_color, @@ -1614,10 +1616,10 @@ async def permissions_get( embeds = [] if not overrides: embeds.append( - Embed( + discord.Embed( title="Permission Overrides", description="You don't have any command level overrides at the moment.", - color=Color.red(), + color=self.bot.error_color, ) ) else: @@ -1630,7 +1632,7 @@ async def permissions_get( lambda x: x is not None, items ) ) - embed = Embed( + embed = discord.Embed( color=self.bot.main_color, description=description ) embed.set_author( @@ -1646,14 +1648,14 @@ async def permissions_get( level = self.bot.config["override_command_level"].get(name) perm = self.bot.command_perm(name) if level is None: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The command permission level was never overridden: `{name}`, " f"current permission level is {perm.name}.", ) else: - embed = Embed( + embed = discord.Embed( title="Success", color=self.bot.main_color, description=f'Permission override for command "{name}" is "{perm.name}".', @@ -1676,9 +1678,9 @@ async def permissions_get( check = level is not PermissionLevel.INVALID if not check: - embed = Embed( + embed = discord.Embed( title="Error", - color=Color.red(), + color=self.bot.error_color, description=f"The referenced {user_or_role} does not exist: `{name}`.", ) return await ctx.send(embed=embed) @@ -1717,7 +1719,7 @@ async def oauth(self, ctx): @oauth.command(name="whitelist") @checks.has_permissions(PermissionLevel.OWNER) - async def oauth_whitelist(self, ctx, target: Union[Role, utils.User]): + async def oauth_whitelist(self, ctx, target: Union[discord.Role, utils.User]): """ Whitelist or un-whitelist a user or role to have access to logs. @@ -1735,7 +1737,7 @@ async def oauth_whitelist(self, ctx, target: Union[Role, utils.User]): await self.bot.config.update() - embed = Embed(color=self.bot.main_color) + embed = discord.Embed(color=self.bot.main_color) embed.title = "Success" if not hasattr(target, "mention"): @@ -1766,7 +1768,7 @@ async def oauth_show(self, ctx): if role: roles.append(role) - embed = Embed(color=self.bot.main_color) + embed = discord.Embed(color=self.bot.main_color) embed.title = "Oauth Whitelist" embed.add_field( diff --git a/core/config.py b/core/config.py index f20049e10a..b67aa23329 100644 --- a/core/config.py +++ b/core/config.py @@ -31,6 +31,7 @@ class ConfigManager: "prefix": "?", "mention": "@here", "main_color": str(discord.Color.blurple()), + "error_color": str(discord.Color.red()), "user_typing": False, "mod_typing": False, "account_age": None, @@ -100,10 +101,10 @@ class ConfigManager: "token": None, # Logging "log_level": "INFO", - "load_plugins": True, + "enable_plugins": True, } - colors = {"mod_color", "recipient_color", "main_color"} + colors = {"mod_color", "recipient_color", "main_color", "error_color"} time_deltas = {"account_age", "guild_age", "thread_auto_close"} @@ -114,7 +115,7 @@ class ConfigManager: "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", - "load_plugins" + "enable_plugins" } defaults = {**public_keys, **private_keys, **protected_keys} diff --git a/core/config_help.json b/core/config_help.json index 3b5028ab28..4afcf63405 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -53,10 +53,25 @@ ], "notes": [ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", - "See also: `mod_color`, `recipient_color`." + "See also: `error_color`, `mod_color`, `recipient_color`." ], "thumbnail": "https://placehold.it/100/7289da?text=+" }, + "error_color": { + "default": "Discord Red [#E74C3C](https://placehold.it/100/e74c3c?text=+)", + "description": "This is the color for Modmail when anything goes wrong, unsuccessful commands, or a stern warning.", + "examples": [ + "`{prefix}config set error_color ocean blue`", + "`{prefix}config set error_color ff1242`", + "`{prefix}config set error_color #ff1242`", + "`{prefix}config set error_color fa1`" + ], + "notes": [ + "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", + "See also: `main_color`, `mod_color`, `recipient_color`." + ], + "thumbnail": "https://placehold.it/100/e74c3c?text=+" + }, "user_typing": { "default": "Disabled", "description": "When this is set to `yes`, whenever the recipient user starts to type in their DM channel, the moderator will see “{bot.user.display_name} is typing…” in the thread channel.", @@ -333,7 +348,7 @@ ], "notes": [ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", - "See also: `main_color`, `mod_color`." + "See also: `mod_color`, `main_color`, `error_color`." ], "thumbnail": "https://placehold.it/100/f1c40f?text=+" }, @@ -348,7 +363,7 @@ ], "notes": [ "Available color names can be found on [Taki's Blog](https://taaku18.github.io/modmail/colors/).", - "See also: `main_color`, `recipient_color`." + "See also: `recipient_color`, `main_color`, `error_color`." ], "thumbnail": "https://placehold.it/100/2ecc71?text=+" }, @@ -468,9 +483,9 @@ "This configuration can only to be set through `.env` file or environment (config) variables." ] }, - "load_plugins": { + "enable_plugins": { "default": "Yes", - "description": "Whether plugins should be loaded into Modmail.", + "description": "Whether plugins should be enabled and loaded into Modmail.", "examples": [ ], "notes": [ diff --git a/core/models.py b/core/models.py index 8b21c10eb0..4d240e70c0 100644 --- a/core/models.py +++ b/core/models.py @@ -3,7 +3,7 @@ from enum import IntEnum from string import Formatter -from discord import Color, Embed +import discord from discord.ext import commands try: @@ -31,7 +31,8 @@ def __init__(self, msg, *args): @property def embed(self): - return Embed(title="Error", description=self.msg, color=Color.red()) + # Single reference of Color.red() + return discord.Embed(title="Error", description=self.msg, color=discord.Color.red()) class ModmailLogger(logging.Logger): diff --git a/core/thread.py b/core/thread.py index 9686e5ecb1..915fc30013 100644 --- a/core/thread.py +++ b/core/thread.py @@ -106,7 +106,7 @@ async def setup(self, *, creator=None, category=None): logger.critical("An error occurred while creating a thread.", exc_info=True) self.manager.cache.pop(self.id) - embed = discord.Embed(color=discord.Color.red()) + embed = discord.Embed(color=self.bot.error_color) embed.title = "Error while trying to create a thread." embed.description = str(e) embed.add_field(name="Recipient", value=recipient.mention) @@ -138,7 +138,7 @@ async def setup(self, *, creator=None, category=None): async def send_genesis_message(): info_embed = self._format_info_embed( - recipient, log_url, log_count, discord.Color.green() + recipient, log_url, log_count, self.bot.main_color ) try: msg = await channel.send(mention, embed=info_embed) @@ -350,7 +350,7 @@ async def _close( desc = "Could not resolve log url." log_url = None - embed = discord.Embed(description=desc, color=discord.Color.red()) + embed = discord.Embed(description=desc, color=self.bot.error_color) if self.recipient is not None: user = f"{self.recipient} (`{self.id}`)" @@ -378,7 +378,7 @@ async def _close( embed = discord.Embed( title=self.bot.config["thread_close_title"], - color=discord.Color.red(), + color=self.bot.error_color, timestamp=datetime.utcnow(), ) @@ -528,7 +528,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None if not any(g.get_member(self.id) for g in self.bot.guilds): return await message.channel.send( embed=discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Your message could not be delivered since " "the recipient shares no servers with the bot.", ) @@ -545,7 +545,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None tasks.append( message.channel.send( embed=discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Your message could not be delivered as " "the recipient is only accepting direct " "messages from friends, or the bot was " @@ -578,7 +578,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None tasks.append( self.channel.send( embed=discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Scheduled close has been cancelled.", ) ) @@ -607,7 +607,7 @@ async def send( self.bot.loop.create_task( self.channel.send( embed=discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description="Scheduled close has been cancelled.", ) ) @@ -696,7 +696,7 @@ async def send( embedded_image = True elif filename is not None: if note: - color = discord.Color.blurple() + color = self.bot.main_color elif from_mod: color = self.bot.mod_color else: @@ -735,7 +735,7 @@ async def send( else: embed.set_footer(text=self.bot.config["anon_tag"]) elif note: - embed.colour = discord.Color.blurple() + embed.colour = self.bot.main_color else: embed.set_footer(text=f"Message ID: {message.id}") embed.colour = self.bot.recipient_color diff --git a/core/utils.py b/core/utils.py index de17e18419..146f6224af 100644 --- a/core/utils.py +++ b/core/utils.py @@ -202,6 +202,7 @@ async def ignore(coro): def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discord.Embed: + # Single reference of Color.red() embed = discord.Embed( color=discord.Color.red(), description=f"**{name.capitalize()} `{word}` cannot be found.**", From 53f930deea0313b91d8e1a9e8845bf7aebe22e27 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 20 Sep 2019 01:11:20 -0700 Subject: [PATCH 0403/1221] Black formatting --- bot.py | 15 ++-- cogs/modmail.py | 10 ++- cogs/plugins.py | 176 ++++++++++++++++++++++++-------------------- cogs/utility.py | 24 +++--- core/_color_data.py | 6 +- core/changelog.py | 16 +++- core/config.py | 8 +- core/models.py | 7 +- core/thread.py | 10 ++- 9 files changed, 156 insertions(+), 116 deletions(-) diff --git a/bot.py b/bot.py index f4ff2c1ec4..6b6d2b986c 100644 --- a/bot.py +++ b/bot.py @@ -71,7 +71,7 @@ def __init__(self): self._api = None self.metadata_loop = None self.formatter = SafeFormatter() - self.loaded_cogs = ['cogs.modmail', 'cogs.plugins', 'cogs.utility'] + self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] self._connected = asyncio.Event() self.start_time = datetime.utcnow() @@ -829,7 +829,7 @@ async def process_commands(self, message): thread = await self.threads.find(channel=ctx.channel) if thread is not None: - if self.config.get('reply_without_command'): + if self.config.get("reply_without_command"): await thread.reply(message) else: await self.api.append_log(message, type_="internal") @@ -857,7 +857,7 @@ async def _void(*_args, **_kwargs): if thread: await thread.channel.trigger_typing() else: - if not self.config.get('mod_typing'): + if not self.config.get("mod_typing"): return thread = await self.threads.find(channel=channel) @@ -895,7 +895,7 @@ async def on_raw_reaction_add(self, payload): if isinstance(channel, discord.DMChannel): if str(reaction) == str(close_emoji): # closing thread - if not self.config.get('recipient_thread_close'): + if not self.config.get("recipient_thread_close"): return thread = await self.threads.find(recipient=user) ts = message.embeds[0].timestamp if message.embeds else None @@ -953,8 +953,7 @@ async def on_member_remove(self, member): thread = await self.threads.find(recipient=member) if thread: embed = discord.Embed( - description="The recipient has left the server.", - color=self.error_color, + description="The recipient has left the server.", color=self.error_color ) await thread.channel.send(embed=embed) @@ -1018,9 +1017,7 @@ async def on_command_error(self, context, exception): elif isinstance(exception, commands.BadArgument): await context.trigger_typing() await context.send( - embed=discord.Embed( - color=self.error_color, description=str(exception) - ) + embed=discord.Embed(color=self.error_color, description=str(exception)) ) elif isinstance(exception, commands.CommandNotFound): logger.warning("CommandNotFound: %s", exception) diff --git a/cogs/modmail.py b/cogs/modmail.py index 33d7f28457..1450a405cf 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -286,7 +286,9 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): @commands.command() @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() - async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None): + async def move( + self, ctx, category: discord.CategoryChannel, *, specifics: str = None + ): """ Move a thread to another category. @@ -297,16 +299,16 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = silent = False if specifics: - silent_words = ['silent', 'silently'] + silent_words = ["silent", "silently"] silent = any(word in silent_words for word in specifics.split()) await thread.channel.edit(category=category, sync_permissions=True) - if self.bot.config('thread_move_notify') and not silent: + if self.bot.config("thread_move_notify") and not silent: embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], - color=self.bot.main_color + color=self.bot.main_color, ) await thread.recipient.send(embed=embed) diff --git a/cogs/plugins.py b/cogs/plugins.py index 556983aaf0..1f0ccf3a03 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -36,13 +36,15 @@ def __init__(self, user, repo, name, branch=None): self.user = user self.repo = repo self.name = name - self.branch = branch if branch is not None else 'master' + self.branch = branch if branch is not None else "master" self.url = f"https://github.com/{user}/{repo}/archive/{self.branch}.zip" self.link = f"https://github.com/{user}/{repo}/tree/{self.branch}/{name}" @property def path(self): - return PurePath('plugins') / self.user / self.repo / f'{self.name}-{self.branch}' + return ( + PurePath("plugins") / self.user / self.repo / f"{self.name}-{self.branch}" + ) @property def abs_path(self): @@ -50,15 +52,19 @@ def abs_path(self): @property def cache_path(self): - return Path(__file__).absolute().parent.parent / 'temp' / \ - 'plugins-cache' / f'{self.user}-{self.repo}-{self.branch}.zip' + return ( + Path(__file__).absolute().parent.parent + / "temp" + / "plugins-cache" + / f"{self.user}-{self.repo}-{self.branch}.zip" + ) @property def ext_string(self): - return f'plugins.{self.user}.{self.repo}.{self.name}-{self.branch}.{self.name}' + return f"plugins.{self.user}.{self.repo}.{self.name}-{self.branch}.{self.name}" def __str__(self): - return f'{self.user}/{self.repo}/{self.name}@{self.branch}' + return f"{self.user}/{self.repo}/{self.name}@{self.branch}" def __lt__(self, other): return self.name.lower() < other.name.lower() @@ -66,19 +72,19 @@ def __lt__(self, other): @classmethod def from_string(cls, s, strict=False): if not strict: - m = match(r'^(.+?)/(.+?)/(.+?)(?:@(.+?))?$', s) + m = match(r"^(.+?)/(.+?)/(.+?)(?:@(.+?))?$", s) else: - m = match(r'^(.+?)/(.+?)/(.+?)@(.+?)$', s) + m = match(r"^(.+?)/(.+?)/(.+?)@(.+?)$", s) if m is not None: return Plugin(*m.groups()) else: - raise InvalidPluginError('Cannot decipher %s.', s) + raise InvalidPluginError("Cannot decipher %s.", s) def __hash__(self): return hash((self.user, self.repo, self.name, self.branch)) def __repr__(self): - return f'' + return f"" def __eq__(self, other): return isinstance(other, Plugin) and self.__str__() == other.__str__() @@ -102,10 +108,10 @@ def __init__(self, bot): self.bot.loop.create_task(self.populate_registry()) - if self.bot.config.get('enable_plugins'): + if self.bot.config.get("enable_plugins"): self.bot.loop.create_task(self.initial_load_plugins()) else: - logger.info('Plugins not loaded since ENABLE_PLUGINS=false.') + logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") async def populate_registry(self): url = "https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json" @@ -124,10 +130,14 @@ async def initial_load_plugins(self): # For backwards compat plugin = Plugin.from_string(plugin_name) except InvalidPluginError: - logger.error("Failed to parse plugin name: %s.", plugin_name, exc_info=True) + logger.error( + "Failed to parse plugin name: %s.", plugin_name, exc_info=True + ) continue - logger.info("Migrated legacy plugin name: %s, now %s.", plugin_name, str(plugin)) + logger.info( + "Migrated legacy plugin name: %s, now %s.", plugin_name, str(plugin) + ) self.bot.config["plugins"].append(str(plugin)) try: @@ -148,56 +158,62 @@ async def download_plugin(self, plugin, force=False): plugin.abs_path.mkdir(parents=True, exist_ok=True) if plugin.cache_path.exists() and not force: - plugin_io = plugin.cache_path.open('rb') - logger.debug('Loading cached %s.', plugin.cache_path) + plugin_io = plugin.cache_path.open("rb") + logger.debug("Loading cached %s.", plugin.cache_path) else: async with self.bot.session.get(plugin.url) as resp: - logger.debug('Downloading %s.', plugin.url) + logger.debug("Downloading %s.", plugin.url) raw = await resp.read() plugin_io = io.BytesIO(raw) if not plugin.cache_path.parent.exists(): plugin.cache_path.parent.mkdir(parents=True) - with plugin.cache_path.open('wb') as f: + with plugin.cache_path.open("wb") as f: f.write(raw) with zipfile.ZipFile(plugin_io) as zipf: for member in zipf.namelist(): path = PurePath(member) if len(path.parts) >= 3 and path.parts[1] == plugin.name: - with zipf.open(member) as src, (plugin.abs_path / Path(*path.parts[2:])).open("wb") as dst: + with zipf.open(member) as src, ( + plugin.abs_path / Path(*path.parts[2:]) + ).open("wb") as dst: shutil.copyfileobj(src, dst) plugin_io.close() async def load_plugin(self, plugin): - if not (plugin.abs_path / f'{plugin.name}.py').exists(): - raise InvalidPluginError(f'{plugin.name}.py not found.') + if not (plugin.abs_path / f"{plugin.name}.py").exists(): + raise InvalidPluginError(f"{plugin.name}.py not found.") - req_txt = plugin.abs_path / 'requirements.txt' + req_txt = plugin.abs_path / "requirements.txt" if req_txt.exists(): # Install PIP requirements venv = hasattr(sys, "real_prefix") # in a virtual env - user_install = ' --user' if not venv else '' + user_install = " --user" if not venv else "" proc = await asyncio.create_subprocess_shell( f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", stderr=PIPE, - stdout=PIPE + stdout=PIPE, ) - logger.debug('Downloading requirements for %s.', plugin.ext_string) + logger.debug("Downloading requirements for %s.", plugin.ext_string) stdout, stderr = await proc.communicate() if stdout: - logger.debug('[stdout]\n%s.', stdout.decode()) + logger.debug("[stdout]\n%s.", stdout.decode()) if stderr: - logger.debug('[stderr]\n%s.', stderr.decode()) - logger.error("Failed to download requirements for %s.", plugin.ext_string, exc_info=True) + logger.debug("[stderr]\n%s.", stderr.decode()) + logger.error( + "Failed to download requirements for %s.", + plugin.ext_string, + exc_info=True, + ) raise InvalidPluginError( f"Unable to download requirements: ```\n{stderr.decode()}\n```" ) @@ -207,27 +223,29 @@ async def load_plugin(self, plugin): try: self.bot.load_extension(plugin.ext_string) - logger.info('Loaded plugin: %s', plugin.ext_string) + logger.info("Loaded plugin: %s", plugin.ext_string) self.loaded_plugins.add(plugin) except commands.ExtensionError as exc: - logger.error('Plugin load failure: %s', plugin.ext_string, exc_info=True) + logger.error("Plugin load failure: %s", plugin.ext_string, exc_info=True) raise InvalidPluginError("Cannot load extension, plugin invalid.") from exc async def parse_user_input(self, ctx, plugin_name, check_version=False): if plugin_name in self.registry: details = self.registry[plugin_name] - user, repo = details["repository"].split('/', maxsplit=1) + user, repo = details["repository"].split("/", maxsplit=1) branch = details.get("branch") if check_version: required_version = details.get("bot_version", False) - if required_version and self.bot.version < parse_version(required_version): + if required_version and self.bot.version < parse_version( + required_version + ): embed = discord.Embed( description="Your bot's version is too low. " - f"This plugin requires version `{required_version}`.", + f"This plugin requires version `{required_version}`.", color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -241,8 +259,8 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): except InvalidPluginError: embed = discord.Embed( description="Invalid plugin name, double check the plugin name " - "or use one of the following formats: " - "username/repo/plugin, username/repo/plugin@branch.", + "or use one of the following formats: " + "username/repo/plugin, username/repo/plugin@branch.", color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -288,7 +306,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description=f"Starting to download plugin from {plugin.link}...", - color=self.bot.main_color + color=self.bot.main_color, ) msg = await ctx.send(embed=embed) @@ -300,7 +318,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description=f"Failed to download plugin, check logs for error.", - color=self.bot.error_color + color=self.bot.error_color, ) return await msg.edit(embed=embed) @@ -308,7 +326,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): self.bot.config["plugins"].append(str(plugin)) await self.bot.config.update() - if self.bot.config.get('enable_plugins'): + if self.bot.config.get("enable_plugins"): invalidate_caches() @@ -319,24 +337,24 @@ async def plugins_add(self, ctx, *, plugin_name: str): embed = discord.Embed( description=f"Failed to download plugin, check logs for error.", - color=self.bot.error_color + color=self.bot.error_color, ) return await msg.edit(embed=embed) embed = discord.Embed( description="Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*", + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*", color=self.bot.main_color, ) else: embed = discord.Embed( description="Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*\n\n" - 'This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, ' - 'to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.', + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*\n\n" + "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.", color=self.bot.main_color, ) return await msg.edit(embed=embed) @@ -356,12 +374,11 @@ async def plugins_remove(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description="Plugin is not installed.", - color=self.bot.error_color + description="Plugin is not installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) - if self.bot.config.get('enable_plugins'): + if self.bot.config.get("enable_plugins"): try: self.bot.unload_extension(plugin.ext_string) self.loaded_plugins.remove(plugin) @@ -393,14 +410,13 @@ async def plugins_update(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description="Plugin is not installed.", - color=self.bot.error_color + description="Plugin is not installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) async with ctx.typing(): await self.download_plugin(plugin, force=True) - if self.bot.config.get('enable_plugins'): + if self.bot.config.get("enable_plugins"): try: self.bot.unload_extension(plugin.ext_string) except commands.ExtensionError: @@ -420,47 +436,45 @@ async def plugins_loaded(self, ctx): Show a list of currently loaded plugins. """ - if not self.bot.config.get('enable_plugins'): + if not self.bot.config.get("enable_plugins"): embed = discord.Embed( description="No plugins are loaded due to `ENABLE_PLUGINS=false`, " - 'to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.', - color=self.bot.error_color + "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.", + color=self.bot.error_color, ) return await ctx.send(embed=embed) if not self._ready_event.is_set(): embed = discord.Embed( description="Plugins are still loading, please try again later.", - color=self.bot.main_color + color=self.bot.main_color, ) return await ctx.send(embed=embed) if not self.loaded_plugins: embed = discord.Embed( description="There are no plugins currently loaded.", - color=self.bot.error_color + color=self.bot.error_color, ) return await ctx.send(embed=embed) loaded_plugins = map(str, sorted(self.loaded_plugins)) - pages = ['```\n'] + pages = ["```\n"] for plugin in loaded_plugins: - msg = str(plugin) + '\n' + msg = str(plugin) + "\n" if len(msg) + len(pages[-1]) + 3 <= 2048: pages[-1] += msg else: - pages[-1] += '```' - pages.append(f'```\n{msg}') + pages[-1] += "```" + pages.append(f"```\n{msg}") - if pages[-1][-3:] != '```': - pages[-1] += '```' + if pages[-1][-3:] != "```": + pages[-1] += "```" embeds = [] for page in pages: embed = discord.Embed( - title="Loaded plugins:", - description=page, - color=self.bot.main_color + title="Loaded plugins:", description=page, color=self.bot.main_color ) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) @@ -470,7 +484,9 @@ async def plugins_loaded(self, ctx): invoke_without_command=True, name="registry", aliases=["list", "info"] ) @checks.has_permissions(PermissionLevel.OWNER) - async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = None): + async def plugins_registry( + self, ctx, *, plugin_name: typing.Union[int, str] = None + ): """ Shows a list of all approved plugins. @@ -515,7 +531,7 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N for plugin_name, details in registry: details = self.registry[plugin_name] - user, repo = details["repository"].split('/', maxsplit=1) + user, repo = details["repository"].split("/", maxsplit=1) branch = details.get("branch") plugin = Plugin(user, repo, plugin_name, branch) @@ -528,13 +544,12 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N ) embed.add_field( - name="Installation", value=f"```{self.bot.prefix}plugins add {plugin_name}```" + name="Installation", + value=f"```{self.bot.prefix}plugins add {plugin_name}```", ) embed.set_author( - name=details["title"], - icon_url=details.get("icon_url"), - url=plugin.link + name=details["title"], icon_url=details.get("icon_url"), url=plugin.link ) if details.get("thumbnail_url"): @@ -547,10 +562,13 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N embed.set_footer(text="This plugin is currently loaded.") else: required_version = details.get("bot_version", False) - if required_version and self.bot.version < parse_version(required_version): + if required_version and self.bot.version < parse_version( + required_version + ): embed.set_footer( text=f"Your bot is unable to install this plugin, " - f"minimum required version is v{required_version}.") + f"minimum required version is v{required_version}." + ) else: embed.set_footer(text="Your bot is able to install this plugin.") @@ -571,16 +589,18 @@ async def plugins_registry_compact(self, ctx): registry = sorted(self.registry.items(), key=lambda elem: elem[0]) - pages = [''] + pages = [""] for plugin_name, details in registry: details = self.registry[plugin_name] - user, repo = details["repository"].split('/', maxsplit=1) + user, repo = details["repository"].split("/", maxsplit=1) branch = details.get("branch") plugin = Plugin(user, repo, plugin_name, branch) - desc = discord.utils.escape_markdown(details["description"].replace("\n", "")) + desc = discord.utils.escape_markdown( + details["description"].replace("\n", "") + ) name = f"[`{plugin.name}`]({plugin.link})" fmt = f"{name} - {desc}" @@ -590,13 +610,13 @@ async def plugins_registry_compact(self, ctx): if limit < 0: fmt = plugin.name limit = 75 - fmt = truncate(fmt, limit) + '[loaded]\n' + fmt = truncate(fmt, limit) + "[loaded]\n" else: limit = 75 - len(plugin_name) - 4 + len(name) if limit < 0: fmt = plugin.name limit = 75 - fmt = truncate(fmt, limit) + '\n' + fmt = truncate(fmt, limit) + "\n" if len(fmt) + len(pages[-1]) <= 2048: pages[-1] += fmt diff --git a/cogs/utility.py b/cogs/utility.py index b62cfb722e..d9fdde9837 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -314,16 +314,16 @@ async def about(self, ctx): embed.add_field( name="Want Modmail in Your Server?", value="Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " - "and join our [Discord server](https://discord.gg/F34cRU8/)!", - inline=False + "and join our [Discord server](https://discord.gg/F34cRU8/)!", + inline=False, ) embed.add_field( name="Support the Developers", value="This bot is completely free for everyone. We rely on kind individuals " - "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " - "to keep this bot free forever!", - inline=False + "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " + "to keep this bot free forever!", + inline=False, ) embed.set_footer(text=footer) @@ -617,7 +617,9 @@ async def set_presence( url = self.bot.config["twitch_url"] if activity_message: - activity = discord.Activity(type=activity_type, name=activity_message, url=url) + activity = discord.Activity( + type=activity_type, name=activity_message, url=url + ) else: msg = "You must supply an activity message to use custom activity." logger.debug(msg) @@ -643,7 +645,7 @@ async def loop_presence(self): """Set presence to the configured value every 45 minutes.""" # TODO: Does this even work? presence = await self.set_presence() - logger.debug('Loop... %s - %s', presence["activity"][1], presence["status"][1]) + logger.debug("Loop... %s - %s", presence["activity"][1], presence["status"][1]) @loop_presence.before_loop async def before_loop_presence(self): @@ -990,7 +992,7 @@ async def alias(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = utils.format_description(i, names) - embed =discord.Embed(color=self.bot.main_color, description=description) + embed = discord.Embed(color=self.bot.main_color, description=description) embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) embeds.append(embed) @@ -1521,7 +1523,11 @@ def _get_perm(self, ctx, name, type_): @permissions.command(name="get", usage="[@user] or [command/level/override] [name]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get( - self, ctx, user_or_role: Union[discord.Role, utils.User, str], *, name: str = None + self, + ctx, + user_or_role: Union[discord.Role, utils.User, str], + *, + name: str = None, ): """ View the currently-set permissions. diff --git a/core/_color_data.py b/core/_color_data.py index acfd834505..ad98d3856f 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -39,11 +39,13 @@ "light gray": "979c9f", "dark gray": "607d8b", "blurple": "7289da", - "grayple": "99aab5" + "grayple": "99aab5", } # Normalize name to "discord:" to avoid name collisions. -DISCORD_COLORS_NORM = {"discord:" + name: value for name, value in DISCORD_COLORS.items()} +DISCORD_COLORS_NORM = { + "discord:" + name: value for name, value in DISCORD_COLORS.items() +} # These colors are from Tableau diff --git a/core/changelog.py b/core/changelog.py index f687cfa57a..3e48a41f97 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -51,7 +51,9 @@ def __init__(self, bot, branch: str, version: str, lines: str): self.version = version.lstrip("vV") self.lines = lines.strip() self.fields = {} - self.changelog_url = f"https://github.com/kyb3r/modmail/blob/{branch}/CHANGELOG.md" + self.changelog_url = ( + f"https://github.com/kyb3r/modmail/blob/{branch}/CHANGELOG.md" + ) self.description = "" self.parse() @@ -126,6 +128,7 @@ class Changelog: VERSION_REGEX : re.Pattern The regex used to parse the versions. """ + VERSION_REGEX = re.compile( r"#\s*([vV]\d+\.\d+(?:\.\d+)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)?|$)", flags=re.DOTALL, @@ -135,7 +138,9 @@ def __init__(self, bot, branch: str, text: str): self.bot = bot self.text = text logger.debug("Fetching changelog from GitHub.") - self.versions = [Version(bot, branch, *m) for m in self.VERSION_REGEX.findall(text)] + self.versions = [ + Version(bot, branch, *m) for m in self.VERSION_REGEX.findall(text) + ] @property def latest_version(self) -> Version: @@ -168,8 +173,11 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": Changelog The newly created `Changelog` parsed from the `url`. """ - branch = 'master' if not bot.version.is_prerelease else 'development' - url = url or f"https://raw.githubusercontent.com/kyb3r/modmail/{branch}/CHANGELOG.md" + branch = "master" if not bot.version.is_prerelease else "development" + url = ( + url + or f"https://raw.githubusercontent.com/kyb3r/modmail/{branch}/CHANGELOG.md" + ) async with await bot.session.get(url) as resp: return cls(bot, branch, await resp.text()) diff --git a/core/config.py b/core/config.py index b67aa23329..0230a923b3 100644 --- a/core/config.py +++ b/core/config.py @@ -115,7 +115,7 @@ class ConfigManager: "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", - "enable_plugins" + "enable_plugins", } defaults = {**public_keys, **private_keys, **protected_keys} @@ -263,7 +263,7 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: hex_ = ALL_COLORS.get(name) if hex_ is None: raise - return self.__setitem__(key, '#' + hex_) + return self.__setitem__(key, "#" + hex_) if key in self.time_deltas: try: @@ -271,7 +271,9 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: except isodate.ISO8601Error: try: converter = UserFriendlyTime() - time = self.bot.loop.run_until_complete(converter.convert(None, item)) + time = self.bot.loop.run_until_complete( + converter.convert(None, item) + ) if time.arg: raise ValueError except BadArgument as exc: diff --git a/core/models.py b/core/models.py index 4d240e70c0..033457a542 100644 --- a/core/models.py +++ b/core/models.py @@ -32,7 +32,9 @@ def __init__(self, msg, *args): @property def embed(self): # Single reference of Color.red() - return discord.Embed(title="Error", description=self.msg, color=discord.Color.red()) + return discord.Embed( + title="Error", description=self.msg, color=discord.Color.red() + ) class ModmailLogger(logging.Logger): @@ -91,7 +93,6 @@ class _Default: class SafeFormatter(Formatter): - def get_field(self, field_name, args, kwargs): first, rest = _string.formatter_field_name_split(field_name) @@ -108,7 +109,7 @@ def get_field(self, field_name, args, kwargs): if n >= 2: break if is_attr: - if str(i).startswith('_'): + if str(i).startswith("_"): break obj = getattr(obj, i) else: diff --git a/core/thread.py b/core/thread.py index 915fc30013..ce25e49652 100644 --- a/core/thread.py +++ b/core/thread.py @@ -389,7 +389,10 @@ async def _close( message = self.bot.config["thread_close_response"] message = self.bot.formatter.format( - message, closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None + message, + closer=closer, + loglink=log_url, + logkey=log_data["key"] if log_data else None, ) embed.description = message @@ -459,15 +462,14 @@ async def _restart_close_timer(self): reset_time = datetime.utcnow() + timedelta(seconds=seconds) human_time = human_timedelta(dt=reset_time) - if self.bot.config.get('thread_auto_close_silently'): + if self.bot.config.get("thread_auto_close_silently"): return await self.close( closer=self.bot.user, silent=True, after=int(seconds), auto_close=True ) # Grab message close_message = self.bot.formatter.format( - self.bot.config["thread_auto_close_response"], - timeout=human_time + self.bot.config["thread_auto_close_response"], timeout=human_time ) time_marker_regex = "%t" From cf08042337326c60cdee2004bd502ff3921e6cba Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 20 Sep 2019 10:18:12 -0700 Subject: [PATCH 0404/1221] Fixed a plugin issue when downloading --- cogs/plugins.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 1f0ccf3a03..1fbc9e8aeb 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -173,13 +173,16 @@ async def download_plugin(self, plugin, force=False): f.write(raw) with zipfile.ZipFile(plugin_io) as zipf: - for member in zipf.namelist(): - path = PurePath(member) + for info in zipf.infolist(): + path = PurePath(info.filename) if len(path.parts) >= 3 and path.parts[1] == plugin.name: - with zipf.open(member) as src, ( - plugin.abs_path / Path(*path.parts[2:]) - ).open("wb") as dst: - shutil.copyfileobj(src, dst) + plugin_path = plugin.abs_path / Path(*path.parts[2:]) + if info.is_dir(): + plugin_path.mkdir(parents=True, exist_ok=True) + else: + plugin_path.parent.mkdir(parents=True, exist_ok=True) + with zipf.open(info) as src, plugin_path.open("wb") as dst: + shutil.copyfileobj(src, dst) plugin_io.close() From efae94216bf9fe8703f96585885fe88090d45f77 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 20 Sep 2019 21:40:33 -0700 Subject: [PATCH 0405/1221] Remove dir when plugin remove --- CHANGELOG.md | 3 +- cogs/modmail.py | 3 +- cogs/plugins.py | 82 +++++++++++++++++++++++++++------------------- cogs/utility.py | 15 ++++----- core/decorators.py | 14 ++++---- core/utils.py | 10 ++++++ 6 files changed, 75 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dde8659b0b..ba0edfa116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Reworked `config.get` and `config.set`, it feeds through the converters before setting/getting. - To get/set the raw value, access through `config[]`. -- Prerelease naming scheme is now `x.x.x-dev`. +- Prerelease naming scheme is now `x.x.x-devN`. +- `trigger_typing` has been moved to `core.utils.trigger_typing`, original location is deprecated. # v3.2.2 diff --git a/cogs/modmail.py b/cogs/modmail.py index 1450a405cf..b81f445b11 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -13,11 +13,10 @@ from natural.date import duration from core import checks -from core.decorators import trigger_typing from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User, create_not_found_embed, format_description +from core.utils import format_preview, User, create_not_found_embed, format_description, trigger_typing logger = logging.getLogger("Modmail") diff --git a/cogs/plugins.py b/cogs/plugins.py index 1fbc9e8aeb..3adbd8c3bb 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -22,7 +22,7 @@ from core import checks from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession -from core.utils import truncate +from core.utils import truncate, trigger_typing logger = logging.getLogger("Modmail") @@ -235,6 +235,14 @@ async def load_plugin(self, plugin): async def parse_user_input(self, ctx, plugin_name, check_version=False): + if not self._ready_event.is_set(): + embed = discord.Embed( + description="Plugins are still loading, please try again later.", + color=self.bot.main_color, + ) + await ctx.send(embed=embed) + return + if plugin_name in self.registry: details = self.registry[plugin_name] user, repo = details["repository"].split("/", maxsplit=1) @@ -281,6 +289,7 @@ async def plugins(self, ctx): @plugins.command(name="add", aliases=["install", "load"]) @checks.has_permissions(PermissionLevel.OWNER) + @trigger_typing async def plugins_add(self, ctx, *, plugin_name: str): """ Install a new plugin for the bot. @@ -313,53 +322,51 @@ async def plugins_add(self, ctx, *, plugin_name: str): ) msg = await ctx.send(embed=embed) - async with ctx.typing(): - try: - await self.download_plugin(plugin, force=True) - except Exception: - logger.warning(f"Unable to download plugin %s.", plugin, exc_info=True) - - embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.", - color=self.bot.error_color, - ) - - return await msg.edit(embed=embed) + try: + await self.download_plugin(plugin, force=True) + except Exception: + logger.warning(f"Unable to download plugin %s.", plugin, exc_info=True) - self.bot.config["plugins"].append(str(plugin)) - await self.bot.config.update() + embed = discord.Embed( + description=f"Failed to download plugin, check logs for error.", + color=self.bot.error_color, + ) - if self.bot.config.get("enable_plugins"): + return await msg.edit(embed=embed) - invalidate_caches() + self.bot.config["plugins"].append(str(plugin)) + await self.bot.config.update() - try: - await self.load_plugin(plugin) - except Exception: - logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) + if self.bot.config.get("enable_plugins"): - embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.", - color=self.bot.error_color, - ) + invalidate_caches() - return await msg.edit(embed=embed) + try: + await self.load_plugin(plugin) + except Exception: + logger.warning(f"Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description="Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*", - color=self.bot.main_color, + description=f"Failed to download plugin, check logs for error.", + color=self.bot.error_color, ) + else: embed = discord.Embed( description="Successfully installed plugin.\n" "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*\n\n" - "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " - "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.", + "Please only install plugins from developers you trust.*", color=self.bot.main_color, ) + else: + embed = discord.Embed( + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*\n\n" + "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.", + color=self.bot.main_color, + ) return await msg.edit(embed=embed) @plugins.command(name="remove", aliases=["del", "delete"]) @@ -390,6 +397,15 @@ async def plugins_remove(self, ctx, *, plugin_name: str): self.bot.config["plugins"].remove(str(plugin)) await self.bot.config.update() + shutil.rmtree( + plugin.abs_path, + onerror=lambda *args: logger.warning('Failed to remove plugin files %s: %s', plugin, str(args[2])) + ) + try: + plugin.abs_path.parent.rmdir() + plugin.abs_path.parent.parent.rmdir() + except OSError: + pass # dir not empty embed = discord.Embed( description="The plugin is successfully uninstalled.", diff --git a/cogs/utility.py b/cogs/utility.py index d9fdde9837..2fa120b5b6 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -24,7 +24,6 @@ from core import checks from core.changelog import Changelog -from core.decorators import trigger_typing from core.models import InvalidConfigError, PermissionLevel from core.paginator import EmbedPaginatorSession, MessagePaginatorSession from core import utils @@ -248,7 +247,7 @@ def cog_unload(self): @commands.command() @checks.has_permissions(PermissionLevel.REGULAR) - @trigger_typing + @utils.trigger_typing async def changelog(self, ctx, version: str.lower = ""): """Shows the changelog of the Modmail.""" changelog = await Changelog.from_url(self.bot) @@ -282,7 +281,7 @@ async def changelog(self, ctx, version: str.lower = ""): @commands.command(aliases=["bot", "info"]) @checks.has_permissions(PermissionLevel.REGULAR) - @trigger_typing + @utils.trigger_typing async def about(self, ctx): """Shows information about this bot.""" embed = discord.Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) @@ -331,7 +330,7 @@ async def about(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.REGULAR) - @trigger_typing + @utils.trigger_typing async def sponsors(self, ctx): """Shows a list of sponsors.""" resp = await self.bot.session.get( @@ -352,7 +351,7 @@ async def sponsors(self, ctx): @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) - @trigger_typing + @utils.trigger_typing async def debug(self, ctx): """Shows the recent application-logs of the bot.""" @@ -407,7 +406,7 @@ async def debug(self, ctx): @debug.command(name="hastebin", aliases=["haste"]) @checks.has_permissions(PermissionLevel.OWNER) - @trigger_typing + @utils.trigger_typing async def debug_hastebin(self, ctx): """Posts application-logs to Hastebin.""" @@ -450,7 +449,7 @@ async def debug_hastebin(self, ctx): @debug.command(name="clear", aliases=["wipe"]) @checks.has_permissions(PermissionLevel.OWNER) - @trigger_typing + @utils.trigger_typing async def debug_clear(self, ctx): """Clears the locally cached logs.""" @@ -658,7 +657,7 @@ async def before_loop_presence(self): @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) - @trigger_typing + @utils.trigger_typing async def ping(self, ctx): """Pong! Returns your websocket latency.""" embed = discord.Embed( diff --git a/core/decorators.py b/core/decorators.py index b9e6f50067..c208af2adc 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -1,12 +1,10 @@ -import functools +import warnings -from discord.ext import commands +from core.utils import trigger_typing as _trigger_typing def trigger_typing(func): - @functools.wraps(func) - async def wrapper(self, ctx: commands.Context, *args, **kwargs): - await ctx.trigger_typing() - return await func(self, ctx, *args, **kwargs) - - return wrapper + warnings.warn("trigger_typing has been moved to core.utils.trigger_typing, this will be removed.", + DeprecationWarning, + stacklevel=2) + return _trigger_typing(func) diff --git a/core/utils.py b/core/utils.py index 146f6224af..ac50504ab5 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,3 +1,4 @@ +import functools import re import shlex import typing @@ -250,3 +251,12 @@ def format_description(i, names): ": ".join((str(a + i * 15), b)) for a, b in enumerate(takewhile(lambda x: x is not None, names), start=1) ) + + +def trigger_typing(func): + @functools.wraps(func) + async def wrapper(self, ctx: commands.Context, *args, **kwargs): + await ctx.trigger_typing() + return await func(self, ctx, *args, **kwargs) + + return wrapper From 9c77931e76897360804bcfffb29dfc962d0da6cf Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Sun, 29 Sep 2019 20:26:03 +0200 Subject: [PATCH 0406/1221] Add ?logs responded command (#338) --- cogs/modmail.py | 53 +++++++++++++++++++++++++++++++++++++++++---- core/_color_data.py | 6 +++-- core/models.py | 3 +-- core/thread.py | 8 ++++--- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index f8246e17e2..dcde629c20 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -17,7 +17,13 @@ from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User, create_not_found_embed, format_description, strtobool +from core.utils import ( + format_preview, + User, + create_not_found_embed, + format_description, + strtobool, +) logger = logging.getLogger("Modmail") @@ -286,7 +292,9 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): @commands.command() @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() - async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None): + async def move( + self, ctx, category: discord.CategoryChannel, *, specifics: str = None + ): """ Move a thread to another category. @@ -297,7 +305,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = silent = False if specifics: - silent_words = ['silent', 'silently'] + silent_words = ["silent", "silently"] silent = any(word in silent_words for word in specifics.split()) await thread.channel.edit(category=category, sync_permissions=True) @@ -311,7 +319,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], - color=self.bot.main_color + color=self.bot.main_color, ) await thread.recipient.send(embed=embed) @@ -718,6 +726,43 @@ async def logs_closed_by(self, ctx, *, user: User = None): session = EmbedPaginatorSession(ctx, *embeds) await session.run() + @logs.command(name="responded") + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def logs_responded(self, ctx, *, user: User = None): + """ + Get all logs where the specified user has responded at least once. + + If no `user` is provided, the user will be the person who sent this command. + `user` may be a user ID, mention, or name. + """ + user = user if user is not None else ctx.author + + entries = [] + async for l in self.bot.db.logs.find( + { + "messages": { + "$elemMatch": { + "author.id": str(user.id), + "author.mod": True, + "type": {"$in": ["anonymous", "thread_message"]}, + } + } + } + ): + entries.append(l) + + embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) + + if not embeds: + embed = discord.Embed( + color=discord.Color.red(), + description="No log entries have been found for that query", + ) + return await ctx.send(embed=embed) + + session = EmbedPaginatorSession(ctx, *embeds) + await session.run() + @logs.command(name="search", aliases=["find"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs_search(self, ctx, limit: Optional[int] = None, *, query): diff --git a/core/_color_data.py b/core/_color_data.py index 997a6a1d25..b943645c4c 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -39,11 +39,13 @@ "light gray": "#979c9f", "dark gray": "#607d8b", "blurple": "#7289da", - "grayple": "#99aab5" + "grayple": "#99aab5", } # Normalize name to "discord:" to avoid name collisions. -DISCORD_COLORS_NORM = {"discord:" + name: value for name, value in DISCORD_COLORS.items()} +DISCORD_COLORS_NORM = { + "discord:" + name: value for name, value in DISCORD_COLORS.items() +} # These colors are from Tableau diff --git a/core/models.py b/core/models.py index 8b21c10eb0..b7cd72906a 100644 --- a/core/models.py +++ b/core/models.py @@ -90,7 +90,6 @@ class _Default: class SafeFormatter(Formatter): - def get_field(self, field_name, args, kwargs): first, rest = _string.formatter_field_name_split(field_name) @@ -107,7 +106,7 @@ def get_field(self, field_name, args, kwargs): if n >= 2: break if is_attr: - if str(i).startswith('_'): + if str(i).startswith("_"): break obj = getattr(obj, i) else: diff --git a/core/thread.py b/core/thread.py index bdf0bc7aa2..33d38410ee 100644 --- a/core/thread.py +++ b/core/thread.py @@ -394,7 +394,10 @@ async def _close( message = self.bot.config["thread_close_response"] message = self.bot.formatter.format( - message, closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None + message, + closer=closer, + loglink=log_url, + logkey=log_data["key"] if log_data else None, ) embed.description = message @@ -493,8 +496,7 @@ async def _restart_close_timer(self): # Grab message close_message = self.bot.formatter.format( - self.bot.config["thread_auto_close_response"], - timeout=human_time + self.bot.config["thread_auto_close_response"], timeout=human_time ) time_marker_regex = "%t" From 191811ec17bd038e4337170b09c20baae0f12345 Mon Sep 17 00:00:00 2001 From: DAzVise <52792999+DAzVise@users.noreply.github.com> Date: Tue, 1 Oct 2019 21:30:25 +0300 Subject: [PATCH 0407/1221] add serverstats plugin --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index fc1dd11a2e..5de1ab1784 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -187,5 +187,14 @@ "title": "Moderate your server", "icon_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png" + }, + "serverstats": { + "repository": "dazvise/modmail-plugins", + "branch": "master", + "description": "Interesting and accurate statistics about your server.", + "bot_version": "2.20.1", + "title": "Server Stats", + "icon_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png", + "thumbnail_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png" } } From 9b7f3988782c4d4dca37f8ff4f7bc36fbe19f240 Mon Sep 17 00:00:00 2001 From: Arttu mahlakaarto Date: Tue, 1 Oct 2019 23:57:16 +0300 Subject: [PATCH 0408/1221] Fixed a missing newline I noticed that the line "When contributing to this project please make sure you follow this and name your branches appropriately!" was being shown partially next to the image, partially below it. Added two spaces after the image to make the line of text appear below it instead. --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d7078e485..a95e344610 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,10 +11,10 @@ We love your input! We want to make contributing to this project as easy and tra If you are proposing new features, please discuss them with us in the [development server](https://discord.gg/etJNHCQ) before you start working on them! ## We Develop with Github -We use github to host code, to track issues and feature requests, as well as accept pull requests. +We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. ## We Use [Git Flow](https://atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) -![Simple Image Of A Git Flow Workflow](https://nvie.com/img/hotfix-branches@2x.png) +![Simple Image Of A Git Flow Workflow](https://nvie.com/img/hotfix-branches@2x.png) When contributing to this project please make sure you follow this and name your branches appropriately! ## All Code Changes Happen Through Pull Requests From 4d50ea9846cfccc74fcbb19e4bc9c2718f04d172 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Wed, 2 Oct 2019 02:37:37 +0200 Subject: [PATCH 0409/1221] added sponsor logo to the github readme (#385) * Update README.md * Update README.md * Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 71b17e8e30..471cc54671 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,11 @@ Special thanks to our sponsors for supporting the project. + + + + + Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins From 31375986694b56ed4cb010e638ae0eff4c4be1b6 Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Wed, 2 Oct 2019 20:56:57 +0200 Subject: [PATCH 0410/1221] Add anon_reply_without_command --- bot.py | 11 ++++++++++- core/config.py | 2 ++ core/config_help.json | 13 ++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index a25777160d..c3a65f2d51 100644 --- a/bot.py +++ b/bot.py @@ -867,7 +867,16 @@ async def process_commands(self, message): except ValueError: reply_without_command = self.config.remove("reply_without_command") - if reply_without_command: + try: + anon_reply_without_command = strtobool( + self.config["anon_reply_without_command"] + ) + except ValueError: + anon_reply_without_command = self.config.remove("anon_reply_without_command") + + if anon_reply_without_command: + await thread.reply(message, anonymous=True) + elif reply_without_command: await thread.reply(message) else: await self.api.append_log(message, type_="internal") diff --git a/core/config.py b/core/config.py index ed9adead23..f3dedbfe1a 100644 --- a/core/config.py +++ b/core/config.py @@ -36,6 +36,7 @@ class ConfigManager: "account_age": None, "guild_age": None, "reply_without_command": False, + "anon_reply_without_command": False, # logging "log_channel_id": None, # threads @@ -110,6 +111,7 @@ class ConfigManager: "user_typing", "mod_typing", "reply_without_command", + "anon_reply_without_command", "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", diff --git a/core/config_help.json b/core/config_help.json index 50d0a9fad9..89bff67bdb 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -111,7 +111,18 @@ "`{prefix}config set reply_without_command no`" ], "notes": [ - "Unfortunately, anonymous `reply_without_command` is currently not possible." + "See also: `anon_reply_without_command`." + ] + }, + "anon_reply_without_command": { + "default": "Disabled", + "description": "Setting this configuration will make all non-command messages sent in the thread channel to be anonymously forwarded to the recipient without the need of `{prefix}reply`.", + "examples": [ + "`{prefix}config set anon_reply_without_command yes`", + "`{prefix}config set anon_reply_without_command no`" + ], + "notes": [ + "See also: `reply_without_command`." ] }, "log_channel_id": { From 1201f21e3221fc31024ded1ca293928db9bd232e Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Fri, 4 Oct 2019 09:12:03 +0200 Subject: [PATCH 0411/1221] Moved db query to core/client;py --- bot.py | 4 +++- cogs/modmail.py | 14 +------------- core/clients.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/bot.py b/bot.py index c3a65f2d51..3abb0aae72 100644 --- a/bot.py +++ b/bot.py @@ -872,7 +872,9 @@ async def process_commands(self, message): self.config["anon_reply_without_command"] ) except ValueError: - anon_reply_without_command = self.config.remove("anon_reply_without_command") + anon_reply_without_command = self.config.remove( + "anon_reply_without_command" + ) if anon_reply_without_command: await thread.reply(message, anonymous=True) diff --git a/cogs/modmail.py b/cogs/modmail.py index dcde629c20..3fa5062b15 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -737,19 +737,7 @@ async def logs_responded(self, ctx, *, user: User = None): """ user = user if user is not None else ctx.author - entries = [] - async for l in self.bot.db.logs.find( - { - "messages": { - "$elemMatch": { - "author.id": str(user.id), - "author.mod": True, - "type": {"$in": ["anonymous", "thread_message"]}, - } - } - } - ): - entries.append(l) + entries = await self.bot.api.get_responded_logs(user.id) embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) diff --git a/core/clients.py b/core/clients.py index a8bb006e6d..edfe928cd8 100644 --- a/core/clients.py +++ b/core/clients.py @@ -92,6 +92,23 @@ async def get_user_logs(self, user_id: Union[str, int]) -> list: return await self.logs.find(query, projection).to_list(None) + async def get_responded_logs(self, user_id: Union[str, int]) -> list: + entries = [] + async for l in self.bot.db.logs.find( + { + "open": False, + "messages": { + "$elemMatch": { + "author.id": str(user_id), + "author.mod": True, + "type": {"$in": ["anonymous", "thread_message"]}, + } + } + } + ): + entries.append(l) + return entries + async def get_log(self, channel_id: Union[str, int]) -> dict: logger.debug("Retrieving channel %s logs.", channel_id) return await self.logs.find_one({"channel_id": str(channel_id)}) From bc8abd819ef32968bcf84797512f3a3c5e6f1f74 Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Fri, 4 Oct 2019 16:55:29 +0200 Subject: [PATCH 0412/1221] Fix regex for scheduled closes --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 3abb0aae72..8b176a089b 100644 --- a/bot.py +++ b/bot.py @@ -683,7 +683,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: self.blocked_users.pop(str(message.author.id)) else: reaction = blocked_emoji - end_time = re.search(r"%(.+?)%$", reason) + end_time = re.search(r"%(.+?)%", reason) if end_time is not None: logger.debug("No longer blocked, user %s.", message.author.name) after = ( From c399eff9f25842c30a2cbef4a2668d9d853c479a Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Fri, 4 Oct 2019 16:59:22 +0200 Subject: [PATCH 0413/1221] Add a comma uwu --- core/clients.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/clients.py b/core/clients.py index edfe928cd8..71525892ef 100644 --- a/core/clients.py +++ b/core/clients.py @@ -103,7 +103,7 @@ async def get_responded_logs(self, user_id: Union[str, int]) -> list: "author.mod": True, "type": {"$in": ["anonymous", "thread_message"]}, } - } + }, } ): entries.append(l) From efb659867de36edfc3ff73b1d1e19d7a53800da8 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 5 Oct 2019 23:22:01 -0700 Subject: [PATCH 0414/1221] Update --- SPONSORS.json | 125 +++++++++++++++++++++++++------------------------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index 6a4b439907..01641f0630 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -1,67 +1,68 @@ [ - { - "embed":{ - "description":"This is a youtube channel that provides high quality reddit content. Videos are uploaded regularly so you are never out of funny, interesting or even creepy content!", - "color":15114050, - "footer":{ - "icon_url":"https://i.imgur.com/fvNKUks.png", - "text":"I am a robot, son of Daniel (UK)" - }, - "thumbnail":{ - "url":"https://i.imgur.com/WyzaPKY.png" - }, - "author":{ - "name":"Sir Reddit", - "url":"https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1", - "icon_url":"https://i.imgur.com/WyzaPKY.png" - }, - "fields":[ - { - "name":"Subscribe!", - "value":"[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)" + { + "embed": { + "description": "This is a youtube channel that provides high quality reddit content. Videos are uploaded regularly so you are never out of funny, interesting or even creepy content!", + "color": 15114050, + "footer": { + "icon_url": "https://i.imgur.com/fvNKUks.png", + "text": "I am a robot, son of Daniel (UK)" }, - { - "name":"Discord Server!", - "value":"[**Click Here**](https://discord.gg/V8ErqHb)" - } - ] - } - }, - { - "embed":{ - "title":"Hey there! ", - "description":"Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/qdujXC8)", - "url":"https://discord.gg/qdujXC8", - "color":13003681, - "footer":{ - "icon_url":"https://imgur.com/Mrc9pLd.gif", - "text":"footer text" - }, - "thumbnail":{ - "url":"https://imgur.com/Mrc9pLd.gif" - }, - "image":{ - "url":"https://imgur.com/ZUFiL6b.gif" - }, - "author":{ - "name":"Roomieofficial", - "url":"https://discord.gg/qdujXC8", - "icon_url":"https://imgur.com/6hBkt7Z.png" - }, - "fields":[ - { - "name":"What is all this about🤔 ", - "value":"We are mainly focused on everything that has anything to do with music or singing! " + "thumbnail": { + "url": "https://i.imgur.com/WyzaPKY.png" }, - { - "name":"Youtube🙄", - "value":"U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads" + "author": { + "name": "Sir Reddit", + "url": "https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1", + "icon_url": "https://i.imgur.com/WyzaPKY.png" }, - { - "name":"Discord😁", - "value":"Make sure to join our [Discord](https://discord.gg/qdujXC8) We have weekly events, 24/7 chats and more! " - } - ] - } - } + "fields": [ + { + "name": "Subscribe!", + "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)" + }, + { + "name": "Discord Server!", + "value": "[**Click Here**](https://discord.gg/V8ErqHb)" + } + ] + } + }, + { + "embed": { + "title": "Hey there!", + "description": "Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/djwMSQ).", + "url": "https://discord.gg/djwMSQ", + "color": 13003681, + "timestamp": "2019-09-07T14:09:41.072Z", + "footer": { + "icon_url": "https://imgur.com/Mrc9pLd.gif", + "text": "everyone is a clown" + }, + "thumbnail": { + "url": "https://imgur.com/Mrc9pLd.gif" + }, + "image": { + "url": "https://imgur.com/ZUFiL6b.gif" + }, + "author": { + "name": "Roomieofficial", + "url": "https://discord.gg/djwMSQ", + "icon_url": "https://imgur.com/6hBkt7Z.png" + }, + "fields": [ + { + "name": "What is all this about 🤔", + "value": "We are mainly focused on everything that has anything to do with music or singing! " + }, + { + "name": "Youtube 🙄", + "value": "U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads." + }, + { + "name": "Discord 😁", + "value": "Make sure to join our [Discord](https://discord.gg/djwMSQ) We have weekly events, 24/7 chats and more!" + } + ] + } + } ] From e0a92d0a9c6c889ea0bdbe0f9334e1685a9c275e Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Thu, 12 Sep 2019 22:20:01 +0200 Subject: [PATCH 0415/1221] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 71b17e8e30..b22b65fb44 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,9 @@ Special thanks to our sponsors for supporting the project. + + + Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins From 6559f150f182a56753850c64715e1c6d1380d5a1 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Thu, 12 Sep 2019 22:22:15 +0200 Subject: [PATCH 0416/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b22b65fb44..6f415fa65f 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Special thanks to our sponsors for supporting the project. - + Become a sponsor on [Patreon](https://patreon.com/kyber). From 368dafa7917e8cf244a4093a53f710353ba3621b Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Thu, 12 Sep 2019 22:22:55 +0200 Subject: [PATCH 0417/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f415fa65f..2435268a4d 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Special thanks to our sponsors for supporting the project. - + Become a sponsor on [Patreon](https://patreon.com/kyber). From 76893c1adba9df74b02511e5a0a52c7c9af05f7b Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Thu, 12 Sep 2019 22:23:42 +0200 Subject: [PATCH 0418/1221] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2435268a4d..fac709178c 100644 --- a/README.md +++ b/README.md @@ -175,8 +175,10 @@ Special thanks to our sponsors for supporting the project. - + + + Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins From 391c791cbc2ee43522e1a437c3e216f028c8f65f Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Thu, 12 Sep 2019 22:24:44 +0200 Subject: [PATCH 0419/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fac709178c..8b06f10335 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Special thanks to our sponsors for supporting the project. - + From dee33f3c3c1aadfbbfb3f7f4e36664e7bd8408f9 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 5 Oct 2019 23:24:09 -0700 Subject: [PATCH 0420/1221] Update --- SPONSORS.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 01641f0630..97c0268fe3 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -30,7 +30,7 @@ { "embed": { "title": "Hey there!", - "description": "Nice to see you here! u can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <-and also join our discord! [Discord](https://discord.gg/djwMSQ).", + "description": "Nice to see you here! You can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <- and also join our [Discord](https://discord.gg/djwMSQ)!", "url": "https://discord.gg/djwMSQ", "color": 13003681, "timestamp": "2019-09-07T14:09:41.072Z", From bba7cfb9f1e17eedb58f2ee111e556307e38b09a Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 5 Oct 2019 23:28:30 -0700 Subject: [PATCH 0421/1221] Update sponsor --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 885dcaee58..7d5f33fdca 100644 --- a/README.md +++ b/README.md @@ -174,11 +174,6 @@ Special thanks to our sponsors for supporting the project. - - - - - From 57268cbc7fba3d7bc8bb24441e2515e1007fff1e Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 5 Oct 2019 23:30:05 -0700 Subject: [PATCH 0422/1221] Update sponsor --- SPONSORS.json | 1 - 1 file changed, 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 97c0268fe3..07f82819d6 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -33,7 +33,6 @@ "description": "Nice to see you here! You can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <- and also join our [Discord](https://discord.gg/djwMSQ)!", "url": "https://discord.gg/djwMSQ", "color": 13003681, - "timestamp": "2019-09-07T14:09:41.072Z", "footer": { "icon_url": "https://imgur.com/Mrc9pLd.gif", "text": "everyone is a clown" From 3881d228603840c729647ca09488c68e181d7a10 Mon Sep 17 00:00:00 2001 From: Robin Mahieu <42642013+papiersnipper@users.noreply.github.com> Date: Sun, 6 Oct 2019 10:40:38 +0200 Subject: [PATCH 0423/1221] Oopsie this line shouldn't be here --- cogs/modmail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 1578016a02..7e19d47768 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -18,7 +18,6 @@ from core.time import UserFriendlyTime, human_timedelta from core.utils import format_preview, User, create_not_found_embed, format_description, trigger_typing - logger = logging.getLogger("Modmail") From 5ecde89520d9457d3a8937bfe5a14925e912624f Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sun, 6 Oct 2019 08:17:50 -0400 Subject: [PATCH 0424/1221] Update mention --- core/config_help.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config_help.json b/core/config_help.json index c24b5868fa..b9f0ad8204 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -39,7 +39,7 @@ "`{prefix}mention Yo~ Here's a new thread for ya!`" ], "notes": [ - "Unfortunately, its not currently possible to disable mention. You do not have to include a mention." + "Unfortunately, it's not currently possible to disable mention. You do not have to include a mention." ] }, "main_color": { From 0c4375462d52e60f64e22fefbd0c0587d2438551 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 6 Oct 2019 19:54:02 -0700 Subject: [PATCH 0425/1221] Ability to update all plugins --- CHANGELOG.md | 1 + cogs/plugins.py | 32 +++++++++++++++++++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0edfa116..d6a5189d56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). - Help command is in alphabetical order grouped by permissions. - Notes are no longer always blurple, its set to `MAIN_COLOR` now. +- Added `?plugins update` for updating all installed plugins. ### Internal diff --git a/cogs/plugins.py b/cogs/plugins.py index 3adbd8c3bb..1e1c07c761 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -413,16 +413,8 @@ async def plugins_remove(self, ctx, *, plugin_name: str): ) await ctx.send(embed=embed) - @plugins.command(name="update") - @checks.has_permissions(PermissionLevel.OWNER) - async def plugins_update(self, ctx, *, plugin_name: str): - """ - Update a plugin for the bot. - - `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference - to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). - """ - + async def update_plugin(self, ctx, plugin_name): + logger.debug("Updating %s.", plugin_name) plugin = await self.parse_user_input(ctx, plugin_name, check_version=True) if plugin is None: return @@ -441,13 +433,31 @@ async def plugins_update(self, ctx, *, plugin_name: str): except commands.ExtensionError: logger.warning("Plugin unload fail.", exc_info=True) await self.load_plugin(plugin) - + logger.debug("Updated %s.", plugin_name) embed = discord.Embed( description=f"Successfully updated {plugin.name}.", color=self.bot.main_color, ) return await ctx.send(embed=embed) + @plugins.command(name="update") + @checks.has_permissions(PermissionLevel.OWNER) + async def plugins_update(self, ctx, *, plugin_name: str = None): + """ + Update a plugin for the bot. + + `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference + to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + + To update all plugins, do `{prefix}plugins update`. + """ + + if plugin_name is None: + for plugin_name in self.bot.config["plugins"]: + await self.update_plugin(ctx, plugin_name) + else: + await self.update_plugin(ctx, plugin_name) + @plugins.command(name="loaded", aliases=["enabled", "installed"]) @checks.has_permissions(PermissionLevel.OWNER) async def plugins_loaded(self, ctx): From 516709e83e8f6ed73019a84a3cb54eaf441dd15e Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 6 Oct 2019 20:36:40 -0700 Subject: [PATCH 0426/1221] Update README --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 71b17e8e30..e8f9d97bbf 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,12 @@ Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins -Modmail supports the use of third-party plugins to extend or add functionalities to the bot. This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. A list of third-party plugins can be found using the `plugins registry` command. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). +Modmail supports the use of third-party plugins to extend or add functionalities to the bot. +This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. + +A list of third-party plugins can be found using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. + +To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). Plugins requests and support is available in our [Modmail Plugins Server](https://discord.gg/4JE4XSW). From 82da38a793083bfcd9c7a9c1536de235c3ca4d84 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 02:01:04 -0700 Subject: [PATCH 0427/1221] Update travis --- .bandit_baseline.json | 243 ++++++++++++++++++++++++++++++++++++++++++ .travis.yml | 19 +++- CHANGELOG.md | 2 + Pipfile | 2 + Pipfile.lock | 134 +++++++++++++++++++---- cogs/utility.py | 11 +- core/thread.py | 8 +- core/utils.py | 7 -- 8 files changed, 387 insertions(+), 39 deletions(-) create mode 100644 .bandit_baseline.json diff --git a/.bandit_baseline.json b/.bandit_baseline.json new file mode 100644 index 0000000000..539fe85a88 --- /dev/null +++ b/.bandit_baseline.json @@ -0,0 +1,243 @@ +{ + "errors": [], + "generated_at": "2019-10-07T08:19:22Z", + "metrics": { + "./bot.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 933, + "nosec": 0 + }, + "_totals": { + "CONFIDENCE.HIGH": 2.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 1.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 2.0, + "SEVERITY.MEDIUM": 1.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 7299, + "nosec": 0 + }, + "cogs/modmail.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 973, + "nosec": 0 + }, + "cogs/plugins.py": { + "CONFIDENCE.HIGH": 1.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 1.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 537, + "nosec": 0 + }, + "cogs/utility.py": { + "CONFIDENCE.HIGH": 1.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 1.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 1587, + "nosec": 0 + }, + "core/_color_data.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 1168, + "nosec": 0 + }, + "core/changelog.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 154, + "nosec": 0 + }, + "core/checks.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 75, + "nosec": 0 + }, + "core/clients.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 200, + "nosec": 0 + }, + "core/config.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 276, + "nosec": 0 + }, + "core/decorators.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 7, + "nosec": 0 + }, + "core/models.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 91, + "nosec": 0 + }, + "core/paginator.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 214, + "nosec": 0 + }, + "core/thread.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 716, + "nosec": 0 + }, + "core/time.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 169, + "nosec": 0 + }, + "core/utils.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 1.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 1.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 199, + "nosec": 0 + } + }, + "results": [ + { + "code": "14 from site import USER_SITE\n15 from subprocess import PIPE\n16 \n17 import discord\n", + "filename": "cogs/plugins.py", + "issue_confidence": "HIGH", + "issue_severity": "LOW", + "issue_text": "Consider possible security implications associated with PIPE module.", + "line_number": 15, + "line_range": [ + 15, + 16 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", + "test_id": "B404", + "test_name": "blacklist" + }, + { + "code": "1824 try:\n1825 exec(to_compile, env) # pylint: disable=exec-used\n1826 except Exception as exc:\n", + "filename": "cogs/utility.py", + "issue_confidence": "HIGH", + "issue_severity": "MEDIUM", + "issue_text": "Use of exec detected.", + "line_number": 1825, + "line_range": [ + 1825 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b102_exec_used.html", + "test_id": "B102", + "test_name": "exec_used" + }, + { + "code": "219 for token in shlex.shlex(alias, punctuation_chars=\"&\"):\n220 if token != \"&&\":\n221 buffer += \" \" + token\n", + "filename": "core/utils.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: '&&'", + "line_number": 220, + "line_range": [ + 220 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b105_hardcoded_password_string.html", + "test_id": "B105", + "test_name": "hardcoded_password_string" + } + ] +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 14f9928d1b..65654b83fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,27 @@ language: python matrix: include: - python: '3.7' - dist: xenial + name: "Python 3.7.1 on Xenial Linux" + - python: '3.6' + name: "Python 3.6.7 on Xenial Linux" + - name: "Python 3.7.4 on macOS" + os: osx + osx_image: xcode11 + language: shell + - name: "Python 3.7.4 on Windows" + os: windows + language: shell + before_install: + - choco install python + - python -m pip install --upgrade pip + env: PATH=/c/Python37:/c/Python37/Scripts:$PATH install: + - pip3 install --upgrade pip + - pip3 install pipenv - pipenv install -d script: + - pipenv run bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - pipenv run python .lint.py + - pipenv run flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 --exit-zero diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a5189d56..a50704deff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Help command is in alphabetical order grouped by permissions. - Notes are no longer always blurple, its set to `MAIN_COLOR` now. - Added `?plugins update` for updating all installed plugins. +- Reintroduce flake8 and use bandit for security issues detection. +- Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. ### Internal diff --git a/Pipfile b/Pipfile index cfb4f8ba59..7b54ff37b2 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,8 @@ verify_ssl = true [dev-packages] black = "==19.3b0" pylint = "*" +bandit = "==1.6.2" +flake8 = "==3.7.8" [packages] colorama = ">=0.4.0" diff --git a/Pipfile.lock b/Pipfile.lock index ee4681eb19..431469cdcf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "564773a120ff7ba8214e5efd98fab8daf98230eb4b27f8af2dd63afcad39994c" + "sha256": "f5ceea517895626871c9bb2c559e39f6b81859f55669897aa9533154a4df1431" }, "pipfile-spec": 6, "requires": { @@ -53,17 +53,17 @@ }, "attrs": { "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", + "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" ], - "version": "==19.1.0" + "version": "==19.2.0" }, "certifi": { "hashes": [ - "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", - "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", + "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" ], - "version": "==2019.6.16" + "version": "==2019.9.11" }, "chardet": { "hashes": [ @@ -97,10 +97,10 @@ }, "emoji": { "hashes": [ - "sha256:b68112d40482a05e5da5d53da33d0aba3cce96891282c9c179cc340003c6c64e" + "sha256:60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174" ], "index": "pypi", - "version": "==0.5.3" + "version": "==0.5.4" }, "future": { "hashes": [ @@ -261,10 +261,10 @@ }, "virtualenv": { "hashes": [ - "sha256:94a6898293d07f84a98add34c4df900f8ec64a570292279f6d91c781d37fd305", - "sha256:f6fc312c031f2d2344f885de114f1cb029dfcffd26aa6e57d2ee2296935c4e7d" + "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", + "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" ], - "version": "==16.7.4" + "version": "==16.7.5" }, "virtualenv-clone": { "hashes": [ @@ -326,17 +326,25 @@ }, "astroid": { "hashes": [ - "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", - "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" + "sha256:98c665ad84d10b18318c5ab7c3d203fe11714cbad2a4aef4f44651f415392754", + "sha256:b7546ffdedbf7abcfbff93cd1de9e9980b1ef744852689decc5aeada324238c6" ], - "version": "==2.2.5" + "version": "==2.3.1" }, "attrs": { "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", + "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" ], - "version": "==19.1.0" + "version": "==19.2.0" + }, + "bandit": { + "hashes": [ + "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", + "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065" + ], + "index": "pypi", + "version": "==1.6.2" }, "black": { "hashes": [ @@ -353,6 +361,35 @@ ], "version": "==7.0" }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "flake8": { + "hashes": [ + "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", + "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" + ], + "index": "pypi", + "version": "==3.7.8" + }, + "gitdb2": { + "hashes": [ + "sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", + "sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b" + ], + "version": "==2.0.6" + }, + "gitpython": { + "hashes": [ + "sha256:631263cc670aa56ce3d3c414cf0fe2e840f2e913514b138ea28d88a477bbcd21", + "sha256:6e97b9f0954807f30c2dd8e3165731ed6c477a1b365f194b69d81d7940a08332" + ], + "version": "==3.0.3" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", @@ -390,13 +427,52 @@ ], "version": "==0.6.1" }, + "pbr": { + "hashes": [ + "sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", + "sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9" + ], + "version": "==5.4.3" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, "pylint": { "hashes": [ - "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", - "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" + "sha256:7edbae11476c2182708063ac387a8f97c760d9cfe36a5ede0ca996f90cf346c8", + "sha256:844ce067788028c1a35086a5c66bc5e599ddd851841c41d6ee1623b36774d9f2" ], "index": "pypi", - "version": "==2.3.1" + "version": "==2.4.2" + }, + "pyyaml": { + "hashes": [ + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "version": "==5.1.2" }, "six": { "hashes": [ @@ -405,6 +481,20 @@ ], "version": "==1.12.0" }, + "smmap2": { + "hashes": [ + "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", + "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a" + ], + "version": "==2.0.5" + }, + "stevedore": { + "hashes": [ + "sha256:01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", + "sha256:e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14" + ], + "version": "==1.31.0" + }, "toml": { "hashes": [ "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", @@ -430,7 +520,7 @@ "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" ], - "markers": "implementation_name == 'cpython'", + "markers": "implementation_name == 'cpython' and python_version < '3.8'", "version": "==1.4.0" }, "wrapt": { diff --git a/cogs/utility.py b/cogs/utility.py index 2fa120b5b6..da95df297b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -272,12 +272,11 @@ async def changelog(self, ctx, version: str.lower = ""): except Exception: try: await paginator.close() - except Exception: - pass - logger.warning("Failed to display changelog.", exc_info=True) - await ctx.send( - f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}" - ) + finally: + logger.warning("Failed to display changelog.", exc_info=True) + await ctx.send( + f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}" + ) @commands.command(aliases=["bot", "info"]) @checks.has_permissions(PermissionLevel.REGULAR) diff --git a/core/thread.py b/core/thread.py index ce25e49652..bb53eed455 100644 --- a/core/thread.py +++ b/core/thread.py @@ -12,7 +12,7 @@ from discord.ext.commands import MissingRequiredArgument, CommandError from core.time import human_timedelta -from core.utils import is_image_url, days, match_user_id, truncate, ignore +from core.utils import is_image_url, days, match_user_id, truncate logger = logging.getLogger("Modmail") @@ -761,8 +761,10 @@ async def send( self.ready = True if delete_message: - self.bot.loop.create_task(ignore(message.delete())) - + try: + await message.delete() + except discord.HTTPException: + logger.warning('Cannot delete message.', exc_info=True) return msg def get_notifications(self) -> str: diff --git a/core/utils.py b/core/utils.py index ac50504ab5..fd85f0d5b0 100644 --- a/core/utils.py +++ b/core/utils.py @@ -195,13 +195,6 @@ def match_user_id(text: str) -> int: return -1 -async def ignore(coro): - try: - await coro - except Exception: - pass - - def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discord.Embed: # Single reference of Color.red() embed = discord.Embed( From 06c8f783ea00eee0b5e2ae8ec7fa25ed75ece5b7 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 02:17:35 -0700 Subject: [PATCH 0428/1221] add main check --- .lint.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.lint.py b/.lint.py index 1cd6feba43..fd3cbc2f19 100644 --- a/.lint.py +++ b/.lint.py @@ -1,16 +1,17 @@ -import sys -from os import listdir -from os.path import join +if __name__ == '__main__': + import sys + from os import listdir + from os.path import join -from pylint.lint import Run + from pylint.lint import Run -THRESHOLD = 9.75 + THRESHOLD = 9.75 -cogs = [join("cogs", c) for c in listdir("cogs") if c.endswith(".py")] -core = [join("core", c) for c in listdir("core") if c.endswith(".py")] + cogs = [join("cogs", c) for c in listdir("cogs") if c.endswith(".py")] + core = [join("core", c) for c in listdir("core") if c.endswith(".py")] -results = Run(["bot.py", *cogs, *core], do_exit=False) + results = Run(["bot.py", *cogs, *core], do_exit=False) -score = results.linter.stats["global_note"] -if score <= THRESHOLD: - sys.exit(1) + score = results.linter.stats["global_note"] + if score <= THRESHOLD: + sys.exit(1) From 7a24b64654d7deb7b0c2aa3232bd31649eb08954 Mon Sep 17 00:00:00 2001 From: Robin Mahieu <42642013+papiersnipper@users.noreply.github.com> Date: Mon, 7 Oct 2019 18:46:13 +0200 Subject: [PATCH 0429/1221] Make error message more personal Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7e19d47768..fa634b6108 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -722,7 +722,7 @@ async def logs_responded(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( color=discord.Color.red(), - description="No log entries have been found for that query", + description=f"{getattr(user, 'mention', user.id)} has not responded to any threads.", ) return await ctx.send(embed=embed) From 31c2c072c9a3fbb08d312a0e531ae69f2af76ac1 Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Mon, 7 Oct 2019 18:56:21 +0200 Subject: [PATCH 0430/1221] Switch to the new features in the current dev branch --- bot.py | 20 ++------------------ cogs/modmail.py | 2 +- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/bot.py b/bot.py index 36cf8f3cd6..170afe47ce 100644 --- a/bot.py +++ b/bot.py @@ -829,25 +829,9 @@ async def process_commands(self, message): thread = await self.threads.find(channel=ctx.channel) if thread is not None: - try: - reply_without_command = strtobool( - self.config["reply_without_command"] - ) - except ValueError: - reply_without_command = self.config.remove("reply_without_command") - - try: - anon_reply_without_command = strtobool( - self.config["anon_reply_without_command"] - ) - except ValueError: - anon_reply_without_command = self.config.remove( - "anon_reply_without_command" - ) - - if anon_reply_without_command: + if self.config.get("anon_reply_without_command"): await thread.reply(message, anonymous=True) - elif reply_without_command: + elif self.config.get("reply_without_command"): await thread.reply(message) else: await self.api.append_log(message, type_="internal") diff --git a/cogs/modmail.py b/cogs/modmail.py index fa634b6108..9e11a26a3f 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -721,7 +721,7 @@ async def logs_responded(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( - color=discord.Color.red(), + color=self.bot.error_color, description=f"{getattr(user, 'mention', user.id)} has not responded to any threads.", ) return await ctx.send(embed=embed) From ed8ee39f6e2f25f475156350a2a8b3f51c71ade7 Mon Sep 17 00:00:00 2001 From: papiersnipper Date: Mon, 7 Oct 2019 18:59:17 +0200 Subject: [PATCH 0431/1221] I heard we're still using balck --- cogs/modmail.py | 8 +++++++- cogs/plugins.py | 4 +++- core/decorators.py | 8 +++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 9e11a26a3f..72f4f9f8ed 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -16,7 +16,13 @@ from core.models import PermissionLevel from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta -from core.utils import format_preview, User, create_not_found_embed, format_description, trigger_typing +from core.utils import ( + format_preview, + User, + create_not_found_embed, + format_description, + trigger_typing, +) logger = logging.getLogger("Modmail") diff --git a/cogs/plugins.py b/cogs/plugins.py index 3adbd8c3bb..5a36f66d31 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -399,7 +399,9 @@ async def plugins_remove(self, ctx, *, plugin_name: str): await self.bot.config.update() shutil.rmtree( plugin.abs_path, - onerror=lambda *args: logger.warning('Failed to remove plugin files %s: %s', plugin, str(args[2])) + onerror=lambda *args: logger.warning( + "Failed to remove plugin files %s: %s", plugin, str(args[2]) + ), ) try: plugin.abs_path.parent.rmdir() diff --git a/core/decorators.py b/core/decorators.py index c208af2adc..0107a6b2d6 100644 --- a/core/decorators.py +++ b/core/decorators.py @@ -4,7 +4,9 @@ def trigger_typing(func): - warnings.warn("trigger_typing has been moved to core.utils.trigger_typing, this will be removed.", - DeprecationWarning, - stacklevel=2) + warnings.warn( + "trigger_typing has been moved to core.utils.trigger_typing, this will be removed.", + DeprecationWarning, + stacklevel=2, + ) return _trigger_typing(func) From 4050d31dbc4b3910eefa3fb2e61688e399fd0122 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 12:42:26 -0700 Subject: [PATCH 0432/1221] Updated changelog 3.3.0-dev2 --- CHANGELOG.md | 15 +++++++++++---- bot.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a50704deff..1bbf974230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,20 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev1 +# v3.3.0-dev2 ### Added -- Two new config vars: - - `ENABLE_PLUGINS` (yes/no default yes), when set to no, plugins will not be loaded into the bot. - - `ERROR_COLOR` (color format, defaults discord red), the color of error messages. +- Three new config vars: + - `enable_plugins` (yes/no default yes) + - When set to no, plugins will not be loaded into the bot. + - `error_color` (color format, defaults discord red) + - The color of error messages. + - `anon_reply_without_command` (yes/no default no) (Thanks to papiersnipper PR#288) + - When set, all non-command messages sent to thread channels are forwarded to the recipient anonymously without the need of `?anonreply`. + - This config takes precedence over `reply_without_command`. +- `?logs responded [user]` command, it will show all logs that the user has sent an reply. (Thanks to papiersnipper PR#288) + - `user` when not provided, defaults to the user who ran the command. ### Changed diff --git a/bot.py b/bot.py index 170afe47ce..212b8041de 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev1" +__version__ = "3.3.0-dev2" import asyncio import logging From 1326e5d65b267fe8ca293d0c79529de4ce7491b5 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 13:31:37 -0700 Subject: [PATCH 0433/1221] Auto close limbo open threads --- CHANGELOG.md | 1 + bot.py | 23 +++++++++++++++++++++++ core/clients.py | 32 +++++++++++++++++--------------- core/thread.py | 5 +++-- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbf974230..ffe4c28b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - This config takes precedence over `reply_without_command`. - `?logs responded [user]` command, it will show all logs that the user has sent an reply. (Thanks to papiersnipper PR#288) - `user` when not provided, defaults to the user who ran the command. +- Open threads in limbo now auto closes if the channel cannot be found. This check is done every time the bot restarts. ### Changed diff --git a/bot.py b/bot.py index 212b8041de..ec9bc4c857 100644 --- a/bot.py +++ b/bot.py @@ -502,6 +502,29 @@ async def on_ready(self): auto_close=items.get("auto_close", False), ) + for log in await self.api.get_open_logs(): + if self.get_channel(int(log['channel_id'])) is None: + logger.debug("Unable to resolve thread with channel %s.", log['channel_id']) + log_data = await self.api.post_log( + log['channel_id'], + { + "open": False, + "closed_at": str(datetime.utcnow()), + "close_message": "Channel has been deleted, no closer found.", + "closer": { + "id": str(self.user.id), + "name": self.user.name, + "discriminator": self.user.discriminator, + "avatar_url": str(self.user.avatar_url), + "mod": True, + }, + }, + ) + if log_data: + logger.debug("Successfully closed thread with channel %s.", log['channel_id']) + else: + logger.debug("Failed to close thread with channel %s, skipping.", log['channel_id']) + self.metadata_loop = tasks.Loop( self.post_metadata, seconds=0, diff --git a/core/clients.py b/core/clients.py index 71525892ef..f01753bf23 100644 --- a/core/clients.py +++ b/core/clients.py @@ -93,21 +93,23 @@ async def get_user_logs(self, user_id: Union[str, int]) -> list: return await self.logs.find(query, projection).to_list(None) async def get_responded_logs(self, user_id: Union[str, int]) -> list: - entries = [] - async for l in self.bot.db.logs.find( - { - "open": False, - "messages": { - "$elemMatch": { - "author.id": str(user_id), - "author.mod": True, - "type": {"$in": ["anonymous", "thread_message"]}, - } - }, - } - ): - entries.append(l) - return entries + query = { + "open": False, + "messages": { + "$elemMatch": { + "author.id": str(user_id), + "author.mod": True, + "type": {"$in": ["anonymous", "thread_message"]}, + } + }, + } + return await self.logs.find(query).to_list(None) + + async def get_open_logs(self) -> list: + query = { + "open": True + } + return await self.logs.find(query).to_list(None) async def get_log(self, channel_id: Union[str, int]) -> dict: logger.debug("Retrieving channel %s logs.", channel_id) diff --git a/core/thread.py b/core/thread.py index bb53eed455..207fdae97d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -762,8 +762,9 @@ async def send( if delete_message: try: - await message.delete() - except discord.HTTPException: + if isinstance(message.channel, discord.TextChannel): + await message.delete() + except Exception: logger.warning('Cannot delete message.', exc_info=True) return msg From 25162b7efb63d2fd7ce53146a0423d5b2c4b8c15 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 13:37:07 -0700 Subject: [PATCH 0434/1221] Add closer check --- cogs/modmail.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 72f4f9f8ed..8701ac6bd6 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -626,7 +626,12 @@ def format_log_embeds(self, logs, avatar_url): embed.add_field( name="Created", value=duration(created_at, now=datetime.utcnow()) ) - embed.add_field(name="Closed By", value=f"<@{entry['closer']['id']}>") + closer = entry.get('closer') + if closer is None: + closer = 'Unknown' + else: + closer = f"<@{closer['id']}>" + embed.add_field(name="Closed By", value=closer) if entry["recipient"]["id"] != entry["creator"]["id"]: embed.add_field(name="Created by", value=f"<@{entry['creator']['id']}>") From df0853f6ba98dfa9258f1c8b2c6ec783b54edc13 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 13:42:58 -0700 Subject: [PATCH 0435/1221] No closer -> no log url --- cogs/modmail.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 8701ac6bd6..9ebc36d8fd 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -628,10 +628,10 @@ def format_log_embeds(self, logs, avatar_url): ) closer = entry.get('closer') if closer is None: - closer = 'Unknown' + closer_msg = 'Unknown' else: - closer = f"<@{closer['id']}>" - embed.add_field(name="Closed By", value=closer) + closer_msg = f"<@{closer['id']}>" + embed.add_field(name="Closed By", value=closer_msg) if entry["recipient"]["id"] != entry["creator"]["id"]: embed.add_field(name="Created by", value=f"<@{entry['creator']['id']}>") @@ -639,7 +639,13 @@ def format_log_embeds(self, logs, avatar_url): embed.add_field( name="Preview", value=format_preview(entry["messages"]), inline=False ) - embed.add_field(name="Link", value=log_url) + + if closer is not None: + # BUG: Currently, logviewer can't display logs without a closer. + embed.add_field(name="Link", value=log_url) + else: + embed.add_field(name="Log Key", value=entry['key']) + embed.set_footer(text="Recipient ID: " + str(entry["recipient"]["id"])) embeds.append(embed) return embeds From 8e01aaafbfd2045c5abf5e62a4574dfe7a662e13 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 7 Oct 2019 13:49:10 -0700 Subject: [PATCH 0436/1221] Formatting --- cogs/modmail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 9ebc36d8fd..67e840bded 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -644,7 +644,8 @@ def format_log_embeds(self, logs, avatar_url): # BUG: Currently, logviewer can't display logs without a closer. embed.add_field(name="Link", value=log_url) else: - embed.add_field(name="Log Key", value=entry['key']) + logger.debug("Invalid log entry: no closer.") + embed.add_field(name="Log Key", value=f"`{entry['key']}`") embed.set_footer(text="Recipient ID: " + str(entry["recipient"]["id"])) embeds.append(embed) From 1267b8181bf42858df3575e4fe26f232f8942c07 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 9 Oct 2019 01:43:53 -0700 Subject: [PATCH 0437/1221] Improved internal activity logic --- CHANGELOG.md | 3 + bot.py | 2 +- cogs/utility.py | 170 ++++++++++++++++++++---------------------------- core/config.py | 25 +++++++ core/thread.py | 14 ++-- 5 files changed, 106 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe4c28b52..add6bb5d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - Added `?plugins update` for updating all installed plugins. - Reintroduce flake8 and use bandit for security issues detection. - Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. +- Eval commands are logged in debug logs. +- Presence updates 30 minutes instead of 45 now. ### Internal @@ -44,6 +46,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - To get/set the raw value, access through `config[]`. - Prerelease naming scheme is now `x.x.x-devN`. - `trigger_typing` has been moved to `core.utils.trigger_typing`, original location is deprecated. +- Simpler status and activity logic. # v3.2.2 diff --git a/bot.py b/bot.py index ec9bc4c857..07a4bc5096 100644 --- a/bot.py +++ b/bot.py @@ -444,7 +444,7 @@ async def setup_indexes(self): ("key", "text"), ] ) - logger.debug("Successfully set up database indexes.") + logger.debug("Successfully configured and verified database indexes.") async def on_ready(self): """Bot startup, sets uptime.""" diff --git a/cogs/utility.py b/cogs/utility.py index da95df297b..d8dd9f97a5 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -501,20 +501,23 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): if not message: raise commands.MissingRequiredArgument(SimpleNamespace(name="message")) - activity, msg = ( - await self.set_presence( - activity_identifier=activity_type, - activity_by_key=True, - activity_message=message, - ) - )["activity"] - if activity is None: + try: + activity_type = ActivityType[activity_type] + except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="activity")) + activity, _ = await self.set_presence(activity_type=activity_type, activity_message=message) + self.bot.config["activity_type"] = activity.type.value - self.bot.config["activity_message"] = message + self.bot.config["activity_message"] = activity.name await self.bot.config.update() + msg = f"Activity set to: {activity.type.name.capitalize()} " + if activity.type == ActivityType.listening: + msg += f"to {activity.name}." + else: + msg += f"{activity.name}." + embed = discord.Embed( title="Activity Changed", description=msg, color=self.bot.main_color ) @@ -529,8 +532,7 @@ async def status(self, ctx, *, status_type: str.lower): Possible status types: - `online` - `idle` - - `dnd` - - `do_not_disturb` or `do not disturb` + - `dnd` or `do not disturb` - `invisible` or `offline` To remove the current status: @@ -542,117 +544,85 @@ async def status(self, ctx, *, status_type: str.lower): await self.set_presence() embed = discord.Embed(title="Status Removed", color=self.bot.main_color) return await ctx.send(embed=embed) - status_type = status_type.replace(" ", "_") - status, msg = ( - await self.set_presence(status_identifier=status_type, status_by_key=True) - )["status"] - if status is None: + status_type = status_type.replace(" ", "_") + try: + status = Status[status_type] + except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="status")) + _, status = await self.set_presence(status=status) + self.bot.config["status"] = status.value await self.bot.config.update() + msg = f"Status set to: {status.value}." embed = discord.Embed( title="Status Changed", description=msg, color=self.bot.main_color ) return await ctx.send(embed=embed) - async def set_presence( - self, - *, - status_identifier=None, - status_by_key=True, - activity_identifier=None, - activity_by_key=True, - activity_message=None, - ): - - activity = status = None - if status_identifier is None: - status_identifier = self.bot.config["status"] - status_by_key = False + async def set_presence(self, *, status=None, activity_type=None, activity_message=None): - try: - if status_by_key: - status = Status[status_identifier] - else: - status = Status.try_value(status_identifier) - except (KeyError, ValueError): - if status_identifier is not None: - msg = f"Invalid status type: {status_identifier}" - logger.warning(msg) - - if activity_identifier is None: - if activity_message is not None: - raise ValueError( - "activity_message must be None if activity_identifier is None." - ) - activity_identifier = self.bot.config["activity_type"] - activity_by_key = False - - try: - if activity_by_key: - activity_type = ActivityType[activity_identifier] - else: - activity_type = ActivityType.try_value(activity_identifier) - except (KeyError, ValueError): - if activity_identifier is not None: - msg = f"Invalid activity type: {activity_identifier}" - logger.warning(msg) + if status is None: + status = self.bot.config.get("status") + + if activity_type is None: + activity_type = self.bot.config.get("activity_type") + + url = None + activity_message = (activity_message or self.bot.config["activity_message"]).strip() + if activity_type is not None and not activity_message: + logger.warning("No activity message found whilst activity is provided, defaults to \"Modmail\".") + activity_message = "Modmail" + + if activity_type == ActivityType.listening: + if activity_message.lower().startswith("to "): + # The actual message is after listening to [...] + # discord automatically add the "to" + activity_message = activity_message[3:].strip() + elif activity_type == ActivityType.streaming: + url = self.bot.config["twitch_url"] + + if activity_type is not None: + activity = discord.Activity( + type=activity_type, name=activity_message, url=url + ) else: - url = None - activity_message = ( - activity_message or self.bot.config["activity_message"] - ).strip() - - if activity_type == ActivityType.listening: - if activity_message.lower().startswith("to "): - # The actual message is after listening to [...] - # discord automatically add the "to" - activity_message = activity_message[3:].strip() - elif activity_type == ActivityType.streaming: - url = self.bot.config["twitch_url"] - - if activity_message: - activity = discord.Activity( - type=activity_type, name=activity_message, url=url - ) - else: - msg = "You must supply an activity message to use custom activity." - logger.debug(msg) - + activity = None await self.bot.change_presence(activity=activity, status=status) - presence = { - "activity": (None, "No activity has been set."), - "status": (None, "No status has been set."), - } - if activity is not None: - use_to = "to " if activity.type == ActivityType.listening else "" - msg = f"Activity set to: {activity.type.name.capitalize()} " - msg += f"{use_to}{activity.name}." - presence["activity"] = (activity, msg) - if status is not None: - msg = f"Status set to: {status.value}." - presence["status"] = (status, msg) - return presence + return activity, status - @tasks.loop(minutes=45) + @tasks.loop(minutes=30) async def loop_presence(self): """Set presence to the configured value every 45 minutes.""" - # TODO: Does this even work? - presence = await self.set_presence() - logger.debug("Loop... %s - %s", presence["activity"][1], presence["status"][1]) + logger.debug("Resetting presence.") + await self.set_presence() @loop_presence.before_loop async def before_loop_presence(self): await self.bot.wait_for_connected() logger.line() - presence = await self.set_presence() - logger.info(presence["activity"][1]) - logger.info(presence["status"][1]) - await asyncio.sleep(2700) + activity, status = await self.set_presence() + + if activity is not None: + msg = f"Activity set to: {activity.type.name.capitalize()} " + if activity.type == ActivityType.listening: + msg += f"to {activity.name}." + else: + msg += f"{activity.name}." + logger.info(msg) + else: + logger.info("No activity has been set.") + if status is not None: + msg = f"Status set to: {status.value}." + logger.info(msg) + else: + logger.info("No status has been set.") + + await asyncio.sleep(1800) + logger.info("Starting presence loop.") @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) @@ -1789,6 +1759,8 @@ async def oauth_show(self, ctx): async def eval_(self, ctx, *, body: str): """Evaluates Python code.""" + logger.warning("Running eval command:\n%s", body) + env = { "ctx": ctx, "bot": self.bot, diff --git a/core/config.py b/core/config.py index e83751aecc..775781eeb9 100644 --- a/core/config.py +++ b/core/config.py @@ -120,6 +120,8 @@ class ConfigManager: "enable_plugins", } + special_types = {"status", "activity_type"} + defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) @@ -236,6 +238,26 @@ def get(self, key: str, convert=True) -> typing.Any: except ValueError: value = self.remove(key) + elif key in self.special_types: + if value is None: + return + + if key == "status": + try: + # noinspection PyArgumentList + value = discord.Status(value) + except ValueError: + logger.warning("Invalid status %s.", value) + value = self.remove(key) + + elif key == "activity_type": + try: + # noinspection PyArgumentList + value = discord.ActivityType(value) + except ValueError: + logger.warning("Invalid activity %s.", value) + value = self.remove(key) + return value def set(self, key: str, item: typing.Any, convert=True) -> None: @@ -294,6 +316,9 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: except ValueError: raise InvalidConfigError("Must be a yes/no value.") + # elif key in self.special_types: + # if key == "status": + return self.__setitem__(key, item) def remove(self, key: str) -> typing.Any: diff --git a/core/thread.py b/core/thread.py index 207fdae97d..13669739b5 100644 --- a/core/thread.py +++ b/core/thread.py @@ -661,8 +661,6 @@ async def send( url=message.jump_url, ) - delete_message = not bool(message.attachments) - ext = [(a.url, a.filename) for a in message.attachments] images = [] @@ -736,6 +734,12 @@ async def send( embed.set_footer(text=mod_tag) # Normal messages else: embed.set_footer(text=self.bot.config["anon_tag"]) + delete_message = not bool(message.attachments) + if delete_message: + try: + await message.delete() + except Exception as e: + logger.warning('Cannot delete message: %s.', str(e)) elif note: embed.colour = self.bot.main_color else: @@ -760,12 +764,6 @@ async def send( await asyncio.gather(*additional_images) self.ready = True - if delete_message: - try: - if isinstance(message.channel, discord.TextChannel): - await message.delete() - except Exception: - logger.warning('Cannot delete message.', exc_info=True) return msg def get_notifications(self) -> str: From 473cb9e7945579b322c128d9b7a054206f28deeb Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 10 Oct 2019 16:52:24 -0700 Subject: [PATCH 0438/1221] Catch an error --- bot.py | 2 +- core/thread.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 07a4bc5096..686d7c3667 100644 --- a/bot.py +++ b/bot.py @@ -911,7 +911,7 @@ async def on_raw_reaction_add(self, payload): try: message = await channel.fetch_message(payload.message_id) - except discord.NotFound: + except (discord.NotFound, discord.Forbidden): return reaction = payload.emoji diff --git a/core/thread.py b/core/thread.py index 13669739b5..95991c8c51 100644 --- a/core/thread.py +++ b/core/thread.py @@ -735,7 +735,7 @@ async def send( else: embed.set_footer(text=self.bot.config["anon_tag"]) delete_message = not bool(message.attachments) - if delete_message: + if delete_message and isinstance(message.channel, discord.TextChannel): try: await message.delete() except Exception as e: From c5cca62d82ee571157a692ec733c2b9f4a2d6826 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 10 Oct 2019 17:22:06 -0700 Subject: [PATCH 0439/1221] Modified logging outputs --- bot.py | 26 ++++++++++++++------------ cogs/plugins.py | 4 ++-- cogs/utility.py | 2 +- core/config.py | 2 +- core/models.py | 17 ++++++++++------- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/bot.py b/bot.py index 686d7c3667..30e631fb65 100644 --- a/bot.py +++ b/bot.py @@ -46,7 +46,8 @@ ch = logging.StreamHandler(stream=sys.stdout) ch.setLevel(logging.INFO) -formatter = logging.Formatter("%(filename)s[%(lineno)d] - %(levelname)s: %(message)s") +formatter = logging.Formatter("%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", + datefmt="%b %d %H:%M:%S") ch.setFormatter(formatter) logger.addHandler(ch) @@ -122,16 +123,16 @@ def startup(self): logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") - logger.line() + logger.line('debug') for cog in self.loaded_cogs: - logger.info("Loading %s.", cog) + logger.debug("Loading %s.", cog) try: self.load_extension(cog) - logger.info("Successfully loaded %s.", cog) + logger.debug("Successfully loaded %s.", cog) except Exception: logger.exception("Failed to load %s.", cog) - logger.line() + logger.line('debug') def _configure_logging(self): level_text = self.config["log_level"].upper() @@ -409,14 +410,13 @@ def command_perm(self, command_name: str) -> PermissionLevel: return level async def on_connect(self): - logger.line() try: await self.validate_database_connection() except Exception: logger.debug("Logging out due to failed database connection.") return await self.logout() - logger.info("Connected to gateway.") + logger.debug("Connected to gateway.") await self.config.refresh() await self.setup_indexes() self._connected.set() @@ -457,10 +457,11 @@ async def on_ready(self): return await self.logout() logger.line() - logger.info("Client ready.") - logger.line() + logger.debug("Client ready.") logger.info("Logged in as: %s", self.user) - logger.info("User ID: %s", self.user.id) + logger.info("Bot ID: %s", self.user.id) + owners = ", ".join(getattr(self.get_user(owner_id), "name", str(owner_id)) for owner_id in self.owner_ids) + logger.info("Owners: %s", owners) logger.info("Prefix: %s", self.prefix) logger.info("Guild Name: %s", self.guild.name) logger.info("Guild ID: %s", self.guild.id) @@ -1099,7 +1100,8 @@ async def validate_database_connection(self): ) raise else: - logger.info("Successfully connected to the database.") + logger.debug("Successfully connected to the database.") + logger.line('debug') async def post_metadata(self): owner = (await self.application_info()).owner @@ -1125,7 +1127,7 @@ async def post_metadata(self): async def before_post_metadata(self): await self.wait_for_connected() logger.debug("Starting metadata loop.") - logger.line() + logger.line('debug') if not self.guild: self.metadata_loop.cancel() diff --git a/cogs/plugins.py b/cogs/plugins.py index c52be27192..52fc20bdd6 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -147,7 +147,7 @@ async def initial_load_plugins(self): logger.error("Error when loading plugin %s.", plugin, exc_info=True) continue - logger.info("Finished loading all plugins.") + logger.debug("Finished loading all plugins.") self._ready_event.set() await self.bot.config.update() @@ -226,7 +226,7 @@ async def load_plugin(self, plugin): try: self.bot.load_extension(plugin.ext_string) - logger.info("Loaded plugin: %s", plugin.ext_string) + logger.info("Loaded plugin: %s", plugin.ext_string.split('.')[-1]) self.loaded_plugins.add(plugin) except commands.ExtensionError as exc: diff --git a/cogs/utility.py b/cogs/utility.py index d8dd9f97a5..a709ac469b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -603,7 +603,7 @@ async def loop_presence(self): @loop_presence.before_loop async def before_loop_presence(self): await self.bot.wait_for_connected() - logger.line() + logger.line('debug') activity, status = await self.set_presence() if activity is not None: diff --git a/core/config.py b/core/config.py index 775781eeb9..781a5c23df 100644 --- a/core/config.py +++ b/core/config.py @@ -182,7 +182,7 @@ async def refresh(self) -> dict: self._cache[k] = v if not self.ready_event.is_set(): self.ready_event.set() - logger.info("Successfully fetched configurations from database.") + logger.debug("Successfully fetched configurations from database.") return self._cache async def wait_until_ready(self) -> None: diff --git a/core/models.py b/core/models.py index 033457a542..47e6375cc5 100644 --- a/core/models.py +++ b/core/models.py @@ -44,7 +44,7 @@ def _debug_(*msgs): @staticmethod def _info_(*msgs): - return f'{Fore.GREEN}{" ".join(msgs)}{Style.RESET_ALL}' + return f'{Fore.LIGHTMAGENTA_EX}{" ".join(msgs)}{Style.RESET_ALL}' @staticmethod def _error_(*msgs): @@ -70,13 +70,16 @@ def critical(self, msg, *args, **kwargs): if self.isEnabledFor(logging.CRITICAL): self._log(logging.CRITICAL, self._error_(msg), args, **kwargs) - def exception(self, msg, *args, exc_info=True, **kwargs): - self.error(msg, *args, exc_info=exc_info, **kwargs) - - def line(self): - if self.isEnabledFor(logging.INFO): + def line(self, level="info"): + if level == "info": + level = logging.INFO + elif level == "debug": + level = logging.DEBUG + else: + level = logging.INFO + if self.isEnabledFor(level): self._log( - logging.INFO, + level, Fore.BLACK + Style.BRIGHT + "-------------------------" From 3f9b824fc78efe3d0aab79f42204831ff3f5865c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 10 Oct 2019 17:36:58 -0700 Subject: [PATCH 0440/1221] Updated registry --- bot.py | 10 +++++----- plugins/registry.json | 9 --------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/bot.py b/bot.py index 30e631fb65..d94c239360 100644 --- a/bot.py +++ b/bot.py @@ -73,7 +73,7 @@ def __init__(self): self.metadata_loop = None self.formatter = SafeFormatter() self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] - + self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") self._connected = asyncio.Event() self.start_time = datetime.utcnow() @@ -144,9 +144,8 @@ def _configure_logging(self): "DEBUG": logging.DEBUG, } - log_file_name = self.token.split(".")[0] ch_debug = logging.FileHandler( - os.path.join(temp_dir, f"{log_file_name}.log"), mode="a+" + self.log_file_name, mode="a+" ) ch_debug.setLevel(logging.DEBUG) @@ -169,6 +168,7 @@ def _configure_logging(self): else: logger.info("Invalid logging level set.") logger.warning("Using default logging level: %s.", level_text) + logger.info("Log file: %s", self.log_file_name) logger.debug("Successfully configured logging.") @property @@ -545,8 +545,8 @@ async def convert_emoji(self, name: str) -> str: if name not in UNICODE_EMOJI: try: name = await converter.convert(ctx, name.strip(":")) - except commands.BadArgument: - logger.warning("%s is not a valid emoji.", name) + except commands.BadArgument as e: + logger.warning("%s is not a valid emoji. %s.", str(e)) raise return name diff --git a/plugins/registry.json b/plugins/registry.json index a7d699fa5a..c03c774b88 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -151,15 +151,6 @@ "title": "Colors!!", "icon_url": "https://cdn1.iconfinder.com/data/icons/weather-19/32/rainbow-512.png", "thumbnail_url": "https://i.imgur.com/fSxnc9W.jpg" - }, - "logger": { - "repository": "Taaku18/modmail-plugins", - "branch": "master", - "description": "Moderation logger, logs message edits, message delete, channel changes, member join, etc.", - "bot_version": "2.20.1", - "title": "Server Activity Logger", - "icon_url": "https://i.imgur.com/dRqPq5M.png", - "thumbnail_url": "https://i.imgur.com/moQn7RC.png" }, "fun": { "repository": "TheKinG2149/modmail-plugins", From b3875bb948fd3c5a8476cef81433a7db3bf7e3dd Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 10 Oct 2019 17:52:01 -0700 Subject: [PATCH 0441/1221] Whoops --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index d94c239360..a5287d69cd 100644 --- a/bot.py +++ b/bot.py @@ -73,7 +73,6 @@ def __init__(self): self.metadata_loop = None self.formatter = SafeFormatter() self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] - self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") self._connected = asyncio.Event() self.start_time = datetime.utcnow() @@ -82,6 +81,7 @@ def __init__(self): self.threads = ThreadManager(self) + self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") self._configure_logging() mongo_uri = self.config["mongo_uri"] From 5aac074d93a09e6abfee45875a6d66912a155322 Mon Sep 17 00:00:00 2001 From: Matrix <49132750+matrix2113@users.noreply.github.com> Date: Mon, 14 Oct 2019 19:32:39 -0400 Subject: [PATCH 0442/1221] Fixed Bot Instances --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d5f33fdca..d12f623c5a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - Bot instances + Bot instances From 3b42de3f3630d6a5f91e5a4eebfa5b1bfb0c7eae Mon Sep 17 00:00:00 2001 From: Matrix <49132750+matrix2113@users.noreply.github.com> Date: Mon, 14 Oct 2019 19:37:25 -0400 Subject: [PATCH 0443/1221] Update bot.py --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index a25777160d..083fb4365e 100644 --- a/bot.py +++ b/bot.py @@ -1151,7 +1151,7 @@ async def post_metadata(self): "last_updated": str(datetime.utcnow()), } - async with self.session.post("https://api.modmail.tk/metadata", json=data): + async with self.session.post("https://api.logviewer.tech/metadata", json=data): logger.debug("Uploading metadata to Modmail server.") async def before_post_metadata(self): From 937d6fdf26928d937c0e2b78ec13ed02179fe580 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 14 Oct 2019 21:31:21 -0700 Subject: [PATCH 0444/1221] Fix block --- CHANGELOG.md | 6 ++++ bot.py | 16 ++++++++-- cogs/modmail.py | 77 ++++++++++++++++++------------------------------- 3 files changed, 47 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index add6bb5d05..78e4b9625e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ however, insignificant breaking changes does not guarantee a major version bump, # v3.3.0-dev2 + +### Important + +- Recommend all users to unblock and re-block all blocked users upon updating to this release. + ### Added - Three new config vars: @@ -39,6 +44,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. - Eval commands are logged in debug logs. - Presence updates 30 minutes instead of 45 now. +- Fixed an assortment of problems to do with block. ### Internal diff --git a/bot.py b/bot.py index a5287d69cd..f01be93590 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev2" +__version__ = "3.3.0-dev3" import asyncio import logging @@ -676,9 +676,16 @@ async def _process_blocked(self, message: discord.Message) -> bool: self.blocked_users.pop(str(message.author.id)) else: reaction = blocked_emoji - end_time = re.search(r"%(.+?)%", reason) + # etc "blah blah blah... until 2019-10-14T21:12:45.559948." + end_time = re.search(r"until ([^`]+?)\.$", reason) + if end_time is None: + # backwards compat + end_time = re.search(r"%([^%]+?)%", reason) + if end_time is not None: + logger.warning(r"Deprecated time message for user %s, block and unblock again to update.", + message.author) + if end_time is not None: - logger.debug("No longer blocked, user %s.", message.author.name) after = ( datetime.fromisoformat(end_time.group(1)) - now ).total_seconds() @@ -686,6 +693,9 @@ async def _process_blocked(self, message: discord.Message) -> bool: # No longer blocked reaction = sent_emoji self.blocked_users.pop(str(message.author.id)) + logger.debug("No longer blocked, user %s.", message.author.name) + else: + logger.debug("User blocked, user %s.", message.author.name) else: logger.debug("User blocked, user %s.", message.author.name) else: diff --git a/cogs/modmail.py b/cogs/modmail.py index 67e840bded..d6911092af 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -953,15 +953,15 @@ async def blocked(self, ctx): else: try: user = await self.bot.fetch_user(id_) - users.append((str(user), reason)) + users.append((user.mention, reason)) except discord.NotFound: - pass + users.append((id_, reason)) if users: embed = embeds[0] for mention, reason in users: - line = mention + f" - `{reason or 'No reason provided'}`\n" + line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( title="Blocked Users (Continued)", @@ -1008,9 +1008,7 @@ async def blocked_whitelist(self, ctx, *, user: User = None): self.bot.blocked_whitelisted_users.append(str(user.id)) if str(user.id) in self.bot.blocked_users: - msg = self.bot.blocked_users.get(str(user.id)) - if msg is None: - msg = "" + msg = self.bot.blocked_users.get(str(user.id)) or "" self.bot.blocked_users.pop(str(user.id)) await self.bot.config.update() @@ -1018,10 +1016,10 @@ async def blocked_whitelist(self, ctx, *, user: User = None): if msg.startswith("System Message: "): # If the user is blocked internally (for example: below minimum account age) # Show an extended message stating the original internal message - reason = msg[16:].strip().rstrip(".") or "no reason" + reason = msg[16:].strip().rstrip(".") embed = discord.Embed( title="Success", - description=f"{mention} was previously blocked internally due to " + description=f"{mention} was previously blocked internally for " f'"{reason}". {mention} is now whitelisted.', color=self.bot.main_color, ) @@ -1051,8 +1049,6 @@ async def block( `duration` may be a simple "human-readable" time text. See `{prefix}help close` for examples. """ - reason = "" - if user is None: thread = ctx.thread if thread: @@ -1064,8 +1060,6 @@ async def block( mention = getattr(user, "mention", f"`{user.id}`") - moderator = ctx.author.name - if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( title="Error", @@ -1074,53 +1068,38 @@ async def block( ) return await ctx.send(embed=embed) + reason = f"by {escape_markdown(ctx.author.name)}#{ctx.author.discriminator}" + if after is not None: - reason = f"{after.arg} by {moderator}" - if reason.startswith("System Message: "): - raise commands.BadArgument( - "The reason cannot start with `System Message:`." - ) if "%" in reason: raise commands.BadArgument('The reason contains illegal character "%".') + if after.arg: + reason += f" for `{after.arg}`" if after.dt > after.now: - reason = f"{reason} %{after.dt.isoformat()}% by {moderator}" + reason += f" until {after.dt.isoformat()}" - if not reason: - reason = f"Blocked by {moderator}" + reason += "." - extend = f" for `{reason}`" if reason is not None else "" msg = self.bot.blocked_users.get(str(user.id)) if msg is None: msg = "" - if ( - str(user.id) not in self.bot.blocked_users - or reason is not None - or msg.startswith("System Message: ") - ): - if str(user.id) in self.bot.blocked_users: - - old_reason = msg.strip().rstrip(".") or f"Blocked by {moderator}" - embed = discord.Embed( - title="Success", - description=f"{mention} was previously blocked for " - f'"{old_reason}". {mention} is now blocked{extend}.', - color=self.bot.main_color, - ) - else: - embed = discord.Embed( - title="Success", - color=self.bot.main_color, - description=f"{mention} is now blocked{extend}.", - ) - self.bot.blocked_users[str(user.id)] = reason - await self.bot.config.update() + if str(user.id) in self.bot.blocked_users and msg: + old_reason = msg.strip().rstrip(".") + embed = discord.Embed( + title="Success", + description=f"{mention} was previously blocked " + f'{old_reason}.\n{mention} is now blocked {reason}', + color=self.bot.main_color, + ) else: embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"{mention} is already blocked.", + title="Success", + color=self.bot.main_color, + description=f"{mention} is now blocked {reason}", ) + self.bot.blocked_users[str(user.id)] = reason + await self.bot.config.update() return await ctx.send(embed=embed) @@ -1156,12 +1135,12 @@ async def unblock(self, ctx, *, user: User = None): reason = msg[16:].strip().rstrip(".") or "no reason" embed = discord.Embed( title="Success", - description=f"{mention} was previously blocked internally due to " - f'"{reason}". {mention} is no longer blocked.', + description=f"{mention} was previously blocked internally " + f'{reason}.\n{mention} is no longer blocked.', color=self.bot.main_color, ) embed.set_footer( - text="However, if the original system block reason still apply, " + text="However, if the original system block reason still applies, " f"{name} will be automatically blocked again. Use " f'"{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' ) From 316f57ff0388e8c52707820a27e70f2ec60b4721 Mon Sep 17 00:00:00 2001 From: XtremeCoder Date: Wed, 16 Oct 2019 22:18:40 +0800 Subject: [PATCH 0445/1221] fix the inconsistency btn `patreon` and `patron` (#396) * fix the inconsistency btn `patreon` and `patron` * Capitalisation Co-Authored-By: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d12f623c5a..c08268b9de 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,9 @@ To configure automatic updates: - Then go to the Deploy tab in your [Heroku account](https://dashboard.heroku.com/apps) of your bot app, select GitHub and connect your fork (usually by typing "Modmail"). - Turn on auto-deploy for the `master` branch. -### Hosting for patrons +### Hosting for Patreons -If you don't want to go through the trouble of setting up your very own Modmail bot, and/or want to support this project, we offer the all inclusive installation, hosting and maintenance of your Modmail with [**Patron**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! +If you don't want to go through the trouble of setting up your very own Modmail bot, and/or want to support this project, we offer the all inclusive installation, hosting and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! ### Locally From 764c538df458a74d56fa04f5e7a8b5ddd0458bcb Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 18 Oct 2019 23:32:37 -0700 Subject: [PATCH 0446/1221] Add "Latest Version" badge --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index c08268b9de..920b6432f7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,12 @@

+
+ + + +
+ From 58e25b0ebfc4e11ad75bd1432cfb08c1b784f391 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 22 Oct 2019 23:08:08 -0700 Subject: [PATCH 0447/1221] Fix travis ci --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 65654b83fc..21035e6f3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,13 +8,13 @@ matrix: name: "Python 3.6.7 on Xenial Linux" - name: "Python 3.7.4 on macOS" os: osx - osx_image: xcode11 + osx_image: xcode11.2 language: shell - - name: "Python 3.7.4 on Windows" + - name: "Python 3.7.5 on Windows" os: windows language: shell before_install: - - choco install python + - choco install python --version=3.7.5 - python -m pip install --upgrade pip env: PATH=/c/Python37:/c/Python37/Scripts:$PATH From af87c8b3cdc98ffa5cf3212dbdccd264a388e045 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 22 Oct 2019 23:24:36 -0700 Subject: [PATCH 0448/1221] Updated changelog version regex for prerelease tag --- core/changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/changelog.py b/core/changelog.py index 3e48a41f97..1e9254286f 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -130,7 +130,7 @@ class Changelog: """ VERSION_REGEX = re.compile( - r"#\s*([vV]\d+\.\d+(?:\.\d+)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)?|$)", + r"#\s*([vV]\d+\.\d+(?:\.\d+)?(?:-\w+?)?)\s+(.*?)(?=#\s*[vV]\d+\.\d+(?:\.\d+)(?:-\w+?)?|$)", flags=re.DOTALL, ) From 36407ffecbda034542521703791a660dace05caf Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 22 Oct 2019 23:35:59 -0700 Subject: [PATCH 0449/1221] Add prerelease notice in about footer --- cogs/utility.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index a709ac469b..57c536f4ea 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -304,7 +304,10 @@ async def about(self, ctx): changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version - if self.bot.version < parse_version(latest.version): + if self.bot.version.is_prerelease: + stable = next(filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions)) + footer = f"You are on the prerelease version • the latest version is v{stable.version}." + elif self.bot.version < parse_version(latest.version): footer = f"A newer version is available v{latest.version}." else: footer = "You are up to date with the latest version." From 579f576b01b68d09eec5cb0cf3fa0cd01bc7cdd5 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 24 Oct 2019 20:41:19 -0700 Subject: [PATCH 0450/1221] Bump discord.py version to 1.2.4 --- Pipfile | 2 +- Pipfile.lock | 52 +++++++++++++++++++++++++++++----------------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Pipfile b/Pipfile index 7b54ff37b2..b794958a5c 100644 --- a/Pipfile +++ b/Pipfile @@ -22,7 +22,7 @@ parsedatetime = "==2.4" aiohttp = "<3.6.0,>=3.3.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = "==1.2.3" +"discord.py" = "==1.2.4" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 431469cdcf..5b21468ed8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f5ceea517895626871c9bb2c559e39f6b81859f55669897aa9533154a4df1431" + "sha256": "e218eea19b5d78a490cf012fb818e002a128bbed21b08f2327963e66c2d47c71" }, "pipfile-spec": 6, "requires": { @@ -53,10 +53,10 @@ }, "attrs": { "hashes": [ - "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", - "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.2.0" + "version": "==19.3.0" }, "certifi": { "hashes": [ @@ -82,10 +82,11 @@ }, "discord.py": { "hashes": [ - "sha256:4684733fa137cc7def18087ae935af615212e423e3dbbe3e84ef01d7ae8ed17d" + "sha256:2d5fec14b1c047b561336e969939e7f34564489851ec1a7edb8d303087127256", + "sha256:3e044d84f0bb275d173e2d958cb4a579e525707f90e3e8a15c59901f79e80663" ], "index": "pypi", - "version": "==1.2.3" + "version": "==1.2.4" }, "dnspython": { "hashes": [ @@ -104,9 +105,9 @@ }, "future": { "hashes": [ - "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" + "sha256:858e38522e8fd0d3ce8f0c1feaf0603358e366d5403209674c7b617fa0c24093" ], - "version": "==0.17.1" + "version": "==0.18.1" }, "idna": { "hashes": [ @@ -261,10 +262,10 @@ }, "virtualenv": { "hashes": [ - "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", - "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" + "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589", + "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136" ], - "version": "==16.7.5" + "version": "==16.7.7" }, "virtualenv-clone": { "hashes": [ @@ -326,17 +327,17 @@ }, "astroid": { "hashes": [ - "sha256:98c665ad84d10b18318c5ab7c3d203fe11714cbad2a4aef4f44651f415392754", - "sha256:b7546ffdedbf7abcfbff93cd1de9e9980b1ef744852689decc5aeada324238c6" + "sha256:09a3fba616519311f1af8a461f804b68f0370e100c9264a035aa7846d7852e33", + "sha256:5a79c9b4bd6c4be777424593f957c996e20beb5f74e0bc332f47713c6f675efe" ], - "version": "==2.3.1" + "version": "==2.3.2" }, "attrs": { "hashes": [ - "sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2", - "sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.2.0" + "version": "==19.3.0" }, "bandit": { "hashes": [ @@ -385,10 +386,10 @@ }, "gitpython": { "hashes": [ - "sha256:631263cc670aa56ce3d3c414cf0fe2e840f2e913514b138ea28d88a477bbcd21", - "sha256:6e97b9f0954807f30c2dd8e3165731ed6c477a1b365f194b69d81d7940a08332" + "sha256:3237caca1139d0a7aa072f6735f5fd2520de52195e0fa1d8b83a9b212a2498b2", + "sha256:a7d6bef0775f66ba47f25911d285bcd692ce9053837ff48a120c2b8cf3a71389" ], - "version": "==3.0.3" + "version": "==3.0.4" }, "isort": { "hashes": [ @@ -450,11 +451,11 @@ }, "pylint": { "hashes": [ - "sha256:7edbae11476c2182708063ac387a8f97c760d9cfe36a5ede0ca996f90cf346c8", - "sha256:844ce067788028c1a35086a5c66bc5e599ddd851841c41d6ee1623b36774d9f2" + "sha256:7b76045426c650d2b0f02fc47c14d7934d17898779da95288a74c2a7ec440702", + "sha256:856476331f3e26598017290fd65bebe81c960e806776f324093a46b76fb2d1c0" ], "index": "pypi", - "version": "==2.4.2" + "version": "==2.4.3" }, "pyyaml": { "hashes": [ @@ -504,20 +505,25 @@ }, "typed-ast": { "hashes": [ + "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", + "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", + "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", + "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", + "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" ], "markers": "implementation_name == 'cpython' and python_version < '3.8'", From 53d9fc60b17aaffe678463ab7178699da572a8a6 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 24 Oct 2019 20:54:54 -0700 Subject: [PATCH 0451/1221] Updated requirements.min.txt and runtime.txt --- README.md | 2 +- requirements.min.txt | 16 +++++++++++++--- runtime.txt | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2fa070457b..763f814c1b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
diff --git a/requirements.min.txt b/requirements.min.txt index 2e9086e887..c8c5c2ab07 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -1,14 +1,24 @@ -# Generated as of July 11, 2019 +# Generated as of October, 2019 # This is the bare minimum requirements.txt for running Modmail. # To install requirements.txt run: pip install -r requirements.min.txt aiohttp==3.5.4 -discord.py==1.2.3 +async-timeout==3.0.1 +attrs==19.3.0 +chardet==3.0.4 +discord.py==1.2.4 dnspython==1.16.0 -emoji==0.5.2 +emoji==0.5.4 +future==0.18.1 +idna==2.8 isodate==0.6.0 motor==2.0.0 +multidict==4.5.2 natural==0.2.0 parsedatetime==2.4 +pymongo==3.9.0 python-dateutil==2.8.0 python-dotenv==0.10.3 +six==1.12.0 +websockets==6.0 +yarl==1.3.0 diff --git a/runtime.txt b/runtime.txt index 42731f2fbe..aefcfbece7 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.4 +python-3.7.5 From 07d1bfd453df871cde303832326d4ac7c9b25ea0 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 25 Oct 2019 22:00:58 -0700 Subject: [PATCH 0452/1221] Fixed a delete message oversight --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 95991c8c51..fd4312fb80 100644 --- a/core/thread.py +++ b/core/thread.py @@ -735,7 +735,7 @@ async def send( else: embed.set_footer(text=self.bot.config["anon_tag"]) delete_message = not bool(message.attachments) - if delete_message and isinstance(message.channel, discord.TextChannel): + if delete_message and isinstance(destination, discord.TextChannel): try: await message.delete() except Exception as e: From 7c823160f2d9c146cc0c3f4e51646b6e3bfa15d2 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 30 Oct 2019 00:53:36 -0700 Subject: [PATCH 0453/1221] Fixed a typo --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index d6911092af..699999c205 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -309,7 +309,7 @@ async def move( await thread.channel.edit(category=category, sync_permissions=True) - if self.bot.config("thread_move_notify") and not silent: + if self.bot.config["thread_move_notify"] and not silent: embed = discord.Embed( title="Thread Moved", description=self.bot.config["thread_move_response"], From 45871710049d79b5d9dc97641224e40dd50dda1d Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 30 Oct 2019 00:57:10 -0700 Subject: [PATCH 0454/1221] Use channel check instead for delete --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index fd4312fb80..ed92bc5dfe 100644 --- a/core/thread.py +++ b/core/thread.py @@ -735,7 +735,7 @@ async def send( else: embed.set_footer(text=self.bot.config["anon_tag"]) delete_message = not bool(message.attachments) - if delete_message and isinstance(destination, discord.TextChannel): + if delete_message and destination == self.channel: try: await message.delete() except Exception as e: From 5f389596e61121b9e2e2fbccf651efa579c5488c Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 31 Oct 2019 00:09:51 -0700 Subject: [PATCH 0455/1221] Delete note command --- CHANGELOG.md | 3 ++- core/thread.py | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e4b9625e..59b38ee9b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev2 +# v3.3.0-dev3 ### Important @@ -31,6 +31,7 @@ however, insignificant breaking changes does not guarantee a major version bump, ### Changed - `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) +- Automatically delete notes command `?note` when there's no attachments attached. - Plugins update (mostly internal). - `git` is no longer used to install plugins, it now downloads through zip files. - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. diff --git a/core/thread.py b/core/thread.py index ed92bc5dfe..41f77a82c4 100644 --- a/core/thread.py +++ b/core/thread.py @@ -734,17 +734,19 @@ async def send( embed.set_footer(text=mod_tag) # Normal messages else: embed.set_footer(text=self.bot.config["anon_tag"]) + elif note: + embed.colour = self.bot.main_color + else: + embed.set_footer(text=f"Message ID: {message.id}") + embed.colour = self.bot.recipient_color + + if from_mod or note: delete_message = not bool(message.attachments) if delete_message and destination == self.channel: try: await message.delete() except Exception as e: logger.warning('Cannot delete message: %s.', str(e)) - elif note: - embed.colour = self.bot.main_color - else: - embed.set_footer(text=f"Message ID: {message.id}") - embed.colour = self.bot.recipient_color try: await destination.trigger_typing() From 2e98510ea668eec4a0547a520038ec6a52e0078b Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 31 Oct 2019 00:48:53 -0700 Subject: [PATCH 0456/1221] Fixed author links --- CHANGELOG.md | 6 +++++- bot.py | 2 +- core/thread.py | 7 ++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b38ee9b5..540b98e3a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev3 +# v3.3.0-dev4 ### Important @@ -32,6 +32,10 @@ however, insignificant breaking changes does not guarantee a major version bump, - `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) - Automatically delete notes command `?note` when there's no attachments attached. +- Embed author links used to be inaccessible in many cases, now: + - `?anonreply`, `?reply`, and `?note` in thread channel will link to the sender's profile. + - `?reply` and recipient's DM will also link the sender's profile. + - `?anonreply` in DM channel will link to the first channel of the main guild. - Plugins update (mostly internal). - `git` is no longer used to install plugins, it now downloads through zip files. - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. diff --git a/bot.py b/bot.py index acb4f89550..451dbc93ca 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev3" +__version__ = "3.3.0-dev4" import asyncio import logging diff --git a/core/thread.py b/core/thread.py index 41f77a82c4..056f3c3073 100644 --- a/core/thread.py +++ b/core/thread.py @@ -647,18 +647,19 @@ async def send( avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: avatar_url = self.bot.guild.icon_url + embed.set_author(name=name, icon_url=avatar_url, + url=f"https://discordapp.com/channels/{self.bot.guild.id}") else: # Normal message name = str(author) avatar_url = author.avatar_url - - embed.set_author(name=name, icon_url=avatar_url, url=message.jump_url) + embed.set_author(name=name, icon_url=avatar_url, url=f"https://discordapp.com/users/{author.id}") else: # Special note messages embed.set_author( name=f"Note ({author.name})", icon_url=system_avatar_url, - url=message.jump_url, + url=f"https://discordapp.com/users/{author.id}" ) ext = [(a.url, a.filename) for a in message.attachments] From adacb8b30add09c2ad1bd0d041bb1a94b325afe2 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Thu, 31 Oct 2019 19:21:59 +0100 Subject: [PATCH 0457/1221] Update SPONSOR.json (#404) updated invite link, after our server got hacked and nuked --- SPONSORS.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index 07f82819d6..d30134e015 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -30,8 +30,8 @@ { "embed": { "title": "Hey there!", - "description": "Nice to see you here! You can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <- and also join our [Discord](https://discord.gg/djwMSQ)!", - "url": "https://discord.gg/djwMSQ", + "description": "Nice to see you here! You can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <- and also join our [Discord](https://discord.gg/zaeVCaV)!", + "url": "https://discord.gg/zaeVCaV", "color": 13003681, "footer": { "icon_url": "https://imgur.com/Mrc9pLd.gif", @@ -45,7 +45,7 @@ }, "author": { "name": "Roomieofficial", - "url": "https://discord.gg/djwMSQ", + "url": "https://discord.gg/zaeVCaV", "icon_url": "https://imgur.com/6hBkt7Z.png" }, "fields": [ @@ -59,7 +59,7 @@ }, { "name": "Discord 😁", - "value": "Make sure to join our [Discord](https://discord.gg/djwMSQ) We have weekly events, 24/7 chats and more!" + "value": "Make sure to join our [Discord](https://discord.gg/zaeVCaV) We have weekly events, 24/7 chats and more!" } ] } From 351ee43c7d0267806886d5b5a40342782b3b3717 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 1 Nov 2019 01:48:51 -0700 Subject: [PATCH 0458/1221] Add ability to disable DM --- CHANGELOG.md | 12 ++++ bot.py | 127 +++++++++++++++++++++++++++++------------- cogs/modmail.py | 99 ++++++++++++++++++++++++++++++-- core/config.py | 9 +++ core/config_help.json | 66 ++++++++++++++++++++++ core/thread.py | 28 ++++++++-- 6 files changed, 293 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 540b98e3a1..438227c419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,18 @@ however, insignificant breaking changes does not guarantee a major version bump, - `?logs responded [user]` command, it will show all logs that the user has sent an reply. (Thanks to papiersnipper PR#288) - `user` when not provided, defaults to the user who ran the command. - Open threads in limbo now auto closes if the channel cannot be found. This check is done every time the bot restarts. +- Ability to disable new threads from getting created. + - `?disable` +- Ability to fully disable Modmail DM. + - `?disable all` +- To re-enable DM: `?enable`, and to see the current status: `?isenable`. +- This disabled Modmail interface is customizable with the following config vars: + - `disabled_new_thread_title` + - `disabled_new_thread_response` + - `disabled_new_thread_footer` + - `disabled_current_thread_title` + - `disabled_current_thread_response` + - `disabled_current_thread_footer` ### Changed diff --git a/bot.py b/bot.py index 451dbc93ca..34df9f1bb5 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev4" +__version__ = "3.3.0-dev5" import asyncio import logging @@ -46,8 +46,10 @@ ch = logging.StreamHandler(stream=sys.stdout) ch.setLevel(logging.INFO) -formatter = logging.Formatter("%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", - datefmt="%b %d %H:%M:%S") +formatter = logging.Formatter( + "%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", + datefmt="%b %d %H:%M:%S", +) ch.setFormatter(formatter) logger.addHandler(ch) @@ -123,7 +125,7 @@ def startup(self): logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") - logger.line('debug') + logger.line("debug") for cog in self.loaded_cogs: logger.debug("Loading %s.", cog) @@ -132,7 +134,7 @@ def startup(self): logger.debug("Successfully loaded %s.", cog) except Exception: logger.exception("Failed to load %s.", cog) - logger.line('debug') + logger.line("debug") def _configure_logging(self): level_text = self.config["log_level"].upper() @@ -144,9 +146,7 @@ def _configure_logging(self): "DEBUG": logging.DEBUG, } - ch_debug = logging.FileHandler( - self.log_file_name, mode="a+" - ) + ch_debug = logging.FileHandler(self.log_file_name, mode="a+") ch_debug.setLevel(logging.DEBUG) formatter_debug = FileFormatter( @@ -460,7 +460,10 @@ async def on_ready(self): logger.debug("Client ready.") logger.info("Logged in as: %s", self.user) logger.info("Bot ID: %s", self.user.id) - owners = ", ".join(getattr(self.get_user(owner_id), "name", str(owner_id)) for owner_id in self.owner_ids) + owners = ", ".join( + getattr(self.get_user(owner_id), "name", str(owner_id)) + for owner_id in self.owner_ids + ) logger.info("Owners: %s", owners) logger.info("Prefix: %s", self.prefix) logger.info("Guild Name: %s", self.guild.name) @@ -504,10 +507,12 @@ async def on_ready(self): ) for log in await self.api.get_open_logs(): - if self.get_channel(int(log['channel_id'])) is None: - logger.debug("Unable to resolve thread with channel %s.", log['channel_id']) + if self.get_channel(int(log["channel_id"])) is None: + logger.debug( + "Unable to resolve thread with channel %s.", log["channel_id"] + ) log_data = await self.api.post_log( - log['channel_id'], + log["channel_id"], { "open": False, "closed_at": str(datetime.utcnow()), @@ -522,9 +527,14 @@ async def on_ready(self): }, ) if log_data: - logger.debug("Successfully closed thread with channel %s.", log['channel_id']) + logger.debug( + "Successfully closed thread with channel %s.", log["channel_id"] + ) else: - logger.debug("Failed to close thread with channel %s, skipping.", log['channel_id']) + logger.debug( + "Failed to close thread with channel %s, skipping.", + log["channel_id"], + ) self.metadata_loop = tasks.Loop( self.post_metadata, @@ -573,7 +583,9 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: return sent_emoji, blocked_emoji - async def _process_blocked(self, message: discord.Message) -> bool: + async def _process_blocked( + self, message: discord.Message + ) -> typing.Tuple[bool, str]: sent_emoji, blocked_emoji = await self.retrieve_emoji() if str(message.author.id) in self.blocked_whitelisted_users: @@ -581,13 +593,7 @@ async def _process_blocked(self, message: discord.Message) -> bool: self.blocked_users.pop(str(message.author.id)) await self.config.update() - if sent_emoji != "disable": - try: - await message.add_reaction(sent_emoji) - except (discord.HTTPException, discord.InvalidArgument): - logger.warning("Failed to add sent_emoji.", exc_info=True) - - return False + return False, sent_emoji now = datetime.utcnow() @@ -682,8 +688,10 @@ async def _process_blocked(self, message: discord.Message) -> bool: # backwards compat end_time = re.search(r"%([^%]+?)%", reason) if end_time is not None: - logger.warning(r"Deprecated time message for user %s, block and unblock again to update.", - message.author) + logger.warning( + r"Deprecated time message for user %s, block and unblock again to update.", + message.author, + ) if end_time is not None: after = ( @@ -702,19 +710,62 @@ async def _process_blocked(self, message: discord.Message) -> bool: reaction = sent_emoji await self.config.update() + return str(message.author.id) in self.blocked_users, reaction + + @staticmethod + async def add_reaction(msg, reaction): if reaction != "disable": try: - await message.add_reaction(reaction) + await msg.add_reaction(reaction) except (discord.HTTPException, discord.InvalidArgument): logger.warning("Failed to add reaction %s.", reaction, exc_info=True) - return str(message.author.id) in self.blocked_users async def process_dm_modmail(self, message: discord.Message) -> None: """Processes messages sent to the bot.""" - blocked = await self._process_blocked(message) - if not blocked: - thread = await self.threads.find_or_create(message.author) - await thread.send(message) + blocked, reaction = await self._process_blocked(message) + if blocked: + return await self.add_reaction(message, reaction) + thread = await self.threads.find(recipient=message.author) + if thread is None: + if self.config["dm_disabled"] >= 1: + embed = discord.Embed( + title=self.config["disabled_new_thread_title"], + color=self.error_color, + description=self.config["disabled_new_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_new_thread_footer"], + icon_url=self.guild.icon_url, + ) + logger.info( + "A new thread was blocked from %s due to disabled Modmail.", + message.author, + ) + _, blocked_emoji = await self.retrieve_emoji() + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + thread = self.threads.create(message.author) + else: + if self.config["dm_disabled"] == 2: + embed = discord.Embed( + title=self.config["disabled_current_thread_title"], + color=self.error_color, + description=self.config["disabled_current_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_current_thread_footer"], + icon_url=self.guild.icon_url, + ) + logger.info( + "A message was blocked from %s due to disabled Modmail.", + message.author, + ) + _, blocked_emoji = await self.retrieve_emoji() + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + + await self.add_reaction(message, reaction) + await thread.send(message) async def get_contexts(self, message, *, cls=commands.Context): """ @@ -898,13 +949,13 @@ async def _void(*_args, **_kwargs): thread = await self.threads.find(channel=channel) if thread is not None and thread.recipient: - if await self._process_blocked( - SimpleNamespace( - author=thread.recipient, - channel=SimpleNamespace(send=_void), - add_reaction=_void, + if ( + await self._process_blocked( + SimpleNamespace( + author=thread.recipient, channel=SimpleNamespace(send=_void) + ) ) - ): + )[0]: return await thread.recipient.trigger_typing() @@ -1111,7 +1162,7 @@ async def validate_database_connection(self): raise else: logger.debug("Successfully connected to the database.") - logger.line('debug') + logger.line("debug") async def post_metadata(self): owner = (await self.application_info()).owner @@ -1137,7 +1188,7 @@ async def post_metadata(self): async def before_post_metadata(self): await self.wait_for_connected() logger.debug("Starting metadata loop.") - logger.line('debug') + logger.line("debug") if not self.guild: self.metadata_loop.cancel() diff --git a/cogs/modmail.py b/cogs/modmail.py index 699999c205..954e617b5c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -626,9 +626,9 @@ def format_log_embeds(self, logs, avatar_url): embed.add_field( name="Created", value=duration(created_at, now=datetime.utcnow()) ) - closer = entry.get('closer') + closer = entry.get("closer") if closer is None: - closer_msg = 'Unknown' + closer_msg = "Unknown" else: closer_msg = f"<@{closer['id']}>" embed.add_field(name="Closed By", value=closer_msg) @@ -916,6 +916,9 @@ async def contact( thread = self.bot.threads.create( user, creator=ctx.author, category=category ) + if self.bot.config["dm_disabled"] >= 1: + logger.info("Contacting user %s when Modmail DM is disabled.", user) + embed = discord.Embed( title="Created Thread", description=f"Thread started by {ctx.author.mention} " @@ -1089,7 +1092,7 @@ async def block( embed = discord.Embed( title="Success", description=f"{mention} was previously blocked " - f'{old_reason}.\n{mention} is now blocked {reason}', + f"{old_reason}.\n{mention} is now blocked {reason}", color=self.bot.main_color, ) else: @@ -1136,7 +1139,7 @@ async def unblock(self, ctx, *, user: User = None): embed = discord.Embed( title="Success", description=f"{mention} was previously blocked internally " - f'{reason}.\n{mention} is no longer blocked.', + f"{reason}.\n{mention} is no longer blocked.", color=self.bot.main_color, ) embed.set_footer( @@ -1197,6 +1200,94 @@ async def delete(self, ctx, message_id: Optional[int] = None): except (discord.HTTPException, discord.InvalidArgument): pass + @commands.command() + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + async def enable(self, ctx): + """ + Re-enables DM functionalities of Modmail. + + Undo's the `{prefix}disable` command, all DM will be relayed after running this command. + """ + embed = discord.Embed( + title="Success", + description=f"Modmail will now accept all DM messages.", + color=self.bot.main_color, + ) + + if self.bot.config["dm_disabled"] != 0: + self.bot.config["dm_disabled"] = 0 + await self.bot.config.update() + + return await ctx.send(embed=embed) + + @commands.group(invoke_without_command=True) + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + async def disable(self, ctx): + """ + Stop accepting new Modmail threads. + + No new threads can be created through DM. + To stop all existing threads from DMing Modmail, do `{prefix}disable all`. + """ + embed = discord.Embed( + title="Success", + description=f"Modmail will not create any new threads.", + color=self.bot.main_color, + ) + if self.bot.config["dm_disabled"] < 1: + self.bot.config["dm_disabled"] = 1 + await self.bot.config.update() + + return await ctx.send(embed=embed) + + @disable.command(name="all") + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + async def disable_all(self, ctx): + """ + Disables all DM functionalities of Modmail. + + No new threads can be created through DM nor no further DM messages will be relayed. + """ + embed = discord.Embed( + title="Success", + description=f"Modmail will not accept any DM messages.", + color=self.bot.main_color, + ) + + if self.bot.config["dm_disabled"] != 2: + self.bot.config["dm_disabled"] = 2 + await self.bot.config.update() + + return await ctx.send(embed=embed) + + @commands.command() + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + async def isenable(self, ctx): + """ + Check if the DM functionalities of Modmail is enabled. + """ + + if self.bot.config["dm_disabled"] == 1: + embed = discord.Embed( + title="New Threads Disabled", + description=f"Modmail will not create any new threads.", + color=self.bot.main_color, + ) + elif self.bot.config["dm_disabled"] == 2: + embed = discord.Embed( + title="All DM Disabled", + description=f"Modmail will accept any DM messages for new and existing threads.", + color=self.bot.main_color, + ) + else: + embed = discord.Embed( + title="Enabled", + description=f"Modmail is receiving all DM messages.", + color=self.bot.main_color, + ) + + return await ctx.send(embed=embed) + def setup(bot): bot.add_cog(Modmail(bot)) diff --git a/core/config.py b/core/config.py index 781a5c23df..428e6005d3 100644 --- a/core/config.py +++ b/core/config.py @@ -58,6 +58,12 @@ class ConfigManager: "thread_self_close_response": "You have closed this Modmail thread.", "thread_move_notify": False, "thread_move_response": "This thread has been moved.", + "disabled_new_thread_title": "Not Delivered", + "disabled_new_thread_response": "We are not accepting new threads.", + "disabled_new_thread_footer": "Please try again later...", + "disabled_current_thread_title": "Not Delivered", + "disabled_current_thread_response": "We are not accepting any messages.", + "disabled_current_thread_footer": "Please try again later...", # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), @@ -73,6 +79,9 @@ class ConfigManager: "activity_message": "", "activity_type": None, "status": None, + # dm_disabled 0 = none, 1 = new threads, 2 = all threads + # TODO: use emum + "dm_disabled": 0, "oauth_whitelist": [], # moderation "blocked": {}, diff --git a/core/config_help.json b/core/config_help.json index cd0167df6f..0a6cee5076 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -348,6 +348,72 @@ "See also: `thread_move_notify`." ] }, + "disabled_new_thread_title": { + "default": "Not Delivered.", + "description": "The title of the message embed when Modmail new thread creation is disabled and user tries to create a new thread.", + "examples": [ + "`{prefix}config set disabled_new_thread_title Closed`" + ], + "notes": [ + "Only has an effect when `{prefix}disable` or `{prefix}disable all` is set.", + "See also: `disabled_new_thread_response`, `disabled_new_thread_footer`, `disabled_current_thread_title`." + ] + }, + "disabled_new_thread_response": { + "default": "We are not accepting new threads.", + "description": "The body of the message embed when Modmail new thread creation is disabled and user tries to create a new thread.", + "examples": [ + "`{prefix}config set disabled_new_thread_response Our working hours is between 8am - 6pm EST.`" + ], + "notes": [ + "Only has an effect when `{prefix}disable` or `{prefix}disable all` is set.", + "See also: `disabled_new_thread_title`, `disabled_new_thread_footer`, `disabled_current_thread_response`." + ] + }, + "disabled_new_thread_footer": { + "default": "Please try again later...", + "description": "The footer of the message embed when Modmail new thread creation is disabled and user tries to create a new thread.", + "examples": [ + "`{prefix}config set disabled_new_thread_footer Contact us later`" + ], + "notes": [ + "Only has an effect when `{prefix}disable` or `{prefix}disable all` is set.", + "See also: `disabled_new_thread_title`, `disabled_new_thread_response`, `disabled_current_thread_footer`." + ] + }, + "disabled_current_thread_title": { + "default": "Not Delivered.", + "description": "The title of the message embed when Modmail DM is disabled and user DMs Modmail from existing thread.", + "examples": [ + "`{prefix}config set disabled_current_thread_title Unavailable`" + ], + "notes": [ + "Only has an effect when `{prefix}disable all` is set.", + "See also: `disabled_current_thread_response`, `disabled_current_thread_footer`, `disabled_new_thread_title`." + ] + }, + "disabled_current_thread_response": { + "default": "We are not accepting any messages.", + "description": "The body of the message embed when Modmail DM is disabled and user DMs Modmail from existing thread.", + "examples": [ + "`{prefix}config set disabled_current_thread_response On break right now.`" + ], + "notes": [ + "Only has an effect when `{prefix}disable all` is set.", + "See also: `disabled_current_thread_title`, `disabled_current_thread_footer`, `disabled_new_thread_response`." + ] + }, + "disabled_current_thread_footer": { + "default": "Please try again later...", + "description": "The footer of the message embed when Modmail DM is disabled and user DMs Modmail from existing thread.", + "examples": [ + "`{prefix}config set disabled_current_thread_footer Message back!`" + ], + "notes": [ + "Only has an effect when `{prefix}disable all` is set.", + "See also: `disabled_current_thread_title`, `disabled_current_thread_response`, `disabled_new_thread_footer`." + ] + }, "recipient_color": { "default": "Discord Gold [#F1C40F](https://placehold.it/100/f1c40f?text=+)", "description": "This is the color of the messages sent by the recipient, this applies to messages received in the thread channel.", diff --git a/core/thread.py b/core/thread.py index 056f3c3073..01523e7d13 100644 --- a/core/thread.py +++ b/core/thread.py @@ -647,19 +647,26 @@ async def send( avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: avatar_url = self.bot.guild.icon_url - embed.set_author(name=name, icon_url=avatar_url, - url=f"https://discordapp.com/channels/{self.bot.guild.id}") + embed.set_author( + name=name, + icon_url=avatar_url, + url=f"https://discordapp.com/channels/{self.bot.guild.id}", + ) else: # Normal message name = str(author) avatar_url = author.avatar_url - embed.set_author(name=name, icon_url=avatar_url, url=f"https://discordapp.com/users/{author.id}") + embed.set_author( + name=name, + icon_url=avatar_url, + url=f"https://discordapp.com/users/{author.id}", + ) else: # Special note messages embed.set_author( name=f"Note ({author.name})", icon_url=system_avatar_url, - url=f"https://discordapp.com/users/{author.id}" + url=f"https://discordapp.com/users/{author.id}", ) ext = [(a.url, a.filename) for a in message.attachments] @@ -747,7 +754,16 @@ async def send( try: await message.delete() except Exception as e: - logger.warning('Cannot delete message: %s.', str(e)) + logger.warning("Cannot delete message: %s.", str(e)) + + if ( + from_mod + and self.bot.config["dm_disabled"] == 2 + and destination != self.channel + ): + logger.info( + "Sending a message to %s when DM disabled is set.", self.recipient + ) try: await destination.trigger_typing() @@ -814,7 +830,7 @@ async def find( recipient: typing.Union[discord.Member, discord.User] = None, channel: discord.TextChannel = None, recipient_id: int = None, - ) -> Thread: + ) -> typing.Optional[Thread]: """Finds a thread from cache or from discord channel topics.""" if recipient is None and channel is not None: thread = self._find_from_channel(channel) From 320fa67da06a521a28342c3d6626fa9af2918fc2 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Fri, 1 Nov 2019 07:20:21 -0700 Subject: [PATCH 0459/1221] Typo --- CHANGELOG.md | 2 +- cogs/modmail.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 438227c419..f2e8313c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev4 +# v3.3.0-dev5 ### Important diff --git a/cogs/modmail.py b/cogs/modmail.py index 954e617b5c..62f36ac6dd 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1270,19 +1270,19 @@ async def isenable(self, ctx): if self.bot.config["dm_disabled"] == 1: embed = discord.Embed( title="New Threads Disabled", - description=f"Modmail will not create any new threads.", - color=self.bot.main_color, + description=f"Modmail is not creating new threads.", + color=self.bot.error_color, ) elif self.bot.config["dm_disabled"] == 2: embed = discord.Embed( title="All DM Disabled", - description=f"Modmail will accept any DM messages for new and existing threads.", - color=self.bot.main_color, + description=f"Modmail is not accepting any DM messages for new and existing threads.", + color=self.bot.error_color, ) else: embed = discord.Embed( title="Enabled", - description=f"Modmail is receiving all DM messages.", + description=f"Modmail is accepting all DM messages.", color=self.bot.main_color, ) From 4e35b21bf30e12c7f3d9b0679a70fbb8927f0df8 Mon Sep 17 00:00:00 2001 From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com> Date: Fri, 1 Nov 2019 21:34:22 +0600 Subject: [PATCH 0460/1221] Update log example URL (#406) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 920b6432f7..70a95b1767 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ This bot is free for everyone and always will be. If you like this project and w When a member sends a direct message to the bot, Modmail will create a channel or "thread" within an isolated category. All further DM messages will automatically relay to that channel, for any available staff can respond within the channel. -All threads are logged and you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.modmail.tk/example). +All threads are logged and you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.logviewer.tech/example). ## Features @@ -71,7 +71,7 @@ All threads are logged and you can view previous threads through their correspon * Minimum duration for members to be in the guild before allowed to contact Modmail (`guild_age`). * **Advanced Logging Functionality:** - * When you close a thread, Modmail will generate a [log link](https://logs.modmail.tk/example) and post it to your log channel. + * When you close a thread, Modmail will generate a [log link](https://logs.logviewer.tech/example) and post it to your log channel. * Native Discord dark-mode feel. * Markdown/formatting support. * Login via Discord to protect your logs ([premium Patreon feature](https://patreon.com/kyber)). From ec5b8270237e6ba6976dc1a9b1fa8fbaceea74dc Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 1 Nov 2019 19:06:23 -0700 Subject: [PATCH 0461/1221] Update LICENSE --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 0ad25db4bd..7579c25e0a 100644 --- a/LICENSE +++ b/LICENSE @@ -629,8 +629,8 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - Copyright (C) + Modmail for Discord. + Copyright (C) 2018-2019 kyb3r This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published From 5ed556d1bd30d87c27a50ae891dee99ca4d4d802 Mon Sep 17 00:00:00 2001 From: Robin Mahieu <42642013+papiersnipper@users.noreply.github.com> Date: Sat, 2 Nov 2019 20:11:19 +0100 Subject: [PATCH 0462/1221] Delete notes, "?logs delete", linked alias (#402) * Add ability to delete notes * Add wipe command * Add option to add existing aliases in new aliases * Fix typo in presence docstring * Update changelog and bump dev version --- .lint.py | 2 +- CHANGELOG.md | 5 +++- bot.py | 2 +- cogs/modmail.py | 29 ++++++++++++++++++++++- cogs/plugins.py | 2 +- cogs/utility.py | 62 ++++++++++++++++++++++++++++++++----------------- core/clients.py | 8 ++++--- core/thread.py | 1 - 8 files changed, 81 insertions(+), 30 deletions(-) diff --git a/.lint.py b/.lint.py index fd3cbc2f19..9d29372fd8 100644 --- a/.lint.py +++ b/.lint.py @@ -1,4 +1,4 @@ -if __name__ == '__main__': +if __name__ == "__main__": import sys from os import listdir from os.path import join diff --git a/CHANGELOG.md b/CHANGELOG.md index f2e8313c5a..cb7e092741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev5 +# v3.3.0-dev6 ### Important @@ -39,6 +39,8 @@ however, insignificant breaking changes does not guarantee a major version bump, - `disabled_current_thread_title` - `disabled_current_thread_response` - `disabled_current_thread_footer` +- Ability to delete notes when providing their ID. (Thanks to papiersnipper PR#402) +- Ability to delete log entries. (Thanks to papiersnipper PR#402) ### Changed @@ -62,6 +64,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Eval commands are logged in debug logs. - Presence updates 30 minutes instead of 45 now. - Fixed an assortment of problems to do with block. +- Existing aliases can be used when creating new aliases. (Thanks to papiersnipper PR#402) ### Internal diff --git a/bot.py b/bot.py index 34df9f1bb5..729722094f 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev5" +__version__ = "3.3.0-dev6" import asyncio import logging diff --git a/cogs/modmail.py b/cogs/modmail.py index 62f36ac6dd..b8215073df 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -722,6 +722,31 @@ async def logs_closed_by(self, ctx, *, user: User = None): session = EmbedPaginatorSession(ctx, *embeds) await session.run() + @logs.command(name="delete", aliases=["wipe"]) + @checks.has_permissions(PermissionLevel.OWNER) + async def logs_delete(self, ctx, key_or_link: str): + """ + Wipe a log entry from the database. + """ + key = key_or_link.split("/")[-1] + + success = await self.bot.api.delete_log_entry(key) + + if not success: + embed = discord.Embed( + title="Error", + description=f"Log entry `{key}` not found.", + color=self.bot.error_color, + ) + else: + embed = discord.Embed( + title="Success", + description=f"Log entry `{key}` successfully deleted.", + color=self.bot.main_color, + ) + + await ctx.send(embed=embed) + @logs.command(name="responded") @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs_responded(self, ctx, *, user: User = None): @@ -1167,10 +1192,12 @@ async def unblock(self, ctx, *, user: User = None): @checks.thread_only() async def delete(self, ctx, message_id: Optional[int] = None): """ - Delete a message that was sent using the reply command. + Delete a message that was sent using the reply command or a note. Deletes the previous message, unless a message ID is provided, which in that case, deletes the message with that message ID. + + Notes can only be deleted when a note ID is provided. """ thread = ctx.thread diff --git a/cogs/plugins.py b/cogs/plugins.py index 52fc20bdd6..b9efd78d9b 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -226,7 +226,7 @@ async def load_plugin(self, plugin): try: self.bot.load_extension(plugin.ext_string) - logger.info("Loaded plugin: %s", plugin.ext_string.split('.')[-1]) + logger.info("Loaded plugin: %s", plugin.ext_string.split(".")[-1]) self.loaded_plugins.add(plugin) except commands.ExtensionError as exc: diff --git a/cogs/utility.py b/cogs/utility.py index 57c536f4ea..51604472bf 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -305,7 +305,12 @@ async def about(self, ctx): latest = changelog.latest_version if self.bot.version.is_prerelease: - stable = next(filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions)) + stable = next( + filter( + lambda v: not parse_version(v.version).is_prerelease, + changelog.versions, + ) + ) footer = f"You are on the prerelease version • the latest version is v{stable.version}." elif self.bot.version < parse_version(latest.version): footer = f"A newer version is available v{latest.version}." @@ -509,7 +514,9 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="activity")) - activity, _ = await self.set_presence(activity_type=activity_type, activity_message=message) + activity, _ = await self.set_presence( + activity_type=activity_type, activity_message=message + ) self.bot.config["activity_type"] = activity.type.value self.bot.config["activity_message"] = activity.name @@ -565,7 +572,9 @@ async def status(self, ctx, *, status_type: str.lower): ) return await ctx.send(embed=embed) - async def set_presence(self, *, status=None, activity_type=None, activity_message=None): + async def set_presence( + self, *, status=None, activity_type=None, activity_message=None + ): if status is None: status = self.bot.config.get("status") @@ -574,9 +583,13 @@ async def set_presence(self, *, status=None, activity_type=None, activity_messag activity_type = self.bot.config.get("activity_type") url = None - activity_message = (activity_message or self.bot.config["activity_message"]).strip() + activity_message = ( + activity_message or self.bot.config["activity_message"] + ).strip() if activity_type is not None and not activity_message: - logger.warning("No activity message found whilst activity is provided, defaults to \"Modmail\".") + logger.warning( + 'No activity message found whilst activity is provided, defaults to "Modmail".' + ) activity_message = "Modmail" if activity_type == ActivityType.listening: @@ -599,14 +612,14 @@ async def set_presence(self, *, status=None, activity_type=None, activity_messag @tasks.loop(minutes=30) async def loop_presence(self): - """Set presence to the configured value every 45 minutes.""" + """Set presence to the configured value every 30 minutes.""" logger.debug("Resetting presence.") await self.set_presence() @loop_presence.before_loop async def before_loop_presence(self): await self.bot.wait_for_connected() - logger.line('debug') + logger.line("debug") activity, status = await self.set_presence() if activity is not None: @@ -1044,13 +1057,16 @@ async def alias_add(self, ctx, name: str.lower, *, value): if len(values) == 1: linked_command = values[0].split()[0].lower() if not self.bot.get_command(linked_command): - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description="The command you are attempting to point " - f"to does not exist: `{linked_command}`.", - ) - return await ctx.send(embed=embed) + if linked_command in self.bot.aliases: + values = [self.bot.aliases.get(linked_command)] + else: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="The command you are attempting to point " + f"to does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) embed = discord.Embed( title="Added alias", @@ -1068,13 +1084,17 @@ async def alias_add(self, ctx, name: str.lower, *, value): for i, val in enumerate(values, start=1): linked_command = val.split()[0] if not self.bot.get_command(linked_command): - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description="The command you are attempting to point " - f"to on step {i} does not exist: `{linked_command}`.", - ) - return await ctx.send(embed=embed) + if linked_command in self.bot.aliases: + index = values.index(linked_command) + values[index] = self.bot.aliases.get(linked_command) + else: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="The command you are attempting to point " + f"to on step {i} does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) embed.description += f"\n{i}: {val}" self.bot.aliases[name] = " && ".join(values) diff --git a/core/clients.py b/core/clients.py index f01753bf23..486f792744 100644 --- a/core/clients.py +++ b/core/clients.py @@ -106,9 +106,7 @@ async def get_responded_logs(self, user_id: Union[str, int]) -> list: return await self.logs.find(query).to_list(None) async def get_open_logs(self) -> list: - query = { - "open": True - } + query = {"open": True} return await self.logs.find(query).to_list(None) async def get_log(self, channel_id: Union[str, int]) -> dict: @@ -162,6 +160,10 @@ async def create_log_entry( prefix = "" return f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{key}" + async def delete_log_entry(self, key: str) -> bool: + result = await self.logs.delete_one({"key": key}) + return result.deleted_count == 1 + async def get_config(self) -> dict: conf = await self.db.config.find_one({"bot_id": self.bot.user.id}) if conf is None: diff --git a/core/thread.py b/core/thread.py index 01523e7d13..65147b4a6f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -439,7 +439,6 @@ async def _fetch_timeout( """ This grabs the timeout value for closing threads automatically from the ConfigManager and parses it for use internally. - :returns: None if no timeout is set. """ timeout = self.bot.config.get("thread_auto_close") From e87ca0ed9b65ed11e6cd9938ea7bbd3a74752fd0 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 2 Nov 2019 12:13:19 -0700 Subject: [PATCH 0463/1221] Keep arugments for linked alias --- cogs/utility.py | 69 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 51604472bf..0e29a5fd23 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1055,10 +1055,14 @@ async def alias_add(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) if len(values) == 1: - linked_command = values[0].split()[0].lower() + linked_command, *messages = values[0].split(maxsplit=1) if not self.bot.get_command(linked_command): - if linked_command in self.bot.aliases: - values = [self.bot.aliases.get(linked_command)] + alias_command = self.bot.aliases.get(linked_command) + if alias_command is not None: + if messages: + values = [f"{alias_command} {messages[0]}"] + else: + values = [alias_command] else: embed = discord.Embed( title="Error", @@ -1082,17 +1086,20 @@ async def alias_add(self, ctx, name: str.lower, *, value): ) for i, val in enumerate(values, start=1): - linked_command = val.split()[0] + linked_command, *messages = val.split(maxsplit=1) if not self.bot.get_command(linked_command): - if linked_command in self.bot.aliases: - index = values.index(linked_command) - values[index] = self.bot.aliases.get(linked_command) + alias_command = self.bot.aliases.get(linked_command) + if alias_command is not None: + if messages: + values = [f"{alias_command} {messages[0]}"] + else: + values = [alias_command] else: embed = discord.Embed( title="Error", color=self.bot.error_color, description="The command you are attempting to point " - f"to on step {i} does not exist: `{linked_command}`.", + f"to n step {i} does not exist: `{linked_command}`.", ) return await ctx.send(embed=embed) embed.description += f"\n{i}: {val}" @@ -1143,15 +1150,22 @@ async def alias_edit(self, ctx, name: str.lower, *, value): return await ctx.send(embed=embed) if len(values) == 1: - linked_command = values[0].split()[0].lower() + linked_command, *messages = values[0].split(maxsplit=1) if not self.bot.get_command(linked_command): - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description="The command you are attempting to point " - f"to does not exist: `{linked_command}`.", - ) - return await ctx.send(embed=embed) + alias_command = self.bot.aliases.get(linked_command) + if alias_command is not None: + if messages: + values = [f"{alias_command} {messages[0]}"] + else: + values = [alias_command] + else: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="The command you are attempting to point " + f"to does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) embed = discord.Embed( title="Edited alias", color=self.bot.main_color, @@ -1166,15 +1180,22 @@ async def alias_edit(self, ctx, name: str.lower, *, value): ) for i, val in enumerate(values, start=1): - linked_command = val.split()[0] + linked_command, *messages = val.split(maxsplit=1) if not self.bot.get_command(linked_command): - embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description="The command you are attempting to point " - f"to on step {i} does not exist: `{linked_command}`.", - ) - return await ctx.send(embed=embed) + alias_command = self.bot.aliases.get(linked_command) + if alias_command is not None: + if messages: + values = [f"{alias_command} {messages[0]}"] + else: + values = [alias_command] + else: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="The command you are attempting to point " + f"to on step {i} does not exist: `{linked_command}`.", + ) + return await ctx.send(embed=embed) embed.description += f"\n{i}: {val}" self.bot.aliases[name] = "&&".join(values) From c8f80397251f01cf2660e3a76fbe17c938008d6a Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 2 Nov 2019 21:15:12 -0700 Subject: [PATCH 0464/1221] Change logging --- bot.py | 51 ++++++++---------------------------------- cogs/modmail.py | 5 ++--- cogs/plugins.py | 5 ++--- cogs/utility.py | 7 +++--- core/changelog.py | 4 ++-- core/checks.py | 6 ++--- core/clients.py | 5 +++-- core/config.py | 5 ++--- core/models.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++ core/thread.py | 4 ++-- core/time.py | 5 +++-- 11 files changed, 86 insertions(+), 67 deletions(-) diff --git a/bot.py b/bot.py index 729722094f..da2801efb3 100644 --- a/bot.py +++ b/bot.py @@ -34,33 +34,12 @@ from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import human_join, parse_alias -from core.models import PermissionLevel, ModmailLogger, SafeFormatter +from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging from core.thread import ThreadManager from core.time import human_timedelta -logger: ModmailLogger = logging.getLogger("Modmail") -logger.__class__ = ModmailLogger - -logger.setLevel(logging.INFO) - -ch = logging.StreamHandler(stream=sys.stdout) -ch.setLevel(logging.INFO) -formatter = logging.Formatter( - "%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", - datefmt="%b %d %H:%M:%S", -) -ch.setFormatter(formatter) -logger.addHandler(ch) - - -class FileFormatter(logging.Formatter): - ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") - - def format(self, record): - record.msg = self.ansi_escape.sub("", record.msg) - return super().format(record) - +logger = getLogger(__name__) temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp") if not os.path.exists(temp_dir): @@ -125,7 +104,7 @@ def startup(self): logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") - logger.line("debug") + logger.line() for cog in self.loaded_cogs: logger.debug("Loading %s.", cog) @@ -145,30 +124,18 @@ def _configure_logging(self): "INFO": logging.INFO, "DEBUG": logging.DEBUG, } - - ch_debug = logging.FileHandler(self.log_file_name, mode="a+") - - ch_debug.setLevel(logging.DEBUG) - formatter_debug = FileFormatter( - "%(asctime)s %(filename)s[%(lineno)d] - %(levelname)s: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - ch_debug.setFormatter(formatter_debug) - logger.addHandler(ch_debug) + logger.line() log_level = logging_levels.get(level_text) if log_level is None: log_level = self.config.remove("log_level") - - logger.line() - if log_level is not None: - logger.setLevel(log_level) - ch.setLevel(log_level) - logger.info("Logging level: %s", level_text) + logger.warning("Invalid logging level set: %s.", level_text) + logger.warning("Using default logging level: INFO.") else: - logger.info("Invalid logging level set.") - logger.warning("Using default logging level: %s.", level_text) + logger.info("Logging level: %s", level_text) + logger.info("Log file: %s", self.log_file_name) + configure_logging(self.log_file_name, log_level) logger.debug("Successfully configured logging.") @property diff --git a/cogs/modmail.py b/cogs/modmail.py index b8215073df..8ce3f838fe 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,5 +1,4 @@ import asyncio -import logging from datetime import datetime from itertools import zip_longest from typing import Optional, Union @@ -13,7 +12,7 @@ from natural.date import duration from core import checks -from core.models import PermissionLevel +from core.models import PermissionLevel, getLogger from core.paginator import EmbedPaginatorSession from core.time import UserFriendlyTime, human_timedelta from core.utils import ( @@ -24,7 +23,7 @@ trigger_typing, ) -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) class Modmail(commands.Cog): diff --git a/cogs/plugins.py b/cogs/plugins.py index b9efd78d9b..15db8214e4 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -1,7 +1,6 @@ import asyncio import io import json -import logging import os import shutil import sys @@ -20,11 +19,11 @@ from pkg_resources import parse_version from core import checks -from core.models import PermissionLevel +from core.models import PermissionLevel, getLogger from core.paginator import EmbedPaginatorSession from core.utils import truncate, trigger_typing -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) class InvalidPluginError(commands.BadArgument): diff --git a/cogs/utility.py b/cogs/utility.py index 0e29a5fd23..9610582334 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1,6 +1,5 @@ import asyncio import inspect -import logging import os import traceback import random @@ -24,11 +23,11 @@ from core import checks from core.changelog import Changelog -from core.models import InvalidConfigError, PermissionLevel +from core.models import InvalidConfigError, PermissionLevel, getLogger from core.paginator import EmbedPaginatorSession, MessagePaginatorSession from core import utils -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) class ModmailHelpCommand(commands.HelpCommand): @@ -619,7 +618,7 @@ async def loop_presence(self): @loop_presence.before_loop async def before_loop_presence(self): await self.bot.wait_for_connected() - logger.line("debug") + logger.line() activity, status = await self.set_presence() if activity is not None: diff --git a/core/changelog.py b/core/changelog.py index 1e9254286f..91856600e9 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -1,12 +1,12 @@ -import logging import re from typing import List from discord import Embed +from core.models import getLogger from core.utils import truncate -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) class Version: diff --git a/core/checks.py b/core/checks.py index 6ccf1647b8..0a5a43e1bb 100644 --- a/core/checks.py +++ b/core/checks.py @@ -1,10 +1,8 @@ -import logging - from discord.ext import commands -from core.models import PermissionLevel +from core.models import PermissionLevel, getLogger -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) def has_permissions_predicate( diff --git a/core/clients.py b/core/clients.py index 486f792744..8d89331664 100644 --- a/core/clients.py +++ b/core/clients.py @@ -1,4 +1,3 @@ -import logging import secrets from datetime import datetime from json import JSONDecodeError @@ -8,7 +7,9 @@ from aiohttp import ClientResponseError, ClientResponse -logger = logging.getLogger("Modmail") +from core.models import getLogger + +logger = getLogger(__name__) class RequestClient: diff --git a/core/config.py b/core/config.py index 428e6005d3..bfc4a3b675 100644 --- a/core/config.py +++ b/core/config.py @@ -1,6 +1,5 @@ import asyncio import json -import logging import os import re import typing @@ -13,11 +12,11 @@ from discord.ext.commands import BadArgument from core._color_data import ALL_COLORS -from core.models import InvalidConfigError, Default +from core.models import InvalidConfigError, Default, getLogger from core.time import UserFriendlyTime from core.utils import strtobool -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) load_dotenv() diff --git a/core/models.py b/core/models.py index 47e6375cc5..5a1b455f61 100644 --- a/core/models.py +++ b/core/models.py @@ -1,5 +1,7 @@ import _string import logging +import re +import sys from enum import IntEnum from string import Formatter @@ -88,6 +90,60 @@ def line(self, level="info"): ) +logging.setLoggerClass(ModmailLogger) +log_level = logging.INFO +loggers = set() + +ch = logging.StreamHandler(stream=sys.stdout) +ch.setLevel(log_level) +formatter = logging.Formatter( + "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", + datefmt="%m/%d/%y %H:%M:%S", +) +ch.setFormatter(formatter) + +ch_debug = None + + +def getLogger(name=None) -> ModmailLogger: + logger = logging.getLogger(name) + logger.setLevel(log_level) + logger.addHandler(ch) + if ch_debug is not None: + logger.addHandler(ch_debug) + loggers.add(logger) + return logger + + +class FileFormatter(logging.Formatter): + ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") + + def format(self, record): + record.msg = self.ansi_escape.sub("", record.msg) + return super().format(record) + + +def configure_logging(name, level=None): + global ch_debug, log_level + ch_debug = logging.FileHandler(name, mode="a+") + + formatter_debug = FileFormatter( + "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ch_debug.setFormatter(formatter_debug) + ch_debug.setLevel(logging.DEBUG) + + if level is not None: + log_level = level + + ch.setLevel(log_level) + + for logger in loggers: + logger.setLevel(log_level) + logger.addHandler(ch_debug) + + class _Default: pass diff --git a/core/thread.py b/core/thread.py index 65147b4a6f..03cefcae3d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1,5 +1,4 @@ import asyncio -import logging import re import string import typing @@ -11,10 +10,11 @@ import discord from discord.ext.commands import MissingRequiredArgument, CommandError +from core.models import getLogger from core.time import human_timedelta from core.utils import is_image_url, days, match_user_id, truncate -logger = logging.getLogger("Modmail") +logger = getLogger(__name__) class Thread: diff --git a/core/time.py b/core/time.py index 5e8049567a..ba27fa021c 100644 --- a/core/time.py +++ b/core/time.py @@ -3,7 +3,6 @@ Source: https://github.com/Rapptz/RoboDanny/blob/rewrite/cogs/utils/time.py """ -import logging import re from datetime import datetime @@ -12,7 +11,9 @@ import parsedatetime as pdt from dateutil.relativedelta import relativedelta -logger = logging.getLogger("Modmail") +from core.models import getLogger + +logger = getLogger(__name__) class ShortTime: From 27bbd71e0004418d4753a1992eede95a1f184fab Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 2 Nov 2019 21:22:08 -0700 Subject: [PATCH 0465/1221] Bump version to stable 3.3.0 --- CHANGELOG.md | 3 ++- bot.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7e092741..0d50213d4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.3.0-dev6 +# v3.3.0 ### Important @@ -73,6 +73,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Prerelease naming scheme is now `x.x.x-devN`. - `trigger_typing` has been moved to `core.utils.trigger_typing`, original location is deprecated. - Simpler status and activity logic. +- New logging logic. # v3.2.2 diff --git a/bot.py b/bot.py index da2801efb3..8a0093246e 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0-dev6" +__version__ = "3.3.0" import asyncio import logging 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 0466/1221] 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 0467/1221] 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 0468/1221] 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 0469/1221] 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 0470/1221] 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 0471/1221] 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 53943820ab02bf5cbe71c22569c0c8e9d251760d Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 14 Nov 2019 20:10:20 -0800 Subject: [PATCH 0472/1221] Fixed grammars in CHANGELOG --- CHANGELOG.md | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d50213d4c..37b853cd5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,19 +45,19 @@ however, insignificant breaking changes does not guarantee a major version bump, ### Changed - `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) -- Automatically delete notes command `?note` when there's no attachments attached. +- Automatically delete notes command `?note` when there're no attachments attached. - Embed author links used to be inaccessible in many cases, now: - - `?anonreply`, `?reply`, and `?note` in thread channel will link to the sender's profile. + - `?anonreply`, `?reply`, and `?note` in the thread channel will link to the sender's profile. - `?reply` and recipient's DM will also link the sender's profile. - `?anonreply` in DM channel will link to the first channel of the main guild. - Plugins update (mostly internal). - `git` is no longer used to install plugins, it now downloads through zip files. - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. - - Reorganised plugins folder structure. - - Logging / plugin-related messages changes. + - Reorganized plugins folder structure. + - Logging / plugin-related messages change. - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). - Help command is in alphabetical order grouped by permissions. -- Notes are no longer always blurple, its set to `MAIN_COLOR` now. +- Notes are no longer always blurple, it's set to `MAIN_COLOR` now. - Added `?plugins update` for updating all installed plugins. - Reintroduce flake8 and use bandit for security issues detection. - Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. @@ -71,7 +71,7 @@ however, insignificant breaking changes does not guarantee a major version bump, - Reworked `config.get` and `config.set`, it feeds through the converters before setting/getting. - To get/set the raw value, access through `config[]`. - Prerelease naming scheme is now `x.x.x-devN`. -- `trigger_typing` has been moved to `core.utils.trigger_typing`, original location is deprecated. +- `trigger_typing` has been moved to `core.utils.trigger_typing`, the original location is deprecated. - Simpler status and activity logic. - New logging logic. @@ -108,7 +108,7 @@ Security update! ### Fixed -- `?help `, will return `Perhaps you meant: `, now its fixed. +- `?help `, will return `Perhaps you meant: `, now it's fixed. - For example, `?help add` used to return `Perhaps you meant: add`, now it wouldn't do this. - Aliases and Permissions command names are always saved lowercase now. - An improved Dockerfile. @@ -116,7 +116,7 @@ Security update! ### Internal - Use regex to parse Changes, Added, Fixed, etc and description. -- Adds `PermissionLevel.INVALID` when commands doesn't have a permission level. +- Adds `PermissionLevel.INVALID` when commands don't have a permission level. # v3.1.1 @@ -212,7 +212,6 @@ Security update! - A lot of bugs with `thread_auto_close` 😅 - # v3.0.0 ### Added @@ -261,7 +260,7 @@ b) The user shares more than 1 server with the bot. ### Changed -Notes taken using the `?note` command are now automatically pinned within the thread channel. +Notes taken with the `?note` command are now automatically pinned within the thread channel. # v2.22.0 @@ -385,7 +384,7 @@ When updating to this version, all prior permission settings with guild-based pe ### New Command -- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer require manage messages permission to recall thread messages. +- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer requires manage messages permission to recall thread messages. ### Changed @@ -399,16 +398,17 @@ When updating to this version, all prior permission settings with guild-based pe - For example, `?logs search e7499e82f8ff`. # v2.17.1 + ### What's new? Stricter fallback genesis embed search. ### Changed -How modmail checks if a channel is a thread: -1. First the bot checks if the channel topic is in the format `User ID: xxxx`, this means it is a thread. -2. If a channel topic is not found, the bot searches through message history of a channel to find the thread creation embed. This step should never yield a thread for a normal user, but in the case of a another bot messing up the channel topic (happened to a user before) this extra step was added. +How modmail checks if a channel is a thread: +1. The bot first checks if the channel topic is in the format `User ID: xxxx`, this means it is a thread. +2. If a channel topic is not found, the bot searches through the message history of a channel to find the thread creation embed. This step should never yield a thread for a normal user, but in the case of another bot messing up the channel topic (happened to a user before) this extra step was added. # v2.17.0 @@ -418,7 +418,6 @@ Added a config option `reply_without_command` which when present, enables the bo To enable this functionality, do `?config set reply_without_command true` and to disable it, use `?config del reply_without_command`. - ### Changed The `move` command now only requires `manage_messages` perms instead of `manage_channels` @@ -435,10 +434,9 @@ An issue where a scheduled close would not execute over a long period of time if All support for Modmail API (api.modmail.tk) has terminated. If you're still using api.modmail.tk, you will need to migrate to the self-hosted database -option ASAP. Your bot will not work unless you switch to the self-hosted option. Refer to the -installation tutorial for information regarding self-hosted Modmail. +option ASAP. Your bot will not work unless you switch to the self-hosted option. Refer to the installation tutorial for information regarding self-hosted Modmail. -If a member leaves/joins (again) while they are a recipient of a thread, a message will be sent to notify you that this has occured. +If a member leaves/joins (again) while they are a recipient of a thread, a message will be sent to notify you that this has occurred. # v2.15.1 @@ -497,8 +495,8 @@ You can do this via `?config set sent_emoji disable` The bot now handles having too many roles to show in the thread created embed. # v2.13.12 -### What's new? +### What's new? Added image link in title in case discord fails to embed an image. # v2.13.11 @@ -511,12 +509,12 @@ Added image link in title in case discord fails to embed an image. ### Changed - `block` reason cannot start with `System Message: ` as it is now reserved for internal user blocking. -- `block`, like `close`, now support a block duration (temp blocking). +- `block`, like `close`, now supports a block duration (temp blocking). # v2.13.10 ### Fixed -- Fixed an issue where status and activity does not work if they were modified wrongly in the database. +- Fixed an issue where status and activity do not work if they were modified wrongly in the database. - This was especially an issue for older Modmail users, as the old `status` configuration variable clashes with the new `status` variable. # v2.13.9 From 0e4c12fbef5caa3e041812c0c7ff6b6882be72eb Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Thu, 14 Nov 2019 20:40:33 -0800 Subject: [PATCH 0473/1221] Create modmailworkflow.yml --- .github/workflows/modmailworkflow.yml | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/modmailworkflow.yml diff --git a/.github/workflows/modmailworkflow.yml b/.github/workflows/modmailworkflow.yml new file mode 100644 index 0000000000..ce3e8214b0 --- /dev/null +++ b/.github/workflows/modmailworkflow.yml @@ -0,0 +1,29 @@ +name: Modmail Workflow + +on: [push, pull_request, pull_request_review] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest, ubuntu-16.04] + python-version: [3.6, 3.7] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pipenv + python -m pipenv install -d + - name: Bandit syntax check + run: | + pipenv run bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json + - name: Lint with pylint and flake8 + pipenv run python .lint.py + pipenv run flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 --exit-zero From 119f2d48678b4365e1aded25cc9d2021ef8067b7 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Thu, 14 Nov 2019 20:42:24 -0800 Subject: [PATCH 0474/1221] Update modmailworkflow.yml --- .github/workflows/modmailworkflow.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/modmailworkflow.yml b/.github/workflows/modmailworkflow.yml index ce3e8214b0..9d78ae0287 100644 --- a/.github/workflows/modmailworkflow.yml +++ b/.github/workflows/modmailworkflow.yml @@ -25,5 +25,6 @@ jobs: run: | pipenv run bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - name: Lint with pylint and flake8 + run: | pipenv run python .lint.py pipenv run flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 --exit-zero From c8921ac2a008e9797a718568b1387d56f52f7006 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Thu, 14 Nov 2019 20:54:36 -0800 Subject: [PATCH 0475/1221] Would this work? --- .github/workflows/{modmailworkflow.yml => lints.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{modmailworkflow.yml => lints.yml} (94%) diff --git a/.github/workflows/modmailworkflow.yml b/.github/workflows/lints.yml similarity index 94% rename from .github/workflows/modmailworkflow.yml rename to .github/workflows/lints.yml index 9d78ae0287..014d177ffa 100644 --- a/.github/workflows/modmailworkflow.yml +++ b/.github/workflows/lints.yml @@ -20,7 +20,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install pipenv - python -m pipenv install -d + python -m pipenv install --python python -d - name: Bandit syntax check run: | pipenv run bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json From c0c8db9810906e993c7b68b8731ff1d52a3990ea Mon Sep 17 00:00:00 2001 From: DAzVise Date: Fri, 15 Nov 2019 15:01:55 +0300 Subject: [PATCH 0476/1221] 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 0477/1221] 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 93a77bf0dd868d2c436279ee9bd37130b50445cc Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Nov 2019 11:07:47 -0800 Subject: [PATCH 0478/1221] =?UTF-8?q?If=20this=20doesn=E2=80=99t=20work=20?= =?UTF-8?q?I=20give=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lints.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 014d177ffa..d766f63b3f 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -19,8 +19,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install pipenv - python -m pipenv install --python python -d + pip install pipenv + pipenv install --three -d - name: Bandit syntax check run: | pipenv run bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json From c8646269d4b874caff416b2139e7a57662bdd6b3 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Nov 2019 14:46:46 -0800 Subject: [PATCH 0479/1221] GitHub workflow patch 1 (#637) * Update lints.yml --- .github/workflows/lints.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index d766f63b3f..8d2a1d6092 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -19,11 +19,13 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pipenv - pipenv install --three -d - - name: Bandit syntax check + python -m pip install pipenv + - name: Configure environment run: | - pipenv run bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json + python -c "import sys; print(sys.executable)" + pipenv install --python python -d + - name: Bandit syntax check + run: pipenv run bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - name: Lint with pylint and flake8 run: | pipenv run python .lint.py From 438b163420bf9f530a39b1c2a072d6dfc3358663 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Nov 2019 15:33:35 -0800 Subject: [PATCH 0480/1221] Update lints.yml --- .github/workflows/lints.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 8d2a1d6092..6d097f2058 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -3,9 +3,10 @@ name: Modmail Workflow on: [push, pull_request, pull_request_review] jobs: - build: + code-style: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macOS-latest, ubuntu-16.04] python-version: [3.6, 3.7] @@ -20,10 +21,12 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install pipenv + continue-on-error: true - name: Configure environment run: | python -c "import sys; print(sys.executable)" pipenv install --python python -d + continue-on-error: true - name: Bandit syntax check run: pipenv run bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - name: Lint with pylint and flake8 From b86ff2ba45c0c3d0f8df98062e0a57c2cf224822 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Nov 2019 18:38:33 -0800 Subject: [PATCH 0481/1221] Add issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/command-request.md | 23 ++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 23 ++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/command-request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..28a29c07b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] bug report title here" +labels: 'maybe: bug' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Bot Info** +Bot version (check with `@modmail about`): +Host method (Heroku, self-host, etc): + +**To Reproduce** +Steps to reproduce the behavior: +1. Who can reproduce (ex. anyone, owners)? +2. Where can it be reproduced (ex. in thread channels, recipient DM's)? +3. Done what to cause the error? +4. Any recently made changes to your bot? +5. Errored + +**Error Logs** +If your Modmail bot is online, type `@modmail debug hastebin` and include the link here. +If your Modmail bot is not online or the previous command did not generate a link, do the following: + +1. Select your *bot* application at https://dashboard.heroku.com +2. [Restart your bot](https://i.imgur.com/3FcrlKz.png) +3. Reproduce the error to populate the error logs +4. [Copy and paste the logs](https://i.imgur.com/TTrhitm.png) + +**Screenshots** +Add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/command-request.md b/.github/ISSUE_TEMPLATE/command-request.md new file mode 100644 index 0000000000..44902f2cf3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/command-request.md @@ -0,0 +1,23 @@ +--- +name: Command request +about: Request a new command +title: "[COMMAND-REQUEST] your title here" +labels: command-request +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Who will this benefit** +Does this feature apply to a great portion of users? + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..48b986344d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE-REQUEST] your title here" +labels: feature-request +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Who will this benefit** +Does this feature apply to a great portion of users? + +**Additional context** +Add any other context or screenshots about the feature request here. From 0a87883ce15e62f654e71694744fc267c7d57b68 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Nov 2019 19:14:50 -0800 Subject: [PATCH 0482/1221] Create stale.yml --- .github/workflows/stale.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..8b02280dcb --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,15 @@ +name: "Close stale issues" +on: + schedule: + - cron: "0 0 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - 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 + days-before-close: 5 From 028abfce3ba8d66050d3b14a47b4226f6c9e5020 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Nov 2019 19:25:07 -0800 Subject: [PATCH 0483/1221] Don't use pipenv --- .github/workflows/lints.yml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 6d097f2058..5913ce3d37 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -20,16 +20,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install pipenv - continue-on-error: true - - name: Configure environment - run: | - python -c "import sys; print(sys.executable)" - pipenv install --python python -d + python -m pip install bandit flake8 pylint continue-on-error: true - name: Bandit syntax check - run: pipenv run bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json + run: bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - name: Lint with pylint and flake8 run: | - pipenv run python .lint.py - pipenv run flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 --exit-zero + python .lint.py + flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 --exit-zero From 97154b9e4ec40943910afe98191390f80808d4ad Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Nov 2019 21:52:59 -0800 Subject: [PATCH 0484/1221] Update lints.yml --- .github/workflows/lints.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 5913ce3d37..a2c37c332c 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -21,10 +21,11 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install bandit flake8 pylint + python -m pip instal -r requirements.min.txt continue-on-error: true - name: Bandit syntax check run: bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - name: Lint with pylint and flake8 run: | - python .lint.py + pylint ./bot.py cogs/*.py core/*.py --exit-zero -r y flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 --exit-zero From 8df7268887e7f89faf8baa5077a1ca8ac9198828 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Nov 2019 21:58:44 -0800 Subject: [PATCH 0485/1221] Update lints.yml --- .github/workflows/lints.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index a2c37c332c..8fe693aaee 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -21,11 +21,10 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install bandit flake8 pylint - python -m pip instal -r requirements.min.txt continue-on-error: true - name: Bandit syntax check run: bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - name: Lint with pylint and flake8 run: | - pylint ./bot.py cogs/*.py core/*.py --exit-zero -r y - flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 --exit-zero + pylint ./bot.py cogs/*.py core/*.py --disable=import-error --exit-zero -r y + flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 From 7fecadc0d23f08cf9d238a2efba07c5ad4a27e46 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Nov 2019 22:04:30 -0800 Subject: [PATCH 0486/1221] Add black with lint --- .github/workflows/lints.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 8fe693aaee..e94de5202c 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macOS-latest, ubuntu-16.04] + os: [ubuntu-latest, windows-latest, macOS-latest] python-version: [3.6, 3.7] steps: @@ -20,11 +20,12 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install bandit flake8 pylint + python -m pip install bandit flake8 pylint black continue-on-error: true - name: Bandit syntax check run: bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json - - name: Lint with pylint and flake8 - run: | - pylint ./bot.py cogs/*.py core/*.py --disable=import-error --exit-zero -r y + - name: Pylint + run: pylint ./bot.py cogs/*.py core/*.py --disable=import-error --exit-zero -r y + - name: Flake8 and black lint flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 + black . --check From 85e8257dd7ac8779dcafdae83419617e0009fd85 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Nov 2019 22:07:08 -0800 Subject: [PATCH 0487/1221] whoops --- .github/workflows/lints.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index e94de5202c..14a9447ab8 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -27,5 +27,6 @@ jobs: - name: Pylint run: pylint ./bot.py cogs/*.py core/*.py --disable=import-error --exit-zero -r y - name: Flake8 and black lint + run: | flake8 ./bot.py cogs/*.py core/*.py --ignore=E501,E203,W503 black . --check From b41acc734430211fe79354886dbaa1d9f6c474dc Mon Sep 17 00:00:00 2001 From: DAzVise Date: Sat, 16 Nov 2019 11:19:57 +0300 Subject: [PATCH 0488/1221] 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 0489/1221] 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 0490/1221] 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 0491/1221] 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 0492/1221] 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 0493/1221] 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 0494/1221] 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 0495/1221] 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 0496/1221] 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 b9321330efb1384a01b0b5d94c9b134329407ee0 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 18 Nov 2019 17:44:08 -0800 Subject: [PATCH 0497/1221] v3.3.1 patch for animation KeyError --- CHANGELOG.md | 7 +++- Pipfile | 2 +- Pipfile.lock | 108 ++++++++++++++++++++++++++------------------------- README.md | 2 +- bot.py | 2 +- 5 files changed, 65 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b853cd5e..a2236ef4a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +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.0 +# v3.3.1 + +### Emergency Patch +- Fixed a recent issue with an animation KeyError due to Discord API update. + +# v3.3.0 ### Important diff --git a/Pipfile b/Pipfile index b794958a5c..549ec62653 100644 --- a/Pipfile +++ b/Pipfile @@ -22,7 +22,7 @@ parsedatetime = "==2.4" aiohttp = "<3.6.0,>=3.3.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = "==1.2.4" +"discord.py" = "==1.2.5" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 5b21468ed8..adf4fb9de7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e218eea19b5d78a490cf012fb818e002a128bbed21b08f2327963e66c2d47c71" + "sha256": "e5dd1237af339923e2b40a66f8a1f12682e8b1fb4de0d81f4260a7fd03076dac" }, "pipfile-spec": 6, "requires": { @@ -82,11 +82,10 @@ }, "discord.py": { "hashes": [ - "sha256:2d5fec14b1c047b561336e969939e7f34564489851ec1a7edb8d303087127256", - "sha256:3e044d84f0bb275d173e2d958cb4a579e525707f90e3e8a15c59901f79e80663" + "sha256:7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d" ], "index": "pypi", - "version": "==1.2.4" + "version": "==1.2.5" }, "dnspython": { "hashes": [ @@ -105,9 +104,9 @@ }, "future": { "hashes": [ - "sha256:858e38522e8fd0d3ce8f0c1feaf0603358e366d5403209674c7b617fa0c24093" + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" ], - "version": "==0.18.1" + "version": "==0.18.2" }, "idna": { "hashes": [ @@ -225,11 +224,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], "index": "pypi", - "version": "==2.8.0" + "version": "==2.8.1" }, "python-dotenv": { "hashes": [ @@ -241,24 +240,26 @@ }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" }, "uvloop": { "hashes": [ - "sha256:0deb6c97c5807c792dd9024bab90e6ca49e981862103cb2ea37b430c1ca0a267", - "sha256:155b34d513655e753d07f499a7e811970e2d397f240dfcbec0b32a9587159c99", - "sha256:1df3ddfa280206e9999ae1c777a20836eb895bcec6dc9fae2cbb6eecfafb099e", - "sha256:5b19361c8767e1dc61f6367f948d4f3dc5504b9f2eba488641b3d26ec14498ba", - "sha256:942cd07035510b149d6160796f4e972137130ae953871b6a98c2cf5d5ab68c2e", - "sha256:c63b6c0bf33144c604dd72f7eecf2d3a3ac7405c503c67bd98128cf306efe18a", - "sha256:e698a20a3b4ccb380d207f9d491d4085d7c38d364f6a0bae98684a1612a9607a" + "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", + "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", + "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", + "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", + "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", + "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", + "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", + "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", + "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" ], "index": "pypi", "markers": "sys_platform != 'win32'", - "version": "==0.13.0" + "version": "==0.14.0" }, "virtualenv": { "hashes": [ @@ -327,10 +328,10 @@ }, "astroid": { "hashes": [ - "sha256:09a3fba616519311f1af8a461f804b68f0370e100c9264a035aa7846d7852e33", - "sha256:5a79c9b4bd6c4be777424593f957c996e20beb5f74e0bc332f47713c6f675efe" + "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", + "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" ], - "version": "==2.3.2" + "version": "==2.3.3" }, "attrs": { "hashes": [ @@ -386,10 +387,10 @@ }, "gitpython": { "hashes": [ - "sha256:3237caca1139d0a7aa072f6735f5fd2520de52195e0fa1d8b83a9b212a2498b2", - "sha256:a7d6bef0775f66ba47f25911d285bcd692ce9053837ff48a120c2b8cf3a71389" + "sha256:9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", + "sha256:c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245" ], - "version": "==3.0.4" + "version": "==3.0.5" }, "isort": { "hashes": [ @@ -400,26 +401,29 @@ }, "lazy-object-proxy": { "hashes": [ - "sha256:02b260c8deb80db09325b99edf62ae344ce9bc64d68b7a634410b8e9a568edbf", - "sha256:18f9c401083a4ba6e162355873f906315332ea7035803d0fd8166051e3d402e3", - "sha256:1f2c6209a8917c525c1e2b55a716135ca4658a3042b5122d4e3413a4030c26ce", - "sha256:2f06d97f0ca0f414f6b707c974aaf8829c2292c1c497642f63824119d770226f", - "sha256:616c94f8176808f4018b39f9638080ed86f96b55370b5a9463b2ee5c926f6c5f", - "sha256:63b91e30ef47ef68a30f0c3c278fbfe9822319c15f34b7538a829515b84ca2a0", - "sha256:77b454f03860b844f758c5d5c6e5f18d27de899a3db367f4af06bec2e6013a8e", - "sha256:83fe27ba321e4cfac466178606147d3c0aa18e8087507caec78ed5a966a64905", - "sha256:84742532d39f72df959d237912344d8a1764c2d03fe58beba96a87bfa11a76d8", - "sha256:874ebf3caaf55a020aeb08acead813baf5a305927a71ce88c9377970fe7ad3c2", - "sha256:9f5caf2c7436d44f3cec97c2fa7791f8a675170badbfa86e1992ca1b84c37009", - "sha256:a0c8758d01fcdfe7ae8e4b4017b13552efa7f1197dd7358dc9da0576f9d0328a", - "sha256:a4def978d9d28cda2d960c279318d46b327632686d82b4917516c36d4c274512", - "sha256:ad4f4be843dace866af5fc142509e9b9817ca0c59342fdb176ab6ad552c927f5", - "sha256:ae33dd198f772f714420c5ab698ff05ff900150486c648d29951e9c70694338e", - "sha256:b4a2b782b8a8c5522ad35c93e04d60e2ba7f7dcb9271ec8e8c3e08239be6c7b4", - "sha256:c462eb33f6abca3b34cdedbe84d761f31a60b814e173b98ede3c81bb48967c4f", - "sha256:fd135b8d35dfdcdb984828c84d695937e58cc5f49e1c854eb311c4d6aa03f4f1" - ], - "version": "==1.4.2" + "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", + "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", + "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", + "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", + "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", + "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", + "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", + "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", + "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", + "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", + "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", + "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", + "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", + "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", + "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", + "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", + "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", + "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", + "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", + "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", + "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" + ], + "version": "==1.4.3" }, "mccabe": { "hashes": [ @@ -451,11 +455,11 @@ }, "pylint": { "hashes": [ - "sha256:7b76045426c650d2b0f02fc47c14d7934d17898779da95288a74c2a7ec440702", - "sha256:856476331f3e26598017290fd65bebe81c960e806776f324093a46b76fb2d1c0" + "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", + "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" ], "index": "pypi", - "version": "==2.4.3" + "version": "==2.4.4" }, "pyyaml": { "hashes": [ @@ -477,10 +481,10 @@ }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" }, "smmap2": { "hashes": [ diff --git a/README.md b/README.md index a177ebfe21..74e4070f2d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
diff --git a/bot.py b/bot.py index 8a0093246e..b574508cc5 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.0" +__version__ = "3.3.1" import asyncio import logging From f5ec6a1c3199a7c7fcfb99ba79a9cdf7419805d5 Mon Sep 17 00:00:00 2001 From: Matrix <49132750+matrix2113@users.noreply.github.com> Date: Mon, 18 Nov 2019 22:28:15 -0500 Subject: [PATCH 0498/1221] RUN TAKI (#643) --- requirements.min.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 48e801d688ff433199e8aa8b8553a3f0b2488d58 Mon Sep 17 00:00:00 2001 From: DAzVise Date: Tue, 19 Nov 2019 18:31:26 +0300 Subject: [PATCH 0499/1221] 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 0500/1221] 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 0501/1221] 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 0502/1221] 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 1d016e895b6f840b4bde0b881fd4f5ea79ec7267 Mon Sep 17 00:00:00 2001 From: Piyush Bhangale Date: Thu, 28 Nov 2019 13:24:25 +0530 Subject: [PATCH 0503/1221] add anti-steal-close plugin (#652) --- plugins/registry.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 21faab7844..7f536487cc 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -26,6 +26,14 @@ "icon_url": "https://i.imgur.com/67bEi82.png", "thumbnail_url": "https://i.imgur.com/67bEi82.png" }, + "anti-steal-close": { + "repository": "officialpiyush/modmail-plugins", + "branch": "master", + "description": "Don't let anyone steal ya close.", + "title": "Anti Steal Close", + "icon_url": "https://i.imgur.com/LovxyV3.png", + "thumbnail_url": "https://i.imgur.com/LovxyV3.png" + }, "embedder": { "repository": "papiersnipper/modmail-plugins", "branch": "master", @@ -177,6 +185,6 @@ "bot_version": "2.20.1", "title": "Server Stats", "icon_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png", - "thumbnail_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png" - } + "thumbnail_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png" + } } 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 0504/1221] 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 0505/1221] 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 0506/1221] 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 0507/1221] 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 0508/1221] ... --- 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 0509/1221] 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 0510/1221] 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 0511/1221] 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 0512/1221] 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 0513/1221] 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 de922213428f771ec2d27dc9a8b52d2997f2fdc0 Mon Sep 17 00:00:00 2001 From: Piyush Bhangale Date: Sun, 8 Dec 2019 20:53:52 +0530 Subject: [PATCH 0514/1221] Add giveaway plugin (#2749) * add anti-steal-close plugin * feat(plugins): add giveaway plugin * if this fixes the conflict * feat(plugins): add Giveaway plugin --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 7f536487cc..1c268d89be 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -88,6 +88,15 @@ "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, + "giveaway": { + "repository": "officialpiyush/modmail-plugins", + "branch": "master", + "description": "Host giveaways on your server", + "bot_version": "2.20.1", + "title": "\uD83C\uDF89 Giveaway Plugin \uD83C\uDF89", + "icon_url": "https://i.imgur.com/qk85xdi.png", + "thumbnail_url": "https://i.imgur.com/gUHB91v.png" + }, "hastebin": { "repository": "officialpiyush/modmail-plugins", "branch": "master", From a26cd4e583ccf2061029a16a09f5cf1f8ff118d3 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 9 Dec 2019 01:06:26 -0800 Subject: [PATCH 0515/1221] v3.3.2, fix perm bug --- .github/workflows/lints.yml | 18 +++++++++++++----- .gitignore | 4 +++- CHANGELOG.md | 6 ++++++ bot.py | 2 +- core/checks.py | 3 ++- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 14a9447ab8..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: @@ -26,7 +33,8 @@ 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 + continue-on-error: true + - 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/.gitignore b/.gitignore index a7fb960a6b..463016ca95 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,7 @@ celerybeat-schedule .venv env/ venv/ +venv2/ ENV/ env.bak/ venv.bak/ @@ -134,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 diff --git a/CHANGELOG.md b/CHANGELOG.md index a2236ef4a6..5f9ef96371 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ 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 + +### Fixed + +- An oversight with the permission system. + # v3.3.1 ### Emergency Patch diff --git a/bot.py b/bot.py index b574508cc5..b7faf32673 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.3.1" +__version__ = "3.3.2" import asyncio import logging diff --git a/core/checks.py b/core/checks.py index 0a5a43e1bb..94f2c40904 100644 --- a/core/checks.py +++ b/core/checks.py @@ -52,8 +52,9 @@ async def check_permissions(ctx, command_name) -> bool: if ( permission_level is not PermissionLevel.OWNER and ctx.channel.permissions_for(ctx.author).administrator + and ctx.guild == ctx.bot.modmail_guild ): - # Administrators have permission to all non-owner commands + # Administrators have permission to all non-owner commands in the Modmail Guild logger.debug("Allowed due to administrator.") return True 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 0516/1221] 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 0517/1221] 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 0518/1221] 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 0519/1221] 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 0520/1221] 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 0521/1221] 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 0522/1221] 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" From 957a711d248a86493fd6612980d042aff1b99b74 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 19 Dec 2019 00:00:31 +0800 Subject: [PATCH 0523/1221] v3.4.1, mask errors --- CHANGELOG.md | 15 +++++++++++++- bot.py | 32 +++++++++++++++++++++-------- cogs/modmail.py | 5 +++-- core/thread.py | 54 ++++++++++++++++++++++++++++--------------------- pyproject.toml | 2 +- 5 files changed, 73 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e9d761f2..5d0929f316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +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.4.0 +# v3.4.1 + +### Fixed +- Masked a bunch of noise errors when deleting messages. +- Added more checks for deleting messages. + +### Breaking + +- `thread_initiate` is now dispatched at the beginning of the setup process. +- `thread_create` is dispatched when the thread is registered as a thread by Modmail (ie. when channel topic is edited). +- `thread_ready` is dispatched when a thread finishes its setup steps. + + +# v3.4.0 ### Added diff --git a/bot.py b/bot.py index b5ccdaccd5..694f860cf1 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.4.0" +__version__ = "3.4.1" import asyncio @@ -1069,19 +1069,32 @@ 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): - thread = await self.threads.find(channel=message.channel) + if isinstance(message.channel, discord.DMChannel): + thread = await self.threads.find(recipient=message.author) + if not thread: + return try: - await thread.delete_message(message) + message = await thread.find_linked_message_from_dm(message) except ValueError as e: - if str(e) not in {"DM message not found.", " Malformed thread message."}: + if str(e) != "Thread channel message not found.": 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) + return embed = message.embeds[0] embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=embed.footer.icon_url) await message.edit(embed=embed) + return + + thread = await self.threads.find(channel=message.channel) + if not thread: + return + try: + await thread.delete_message(message, note=False) + 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) + return + except discord.NotFound: + return async def on_bulk_message_delete(self, messages): await discord.utils.async_all(self.on_message_delete(msg) for msg in messages) @@ -1094,6 +1107,9 @@ async def on_message_edit(self, before, after): if isinstance(after.channel, discord.DMChannel): thread = await self.threads.find(recipient=before.author) + if not thread: + return + try: await thread.edit_dm_message(after, after.content) except ValueError: diff --git a/cogs/modmail.py b/cogs/modmail.py index 6531e61e70..7d36cecb62 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1150,8 +1150,9 @@ async def delete(self, ctx, message_id: int = None): thread = ctx.thread try: - await thread.delete_message(message_id) - except ValueError: + await thread.delete_message(message_id, note=True) + except ValueError as e: + logger.warning("Failed to delete message: %s.", e) return await ctx.send( embed=discord.Embed( title="Failed", diff --git a/core/thread.py b/core/thread.py index 0926a0acd0..960dafe0f7 100644 --- a/core/thread.py +++ b/core/thread.py @@ -72,15 +72,13 @@ def ready(self) -> bool: def ready(self, flag: bool): if flag: self._ready_event.set() - self.bot.dispatch("thread_ready", self) + self.bot.dispatch("thread_create", self) else: self._ready_event.clear() async def setup(self, *, creator=None, category=None): """Create the thread channel and other io related initialisation tasks""" - - self.bot.dispatch("thread_create", self) - + self.bot.dispatch("thread_initiate", self) recipient = self.recipient # in case it creates a channel outside of category @@ -175,6 +173,7 @@ async def send_recipient_genesis_message(): await self.bot.add_reaction(msg, close_emoji) await asyncio.gather(send_genesis_message(), send_recipient_genesis_message()) + self.bot.dispatch("thread_ready", self) def _format_info_embed(self, user, log_url, log_count, color): """Get information about a member of a server @@ -457,9 +456,14 @@ async def find_linked_messages( message_id: typing.Optional[int] = None, either_direction: bool = False, message1: discord.Message = None, + note: bool = True, ) -> typing.Tuple[discord.Message, typing.Optional[discord.Message]]: if message1 is not None: - if not message1.embeds or not message1.embeds[0].author.url: + if ( + not message1.embeds + or not message1.embeds[0].author.url + or message1.author != self.bot.user + ): raise ValueError("Malformed thread message.") elif message_id is not None: @@ -469,13 +473,18 @@ async def find_linked_messages( raise ValueError("Thread message not found.") if not ( - message1.embeds and message1.embeds[0].author.url and message1.embeds[0].color + message1.embeds + and message1.embeds[0].author.url + and message1.embeds[0].color + and message1.author == self.bot.user ): raise ValueError("Thread message not found.") if message1.embeds[0].color.value == self.bot.main_color and message1.embeds[ 0 ].author.name.startswith("Note"): + if not note: + raise ValueError("Thread message not found.") return message1, None if message1.embeds[0].color.value != self.bot.mod_color and not ( @@ -495,6 +504,8 @@ async def find_linked_messages( and message1.embeds[0].color.value == self.bot.recipient_color ) ) + and message1.embeds[0].author.url.split("#")[-1].isdigit() + and message1.author == self.bot.user ): break else: @@ -516,7 +527,7 @@ async def find_linked_messages( if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id: return message1, msg except ValueError: - raise ValueError("DM message not found.") + continue raise ValueError("DM message not found.") async def edit_message(self, message_id: typing.Optional[int], message: str) -> None: @@ -537,16 +548,13 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> await asyncio.gather(*tasks) - async def delete_message(self, message: typing.Union[int, discord.Message] = None) -> None: - try: - 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 - + async def delete_message( + self, message: typing.Union[int, discord.Message] = None, note: bool = True + ) -> None: + if isinstance(message, discord.Message): + message1, message2 = await self.find_linked_messages(message1=message, note=note) + else: + message1, message2 = await self.find_linked_messages(message, note=note) tasks = [] if not isinstance(message, discord.Message): tasks += [message1.delete()] @@ -571,12 +579,12 @@ async def find_linked_message_from_dm(self, message, either_direction=False): return linked_message msg_id = url.split("#")[-1] - try: - if int(msg_id) == message.id: - return linked_message - except ValueError: - raise ValueError("Malformed dm channel message.") - raise ValueError("DM channel message not found.") + if not msg_id.isdigit(): + continue + msg_id = int(msg_id) + if int(msg_id) == message.id: + return linked_message + raise ValueError("Thread channel message not found.") async def edit_dm_message(self, message: discord.Message, content: str) -> None: try: diff --git a/pyproject.toml b/pyproject.toml index e9c402661d..f5af2276c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.4.0' +version = '3.4.1' 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 be598f56193cd89c35311043a8ce2055d11d872e Mon Sep 17 00:00:00 2001 From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com> Date: Mon, 6 Jan 2020 18:22:09 +0600 Subject: [PATCH 0524/1221] Bump to Python 3.7.6 --- runtime.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime.txt b/runtime.txt index aefcfbece7..6919bf9ede 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.5 +python-3.7.6 From b0189fc0e0557c28b1df17be1f57c673b13deeb9 Mon Sep 17 00:00:00 2001 From: KarateWumpus <48181821+KarateWumpus@users.noreply.github.com> Date: Wed, 8 Jan 2020 19:04:11 +0100 Subject: [PATCH 0525/1221] New name, same me --- plugins/registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 1c268d89be..3722b6d781 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -170,7 +170,7 @@ "thumbnail_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png" }, "stats": { - "repository": "MiTonder/modmail-plugins", + "repository": "KarateWumpus/modmail-plugins", "branch": "master", "description": "Get useful stats directly in an embed about either the Modmail bot, a user or the server.", "bot_version": "2.24.1", From 1f671ec54cdadab9f34c23ad97165f128e4d59bb Mon Sep 17 00:00:00 2001 From: DAzVise <52792999+DAzVise@users.noreply.github.com> Date: Thu, 9 Jan 2020 09:21:42 +0300 Subject: [PATCH 0526/1221] Edit plugin description --- plugins/registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 3722b6d781..00518c1c65 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -190,7 +190,7 @@ "serverstats": { "repository": "dazvise/modmail-plugins", "branch": "master", - "description": "Interesting and accurate statistics about your server.", + "description": "Voice channels containing interesting and accurate statistics about your server such as Member Count.", "bot_version": "2.20.1", "title": "Server Stats", "icon_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png", From b2f4a40325a31a04940a8044bfbc44c8c790067f Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Thu, 9 Jan 2020 00:39:17 -0800 Subject: [PATCH 0527/1221] Update README.md --- README.md | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d162f03bc3..ec28a420b7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
@@ -31,10 +31,6 @@ Made with Python 3.7 - - - - @@ -147,7 +143,7 @@ You can build your own Docker image: $ docker build . --tag=modmail ``` -or run directly from a pre-built version from https://hub.docker.com/. Currently there are two community release of Modmail: +or run directly from a pre-built version from https://hub.docker.com/. - Kyber's: @@ -155,21 +151,11 @@ or run directly from a pre-built version from https://hub.docker.com/. Currently $ docker pull kyb3rr/modmail ``` -- Taku's: - -```console -$ docker pull taaku18/modmail -# You can also choose one of the following: -$ docker pull taaku18/modmail:dev -$ docker pull taaku18/modmail: ( ex: 3.2.0, 3.2, etc.) -``` - And to run your docker image: ```console -$ docker run --env-file .env user/modmail +$ docker run --env-file .env kyb3rr/modmail ``` -- Replace `user/modmail` with `kyb3rr/modmail`, `taaku18/modmail`, `taaku18/modmail:3.2`, etc as above. - `.env` should be the path to your env file, you can also supply a path: `/path/to/.env`. ## Sponsors From e7771f02f4b825040367bd6603ddfd7a42e02459 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Thu, 9 Jan 2020 01:10:25 -0800 Subject: [PATCH 0528/1221] Update feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 48b986344d..7cd7a506cd 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: Suggest an idea for this project -title: "[FEATURE-REQUEST] your title here" +title: "your title here" labels: feature-request assignees: '' From 00e92ae65a6dbc26ece1c42b245dd93ae2529830 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Thu, 9 Jan 2020 01:10:47 -0800 Subject: [PATCH 0529/1221] Update command-request.md --- .github/ISSUE_TEMPLATE/command-request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/command-request.md b/.github/ISSUE_TEMPLATE/command-request.md index 44902f2cf3..d3c1673a6b 100644 --- a/.github/ISSUE_TEMPLATE/command-request.md +++ b/.github/ISSUE_TEMPLATE/command-request.md @@ -1,7 +1,7 @@ --- name: Command request about: Request a new command -title: "[COMMAND-REQUEST] your title here" +title: "your title here" labels: command-request assignees: '' From cac233bee91f0675772138413c45ae1edc10e53d Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 9 Jan 2020 09:33:04 -0800 Subject: [PATCH 0530/1221] Spell check --- CHANGELOG.md | 252 ++++++++++++++++++++++++------------------------ CONTRIBUTING.md | 17 ++-- README.md | 28 +++--- pyproject.toml | 4 +- 4 files changed, 150 insertions(+), 151 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d0929f316..0dcb9acbcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); -however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). +however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. # v3.4.1 @@ -16,8 +16,8 @@ however, insignificant breaking changes does not guarantee a major version bump, ### Breaking -- `thread_initiate` is now dispatched at the beginning of the setup process. -- `thread_create` is dispatched when the thread is registered as a thread by Modmail (ie. when channel topic is edited). +- `thread_initiate` will be dispatched at the beginning of the setup process. +- `thread_create` is dispatched when the thread is registered as a thread by Modmail (i.e., when channel topic is edited). - `thread_ready` is dispatched when a thread finishes its setup steps. @@ -36,35 +36,35 @@ 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). -- Recipient get feedback when they edit message. +- Recipients get feedback when they edit their messages. - Chained delete for DMs now comes with a message. - poetry (in case someone needs it). ### Changed - The look of alias and snippet when previewing. -- Message ID of the thread embed is saved in DB, instead of the original message. +- The database now saves the message ID of the thread embed, 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. +- A hard limit of a maximum of 25 steps for aliases. - `?disable` is now `?disable new`. ### 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. +- Fixed many 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 add / remove. +- Chained reaction add/remove. - Chained delete for thread channels. ### Internal -- Commit to black format line width max = 99, consistent with pylint. -- Alias parser is rewritten without shlex. +- Commit to black format line width max = 99, consistent with PyLint. +- No longer requires shlex for alias parsing. - New checks with thread create / find. -- No more flake8 and travis. +- No more flake8 and Travis. # v3.3.2 @@ -88,19 +88,19 @@ however, insignificant breaking changes does not guarantee a major version bump, - Three new config vars: - `enable_plugins` (yes/no default yes) - - When set to no, plugins will not be loaded into the bot. + - When set to no, Modmail will not load plugins. - `error_color` (color format, defaults discord red) - The color of error messages. - `anon_reply_without_command` (yes/no default no) (Thanks to papiersnipper PR#288) - When set, all non-command messages sent to thread channels are forwarded to the recipient anonymously without the need of `?anonreply`. - This config takes precedence over `reply_without_command`. -- `?logs responded [user]` command, it will show all logs that the user has sent an reply. (Thanks to papiersnipper PR#288) +- `?logs responded [user]` command. It will show all the logs that the user has sent a reply. (Thanks to papiersnipper PR#288) - `user` when not provided, defaults to the user who ran the command. -- Open threads in limbo now auto closes if the channel cannot be found. This check is done every time the bot restarts. +- Open threads in limbo now auto-close if Modmail cannot find the channel. Modmail does this check every time the bot restarts. - Ability to disable new threads from getting created. - - `?disable` + - `?disable`. - Ability to fully disable Modmail DM. - - `?disable all` + - `?disable all`. - To re-enable DM: `?enable`, and to see the current status: `?isenable`. - This disabled Modmail interface is customizable with the following config vars: - `disabled_new_thread_title` @@ -114,33 +114,33 @@ however, insignificant breaking changes does not guarantee a major version bump, ### Changed -- `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) +- `?contact` no longer send the "thread created" message to where the command was run, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) - Automatically delete notes command `?note` when there're no attachments attached. - Embed author links used to be inaccessible in many cases, now: - `?anonreply`, `?reply`, and `?note` in the thread channel will link to the sender's profile. - - `?reply` and recipient's DM will also link the sender's profile. + - `?reply` and the recipient's DM will also link the sender's profile. - `?anonreply` in DM channel will link to the first channel of the main guild. - Plugins update (mostly internal). - - `git` is no longer used to install plugins, it now downloads through zip files. + - `git` is no longer used to install plugins; it now downloads through zip files. - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. - Reorganized plugins folder structure. - Logging / plugin-related messages change. - - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). -- Help command is in alphabetical order grouped by permissions. -- Notes are no longer always blurple, it's set to `MAIN_COLOR` now. + - Updating one plugin will not update other plugins; repositories no longer separate plugins, but the plugin name itself. +- The help command is in alphabetical order grouped by permissions. +- Notes are no longer always blurple; it's set to `MAIN_COLOR` now. - Added `?plugins update` for updating all installed plugins. - Reintroduce flake8 and use bandit for security issues detection. -- Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. -- Eval commands are logged in debug logs. +- Add Travis checks for 3.6 in Linux and 3.7 for macOS and Windows. +- Debug logs not logs eval commands. - Presence updates 30 minutes instead of 45 now. -- Fixed an assortment of problems to do with block. +- Fixed an assortment of problems to do with `?block`. - Existing aliases can be used when creating new aliases. (Thanks to papiersnipper PR#402) ### Internal - Reworked `config.get` and `config.set`, it feeds through the converters before setting/getting. - To get/set the raw value, access through `config[]`. -- Prerelease naming scheme is now `x.x.x-devN`. +- The prerelease naming scheme is now `x.x.x-devN`. - `trigger_typing` has been moved to `core.utils.trigger_typing`, the original location is deprecated. - Simpler status and activity logic. - New logging logic. @@ -153,9 +153,9 @@ Security update! - Supporter permission users used to be able to "hack" snippets to reveal all your config vars, including your token and MongoURI. - Implemented some changes to address this bug: - - All customizable variables used in snippets, close messages, etc, using the `{}` syntax, now forbids chaining 2 or more attributes and attributes that starts with `_`. -- It is advised to update to this version. -- If you felt your credentials have been leaked, consider changing your bot token / mongo uri. + - All customizable variables used in snippets, close messages, etc., using the `{}` syntax, now forbids chaining two or more attributes and attributes that start with `_`. +- We advise you to update to this version. +- If you felt your credentials had been leaked, consider changing your bot token / MongoURI. # v3.2.1 @@ -185,7 +185,7 @@ Security update! ### Internal -- Use regex to parse Changes, Added, Fixed, etc and description. +- Use regex to parse Changes, Added, Fixed, etc. and description. - Adds `PermissionLevel.INVALID` when commands don't have a permission level. # v3.1.1 @@ -205,33 +205,33 @@ Security update! ### Added - `?sfw`, mark a thread as "safe for work", undos `?nsfw`. -- New config variable, `thread_auto_close_silently`, when set to a truthy value, no message will be sent when thread is auto-closed. +- New config variable, `thread_auto_close_silently`, when set to a truthy value, no message will be sent when a thread is auto-closed. - New configuration variable `thread_self_closable_creation_footer` — the footer when `recipient_thread_close` is enabled. - Added a minimalistic version of requirements.txt (named requirements.min.txt) that contains only the absolute minimum of Modmail. - For users having trouble with pipenv or any other reason. -- Multi-step alias, see `?help alias add`. Public beta testing, might be unstable. +- Multi-step alias, see `?help alias add`. Public beta testing might be unstable. - Misc commands without cogs are now displayed in `?help`. - `?help` works for alias and snippets. - `?config help ` shows a help embed for the configuration. -- Support setting permissions for sub commands. -- Support numbers (1-5) as substitutes for Permission Level REGULAR - OWNER in `?perms` sub commands. +- Support setting permissions for subcommands. +- Support numbers (1-5) as substitutes for Permission Level REGULAR - OWNER in `?perms` subcommands. ### Changes - `thread_auto_close_response` has a configurable variable `{timeout}`. - `?snippet` is now the default command name instead of `?snippets` (`?snippets` is still usable). This is to make this consistent with `?alias`/`?aliases`. -- `colorama` is no longer a necessity, this is due to some unsupported OS. -- Changelog command can now take a version argument to jump straight to specified version. +- `colorama` is no longer a necessity; this is due to some unsupported OS. +- Changelog command can now take a version argument to jump straight to the specified version. - `?plugin enabled` results are now sorted alphabetically. -- `?plugin registry` results are now sorted alphabetically, helps user find plugins more easily. +- `?plugin registry` results are now sorted alphabetically, helps users find plugins more easily. - `?plugin registry page-number` plugin registry can specify a page number for quick access. - A reworked interface for `?snippet` and `?alias`. - Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown). - - Add an `?alias raw ` command for viewing the raw content of a alias (escaped markdown). + - Add an `?alias raw ` command for displaying the raw content of an alias (escaped markdown). - The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/. - Removed unclear `rm` alias for some `remove` commands. - Paginate `?config options`. -- All users configured with a permission level greater than REGULAR has access to the main Modmail category. +- All users configured with a permission level higher than REGULAR has access to the main Modmail category. - Category overrides also changes when a level is removed or added to a user or role. - `@everyone` is now accepted for `?perms add`. @@ -239,12 +239,12 @@ Security update! - `?notify` no longer carries over to the next thread. - `discord.NotFound` errors for `on_raw_reaction_add`. -- `mod_typing` ~~and `user_typing`~~ (`user_typing` is now by-design to show) will no longer show when user is blocked. +- `mod_typing` ~~and `user_typing`~~ (`user_typing` is now by-design to show) will no longer show when the user is blocked. - Better `?block` usage message. -- Resolves errors when message was sent by mods after thread is closed somehow. +- Resolved errors when mods sent messages after a thread is closed somehow. - Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`. -- When creating snippets and aliases, it now checks if another snippets/aliases with the same name exists. -- Was looking for `config.json` in the wrong directory. +- When creating snippets and aliases, it now checks if other snippets/aliases with the same name exist. +- Modmail looked for `config.json` in the wrong directory. ### Internal @@ -264,13 +264,13 @@ Security update! ### Added - New commands, `?alias edit ` and `?snippets edit `. - - They can be used to edit aliases and snippets respectively. + - They can be used to edit aliases and snippets, respectively. # v3.0.2 ### Added -- New command, `?blocked whitelist `, this command prevents users from getting blocked by any means. +- A new command, `?blocked whitelist `, this command prevents users from getting blocked by any means. ### Changed @@ -280,30 +280,30 @@ Security update! ### Fixed -- A lot of bugs with `thread_auto_close` 😅 +- Many bugs with `thread_auto_close`. # v3.0.0 ### Added -- Sponsors command that will list sponsors. -- An alert will now be sent to the log channel if a thread channel fails to create. This could be due to a variety of problems such as insufficient permissions or the category channel limit is met. +- `?sponsors` command will list sponsors. +- An alert will now be sent to the log channel if a thread channel fails to create. This could be due to a variety of problems such as insufficient permissions, or the category channel limit is met. - Threads will close automatically after some time when `thread_auto_close` is set. -- Custom closing message can be set with `thread_auto_close_response`. +- Custom closing messages can be configured with `thread_auto_close_response`. ### Breaking Changes -- Removed autoupdate functionality and the `?update` command in favour of the [Pull app](https://github.com/apps/pull). +- Removed auto-update functionality and the `?update` command in favor of the [Pull app](https://github.com/apps/pull). Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/updating) ### Changed -- Channel names now can contain unicode characters. -- Debug logs are now located in a unique file for each bot. (Internal change) +- Channel names now can contain Unicode characters. +- Debug logs are now located in a different file for each bot. (Internal change) - Default cogs always appear first in the help command now. ### Fixed -- Editing notes now works, minor bug with edit command is fixed. +- Editing notes now work, minor bug with edit command is fixed. - Bug in the `?oauth` command where the response message fails to send when an ID is provided. - Plugin requirement installation now works in virtual environments @@ -318,7 +318,7 @@ Fixed a bug with branches and `?plugin update`. ### Added -Branch support for `?plugin add` and in registry. Typically for developers. +Branch support for `?plugin add` and in the registry. Typically for developers. # v2.23.0 @@ -326,11 +326,11 @@ Branch support for `?plugin add` and in registry. Typically for developers. Added a "Mutual servers" field to the genesis embed if: a) The user is not in the main guild. -b) The user shares more than 1 server with the bot. +b) The user shares more than one server with the bot. ### Changed -Notes taken with the `?note` command are now automatically pinned within the thread channel. +Notes with the `?note` command are now automatically pinned within the thread channel. # v2.22.0 @@ -372,14 +372,14 @@ Add your plugin in the `plugins/registry.json` file in the main repository. This update contains mostly internal changes. - Implemented support for the new discord.py v1.1.1. - Improved help text for most commands. - - Completely revamped help command, few user changes. - - Removed abc (internal). + - Completely revamped help command, few users changes. + - Removed ABC (internal). # v2.20.0 ### What's new? -New `oauth` whitelist command which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app check the logviewer [repo](https://github.com/kyb3r/logviewer). +New `?oauth whitelist` command, which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app, check the logviewer [repo](https://github.com/kyb3r/logviewer). # v2.19.1 @@ -387,7 +387,7 @@ New `oauth` whitelist command which allows you to whitelist users so they can lo - Ability to force an update despite having the same version number. Helpful to keep up-to-date with the latest GitHub commit. - `?update force`. -- Plugin developers now have a new event called `on_plugin_ready`, this is coroutine is awaited when all plugins are loaded. Use `on_plugin_ready` instead of `on_ready` since `on_ready` will not get called in plugins. +- Plugin developers now have a new event called `on_plugin_ready`; this is a coroutine and is awaited when all plugins are loaded. Use `on_plugin_ready` instead of `on_ready` since `on_ready` will not get called in plugins. # v2.19.0 @@ -408,17 +408,17 @@ Fix the teams permission bug. ### Changed -Commands now have better error messages, instead of just sending the help message for a command when an argument fails to be converted to its specified object, the bot now says things like "User 'bob' not found" instead. +Commands now have better error messages. Instead of sending the help message for a command when an argument fails to be converted, the bot now says like "User 'bob' not found" instead. # v2.18.1 -Un-deprecated the `OWNERS` config variable to support discord developer team accounts. +Un-deprecated the `OWNERS` config variable to support Discord developer team accounts. # v2.18.0 ### New Permissions System -- A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. +- A brand new permission system! Replaced the old guild-based permissions (i.e., manage channels, manage messages), with the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. - There are five permission levels: - Owner [5] - Administrator [4] @@ -437,7 +437,7 @@ You may add a role or user to a permission group through any of the following me The same applies to individual commands permissions: - `?permissions add command command-name @member#1234` -- ... and the other methods listed above. +- and the other methods listed above. To revoke permission, use `remove` instead of `add`. @@ -450,11 +450,11 @@ By default, all newly set up Modmail will have `OWNER` set to the owner of the b ### Breaking When updating to this version, all prior permission settings with guild-based permissions will be invalidated. You will need to convert to the above system. -`OWNERS` will also get removed, you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. +`OWNERS` will also get removed; you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. ### New Command -- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer requires manage messages permission to recall thread messages. +- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer require "manage messages" permission to recall thread messages. ### Changed @@ -475,28 +475,28 @@ Stricter fallback genesis embed search. ### Changed -How modmail checks if a channel is a thread: +How Modmail checks if a channel is a thread: -1. The bot first checks if the channel topic is in the format `User ID: xxxx`, this means it is a thread. -2. If a channel topic is not found, the bot searches through the message history of a channel to find the thread creation embed. This step should never yield a thread for a normal user, but in the case of another bot messing up the channel topic (happened to a user before) this extra step was added. +1. The bot first checks if the channel topic is in the format `User ID: XXXX`, this means it is a thread. +2. If a channel topic is not found, the bot searches through the message history of a channel to find the thread creation embed. This step should never yield a thread for an average user. Still, in the case of another bot messing up the channel topic (happened to a user before), this extra step was added. # v2.17.0 ### What's new? -Added a config option `reply_without_command` which when present, enables the bot to forward any message sent in a thread channel to the recipient. (Replying without using a command) +Added a config option `reply_without_command`, which, when present, enables the bot to forward any message sent in a thread channel to the recipient. (Replying without using a command) To enable this functionality, do `?config set reply_without_command true` and to disable it, use `?config del reply_without_command`. ### Changed -The `move` command now only requires `manage_messages` perms instead of `manage_channels` +The `move` command now only requires `manage_messages` perms instead of `manage_channels`. # v2.16.1 ### Fixed -An issue where a scheduled close would not execute over a long period of time if the recipient no shares any servers with the bot. +An issue where a scheduled close would not execute over a long time if the recipient no shares any servers with the bot. # v2.16.0 @@ -526,8 +526,8 @@ Added the ability to change the default close message via the introduction of tw They will be provided by string variables that you can incorporate into them: - `closer` - the user object that closed the thread. -- `logkey` - the key for the thread logs e.g. (`5219ccc82ad4`) -- `loglink` - the full link to the thread logs e.g. (`https://logwebsite.com/logs/5219ccc82ad4`) +- `logkey` - the key for the thread logs, e.g. (`5219ccc82ad4`) +- `loglink` - the full link to the thread logs, e.g. (`https://logwebsite.com/logs/5219ccc82ad4`) Example usage would be: ``?config set thread_close_message {closer.mention} closed the thread, here is the link to your logs: [**`{logkey}`**]({loglink})`` @@ -556,9 +556,9 @@ You now have complete control of the look of the thread creation and close embed ### What's new? -Added the ability to disable the `sent_emoji` and `blocked_emoji` when a user messages modmail. +Added the ability to disable the `sent_emoji` and `blocked_emoji` when a user messages Modmail. -You can do this via `?config set sent_emoji disable` +You can do this via `?config set sent_emoji disable`. ### Fixed @@ -575,17 +575,17 @@ Added image link in title in case discord fails to embed an image. - Introduced a new configuration variable `account_age` for setting a minimum account creation age. - Users blocked by this reason will be stored in `blocked` along with other reasons for being blocked. - `account_age` needs to be an ISO-8601 Duration Format (examples: `P12DT3H` 12 days and 3 hours, `P3Y5M` 3 years and 5 months `PT4H14M999S` 4 hours 14 minutes and 999 seconds). https://en.wikipedia.org/wiki/ISO_8601#Durations. - - You can set `account_age` using `config set account_age time` where "time" can be a simple human readable time string or an ISO-8601 Duration Format string. + - You can set `account_age` using `config set account_age time` where "time" can be a simple human-readable time string or an ISO-8601 Duration Format string. ### Changed -- `block` reason cannot start with `System Message: ` as it is now reserved for internal user blocking. -- `block`, like `close`, now supports a block duration (temp blocking). +- `?block` reason cannot start with `System Message: ` as it is now reserved for internal user blocking. +- `?block`, like `?close`, now supports a block duration (temp blocking). # v2.13.10 ### Fixed - Fixed an issue where status and activity do not work if they were modified wrongly in the database. - - This was especially an issue for older Modmail users, as the old `status` configuration variable clashes with the new `status` variable. + - This was primarily an issue for older Modmail users, as the old `status` configuration variable clashes with the new `status` variable. # v2.13.9 @@ -604,7 +604,7 @@ Added image link in title in case discord fails to embed an image. ### What's new? - The ability to enable typing interactions. - - If you want the bot to type in the thread channel if the user is also typing, add the config variable `user_typing` and set it to "yes" or "true". use `config del` to disable the functionality. The same thing in reverse is also possible if you want the user to see the bot type when someone is typing in the thread channel add the `mod_typing` config variable. + - If you want the bot to type in the thread channel if the user is also typing, add the config variable `user_typing` and set it to "yes" or "true". Use `config del` to disable the functionality. The same thing in reverse is also possible if you want the user to see the bot type when someone is typing in the thread channel add the `mod_typing` config variable. - New `status` command, change the bot's status to `online`, `idle`, `dnd`, `invisible`, or `offline`. - To remove the status (change it back to default), use `status clear`. - This also introduces a new internal configuration variable: `status`. Possible values are `online`, `idle`, `dnd`, `invisible`, and `offline`. @@ -621,15 +621,15 @@ Added image link in title in case discord fails to embed an image. ### What's new? - You will no longer need to view your bot debug logs from Heroku. `debug` will show you the recent logs within 24h through a series of embeds. - - If you don't mind your data (may or may not be limited to: user ID, guild ID, bot name) be on the internet, `debug hastebin` will upload a formatted logs file to https://hasteb.in. + - If you don't mind your data (may or may not be limited to user ID, guild ID, bot name) be on the internet, `debug hastebin` will upload a formatted logs file to https://hasteb.in. - `debug clear` will clear the locally cached logs. - - Local logs are automatically cleared at least once every 27h for bots hosted on Heroku. + - Local logs are automatically erased at least once every 27h for bots hosted on Heroku. ### Fixed -- Will no longer show `Unclosed client session` and `Task was destroyed but it is pending!` when the bot terminates. +- Will no longer show `Unclosed client session` and `Task was destroyed, but it is pending!` when the bot terminates. - `thread.create` is now synchronous so that the first message sent can be queued to be sent as soon as a thread is created. - This fixes a problem where if multiple messages are sent in quick succession, the first message sent (which triggers the thread creation) is not sent in order. -- Trying to reply to someone who has DMs disabled or has blocked the bot is now handled and the bot will send a message saying so. +- Trying to reply to someone who has DMs disabled or has blocked the bot is now handled, and the bot will send a message saying so. ### Changed - `print` is replaced by logging. @@ -663,14 +663,14 @@ Added image link in title in case discord fails to embed an image. ### What's new? - Plugins: - - Think of it like addons! Anyone (with the skills) can create a plugin, make it public and distribute it. Add a welcome message to Modmail, or moderation commands? It's all up to your imagination! Have a niche feature request that you think only your server would benefit from? Plugins are your go-to! + - Think of it like addons! Anyone (with the skills) can create a plugin, make it public and distribute it. Add a welcome message to Modmail, or moderation commands? It's all up to your imagination! Have a niche feature request that you think only your server would benefit? Plugins are your go-to! - [Creating Plugins Documentation](https://github.com/kyb3r/modmail/wiki/Plugins). # v2.12.5 ### Fixed -- `config del` command will now work properly on self-hosted db bots. +- `config del` command will now work correctly on self-hosted DB bots. # v2.12.4 @@ -684,14 +684,14 @@ Added image link in title in case discord fails to embed an image. ### Fixed - Patched a bug where `logs` sub-commands were accessible by anyone. -- Patched a bug where an error was raised if there was an open thread where the recipient had left the server. +- Patched a bug where an error was raised when a thread is open where the recipient left the server. Huge thanks to Sasiko for reporting these issues. # v2.12.2 ### Fixed -- Fixed a bug in self-hosted `update` command. +- Fixed a bug in self-hosted `?update` command. # v2.12.1 @@ -702,12 +702,12 @@ Huge thanks to Sasiko for reporting these issues. # v2.12.0 ### Important -**In the future, the Modmail API (https://modmail.tk) will be deprecated. This is due to the fact that we are providing a free service without getting anything in return, and thus we do not have the resources to scale to accommodate for more users. +**In the future, the Modmail API (https://modmail.tk) will be deprecated. This is because we are providing free service without getting anything in return. Thus we do not have the resources to scale to accommodate more users. We recommend using your own database for logs. In the future you will soon get a `backup` command so you can download all your pre-existing data and migrate to your own database.** ### Changed - A lot of painful code cleanup, which is good for us (the developers), but shouldn't affect you. -- The appearance of the `logs` command. Should be clearer with better info now. +- The appearance of the `?logs` command. It should be clearer with better info now. - Bot owners get access to all commands regardless of server permissions. - Blocked users no longer receive a message, only the blocked emoji will be sent. @@ -715,10 +715,10 @@ We recommend using your own database for logs. In the future you will soon get a - **Note:** The following commands only work if you are self-hosting your logs. We recommend you to use your own database. - Log search queries, in the form of two new commands. - `logs search [query]` - this searches all log messages for a query string. -- `logs closed-by [user]` this returns all logs closed by a certain user +- `logs closed-by [user]` this returns all logs closed by a particular user ### Fixed -- `activity listening to music` no longer result in two "to"s ("listening to to music"). +- `activity listening to music` no longer results in two "to"s ("listening to to music"). - This may require you to change your activity message to accommodate this fix. - A problem where `main_category_id` and `log_channel_id` weren't updated when their corresponding channel or category get deleted. @@ -741,7 +741,7 @@ We recommend using your own database for logs. In the future you will soon get a ### What's new? - `anonreply` command to anonymously reply to the recipient. -The username of the anonymous user defaults to the `mod_tag` (the footer text of a mod reply message). The avatar defaults the guild icon URL. However you can change both of these via the `anon_username`, `anon_avatar_url` and `anon_tag` config variables. +The username of the anonymous user defaults to the `mod_tag` (the footer text of a mod reply message) — the avatar defaults to the guild icon URL. However, you can change both of these via the `anon_username`, `anon_avatar_url`, and `anon_tag` config variables. ### Changed - Your bot now logs all messages sent in a thread channel, including discussions that take place. You can now toggle to view them in the log viewer app. @@ -766,7 +766,7 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of - All commands are now blurple instead of green. ### Fixed -- Bug where the close command wouldn't work if you didnt configure a log channel. +- Bug where the close command wouldn't work if you didn't configure a log channel. ### What's new? - Ability to set your own custom `mod_color` and `recipient_color` for the thread message embeds. @@ -783,13 +783,13 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of # v2.9.0 ### What's new? -- New command `note` will add a system message to your thread logs. This is useful for noting the context of a conversation. +- New command `note` will add a system message to your thread logs. - - This is useful for noting the context of a conversation. # v2.8.1 ### Fixed - Fixed bug where thread logs were getting duplicated when using the `contact` command. -- Fixed bug where the wrong key was used for logs which caused some `log` command log links to point to an HTTP 404 Not Found. +- Fixed bug where the wrong key was used for logs, which caused some `log` command log links to point to an HTTP 404 Not Found. - A minor oversight from commit 1ba74d9. # v2.8.0 @@ -806,7 +806,7 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of ### Security Thread channels will now default to being private (`@everyone`'s read message perms set to `false`). - If the thread creation category could not be resolved. - - This will save you from some trouble if for whatever reason your configuration gets messed up. + - This will save you from some trouble if, for whatever reason, your configuration gets messed up. # v2.7.1 @@ -818,20 +818,20 @@ Thread channels will now default to being private (`@everyone`'s read message pe ### Note -- If your Modmail bot was set up a long time ago, you may experience an issue where messages were sent outside of the category. +- If your Modmail bot was set up a long time ago, you might experience an issue where messages were sent outside of the category. - To fix this, set `main_category_id` to the ID of the Modmail category. # v2.7.0 ### Changed -- `move` command now syncs thread channel permissions with the category that it was moved to. +- `move` command now syncs thread channel permissions with the destination category. - `contact` command now supports an optional category argument (where the thread channel will be created). # v2.6.3 ### Fixes -- Fixed small issue with finding thread. +- Fixed small issue with finding threads. # v2.6.2 @@ -851,8 +851,8 @@ Thread channels will now default to being private (`@everyone`'s read message pe ### Changed - Log URLs are moved to their own collection. - Log URLs are now `https://logs.modmail.tk/LOGKEY`, no more numbers before the log key. -- We still support the numbers so as to not break everyone's URLs so quickly but both work at the moment. -- This is a huge change to the backend logging and there might be migration errors. If so, please contact us in our [discord server](https://discord.gg/2fMbf2N). +- We still support the numbers to not break everyone's URLs so quickly, but both work at the moment. +- This is a huge change to the backend logging, and there might be migration errors. If so, please contact us in our [Discord server](https://discord.gg/2fMbf2N). # v2.5.2 @@ -862,13 +862,13 @@ Thread channels will now default to being private (`@everyone`'s read message pe # v2.5.1 ### Fixes -- Emergency patch to save config. +- Emergency patch to save configs. # v2.5.0 ### Background - Bots hosted by Heroku restart at least once every 27 hours. -- During this period, local caches are deleted, which results in the inability to set the scheduled close time to longer than 24 hours. This update resolves this issue. +- During this period, local caches will be deleted, which results in the inability to set the scheduled close time to longer than 24 hours. This update resolves this issue. - [PR #135](https://github.com/kyb3r/modmail/pull/135) ### Changed @@ -886,7 +886,7 @@ Fixed activity setting due to flawed logic in `config.get()` function. # v2.4.4 ### Fixed -Fixed a bug in activity command where it would fail to set the activity on bot restart if the activity type was `playing`. +Fixed a bug in the `?activity` command where it would fail to set the activity on bot restart if the activity type was `playing`. # v2.4.3 @@ -901,17 +901,17 @@ Fixed a bug in activity command where it would fail to set the activity on bot r # v2.4.1 ### Fixed -- Small bug in `activity` command. +- Small bug in `?activity` command. # v2.4.0 ### What's new? -- Added the `activity` command for setting the activity -- [PR #131](https://github.com/kyb3r/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening` and `streaming`). +- Added the `?activity` command for setting the activity +- [PR #131](https://github.com/kyb3r/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening`, and `streaming`). ### Removed - Removed the deprecated `status` command. -- This also means you will have to reset your bot status with the `activity` command, as `status` command is removed. +- This also means you will have to reset your bot status with the `?activity` command, as the `?status` command was removed. # v2.3.0 @@ -932,7 +932,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### What's new? - Notify command `notify [role]`. - Notify a given role or yourself to the next thread message received. - - Once a thread message is received you will be pinged once only. + - Once a thread message is received, you will be pinged once only. - Subscribe command `sub [role]` / `unsub [role]`. - Subscribes yourself or a given role to be notified when thread messages are received. @@ -949,15 +949,15 @@ Fixed a bug in activity command where it would fail to set the activity on bot r # v2.1.0 ### What's new? -- Ability to set a custom thread creation response message. +- Ability to set a custom thread-creation-response message. - Via `config set thread_creation_response [message]`. ### Changed -- Improve logs command format. -- Improve thread log channel message to have more relevant info. +- Improve `?logs` command format. +- Improve thread log channel messages to have more relevant info. - Improve close command. - - You now can close the thread after a delay and use a custom thread close message. - - You also now have the ability to close a thread silently. + - You can now close the thread after a delay and use a custom thread close message. + - You also now can close a thread silently. # v2.0.10 @@ -972,7 +972,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Fixes - Support multiple images and file attachments in one message. -- This is only possible on mobile so its good to handle it in code. +- This is only possible on mobile, so its good to handle it in code. # v2.0.8 @@ -983,7 +983,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r - You can do this via the `config set main_category_id ` command. ### Changed -- You now have the ability to supply a reason when blocking a user. +- You can now supply a reason when blocking a user. - Blocked users are now stored in the database instead of in the channel topic. - This means you can delete the top channel in the Modmail category now (after migrating the currently blocked users). @@ -994,8 +994,8 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Changed - `update` command now shows the latest changes directly from CHANGELOG.md. -- Auto update messages also show the latest changes from the GitHub repo. -- Removed "latest changes" section from the `about` command. +- Auto-update messages also show the latest changes from the GitHub repo. +- Removed the "latest changes" section from the `about` command. # v2.0.6 @@ -1008,20 +1008,20 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Changed - `alias` command now checks if you are adding a valid alias-command combo. -- Deleting a channel manually will now correctly close the thread and post logs. +- Manually deleting a channel will now correctly close the thread and post logs. # v2.0.4 ### Fixed -- Fixed a one-off bug where the channel topic disappears, but Modmail operations should still continue. +- Fixed a one-off bug where the channel topic disappears, but Modmail operations should continue. - Fixed `linked_message_id` issues. # v2.0.3 ### Fixed -- Thread creation embed now shows the correct number of past logs. +- The thread creation embed now shows the correct number of past logs. - If using a separate server setup, roles in the info embed now are shown as names instead of mentions. - - This is due to the fact that you can't mention roles across servers. + - This is because you can't mention roles across servers. # v2.0.2 @@ -1033,7 +1033,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Changed - Improved `block` / `unblock` commands. - - They now take a wider range of arguments: usernames, nicknames, mentions and user IDs. + - They now take a more comprehensive range of arguments: usernames, nicknames, mentions, and user IDs. ### Fixed - Setup command now configures permissions correctly so that the bot will always be able to see the main operations category. @@ -1041,7 +1041,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r # v2.0.0 This release introduces the use of our centralized [API service](https://github.com/kyb3r/webserver) to enable dynamic configuration, auto-updates, and thread logs. -To use this release you must acquire an API token from https://modmail.tk. +To use this release, you must acquire an API token from https://modmail.tk. Read the updated installation guide [here](https://github.com/kyb3r/modmail/wiki/installation). ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a95e344610..568e5f2175 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,33 +15,32 @@ We use GitHub to host code, to track issues and feature requests, as well as acc ## We Use [Git Flow](https://atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) ![Simple Image Of A Git Flow Workflow](https://nvie.com/img/hotfix-branches@2x.png) -When contributing to this project please make sure you follow this and name your branches appropriately! +When contributing to this project, please make sure you follow this and name your branches appropriately! ## All Code Changes Happen Through Pull Requests Make sure you know how Git Flow works before contributing! Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 1. Fork the repo and create your branch from `master` or `development` according to Git Flow. -2. If you've added code that should be tested, add tests. -3. If you've changed APIs, update the documentation. -4. Ensure the test suite passes. -5. Make sure your code lints. -6. Issue that pull request! +2. Update the CHANGELOG. +3. If you've changed `core/*` or `bot.py`, mark changelog as "BREAKING" since plugins may break. +4. Make sure your code passes the lint checks. +5. Create Issues and pull requests! ## Any contributions you make will be under the GNU Affero General Public License v3.0 In short, when you submit code changes, your submissions are understood to be under the same [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project. Feel free to contact the maintainers if that's a concern. ## Report bugs using [Github Issues](https://github.com/kyb3r/modmail/issues) -We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! +We use GitHub issues to track public bugs. Report a bug by [opening a new Issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! ## Write bug reports with detail, background, and sample code **Great Bug Reports** tend to have: -- A quick summary and/or background +- A quick summary and background - Steps to reproduce - Be specific! - What you expected would happen -- What actually happens +- What *actually* happens - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) diff --git a/README.md b/README.md index ec28a420b7..961a664a21 100644 --- a/README.md +++ b/README.md @@ -46,25 +46,25 @@ ## What is Modmail? -Modmail is similar to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. +Modmail is similar to Reddit's Modmail, both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included! ## How does it work? -When a member sends a direct message to the bot, Modmail will create a channel or "thread" within an isolated category. All further DM messages will automatically relay to that channel, for any available staff can respond within the channel. +When a member sends a direct message to the bot, Modmail will create a channel or "thread" into a designated category. All further DM messages will automatically relay to that channel; any available staff can respond within the channel. -All threads are logged and you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.logviewer.tech/example). +Our Logviewer will save the threads so you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.logviewer.tech/example). ## Features * **Highly Customisable:** * Bot activity, prefix, category, log channel, etc. * Command permission system. - * Interface elements (color, responses, reactions, etc). + * Interface elements (color, responses, reactions, etc.). * Snippets and *command aliases*. * Minimum duration for accounts to be created before allowed to contact Modmail (`account_age`). - * Minimum duration for members to be in the guild before allowed to contact Modmail (`guild_age`). + * Minimum length for members to be in the guild before allowed to contact Modmail (`guild_age`). * **Advanced Logging Functionality:** * When you close a thread, Modmail will generate a [log link](https://logs.logviewer.tech/example) and post it to your log channel. @@ -86,11 +86,11 @@ This list is ever-growing thanks to active development and our exceptional contr Where can I find the Modmail bot invite link? -Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time)... +Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time). ### Heroku -This bot can be hosted on Heroku. +You can host this bot on Heroku. Installation via Heroku is possible with your web browser alone. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. @@ -104,11 +104,11 @@ To configure automatic updates: ### Hosting for Patreons -If you don't want to go through the trouble of setting up your very own Modmail bot, and/or want to support this project, we offer the all inclusive installation, hosting and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! +If you don't want to go through the trouble of setting up your very own Modmail bot or wish to support this project, we got a solution for you! We offer the complete installation, hosting, and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! ### Locally -Local hosting of Modmail is also possible, first you will need [`Python 3.7`](https://www.python.org/downloads/). +Local hosting of Modmail is also possible. First, you will need [`Python 3.7`](https://www.python.org/downloads/). Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. @@ -143,7 +143,7 @@ You can build your own Docker image: $ docker build . --tag=modmail ``` -or run directly from a pre-built version from https://hub.docker.com/. +Or run directly from a pre-built version from https://hub.docker.com/. - Kyber's: @@ -156,7 +156,7 @@ And to run your docker image: ```console $ docker run --env-file .env kyb3rr/modmail ``` -- `.env` should be the path to your env file, you can also supply a path: `/path/to/.env`. +- `.env` should be the path to your env file; you can also supply a path: `/path/to/.env`. ## Sponsors @@ -175,9 +175,9 @@ Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins Modmail supports the use of third-party plugins to extend or add functionalities to the bot. -This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. +Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail. -A list of third-party plugins can be found using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. +You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). @@ -185,6 +185,6 @@ Plugins requests and support is available in our [Modmail Plugins Server](https: ## Contributing -Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! diff --git a/pyproject.toml b/pyproject.toml index f5af2276c9..3ea9cf9b8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 99 -target-version = ['py36'] +target-version = ['py37'] include = '\.pyi?$' exclude = ''' ( @@ -22,7 +22,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' version = '3.4.1' -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.' +description = "Modmail is similar to Reddit's 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 ', From 5ff81d5b8a292dd2b06694c2493b58ba5cff27fb Mon Sep 17 00:00:00 2001 From: Cyrus <54488650+RealCyGuy@users.noreply.github.com> Date: Thu, 6 Feb 2020 18:45:31 -0800 Subject: [PATCH 0531/1221] links to python 3.7 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 961a664a21..9873ba5f12 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ If you don't want to go through the trouble of setting up your very own Modmail ### Locally -Local hosting of Modmail is also possible. First, you will need [`Python 3.7`](https://www.python.org/downloads/). +Local hosting of Modmail is also possible. First, you will need [`Python 3.7`](https://www.python.org/downloads/release/python-376/). Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. From df91e75a075834c60013f56e5aeef4d0fbfbe5d6 Mon Sep 17 00:00:00 2001 From: "MiscDev.py" <60783846+mischievousdev@users.noreply.github.com> Date: Thu, 20 Feb 2020 18:10:39 +0530 Subject: [PATCH 0532/1221] Add my plugin --- plugins/registry.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 00518c1c65..0fe6428789 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -195,5 +195,13 @@ "title": "Server Stats", "icon_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png", "thumbnail_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png" + }, + "githubstats": { + "repository": "mischievousdev/modmail-plugins", + "branch": "master", + "description": "Github statistics in discord", + "bot_version": "2.20.1", + "icon_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg", + "thumbnail_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg" } } From fae731e925ca76b534b10ce0472f014ddf816fde Mon Sep 17 00:00:00 2001 From: Piyush Bhangale Date: Fri, 21 Feb 2020 10:30:33 +0530 Subject: [PATCH 0533/1221] feat: add github_token in config --- core/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/config.py b/core/config.py index a0eda4a2dc..f9097e737e 100644 --- a/core/config.py +++ b/core/config.py @@ -110,6 +110,8 @@ class ConfigManager: "owners": None, # bot "token": None, + # github access token for private repositories + "github_token": None # Logging "log_level": "INFO", "enable_plugins": True, From 8b127b5e1fc43ad1a11036ae3ed5710de5fc2d34 Mon Sep 17 00:00:00 2001 From: Piyush Bhangale Date: Fri, 21 Feb 2020 10:36:02 +0530 Subject: [PATCH 0534/1221] feat: add authorization header if present --- cogs/plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index e4543d2f1e..e8b7095c0c 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -154,7 +154,10 @@ async def download_plugin(self, plugin, force=False): logger.debug("Loading cached %s.", plugin.cache_path) else: - async with self.bot.session.get(plugin.url) as resp: + headers = {} + if self.bot.config.get("github_token") is not None: + headers["Authorization"] = f"token {self.bot.config.get('github_token')}" + async with self.bot.session.get(plugin.url, headers=headers) as resp: logger.debug("Downloading %s.", plugin.url) raw = await resp.read() plugin_io = io.BytesIO(raw) From 5aa1f007ddbcf7e0cb93fb9a0bc7e351cc903525 Mon Sep 17 00:00:00 2001 From: Piyush Bhangale Date: Fri, 21 Feb 2020 10:38:26 +0530 Subject: [PATCH 0535/1221] fix: add a missing comma --- core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.py b/core/config.py index f9097e737e..9b2763aa55 100644 --- a/core/config.py +++ b/core/config.py @@ -111,7 +111,7 @@ class ConfigManager: # bot "token": None, # github access token for private repositories - "github_token": None + "github_token": None, # Logging "log_level": "INFO", "enable_plugins": True, From 98c691eade294e0eea7b7afe0af77e10f159131b Mon Sep 17 00:00:00 2001 From: Cyrus <54488650+RealCyGuy@users.noreply.github.com> Date: Thu, 20 Feb 2020 21:27:45 -0800 Subject: [PATCH 0536/1221] Add my plugin, the suggestion plugin. --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 00518c1c65..0c887c88b7 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -196,4 +196,13 @@ "icon_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png", "thumbnail_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png" } + "suggest": { + "repository": "realcyguy/modmail-plugins", + "branch": "master", + "description": "Send suggestions to a selected server! It even has moderation...", + "bot_version": "3.4.1", + "title": "Suggest stuff.", + "icon_url": "https://i.imgur.com/qtE7AH8.png", + "thumbnail_url": "https://i.imgur.com/qtE7AH8.png" + } } From 663caabcdca8ad15f8b27577f4f526cf687815bb Mon Sep 17 00:00:00 2001 From: Cyrus <54488650+RealCyGuy@users.noreply.github.com> Date: Thu, 20 Feb 2020 22:36:16 -0800 Subject: [PATCH 0537/1221] added comma --- plugins/registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 0c887c88b7..32210e50be 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -195,7 +195,7 @@ "title": "Server Stats", "icon_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png", "thumbnail_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png" - } + }, "suggest": { "repository": "realcyguy/modmail-plugins", "branch": "master", From dd3d5412433f41b15b88714bb3608d709764ec97 Mon Sep 17 00:00:00 2001 From: "MiscDev.py" <60783846+mischievousdev@users.noreply.github.com> Date: Tue, 10 Mar 2020 16:26:22 +0530 Subject: [PATCH 0538/1221] Added title in githubstats plugin --- plugins/registry.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/registry.json b/plugins/registry.json index c66b01b081..379245539b 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -210,6 +210,7 @@ "branch": "master", "description": "Github statistics in discord", "bot_version": "2.20.1", + "title": "Github Stats", "icon_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg", "thumbnail_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg" } From 4c235f1a30e2f983a34ec1ecb5e2c3dc1ed7f75d Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Thu, 12 Mar 2020 01:35:34 -0700 Subject: [PATCH 0539/1221] support newer venv and virtualenv since real_prefix is no longer set --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index e4543d2f1e..b602d6e78e 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -187,7 +187,7 @@ async def load_plugin(self, plugin): if req_txt.exists(): # Install PIP requirements - venv = hasattr(sys, "real_prefix") # in a virtual env + venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env user_install = " --user" if not venv else "" proc = await asyncio.create_subprocess_shell( f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", From 9dd6a268f440f5516d1e0f9cbf766118c114aab6 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 13 Mar 2020 23:40:36 +0100 Subject: [PATCH 0540/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9873ba5f12..4b36c5a3d2 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ Special thanks to our sponsors for supporting the project. - + From fc2d0a2d6d77bb1453eeb6862b966b57b99cdef1 Mon Sep 17 00:00:00 2001 From: Taven Argus Date: Thu, 16 Apr 2020 04:21:56 +0000 Subject: [PATCH 0541/1221] Ensure optional dependencies in pyproject.toml can be installed by poetry. This adds an extras section so that the defined optionals can be installed with poetry. Source: https://stackoverflow.com/a/60990574/5586359 --- poetry.lock | 467 ++++++++++++++++++++++++++++++++++++++++++------- pyproject.toml | 2 + 2 files changed, 405 insertions(+), 64 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1841d893a1..f4b23956b8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,6 +13,9 @@ chardet = ">=2.0,<4.0" multidict = ">=4.0,<5.0" yarl = ">=1.0,<2.0" +[package.extras] +speedups = ["aiodns", "brotlipy", "cchardet"] + [[package]] category = "dev" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." @@ -54,6 +57,12 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "19.3.0" +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + [[package]] category = "dev" description = "Security oriented static analyser for python code." @@ -83,6 +92,9 @@ attrs = ">=18.1.0" click = ">=6.5" toml = ">=0.9.4" +[package.extras] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + [[package]] category = "main" description = "Universal encoding detector for Python 2 and 3" @@ -96,8 +108,8 @@ 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" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.1" [[package]] category = "main" @@ -119,6 +131,10 @@ version = "1.2.5" aiohttp = ">=3.3.0,<3.6.0" websockets = ">=6.0,<7.0" +[package.extras] +docs = ["sphinx (1.7.4)", "sphinxcontrib-asyncio", "sphinxcontrib-websupport"] +voice = ["PyNaCl (1.3.0)"] + [[package]] category = "main" description = "DNS toolkit" @@ -127,6 +143,10 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.16.0" +[package.extras] +DNSSEC = ["pycryptodome", "ecdsa (>=0.13)"] +IDNA = ["idna (>=2.1)"] + [[package]] category = "main" description = "Emoji for Python" @@ -135,6 +155,9 @@ optional = false python-versions = "*" version = "0.5.4" +[package.extras] +dev = ["nose", "coverage", "coveralls"] + [[package]] category = "main" description = "Backport of the concurrent.futures package from Python 3.2" @@ -146,24 +169,24 @@ version = "3.1.1" [[package]] category = "dev" description = "Git Object Database" -name = "gitdb2" +name = "gitdb" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.6" +python-versions = ">=3.4" +version = "4.0.4" [package.dependencies] -smmap2 = ">=2.0.0" +smmap = ">=3.0.1,<4" [[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" +python-versions = ">=3.4" +version = "3.1.1" [package.dependencies] -gitdb2 = ">=2.0.0" +gitdb = ">=4.0.1,<5" [[package]] category = "main" @@ -171,7 +194,7 @@ 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" +version = "2.9" [[package]] category = "main" @@ -192,6 +215,12 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "4.3.21" +[package.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs (>=1.4.0)"] + [[package]] category = "dev" description = "A fast and thorough lazy object proxy." @@ -226,7 +255,7 @@ description = "multidict implementation" name = "multidict" optional = false python-versions = ">=3.5" -version = "4.7.1" +version = "4.7.5" [[package]] category = "main" @@ -253,7 +282,7 @@ description = "Python Build Reasonableness" name = "pbr" optional = false python-versions = "*" -version = "5.4.4" +version = "5.4.5" [[package]] category = "dev" @@ -275,7 +304,15 @@ description = "Python driver for MongoDB " name = "pymongo" optional = true python-versions = "*" -version = "3.10.0" +version = "3.10.1" + +[package.extras] +encryption = ["pymongocrypt (<2.0.0)"] +gssapi = ["pykerberos"] +snappy = ["python-snappy"] +srv = ["dnspython (>=1.16.0,<1.17.0)"] +tls = ["ipaddress"] +zstd = ["zstandard"] [[package]] category = "main" @@ -294,31 +331,34 @@ description = "Add .env support to your django/flask apps in development and dep name = "python-dotenv" optional = false python-versions = "*" -version = "0.10.3" +version = "0.10.5" + +[package.extras] +cli = ["click (>=5.0)"] [[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" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.3.1" [[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" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.14.0" [[package]] category = "dev" description = "A pure Python implementation of a sliding window memory map manager" -name = "smmap2" +name = "smmap" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.5" +version = "3.0.2" [[package]] category = "dev" @@ -326,7 +366,7 @@ description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false python-versions = "*" -version = "1.31.0" +version = "1.32.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" @@ -347,7 +387,7 @@ marker = "implementation_name == \"cpython\" and python_version < \"3.8\"" name = "typed-ast" optional = false python-versions = "*" -version = "1.4.0" +version = "1.4.1" [[package]] category = "main" @@ -385,48 +425,347 @@ version = "1.4.2" idna = ">=2.0" multidict = ">=4.0" +[extras] +mongodb = ["motor"] + [metadata] -content-hash = "fbe9e329f33e482854cff5bf05b006de9830c2d46bf3874e2ee4f8a8da0b1797" +content-hash = "793f86eb19d73f473f00883384ec965876c3930f41f94a171d2d1bc38f66903b" 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"] -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"] -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"] +[metadata.files] +aiohttp = [ + {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_10_x86_64.whl", hash = "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5"}, + {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed"}, + {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303"}, + {file = "aiohttp-3.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10"}, + {file = "aiohttp-3.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa"}, + {file = "aiohttp-3.5.4-cp35-cp35m-win32.whl", hash = "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4"}, + {file = "aiohttp-3.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72"}, + {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5"}, + {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12"}, + {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6"}, + {file = "aiohttp-3.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6"}, + {file = "aiohttp-3.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d"}, + {file = "aiohttp-3.5.4-cp36-cp36m-win32.whl", hash = "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1"}, + {file = "aiohttp-3.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d"}, + {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300"}, + {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390"}, + {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366"}, + {file = "aiohttp-3.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"}, + {file = "aiohttp-3.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55"}, + {file = "aiohttp-3.5.4-cp37-cp37m-win32.whl", hash = "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc"}, + {file = "aiohttp-3.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939"}, + {file = "aiohttp-3.5.4.tar.gz", hash = "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf"}, +] +appdirs = [ + {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, + {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, +] +astroid = [ + {file = "astroid-2.3.3-py3-none-any.whl", hash = "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"}, + {file = "astroid-2.3.3.tar.gz", hash = "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a"}, +] +async-timeout = [ + {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, + {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +bandit = [ + {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, + {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, +] +black = [ + {file = "black-19.3b0-py36-none-any.whl", hash = "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf"}, + {file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, + {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +"discord.py" = [ + {file = "discord.py-1.2.5-py3-none-any.whl", hash = "sha256:7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d"}, +] +dnspython = [ + {file = "dnspython-1.16.0-py2.py3-none-any.whl", hash = "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"}, + {file = "dnspython-1.16.0.zip", hash = "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01"}, +] +emoji = [ + {file = "emoji-0.5.4.tar.gz", hash = "sha256:60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174"}, +] +futures = [ + {file = "futures-3.1.1-py2-none-any.whl", hash = "sha256:c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"}, + {file = "futures-3.1.1-py3-none-any.whl", hash = "sha256:3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b"}, + {file = "futures-3.1.1.tar.gz", hash = "sha256:51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd"}, +] +gitdb = [ + {file = "gitdb-4.0.4-py3-none-any.whl", hash = "sha256:ba1132c0912e8c917aa8aa990bee26315064c7b7f171ceaaac0afeb1dc656c6a"}, + {file = "gitdb-4.0.4.tar.gz", hash = "sha256:6f0ecd46f99bb4874e5678d628c3a198e2b4ef38daea2756a2bfd8df7dd5c1a5"}, +] +gitpython = [ + {file = "GitPython-3.1.1-py3-none-any.whl", hash = "sha256:71b8dad7409efbdae4930f2b0b646aaeccce292484ffa0bc74f1195582578b3d"}, + {file = "GitPython-3.1.1.tar.gz", hash = "sha256:6d4f10e2aaad1864bb0f17ec06a2c2831534140e5883c350d58b4e85189dab74"}, +] +idna = [ + {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, + {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, +] +isodate = [ + {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"}, + {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"}, +] +isort = [ + {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, + {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, + {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, + {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, + {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, + {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, + {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, + {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +motor = [ + {file = "motor-2.1.0-py2-none-any.whl", hash = "sha256:599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909"}, + {file = "motor-2.1.0-py3-none-any.whl", hash = "sha256:97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a"}, + {file = "motor-2.1.0.tar.gz", hash = "sha256:756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4"}, +] +multidict = [ + {file = "multidict-4.7.5-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3"}, + {file = "multidict-4.7.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35"}, + {file = "multidict-4.7.5-cp35-cp35m-win32.whl", hash = "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1"}, + {file = "multidict-4.7.5-cp35-cp35m-win_amd64.whl", hash = "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd"}, + {file = "multidict-4.7.5-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20"}, + {file = "multidict-4.7.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136"}, + {file = "multidict-4.7.5-cp36-cp36m-win32.whl", hash = "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e"}, + {file = "multidict-4.7.5-cp36-cp36m-win_amd64.whl", hash = "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78"}, + {file = "multidict-4.7.5-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8"}, + {file = "multidict-4.7.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab"}, + {file = "multidict-4.7.5-cp37-cp37m-win32.whl", hash = "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928"}, + {file = "multidict-4.7.5-cp37-cp37m-win_amd64.whl", hash = "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1"}, + {file = "multidict-4.7.5-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4"}, + {file = "multidict-4.7.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2"}, + {file = "multidict-4.7.5-cp38-cp38-win32.whl", hash = "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5"}, + {file = "multidict-4.7.5-cp38-cp38-win_amd64.whl", hash = "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969"}, + {file = "multidict-4.7.5.tar.gz", hash = "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e"}, +] +natural = [ + {file = "natural-0.2.0.tar.gz", hash = "sha256:18c83662d2d33fd7e6eee4e3b0d7366e1ce86225664e3127a2aaf0a3233f7df2"}, +] +parsedatetime = [ + {file = "parsedatetime-2.5-py2-none-any.whl", hash = "sha256:3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1"}, + {file = "parsedatetime-2.5.tar.gz", hash = "sha256:d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"}, +] +pbr = [ + {file = "pbr-5.4.5-py2.py3-none-any.whl", hash = "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"}, + {file = "pbr-5.4.5.tar.gz", hash = "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c"}, +] +pylint = [ + {file = "pylint-2.4.4-py3-none-any.whl", hash = "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"}, + {file = "pylint-2.4.4.tar.gz", hash = "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd"}, +] +pymongo = [ + {file = "pymongo-3.10.1-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee"}, + {file = "pymongo-3.10.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed"}, + {file = "pymongo-3.10.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6"}, + {file = "pymongo-3.10.1-cp27-cp27m-win32.whl", hash = "sha256:e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5"}, + {file = "pymongo-3.10.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe"}, + {file = "pymongo-3.10.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc"}, + {file = "pymongo-3.10.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764"}, + {file = "pymongo-3.10.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372"}, + {file = "pymongo-3.10.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32"}, + {file = "pymongo-3.10.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc"}, + {file = "pymongo-3.10.1-cp34-cp34m-win32.whl", hash = "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a"}, + {file = "pymongo-3.10.1-cp34-cp34m-win_amd64.whl", hash = "sha256:7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae"}, + {file = "pymongo-3.10.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35"}, + {file = "pymongo-3.10.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b"}, + {file = "pymongo-3.10.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80"}, + {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409"}, + {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113"}, + {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f"}, + {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf"}, + {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d"}, + {file = "pymongo-3.10.1-cp35-cp35m-win32.whl", hash = "sha256:c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57"}, + {file = "pymongo-3.10.1-cp35-cp35m-win_amd64.whl", hash = "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70"}, + {file = "pymongo-3.10.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8"}, + {file = "pymongo-3.10.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4"}, + {file = "pymongo-3.10.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606"}, + {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e"}, + {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151"}, + {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54"}, + {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464"}, + {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73"}, + {file = "pymongo-3.10.1-cp36-cp36m-win32.whl", hash = "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33"}, + {file = "pymongo-3.10.1-cp36-cp36m-win_amd64.whl", hash = "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6"}, + {file = "pymongo-3.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f"}, + {file = "pymongo-3.10.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d"}, + {file = "pymongo-3.10.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d"}, + {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794"}, + {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7"}, + {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2"}, + {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11"}, + {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951"}, + {file = "pymongo-3.10.1-cp37-cp37m-win32.whl", hash = "sha256:c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39"}, + {file = "pymongo-3.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b"}, + {file = "pymongo-3.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03"}, + {file = "pymongo-3.10.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c"}, + {file = "pymongo-3.10.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892"}, + {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103"}, + {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7"}, + {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d"}, + {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82"}, + {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be"}, + {file = "pymongo-3.10.1-cp38-cp38-win32.whl", hash = "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f"}, + {file = "pymongo-3.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac"}, + {file = "pymongo-3.10.1-py2.7-macosx-10.14-intel.egg", hash = "sha256:bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a"}, + {file = "pymongo-3.10.1-py2.7-win-amd64.egg", hash = "sha256:ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012"}, + {file = "pymongo-3.10.1-py2.7-win32.egg", hash = "sha256:f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a"}, + {file = "pymongo-3.10.1.tar.gz", hash = "sha256:993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +python-dotenv = [ + {file = "python-dotenv-0.10.5.tar.gz", hash = "sha256:f254bfd0c970d64ccbb6c9ebef3667ab301a71473569c991253a481f1c98dddc"}, + {file = "python_dotenv-0.10.5-py2.py3-none-any.whl", hash = "sha256:440c7c23d53b7d352f9c94d6f70860242c2f071cf5c029dd661ccb22d64ae42b"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +six = [ + {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, + {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, +] +smmap = [ + {file = "smmap-3.0.2-py2.py3-none-any.whl", hash = "sha256:52ea78b3e708d2c2b0cfe93b6fc3fbeec53db913345c26be6ed84c11ed8bebc1"}, + {file = "smmap-3.0.2.tar.gz", hash = "sha256:b46d3fc69ba5f367df96d91f8271e8ad667a198d5a28e215a6c3d9acd133a911"}, +] +stevedore = [ + {file = "stevedore-1.32.0-py2.py3-none-any.whl", hash = "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"}, + {file = "stevedore-1.32.0.tar.gz", hash = "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b"}, +] +toml = [ + {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, + {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, + {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +uvloop = [ + {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, + {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"}, + {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"}, + {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"}, + {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"}, + {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"}, + {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"}, + {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, + {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, +] +websockets = [ + {file = "websockets-6.0-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d"}, + {file = "websockets-6.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484"}, + {file = "websockets-6.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e"}, + {file = "websockets-6.0-cp34-cp34m-win32.whl", hash = "sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"}, + {file = "websockets-6.0-cp34-cp34m-win_amd64.whl", hash = "sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96"}, + {file = "websockets-6.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538"}, + {file = "websockets-6.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0"}, + {file = "websockets-6.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1"}, + {file = "websockets-6.0-cp35-cp35m-win32.whl", hash = "sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d"}, + {file = "websockets-6.0-cp35-cp35m-win_amd64.whl", hash = "sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6"}, + {file = "websockets-6.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf"}, + {file = "websockets-6.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d"}, + {file = "websockets-6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb"}, + {file = "websockets-6.0-cp36-cp36m-win32.whl", hash = "sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c"}, + {file = "websockets-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff"}, + {file = "websockets-6.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908"}, + {file = "websockets-6.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559"}, + {file = "websockets-6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4"}, + {file = "websockets-6.0-cp37-cp37m-win32.whl", hash = "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136"}, + {file = "websockets-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584"}, + {file = "websockets-6.0.tar.gz", hash = "sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c"}, +] +wrapt = [ + {file = "wrapt-1.11.2.tar.gz", hash = "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"}, +] +yarl = [ + {file = "yarl-1.4.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b"}, + {file = "yarl-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1"}, + {file = "yarl-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080"}, + {file = "yarl-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a"}, + {file = "yarl-1.4.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f"}, + {file = "yarl-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea"}, + {file = "yarl-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb"}, + {file = "yarl-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70"}, + {file = "yarl-1.4.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d"}, + {file = "yarl-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce"}, + {file = "yarl-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"}, + {file = "yarl-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce"}, + {file = "yarl-1.4.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b"}, + {file = "yarl-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae"}, + {file = "yarl-1.4.2-cp38-cp38-win32.whl", hash = "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462"}, + {file = "yarl-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6"}, + {file = "yarl-1.4.2.tar.gz", hash = "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b"}, +] diff --git a/pyproject.toml b/pyproject.toml index 3ea9cf9b8e..a114573201 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,3 +54,5 @@ black = {version = "=19.3b0", allows-prereleases = true} pylint = "^2.4" bandit = "^1.6" +[tool.poetry.extras] +mongodb = ["motor"] From f8931cc9816dc6603423b5c05f0dc5d3903954ac Mon Sep 17 00:00:00 2001 From: Mark David <44349634+markd69@users.noreply.github.com> Date: Thu, 16 Apr 2020 14:05:36 -0400 Subject: [PATCH 0542/1221] Changed Typo on !unsubscribe (tag) When a user is tagged in the !unsubscribe command it shows as they are already subscribed but they are not. --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7d36cecb62..3b9c591294 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -502,7 +502,7 @@ 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 not subscribed to this thread.", ) else: mentions.append(mention) From ec831a302e8f42e78618b34ee138ec9f33267986 Mon Sep 17 00:00:00 2001 From: Cyrus <54488650+RealCyGuy@users.noreply.github.com> Date: Sun, 19 Apr 2020 17:54:33 -0700 Subject: [PATCH 0543/1221] make approved issues not stale --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b3003ccf92..c204febe99 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,4 +14,4 @@ jobs: 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' + exempt-issue-label: 'high priority,approved' From 9d391ea002e0c1f03b541c7e9ae9a6bc7ce2f117 Mon Sep 17 00:00:00 2001 From: Cyrus <54488650+RealCyGuy@users.noreply.github.com> Date: Sun, 19 Apr 2020 18:00:00 -0700 Subject: [PATCH 0544/1221] change high priority to actual name --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index c204febe99..e4408c1388 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,4 +14,4 @@ jobs: 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,approved' + exempt-issue-label: 'priority: high,approved' From fd09373270da732d879ed13442ba6e25c252ba68 Mon Sep 17 00:00:00 2001 From: Cyrus <54488650+RealCyGuy@users.noreply.github.com> Date: Sun, 19 Apr 2020 18:00:32 -0700 Subject: [PATCH 0545/1221] security is also not stale --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e4408c1388..f5f4eb1976 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,4 +14,4 @@ jobs: 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: 'priority: high,approved' + exempt-issue-label: 'priority: high,approved,security' From 6febccf05395988607b7f663ef9a542a2aff674c Mon Sep 17 00:00:00 2001 From: Cyrus <54488650+RealCyGuy@users.noreply.github.com> Date: Mon, 20 Apr 2020 08:38:51 -0700 Subject: [PATCH 0546/1221] added bug --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f5f4eb1976..d0811bfd30 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,4 +14,4 @@ jobs: 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: 'priority: high,approved,security' + exempt-issue-label: 'priority: high,approved,security,bug' From 0ea0a907d2bb93257c9cd0c3143fea65cc93d37f Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Tue, 26 May 2020 22:43:46 -0700 Subject: [PATCH 0547/1221] Update bot.py --- bot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot.py b/bot.py index 694f860cf1..9b8bdd1141 100644 --- a/bot.py +++ b/bot.py @@ -46,6 +46,9 @@ if not os.path.exists(temp_dir): os.mkdir(temp_dir) +if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + class ModmailBot(commands.Bot): def __init__(self): From 6af5ff1113f0e2323439039186c0e446ffb56329 Mon Sep 17 00:00:00 2001 From: xTeen <62380638+Teen1@users.noreply.github.com> Date: Mon, 1 Jun 2020 18:28:58 +0300 Subject: [PATCH 0548/1221] Added new plugin added slowmode plugin to registry --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 379245539b..1a5872225d 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -214,4 +214,13 @@ "icon_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg", "thumbnail_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg" } + "slowmode": { + "repository": "teen1/modmail-plugins", + "branch": "master", + "description": "Configure slow mode for your channels with Modmail!", + "bot_version": "2.20.1", + "title": "Slow Mode", + "icon_url": "https://cdn.discordapp.com/attachments/717029057635549274/717033838966210601/Slow_mode_-_icon.png", + "thumbnail_url": "https://cdn.discordapp.com/attachments/717029057635549274/717029110907666482/Slow_mode_plugin_-_thumbnail.png" + } } From 46be01e97c57b03eca48b190a216168f8dc1b318 Mon Sep 17 00:00:00 2001 From: xTeen <62380638+Teen1@users.noreply.github.com> Date: Mon, 1 Jun 2020 18:41:46 +0300 Subject: [PATCH 0549/1221] Update registry.json --- plugins/registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 1a5872225d..b15bb374dc 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -213,7 +213,7 @@ "title": "Github Stats", "icon_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg", "thumbnail_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg" - } + }, "slowmode": { "repository": "teen1/modmail-plugins", "branch": "master", From 0d4c8e22a5d34c51ba7b60adb75f2d80218f6f33 Mon Sep 17 00:00:00 2001 From: Ralph Date: Fri, 5 Jun 2020 01:47:48 -0400 Subject: [PATCH 0550/1221] Reformat plugins.py to pass black check --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index b602d6e78e..d14035286d 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -187,7 +187,7 @@ async def load_plugin(self, plugin): if req_txt.exists(): # Install PIP requirements - venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env + venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env user_install = " --user" if not venv else "" proc = await asyncio.create_subprocess_shell( f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", From e0387a4fed625f2aecf2caa4773896aa4cab49a1 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sat, 6 Jun 2020 23:15:48 -0700 Subject: [PATCH 0551/1221] Bumped versions in requirements, unstable --- Pipfile | 8 +- Pipfile.lock | 504 ++++++++++++++++++++++++------------------- poetry.lock | 496 ++++++++---------------------------------- pyproject.toml | 10 +- requirements.min.txt | 28 +-- runtime.txt | 2 +- 6 files changed, 396 insertions(+), 652 deletions(-) diff --git a/Pipfile b/Pipfile index 9173c3a5fe..16bbac8e49 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,7 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -black = "==19.3b0" +black = "==19.10b0" pylint = "*" bandit = "==1.6.2" @@ -17,11 +17,11 @@ motor = ">=2.0.0" natural = "==0.2.0" isodate = ">=0.6.0" dnspython = "~=1.16.0" -parsedatetime = "==2.5" -aiohttp = "<3.6.0,>=3.3.0" +parsedatetime = "==2.6" +aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = "==1.2.5" +"discord.py" = "==1.3.3" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 06d263645b..14f91762aa 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c2eb0898f236534a02cb1c198d74c82fed052b4445e39f99c1af3e58d22aa435" + "sha256": "bfb85b7c7862e17e905d739f0fb63248d3bf446035f32e3afac619f6d2c5a0ba" }, "pipfile-spec": 6, "requires": { @@ -18,31 +18,28 @@ "default": { "aiohttp": { "hashes": [ - "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55", - "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed", - "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10", - "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5", - "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1", - "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939", - "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390", - "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa", - "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc", - "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5", - "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d", - "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf", - "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6", - "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72", - "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12", - "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366", - "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4", - "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300", - "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d", - "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303", - "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6", - "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889" + "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", + "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", + "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", + "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", + "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", + "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", + "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", + "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", + "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", + "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", + "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", + "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" ], "index": "pypi", - "version": "==3.5.4" + "version": "==3.6.2" + }, + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" }, "async-timeout": { "hashes": [ @@ -60,10 +57,10 @@ }, "certifi": { "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" ], - "version": "==2019.11.28" + "version": "==2020.4.5.1" }, "chardet": { "hashes": [ @@ -82,10 +79,17 @@ }, "discord.py": { "hashes": [ - "sha256:7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d" + "sha256:406871b06d86c3dc49fba63238519f28628dac946fef8a0e22988ff58ec05580", + "sha256:ad00e34c72d2faa8db2157b651d05f3c415d7d05078e7e41dc9e8dc240051beb" ], "index": "pypi", - "version": "==1.2.5" + "version": "==1.3.3" + }, + "distlib": { + "hashes": [ + "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" + ], + "version": "==0.3.0" }, "dnspython": { "hashes": [ @@ -102,12 +106,27 @@ "index": "pypi", "version": "==0.5.4" }, + "filelock": { + "hashes": [ + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + ], + "version": "==3.0.12" + }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" ], - "version": "==2.8" + "version": "==2.9" + }, + "importlib-metadata": { + "hashes": [ + "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545", + "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958" + ], + "markers": "python_version < '3.8'", + "version": "==1.6.1" }, "isodate": { "hashes": [ @@ -128,25 +147,25 @@ }, "multidict": { "hashes": [ - "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" + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + ], + "version": "==4.7.6" }, "natural": { "hashes": [ @@ -157,78 +176,77 @@ }, "parsedatetime": { "hashes": [ - "sha256:3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", - "sha256:d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667" + "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", + "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b" ], "index": "pypi", - "version": "==2.5" + "version": "==2.6" }, "pipenv": { "hashes": [ - "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330", - "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e", - "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846" + "sha256:7dd49a7345fa5da7843907bab42a996dece474e45dd309dbd7619739dc60478b", + "sha256:cc289dc78feaa14def4e1723b0b4d85ff628c8e5f740eeeb68197b90dafa8dff" ], "index": "pypi", - "version": "==2018.11.26" + "version": "==2020.6.2" }, "pymongo": { "hashes": [ - "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" + "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", + "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", + "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", + "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", + "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", + "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103", + "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", + "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", + "sha256:2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", + "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", + "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", + "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", + "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", + "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", + "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", + "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", + "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", + "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", + "sha256:4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", + "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", + "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", + "sha256:56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", + "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", + "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", + "sha256:619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", + "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", + "sha256:63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113", + "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", + "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", + "sha256:7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae", + "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", + "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", + "sha256:993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", + "sha256:9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", + "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", + "sha256:a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", + "sha256:a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", + "sha256:a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", + "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", + "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", + "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", + "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", + "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", + "sha256:c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", + "sha256:c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", + "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", + "sha256:c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", + "sha256:da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", + "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", + "sha256:e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", + "sha256:e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", + "sha256:e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", + "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606" + ], + "version": "==3.10.1" }, "python-dateutil": { "hashes": [ @@ -240,18 +258,18 @@ }, "python-dotenv": { "hashes": [ - "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093", - "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544" + "sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7", + "sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74" ], "index": "pypi", - "version": "==0.10.3" + "version": "==0.13.0" }, "six": { "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.13.0" + "version": "==1.15.0" }, "uvloop": { "hashes": [ @@ -271,43 +289,44 @@ }, "virtualenv": { "hashes": [ - "sha256:116655188441670978117d0ebb6451eb6a7526f9ae0796cc0dee6bd7356909b0", - "sha256:b57776b44f91511866594e477dd10e76a6eb44439cdd7f06dcd30ba4c5bd854f" + "sha256:a116629d4e7f4d03433b8afa27f43deba09d48bc48f5ecefa4f015a178efb6cf", + "sha256:a730548b27366c5e6cbdf6f97406d861cccece2e22275e8e1a757aeff5e00c70" ], - "version": "==16.7.8" + "version": "==20.0.21" }, "virtualenv-clone": { "hashes": [ - "sha256:532f789a5c88adf339506e3ca03326f20ee82fd08ee5586b44dc859b5b4468c5", - "sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557" + "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", + "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" ], - "version": "==0.5.3" + "version": "==0.5.4" }, "websockets": { "hashes": [ - "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", - "sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6", - "sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1", - "sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538", - "sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4", - "sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908", - "sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0", - "sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d", - "sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c", - "sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d", - "sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c", - "sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb", - "sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf", - "sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e", - "sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96", - "sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584", - "sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484", - "sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d", - "sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559", - "sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff", - "sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454" - ], - "version": "==6.0" + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + ], + "version": "==8.1" }, "yarl": { "hashes": [ @@ -330,22 +349,29 @@ "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" ], "version": "==1.4.2" + }, + "zipp": { + "hashes": [ + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + ], + "version": "==3.1.0" } }, "develop": { "appdirs": { "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" ], - "version": "==1.4.3" + "version": "==1.4.4" }, "astroid": { "hashes": [ - "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", - "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" + "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", + "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" ], - "version": "==2.3.3" + "version": "==2.4.1" }, "attrs": { "hashes": [ @@ -364,32 +390,32 @@ }, "black": { "hashes": [ - "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", - "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", + "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" ], "index": "pypi", - "version": "==19.3b0" + "version": "==19.10b0" }, "click": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], - "version": "==7.0" + "version": "==7.1.2" }, - "gitdb2": { + "gitdb": { "hashes": [ - "sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", - "sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b" + "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", + "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], - "version": "==2.0.6" + "version": "==4.0.5" }, "gitpython": { "hashes": [ - "sha256:9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", - "sha256:c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245" + "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a", + "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac" ], - "version": "==3.0.5" + "version": "==3.1.3" }, "isort": { "hashes": [ @@ -431,96 +457,130 @@ ], "version": "==0.6.1" }, + "pathspec": { + "hashes": [ + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + ], + "version": "==0.8.0" + }, "pbr": { "hashes": [ - "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", - "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488" + "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c", + "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8" ], - "version": "==5.4.4" + "version": "==5.4.5" }, "pylint": { "hashes": [ - "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", - "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" + "sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", + "sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b" ], "index": "pypi", - "version": "==2.4.4" + "version": "==2.5.2" }, "pyyaml": { "hashes": [ - "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" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" + ], + "version": "==5.3.1" + }, + "regex": { + "hashes": [ + "sha256:150125da109fccdcc8fec3b0b386b2a5d6ca7cff076f8b622486d1ca868b0c10", + "sha256:163bc0805e46acfa098dfc8c0b07f371577d505f603e48afc425ff475cdac3a5", + "sha256:20c513893ff80bdbe4b4ce11ea2e93d49481f05b270595d82af69ffc402010a6", + "sha256:21fc17cb868c4264f0813f992f46f9ae6fc8c309d4741091de4153bd1f6a6176", + "sha256:2c928bc8e0c453d73dffa3193a6e37ee752ea36df0dd4601e21024d98274dfad", + "sha256:2d9beca70e36f9c60d679e108c5fe49f3d4da79d13a13f91e5e759443bd954f9", + "sha256:5735f26cacdb50b3d6d35ebf8fdeb504bd8b381e2d079d2d9f12ce534fc14ecd", + "sha256:6edc5c190248d3b612f2cca45448cf8ebc3621d41afcd1c5708853cbb1dbb3b3", + "sha256:7606dba82435429641efe4fbc580574942f89cf2b9c5c1f8bc1eab2bacbf7e8b", + "sha256:8d1ee3796795e609ef7a3a5a35eaf4728038d986aa12c06b3fd1b92ee81911f4", + "sha256:8d9bb2d90e23c51aacbc58c1a11320f49b335cd67a91986cdbebcc3e843e4de8", + "sha256:97d414c41f19fd2362e493810caa8445c05e0a2d63a14081c972aad66284a8d2", + "sha256:9e37502817225ee99d91d8418f5119e98c380b00e772d06915690c05290f32ee", + "sha256:af7209b2fcc79ee2b0ad4ea080d70bb748450ec4f282cc9e864861e469b1072e", + "sha256:c0849b0864ff451f04c8afb5fc28e9ed592262e03debdd227cf0f53e04a55dcd", + "sha256:c4ac9215650688e78dea29b46adbdafb7b85058eebe92ef6ea848e14466c915f", + "sha256:dcda6d4e1bbfc939b177c237aee41c9678eaaf71df482688f8986e8251e12345", + "sha256:dd8501b8d9ea1aba53c4bc7d47bc72933f9b4213d534cf400f16c1431f51c8ba", + "sha256:ec0e509ed1877ff1cbc6f0864689bb60384a303502c4d72d9a635f8a4676fd3f", + "sha256:f6c8c3f56fef719180464855346e6e80971b86dfd9e5a0e356664b5baca53072", + "sha256:ffd4f80602490a309064cf2b203e220d581c51660e01055c64bf5da450485ee6" + ], + "version": "==2020.6.7" }, "six": { "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.13.0" + "version": "==1.15.0" }, - "smmap2": { + "smmap": { "hashes": [ - "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", - "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a" + "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", + "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], - "version": "==2.0.5" + "version": "==3.0.4" }, "stevedore": { "hashes": [ - "sha256:01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", - "sha256:e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14" + "sha256:001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9", + "sha256:471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691" ], - "version": "==1.31.0" + "version": "==2.0.0" }, "toml": { "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], - "version": "==0.10.0" + "version": "==0.10.1" }, "typed-ast": { "hashes": [ - "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", - "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", - "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", - "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", - "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", - "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", - "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", - "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", - "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", - "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", - "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", - "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", - "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", - "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", - "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", - "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", - "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", - "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", - "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", - "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.0" + "version": "==1.4.1" }, "wrapt": { "hashes": [ - "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], - "version": "==1.11.2" + "version": "==1.12.1" } } } diff --git a/poetry.lock b/poetry.lock index f4b23956b8..4aa8248546 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,38 +4,35 @@ description = "Async http client/server framework (asyncio)" name = "aiohttp" optional = false python-versions = ">=3.5.3" -version = "3.5.4" +version = "3.6.2" [package.dependencies] async-timeout = ">=3.0,<4.0" attrs = ">=17.3.0" chardet = ">=2.0,<4.0" -multidict = ">=4.0,<5.0" +multidict = ">=4.5,<5.0" yarl = ">=1.0,<2.0" -[package.extras] -speedups = ["aiodns", "brotlipy", "cchardet"] - [[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" +version = "1.4.4" [[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" +python-versions = ">=3.5" +version = "2.4.1" [package.dependencies] lazy-object-proxy = ">=1.4.0,<1.5.0" six = ">=1.12,<2.0" -wrapt = ">=1.11.0,<1.12.0" +wrapt = ">=1.11,<2.0" [package.dependencies.typed-ast] python = "<3.8" @@ -57,12 +54,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "19.3.0" -[package.extras] -azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] -dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] -docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] - [[package]] category = "dev" description = "Security oriented static analyser for python code." @@ -84,16 +75,16 @@ description = "The uncompromising code formatter." name = "black" optional = false python-versions = ">=3.6" -version = "19.3b0" +version = "19.10b0" [package.dependencies] appdirs = "*" attrs = ">=18.1.0" click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" toml = ">=0.9.4" - -[package.extras] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +typed-ast = ">=1.4.0" [[package]] category = "main" @@ -109,7 +100,7 @@ description = "Composable command line interface toolkit" name = "click" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.1" +version = "7.1.2" [[package]] category = "main" @@ -121,19 +112,15 @@ version = "0.4.3" [[package]] category = "main" -description = "A python wrapper for the Discord API" +description = "A Python wrapper for the Discord API" name = "discord.py" optional = false python-versions = ">=3.5.3" -version = "1.2.5" +version = "1.3.3" [package.dependencies] -aiohttp = ">=3.3.0,<3.6.0" -websockets = ">=6.0,<7.0" - -[package.extras] -docs = ["sphinx (1.7.4)", "sphinxcontrib-asyncio", "sphinxcontrib-websupport"] -voice = ["PyNaCl (1.3.0)"] +aiohttp = ">=3.6.0,<3.7.0" +websockets = ">=6.0,<7.0 || >7.0,<8.0 || >8.0,<8.0.1 || >8.0.1,<9.0" [[package]] category = "main" @@ -143,10 +130,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.16.0" -[package.extras] -DNSSEC = ["pycryptodome", "ecdsa (>=0.13)"] -IDNA = ["idna (>=2.1)"] - [[package]] category = "main" description = "Emoji for Python" @@ -155,9 +138,6 @@ optional = false python-versions = "*" version = "0.5.4" -[package.extras] -dev = ["nose", "coverage", "coveralls"] - [[package]] category = "main" description = "Backport of the concurrent.futures package from Python 3.2" @@ -172,7 +152,7 @@ description = "Git Object Database" name = "gitdb" optional = false python-versions = ">=3.4" -version = "4.0.4" +version = "4.0.5" [package.dependencies] smmap = ">=3.0.1,<4" @@ -183,7 +163,7 @@ description = "Python Git Library" name = "gitpython" optional = false python-versions = ">=3.4" -version = "3.1.1" +version = "3.1.3" [package.dependencies] gitdb = ">=4.0.1,<5" @@ -215,12 +195,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "4.3.21" -[package.extras] -pipfile = ["pipreqs", "requirementslib"] -pyproject = ["toml"] -requirements = ["pipreqs", "pip-api"] -xdg_home = ["appdirs (>=1.4.0)"] - [[package]] category = "dev" description = "A fast and thorough lazy object proxy." @@ -255,7 +229,7 @@ description = "multidict implementation" name = "multidict" optional = false python-versions = ">=3.5" -version = "4.7.5" +version = "4.7.6" [[package]] category = "main" @@ -274,7 +248,15 @@ description = "Parse human-readable date/time text." name = "parsedatetime" optional = false python-versions = "*" -version = "2.5" +version = "2.6" + +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.8.0" [[package]] category = "dev" @@ -290,13 +272,14 @@ description = "python code static checker" name = "pylint" optional = false python-versions = ">=3.5.*" -version = "2.4.4" +version = "2.5.2" [package.dependencies] -astroid = ">=2.3.0,<2.4" +astroid = ">=2.4.0,<=2.5" colorama = "*" isort = ">=4.2.5,<5" mccabe = ">=0.6,<0.7" +toml = ">=0.7.1" [[package]] category = "main" @@ -306,14 +289,6 @@ optional = true python-versions = "*" version = "3.10.1" -[package.extras] -encryption = ["pymongocrypt (<2.0.0)"] -gssapi = ["pykerberos"] -snappy = ["python-snappy"] -srv = ["dnspython (>=1.16.0,<1.17.0)"] -tls = ["ipaddress"] -zstd = ["zstandard"] - [[package]] category = "main" description = "Extensions to the standard Python datetime module" @@ -333,9 +308,6 @@ optional = false python-versions = "*" version = "0.10.5" -[package.extras] -cli = ["click (>=5.0)"] - [[package]] category = "dev" description = "YAML parser and emitter for Python" @@ -344,13 +316,21 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "5.3.1" +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2020.6.7" + [[package]] category = "main" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.14.0" +version = "1.15.0" [[package]] category = "dev" @@ -358,19 +338,18 @@ description = "A pure Python implementation of a sliding window memory map manag name = "smmap" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.0.2" +version = "3.0.4" [[package]] category = "dev" description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false -python-versions = "*" -version = "1.32.0" +python-versions = ">=3.6" +version = "2.0.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" -six = ">=1.10.0" [[package]] category = "dev" @@ -378,12 +357,11 @@ description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" optional = false python-versions = "*" -version = "0.10.0" +version = "0.10.1" [[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 = "*" @@ -402,8 +380,8 @@ category = "main" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" name = "websockets" optional = false -python-versions = ">=3.4" -version = "6.0" +python-versions = ">=3.6.1" +version = "8.1" [[package]] category = "dev" @@ -411,7 +389,7 @@ description = "Module for decorators, wrappers and monkey patching." name = "wrapt" optional = false python-versions = "*" -version = "1.11.2" +version = "1.12.1" [[package]] category = "main" @@ -429,343 +407,49 @@ multidict = ">=4.0" mongodb = ["motor"] [metadata] -content-hash = "793f86eb19d73f473f00883384ec965876c3930f41f94a171d2d1bc38f66903b" +content-hash = "68d5c15e62c4bf5f65fd3b0a9a2586b15557f724c3de5b756534bacd52cbfe40" python-versions = "^3.7" -[metadata.files] -aiohttp = [ - {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_10_x86_64.whl", hash = "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5"}, - {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed"}, - {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303"}, - {file = "aiohttp-3.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10"}, - {file = "aiohttp-3.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa"}, - {file = "aiohttp-3.5.4-cp35-cp35m-win32.whl", hash = "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4"}, - {file = "aiohttp-3.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72"}, - {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5"}, - {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12"}, - {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6"}, - {file = "aiohttp-3.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6"}, - {file = "aiohttp-3.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d"}, - {file = "aiohttp-3.5.4-cp36-cp36m-win32.whl", hash = "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1"}, - {file = "aiohttp-3.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d"}, - {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300"}, - {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390"}, - {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366"}, - {file = "aiohttp-3.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"}, - {file = "aiohttp-3.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55"}, - {file = "aiohttp-3.5.4-cp37-cp37m-win32.whl", hash = "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc"}, - {file = "aiohttp-3.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939"}, - {file = "aiohttp-3.5.4.tar.gz", hash = "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf"}, -] -appdirs = [ - {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, - {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, -] -astroid = [ - {file = "astroid-2.3.3-py3-none-any.whl", hash = "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"}, - {file = "astroid-2.3.3.tar.gz", hash = "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a"}, -] -async-timeout = [ - {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, - {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, -] -attrs = [ - {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, - {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, -] -bandit = [ - {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, - {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, -] -black = [ - {file = "black-19.3b0-py36-none-any.whl", hash = "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf"}, - {file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"}, -] -chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, -] -click = [ - {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, - {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, -] -colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, -] -"discord.py" = [ - {file = "discord.py-1.2.5-py3-none-any.whl", hash = "sha256:7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d"}, -] -dnspython = [ - {file = "dnspython-1.16.0-py2.py3-none-any.whl", hash = "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"}, - {file = "dnspython-1.16.0.zip", hash = "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01"}, -] -emoji = [ - {file = "emoji-0.5.4.tar.gz", hash = "sha256:60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174"}, -] -futures = [ - {file = "futures-3.1.1-py2-none-any.whl", hash = "sha256:c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"}, - {file = "futures-3.1.1-py3-none-any.whl", hash = "sha256:3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b"}, - {file = "futures-3.1.1.tar.gz", hash = "sha256:51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd"}, -] -gitdb = [ - {file = "gitdb-4.0.4-py3-none-any.whl", hash = "sha256:ba1132c0912e8c917aa8aa990bee26315064c7b7f171ceaaac0afeb1dc656c6a"}, - {file = "gitdb-4.0.4.tar.gz", hash = "sha256:6f0ecd46f99bb4874e5678d628c3a198e2b4ef38daea2756a2bfd8df7dd5c1a5"}, -] -gitpython = [ - {file = "GitPython-3.1.1-py3-none-any.whl", hash = "sha256:71b8dad7409efbdae4930f2b0b646aaeccce292484ffa0bc74f1195582578b3d"}, - {file = "GitPython-3.1.1.tar.gz", hash = "sha256:6d4f10e2aaad1864bb0f17ec06a2c2831534140e5883c350d58b4e85189dab74"}, -] -idna = [ - {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, - {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, -] -isodate = [ - {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"}, - {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"}, -] -isort = [ - {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, - {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, -] -lazy-object-proxy = [ - {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, - {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, - {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, - {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, - {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, - {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, - {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, - {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, - {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, - {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, - {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, - {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, - {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -motor = [ - {file = "motor-2.1.0-py2-none-any.whl", hash = "sha256:599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909"}, - {file = "motor-2.1.0-py3-none-any.whl", hash = "sha256:97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a"}, - {file = "motor-2.1.0.tar.gz", hash = "sha256:756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4"}, -] -multidict = [ - {file = "multidict-4.7.5-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3"}, - {file = "multidict-4.7.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35"}, - {file = "multidict-4.7.5-cp35-cp35m-win32.whl", hash = "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1"}, - {file = "multidict-4.7.5-cp35-cp35m-win_amd64.whl", hash = "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd"}, - {file = "multidict-4.7.5-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20"}, - {file = "multidict-4.7.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136"}, - {file = "multidict-4.7.5-cp36-cp36m-win32.whl", hash = "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e"}, - {file = "multidict-4.7.5-cp36-cp36m-win_amd64.whl", hash = "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78"}, - {file = "multidict-4.7.5-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8"}, - {file = "multidict-4.7.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab"}, - {file = "multidict-4.7.5-cp37-cp37m-win32.whl", hash = "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928"}, - {file = "multidict-4.7.5-cp37-cp37m-win_amd64.whl", hash = "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1"}, - {file = "multidict-4.7.5-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4"}, - {file = "multidict-4.7.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2"}, - {file = "multidict-4.7.5-cp38-cp38-win32.whl", hash = "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5"}, - {file = "multidict-4.7.5-cp38-cp38-win_amd64.whl", hash = "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969"}, - {file = "multidict-4.7.5.tar.gz", hash = "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e"}, -] -natural = [ - {file = "natural-0.2.0.tar.gz", hash = "sha256:18c83662d2d33fd7e6eee4e3b0d7366e1ce86225664e3127a2aaf0a3233f7df2"}, -] -parsedatetime = [ - {file = "parsedatetime-2.5-py2-none-any.whl", hash = "sha256:3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1"}, - {file = "parsedatetime-2.5.tar.gz", hash = "sha256:d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"}, -] -pbr = [ - {file = "pbr-5.4.5-py2.py3-none-any.whl", hash = "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"}, - {file = "pbr-5.4.5.tar.gz", hash = "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c"}, -] -pylint = [ - {file = "pylint-2.4.4-py3-none-any.whl", hash = "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"}, - {file = "pylint-2.4.4.tar.gz", hash = "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd"}, -] -pymongo = [ - {file = "pymongo-3.10.1-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee"}, - {file = "pymongo-3.10.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed"}, - {file = "pymongo-3.10.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6"}, - {file = "pymongo-3.10.1-cp27-cp27m-win32.whl", hash = "sha256:e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5"}, - {file = "pymongo-3.10.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe"}, - {file = "pymongo-3.10.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc"}, - {file = "pymongo-3.10.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764"}, - {file = "pymongo-3.10.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372"}, - {file = "pymongo-3.10.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32"}, - {file = "pymongo-3.10.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc"}, - {file = "pymongo-3.10.1-cp34-cp34m-win32.whl", hash = "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a"}, - {file = "pymongo-3.10.1-cp34-cp34m-win_amd64.whl", hash = "sha256:7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae"}, - {file = "pymongo-3.10.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d"}, - {file = "pymongo-3.10.1-cp35-cp35m-win32.whl", hash = "sha256:c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57"}, - {file = "pymongo-3.10.1-cp35-cp35m-win_amd64.whl", hash = "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70"}, - {file = "pymongo-3.10.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73"}, - {file = "pymongo-3.10.1-cp36-cp36m-win32.whl", hash = "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33"}, - {file = "pymongo-3.10.1-cp36-cp36m-win_amd64.whl", hash = "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6"}, - {file = "pymongo-3.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951"}, - {file = "pymongo-3.10.1-cp37-cp37m-win32.whl", hash = "sha256:c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39"}, - {file = "pymongo-3.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b"}, - {file = "pymongo-3.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be"}, - {file = "pymongo-3.10.1-cp38-cp38-win32.whl", hash = "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f"}, - {file = "pymongo-3.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac"}, - {file = "pymongo-3.10.1-py2.7-macosx-10.14-intel.egg", hash = "sha256:bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a"}, - {file = "pymongo-3.10.1-py2.7-win-amd64.egg", hash = "sha256:ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012"}, - {file = "pymongo-3.10.1-py2.7-win32.egg", hash = "sha256:f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a"}, - {file = "pymongo-3.10.1.tar.gz", hash = "sha256:993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa"}, -] -python-dateutil = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, -] -python-dotenv = [ - {file = "python-dotenv-0.10.5.tar.gz", hash = "sha256:f254bfd0c970d64ccbb6c9ebef3667ab301a71473569c991253a481f1c98dddc"}, - {file = "python_dotenv-0.10.5-py2.py3-none-any.whl", hash = "sha256:440c7c23d53b7d352f9c94d6f70860242c2f071cf5c029dd661ccb22d64ae42b"}, -] -pyyaml = [ - {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, - {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, - {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, - {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, - {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, -] -six = [ - {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, - {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, -] -smmap = [ - {file = "smmap-3.0.2-py2.py3-none-any.whl", hash = "sha256:52ea78b3e708d2c2b0cfe93b6fc3fbeec53db913345c26be6ed84c11ed8bebc1"}, - {file = "smmap-3.0.2.tar.gz", hash = "sha256:b46d3fc69ba5f367df96d91f8271e8ad667a198d5a28e215a6c3d9acd133a911"}, -] -stevedore = [ - {file = "stevedore-1.32.0-py2.py3-none-any.whl", hash = "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"}, - {file = "stevedore-1.32.0.tar.gz", hash = "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b"}, -] -toml = [ - {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, - {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, - {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, -] -typed-ast = [ - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, - {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, - {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, - {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, - {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, -] -uvloop = [ - {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, - {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"}, - {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"}, - {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"}, - {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"}, - {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"}, - {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"}, - {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, - {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, -] -websockets = [ - {file = "websockets-6.0-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d"}, - {file = "websockets-6.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484"}, - {file = "websockets-6.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e"}, - {file = "websockets-6.0-cp34-cp34m-win32.whl", hash = "sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"}, - {file = "websockets-6.0-cp34-cp34m-win_amd64.whl", hash = "sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96"}, - {file = "websockets-6.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538"}, - {file = "websockets-6.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0"}, - {file = "websockets-6.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1"}, - {file = "websockets-6.0-cp35-cp35m-win32.whl", hash = "sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d"}, - {file = "websockets-6.0-cp35-cp35m-win_amd64.whl", hash = "sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6"}, - {file = "websockets-6.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf"}, - {file = "websockets-6.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d"}, - {file = "websockets-6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb"}, - {file = "websockets-6.0-cp36-cp36m-win32.whl", hash = "sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c"}, - {file = "websockets-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff"}, - {file = "websockets-6.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908"}, - {file = "websockets-6.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559"}, - {file = "websockets-6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4"}, - {file = "websockets-6.0-cp37-cp37m-win32.whl", hash = "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136"}, - {file = "websockets-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584"}, - {file = "websockets-6.0.tar.gz", hash = "sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c"}, -] -wrapt = [ - {file = "wrapt-1.11.2.tar.gz", hash = "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"}, -] -yarl = [ - {file = "yarl-1.4.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b"}, - {file = "yarl-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1"}, - {file = "yarl-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080"}, - {file = "yarl-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a"}, - {file = "yarl-1.4.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f"}, - {file = "yarl-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea"}, - {file = "yarl-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb"}, - {file = "yarl-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70"}, - {file = "yarl-1.4.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d"}, - {file = "yarl-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce"}, - {file = "yarl-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"}, - {file = "yarl-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce"}, - {file = "yarl-1.4.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b"}, - {file = "yarl-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae"}, - {file = "yarl-1.4.2-cp38-cp38-win32.whl", hash = "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462"}, - {file = "yarl-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6"}, - {file = "yarl-1.4.2.tar.gz", hash = "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b"}, -] +[metadata.hashes] +aiohttp = ["1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", "259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", "2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", "32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", "344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", "460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", "4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", "50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", "6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", "65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", "ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", "b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"] +appdirs = ["7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", "a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"] +astroid = ["4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", "d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"] +async-timeout = ["0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"] +attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] +bandit = ["336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", "41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"] +black = ["1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", "c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"] +chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] +click = ["d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"] +colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"] +"discord.py" = ["406871b06d86c3dc49fba63238519f28628dac946fef8a0e22988ff58ec05580", "ad00e34c72d2faa8db2157b651d05f3c415d7d05078e7e41dc9e8dc240051beb"] +dnspython = ["36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", "f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"] +emoji = ["60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174"] +futures = ["3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b", "51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd", "c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"] +gitdb = ["91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"] +gitpython = ["e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a", "ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"] +idna = ["7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"] +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 = ["1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", "275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", "3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", "4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", "5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", "51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", "5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", "6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", "7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", "9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", "c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", "d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", "e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", "f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", "fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", "fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"] +natural = ["18c83662d2d33fd7e6eee4e3b0d7366e1ce86225664e3127a2aaf0a3233f7df2"] +parsedatetime = ["4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", "cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b"] +pathspec = ["7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", "da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"] +pbr = ["07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c", "579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"] +pylint = ["b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", "dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b"] +pymongo = ["01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", "0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", "1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", "18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", "19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", "20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103", "26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", "26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", "2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", "2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", "316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", "31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", "334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", "358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", "3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", "444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", "47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", "4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", "4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", "53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", "568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", "56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", "5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", "61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", "619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", "6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", "63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113", "6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", "7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", "7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae", "80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", "95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", "993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", "9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", "a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", "a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", "a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", "a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", "a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", "ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012", "ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", "b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", "b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", "bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", "bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a", "c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", "c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", "c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", "c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", "da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", "dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", "e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", "e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", "e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", "f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a", "f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606"] +python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"] +python-dotenv = ["440c7c23d53b7d352f9c94d6f70860242c2f071cf5c029dd661ccb22d64ae42b", "f254bfd0c970d64ccbb6c9ebef3667ab301a71473569c991253a481f1c98dddc"] +pyyaml = ["06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", "69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", "b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"] +regex = ["150125da109fccdcc8fec3b0b386b2a5d6ca7cff076f8b622486d1ca868b0c10", "163bc0805e46acfa098dfc8c0b07f371577d505f603e48afc425ff475cdac3a5", "20c513893ff80bdbe4b4ce11ea2e93d49481f05b270595d82af69ffc402010a6", "21fc17cb868c4264f0813f992f46f9ae6fc8c309d4741091de4153bd1f6a6176", "2c928bc8e0c453d73dffa3193a6e37ee752ea36df0dd4601e21024d98274dfad", "2d9beca70e36f9c60d679e108c5fe49f3d4da79d13a13f91e5e759443bd954f9", "5735f26cacdb50b3d6d35ebf8fdeb504bd8b381e2d079d2d9f12ce534fc14ecd", "6edc5c190248d3b612f2cca45448cf8ebc3621d41afcd1c5708853cbb1dbb3b3", "7606dba82435429641efe4fbc580574942f89cf2b9c5c1f8bc1eab2bacbf7e8b", "8d1ee3796795e609ef7a3a5a35eaf4728038d986aa12c06b3fd1b92ee81911f4", "8d9bb2d90e23c51aacbc58c1a11320f49b335cd67a91986cdbebcc3e843e4de8", "97d414c41f19fd2362e493810caa8445c05e0a2d63a14081c972aad66284a8d2", "9e37502817225ee99d91d8418f5119e98c380b00e772d06915690c05290f32ee", "af7209b2fcc79ee2b0ad4ea080d70bb748450ec4f282cc9e864861e469b1072e", "c0849b0864ff451f04c8afb5fc28e9ed592262e03debdd227cf0f53e04a55dcd", "c4ac9215650688e78dea29b46adbdafb7b85058eebe92ef6ea848e14466c915f", "dcda6d4e1bbfc939b177c237aee41c9678eaaf71df482688f8986e8251e12345", "dd8501b8d9ea1aba53c4bc7d47bc72933f9b4213d534cf400f16c1431f51c8ba", "ec0e509ed1877ff1cbc6f0864689bb60384a303502c4d72d9a635f8a4676fd3f", "f6c8c3f56fef719180464855346e6e80971b86dfd9e5a0e356664b5baca53072", "ffd4f80602490a309064cf2b203e220d581c51660e01055c64bf5da450485ee6"] +six = ["30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"] +smmap = ["54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"] +stevedore = ["001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9", "471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691"] +toml = ["926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", "bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"] +typed-ast = ["0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", "249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", "4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", "6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", "bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"] +uvloop = ["08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", "123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", "4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", "4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", "afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", "b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", "bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", "e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", "f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"] +websockets = ["0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", "1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", "20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", "295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", "2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", "3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", "3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", "3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", "4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", "5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", "5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", "751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", "7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", "965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", "9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", "9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", "c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", "c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", "ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", "d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", "e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", "f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"] +wrapt = ["b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"] +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 a114573201..6cedd6065c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,10 +36,10 @@ keywords = ['discord', 'modmail'] [tool.poetry.dependencies] python = "^3.7" -"discord.py" = "=1.2.5" -uvloop = "^0.14.0" +"discord.py" = "=1.3.3" +uvloop = {version=">=0.12.0", sys_platform = "!= 'win32'"} python-dotenv = "^0.10.3" -parsedatetime = "^2.5" +parsedatetime = "^2.6" dnspython = "^1.16" isodate = "^0.6.0" natural = "^0.2.0" @@ -47,10 +47,10 @@ 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" +aiohttp = ">=3.6.0,<3.7.0" [tool.poetry.dev-dependencies] -black = {version = "=19.3b0", allows-prereleases = true} +black = {version = "=19.10b0", allows-prereleases = true} pylint = "^2.4" bandit = "^1.6" diff --git a/requirements.min.txt b/requirements.min.txt index dded194188..e3ba980fc1 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -1,24 +1,24 @@ -# Generated as of October, 2019 +# Generated as of June, 2020 # This is the bare minimum requirements.txt for running Modmail. # To install requirements.txt run: pip install -r requirements.min.txt -aiohttp==3.5.4 +aiohttp==3.6.2 async-timeout==3.0.1 attrs==19.3.0 chardet==3.0.4 -discord.py==1.2.5 +discord.py==1.3.3 dnspython==1.16.0 emoji==0.5.4 -future==0.18.1 -idna==2.8 +future==0.18.2 +idna==2.9 isodate==0.6.0 -motor==2.0.0 -multidict==4.5.2 +motor==2.1.0 +multidict==4.7.6 natural==0.2.0 -parsedatetime==2.4 -pymongo==3.9.0 -python-dateutil==2.8.0 -python-dotenv==0.10.3 -six==1.12.0 -websockets==6.0 -yarl==1.3.0 +parsedatetime==2.6 +pymongo==3.10.1 +python-dateutil==2.8.1 +python-dotenv==0.13.0 +six==1.15.0 +websockets==8.1 +yarl==1.4.2 diff --git a/runtime.txt b/runtime.txt index 6919bf9ede..05802392e0 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.6 +python-3.7.7 \ No newline at end of file From fd61637a92ad556db121768def88a0654e3943e8 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 7 Jun 2020 00:22:36 -0700 Subject: [PATCH 0552/1221] Implement teams in metadata --- bot.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 694f860cf1..8016a65ec6 100644 --- a/bot.py +++ b/bot.py @@ -1196,10 +1196,9 @@ async def validate_database_connection(self): logger.line("debug") async def post_metadata(self): - owner = (await self.application_info()).owner + info = await self.application_info() + data = { - "owner_name": str(owner), - "owner_id": owner.id, "bot_id": self.user.id, "bot_name": str(self.user), "avatar_url": str(self.user.avatar_url), @@ -1213,6 +1212,19 @@ async def post_metadata(self): "last_updated": str(datetime.utcnow()), } + if discord.AppInfo.team is not None: + data.update( + { + "owner_name": info.team.owner.name + if info.team.owner is not None + else "No Owner", + "owner_id": info.team.owner_id, + "team": True, + } + ) + else: + data.update({"owner_name": info.owner.name, "owner_id": info.owner.id, "team": False}) + async with self.session.post("https://api.logviewer.tech/metadata", json=data): logger.debug("Uploading metadata to Modmail server.") From 67ef0a34d701bae662a5ef02d1b9f40f652cb18e Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 7 Jun 2020 05:16:01 -0700 Subject: [PATCH 0553/1221] Use event_type for raw reaction update, rename owner_ids -> bot_owner_ids to prevent overriding new owner_ids attribute for teams --- bot.py | 17 +++++++++-------- cogs/modmail.py | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/bot.py b/bot.py index 8016a65ec6..88b28ec61e 100644 --- a/bot.py +++ b/bot.py @@ -180,7 +180,7 @@ def run(self, *args, **kwargs): logger.error(" - Shutting down bot - ") @property - def owner_ids(self): + def bot_owner_ids(self): owner_ids = self.config["owners"] if owner_ids is not None: owner_ids = set(map(int, str(owner_ids).split(","))) @@ -192,7 +192,7 @@ def owner_ids(self): return owner_ids async def is_owner(self, user: discord.User) -> bool: - if user.id in self.owner_ids: + if user.id in self.bot_owner_ids: return True return await super().is_owner(user) @@ -416,7 +416,8 @@ 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.bot_owner_ids ) logger.info("Owners: %s", owners) logger.info("Prefix: %s", self.prefix) @@ -935,7 +936,7 @@ async def on_typing(self, channel, user, _): return await thread.recipient.trigger_typing() - async def handle_reaction_events(self, payload, *, add): + async def handle_reaction_events(self, payload): user = self.get_user(payload.user_id) if user.bot: return @@ -961,7 +962,7 @@ async def handle_reaction_events(self, payload, *, add): if not thread: return if ( - add + payload.event_type == "REACTION_ADD" and message.embeds and str(reaction) == str(close_emoji) and self.config.get("recipient_thread_close") @@ -992,7 +993,7 @@ async def handle_reaction_events(self, payload, *, add): logger.warning("Failed to find linked message for reactions: %s", e) return - if add: + if payload.event_type == "REACTION_ADD": if await self.add_reaction(linked_message, reaction): await self.add_reaction(message, reaction) else: @@ -1003,10 +1004,10 @@ async def handle_reaction_events(self, payload, *, add): logger.warning("Failed to remove reaction: %s", e) async def on_raw_reaction_add(self, payload): - await self.handle_reaction_events(payload, add=True) + await self.handle_reaction_events(payload) async def on_raw_reaction_remove(self, payload): - await self.handle_reaction_events(payload, add=False) + await self.handle_reaction_events(payload) async def on_guild_channel_delete(self, channel): if channel.guild != self.modmail_guild: diff --git a/cogs/modmail.py b/cogs/modmail.py index 3b9c591294..acbcd74722 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -120,8 +120,8 @@ async def setup(self, ctx): 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) + for owner_id in self.bot.bot_owner_ids: + await self.bot.update_perms(PermissionLevel.OWNER, owner_id) @commands.group(aliases=["snippets"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) From 48314e16185701444d5a9616497760fe000cdcba Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 7 Jun 2020 05:30:32 -0700 Subject: [PATCH 0554/1221] Channel delete no longer show warnings and threads get closed properly now --- bot.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bot.py b/bot.py index 88b28ec61e..57938637dd 100644 --- a/bot.py +++ b/bot.py @@ -1013,19 +1013,6 @@ async def on_guild_channel_delete(self, channel): if channel.guild != self.modmail_guild: return - 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 - # TODO: waiting for dpy - logger.warning("Failed to retrieve audit log: %s.", e) - return - - if mod == self.user: - return - if isinstance(channel, discord.CategoryChannel): if self.main_category == channel: logger.debug("Main category was deleted.") @@ -1042,6 +1029,19 @@ async def on_guild_channel_delete(self, channel): await self.config.update() return + audit_logs = self.modmail_guild.audit_logs() + entry = await audit_logs.find( + lambda a: a.target == channel and a.action == discord.AuditLogAction.channel_delete + ) + + if entry is None: + logger.debug("Cannot find the audit log entry for channel delete of %d.", channel.id) + return + + mod = entry.user + if mod == self.user: + return + thread = await self.threads.find(channel=channel) if thread and thread.channel == channel: logger.debug("Manually closed channel %s.", channel.name) From 04e15ef2fe041cbad72a80464414a285e158f1aa Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 7 Jun 2020 05:49:33 -0700 Subject: [PATCH 0555/1221] Use auditlog for message delete event to decrease error rate --- bot.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/bot.py b/bot.py index 57938637dd..a615554b4e 100644 --- a/bot.py +++ b/bot.py @@ -1069,8 +1069,10 @@ 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 isinstance(message.channel, discord.DMChannel): + if message.author == self.user: + return thread = await self.threads.find(recipient=message.author) if not thread: return @@ -1078,7 +1080,7 @@ async def on_message_delete(self, message): message = await thread.find_linked_message_from_dm(message) except ValueError as e: if str(e) != "Thread channel message not found.": - logger.warning("Failed to find linked message to delete: %s", e) + logger.debug("Failed to find linked message to delete: %s", e) return embed = message.embeds[0] embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=embed.footer.icon_url) @@ -1088,14 +1090,34 @@ async def on_message_delete(self, message): thread = await self.threads.find(channel=message.channel) if not thread: return + + audit_logs = self.modmail_guild.audit_logs() + entry = await audit_logs.find( + lambda a: a.target == message and a.action == discord.AuditLogAction.message_delete + ) + + if entry is None: + logger.debug("Cannot find the audit log entry for message delete of %d.", message.id) + elif entry.user == self.user: + return + try: await thread.delete_message(message, note=False) + embed = discord.Embed( + description="Successfully deleted message.", color=self.main_color + ) 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) - return + logger.debug("Failed to find linked message to delete: %s", e) + embed = discord.Embed( + description="Failed to delete message.", color=self.error_color + ) + else: + return except discord.NotFound: return + embed.set_footer(text=f"Message ID: {message.id} from {message.author}.") + return await message.channel.send(embed=embed) async def on_bulk_message_delete(self, messages): await discord.utils.async_all(self.on_message_delete(msg) for msg in messages) From f98354ca3075a2bfd7b4d25fba81f08abc4af297 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 7 Jun 2020 06:34:00 -0700 Subject: [PATCH 0556/1221] Slight error fix --- bot.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/bot.py b/bot.py index a615554b4e..4b6033c40a 100644 --- a/bot.py +++ b/bot.py @@ -499,6 +499,17 @@ async def on_ready(self): self.metadata_loop.before_loop(self.before_post_metadata) self.metadata_loop.start() + other_guilds = [ + guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild} + ] + if any(other_guilds): + logger.warning( + "The bot is in more servers other than the main and staff server." + "This may cause data compromise (%s).", + ", ".join(guild.name for guild in other_guilds), + ) + logger.warning("If the external servers are valid, you may ignore this message.") + async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() @@ -1029,10 +1040,10 @@ async def on_guild_channel_delete(self, channel): await self.config.update() return - audit_logs = self.modmail_guild.audit_logs() - entry = await audit_logs.find( - lambda a: a.target == channel and a.action == discord.AuditLogAction.channel_delete + audit_logs = self.modmail_guild.audit_logs( + limit=10, action=discord.AuditLogAction.channel_delete ) + entry = await audit_logs.find(lambda a: int(a.target.id) == channel.id) if entry is None: logger.debug("Cannot find the audit log entry for channel delete of %d.", channel.id) @@ -1070,6 +1081,9 @@ async def on_member_join(self, member): async def on_message_delete(self, message): """Support for deleting linked messages""" + if message.is_system(): + return + if isinstance(message.channel, discord.DMChannel): if message.author == self.user: return @@ -1087,18 +1101,20 @@ async def on_message_delete(self, message): await message.edit(embed=embed) return + if message.author != self.user: + return + thread = await self.threads.find(channel=message.channel) if not thread: return - audit_logs = self.modmail_guild.audit_logs() - entry = await audit_logs.find( - lambda a: a.target == message and a.action == discord.AuditLogAction.message_delete + audit_logs = self.modmail_guild.audit_logs( + limit=10, action=discord.AuditLogAction.message_delete ) + entry = await audit_logs.find(lambda a: a.target == self.user) + if entry is None: - logger.debug("Cannot find the audit log entry for message delete of %d.", message.id) - elif entry.user == self.user: return try: @@ -1235,7 +1251,7 @@ async def post_metadata(self): "last_updated": str(datetime.utcnow()), } - if discord.AppInfo.team is not None: + if info.team is not None: data.update( { "owner_name": info.team.owner.name From 9a92a7418f619deebceb96a9a62055fb0ecd2c38 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 7 Jun 2020 06:39:40 -0700 Subject: [PATCH 0557/1221] Add skeleton for dpy dump update --- CHANGELOG.md | 9 +++++++++ bot.py | 2 +- pyproject.toml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dcb9acbcd..3b5fcb7515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. +# v3.5.0dev1 + +### Changed + +- Bump discord.py version to v1.3.3. +- Renamed `bot.owner_ids` to `bot.bot_owner_ids` as the attribute is now defined interally for team support. +- Deleting channel manually will now close the thread. +- Deleting messages will no longer cause the bot to produce warnings. + # v3.4.1 ### Fixed diff --git a/bot.py b/bot.py index 4b6033c40a..9b575bbdce 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.4.1" +__version__ = "3.5.0-dev1" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 6cedd6065c..eb11a19cb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.4.1' +version = '3.5.0-dev1' description = "Modmail is similar to Reddit's 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 6bf3064c169c3aa7c5db9352d163fe40c0dbc476 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Sun, 7 Jun 2020 16:40:06 -0700 Subject: [PATCH 0558/1221] disable eval config --- CHANGELOG.md | 7 ++++++- bot.py | 6 +++++- cogs/utility.py | 3 +++ core/config.py | 6 ++++-- pyproject.toml | 2 +- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5fcb7515..191ed404a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,12 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.5.0dev1 +# v3.5.0dev2 + +### Added + +- A confirmation when you manually delete a thread message embed. +- Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. (GH #2803) ### Changed diff --git a/bot.py b/bot.py index 9b575bbdce..d84e3e1cc9 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.5.0-dev1" +__version__ = "3.5.0-dev2" import asyncio @@ -1200,6 +1200,10 @@ async def on_command_error(self, context, exception): corrected_permission_level.name, ) logger.warning("CheckFailure: %s", exception) + elif isinstance(exception, commands.DisabledCommand): + logger.info( + "DisabledCommand: %s is trying to run eval but it's disabled", context.author.name + ) else: logger.error("Unexpected exception:", exc_info=exception) diff --git a/cogs/utility.py b/cogs/utility.py index f8460c7d52..bd04bd6a09 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -239,6 +239,9 @@ def __init__(self, bot): ) self.bot.help_command.cog = self self.loop_presence.start() # pylint: disable=no-member + if not self.bot.config.get("enable_eval"): + self.eval_.enabled = False + logger.info("Eval disabled. enable_eval=False") def cog_unload(self): self.bot.help_command = self._original_help_command diff --git a/core/config.py b/core/config.py index a0eda4a2dc..a34c5a8e73 100644 --- a/core/config.py +++ b/core/config.py @@ -81,7 +81,7 @@ class ConfigManager: "activity_type": None, "status": None, # dm_disabled 0 = none, 1 = new threads, 2 = all threads - # TODO: use emum + # TODO: use enum "dm_disabled": 0, "oauth_whitelist": [], # moderation @@ -110,9 +110,10 @@ class ConfigManager: "owners": None, # bot "token": None, + "enable_plugins": True, + "enable_eval": False, # Logging "log_level": "INFO", - "enable_plugins": True, } colors = {"mod_color", "recipient_color", "main_color", "error_color"} @@ -128,6 +129,7 @@ class ConfigManager: "thread_auto_close_silently", "thread_move_notify", "enable_plugins", + "enable_eval", } special_types = {"status", "activity_type"} diff --git a/pyproject.toml b/pyproject.toml index eb11a19cb7..54f4efb4e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.5.0-dev1' +version = '3.5.0' description = "Modmail is similar to Reddit's 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 a7b155b1d0e12fb536d78836d62bd55eb5af4290 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Mon, 8 Jun 2020 16:30:47 -0700 Subject: [PATCH 0559/1221] Plugins change --- CHANGELOG.md | 4 +++- bot.py | 2 +- cogs/plugins.py | 64 +++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 191ed404a1..f82dcdc294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.5.0dev2 +# v3.5.0dev3 ### Added - A confirmation when you manually delete a thread message embed. - Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. (GH #2803) +- Added `?plugins reset` command to completely reset everything related to plugins. This will fix some problems caused by broken plugins in the file system. ### Changed @@ -20,6 +21,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Renamed `bot.owner_ids` to `bot.bot_owner_ids` as the attribute is now defined interally for team support. - Deleting channel manually will now close the thread. - Deleting messages will no longer cause the bot to produce warnings. +- Plugins will automatically be removed when it fails to load. # v3.4.1 diff --git a/bot.py b/bot.py index d84e3e1cc9..2d0d66f047 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.5.0-dev2" +__version__ = "3.5.0-dev3" import asyncio diff --git a/cogs/plugins.py b/cogs/plugins.py index d14035286d..09dde2b392 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -136,7 +136,12 @@ async def initial_load_plugins(self): await self.download_plugin(plugin) await self.load_plugin(plugin) except Exception: - logger.error("Error when loading plugin %s.", plugin, exc_info=True) + self.bot.config["plugins"].remove(plugin_name) + logger.error( + "Error when loading plugin %s. Plugin removed from config.", + plugin, + exc_info=True, + ) continue logger.debug("Finished loading all plugins.") @@ -415,17 +420,28 @@ async def update_plugin(self, ctx, plugin_name): return await ctx.send(embed=embed) async with ctx.typing(): + embed = discord.Embed( + description=f"Successfully updated {plugin.name}.", color=self.bot.main_color + ) await self.download_plugin(plugin, force=True) if self.bot.config.get("enable_plugins"): try: self.bot.unload_extension(plugin.ext_string) except commands.ExtensionError: logger.warning("Plugin unload fail.", exc_info=True) - await self.load_plugin(plugin) - logger.debug("Updated %s.", plugin_name) - embed = discord.Embed( - description=f"Successfully updated {plugin.name}.", color=self.bot.main_color - ) + try: + await self.load_plugin(plugin) + except Exception: + embed = discord.Embed( + description=f"Failed to update {plugin.name}. This plugin will now be removed from your bot.", + color=self.bot.error_color, + ) + self.bot.config["plugins"].remove(plugin_name) + logger.debug("Failed to update %s. Removed plugin from config.", plugin_name) + else: + logger.debug("Updated %s.", plugin_name) + else: + logger.debug("Updated %s.", plugin_name) return await ctx.send(embed=embed) @plugins.command(name="update") @@ -442,11 +458,45 @@ 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"]: + for plugin_name in list(self.bot.config["plugins"]): await self.update_plugin(ctx, plugin_name) else: await self.update_plugin(ctx, plugin_name) + @plugins.command(name="reset") + @checks.has_permissions(PermissionLevel.OWNER) + async def plugins_reset(self, ctx): + """ + Reset all plugins for the bot. + + Deletes all cache and plugins from config and unloads from the bot. + """ + logger.warning("Purging plugins.") + for ext in list(self.bot.extensions): + if not ext.startswith("plugins."): + continue + try: + logger.error("Unloading plugin: %s.", ext) + self.bot.unload_extension(ext) + except Exception: + logger.error("Failed to unload plugin: %s.", ext) + self.bot.config["plugins"].clear() + + cache_path = Path(__file__).absolute().parent.parent / "temp" / "plugins-cache" + if cache_path.exists(): + logger.warning("Removing cache path.") + shutil.rmtree(cache_path) + + for entry in os.scandir(Path(__file__).absolute().parent.parent / "plugins"): + if entry.is_dir(): + shutil.rmtree(entry.path) + logger.warning("Removing %s.", entry.name) + + embed = discord.Embed( + description=f"Successfully purged all plugins from the bot.", color=self.bot.main_color + ) + return await ctx.send(embed=embed) + @plugins.command(name="loaded", aliases=["enabled", "installed"]) @checks.has_permissions(PermissionLevel.OWNER) async def plugins_loaded(self, ctx): From fcacfb6071f3d18c4a1a3265ea42bd48e75ceaa7 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Jun 2020 02:40:04 -0700 Subject: [PATCH 0560/1221] Add to changelog and bump version --- CHANGELOG.md | 3 ++- bot.py | 2 +- cogs/plugins.py | 7 ++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f82dcdc294..baa39edb39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,14 @@ This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2. however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.5.0dev3 +# v3.5.0dev4 ### Added - A confirmation when you manually delete a thread message embed. - Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. (GH #2803) - Added `?plugins reset` command to completely reset everything related to plugins. This will fix some problems caused by broken plugins in the file system. +- Support private GitHub repos for plugins (thanks to @officialpiyush pr#2767) ### Changed diff --git a/bot.py b/bot.py index 2d0d66f047..28d786b3a9 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.5.0-dev3" +__version__ = "3.5.0-dev4" import asyncio diff --git a/cogs/plugins.py b/cogs/plugins.py index 74881f81f8..8ea358bcb3 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -157,11 +157,12 @@ async def download_plugin(self, plugin, force=False): if plugin.cache_path.exists() and not force: plugin_io = plugin.cache_path.open("rb") logger.debug("Loading cached %s.", plugin.cache_path) - else: headers = {} - if self.bot.config.get("github_token") is not None: - headers["Authorization"] = f"token {self.bot.config.get('github_token')}" + github_token = self.bot.config.get("github_token") + if github_token is not None: + headers["Authorization"] = f"token {github_token}" + async with self.bot.session.get(plugin.url, headers=headers) as resp: logger.debug("Downloading %s.", plugin.url) raw = await resp.read() From 691f916fb9ed22c3c00a97fae3b261b45e0f43d3 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Jun 2020 02:47:19 -0700 Subject: [PATCH 0561/1221] Bump version for windows plugins support --- CHANGELOG.md | 7 ++++++- bot.py | 7 +++++-- cogs/plugins.py | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index baa39edb39..eda457f786 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 do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.5.0dev4 +# v3.5.0dev5 ### Added @@ -24,6 +24,11 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Deleting messages will no longer cause the bot to produce warnings. - Plugins will automatically be removed when it fails to load. +### Fixed + +- Plugins not loading in Windows OS. Now uses proactor event loop for asyncio which should fix this. + + # v3.4.1 ### Fixed diff --git a/bot.py b/bot.py index 854a81e0b4..a1a2f38717 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.5.0-dev4" +__version__ = "3.5.0-dev5" import asyncio @@ -47,7 +47,10 @@ os.mkdir(temp_dir) if sys.platform == 'win32': - asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + try: + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + except AttributeError: + logger.error('Failed to use WindowsProactorEventLoopPolicy.', exc_info=True) class ModmailBot(commands.Bot): diff --git a/cogs/plugins.py b/cogs/plugins.py index 8ea358bcb3..df8259736c 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -159,7 +159,7 @@ async def download_plugin(self, plugin, force=False): logger.debug("Loading cached %s.", plugin.cache_path) else: headers = {} - github_token = self.bot.config.get("github_token") + github_token = self.bot.config["github_token"] if github_token is not None: headers["Authorization"] = f"token {github_token}" From ac4437737935936596e48be923b7812c27ffe8b4 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Jun 2020 02:47:49 -0700 Subject: [PATCH 0562/1221] Black format --- bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index a1a2f38717..5090402d72 100644 --- a/bot.py +++ b/bot.py @@ -46,11 +46,11 @@ if not os.path.exists(temp_dir): os.mkdir(temp_dir) -if sys.platform == 'win32': +if sys.platform == "win32": try: asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) except AttributeError: - logger.error('Failed to use WindowsProactorEventLoopPolicy.', exc_info=True) + logger.error("Failed to use WindowsProactorEventLoopPolicy.", exc_info=True) class ModmailBot(commands.Bot): From 15372988d3479afc6d43e082c34e85af6fc208ec Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Jun 2020 05:33:50 -0700 Subject: [PATCH 0563/1221] Add additional database support potentials --- CHANGELOG.md | 4 +- bot.py | 83 +++------------------- cogs/modmail.py | 17 +---- core/clients.py | 180 +++++++++++++++++++++++++++++++++++++++++++++--- core/config.py | 2 + 5 files changed, 186 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eda457f786..248a55d603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,10 +19,12 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Changed - Bump discord.py version to v1.3.3. -- Renamed `bot.owner_ids` to `bot.bot_owner_ids` as the attribute is now defined interally for team support. +- Renamed `bot.owner_ids` to `bot.bot_owner_ids` as the attribute is now defined internally for team support. - Deleting channel manually will now close the thread. - Deleting messages will no longer cause the bot to produce warnings. - Plugins will automatically be removed when it fails to load. +- Moved all database-related activities to clients.py under MongoDBClient, with possible future hook for additional database support. +- Deprecated `bot.plugin_db.get_partition` in favour of `bot.api.get_plugin_partition` (not final). ### Fixed diff --git a/bot.py b/bot.py index 5090402d72..bba04ff50d 100644 --- a/bot.py +++ b/bot.py @@ -18,8 +18,6 @@ from aiohttp import ClientSession from emoji import UNICODE_EMOJI -from motor.motor_asyncio import AsyncIOMotorClient -from pymongo.errors import ConfigurationError from pkg_resources import parse_version @@ -32,7 +30,7 @@ pass from core import checks -from core.clients import ApiClient, PluginDatabaseClient +from core.clients import ApiClient, PluginDatabaseClient, MongoDBClient from core.config import ConfigManager from core.utils import human_join, normalize_alias from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging @@ -72,22 +70,7 @@ def __init__(self): self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") self._configure_logging() - mongo_uri = self.config["mongo_uri"] - if mongo_uri is None: - logger.critical("A Mongo URI is necessary for the bot to function.") - raise RuntimeError - - try: - self.db = AsyncIOMotorClient(mongo_uri).modmail_bot - except ConfigurationError as e: - logger.critical( - "Your MONGO_URI might be copied wrong, try re-copying from the source again. " - "Otherwise noted in the following message:" - ) - logger.critical(e) - sys.exit(0) - - self.plugin_db = PluginDatabaseClient(self) + self.plugin_db = PluginDatabaseClient(self) # Deprecated self.startup() @property @@ -158,7 +141,11 @@ def session(self) -> ClientSession: @property def api(self) -> ApiClient: if self._api is None: - self._api = ApiClient(self) + if self.config["database_type"].lower() == "mongodb": + self._api = MongoDBClient(self) + else: + logger.critical("Invalid database type.") + raise RuntimeError return self._api async def get_prefix(self, message=None): @@ -376,37 +363,16 @@ def command_perm(self, command_name: str) -> PermissionLevel: async def on_connect(self): try: - await self.validate_database_connection() + await self.api.validate_database_connection() except Exception: logger.debug("Logging out due to failed database connection.") return await self.logout() logger.debug("Connected to gateway.") await self.config.refresh() - await self.setup_indexes() + await self.api.setup_indexes() self._connected.set() - async def setup_indexes(self): - """Setup text indexes so we can use the $search operator""" - coll = self.db.logs - index_name = "messages.content_text_messages.author.name_text_key_text" - - index_info = await coll.index_information() - - # Backwards compatibility - old_index = "messages.content_text_messages.author.name_text" - if old_index in index_info: - logger.info("Dropping old index: %s", old_index) - await coll.drop_index(old_index) - - if index_name not in index_info: - 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")] - ) - logger.debug("Successfully configured and verified database indexes.") - async def on_ready(self): """Bot startup, sets uptime.""" @@ -1213,37 +1179,6 @@ async def on_command_error(self, context, exception): else: logger.error("Unexpected exception:", exc_info=exception) - async def validate_database_connection(self): - try: - await self.db.command("buildinfo") - except Exception as exc: - logger.critical("Something went wrong while connecting to the database.") - message = f"{type(exc).__name__}: {str(exc)}" - logger.critical(message) - - if "ServerSelectionTimeoutError" in message: - logger.critical( - "This may have been caused by not whitelisting " - "IPs correctly. Make sure to whitelist all " - "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" - ) - - if "OperationFailure" in message: - logger.critical( - "This is due to having invalid credentials in your MONGO_URI. " - "Remember you need to substitute `` with your actual password." - ) - logger.critical( - "Be sure to URL encode your username and password (not the entire URL!!), " - "https://www.urlencoder.io/, if this issue persists, try changing your username and password " - "to only include alphanumeric characters, no symbols." - "" - ) - raise - else: - logger.debug("Successfully connected to the database.") - logger.line("debug") - async def post_metadata(self): info = await self.application_info() diff --git a/cogs/modmail.py b/cogs/modmail.py index acbcd74722..5c4de73e26 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -669,12 +669,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)} - - projection = {"messages": {"$slice": 5}} - - entries = await self.bot.db.logs.find(query, projection).to_list(None) - + entries = await self.bot.api.search_closed_by(user.id) embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) if not embeds: @@ -748,15 +743,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): await ctx.trigger_typing() - query = { - "guild_id": str(self.bot.guild_id), - "open": False, - "$text": {"$search": f'"{query}"'}, - } - - projection = {"messages": {"$slice": 5}} - - entries = await self.bot.db.logs.find(query, projection).to_list(limit) + entries = await self.bot.api.search_by_text(query, limit) embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) diff --git a/core/clients.py b/core/clients.py index 54cc28c9be..1e6008e042 100644 --- a/core/clients.py +++ b/core/clients.py @@ -1,18 +1,21 @@ import secrets +import sys from datetime import datetime from json import JSONDecodeError -from typing import Union +from typing import Union, Optional from discord import Member, DMChannel, TextChannel, Message from aiohttp import ClientResponseError, ClientResponse +from motor.motor_asyncio import AsyncIOMotorClient +from pymongo.errors import ConfigurationError from core.models import getLogger logger = getLogger(__name__) -class RequestClient: +class ApiClient: """ This class represents the general request class for all type of clients. @@ -29,8 +32,9 @@ class RequestClient: The bot's current running `ClientSession`. """ - def __init__(self, bot): + def __init__(self, bot, db): self.bot = bot + self.db = db self.session = bot.session async def request( @@ -74,16 +78,152 @@ async def request( except (JSONDecodeError, ClientResponseError): return await resp.text() - -class ApiClient(RequestClient): - @property - def db(self): - return self.bot.db - @property def logs(self): return self.db.logs + async def setup_indexes(self): + return NotImplemented + + async def validate_database_connection(self): + return NotImplemented + + async def get_user_logs(self, user_id: Union[str, int]) -> list: + return NotImplemented + + async def get_latest_user_logs(self, user_id: Union[str, int]): + return NotImplemented + + async def get_responded_logs(self, user_id: Union[str, int]) -> list: + return NotImplemented + + async def get_open_logs(self) -> list: + return NotImplemented + + async def get_log(self, channel_id: Union[str, int]) -> dict: + return NotImplemented + + async def get_log_link(self, channel_id: Union[str, int]) -> str: + return NotImplemented + + async def create_log_entry( + self, recipient: Member, channel: TextChannel, creator: Member + ) -> str: + return NotImplemented + + async def delete_log_entry(self, key: str) -> bool: + return NotImplemented + + async def get_config(self) -> dict: + return NotImplemented + + async def update_config(self, data: dict): + return NotImplemented + + async def edit_message(self, message_id: Union[int, str], new_content: str) -> None: + return NotImplemented + + async def append_log( + self, + message: Message, + *, + message_id: str = "", + channel_id: str = "", + type_: str = "thread_message", + ) -> dict: + return NotImplemented + + async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: + return NotImplemented + + async def search_closed_by(self, user_id: Union[int, str]): + return NotImplemented + + async def search_by_text(self, text: str, limit: Optional[int]): + return NotImplemented + + def get_plugin_partition(self, cog): + return NotImplemented + + +class MongoDBClient(ApiClient): + def __init__(self, bot): + mongo_uri = bot.config["connection_uri"] + if mongo_uri is None: + mongo_uri = bot.config["mongo_uri"] + if mongo_uri is not None: + logger.warning( + "You're using the old config MONGO_URI, " + "consider switching to the new CONNECTION_URI config." + ) + else: + logger.critical("A Mongo URI is necessary for the bot to function.") + raise RuntimeError + + try: + db = AsyncIOMotorClient(mongo_uri).modmail_bot + except ConfigurationError as e: + logger.critical( + "Your MONGO_URI might be copied wrong, try re-copying from the source again. " + "Otherwise noted in the following message:" + ) + logger.critical(e) + sys.exit(0) + + super().__init__(bot, db) + + async def setup_indexes(self): + """Setup text indexes so we can use the $search operator""" + coll = self.db.logs + index_name = "messages.content_text_messages.author.name_text_key_text" + + index_info = await coll.index_information() + + # Backwards compatibility + old_index = "messages.content_text_messages.author.name_text" + if old_index in index_info: + logger.info("Dropping old index: %s", old_index) + await coll.drop_index(old_index) + + if index_name not in index_info: + 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")] + ) + logger.debug("Successfully configured and verified database indexes.") + + async def validate_database_connection(self): + try: + await self.db.command("buildinfo") + except Exception as exc: + logger.critical("Something went wrong while connecting to the database.") + message = f"{type(exc).__name__}: {str(exc)}" + logger.critical(message) + + if "ServerSelectionTimeoutError" in message: + logger.critical( + "This may have been caused by not whitelisting " + "IPs correctly. Make sure to whitelist all " + "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" + ) + + if "OperationFailure" in message: + logger.critical( + "This is due to having invalid credentials in your MONGO_URI. " + "Remember you need to substitute `` with your actual password." + ) + logger.critical( + "Be sure to URL encode your username and password (not the entire URL!!), " + "https://www.urlencoder.io/, if this issue persists, try changing your username and password " + "to only include alphanumeric characters, no symbols." + "" + ) + raise + else: + logger.debug("Successfully connected to the database.") + logger.line("debug") + async def get_user_logs(self, user_id: Union[str, int]) -> list: query = {"recipient.id": str(user_id), "guild_id": str(self.bot.guild_id)} projection = {"messages": {"$slice": 5}} @@ -245,6 +385,26 @@ async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: {"channel_id": str(channel_id)}, {"$set": data}, return_document=True ) + async def search_closed_by(self, user_id: Union[int, str]): + return await self.logs.find( + {"guild_id": str(self.bot.guild_id), "open": False, "closer.id": str(user_id)}, + {"messages": {"$slice": 5}}, + ).to_list(None) + + async def search_by_text(self, text: str, limit: Optional[int]): + return await self.bot.db.logs.find( + { + "guild_id": str(self.bot.guild_id), + "open": False, + "$text": {"$search": f'"{text}"'}, + }, + {"messages": {"$slice": 5}}, + ).to_list(limit) + + def get_plugin_partition(self, cog): + cls_name = cog.__class__.__name__ + return self.db.plugins[cls_name] + class PluginDatabaseClient: def __init__(self, bot): @@ -252,4 +412,4 @@ def __init__(self, bot): def get_partition(self, cog): cls_name = cog.__class__.__name__ - return self.bot.db.plugins[cls_name] + return self.bot.api.db.plugins[cls_name] diff --git a/core/config.py b/core/config.py index 786729fc75..f945557718 100644 --- a/core/config.py +++ b/core/config.py @@ -107,6 +107,8 @@ class ConfigManager: "log_url": "https://example.com/", "log_url_prefix": "/logs", "mongo_uri": None, + "database_type": "mongodb", + "connection_uri": None, # replace mongo uri in the future "owners": None, # bot "token": None, From 491d07a60eedfb7d38c821939a81bee3f8cc2fc3 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Jun 2020 05:44:50 -0700 Subject: [PATCH 0564/1221] Update app.json --- CHANGELOG.md | 1 + app.json | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 248a55d603..0956794786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Plugins will automatically be removed when it fails to load. - Moved all database-related activities to clients.py under MongoDBClient, with possible future hook for additional database support. - Deprecated `bot.plugin_db.get_partition` in favour of `bot.api.get_plugin_partition` (not final). +- Deprecated `MONGO_URI` config var (but will keep support in the future) in favour of `CONNECTION_URI` and `DATABASE_TYPE`. Right now there is one supported database - "mongodb", which is the default. ### Fixed diff --git a/app.json b/app.json index 76fe320a30..b6a833b1ef 100644 --- a/app.json +++ b/app.json @@ -15,10 +15,14 @@ "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", "required": true }, - "MONGO_URI": { - "description": "Mongo DB connection URI for self-hosting your data.", + "CONNECTION_URI": { + "description": "The connection URI for your database.", "required": true }, + "DATABASE_TYPE": { + "description": "The type of your database. There is only one supported database at the moment - MongoDB (default).", + "required": false + }, "LOG_URL": { "description": "The url of the log viewer app for viewing self-hosted logs.", "required": true From f6957c415b25e0852231b54bc22f2a5953e8dc7b Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Jun 2020 17:58:19 -0700 Subject: [PATCH 0565/1221] Update Pipfile --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index 16bbac8e49..e506daf6dd 100644 --- a/Pipfile +++ b/Pipfile @@ -21,6 +21,7 @@ parsedatetime = "==2.6" aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" +pip = ">=18.0" "discord.py" = "==1.3.3" [requires] From c12a16e0647d181c09c4ec47f86381d33539ba78 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Jun 2020 18:01:05 -0700 Subject: [PATCH 0566/1221] Add pip to pipfile, apparently theres a dependency problem? --- Pipfile.lock | 177 +++++++++++++++++++++++++++++---------------------- 1 file changed, 100 insertions(+), 77 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 14f91762aa..d8c22e241e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bfb85b7c7862e17e905d739f0fb63248d3bf446035f32e3afac619f6d2c5a0ba" + "sha256": "cd8e330d55a4f896b01872d9fd56534288882d1ab6c68f0fc0841a13d8df9f00" }, "pipfile-spec": 6, "requires": { @@ -46,6 +46,7 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -53,14 +54,15 @@ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==19.3.0" }, "certifi": { "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1", + "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc" ], - "version": "==2020.4.5.1" + "version": "==2020.4.5.2" }, "chardet": { "hashes": [ @@ -118,6 +120,7 @@ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9" }, "importlib-metadata": { @@ -165,6 +168,7 @@ "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], + "markers": "python_version >= '3.5'", "version": "==4.7.6" }, "natural": { @@ -192,59 +196,62 @@ }, "pymongo": { "hashes": [ - "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", - "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", - "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", - "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", - "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", - "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103", - "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", - "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", - "sha256:2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", - "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", - "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", - "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", - "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", - "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", - "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", - "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", - "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", - "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", "sha256:4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", - "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", - "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", - "sha256:56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", - "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", - "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", + "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", + "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606", + "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", + "sha256:ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012", + "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", "sha256:619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", - "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", + "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", + "sha256:f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a", + "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", + "sha256:bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a", + "sha256:2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", + "sha256:56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", "sha256:63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113", - "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", - "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", "sha256:7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae", - "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", - "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", - "sha256:993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", - "sha256:9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", - "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", - "sha256:a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", - "sha256:a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", "sha256:a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", - "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", - "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", - "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", - "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", - "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", "sha256:c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", - "sha256:c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", - "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", + "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", + "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", + "sha256:993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", + "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", "sha256:c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", - "sha256:da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", - "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", "sha256:e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", + "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", + "sha256:9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", + "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", + "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", + "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", + "sha256:a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", + "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", + "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", + "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", + "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", + "sha256:a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", + "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", + "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", + "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", + "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", + "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", + "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", + "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", + "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", "sha256:e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", + "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", + "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", + "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", "sha256:e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", - "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606" + "sha256:c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", + "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", + "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", + "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", + "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", + "sha256:da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", + "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", + "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", + "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103" ], "version": "==3.10.1" }, @@ -269,6 +276,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "uvloop": { @@ -292,6 +300,7 @@ "sha256:a116629d4e7f4d03433b8afa27f43deba09d48bc48f5ecefa4f015a178efb6cf", "sha256:a730548b27366c5e6cbdf6f97406d861cccece2e22275e8e1a757aeff5e00c70" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.0.21" }, "virtualenv-clone": { @@ -299,6 +308,7 @@ "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.5.4" }, "websockets": { @@ -326,6 +336,7 @@ "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" ], + "markers": "python_full_version >= '3.6.1'", "version": "==8.1" }, "yarl": { @@ -348,6 +359,7 @@ "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" ], + "markers": "python_version >= '3.5'", "version": "==1.4.2" }, "zipp": { @@ -355,6 +367,7 @@ "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" ], + "markers": "python_version >= '3.6'", "version": "==3.1.0" } }, @@ -368,16 +381,18 @@ }, "astroid": { "hashes": [ - "sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", - "sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38" + "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", + "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], - "version": "==2.4.1" + "markers": "python_version >= '3.5'", + "version": "==2.4.2" }, "attrs": { "hashes": [ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==19.3.0" }, "bandit": { @@ -401,6 +416,7 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "gitdb": { @@ -408,6 +424,7 @@ "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], + "markers": "python_version >= '3.4'", "version": "==4.0.5" }, "gitpython": { @@ -415,6 +432,7 @@ "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a", "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac" ], + "markers": "python_version >= '3.4'", "version": "==3.1.3" }, "isort": { @@ -422,6 +440,7 @@ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==4.3.21" }, "lazy-object-proxy": { @@ -448,6 +467,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -473,11 +493,11 @@ }, "pylint": { "hashes": [ - "sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", - "sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b" + "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", + "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" ], "index": "pypi", - "version": "==2.5.2" + "version": "==2.5.3" }, "pyyaml": { "hashes": [ @@ -497,35 +517,36 @@ }, "regex": { "hashes": [ - "sha256:150125da109fccdcc8fec3b0b386b2a5d6ca7cff076f8b622486d1ca868b0c10", - "sha256:163bc0805e46acfa098dfc8c0b07f371577d505f603e48afc425ff475cdac3a5", - "sha256:20c513893ff80bdbe4b4ce11ea2e93d49481f05b270595d82af69ffc402010a6", - "sha256:21fc17cb868c4264f0813f992f46f9ae6fc8c309d4741091de4153bd1f6a6176", - "sha256:2c928bc8e0c453d73dffa3193a6e37ee752ea36df0dd4601e21024d98274dfad", - "sha256:2d9beca70e36f9c60d679e108c5fe49f3d4da79d13a13f91e5e759443bd954f9", - "sha256:5735f26cacdb50b3d6d35ebf8fdeb504bd8b381e2d079d2d9f12ce534fc14ecd", - "sha256:6edc5c190248d3b612f2cca45448cf8ebc3621d41afcd1c5708853cbb1dbb3b3", - "sha256:7606dba82435429641efe4fbc580574942f89cf2b9c5c1f8bc1eab2bacbf7e8b", - "sha256:8d1ee3796795e609ef7a3a5a35eaf4728038d986aa12c06b3fd1b92ee81911f4", - "sha256:8d9bb2d90e23c51aacbc58c1a11320f49b335cd67a91986cdbebcc3e843e4de8", - "sha256:97d414c41f19fd2362e493810caa8445c05e0a2d63a14081c972aad66284a8d2", - "sha256:9e37502817225ee99d91d8418f5119e98c380b00e772d06915690c05290f32ee", - "sha256:af7209b2fcc79ee2b0ad4ea080d70bb748450ec4f282cc9e864861e469b1072e", - "sha256:c0849b0864ff451f04c8afb5fc28e9ed592262e03debdd227cf0f53e04a55dcd", - "sha256:c4ac9215650688e78dea29b46adbdafb7b85058eebe92ef6ea848e14466c915f", - "sha256:dcda6d4e1bbfc939b177c237aee41c9678eaaf71df482688f8986e8251e12345", - "sha256:dd8501b8d9ea1aba53c4bc7d47bc72933f9b4213d534cf400f16c1431f51c8ba", - "sha256:ec0e509ed1877ff1cbc6f0864689bb60384a303502c4d72d9a635f8a4676fd3f", - "sha256:f6c8c3f56fef719180464855346e6e80971b86dfd9e5a0e356664b5baca53072", - "sha256:ffd4f80602490a309064cf2b203e220d581c51660e01055c64bf5da450485ee6" - ], - "version": "==2020.6.7" + "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a", + "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938", + "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29", + "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae", + "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387", + "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a", + "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf", + "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610", + "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9", + "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5", + "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3", + "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89", + "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded", + "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754", + "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f", + "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868", + "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd", + "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910", + "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3", + "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac", + "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c" + ], + "version": "==2020.6.8" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "smmap": { @@ -533,6 +554,7 @@ "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "stevedore": { @@ -540,6 +562,7 @@ "sha256:001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9", "sha256:471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691" ], + "markers": "python_version >= '3.6'", "version": "==2.0.0" }, "toml": { @@ -573,7 +596,7 @@ "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", + "markers": "python_version < '3.8' and implementation_name == 'cpython'", "version": "==1.4.1" }, "wrapt": { From f51a7fb54c50f3d22b5018a08fecec27cbea61b1 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Jun 2020 18:08:49 -0700 Subject: [PATCH 0567/1221] Rollback py version for heroku --- Pipfile | 1 - Pipfile.lock | 94 ++++++++++++++++++++++++++-------------------------- runtime.txt | 2 +- 3 files changed, 48 insertions(+), 49 deletions(-) diff --git a/Pipfile b/Pipfile index e506daf6dd..16bbac8e49 100644 --- a/Pipfile +++ b/Pipfile @@ -21,7 +21,6 @@ parsedatetime = "==2.6" aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" -pip = ">=18.0" "discord.py" = "==1.3.3" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index d8c22e241e..9a650f4af4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "cd8e330d55a4f896b01872d9fd56534288882d1ab6c68f0fc0841a13d8df9f00" + "sha256": "bfb85b7c7862e17e905d739f0fb63248d3bf446035f32e3afac619f6d2c5a0ba" }, "pipfile-spec": 6, "requires": { @@ -196,62 +196,62 @@ }, "pymongo": { "hashes": [ - "sha256:4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", - "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", - "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606", - "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", - "sha256:ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012", - "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", - "sha256:619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", - "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", - "sha256:f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a", - "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", - "sha256:bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a", + "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", + "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", + "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", + "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", + "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", + "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103", + "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", + "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", "sha256:2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", + "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", + "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", + "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", + "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", + "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", + "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", + "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", + "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", + "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", + "sha256:4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", + "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", + "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", "sha256:56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", + "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", + "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", + "sha256:619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", + "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", "sha256:63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113", + "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", + "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", "sha256:7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae", - "sha256:a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", - "sha256:c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", - "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", - "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", + "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", + "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", "sha256:993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", - "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", - "sha256:c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", - "sha256:e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", - "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", "sha256:9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", - "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", - "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", - "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", + "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", "sha256:a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", - "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", - "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", - "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", - "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", "sha256:a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", - "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", - "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", - "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", - "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", - "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", - "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", - "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", - "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", - "sha256:e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", - "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", - "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", - "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", - "sha256:e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", + "sha256:a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", + "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", + "sha256:ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012", + "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", + "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", + "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", + "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", + "sha256:bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a", + "sha256:c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", "sha256:c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", - "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", - "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", - "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", - "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", + "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", + "sha256:c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", "sha256:da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", - "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", - "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", - "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103" + "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", + "sha256:e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", + "sha256:e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", + "sha256:e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", + "sha256:f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a", + "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606" ], "version": "==3.10.1" }, diff --git a/runtime.txt b/runtime.txt index 05802392e0..257b314f5f 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.7 \ No newline at end of file +python-3.7.6 \ No newline at end of file From 62a30dd033afe7cdb277353bd698c80007d15a74 Mon Sep 17 00:00:00 2001 From: SpyTec Date: Thu, 2 Jul 2020 20:37:57 +0200 Subject: [PATCH 0568/1221] Botched multiple config handler --- bot.py | 13 ++++++++----- core/config.py | 11 +++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/bot.py b/bot.py index 694f860cf1..d16c953bb3 100644 --- a/bot.py +++ b/bot.py @@ -48,7 +48,7 @@ class ModmailBot(commands.Bot): - def __init__(self): + def __init__(self, args): super().__init__(command_prefix=None) # implemented in `get_prefix` self._session = None self._api = None @@ -58,7 +58,10 @@ def __init__(self): self._connected = asyncio.Event() self.start_time = datetime.utcnow() - self.config = ConfigManager(self) + if len(args): + self.config = ConfigManager(self, args[0]) + else: + self.config = ConfigManager(self) self.config.populate_cache() self.threads = ThreadManager(self) @@ -1224,7 +1227,7 @@ async def before_post_metadata(self): self.metadata_loop.cancel() -def main(): +def main(args): try: # noinspection PyUnresolvedReferences import uvloop @@ -1234,9 +1237,9 @@ def main(): except ImportError: pass - bot = ModmailBot() + bot = ModmailBot(args) bot.run() if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/core/config.py b/core/config.py index a0eda4a2dc..64f603d758 100644 --- a/core/config.py +++ b/core/config.py @@ -5,7 +5,6 @@ import typing from copy import deepcopy -from dotenv import load_dotenv import isodate import discord @@ -17,7 +16,6 @@ from core.utils import strtobool logger = getLogger(__name__) -load_dotenv() class ConfigManager: @@ -135,11 +133,12 @@ class ConfigManager: defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) - def __init__(self, bot): + def __init__(self, bot, config_file = "config.json"): self.bot = bot self._cache = {} self.ready_event = asyncio.Event() self.config_help = {} + self.config_file = config_file def __repr__(self): return repr(self._cache) @@ -148,12 +147,12 @@ 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" + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), self.config_file ) if os.path.exists(config_json): - logger.debug("Loading envs from config.json.") + logger.debug("Loading envs from {}".format(self.config_file)) with open(config_json, "r", encoding="utf-8") as f: # Config json should override env vars try: From baff27ef0e7c8970bc57c60ce03fb5ed9c9b2f06 Mon Sep 17 00:00:00 2001 From: RheaAyase Date: Thu, 2 Jul 2020 23:03:52 +0200 Subject: [PATCH 0569/1221] Unbotched support for multiple instances using config folders --- core/config.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/config.py b/core/config.py index 64f603d758..ddf3ace149 100644 --- a/core/config.py +++ b/core/config.py @@ -5,6 +5,7 @@ import typing from copy import deepcopy +from dotenv import load_dotenv import isodate import discord @@ -133,12 +134,13 @@ class ConfigManager: defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) - def __init__(self, bot, config_file = "config.json"): + def __init__(self, bot, config_path = os.path.dirname(os.path.abspath(__file__))): self.bot = bot self._cache = {} self.ready_event = asyncio.Event() self.config_help = {} - self.config_file = config_file + self.config_path = config_path + load_dotenv(dotenv_path=os.path.join(self.config_path, ".env")) def __repr__(self): return repr(self._cache) @@ -147,12 +149,10 @@ 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}) - config_json = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), self.config_file - ) + data.update({k.lower(): v for k, v in os.environ.items() if k.lower() in self.all_keys}) + config_json = os.path.join(self.config_path, "config.json") if os.path.exists(config_json): - logger.debug("Loading envs from {}".format(self.config_file)) + logger.debug("Loading envs from {}".format(config_json)) with open(config_json, "r", encoding="utf-8") as f: # Config json should override env vars try: From 9569e15092bda9447473657e13826b86af24832e Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 3 Jul 2020 18:15:32 +0800 Subject: [PATCH 0570/1221] Fix black from CI --- bot.py | 2 +- core/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index d16c953bb3..24f1218585 100644 --- a/bot.py +++ b/bot.py @@ -60,7 +60,7 @@ def __init__(self, args): if len(args): self.config = ConfigManager(self, args[0]) - else: + else: self.config = ConfigManager(self) self.config.populate_cache() diff --git a/core/config.py b/core/config.py index ddf3ace149..cfd79d9df9 100644 --- a/core/config.py +++ b/core/config.py @@ -134,7 +134,7 @@ class ConfigManager: defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) - def __init__(self, bot, config_path = os.path.dirname(os.path.abspath(__file__))): + def __init__(self, bot, config_path=os.path.dirname(os.path.abspath(__file__))): self.bot = bot self._cache = {} self.ready_event = asyncio.Event() From ee7d2360f73494b92cd76de08947603f0479746b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 3 Jul 2020 18:22:04 +0800 Subject: [PATCH 0571/1221] oops --- .github/ISSUE_TEMPLATE/command-request.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/workflows/stale.yml | 2 +- CHANGELOG.md | 262 +++-- CONTRIBUTING.md | 17 +- README.md | 48 +- bot.py | 58 +- cogs/modmail.py | 290 ++--- cogs/plugins.py | 80 +- cogs/utility.py | 373 ++++--- core/config.py | 11 +- core/paginator.py | 2 +- core/thread.py | 54 +- core/time.py | 2 +- core/translations.py | 25 + languages/en.csv | 1172 +++++++++++++++++++++ plugins/registry.json | 31 +- poetry.lock | 467 ++------ pyproject.toml | 8 +- runtime.txt | 2 +- translation_files.py | 117 ++ 21 files changed, 2016 insertions(+), 1009 deletions(-) create mode 100644 core/translations.py create mode 100644 languages/en.csv create mode 100644 translation_files.py diff --git a/.github/ISSUE_TEMPLATE/command-request.md b/.github/ISSUE_TEMPLATE/command-request.md index d3c1673a6b..44902f2cf3 100644 --- a/.github/ISSUE_TEMPLATE/command-request.md +++ b/.github/ISSUE_TEMPLATE/command-request.md @@ -1,7 +1,7 @@ --- name: Command request about: Request a new command -title: "your title here" +title: "[COMMAND-REQUEST] your title here" labels: command-request assignees: '' diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 7cd7a506cd..48b986344d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: Suggest an idea for this project -title: "your title here" +title: "[FEATURE-REQUEST] your title here" labels: feature-request assignees: '' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d0811bfd30..b3003ccf92 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -14,4 +14,4 @@ jobs: 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: 'priority: high,approved,security,bug' + exempt-issue-label: 'high priority' diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dcb9acbcd..16c89216f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,22 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); -however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. +however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). -# v3.4.1 - -### Fixed - -- Masked a bunch of noise errors when deleting messages. -- Added more checks for deleting messages. - -### Breaking - -- `thread_initiate` will be dispatched at the beginning of the setup process. -- `thread_create` is dispatched when the thread is registered as a thread by Modmail (i.e., when channel topic is edited). -- `thread_ready` is dispatched when a thread finishes its setup steps. +# v3.5.0-dev0 +Translations WIP. # v3.4.0 @@ -36,35 +26,35 @@ however, insignificant breaking changes do not guarantee a major version bump, s - 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 get feedback when they edit their messages. +- Recipient get feedback when they edit message. - Chained delete for DMs now comes with a message. - poetry (in case someone needs it). ### Changed - The look of alias and snippet when previewing. -- The database now saves the message ID of the thread embed, instead of the original message. +- 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. -- A hard limit of a maximum of 25 steps for aliases. +- Hard limit of maximum 25 steps for alias. - `?disable` is now `?disable new`. ### Fixed - Setting config vars using human time wasn't working. - Fixed some bugs with aliases. -- Fixed many issues with `?edit` and `?delete` and recipient message edit. +- 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 add/remove. +- Chained reaction add / remove. - Chained delete for thread channels. ### Internal -- Commit to black format line width max = 99, consistent with PyLint. -- No longer requires shlex for alias parsing. +- 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. +- No more flake8 and travis. # v3.3.2 @@ -88,19 +78,19 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Three new config vars: - `enable_plugins` (yes/no default yes) - - When set to no, Modmail will not load plugins. + - When set to no, plugins will not be loaded into the bot. - `error_color` (color format, defaults discord red) - The color of error messages. - `anon_reply_without_command` (yes/no default no) (Thanks to papiersnipper PR#288) - When set, all non-command messages sent to thread channels are forwarded to the recipient anonymously without the need of `?anonreply`. - This config takes precedence over `reply_without_command`. -- `?logs responded [user]` command. It will show all the logs that the user has sent a reply. (Thanks to papiersnipper PR#288) +- `?logs responded [user]` command, it will show all logs that the user has sent an reply. (Thanks to papiersnipper PR#288) - `user` when not provided, defaults to the user who ran the command. -- Open threads in limbo now auto-close if Modmail cannot find the channel. Modmail does this check every time the bot restarts. +- Open threads in limbo now auto closes if the channel cannot be found. This check is done every time the bot restarts. - Ability to disable new threads from getting created. - - `?disable`. + - `?disable` - Ability to fully disable Modmail DM. - - `?disable all`. + - `?disable all` - To re-enable DM: `?enable`, and to see the current status: `?isenable`. - This disabled Modmail interface is customizable with the following config vars: - `disabled_new_thread_title` @@ -114,33 +104,33 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Changed -- `?contact` no longer send the "thread created" message to where the command was run, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) +- `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) - Automatically delete notes command `?note` when there're no attachments attached. - Embed author links used to be inaccessible in many cases, now: - `?anonreply`, `?reply`, and `?note` in the thread channel will link to the sender's profile. - - `?reply` and the recipient's DM will also link the sender's profile. + - `?reply` and recipient's DM will also link the sender's profile. - `?anonreply` in DM channel will link to the first channel of the main guild. - Plugins update (mostly internal). - - `git` is no longer used to install plugins; it now downloads through zip files. + - `git` is no longer used to install plugins, it now downloads through zip files. - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. - Reorganized plugins folder structure. - Logging / plugin-related messages change. - - Updating one plugin will not update other plugins; repositories no longer separate plugins, but the plugin name itself. -- The help command is in alphabetical order grouped by permissions. -- Notes are no longer always blurple; it's set to `MAIN_COLOR` now. + - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). +- Help command is in alphabetical order grouped by permissions. +- Notes are no longer always blurple, it's set to `MAIN_COLOR` now. - Added `?plugins update` for updating all installed plugins. - Reintroduce flake8 and use bandit for security issues detection. -- Add Travis checks for 3.6 in Linux and 3.7 for macOS and Windows. -- Debug logs not logs eval commands. +- Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. +- Eval commands are logged in debug logs. - Presence updates 30 minutes instead of 45 now. -- Fixed an assortment of problems to do with `?block`. +- Fixed an assortment of problems to do with block. - Existing aliases can be used when creating new aliases. (Thanks to papiersnipper PR#402) ### Internal - Reworked `config.get` and `config.set`, it feeds through the converters before setting/getting. - To get/set the raw value, access through `config[]`. -- The prerelease naming scheme is now `x.x.x-devN`. +- Prerelease naming scheme is now `x.x.x-devN`. - `trigger_typing` has been moved to `core.utils.trigger_typing`, the original location is deprecated. - Simpler status and activity logic. - New logging logic. @@ -153,9 +143,9 @@ Security update! - Supporter permission users used to be able to "hack" snippets to reveal all your config vars, including your token and MongoURI. - Implemented some changes to address this bug: - - All customizable variables used in snippets, close messages, etc., using the `{}` syntax, now forbids chaining two or more attributes and attributes that start with `_`. -- We advise you to update to this version. -- If you felt your credentials had been leaked, consider changing your bot token / MongoURI. + - All customizable variables used in snippets, close messages, etc, using the `{}` syntax, now forbids chaining 2 or more attributes and attributes that starts with `_`. +- It is advised to update to this version. +- If you felt your credentials have been leaked, consider changing your bot token / mongo uri. # v3.2.1 @@ -185,7 +175,7 @@ Security update! ### Internal -- Use regex to parse Changes, Added, Fixed, etc. and description. +- Use regex to parse Changes, Added, Fixed, etc and description. - Adds `PermissionLevel.INVALID` when commands don't have a permission level. # v3.1.1 @@ -205,33 +195,33 @@ Security update! ### Added - `?sfw`, mark a thread as "safe for work", undos `?nsfw`. -- New config variable, `thread_auto_close_silently`, when set to a truthy value, no message will be sent when a thread is auto-closed. +- New config variable, `thread_auto_close_silently`, when set to a truthy value, no message will be sent when thread is auto-closed. - New configuration variable `thread_self_closable_creation_footer` — the footer when `recipient_thread_close` is enabled. - Added a minimalistic version of requirements.txt (named requirements.min.txt) that contains only the absolute minimum of Modmail. - For users having trouble with pipenv or any other reason. -- Multi-step alias, see `?help alias add`. Public beta testing might be unstable. +- Multi-step alias, see `?help alias add`. Public beta testing, might be unstable. - Misc commands without cogs are now displayed in `?help`. - `?help` works for alias and snippets. - `?config help ` shows a help embed for the configuration. -- Support setting permissions for subcommands. -- Support numbers (1-5) as substitutes for Permission Level REGULAR - OWNER in `?perms` subcommands. +- Support setting permissions for sub commands. +- Support numbers (1-5) as substitutes for Permission Level REGULAR - OWNER in `?perms` sub commands. ### Changes - `thread_auto_close_response` has a configurable variable `{timeout}`. - `?snippet` is now the default command name instead of `?snippets` (`?snippets` is still usable). This is to make this consistent with `?alias`/`?aliases`. -- `colorama` is no longer a necessity; this is due to some unsupported OS. -- Changelog command can now take a version argument to jump straight to the specified version. +- `colorama` is no longer a necessity, this is due to some unsupported OS. +- Changelog command can now take a version argument to jump straight to specified version. - `?plugin enabled` results are now sorted alphabetically. -- `?plugin registry` results are now sorted alphabetically, helps users find plugins more easily. +- `?plugin registry` results are now sorted alphabetically, helps user find plugins more easily. - `?plugin registry page-number` plugin registry can specify a page number for quick access. - A reworked interface for `?snippet` and `?alias`. - Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown). - - Add an `?alias raw ` command for displaying the raw content of an alias (escaped markdown). + - Add an `?alias raw ` command for viewing the raw content of a alias (escaped markdown). - The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/. - Removed unclear `rm` alias for some `remove` commands. - Paginate `?config options`. -- All users configured with a permission level higher than REGULAR has access to the main Modmail category. +- All users configured with a permission level greater than REGULAR has access to the main Modmail category. - Category overrides also changes when a level is removed or added to a user or role. - `@everyone` is now accepted for `?perms add`. @@ -239,12 +229,12 @@ Security update! - `?notify` no longer carries over to the next thread. - `discord.NotFound` errors for `on_raw_reaction_add`. -- `mod_typing` ~~and `user_typing`~~ (`user_typing` is now by-design to show) will no longer show when the user is blocked. +- `mod_typing` ~~and `user_typing`~~ (`user_typing` is now by-design to show) will no longer show when user is blocked. - Better `?block` usage message. -- Resolved errors when mods sent messages after a thread is closed somehow. +- Resolves errors when message was sent by mods after thread is closed somehow. - Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`. -- When creating snippets and aliases, it now checks if other snippets/aliases with the same name exist. -- Modmail looked for `config.json` in the wrong directory. +- When creating snippets and aliases, it now checks if another snippets/aliases with the same name exists. +- Was looking for `config.json` in the wrong directory. ### Internal @@ -264,13 +254,13 @@ Security update! ### Added - New commands, `?alias edit ` and `?snippets edit `. - - They can be used to edit aliases and snippets, respectively. + - They can be used to edit aliases and snippets respectively. # v3.0.2 ### Added -- A new command, `?blocked whitelist `, this command prevents users from getting blocked by any means. +- New command, `?blocked whitelist `, this command prevents users from getting blocked by any means. ### Changed @@ -280,30 +270,30 @@ Security update! ### Fixed -- Many bugs with `thread_auto_close`. +- A lot of bugs with `thread_auto_close` 😅 # v3.0.0 ### Added -- `?sponsors` command will list sponsors. -- An alert will now be sent to the log channel if a thread channel fails to create. This could be due to a variety of problems such as insufficient permissions, or the category channel limit is met. +- Sponsors command that will list sponsors. +- An alert will now be sent to the log channel if a thread channel fails to create. This could be due to a variety of problems such as insufficient permissions or the category channel limit is met. - Threads will close automatically after some time when `thread_auto_close` is set. -- Custom closing messages can be configured with `thread_auto_close_response`. +- Custom closing message can be set with `thread_auto_close_response`. ### Breaking Changes -- Removed auto-update functionality and the `?update` command in favor of the [Pull app](https://github.com/apps/pull). +- Removed autoupdate functionality and the `?update` command in favour of the [Pull app](https://github.com/apps/pull). Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/updating) ### Changed -- Channel names now can contain Unicode characters. -- Debug logs are now located in a different file for each bot. (Internal change) +- Channel names now can contain unicode characters. +- Debug logs are now located in a unique file for each bot. (Internal change) - Default cogs always appear first in the help command now. ### Fixed -- Editing notes now work, minor bug with edit command is fixed. +- Editing notes now works, minor bug with edit command is fixed. - Bug in the `?oauth` command where the response message fails to send when an ID is provided. - Plugin requirement installation now works in virtual environments @@ -318,7 +308,7 @@ Fixed a bug with branches and `?plugin update`. ### Added -Branch support for `?plugin add` and in the registry. Typically for developers. +Branch support for `?plugin add` and in registry. Typically for developers. # v2.23.0 @@ -326,11 +316,11 @@ Branch support for `?plugin add` and in the registry. Typically for developers. Added a "Mutual servers" field to the genesis embed if: a) The user is not in the main guild. -b) The user shares more than one server with the bot. +b) The user shares more than 1 server with the bot. ### Changed -Notes with the `?note` command are now automatically pinned within the thread channel. +Notes taken with the `?note` command are now automatically pinned within the thread channel. # v2.22.0 @@ -372,14 +362,14 @@ Add your plugin in the `plugins/registry.json` file in the main repository. This update contains mostly internal changes. - Implemented support for the new discord.py v1.1.1. - Improved help text for most commands. - - Completely revamped help command, few users changes. - - Removed ABC (internal). + - Completely revamped help command, few user changes. + - Removed abc (internal). # v2.20.0 ### What's new? -New `?oauth whitelist` command, which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app, check the logviewer [repo](https://github.com/kyb3r/logviewer). +New `oauth` whitelist command which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app check the logviewer [repo](https://github.com/kyb3r/logviewer). # v2.19.1 @@ -387,7 +377,7 @@ New `?oauth whitelist` command, which allows you to whitelist users so they can - Ability to force an update despite having the same version number. Helpful to keep up-to-date with the latest GitHub commit. - `?update force`. -- Plugin developers now have a new event called `on_plugin_ready`; this is a coroutine and is awaited when all plugins are loaded. Use `on_plugin_ready` instead of `on_ready` since `on_ready` will not get called in plugins. +- Plugin developers now have a new event called `on_plugin_ready`, this is coroutine is awaited when all plugins are loaded. Use `on_plugin_ready` instead of `on_ready` since `on_ready` will not get called in plugins. # v2.19.0 @@ -408,17 +398,17 @@ Fix the teams permission bug. ### Changed -Commands now have better error messages. Instead of sending the help message for a command when an argument fails to be converted, the bot now says like "User 'bob' not found" instead. +Commands now have better error messages, instead of just sending the help message for a command when an argument fails to be converted to its specified object, the bot now says things like "User 'bob' not found" instead. # v2.18.1 -Un-deprecated the `OWNERS` config variable to support Discord developer team accounts. +Un-deprecated the `OWNERS` config variable to support discord developer team accounts. # v2.18.0 ### New Permissions System -- A brand new permission system! Replaced the old guild-based permissions (i.e., manage channels, manage messages), with the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. +- A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. - There are five permission levels: - Owner [5] - Administrator [4] @@ -437,7 +427,7 @@ You may add a role or user to a permission group through any of the following me The same applies to individual commands permissions: - `?permissions add command command-name @member#1234` -- and the other methods listed above. +- ... and the other methods listed above. To revoke permission, use `remove` instead of `add`. @@ -450,11 +440,11 @@ By default, all newly set up Modmail will have `OWNER` set to the owner of the b ### Breaking When updating to this version, all prior permission settings with guild-based permissions will be invalidated. You will need to convert to the above system. -`OWNERS` will also get removed; you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. +`OWNERS` will also get removed, you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. ### New Command -- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer require "manage messages" permission to recall thread messages. +- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer requires manage messages permission to recall thread messages. ### Changed @@ -475,28 +465,28 @@ Stricter fallback genesis embed search. ### Changed -How Modmail checks if a channel is a thread: +How modmail checks if a channel is a thread: -1. The bot first checks if the channel topic is in the format `User ID: XXXX`, this means it is a thread. -2. If a channel topic is not found, the bot searches through the message history of a channel to find the thread creation embed. This step should never yield a thread for an average user. Still, in the case of another bot messing up the channel topic (happened to a user before), this extra step was added. +1. The bot first checks if the channel topic is in the format `User ID: xxxx`, this means it is a thread. +2. If a channel topic is not found, the bot searches through the message history of a channel to find the thread creation embed. This step should never yield a thread for a normal user, but in the case of another bot messing up the channel topic (happened to a user before) this extra step was added. # v2.17.0 ### What's new? -Added a config option `reply_without_command`, which, when present, enables the bot to forward any message sent in a thread channel to the recipient. (Replying without using a command) +Added a config option `reply_without_command` which when present, enables the bot to forward any message sent in a thread channel to the recipient. (Replying without using a command) To enable this functionality, do `?config set reply_without_command true` and to disable it, use `?config del reply_without_command`. ### Changed -The `move` command now only requires `manage_messages` perms instead of `manage_channels`. +The `move` command now only requires `manage_messages` perms instead of `manage_channels` # v2.16.1 ### Fixed -An issue where a scheduled close would not execute over a long time if the recipient no shares any servers with the bot. +An issue where a scheduled close would not execute over a long period of time if the recipient no shares any servers with the bot. # v2.16.0 @@ -526,8 +516,8 @@ Added the ability to change the default close message via the introduction of tw They will be provided by string variables that you can incorporate into them: - `closer` - the user object that closed the thread. -- `logkey` - the key for the thread logs, e.g. (`5219ccc82ad4`) -- `loglink` - the full link to the thread logs, e.g. (`https://logwebsite.com/logs/5219ccc82ad4`) +- `logkey` - the key for the thread logs e.g. (`5219ccc82ad4`) +- `loglink` - the full link to the thread logs e.g. (`https://logwebsite.com/logs/5219ccc82ad4`) Example usage would be: ``?config set thread_close_message {closer.mention} closed the thread, here is the link to your logs: [**`{logkey}`**]({loglink})`` @@ -556,9 +546,9 @@ You now have complete control of the look of the thread creation and close embed ### What's new? -Added the ability to disable the `sent_emoji` and `blocked_emoji` when a user messages Modmail. +Added the ability to disable the `sent_emoji` and `blocked_emoji` when a user messages modmail. -You can do this via `?config set sent_emoji disable`. +You can do this via `?config set sent_emoji disable` ### Fixed @@ -575,17 +565,17 @@ Added image link in title in case discord fails to embed an image. - Introduced a new configuration variable `account_age` for setting a minimum account creation age. - Users blocked by this reason will be stored in `blocked` along with other reasons for being blocked. - `account_age` needs to be an ISO-8601 Duration Format (examples: `P12DT3H` 12 days and 3 hours, `P3Y5M` 3 years and 5 months `PT4H14M999S` 4 hours 14 minutes and 999 seconds). https://en.wikipedia.org/wiki/ISO_8601#Durations. - - You can set `account_age` using `config set account_age time` where "time" can be a simple human-readable time string or an ISO-8601 Duration Format string. + - You can set `account_age` using `config set account_age time` where "time" can be a simple human readable time string or an ISO-8601 Duration Format string. ### Changed -- `?block` reason cannot start with `System Message: ` as it is now reserved for internal user blocking. -- `?block`, like `?close`, now supports a block duration (temp blocking). +- `block` reason cannot start with `System Message: ` as it is now reserved for internal user blocking. +- `block`, like `close`, now supports a block duration (temp blocking). # v2.13.10 ### Fixed - Fixed an issue where status and activity do not work if they were modified wrongly in the database. - - This was primarily an issue for older Modmail users, as the old `status` configuration variable clashes with the new `status` variable. + - This was especially an issue for older Modmail users, as the old `status` configuration variable clashes with the new `status` variable. # v2.13.9 @@ -604,7 +594,7 @@ Added image link in title in case discord fails to embed an image. ### What's new? - The ability to enable typing interactions. - - If you want the bot to type in the thread channel if the user is also typing, add the config variable `user_typing` and set it to "yes" or "true". Use `config del` to disable the functionality. The same thing in reverse is also possible if you want the user to see the bot type when someone is typing in the thread channel add the `mod_typing` config variable. + - If you want the bot to type in the thread channel if the user is also typing, add the config variable `user_typing` and set it to "yes" or "true". use `config del` to disable the functionality. The same thing in reverse is also possible if you want the user to see the bot type when someone is typing in the thread channel add the `mod_typing` config variable. - New `status` command, change the bot's status to `online`, `idle`, `dnd`, `invisible`, or `offline`. - To remove the status (change it back to default), use `status clear`. - This also introduces a new internal configuration variable: `status`. Possible values are `online`, `idle`, `dnd`, `invisible`, and `offline`. @@ -621,15 +611,15 @@ Added image link in title in case discord fails to embed an image. ### What's new? - You will no longer need to view your bot debug logs from Heroku. `debug` will show you the recent logs within 24h through a series of embeds. - - If you don't mind your data (may or may not be limited to user ID, guild ID, bot name) be on the internet, `debug hastebin` will upload a formatted logs file to https://hasteb.in. + - If you don't mind your data (may or may not be limited to: user ID, guild ID, bot name) be on the internet, `debug hastebin` will upload a formatted logs file to https://hasteb.in. - `debug clear` will clear the locally cached logs. - - Local logs are automatically erased at least once every 27h for bots hosted on Heroku. + - Local logs are automatically cleared at least once every 27h for bots hosted on Heroku. ### Fixed -- Will no longer show `Unclosed client session` and `Task was destroyed, but it is pending!` when the bot terminates. +- Will no longer show `Unclosed client session` and `Task was destroyed but it is pending!` when the bot terminates. - `thread.create` is now synchronous so that the first message sent can be queued to be sent as soon as a thread is created. - This fixes a problem where if multiple messages are sent in quick succession, the first message sent (which triggers the thread creation) is not sent in order. -- Trying to reply to someone who has DMs disabled or has blocked the bot is now handled, and the bot will send a message saying so. +- Trying to reply to someone who has DMs disabled or has blocked the bot is now handled and the bot will send a message saying so. ### Changed - `print` is replaced by logging. @@ -663,14 +653,14 @@ Added image link in title in case discord fails to embed an image. ### What's new? - Plugins: - - Think of it like addons! Anyone (with the skills) can create a plugin, make it public and distribute it. Add a welcome message to Modmail, or moderation commands? It's all up to your imagination! Have a niche feature request that you think only your server would benefit? Plugins are your go-to! + - Think of it like addons! Anyone (with the skills) can create a plugin, make it public and distribute it. Add a welcome message to Modmail, or moderation commands? It's all up to your imagination! Have a niche feature request that you think only your server would benefit from? Plugins are your go-to! - [Creating Plugins Documentation](https://github.com/kyb3r/modmail/wiki/Plugins). # v2.12.5 ### Fixed -- `config del` command will now work correctly on self-hosted DB bots. +- `config del` command will now work properly on self-hosted db bots. # v2.12.4 @@ -684,14 +674,14 @@ Added image link in title in case discord fails to embed an image. ### Fixed - Patched a bug where `logs` sub-commands were accessible by anyone. -- Patched a bug where an error was raised when a thread is open where the recipient left the server. +- Patched a bug where an error was raised if there was an open thread where the recipient had left the server. Huge thanks to Sasiko for reporting these issues. # v2.12.2 ### Fixed -- Fixed a bug in self-hosted `?update` command. +- Fixed a bug in self-hosted `update` command. # v2.12.1 @@ -702,12 +692,12 @@ Huge thanks to Sasiko for reporting these issues. # v2.12.0 ### Important -**In the future, the Modmail API (https://modmail.tk) will be deprecated. This is because we are providing free service without getting anything in return. Thus we do not have the resources to scale to accommodate more users. +**In the future, the Modmail API (https://modmail.tk) will be deprecated. This is due to the fact that we are providing a free service without getting anything in return, and thus we do not have the resources to scale to accommodate for more users. We recommend using your own database for logs. In the future you will soon get a `backup` command so you can download all your pre-existing data and migrate to your own database.** ### Changed - A lot of painful code cleanup, which is good for us (the developers), but shouldn't affect you. -- The appearance of the `?logs` command. It should be clearer with better info now. +- The appearance of the `logs` command. Should be clearer with better info now. - Bot owners get access to all commands regardless of server permissions. - Blocked users no longer receive a message, only the blocked emoji will be sent. @@ -715,10 +705,10 @@ We recommend using your own database for logs. In the future you will soon get a - **Note:** The following commands only work if you are self-hosting your logs. We recommend you to use your own database. - Log search queries, in the form of two new commands. - `logs search [query]` - this searches all log messages for a query string. -- `logs closed-by [user]` this returns all logs closed by a particular user +- `logs closed-by [user]` this returns all logs closed by a certain user ### Fixed -- `activity listening to music` no longer results in two "to"s ("listening to to music"). +- `activity listening to music` no longer result in two "to"s ("listening to to music"). - This may require you to change your activity message to accommodate this fix. - A problem where `main_category_id` and `log_channel_id` weren't updated when their corresponding channel or category get deleted. @@ -741,7 +731,7 @@ We recommend using your own database for logs. In the future you will soon get a ### What's new? - `anonreply` command to anonymously reply to the recipient. -The username of the anonymous user defaults to the `mod_tag` (the footer text of a mod reply message) — the avatar defaults to the guild icon URL. However, you can change both of these via the `anon_username`, `anon_avatar_url`, and `anon_tag` config variables. +The username of the anonymous user defaults to the `mod_tag` (the footer text of a mod reply message). The avatar defaults the guild icon URL. However you can change both of these via the `anon_username`, `anon_avatar_url` and `anon_tag` config variables. ### Changed - Your bot now logs all messages sent in a thread channel, including discussions that take place. You can now toggle to view them in the log viewer app. @@ -766,7 +756,7 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of - All commands are now blurple instead of green. ### Fixed -- Bug where the close command wouldn't work if you didn't configure a log channel. +- Bug where the close command wouldn't work if you didnt configure a log channel. ### What's new? - Ability to set your own custom `mod_color` and `recipient_color` for the thread message embeds. @@ -783,13 +773,13 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of # v2.9.0 ### What's new? -- New command `note` will add a system message to your thread logs. - - This is useful for noting the context of a conversation. +- New command `note` will add a system message to your thread logs. This is useful for noting the context of a conversation. # v2.8.1 ### Fixed - Fixed bug where thread logs were getting duplicated when using the `contact` command. -- Fixed bug where the wrong key was used for logs, which caused some `log` command log links to point to an HTTP 404 Not Found. +- Fixed bug where the wrong key was used for logs which caused some `log` command log links to point to an HTTP 404 Not Found. - A minor oversight from commit 1ba74d9. # v2.8.0 @@ -806,7 +796,7 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of ### Security Thread channels will now default to being private (`@everyone`'s read message perms set to `false`). - If the thread creation category could not be resolved. - - This will save you from some trouble if, for whatever reason, your configuration gets messed up. + - This will save you from some trouble if for whatever reason your configuration gets messed up. # v2.7.1 @@ -818,20 +808,20 @@ Thread channels will now default to being private (`@everyone`'s read message pe ### Note -- If your Modmail bot was set up a long time ago, you might experience an issue where messages were sent outside of the category. +- If your Modmail bot was set up a long time ago, you may experience an issue where messages were sent outside of the category. - To fix this, set `main_category_id` to the ID of the Modmail category. # v2.7.0 ### Changed -- `move` command now syncs thread channel permissions with the destination category. +- `move` command now syncs thread channel permissions with the category that it was moved to. - `contact` command now supports an optional category argument (where the thread channel will be created). # v2.6.3 ### Fixes -- Fixed small issue with finding threads. +- Fixed small issue with finding thread. # v2.6.2 @@ -851,8 +841,8 @@ Thread channels will now default to being private (`@everyone`'s read message pe ### Changed - Log URLs are moved to their own collection. - Log URLs are now `https://logs.modmail.tk/LOGKEY`, no more numbers before the log key. -- We still support the numbers to not break everyone's URLs so quickly, but both work at the moment. -- This is a huge change to the backend logging, and there might be migration errors. If so, please contact us in our [Discord server](https://discord.gg/2fMbf2N). +- We still support the numbers so as to not break everyone's URLs so quickly but both work at the moment. +- This is a huge change to the backend logging and there might be migration errors. If so, please contact us in our [discord server](https://discord.gg/2fMbf2N). # v2.5.2 @@ -862,13 +852,13 @@ Thread channels will now default to being private (`@everyone`'s read message pe # v2.5.1 ### Fixes -- Emergency patch to save configs. +- Emergency patch to save config. # v2.5.0 ### Background - Bots hosted by Heroku restart at least once every 27 hours. -- During this period, local caches will be deleted, which results in the inability to set the scheduled close time to longer than 24 hours. This update resolves this issue. +- During this period, local caches are deleted, which results in the inability to set the scheduled close time to longer than 24 hours. This update resolves this issue. - [PR #135](https://github.com/kyb3r/modmail/pull/135) ### Changed @@ -886,7 +876,7 @@ Fixed activity setting due to flawed logic in `config.get()` function. # v2.4.4 ### Fixed -Fixed a bug in the `?activity` command where it would fail to set the activity on bot restart if the activity type was `playing`. +Fixed a bug in activity command where it would fail to set the activity on bot restart if the activity type was `playing`. # v2.4.3 @@ -901,17 +891,17 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o # v2.4.1 ### Fixed -- Small bug in `?activity` command. +- Small bug in `activity` command. # v2.4.0 ### What's new? -- Added the `?activity` command for setting the activity -- [PR #131](https://github.com/kyb3r/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening`, and `streaming`). +- Added the `activity` command for setting the activity +- [PR #131](https://github.com/kyb3r/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening` and `streaming`). ### Removed - Removed the deprecated `status` command. -- This also means you will have to reset your bot status with the `?activity` command, as the `?status` command was removed. +- This also means you will have to reset your bot status with the `activity` command, as `status` command is removed. # v2.3.0 @@ -932,7 +922,7 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o ### What's new? - Notify command `notify [role]`. - Notify a given role or yourself to the next thread message received. - - Once a thread message is received, you will be pinged once only. + - Once a thread message is received you will be pinged once only. - Subscribe command `sub [role]` / `unsub [role]`. - Subscribes yourself or a given role to be notified when thread messages are received. @@ -949,15 +939,15 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o # v2.1.0 ### What's new? -- Ability to set a custom thread-creation-response message. +- Ability to set a custom thread creation response message. - Via `config set thread_creation_response [message]`. ### Changed -- Improve `?logs` command format. -- Improve thread log channel messages to have more relevant info. +- Improve logs command format. +- Improve thread log channel message to have more relevant info. - Improve close command. - - You can now close the thread after a delay and use a custom thread close message. - - You also now can close a thread silently. + - You now can close the thread after a delay and use a custom thread close message. + - You also now have the ability to close a thread silently. # v2.0.10 @@ -972,7 +962,7 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o ### Fixes - Support multiple images and file attachments in one message. -- This is only possible on mobile, so its good to handle it in code. +- This is only possible on mobile so its good to handle it in code. # v2.0.8 @@ -983,7 +973,7 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o - You can do this via the `config set main_category_id ` command. ### Changed -- You can now supply a reason when blocking a user. +- You now have the ability to supply a reason when blocking a user. - Blocked users are now stored in the database instead of in the channel topic. - This means you can delete the top channel in the Modmail category now (after migrating the currently blocked users). @@ -994,8 +984,8 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o ### Changed - `update` command now shows the latest changes directly from CHANGELOG.md. -- Auto-update messages also show the latest changes from the GitHub repo. -- Removed the "latest changes" section from the `about` command. +- Auto update messages also show the latest changes from the GitHub repo. +- Removed "latest changes" section from the `about` command. # v2.0.6 @@ -1008,20 +998,20 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o ### Changed - `alias` command now checks if you are adding a valid alias-command combo. -- Manually deleting a channel will now correctly close the thread and post logs. +- Deleting a channel manually will now correctly close the thread and post logs. # v2.0.4 ### Fixed -- Fixed a one-off bug where the channel topic disappears, but Modmail operations should continue. +- Fixed a one-off bug where the channel topic disappears, but Modmail operations should still continue. - Fixed `linked_message_id` issues. # v2.0.3 ### Fixed -- The thread creation embed now shows the correct number of past logs. +- Thread creation embed now shows the correct number of past logs. - If using a separate server setup, roles in the info embed now are shown as names instead of mentions. - - This is because you can't mention roles across servers. + - This is due to the fact that you can't mention roles across servers. # v2.0.2 @@ -1033,7 +1023,7 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o ### Changed - Improved `block` / `unblock` commands. - - They now take a more comprehensive range of arguments: usernames, nicknames, mentions, and user IDs. + - They now take a wider range of arguments: usernames, nicknames, mentions and user IDs. ### Fixed - Setup command now configures permissions correctly so that the bot will always be able to see the main operations category. @@ -1041,7 +1031,7 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o # v2.0.0 This release introduces the use of our centralized [API service](https://github.com/kyb3r/webserver) to enable dynamic configuration, auto-updates, and thread logs. -To use this release, you must acquire an API token from https://modmail.tk. +To use this release you must acquire an API token from https://modmail.tk. Read the updated installation guide [here](https://github.com/kyb3r/modmail/wiki/installation). ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 568e5f2175..a95e344610 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,32 +15,33 @@ We use GitHub to host code, to track issues and feature requests, as well as acc ## We Use [Git Flow](https://atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) ![Simple Image Of A Git Flow Workflow](https://nvie.com/img/hotfix-branches@2x.png) -When contributing to this project, please make sure you follow this and name your branches appropriately! +When contributing to this project please make sure you follow this and name your branches appropriately! ## All Code Changes Happen Through Pull Requests Make sure you know how Git Flow works before contributing! Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 1. Fork the repo and create your branch from `master` or `development` according to Git Flow. -2. Update the CHANGELOG. -3. If you've changed `core/*` or `bot.py`, mark changelog as "BREAKING" since plugins may break. -4. Make sure your code passes the lint checks. -5. Create Issues and pull requests! +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. Issue that pull request! ## Any contributions you make will be under the GNU Affero General Public License v3.0 In short, when you submit code changes, your submissions are understood to be under the same [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project. Feel free to contact the maintainers if that's a concern. ## Report bugs using [Github Issues](https://github.com/kyb3r/modmail/issues) -We use GitHub issues to track public bugs. Report a bug by [opening a new Issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! +We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! ## Write bug reports with detail, background, and sample code **Great Bug Reports** tend to have: -- A quick summary and background +- A quick summary and/or background - Steps to reproduce - Be specific! - What you expected would happen -- What *actually* happens +- What actually happens - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) diff --git a/README.md b/README.md index 4b36c5a3d2..52e4c4abd8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
@@ -31,6 +31,10 @@ Made with Python 3.7 + + + + @@ -46,25 +50,25 @@ ## What is Modmail? -Modmail is similar to Reddit's Modmail, both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. +Modmail is similar to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included! ## How does it work? -When a member sends a direct message to the bot, Modmail will create a channel or "thread" into a designated category. All further DM messages will automatically relay to that channel; any available staff can respond within the channel. +When a member sends a direct message to the bot, Modmail will create a channel or "thread" within an isolated category. All further DM messages will automatically relay to that channel, for any available staff can respond within the channel. -Our Logviewer will save the threads so you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.logviewer.tech/example). +All threads are logged and you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.logviewer.tech/example). ## Features * **Highly Customisable:** * Bot activity, prefix, category, log channel, etc. * Command permission system. - * Interface elements (color, responses, reactions, etc.). + * Interface elements (color, responses, reactions, etc). * Snippets and *command aliases*. * Minimum duration for accounts to be created before allowed to contact Modmail (`account_age`). - * Minimum length for members to be in the guild before allowed to contact Modmail (`guild_age`). + * Minimum duration for members to be in the guild before allowed to contact Modmail (`guild_age`). * **Advanced Logging Functionality:** * When you close a thread, Modmail will generate a [log link](https://logs.logviewer.tech/example) and post it to your log channel. @@ -86,11 +90,11 @@ This list is ever-growing thanks to active development and our exceptional contr Where can I find the Modmail bot invite link? -Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time). +Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time)... ### Heroku -You can host this bot on Heroku. +This bot can be hosted on Heroku. Installation via Heroku is possible with your web browser alone. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. @@ -104,11 +108,11 @@ To configure automatic updates: ### Hosting for Patreons -If you don't want to go through the trouble of setting up your very own Modmail bot or wish to support this project, we got a solution for you! We offer the complete installation, hosting, and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! +If you don't want to go through the trouble of setting up your very own Modmail bot, and/or want to support this project, we offer the all inclusive installation, hosting and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! ### Locally -Local hosting of Modmail is also possible. First, you will need [`Python 3.7`](https://www.python.org/downloads/release/python-376/). +Local hosting of Modmail is also possible, first you will need [`Python 3.7`](https://www.python.org/downloads/). Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. @@ -143,7 +147,7 @@ You can build your own Docker image: $ docker build . --tag=modmail ``` -Or run directly from a pre-built version from https://hub.docker.com/. +or run directly from a pre-built version from https://hub.docker.com/. Currently there are two community release of Modmail: - Kyber's: @@ -151,12 +155,22 @@ Or run directly from a pre-built version from https://hub.docker.com/. $ docker pull kyb3rr/modmail ``` +- Taku's: + +```console +$ docker pull taaku18/modmail +# You can also choose one of the following: +$ docker pull taaku18/modmail:dev +$ docker pull taaku18/modmail: ( ex: 3.2.0, 3.2, etc.) +``` + And to run your docker image: ```console -$ docker run --env-file .env kyb3rr/modmail +$ docker run --env-file .env user/modmail ``` -- `.env` should be the path to your env file; you can also supply a path: `/path/to/.env`. +- Replace `user/modmail` with `kyb3rr/modmail`, `taaku18/modmail`, `taaku18/modmail:3.2`, etc as above. +- `.env` should be the path to your env file, you can also supply a path: `/path/to/.env`. ## Sponsors @@ -166,7 +180,7 @@ Special thanks to our sponsors for supporting the project. - + @@ -175,9 +189,9 @@ Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins Modmail supports the use of third-party plugins to extend or add functionalities to the bot. -Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail. +This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. -You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. +A list of third-party plugins can be found using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). @@ -185,6 +199,6 @@ Plugins requests and support is available in our [Modmail Plugins Server](https: ## Contributing -Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! diff --git a/bot.py b/bot.py index 24f1218585..ddbeec2d31 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.4.1" +__version__ = "3.5.0-dev0" import asyncio @@ -31,7 +31,7 @@ except ImportError: pass -from core import checks +from core import checks, translations from core.clients import ApiClient, PluginDatabaseClient from core.config import ConfigManager from core.utils import human_join, normalize_alias @@ -48,7 +48,7 @@ class ModmailBot(commands.Bot): - def __init__(self, args): + def __init__(self): super().__init__(command_prefix=None) # implemented in `get_prefix` self._session = None self._api = None @@ -58,10 +58,7 @@ def __init__(self, args): self._connected = asyncio.Event() self.start_time = datetime.utcnow() - if len(args): - self.config = ConfigManager(self, args[0]) - else: - self.config = ConfigManager(self) + self.config = ConfigManager(self) self.config.populate_cache() self.threads = ThreadManager(self) @@ -472,7 +469,7 @@ async def on_ready(self): { "open": False, "closed_at": str(datetime.utcnow()), - "close_message": "Channel has been deleted, no closer found.", + "close_message": _("Channel has been deleted, no closer found."), "closer": { "id": str(self.user.id), "name": self.user.name, @@ -552,7 +549,7 @@ def check_account_age(self, author: discord.Member) -> bool: logger.debug("Blocked due to account age, user %s.", author.name) if str(author.id) not in self.blocked_users: - new_reason = f"System Message: New Account. Required to wait for {delta}." + new_reason = _("System Message: New Account. Required to wait for {time}.").format(time=delta) self.blocked_users[str(author.id)] = new_reason return False @@ -618,7 +615,7 @@ def check_manual_blocked(self, author: discord.Member) -> bool: return False async def _process_blocked(self, message): - _, blocked_emoji = await self.retrieve_emoji() + x, 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 @@ -988,7 +985,7 @@ async def handle_reaction_events(self, payload, *, add): if not thread: return try: - _, linked_message = await thread.find_linked_messages( + x, linked_message = await thread.find_linked_messages( message.id, either_direction=True ) except ValueError as e: @@ -1072,32 +1069,19 @@ 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 isinstance(message.channel, discord.DMChannel): - thread = await self.threads.find(recipient=message.author) - if not thread: - return + if message.embeds and not isinstance(message.channel, discord.DMChannel): + thread = await self.threads.find(channel=message.channel) try: - message = await thread.find_linked_message_from_dm(message) + await thread.delete_message(message) except ValueError as e: - if str(e) != "Thread channel message not found.": + if str(e) not in {"DM message not found.", " Malformed thread message."}: logger.warning("Failed to find linked message to delete: %s", e) - return + 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) - return - - thread = await self.threads.find(channel=message.channel) - if not thread: - return - try: - await thread.delete_message(message, note=False) - 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) - return - except discord.NotFound: - return async def on_bulk_message_delete(self, messages): await discord.utils.async_all(self.on_message_delete(msg) for msg in messages) @@ -1110,13 +1094,10 @@ async def on_message_edit(self, before, after): if isinstance(after.channel, discord.DMChannel): thread = await self.threads.find(recipient=before.author) - if not thread: - return - try: await thread.edit_dm_message(after, after.content) except ValueError: - _, blocked_emoji = await self.retrieve_emoji() + x, blocked_emoji = await self.retrieve_emoji() await self.add_reaction(after, blocked_emoji) else: embed = discord.Embed( @@ -1227,7 +1208,7 @@ async def before_post_metadata(self): self.metadata_loop.cancel() -def main(args): +def main(): try: # noinspection PyUnresolvedReferences import uvloop @@ -1237,9 +1218,10 @@ def main(args): except ImportError: pass - bot = ModmailBot(args) + translations.init() + bot = ModmailBot() bot.run() if __name__ == "__main__": - main(sys.argv[1:]) + main() diff --git a/cogs/modmail.py b/cogs/modmail.py index 3b9c591294..7f5b2fe15b 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -41,17 +41,17 @@ async def setup(self, ctx): if ctx.guild != self.bot.modmail_guild: return await ctx.send( - f"You can only setup in the Modmail guild: {self.bot.modmail_guild}." + _("You can only setup in the Modmail guild: {guild_name}.".format(guild_name=self.bot.modmail_guild)) ) if self.bot.main_category is not None: logger.debug("Can't re-setup server, main_category is found.") - return await ctx.send(f"{self.bot.modmail_guild} is already set up.") + return await ctx.send(_("{guild_name} is already set up.").format(guild_name=self.bot.modmail_guild)) if self.bot.modmail_guild is None: embed = discord.Embed( - title="Error", - description="Modmail functioning guild not found.", + title=_("Error"), + description=_("Modmail functioning guild not found."), color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -88,21 +88,23 @@ async def setup(self, ctx): ) embed = discord.Embed( - title="Friendly Reminder", - description=f"You may use the `{self.bot.prefix}config set log_channel_id " - "` command to set up a custom log channel, then you can delete this default " - f"{log_channel.mention} log channel.", + title=_("Friendly Reminder"), + description=_("You may use the `{prefix}config set log_channel_id " + "` command to set up a custom log channel, then you can delete this default " + "{log_channel} log channel.").format(prefix=self.bot.prefix, log_channel=log_channel.mention), color=self.bot.main_color, ) embed.add_field( - 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: and if you are " - "feeling extra generous, buy us coffee on [Patreon](https://patreon.com/kyber) :heart:!", + name=_("Thanks for using the 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)!"), ) - embed.set_footer(text=f'Type "{self.bot.prefix}help" for a complete list of commands.') + embed.set_footer( + text=_('Type "{prefix}help" for a complete list of commands.').format(prefix=self.bot.prefix) + ) await log_channel.send(embed=embed) self.bot.config["main_category_id"] = category.id @@ -110,12 +112,12 @@ 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" - 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." + _("**Successfully set up server.**\n" + "Consider setting permission levels " + "to give access to roles or users the ability to use Modmail.\n\n" + "Type:\n- `{prefix}permissions` and `{prefix}permissions add` " + "for more info on setting permissions.\n" + "- `{prefix}config help` for a list of available customizations.").format(prefix=self.bot.prefix) ) if not self.bot.config["command_permissions"] and not self.bot.config["level_permissions"]: @@ -170,7 +172,7 @@ async def snippet(self, ctx, *, name: str.lower = None): 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) + embed.set_author(name=_("Snippets"), icon_url=ctx.guild.icon_url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -212,25 +214,25 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte """ if name in self.bot.snippets: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"Snippet `{name}` already exists.", + description=_("Snippet `{name}` already exists.").format(name=name), ) return await ctx.send(embed=embed) if name in self.bot.aliases: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"An alias that shares the same name exists: `{name}`.", + description=_("An alias with the same name already exists: `{name}`.").format(name=name), ) return await ctx.send(embed=embed) if len(name) > 120: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description="Snippet names cannot be longer than 120 characters.", + description=_("Snippet names cannot be longer than 120 characters."), ) return await ctx.send(embed=embed) @@ -251,9 +253,9 @@ async def snippet_remove(self, ctx, *, name: str.lower): if name in self.bot.snippets: embed = discord.Embed( - title="Removed snippet", + title=_("Removed snippet"), color=self.bot.main_color, - description=f"Snippet `{name}` is now deleted.", + description=_("Snippet `{name}` is now deleted.").format(name=name), ) self.bot.snippets.pop(name) await self.bot.config.update() @@ -276,9 +278,9 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): await self.bot.config.update() embed = discord.Embed( - title="Edited snippet", + title=_("Edited snippet"), color=self.bot.main_color, - description=f'`{name}` will now send "{value}".', + description=f_('`{name}` will now send "{value}".'), ) else: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") @@ -305,13 +307,13 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = if self.bot.config["thread_move_notify"] and not silent: embed = discord.Embed( - title="Thread Moved", + title=_("Thread Moved"), description=self.bot.config["thread_move_response"], color=self.bot.main_color, ) await thread.recipient.send(embed=embed) - sent_emoji, _ = await self.bot.retrieve_emoji() + sent_emoji, x = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) async def send_scheduled_close_message(self, ctx, after, silent=False): @@ -320,15 +322,17 @@ async def send_scheduled_close_message(self, ctx, after, silent=False): silent = "*silently* " if silent else "" embed = discord.Embed( - title="Scheduled close", - description=f"This thread will close {silent}in {human_delta}.", + title=_("Scheduled close"), + description=_("This thread will close {silent}in {time}.").format(silent=silent, time=human_delta), color=self.bot.error_color, ) if after.arg and not silent: - embed.add_field(name="Message", value=after.arg) + 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) @@ -362,20 +366,21 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): close_after = (after.dt - now).total_seconds() if after else 0 message = after.arg if after else None - silent = str(message).lower() in {"silent", "silently"} - cancel = str(message).lower() == "cancel" + silent = str(message).lower() in {_("silent"), _("silently")} + cancel = str(message).lower() == _("cancel") if cancel: 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( color=self.bot.error_color, - description="This thread has not already been scheduled to close.", + description=_("This thread has not already been scheduled to close."), ) return await ctx.send(embed=embed) @@ -425,14 +430,15 @@ async def notify( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=f"{mention} is already going to be mentioned.", + description=_("{mention} is already going to be mentioned.").format(mention=mention), ) else: mentions.append(mention) 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=_("{mention} will be mentioned " + "on the next message received.").format(mention=mention), ) return await ctx.send(embed=embed) @@ -463,13 +469,14 @@ async def unnotify( if mention not in mentions: embed = discord.Embed( color=self.bot.error_color, - description=f"{mention} does not have a pending notification.", + description=_("{mention} does not have a pending notification.").format(mention=mention), ) else: 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=_("{mention} will no longer be notified.").format(mention=mention), ) return await ctx.send(embed=embed) @@ -502,14 +509,15 @@ async def subscribe( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=f"{mention} is not subscribed to this thread.", + description=_("{mention} is already subscribed to this thread.").format(mention=mention), ) 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=_("{mention} will now be " + "notified of all messages received.").format(mention=mention), ) return await ctx.send(embed=embed) @@ -540,14 +548,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=_("{mention} is not already subscribed to this thread.").format(mention=mention), ) else: mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=f"{mention} is now unsubscribed from this thread.", + description=_("{mention} is now unsubscribed to this thread.").format(mention=mention), ) return await ctx.send(embed=embed) @@ -557,7 +565,7 @@ async def unsubscribe( 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() + sent_emoji, x = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -566,7 +574,7 @@ async def nsfw(self, ctx): 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() + sent_emoji, x = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -599,24 +607,26 @@ def format_log_embeds(self, logs, avatar_url): embed.add_field(name="Created", value=duration(created_at, now=datetime.utcnow())) closer = entry.get("closer") if closer is None: - closer_msg = "Unknown" + closer_msg = _("Unknown") else: closer_msg = f"<@{closer['id']}>" - embed.add_field(name="Closed By", value=closer_msg) + embed.add_field(name=_("Closed By"), value=closer_msg) if entry["recipient"]["id"] != entry["creator"]["id"]: - embed.add_field(name="Created by", value=f"<@{entry['creator']['id']}>") + 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. - embed.add_field(name="Link", value=log_url) + embed.add_field(name=_("Link"), value=log_url) else: logger.debug("Invalid log entry: no closer.") - embed.add_field(name="Log Key", value=f"`{entry['key']}`") + embed.add_field(name=_("Log Key"), value=f"`{entry['key']}`") - embed.set_footer(text="Recipient ID: " + str(entry["recipient"]["id"])) + embed.set_footer(text=_("Recipient ID") + ": " + str(entry["recipient"]["id"])) embeds.append(embed) return embeds @@ -647,7 +657,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) @@ -680,7 +690,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) @@ -699,14 +709,14 @@ async def logs_delete(self, ctx, key_or_link: str): if not success: embed = discord.Embed( - title="Error", - description=f"Log entry `{key}` not found.", + title=_("Error"), + description=_("Log entry `{key}` not found.").format(key=key), color=self.bot.error_color, ) else: embed = discord.Embed( title="Success", - description=f"Log entry `{key}` successfully deleted.", + description=_("Log entry `{key}` successfully deleted.").format(key=key), color=self.bot.main_color, ) @@ -730,7 +740,7 @@ async def logs_responded(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( color=self.bot.error_color, - description=f"{getattr(user, 'mention', user.id)} has not responded to any threads.", + description=_("{mention} has not responded to any threads.").format(mention=getattr(user, 'mention', user.id)), ) return await ctx.send(embed=embed) @@ -763,7 +773,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): 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) @@ -856,13 +866,13 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): except ValueError: return await ctx.send( embed=discord.Embed( - title="Failed", - description="Cannot find a message to edit.", + title=_("Failed"), + description=_("Cannot find a message to edit."), color=self.bot.error_color, ) ) - sent_emoji, _ = await self.bot.retrieve_emoji() + sent_emoji, x = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -886,7 +896,8 @@ 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) @@ -894,8 +905,8 @@ async def contact( if exists: embed = discord.Embed( color=self.bot.error_color, - description="A thread for this user already " - f"exists in {exists.channel.mention}.", + description=_("A thread for this user already " + "exists in {mention}.").format(mention=exists.channel.mention), ) await ctx.channel.send(embed=embed) @@ -905,13 +916,14 @@ async def contact( 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} for {user.mention}.", + title=_("Created Thread"), + description=_("Thread started by {author_mention} " + "for {user_mention}.").format(author_mention=ctx.author.mention, user_mention=user.mention), color=self.bot.main_color, ) await thread.wait_until_ready() await thread.channel.send(embed=embed) - sent_emoji, _ = await self.bot.retrieve_emoji() + sent_emoji, x = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) await asyncio.sleep(3) await ctx.message.delete() @@ -922,7 +934,11 @@ 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 = [] @@ -941,10 +957,10 @@ async def blocked(self, ctx): embed = embeds[0] for mention, reason in users: - line = mention + f" - {reason or 'No Reason Provided'}\n" + line = mention + f" - {reason or _('No Reason Provided')}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title="Blocked Users (Continued)", + title=_("Blocked Users") + " " + _("(Continued)"), color=self.bot.main_color, description=line, ) @@ -952,7 +968,7 @@ async def blocked(self, ctx): else: embed.description += line else: - embeds[0].description = "Currently there are no blocked users." + embeds[0].description = _("Currently there are no blocked users.") session = EmbedPaginatorSession(ctx, *embeds) await session.run() @@ -978,8 +994,8 @@ async def blocked_whitelist(self, ctx, *, user: User = None): if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( - title="Success", - description=f"{mention} is no longer whitelisted.", + title=_("Success"), + description=_("{mention} is no longer whitelisted.").format(mention=mention), color=self.bot.main_color, ) self.bot.blocked_whitelisted_users.remove(str(user.id)) @@ -999,8 +1015,8 @@ async def blocked_whitelist(self, ctx, *, user: User = None): reason = msg[16:].strip().rstrip(".") embed = discord.Embed( title="Success", - description=f"{mention} was previously blocked internally for " - f'"{reason}". {mention} is now whitelisted.', + description=_("{mention} was previously blocked internally for " + '"{reason}". {mention} is now whitelisted.').format(mention=mention, reason=reason), color=self.bot.main_color, ) else: @@ -1040,8 +1056,8 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( - title="Error", - description=f"Cannot block {mention}, user is whitelisted.", + title=_("Error"), + description=_("Cannot block {mention}, user is whitelisted.").format(mention=mention), color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -1065,16 +1081,16 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi if str(user.id) in self.bot.blocked_users and msg: old_reason = msg.strip().rstrip(".") embed = discord.Embed( - title="Success", - description=f"{mention} was previously blocked {old_reason}.\n" - f"{mention} is now blocked {reason}", + title=_("Success"), + description=_("{mention} was previously blocked {old_reason}.\n" + "{mention} is now blocked {reason}").format(mention=mention, old_reason=old_reason, reason=reason), color=self.bot.main_color, ) else: embed = discord.Embed( - title="Success", + title=_("Success"), color=self.bot.main_color, - description=f"{mention} is now blocked {reason}", + description=_("{mention} is now blocked {reason}").format(mention=mention, reason=reason), ) self.bot.blocked_users[str(user.id)] = reason await self.bot.config.update() @@ -1113,24 +1129,28 @@ 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 {reason}.\n" - f"{mention} is no longer blocked.", + description=_("{mention} was previously blocked internally " + "{reason}.\n{mention} is no longer blocked.").format(mention=mention, reason=reason), 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. " - f'Use "{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' + text=_("However, if the original system block reason still applies, " + "{name} will be automatically blocked again. Use " + '"{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.').format( + name=name, prefix=self.bot.prefix, user_id=user.id + ) ) else: embed = discord.Embed( - title="Success", + title=_("Success"), color=self.bot.main_color, - description=f"{mention} is no longer blocked.", + description=_("{mention} is no longer blocked.").format(mention=mention), ) else: embed = discord.Embed( - title="Error", description=f"{mention} is not blocked.", color=self.bot.error_color + title=_("Error"), + description=_("{mention} is not blocked.").format(mention=mention), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -1150,18 +1170,17 @@ async def delete(self, ctx, message_id: int = None): thread = ctx.thread try: - await thread.delete_message(message_id, note=True) - except ValueError as e: - logger.warning("Failed to delete message: %s.", e) + await thread.delete_message(message_id) + except ValueError: return await ctx.send( embed=discord.Embed( - title="Failed", - description="Cannot find a message to delete.", + title=_("Failed"), + description=_("Cannot find a message to delete."), color=self.bot.error_color, ) ) - sent_emoji, _ = await self.bot.retrieve_emoji() + sent_emoji, x = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -1283,15 +1302,20 @@ async def enable(self, ctx): Undo's the `{prefix}disable` command, all DM will be relayed after running this command. """ - embed = discord.Embed( - title="Success", - description="Modmail will now accept all DM messages.", - color=self.bot.main_color, - ) if self.bot.config["dm_disabled"] != 0: + embed = discord.Embed( + title=_("Success"), + description=_("Modmail will now accept **all** DM messages."), + color=self.bot.main_color, + ) self.bot.config["dm_disabled"] = 0 await self.bot.config.update() + else: + embed = discord.Embed( + description=_("Modmail is already accepting all DM messages."), + color=self.bot.error_color, + ) return await ctx.send(embed=embed) @@ -1315,12 +1339,25 @@ async def disable_new(self, ctx): No new threads can be created through DM. """ - embed = discord.Embed( - title="Success", - description="Modmail will not create any new threads.", - color=self.bot.main_color, - ) if self.bot.config["dm_disabled"] < 1: + embed = discord.Embed( + title=_("Success"), + description=_("Modmail will not create any **new** threads."), + color=self.bot.main_color, + ) + self.bot.config["dm_disabled"] = 1 + await self.bot.config.update() + elif self.bot.config["dm_disabled"] == 1: + embed = discord.Embed( + description=_("Modmail is already not creating any new threads."), + color=self.bot.error_color, + ) + else: + embed = discord.Embed( + title=_("Success"), + description=_("Modmail will not create **new** threads, but existing threads will now be functioning."), + color=self.bot.main_color, + ) self.bot.config["dm_disabled"] = 1 await self.bot.config.update() @@ -1334,15 +1371,20 @@ async def disable_all(self, ctx): No new threads can be created through DM nor no further DM messages will be relayed. """ - embed = discord.Embed( - title="Success", - description="Modmail will not accept any DM messages.", - color=self.bot.main_color, - ) - if self.bot.config["dm_disabled"] != 2: + if self.bot.config["dm_disabled"] < 2: + embed = discord.Embed( + title=_("Success"), + description=_("Modmail will not accept **any** DM messages."), + color=self.bot.main_color, + ) self.bot.config["dm_disabled"] = 2 await self.bot.config.update() + else: + embed = discord.Embed( + description=_("Modmail is already not accepting any DM messages."), + color=self.bot.error_color, + ) return await ctx.send(embed=embed) @@ -1355,20 +1397,20 @@ async def isenable(self, ctx): if self.bot.config["dm_disabled"] == 1: embed = discord.Embed( - title="New Threads Disabled", - description="Modmail is not creating new threads.", + title=_("New Threads Disabled"), + 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="Modmail is not accepting any DM messages for new and existing threads.", + title=_("All DM Disabled"), + 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="Modmail is accepting all DM messages.", + title=_("Enabled"), + description=_("Modmail is accepting all DM messages."), color=self.bot.main_color, ) diff --git a/cogs/plugins.py b/cogs/plugins.py index d14035286d..8868df3e77 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -104,7 +104,7 @@ def __init__(self, bot): self.bot.loop.create_task(self.populate_registry()) - if self.bot.config.get("enable_plugins"): + if getattr(self.bot, 'config', None) and self.bot.config.get("enable_plugins"): self.bot.loop.create_task(self.initial_load_plugins()) else: logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") @@ -187,7 +187,7 @@ async def load_plugin(self, plugin): if req_txt.exists(): # Install PIP requirements - venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env + venv = hasattr(sys, "real_prefix") # in a virtual env user_install = " --user" if not venv else "" proc = await asyncio.create_subprocess_shell( f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", @@ -227,7 +227,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if not self._ready_event.is_set(): embed = discord.Embed( - description="Plugins are still loading, please try again later.", + description=_("Plugins are still loading, please try again later."), color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -243,8 +243,8 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if required_version and self.bot.version < parse_version(required_version): embed = discord.Embed( - description="Your bot's version is too low. " - f"This plugin requires version `{required_version}`.", + description=_("Your bot's version is too low. " + "This plugin requires version `{required_version}`.").format(required_version=required_version), color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -257,9 +257,9 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): plugin = Plugin.from_string(plugin_name) except InvalidPluginError: embed = discord.Embed( - description="Invalid plugin name, double check the plugin name " - "or use one of the following formats: " - "username/repo/plugin, username/repo/plugin@branch.", + description=_("Invalid plugin name, double check the plugin name " + "or use one of the following formats: " + "username/repo/plugin, username/repo/plugin@branch."), color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -292,20 +292,21 @@ 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) if plugin.name in self.bot.cogs: # another class with the same name embed = discord.Embed( - description="Cannot install this plugin (dupe cog name).", + description=_("Cannot install this plugin (dupe cog name)."), color=self.bot.error_color, ) return await ctx.send(embed=embed) embed = discord.Embed( - description=f"Starting to download plugin from {plugin.link}...", + description=_("Starting to download plugin from {plugin_link}...").format(plugin_link=plugin.link), color=self.bot.main_color, ) msg = await ctx.send(embed=embed) @@ -316,7 +317,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): logger.warning("Unable to download plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description="Failed to download plugin, check logs for error.", + description=_("Failed to download plugin, check logs for error."), color=self.bot.error_color, ) @@ -335,24 +336,24 @@ async def plugins_add(self, ctx, *, plugin_name: str): logger.warning("Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description="Failed to download plugin, check logs for error.", + description=_("Failed to download plugin, check logs for error."), color=self.bot.error_color, ) else: embed = discord.Embed( - description="Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*", + description=_("Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*"), color=self.bot.main_color, ) else: embed = discord.Embed( - description="Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*\n\n" - "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " - "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.", + description=_("Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*") + "\n\n" + + _("This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot."), color=self.bot.main_color, ) return await msg.edit(embed=embed) @@ -372,7 +373,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description="Plugin is not installed.", color=self.bot.error_color + description=_("Plugin is not installed."), color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -398,7 +399,8 @@ 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) @@ -410,7 +412,7 @@ async def update_plugin(self, ctx, plugin_name): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description="Plugin is not installed.", color=self.bot.error_color + description=_("Plugin is not installed."), color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -424,7 +426,8 @@ 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=_("Successfully updated {plugin_name}.").format(plugin_name=plugin.name), + color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -456,22 +459,23 @@ async def plugins_loaded(self, ctx): if not self.bot.config.get("enable_plugins"): embed = discord.Embed( - description="No plugins are loaded due to `ENABLE_PLUGINS=false`, " - "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.", + description=_("No plugins are loaded due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot."), color=self.bot.error_color, ) return await ctx.send(embed=embed) if not self._ready_event.is_set(): embed = discord.Embed( - description="Plugins are still loading, please try again later.", + description=_("Plugins are still loading, please try again later."), color=self.bot.main_color, ) return await ctx.send(embed=embed) if not self.loaded_plugins: embed = discord.Embed( - description="There are no plugins currently loaded.", color=self.bot.error_color + description=_("There are no plugins currently loaded."), + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -491,7 +495,7 @@ async def plugins_loaded(self, ctx): embeds = [] for page in pages: embed = discord.Embed( - title="Loaded plugins:", description=page, color=self.bot.main_color + title=_("Loaded plugins:"), description=page, color=self.bot.main_color ) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) @@ -527,14 +531,15 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N if not index and plugin_name is not None: embed = discord.Embed( color=self.bot.error_color, - description=f'Could not find a plugin with name "{plugin_name}" within the registry.', + description=_('Could not find a plugin with name "{plugin_name}" within the registry.').format(plugin_name=plugin_name), ) matches = get_close_matches(plugin_name, self.registry.keys()) 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) @@ -554,7 +559,8 @@ 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 {name}```" + name=_("Installation"), + value=f"```{self.bot.prefix}plugins add {plugin_name}```", ) embed.set_author( @@ -573,11 +579,11 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N required_version = details.get("bot_version", False) if required_version and self.bot.version < parse_version(required_version): embed.set_footer( - text="Your bot is unable to install this plugin, " - f"minimum required version is v{required_version}." + text=_("Your bot is unable to install this plugin, " + "minimum required version is v{required_version}.").format(required_version=required_version) ) else: - embed.set_footer(text="Your bot is able to install this plugin.") + embed.set_footer(text=_("Your bot is able to install this plugin.")) embeds.append(embed) @@ -632,7 +638,7 @@ async def plugins_registry_compact(self, ctx): for page in pages: embed = discord.Embed(color=self.bot.main_color, description=page) - embed.set_author(name="Plugin Registry", icon_url=self.bot.user.avatar_url) + embed.set_author(name=_("Plugin Registry"), icon_url=self.bot.user.avatar_url) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) diff --git a/cogs/utility.py b/cogs/utility.py index f8460c7d52..2704e57f1b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -47,7 +47,7 @@ async def format_cog_help(self, cog, *, no_cog=False): else: format_ = f"`[{perm_level}] {prefix + cmd.qualified_name}` " - format_ += f"- {cmd.short_doc}\n" + format_ += f"- {_(cmd.short_doc)}\n" if not format_.strip(): continue if len(format_) + len(formats[-1]) >= 1024: @@ -58,21 +58,25 @@ async def format_cog_help(self, cog, *, no_cog=False): embeds = [] for format_ in formats: description = ( - cog.description or "No description." + _(cog.description) or _("No description.") if not no_cog - else "Miscellaneous commands without a category." + else _("Miscellaneous commands without a category.") ) embed = discord.Embed(description=f"*{description}*", color=bot.main_color) - embed.add_field(name="Commands", value=format_ or "No commands.") + 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" + continued = " " + _("(Continued)") if embeds else "" + 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( - text=f'Type "{prefix}{self.command_attrs["name"]} command" ' - "for more info on a specific command." + text=_('Type "{prefix}{command} command" ' + "for more info on a specific command.").format(prefix=prefix, command=self.command_attrs["name"]) ) embeds.append(embed) return embeds @@ -112,19 +116,19 @@ async def _get_help_embed(self, topic): if perm_level is not PermissionLevel.INVALID: perm_level = f"{perm_level.name} [{perm_level}]" else: - perm_level = "NONE" + perm_level = _("NONE") embed = discord.Embed( title=f"`{self.get_command_signature(topic)}`", color=self.context.bot.main_color, - description=self.process_help_msg(topic.help), + description=self.process_help_msg(_(topic.help)), ) return embed, perm_level async def send_command_help(self, command): topic = await self._get_help_embed(command) if topic is not None: - topic[0].set_footer(text=f"Permission level: {topic[1]}") + topic[0].set_footer(text=_("Permission level: {level}").format(level=topic[1])) await self.get_destination().send(embed=topic[0]) async def send_group_help(self, group): @@ -132,7 +136,7 @@ async def send_group_help(self, group): if topic is None: return embed = topic[0] - embed.add_field(name="Permission Level", value=topic[1], inline=False) + embed.add_field(name=_("Permission Level"), value=topic[1], inline=False) format_ = "" length = len(group.commands) @@ -146,12 +150,12 @@ async def send_group_help(self, group): branch = "└─" else: branch = "├─" - format_ += f"`{branch} {command.name}` - {command.short_doc}\n" + format_ += f"`{branch} {command.name}` - {_(command.short_doc)}\n" - embed.add_field(name="Sub Command(s)", value=format_[:1024], inline=False) + embed.add_field(name=_("Sub Command(s)"), value=format_[:1024], inline=False) embed.set_footer( - text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' - "for more info on a command." + text=_('Type "{prefix}{command} command" ' + "for more info on a command.").format(prefix=self.clean_prefix, command=self.command_attrs["name"]) ) await self.get_destination().send(embed=embed) @@ -161,7 +165,7 @@ async def send_error_message(self, error): val = self.context.bot.snippets.get(command) if val is not None: embed = discord.Embed( - title=f"{command} is a snippet.", color=self.context.bot.main_color + title=_("{command} is a snippet.").format(command=command), color=self.context.bot.main_color ) embed.add_field(name=f"`{command}` will send:", value=val) return await self.get_destination().send(embed=embed) @@ -172,10 +176,10 @@ async def send_error_message(self, error): if not values: embed = discord.Embed( - title="Error", + 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.", + description=_("Alias `{command}` is invalid, this alias will now be deleted." + "This alias will now be deleted.").format(command=command), ) embed.add_field(name=f"{command}` used to be:", value=val) self.context.bot.aliases.pop(command) @@ -183,28 +187,28 @@ async def send_error_message(self, error): else: if len(values) == 1: embed = discord.Embed( - title=f"{command} is an alias.", color=self.context.bot.main_color + title=_("{command} is an alias.").format(command=command), color=self.context.bot.main_color ) - embed.add_field(name=f"`{command}` points to:", value=values[0]) + embed.add_field(name=_("`{command}` points to:").format(command=command), value=values[0]) else: embed = discord.Embed( - title=f"{command} is an alias.", + title=_("{command} is an alias.").format(command=command), color=self.context.bot.main_color, - description=f"**`{command}` points to the following steps:**", + description=_("**`{command}` points to the following steps:**").format(command=command), ) for i, val in enumerate(values, start=1): - embed.add_field(name=f"Step {i}:", value=val) + embed.add_field(name=_("Step") + f" {i}:", value=val) embed.set_footer( - text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" ' - "for more details on aliases." + text=_('Type "{prefix}{command} alias" ' + "for more details on aliases.").format(prefix=self.clean_prefix, command=self.command_attrs["name"]) ) return await self.get_destination().send(embed=embed) logger.warning("CommandNotFound: %s", error) embed = discord.Embed(color=self.context.bot.error_color) - embed.set_footer(text=f'Command/Category "{command}" not found.') + embed.set_footer(text=_('Command/Category "{command}" not found.').format(command=command)) choices = set() @@ -214,12 +218,14 @@ async def send_error_message(self, error): closest = get_close_matches(command, choices) if closest: - embed.add_field(name="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.title = _("Cannot find command or category") embed.set_footer( - text=f'Type "{self.clean_prefix}{self.command_attrs["name"]}" ' - "for a list of all available commands." + text=_('Type "{prefix}{command}" ' + "for a list of all available commands.").format(prefix=self.clean_prefix, command=self.command_attrs["name"]) ) await self.get_destination().send(embed=embed) @@ -233,7 +239,7 @@ def __init__(self, bot): self.bot.help_command = ModmailHelpCommand( verify_checks=False, command_attrs={ - "help": "Shows this help message.", + "help": _("Shows this help message."), "checks": [checks.has_permissions_predicate(PermissionLevel.REGULAR)], }, ) @@ -257,7 +263,7 @@ async def changelog(self, ctx, version: str.lower = ""): return await ctx.send( embed=discord.Embed( color=self.bot.error_color, - description=f"The specified version `{version}` could not be found.", + description=_("The specified version `{version}` could not be found.").format(version=version), ) ) @@ -273,7 +279,7 @@ async def changelog(self, ctx, version: str.lower = ""): finally: logger.warning("Failed to display changelog.", exc_info=True) await ctx.send( - f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}" + _("View the changelog here: {url}").format(url=f'{changelog.latest_version.changelog_url}#v{version[::2]}') ) @commands.command(aliases=["info"]) @@ -283,21 +289,21 @@ async def about(self, ctx): """Shows information about this bot.""" embed = discord.Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) embed.set_author( - name="Modmail - About", + name=_("Modmail - About"), icon_url=self.bot.user.avatar_url, url="https://discord.gg/F34cRU8", ) embed.set_thumbnail(url=self.bot.user.avatar_url) - desc = "This is an open source Discord bot that serves as a means for " - desc += "members to easily communicate with server administrators in " - desc += "an organised manner." + desc = _("This is an open source Discord bot that serves as a means for " + "members to easily communicate with server administrators in " + "an organised manner.") embed.description = desc - embed.add_field(name="Uptime", value=self.bot.uptime) - embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms") - embed.add_field(name="Version", value=f"`{self.bot.version}`") - embed.add_field(name="Authors", value="`kyb3r`, `Taki`, `fourjr`") + embed.add_field(name=_("Uptime"), value=self.bot.uptime) + embed.add_field(name=_("Latency"), value=f"{self.bot.latency * 1000:.2f} ms") + embed.add_field(name=_("Version"), value=f"`{self.bot.version}`") + embed.add_field(name=_("Authors"), value="`kyb3r`, `Taki`, `fourjr`") changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version @@ -306,26 +312,24 @@ async def about(self, ctx): stable = next( filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions) ) - footer = ( - f"You are on the prerelease version • the latest version is v{stable.version}." - ) + footer = _("You are on the prerelease version • the latest version is v{version}.").format(version=stable.version) elif self.bot.version < parse_version(latest.version): - footer = f"A newer version is available v{latest.version}." + footer = _("A newer version is available v{version}.").format(version=latest.version) else: - footer = "You are up to date with the latest version." + footer = _("You are up to date with the latest version.") embed.add_field( - name="Want Modmail in Your Server?", - value="Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " - "and join our [Discord server](https://discord.gg/F34cRU8/)!", + name=_("Want Modmail in Your Server?"), + value=_("Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " + "and join our [Discord server](https://discord.gg/F34cRU8/)!"), inline=False, ) embed.add_field( - name="Support the Developers", - value="This bot is completely free for everyone. We rely on kind individuals " - "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " - "to keep this bot free forever!", + name=_("Support the Developers"), + value=_("This bot is completely free for everyone. We rely on kind individuals " + "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " + "to keep this bot free forever!"), inline=False, ) @@ -372,10 +376,10 @@ async def debug(self, ctx): if not logs: embed = discord.Embed( color=self.bot.main_color, - title="Debug Logs:", - description="You don't have any logs at the moment.", + title=_("Debug Logs:"), + description=_("You don't have any logs at the moment."), ) - embed.set_footer(text="Go to Heroku to see your logs.") + embed.set_footer(text=_("Go to Heroku to see your logs.")) return await ctx.send(embed=embed) messages = [] @@ -401,7 +405,7 @@ async def debug(self, ctx): messages.append(msg) embed = discord.Embed(color=self.bot.main_color) - embed.set_footer(text="Debug logs - Navigate using the reactions below.") + embed.set_footer(text=_("Debug logs - Navigate using the reactions below.")) session = MessagePaginatorSession(ctx, *messages, embed=embed) session.current = len(messages) - 1 @@ -433,7 +437,7 @@ async def debug_hastebin(self, ctx): logger.error(data["message"]) raise embed = discord.Embed( - title="Debug Logs", + title=_("Debug Logs"), color=self.bot.main_color, description=f"{haste_url}/" + key, ) @@ -441,9 +445,9 @@ 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.") + embed.set_footer(text=_("Go to Heroku to see your logs.")) await ctx.send(embed=embed) @debug.command(name="clear", aliases=["wipe"]) @@ -463,7 +467,7 @@ async def debug_clear(self, ctx): pass await ctx.send( embed=discord.Embed( - color=self.bot.main_color, description="Cached logs are now cleared." + color=self.bot.main_color, description=_("Cached logs are now cleared.") ) ) @@ -494,7 +498,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): self.bot.config.remove("activity_message") await self.bot.config.update() await self.set_presence() - embed = discord.Embed(title="Activity Removed", color=self.bot.main_color) + embed = discord.Embed(title=_("Activity Removed"), color=self.bot.main_color) return await ctx.send(embed=embed) if not message: @@ -505,7 +509,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="activity")) - activity, _ = await self.set_presence( + activity, x = await self.set_presence( activity_type=activity_type, activity_message=message ) @@ -513,13 +517,15 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): self.bot.config["activity_message"] = activity.name await self.bot.config.update() - msg = f"Activity set to: {activity.type.name.capitalize()} " + msg = _("Activity set to: {name} ").format(name=activity.type.name.capitalize()) if activity.type == ActivityType.listening: - msg += f"to {activity.name}." + msg += _("to {name}.").format(name=activity.name) 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() @@ -541,7 +547,7 @@ async def status(self, ctx, *, status_type: str.lower): self.bot.config.remove("status") await self.bot.config.update() await self.set_presence() - embed = discord.Embed(title="Status Removed", color=self.bot.main_color) + embed = discord.Embed(title=_("Status Removed"), color=self.bot.main_color) return await ctx.send(embed=embed) status_type = status_type.replace(" ", "_") @@ -550,13 +556,15 @@ async def status(self, ctx, *, status_type: str.lower): except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="status")) - _, status = await self.set_presence(status=status) + x, status = await self.set_presence(status=status) self.bot.config["status"] = status.value await self.bot.config.update() - msg = f"Status set to: {status.value}." - embed = discord.Embed(title="Status Changed", description=msg, color=self.bot.main_color) + msg = _("Status set to: {value}.").format(value=status.value) + embed = discord.Embed( + title=_("Status Changed"), description=msg, color=self.bot.main_color + ) return await ctx.send(embed=embed) async def set_presence(self, *, status=None, activity_type=None, activity_message=None): @@ -627,7 +635,7 @@ async def before_loop_presence(self): async def ping(self, ctx): """Pong! Returns your websocket latency.""" embed = discord.Embed( - title="Pong! Websocket Latency:", + title=_("Pong! Websocket Latency:"), description=f"{self.bot.ws.latency * 1000:.4f} ms", color=self.bot.main_color, ) @@ -646,12 +654,14 @@ 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( - title="Changed mention!", - description=f'On thread creation the bot now says "{mention}".', + title=_("Changed mention!"), + description=_('On thread creation the bot now says "{mention}".').format(mention=mention), color=self.bot.main_color, ) self.bot.config["mention"] = mention @@ -670,14 +680,14 @@ async def prefix(self, ctx, *, prefix=None): current = self.bot.prefix embed = discord.Embed( - title="Current prefix", color=self.bot.main_color, description=f"{current}" + title=_("Current prefix"), color=self.bot.main_color, description=f"{current}" ) if prefix is None: await ctx.send(embed=embed) else: - embed.title = "Changed prefix!" - embed.description = f"Set prefix to `{prefix}`" + embed.title = _("Changed prefix!") + embed.description = _("Set prefix to `{prefix}`").format(prefix=prefix) self.bot.config["prefix"] = prefix await self.bot.config.update() await ctx.send(embed=embed) @@ -712,7 +722,7 @@ async def config_options(self, ctx): f"`{name}`" for name in takewhile(lambda x: x is not None, names) ) embed = discord.Embed( - title="Available configuration keys:", + title=_("Available configuration keys:"), color=self.bot.main_color, description=description, ) @@ -733,18 +743,20 @@ async def config_set(self, ctx, key: str.lower, *, value: str): self.bot.config.set(key, value) await self.bot.config.update() embed = discord.Embed( - title="Success", + title=_("Success"), color=self.bot.main_color, - description=f"Set `{key}` to `{self.bot.config[key]}`.", + description=_("Set `{key}` to `{value}`.").format(key=key, value=self.bot.config[key]), ) except InvalidConfigError as exc: 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=_("{key} is an invalid key.").format(key=key), ) valid_keys = [f"`{k}`" for k in sorted(keys)] - embed.add_field(name="Valid keys", value=", ".join(valid_keys)) + embed.add_field(name=_("Valid keys"), value=", ".join(valid_keys)) return await ctx.send(embed=embed) @@ -757,16 +769,18 @@ async def config_remove(self, ctx, *, key: str.lower): self.bot.config.remove(key) await self.bot.config.update() embed = discord.Embed( - title="Success", + title=_("Success"), color=self.bot.main_color, - description=f"`{key}` had been reset to default.", + description=_("`{key}` had been reset to default.").format(key=key), ) 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=_("{key} is an invalid key.").format(key=key), ) valid_keys = [f"`{k}`" for k in sorted(keys)] - embed.add_field(name="Valid keys", value=", ".join(valid_keys)) + embed.add_field(name=_("Valid keys"), value=", ".join(valid_keys)) return await ctx.send(embed=embed) @@ -782,24 +796,30 @@ async def config_get(self, ctx, *, key: str.lower = None): if key: if key in keys: - desc = f"`{key}` is set to `{self.bot.config[key]}`" + desc = _("`{key}` is set to `{value}`").format(key=key, value=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( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"`{key}` is an invalid key.", + description=_("`{key}` is an invalid key.").format(key=key), ) embed.set_footer( - text=f'Type "{self.bot.prefix}config options" for a list of config variables.' + text=_('Type "{prefix}config options" for a list of config variables.').format(prefix=self.bot.prefix) ) else: embed = discord.Embed( color=self.bot.main_color, - description="Here is a list of currently set configuration variable(s).", + 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 ) embed.set_author(name="Current config(s):", icon_url=self.bot.user.avatar_url) config = self.bot.config.filter_default(self.bot.config) @@ -823,9 +843,9 @@ async def config_help(self, ctx, key: str.lower = None): key, {**self.bot.config.public_keys, **self.bot.config.protected_keys} ) embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"`{key}` is an invalid key.", + description=_("`{key}` is an invalid key.").format(key=key), ) if closest: embed.add_field( @@ -837,9 +857,9 @@ async def config_help(self, ctx, key: str.lower = None): if key is not None and key not in config_help: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"No help details found for `{key}`.", + description=_("No help details found for `{key}`.").format(key=key), ) return await ctx.send(embed=embed) @@ -852,21 +872,24 @@ 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=_("Configuration description on {current_key}:").format(current_key=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"]: example_text += f"- {fmt(example)}\n" - embed.add_field(name="Example(s):", value=example_text, inline=False) + embed.add_field(name=_("Example(s):"), value=example_text, inline=False) note_text = "" for note in info["notes"]: note_text += f"- {fmt(note)}\n" if note_text: - embed.add_field(name="Note(s):", value=note_text, inline=False) + embed.add_field(name=_("Note(s):"), value=note_text, inline=False) if info.get("image") is not None: embed.set_image(url=fmt(info["image"])) @@ -911,10 +934,10 @@ async def alias(self, ctx, *, name: str.lower = None): if not values: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"Alias `{name}` is invalid, this alias will now be deleted." - "This alias will now be deleted.", + description=_("Alias `{name}` is invalid, it used to be `{value}`. " + "This alias will now be deleted.").format(name=name, value=escape_markdown(val)), ) embed.add_field(name=f"{name}` used to be:", value=utils.truncate(val, 1024)) self.bot.aliases.pop(name) @@ -923,7 +946,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=_("Alias") + f' - "{name}":', description=values[0], color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -941,10 +964,11 @@ 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) + embed.set_footer(text=_("Do {prefix}help alias for more commands.").format(prefix=self.bot.prefix)) + embed.set_author(name=_("Aliases"), icon_url=ctx.guild.icon_url) return await ctx.send(embed=embed) embeds = [] @@ -952,7 +976,7 @@ async def alias(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = utils.format_description(i, names) embed = discord.Embed(color=self.bot.main_color, description=description) - embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) + embed.set_author(name=_("Command Aliases"), icon_url=ctx.guild.icon_url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -980,9 +1004,9 @@ async def make_alias(self, name, value, action): values = utils.parse_alias(value) if not values: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description="Invalid multi-step alias, try wrapping each steps in quotes.", + 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 embed @@ -997,12 +1021,12 @@ async def make_alias(self, name, value, action): multiple_alias = len(values) > 1 - embed = discord.Embed(title=f"{action} alias", color=self.bot.main_color) + embed = discord.Embed(title=_("{action} alias").format(action), color=self.bot.main_color) if not multiple_alias: - embed.add_field(name=f"`{name}` points to:", value=utils.truncate(values[0], 1024)) + embed.add_field(name=_("`{name}` points to:").format(name=name), value=utils.truncate(values[0], 1024)) else: - embed.description = f"`{name}` now points to the following steps:" + embed.description = _("`{name}` now points to the following steps:").format(name=name) for i, val in enumerate(values, start=1): view = StringView(val) @@ -1018,20 +1042,20 @@ async def make_alias(self, name, value, action): if multiple_alias: embed.description = ( - "The command you are attempting to point " - f"to does not exist: `{linked_command}`." + _("The command you are attempting to point " + "to does not exist: `{command}`.").format(command=linked_command) ) else: embed.description = ( - "The command you are attempting to point " - f"to on step {i} does not exist: `{linked_command}`." + _("The command you are attempting to point " + "to on step {number} does not exist: `{command}`.").format(number=i, command=linked_command) ) return embed else: save_aliases.append(val) if multiple_alias: - embed.add_field(name=f"Step {i}:", value=utils.truncate(val, 1024)) + embed.add_field(name=_("Step") + f" {i}:", value=utils.truncate(val, 1024)) self.bot.aliases[name] = " && ".join(f'"{a}"' for a in save_aliases) await self.bot.config.update() @@ -1056,30 +1080,30 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed = None if self.bot.get_command(name): embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"A command with the same name already exists: `{name}`.", + description=_("A command with the same name already exists: `{name}`.").format(name=name), ) elif name in self.bot.aliases: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"Another alias with the same name already exists: `{name}`.", + description=_("Another alias with the same name already exists: `{name}`.").format(name=name), ) elif name in self.bot.snippets: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"A snippet with the same name already exists: `{name}`.", + description=_("A snippet with the same name already exists: `{name}`.").format(name=name), ) elif len(name) > 120: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description="Alias names cannot be longer than 120 characters.", + description=_("Alias names cannot be longer than 120 characters."), ) if embed is None: @@ -1096,9 +1120,9 @@ async def alias_remove(self, ctx, *, name: str.lower): await self.bot.config.update() embed = discord.Embed( - title="Removed alias", + title=_("Removed alias"), color=self.bot.main_color, - description=f"Successfully deleted `{name}`.", + description=_("Successfully deleted `{name}`.").format(name=name), ) else: embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias") @@ -1202,9 +1226,9 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name level = self._parse_level(level_name) if level is PermissionLevel.INVALID: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"The referenced level does not exist: `{level_name}`.", + description=_("The referenced level does not exist: `{level}`.").format(level=level_name), ) else: logger.info( @@ -1216,10 +1240,10 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name await self.bot.config.update() embed = discord.Embed( - title="Success", + title=_("Success"), color=self.bot.main_color, - description="Successfully set command permission level for " - f"`{command.qualified_name}` to `{level.name}`.", + description=_("Successfully set command permission level for " + "`{command}` to `{level}`.").format(command=command.qualified_name, level=level.name), ) return await ctx.send(embed=embed) @@ -1262,9 +1286,9 @@ async def permissions_add( if not check: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"The referenced {type_} does not exist: `{name}`.", + description=_("The referenced {type} does not exist: `{name}`.").format(type=type_, name=name), ) return await ctx.send(embed=embed) @@ -1287,9 +1311,9 @@ async def permissions_add( await self.bot.main_category.set_permissions(key, read_messages=True) embed = discord.Embed( - title="Success", + title=_("Success"), color=self.bot.main_color, - description=f"Permission for `{name}` was successfully updated.", + description=_("Permission for `{name}` was successfully updated.").format(name=name), ) return await ctx.send(embed=embed) @@ -1338,10 +1362,10 @@ async def permissions_remove( if level is None: perm = self.bot.command_perm(name) embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"The command permission level was never overridden: `{name}`, " - f"current permission level is {perm.name}.", + description=_("The command permission level was never overridden: `{name}`, " + "current permission level is {perm_name}.").format(name=name, perm_name=perm.name), ) else: logger.info("Restored command permission level for `%s`.", name) @@ -1349,9 +1373,9 @@ async def permissions_remove( await self.bot.config.update() perm = self.bot.command_perm(name) embed = discord.Embed( - title="Success", + title=_("Success"), color=self.bot.main_color, - description=f"Command permission level for `{name}` was successfully restored to {perm.name}.", + description=_("Command permission level for `{name}` was successfully restored to {perm_name}.").format(name=name, perm_name=perm.name), ) return await ctx.send(embed=embed) @@ -1363,9 +1387,9 @@ async def permissions_remove( level = self._parse_level(name) if level is PermissionLevel.INVALID: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"The referenced level does not exist: `{name}`.", + description=_("The referenced level does not exist: `{name}`.").format(name=name), ) return await ctx.send(embed=embed) name = level.name @@ -1390,9 +1414,9 @@ async def permissions_remove( await self.bot.main_category.set_permissions(member, overwrite=None) embed = discord.Embed( - title="Success", + title=_("Success"), color=self.bot.main_color, - description=f"Permission for `{name}` was successfully updated.", + description=_("Permission for `{name}` was successfully updated.").format(name=name), ) return await ctx.send(embed=embed) @@ -1403,8 +1427,8 @@ def _get_perm(self, ctx, name, type_): permissions = self.bot.config["level_permissions"].get(name, []) if not permissions: embed = discord.Embed( - title=f"Permission entries for {type_} `{name}`:", - description="No permission entries found.", + title=_("Permission entries for {type} `{name}`:").format(type=type_, name=name), + description=_("No permission entries found."), color=self.bot.main_color, ) else: @@ -1428,7 +1452,7 @@ def _get_perm(self, ctx, name, type_): values.append(str(perm)) embed = discord.Embed( - title=f"Permission entries for {type_} `{name}`:", + title=_("Permission entries for {type} `{name}`:").format(type=type_, name=name), description=", ".join(values), color=self.bot.main_color, ) @@ -1491,22 +1515,22 @@ async def permissions_get( desc_cmd = ( ", ".join(map(lambda x: f"`{x}`", cmds)) if cmds - else "No permission entries found." + else _("No permission entries found.") ) desc_level = ( ", ".join(map(lambda x: f"`{x}`", levels)) if levels - else "No permission entries found." + else _("No permission entries found.") ) embeds = [ discord.Embed( - title=f"{mention} has permission with the following commands:", + title=_("{mention} has permission with the following commands:").format(mention=mention), description=desc_cmd, color=self.bot.main_color, ), discord.Embed( - title=f"{mention} has permission with the following permission levels:", + title=_("{mention} has permission with the following permission levels:").format(mention=mention), description=desc_level, color=self.bot.main_color, ), @@ -1531,8 +1555,8 @@ async def permissions_get( if not overrides: embeds.append( discord.Embed( - title="Permission Overrides", - description="You don't have any command level overrides at the moment.", + title=_("Permission Overrides"), + description=_("You don't have any command level overrides at the moment."), color=self.bot.error_color, ) ) @@ -1546,7 +1570,7 @@ async def permissions_get( color=self.bot.main_color, description=description ) embed.set_author( - name="Permission Overrides", icon_url=ctx.guild.icon_url + name=_("Permission Overrides"), icon_url=ctx.guild.icon_url ) embeds.append(embed) @@ -1559,16 +1583,16 @@ async def permissions_get( perm = self.bot.command_perm(name) if level is None: embed = discord.Embed( - title="Error", + title=_("Error"), color=self.bot.error_color, - description=f"The command permission level was never overridden: `{name}`, " - f"current permission level is {perm.name}.", + description=_("The command permission level was never overridden: `{name}`, " + "current permission level is {perm_name}.").format(name=name, perm_name=perm.name), ) else: embed = discord.Embed( - title="Success", + title=_("Success"), color=self.bot.main_color, - description=f'Permission override for command "{name}" is "{perm.name}".', + description=_('Permission override for command "{name}" is "{perm_name}".').format(name=name, perm_name=perm.name), ) return await ctx.send(embed=embed) @@ -1644,14 +1668,19 @@ async def oauth_whitelist(self, ctx, target: Union[discord.Role, utils.User]): await self.bot.config.update() embed = discord.Embed(color=self.bot.main_color) - embed.title = "Success" + embed.title = _("Success") if not hasattr(target, "mention"): 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 {target.mention} to view logs." - ) + if removed: + embed.description = ( + _("Un-whitelisted {target_mention} to view logs.").format(target_mention=target.mention) + ) + else: + embed.description = ( + _("Whitelisted {target_mention} to view logs.").format(target_mention=target.mention) + ) await ctx.send(embed=embed) @@ -1673,10 +1702,14 @@ async def oauth_show(self, ctx): roles.append(role) embed = discord.Embed(color=self.bot.main_color) - embed.title = "Oauth Whitelist" + 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/config.py b/core/config.py index cfd79d9df9..a0eda4a2dc 100644 --- a/core/config.py +++ b/core/config.py @@ -17,6 +17,7 @@ from core.utils import strtobool logger = getLogger(__name__) +load_dotenv() class ConfigManager: @@ -134,13 +135,11 @@ class ConfigManager: defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) - def __init__(self, bot, config_path=os.path.dirname(os.path.abspath(__file__))): + def __init__(self, bot): self.bot = bot self._cache = {} self.ready_event = asyncio.Event() self.config_help = {} - self.config_path = config_path - load_dotenv(dotenv_path=os.path.join(self.config_path, ".env")) def __repr__(self): return repr(self._cache) @@ -150,9 +149,11 @@ def populate_cache(self) -> dict: # 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}) - config_json = os.path.join(self.config_path, "config.json") + config_json = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json" + ) if os.path.exists(config_json): - logger.debug("Loading envs from {}".format(config_json)) + logger.debug("Loading envs from config.json.") with open(config_json, "r", encoding="utf-8") as f: # Config json should override env vars try: diff --git a/core/paginator.py b/core/paginator.py index 7ba1c98b60..45c059f844 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -176,7 +176,7 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: """ self.running = False - sent_emoji, _ = await self.ctx.bot.retrieve_emoji() + sent_emoji, x = await self.ctx.bot.retrieve_emoji() await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji) if delete: diff --git a/core/thread.py b/core/thread.py index 960dafe0f7..0926a0acd0 100644 --- a/core/thread.py +++ b/core/thread.py @@ -72,13 +72,15 @@ def ready(self) -> bool: def ready(self, flag: bool): if flag: self._ready_event.set() - self.bot.dispatch("thread_create", self) + self.bot.dispatch("thread_ready", self) else: self._ready_event.clear() async def setup(self, *, creator=None, category=None): """Create the thread channel and other io related initialisation tasks""" - self.bot.dispatch("thread_initiate", self) + + self.bot.dispatch("thread_create", self) + recipient = self.recipient # in case it creates a channel outside of category @@ -173,7 +175,6 @@ async def send_recipient_genesis_message(): await self.bot.add_reaction(msg, close_emoji) await asyncio.gather(send_genesis_message(), send_recipient_genesis_message()) - self.bot.dispatch("thread_ready", self) def _format_info_embed(self, user, log_url, log_count, color): """Get information about a member of a server @@ -456,14 +457,9 @@ async def find_linked_messages( message_id: typing.Optional[int] = None, either_direction: bool = False, message1: discord.Message = None, - note: bool = True, ) -> typing.Tuple[discord.Message, typing.Optional[discord.Message]]: if message1 is not None: - if ( - not message1.embeds - or not message1.embeds[0].author.url - or message1.author != self.bot.user - ): + if not message1.embeds or not message1.embeds[0].author.url: raise ValueError("Malformed thread message.") elif message_id is not None: @@ -473,18 +469,13 @@ async def find_linked_messages( raise ValueError("Thread message not found.") if not ( - message1.embeds - and message1.embeds[0].author.url - and message1.embeds[0].color - and message1.author == self.bot.user + 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"): - if not note: - raise ValueError("Thread message not found.") return message1, None if message1.embeds[0].color.value != self.bot.mod_color and not ( @@ -504,8 +495,6 @@ async def find_linked_messages( and message1.embeds[0].color.value == self.bot.recipient_color ) ) - and message1.embeds[0].author.url.split("#")[-1].isdigit() - and message1.author == self.bot.user ): break else: @@ -527,7 +516,7 @@ async def find_linked_messages( if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id: return message1, msg except ValueError: - continue + 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: @@ -548,13 +537,16 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> await asyncio.gather(*tasks) - async def delete_message( - self, message: typing.Union[int, discord.Message] = None, note: bool = True - ) -> None: - if isinstance(message, discord.Message): - message1, message2 = await self.find_linked_messages(message1=message, note=note) - else: - message1, message2 = await self.find_linked_messages(message, note=note) + async def delete_message(self, message: typing.Union[int, discord.Message] = None) -> None: + try: + 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 = [] if not isinstance(message, discord.Message): tasks += [message1.delete()] @@ -579,12 +571,12 @@ async def find_linked_message_from_dm(self, message, either_direction=False): return linked_message msg_id = url.split("#")[-1] - if not msg_id.isdigit(): - continue - msg_id = int(msg_id) - if int(msg_id) == message.id: - return linked_message - raise ValueError("Thread channel message not found.") + try: + if int(msg_id) == message.id: + return linked_message + except ValueError: + 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: diff --git a/core/time.py b/core/time.py index 331e26349f..cc30d84f30 100644 --- a/core/time.py +++ b/core/time.py @@ -134,7 +134,7 @@ def convert(self, ctx, argument): # foo date time # first the first two cases: - dt, status, begin, end, _ = elements[0] + dt, status, begin, end, x = elements[0] if not status.hasDateOrTime: return self.check_constraints(self.now, argument) diff --git a/core/translations.py b/core/translations.py new file mode 100644 index 0000000000..218b5f91ae --- /dev/null +++ b/core/translations.py @@ -0,0 +1,25 @@ +import builtins +import csv +import os + + +class Translator: + def __init__(self): + self.language = os.getenv('language', 'en') + self.texts = {} + self.generate_texts() + + def generate_texts(self): + with open(f'languages/{self.language}.csv', encoding='utf8') as f: + reader = csv.reader(f, dialect='unix') + + for n, row in enumerate(reader): + if n != 0: + self.texts[row[0]] = row[1] + + def translate(self, identifier): + return self.texts.get(identifier, identifier) + + +def init(): + builtins._ = Translator().translate diff --git a/languages/en.csv b/languages/en.csv new file mode 100644 index 0000000000..388091bab5 --- /dev/null +++ b/languages/en.csv @@ -0,0 +1,1172 @@ +"Identifier","English","Context" +"You can only setup in the Modmail guild: {guild_name}.","You can only setup in the Modmail guild: {guild_name}.","File: cogs\modmail.py/L44" +"{guild_name} is already set up.","{guild_name} is already set up.","File: cogs\modmail.py/L49" +"Error","Error","File: cogs\modmail.py/L53/L217/L225/L233/L712/L1059/L1151 cogs\utility.py/L168/L748/L774/L802/L822/L892/L979/L1045/L1085/L1092/L1099/L1125/L1245/L1316/L1378/L1559" +"Modmail functioning guild not found.","Modmail functioning guild not found.","File: cogs\modmail.py/L54" +"Friendly Reminder","Friendly Reminder","File: cogs\modmail.py/L91" +"You may use the `{prefix}config set log_channel_id ` command to set up a custom log channel, then you can delete this default {log_channel} log channel.","You may use the `{prefix}config set log_channel_id ` command to set up a custom log channel, then you can delete this default {log_channel} log channel.","File: cogs\modmail.py/L92" +"Thanks for using the bot!","Thanks for using the bot!","File: cogs\modmail.py/L99" +"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)!","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)!","File: cogs\modmail.py/L100" +"Type ""{prefix}help"" for a complete list of commands.","Type ""{prefix}help"" for a complete list of commands.","File: cogs\modmail.py/L106" +"**Successfully set up server.**\nConsider setting permission levels to give access to roles or users the ability to use Modmail.\n\nType:\n- `{prefix}permissions` and `{prefix}permissions add` for more info on setting permissions.\n- `{prefix}config help` for a list of available customizations.","**Successfully set up server.**\nConsider setting permission levels to give access to roles or users the ability to use Modmail.\n\nType:\n- `{prefix}permissions` and `{prefix}permissions add` for more info on setting permissions.\n- `{prefix}config help` for a list of available customizations.","File: cogs\modmail.py/L115" +"Snippets","Snippets","File: cogs\modmail.py/L175" +"Snippet `{name}` already exists.","Snippet `{name}` already exists.","File: cogs\modmail.py/L219" +"An alias with the same name already exists: `{name}`.","An alias with the same name already exists: `{name}`.","File: cogs\modmail.py/L227" +"Snippet names cannot be longer than 120 characters.","Snippet names cannot be longer than 120 characters.","File: cogs\modmail.py/L235" +"Removed snippet","Removed snippet","File: cogs\modmail.py/L256" +"Snippet `{name}` is now deleted.","Snippet `{name}` is now deleted.","File: cogs\modmail.py/L258" +"Edited snippet","Edited snippet","File: cogs\modmail.py/L281" +"Thread Moved","Thread Moved","File: cogs\modmail.py/L310" +"Scheduled close","Scheduled close","File: cogs\modmail.py/L325" +"This thread will close {silent}in {time}.","This thread will close {silent}in {time}.","File: cogs\modmail.py/L326" +"Message","Message","File: cogs\modmail.py/L331" +"Closing will be cancelled if a thread message is sent.","Closing will be cancelled if a thread message is sent.","File: cogs\modmail.py/L334" +"silent","silent","File: cogs\modmail.py/L369" +"silently","silently","File: cogs\modmail.py/L369" +"cancel","cancel","File: cogs\modmail.py/L370" +"Scheduled close has been cancelled.","Scheduled close has been cancelled.","File: cogs\modmail.py/L378" +"This thread has not already been scheduled to close.","This thread has not already been scheduled to close.","File: cogs\modmail.py/L383" +"{mention} is already going to be mentioned.","{mention} is already going to be mentioned.","File: cogs\modmail.py/L433" +"{mention} will be mentioned on the next message received.","{mention} will be mentioned on the next message received.","File: cogs\modmail.py/L440" +"{mention} does not have a pending notification.","{mention} does not have a pending notification.","File: cogs\modmail.py/L472" +"{mention} will no longer be notified.","{mention} will no longer be notified.","File: cogs\modmail.py/L479" +"{mention} is already subscribed to this thread.","{mention} is already subscribed to this thread.","File: cogs\modmail.py/L512" +"{mention} will now be notified of all messages received.","{mention} will now be notified of all messages received.","File: cogs\modmail.py/L519" +"{mention} is not already subscribed to this thread.","{mention} is not already subscribed to this thread.","File: cogs\modmail.py/L551" +"{mention} is now unsubscribed to this thread.","{mention} is now unsubscribed to this thread.","File: cogs\modmail.py/L558" +"Unknown","Unknown","File: cogs\modmail.py/L610" +"Closed By","Closed By","File: cogs\modmail.py/L613" +"Created by","Created by","File: cogs\modmail.py/L616" +"Preview","Preview","File: cogs\modmail.py/L619" +"Link","Link","File: cogs\modmail.py/L624" +"Log Key","Log Key","File: cogs\modmail.py/L627" +"Recipient ID","Recipient ID","File: cogs\modmail.py/L629" +"This user does not have any previous logs.","This user does not have any previous logs.","File: cogs\modmail.py/L660" +"No log entries have been found for that query","No log entries have been found for that query","File: cogs\modmail.py/L693" +"Log entry `{key}` not found.","Log entry `{key}` not found.","File: cogs\modmail.py/L713" +"Log entry `{key}` successfully deleted.","Log entry `{key}` successfully deleted.","File: cogs\modmail.py/L719" +"{mention} has not responded to any threads.","{mention} has not responded to any threads.","File: cogs\modmail.py/L743" +"No log entries have been found for that query.","No log entries have been found for that query.","File: cogs\modmail.py/L776" +"Failed","Failed","File: cogs\modmail.py/L869/L1177" +"Cannot find a message to edit.","Cannot find a message to edit.","File: cogs\modmail.py/L870" +"Cannot start a thread with a bot.","Cannot start a thread with a bot.","File: cogs\modmail.py/L900" +"A thread for this user already exists in {mention}.","A thread for this user already exists in {mention}.","File: cogs\modmail.py/L908" +"Created Thread","Created Thread","File: cogs\modmail.py/L919" +"Thread started by {author_mention} for {user_mention}.","Thread started by {author_mention} for {user_mention}.","File: cogs\modmail.py/L920" +"Blocked Users","Blocked Users","File: cogs\modmail.py/L939/L960" +"No Reason Provided","No Reason Provided","File: cogs\modmail.py/L960" +"(Continued)","(Continued)","File: cogs\modmail.py/L963 cogs\utility.py/L67" +"Currently there are no blocked users.","Currently there are no blocked users.","File: cogs\modmail.py/L971" +"Success","Success","File: cogs\modmail.py/L997/L1084/L1091/L1308/L1344/L1357/L1377/L1358 cogs\utility.py/L725/L759/L1231/L1291/L1367/L1392/L1559/L1595" +"{mention} is no longer whitelisted.","{mention} is no longer whitelisted.","File: cogs\modmail.py/L998" +"{mention} was previously blocked internally for ","{mention} was previously blocked internally for ","File: cogs\modmail.py/L1018" +"Cannot block {mention}, user is whitelisted.","Cannot block {mention}, user is whitelisted.","File: cogs\modmail.py/L1060" +"{mention} was previously blocked {old_reason}.\n{mention} is now blocked {reason}","{mention} was previously blocked {old_reason}.\n{mention} is now blocked {reason}","File: cogs\modmail.py/L1085" +"{mention} is now blocked {reason}","{mention} is now blocked {reason}","File: cogs\modmail.py/L1093" +"{mention} was previously blocked internally {reason}.\n{mention} is no longer blocked.","{mention} was previously blocked internally {reason}.\n{mention} is no longer blocked.","File: cogs\modmail.py/L1132" +"However, if the original system block reason still applies, {name} will be automatically blocked again. Use ","However, if the original system block reason still applies, {name} will be automatically blocked again. Use ","File: cogs\modmail.py/L1137" +"{mention} is no longer blocked.","{mention} is no longer blocked.","File: cogs\modmail.py/L1147" +"{mention} is not blocked.","{mention} is not blocked.","File: cogs\modmail.py/L1152" +"Cannot find a message to delete.","Cannot find a message to delete.","File: cogs\modmail.py/L1178" +"Modmail will now accept **all** DM messages.","Modmail will now accept **all** DM messages.","File: cogs\modmail.py/L1309" +"Modmail is already accepting all DM messages.","Modmail is already accepting all DM messages.","File: cogs\modmail.py/L1316" +"Modmail will not create any **new** threads.","Modmail will not create any **new** threads.","File: cogs\modmail.py/L1345" +"Modmail is already not creating any new threads.","Modmail is already not creating any new threads.","File: cogs\modmail.py/L1352" +"Modmail will not create **new** threads, but existing threads will now be functioning.","Modmail will not create **new** threads, but existing threads will now be functioning.","File: cogs\modmail.py/L1358" +"Modmail will not accept **any** DM messages.","Modmail will not accept **any** DM messages.","File: cogs\modmail.py/L1378" +"Modmail is already not accepting any DM messages.","Modmail is already not accepting any DM messages.","File: cogs\modmail.py/L1385" +"New Threads Disabled","New Threads Disabled","File: cogs\modmail.py/L1400" +"Modmail is not creating new threads.","Modmail is not creating new threads.","File: cogs\modmail.py/L1401" +"All DM Disabled","All DM Disabled","File: cogs\modmail.py/L1406" +"Modmail is not accepting any DM messages for new and existing threads.","Modmail is not accepting any DM messages for new and existing threads.","File: cogs\modmail.py/L1407" +"Enabled","Enabled","File: cogs\modmail.py/L1412" +"Modmail is accepting all DM messages.","Modmail is accepting all DM messages.","File: cogs\modmail.py/L1413" +"Plugins are still loading, please try again later.","Plugins are still loading, please try again later.","File: cogs\plugins.py/L230/L470" +"Your bot's version is too low. This plugin requires version `{required_version}`.","Your bot's version is too low. This plugin requires version `{required_version}`.","File: cogs\plugins.py/L246" +"Invalid plugin name, double check the plugin name or use one of the following formats: username/repo/plugin, username/repo/plugin@branch.","Invalid plugin name, double check the plugin name or use one of the following formats: username/repo/plugin, username/repo/plugin@branch.","File: cogs\plugins.py/L260" +"This plugin is already installed.","This plugin is already installed.","File: cogs\plugins.py/L295" +"Cannot install this plugin (dupe cog name).","Cannot install this plugin (dupe cog name).","File: cogs\plugins.py/L303" +"Starting to download plugin from {plugin_link}...","Starting to download plugin from {plugin_link}...","File: cogs\plugins.py/L309" +"Failed to download plugin, check logs for error.","Failed to download plugin, check logs for error.","File: cogs\plugins.py/L320" +"Successfully installed plugin.\n*Friendly reminder, plugins have absolute control over your bot. Please only install plugins from developers you trust.*","Successfully installed plugin.\n*Friendly reminder, plugins have absolute control over your bot. Please only install plugins from developers you trust.*","File: cogs\plugins.py/L345" +"This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.","This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.","File: cogs\plugins.py/L355" +"Plugin is not installed.","Plugin is not installed.","File: cogs\plugins.py/L376/L415" +"The plugin is successfully uninstalled.","The plugin is successfully uninstalled.","File: cogs\plugins.py/L402" +"Successfully updated {plugin_name}.","Successfully updated {plugin_name}.","File: cogs\plugins.py/L429" +"No plugins are loaded due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.","No plugins are loaded due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.","File: cogs\plugins.py/L462" +"There are no plugins currently loaded.","There are no plugins currently loaded.","File: cogs\plugins.py/L477" +"Loaded plugins:","Loaded plugins:","File: cogs\plugins.py/L498" +"Could not find a plugin with name ""{plugin_name}"" within the registry.","Could not find a plugin with name ""{plugin_name}"" within the registry.","File: cogs\plugins.py/L534" +"Perhaps you meant:","Perhaps you meant:","File: cogs\plugins.py/L541 cogs\utility.py/L211" +"Installation","Installation","File: cogs\plugins.py/L562" +"Your bot is unable to install this plugin, minimum required version is v{required_version}.","Your bot is unable to install this plugin, minimum required version is v{required_version}.","File: cogs\plugins.py/L582" +"Your bot is able to install this plugin.","Your bot is able to install this plugin.","File: cogs\plugins.py/L586" +"Plugin Registry","Plugin Registry","File: cogs\plugins.py/L641" +" + if not format_.strip(): + continue + if len(format_) + len(formats[-1]) >= 1024: + formats.append(format_) + else: + formats[-1] += format_ + + embeds = [] + for format_ in formats: + description = ( + _(cog.description) or _()"," + if not format_.strip(): + continue + if len(format_) + len(formats[-1]) >= 1024: + formats.append(format_) + else: + formats[-1] += format_ + + embeds = [] + for format_ in formats: + description = ( + _(cog.description) or _()","File: cogs\utility.py/L50" +"Miscellaneous commands without a category.","Miscellaneous commands without a category.","File: cogs\utility.py/L63" +"Commands","Commands","File: cogs\utility.py/L67" +"No commands.","No commands.","File: cogs\utility.py/L67" +"Help","Help","File: cogs\utility.py/L71" +"Miscellaneous Commands","Miscellaneous Commands","File: cogs\utility.py/L73" +"Type ""{prefix}{command} command"" ","Type ""{prefix}{command} command"" ","File: cogs\utility.py/L78/L153" +"NONE","NONE","File: cogs\utility.py/L119" +"","","File: cogs\utility.py/L124" +"Permission Level","Permission Level","File: cogs\utility.py/L139" +" + + embed.add_field(name=_()"," + + embed.add_field(name=_()","File: cogs\utility.py/L153" +"{command} is a snippet.","{command} is a snippet.","File: cogs\utility.py/L168" +"Alias `{command}` is invalid, this alias will now be deleted.This alias will now be deleted.","Alias `{command}` is invalid, this alias will now be deleted.This alias will now be deleted.","File: cogs\utility.py/L181" +"{command} is an alias.","{command} is an alias.","File: cogs\utility.py/L190/L192" +"`{command}` points to:","`{command}` points to:","File: cogs\utility.py/L192" +"**`{command}` points to the following steps:**","**`{command}` points to the following steps:**","File: cogs\utility.py/L197" +"Step","Step","File: cogs\utility.py/L200/L1045" +"Type ""{prefix}{command} alias"" ","Type ""{prefix}{command} alias"" ","File: cogs\utility.py/L203" +"Command/Category ""{command}"" not found.","Command/Category ""{command}"" not found.","File: cogs\utility.py/L211" +"Cannot find command or category","Cannot find command or category","File: cogs\utility.py/L225" +"Type ""{prefix}{command}"" ","Type ""{prefix}{command}"" ","File: cogs\utility.py/L227" +"Shows this help message.","Shows this help message.","File: cogs\utility.py/L242" +"The specified version `{version}` could not be found.","The specified version `{version}` could not be found.","File: cogs\utility.py/L266" +"View the changelog here: {url}","View the changelog here: {url}","File: cogs\utility.py/L282" +"Modmail - About","Modmail - About","File: cogs\utility.py/L292" +"This is an open source Discord bot that serves as a means for members to easily communicate with server administrators in an organised manner.","This is an open source Discord bot that serves as a means for members to easily communicate with server administrators in an organised manner.","File: cogs\utility.py/L298" +"Uptime","Uptime","File: cogs\utility.py/L303" +"Latency","Latency","File: cogs\utility.py/L304" +"Version","Version","File: cogs\utility.py/L305" +"Authors","Authors","File: cogs\utility.py/L306" +"You are on the prerelease version • the latest version is v{version}.","You are on the prerelease version • the latest version is v{version}.","File: cogs\utility.py/L315" +"A newer version is available v{version}.","A newer version is available v{version}.","File: cogs\utility.py/L317" +"You are up to date with the latest version.","You are up to date with the latest version.","File: cogs\utility.py/L319" +"Want Modmail in Your Server?","Want Modmail in Your Server?","File: cogs\utility.py/L322" +"Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) and join our [Discord server](https://discord.gg/F34cRU8/)!","Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) and join our [Discord server](https://discord.gg/F34cRU8/)!","File: cogs\utility.py/L323" +"Support the Developers","Support the Developers","File: cogs\utility.py/L329" +"This bot is completely free for everyone. We rely on kind individuals like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) to keep this bot free forever!","This bot is completely free for everyone. We rely on kind individuals like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) to keep this bot free forever!","File: cogs\utility.py/L330" +"Debug Logs:","Debug Logs:","File: cogs\utility.py/L379" +"You don't have any logs at the moment.","You don't have any logs at the moment.","File: cogs\utility.py/L380" +"Go to Heroku to see your logs.","Go to Heroku to see your logs.","File: cogs\utility.py/L382/L450" +"Debug logs - Navigate using the reactions below.","Debug logs - Navigate using the reactions below.","File: cogs\utility.py/L408" +"Debug Logs","Debug Logs","File: cogs\utility.py/L440" +"Something's wrong. We're unable to upload your logs to hastebin.","Something's wrong. We're unable to upload your logs to hastebin.","File: cogs\utility.py/L448" +"Cached logs are now cleared.","Cached logs are now cleared.","File: cogs\utility.py/L470" +"Activity Removed","Activity Removed","File: cogs\utility.py/L501" +"Activity set to: {name} ","Activity set to: {name} ","File: cogs\utility.py/L520" +"to {name}.","to {name}.","File: cogs\utility.py/L522" +"Activity Changed","Activity Changed","File: cogs\utility.py/L527" +"Status Removed","Status Removed","File: cogs\utility.py/L550" +"Status set to: {value}.","Status set to: {value}.","File: cogs\utility.py/L564" +"Status Changed","Status Changed","File: cogs\utility.py/L566" +"Pong! Websocket Latency:","Pong! Websocket Latency:","File: cogs\utility.py/L638" +"Current mention:","Current mention:","File: cogs\utility.py/L657" +"Changed mention!","Changed mention!","File: cogs\utility.py/L663" +"On thread creation the bot now says ""{mention}"".","On thread creation the bot now says ""{mention}"".","File: cogs\utility.py/L664" +"Current prefix","Current prefix","File: cogs\utility.py/L683" +"Changed prefix!","Changed prefix!","File: cogs\utility.py/L689" +"Set prefix to `{prefix}`","Set prefix to `{prefix}`","File: cogs\utility.py/L690" +"Available configuration keys:","Available configuration keys:","File: cogs\utility.py/L725" +"Set `{key}` to `{value}`.","Set `{key}` to `{value}`.","File: cogs\utility.py/L748" +"{key} is an invalid key.","{key} is an invalid key.","File: cogs\utility.py/L756/L780" +"Valid keys","Valid keys","File: cogs\utility.py/L759/L783" +"`{key}` had been reset to default.","`{key}` had been reset to default.","File: cogs\utility.py/L774" +"`{key}` is set to `{value}`","`{key}` is set to `{value}`","File: cogs\utility.py/L799" +"Config variable","Config variable","File: cogs\utility.py/L802" +"`{key}` is an invalid key.","`{key}` is an invalid key.","File: cogs\utility.py/L809/L822" +"Type ""{prefix}config options"" for a list of config variables.","Type ""{prefix}config options"" for a list of config variables.","File: cogs\utility.py/L812" +"Here is a list of currently set configuration variable(s).","Here is a list of currently set configuration variable(s).","File: cogs\utility.py/L818" +"Current config(s):","Current config(s):","File: cogs\utility.py/L822" +"No help details found for `{key}`.","No help details found for `{key}`.","File: cogs\utility.py/L862" +"Configuration description on {current_key}:","Configuration description on {current_key}:","File: cogs\utility.py/L875" +"Information:","Information:","File: cogs\utility.py/L880" +"Example(s):","Example(s):","File: cogs\utility.py/L886" +"Note(s):","Note(s):","File: cogs\utility.py/L892" +"Alias `{name}` is invalid, it used to be `{value}`. This alias will now be deleted.","Alias `{name}` is invalid, it used to be `{value}`. This alias will now be deleted.","File: cogs\utility.py/L939" +"Alias","Alias","File: cogs\utility.py/L949" +"You dont have any aliases at the moment.","You dont have any aliases at the moment.","File: cogs\utility.py/L968" +"Do {prefix}help alias for more commands.","Do {prefix}help alias for more commands.","File: cogs\utility.py/L970" +"Aliases","Aliases","File: cogs\utility.py/L971" +"Command Aliases","Command Aliases","File: cogs\utility.py/L979" +"Invalid multi-step alias, try wrapping each steps in quotes.","Invalid multi-step alias, try wrapping each steps in quotes.","File: cogs\utility.py/L1009" +"{action} alias","{action} alias","File: cogs\utility.py/L1024" +"`{name}` points to:","`{name}` points to:","File: cogs\utility.py/L1027" +"`{name}` now points to the following steps:","`{name}` now points to the following steps:","File: cogs\utility.py/L1029" +"The command you are attempting to point to does not exist: `{command}`.","The command you are attempting to point to does not exist: `{command}`.","File: cogs\utility.py/L1045" +"The command you are attempting to point to on step {number} does not exist: `{command}`.","The command you are attempting to point to on step {number} does not exist: `{command}`.","File: cogs\utility.py/L1045" +"A command with the same name already exists: `{name}`.","A command with the same name already exists: `{name}`.","File: cogs\utility.py/L1085" +"Another alias with the same name already exists: `{name}`.","Another alias with the same name already exists: `{name}`.","File: cogs\utility.py/L1092" +"A snippet with the same name already exists: `{name}`.","A snippet with the same name already exists: `{name}`.","File: cogs\utility.py/L1099" +"Alias names cannot be longer than 120 characters.","Alias names cannot be longer than 120 characters.","File: cogs\utility.py/L1106" +"Removed alias","Removed alias","File: cogs\utility.py/L1123" +"Successfully deleted `{name}`.","Successfully deleted `{name}`.","File: cogs\utility.py/L1125" +"The referenced level does not exist: `{level}`.","The referenced level does not exist: `{level}`.","File: cogs\utility.py/L1231" +"Successfully set command permission level for `{command}` to `{level}`.","Successfully set command permission level for `{command}` to `{level}`.","File: cogs\utility.py/L1245" +"The referenced {type} does not exist: `{name}`.","The referenced {type} does not exist: `{name}`.","File: cogs\utility.py/L1291" +"Permission for `{name}` was successfully updated.","Permission for `{name}` was successfully updated.","File: cogs\utility.py/L1316/L1419" +"The command permission level was never overridden: `{name}`, current permission level is {perm_name}.","The command permission level was never overridden: `{name}`, current permission level is {perm_name}.","File: cogs\utility.py/L1367/L1559" +"Command permission level for `{name}` was successfully restored to {perm_name}.","Command permission level for `{name}` was successfully restored to {perm_name}.","File: cogs\utility.py/L1378" +"The referenced level does not exist: `{name}`.","The referenced level does not exist: `{name}`.","File: cogs\utility.py/L1392" +"Permission entries for {type} `{name}`:","Permission entries for {type} `{name}`:","File: cogs\utility.py/L1430/L1455" +"No permission entries found.","No permission entries found.","File: cogs\utility.py/L1431/L1523" +"{mention} has permission with the following commands:","{mention} has permission with the following commands:","File: cogs\utility.py/L1528" +"{mention} has permission with the following permission levels:","{mention} has permission with the following permission levels:","File: cogs\utility.py/L1533" +"Permission Overrides","Permission Overrides","File: cogs\utility.py/L1558/L1559" +"You don't have any command level overrides at the moment.","You don't have any command level overrides at the moment.","File: cogs\utility.py/L1559" +"Permission override for command ""{name}"" is ""{perm_name}"".","Permission override for command ""{name}"" is ""{perm_name}"".","File: cogs\utility.py/L1595" +"Un-whitelisted {target_mention} to view logs.","Un-whitelisted {target_mention} to view logs.","File: cogs\utility.py/L1678" +"Whitelisted {target_mention} to view logs.","Whitelisted {target_mention} to view logs.","File: cogs\utility.py/L1682" +"Oauth Whitelist","Oauth Whitelist","File: cogs\utility.py/L1705" +"Users","Users","File: cogs\utility.py/L1708" +"None","None","File: cogs\utility.py/L1708" +"Channel has been deleted, no closer found.","Channel has been deleted, no closer found.","File: bot.py/L472" +"System Message: New Account. Required to wait for {time}.","System Message: New Account. Required to wait for {time}.","File: bot.py/L552" +"Commands directly related to Modmail functionality.","Commands directly related to Modmail functionality.","Cog: Modmail" +"Sets up a server for Modmail.","Sets up a server for Modmail.","Cog: Modmail +Command: setup" +"Sets up a server for Modmail. + +You only need to run this command +once after configuring Modmail.","Sets up a server for Modmail. + +You only need to run this command +once after configuring Modmail.","Cog: Modmail +Command: setup" +"Create pre-defined messages for use in threads.","Create pre-defined messages for use in threads.","Cog: Modmail +Command: snippet" +"Create pre-defined messages for use in threads. + +When `{prefix}snippet` is used by itself, this will retrieve +a list of snippets that are currently set. `{prefix}snippet-name` will show what the +snippet point to. + +To create a snippet: +- `{prefix}snippet add snippet-name A pre-defined text.` + +You can use your snippet in a thread channel +with `{prefix}snippet-name`, the message ""A pre-defined text."" +will be sent to the recipient. + +Currently, there is not a built-in anonymous snippet command; however, a workaround +is available using `{prefix}alias`. Here is how: +- `{prefix}alias add snippet-name anonreply A pre-defined anonymous text.` + +See also `{prefix}alias`.","Create pre-defined messages for use in threads. + +When `{prefix}snippet` is used by itself, this will retrieve +a list of snippets that are currently set. `{prefix}snippet-name` will show what the +snippet point to. + +To create a snippet: +- `{prefix}snippet add snippet-name A pre-defined text.` + +You can use your snippet in a thread channel +with `{prefix}snippet-name`, the message ""A pre-defined text."" +will be sent to the recipient. + +Currently, there is not a built-in anonymous snippet command; however, a workaround +is available using `{prefix}alias`. Here is how: +- `{prefix}alias add snippet-name anonreply A pre-defined anonymous text.` + +See also `{prefix}alias`.","Cog: Modmail +Command: snippet" +"View the raw content of a snippet.","View the raw content of a snippet.","Cog: Modmail +Command: snippet raw" +"View the raw content of a snippet.","View the raw content of a snippet.","Cog: Modmail +Command: snippet raw" +"Add a snippet.","Add a snippet.","Cog: Modmail +Command: snippet add" +"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. +```","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. +```","Cog: Modmail +Command: snippet add" +"Remove a snippet.","Remove a snippet.","Cog: Modmail +Command: snippet remove" +"Remove a snippet.","Remove a snippet.","Cog: Modmail +Command: snippet remove" +"Edit a snippet.","Edit a snippet.","Cog: Modmail +Command: snippet edit" +"Edit a snippet. + +To edit a multi-word snippet name, use quotes: ``` +{prefix}snippet edit ""two word"" this is a new two word snippet. +```","Edit a snippet. + +To edit a multi-word snippet name, use quotes: ``` +{prefix}snippet edit ""two word"" this is a new two word snippet. +```","Cog: Modmail +Command: snippet edit" +"Move a thread to another category.","Move a thread to another category.","Cog: Modmail +Command: move" +"Move a thread to another category. + +`category` may be a category ID, mention, or name. +`specifics` is a string which takes in arguments on how to perform the move. Ex: ""silently""","Move a thread to another category. + +`category` may be a category ID, mention, or name. +`specifics` is a string which takes in arguments on how to perform the move. Ex: ""silently""","Cog: Modmail +Command: move" +"Close the current thread.","Close the current thread.","Cog: Modmail +Command: close" +"Close the current thread. + +Close after a period of time: +- `{prefix}close in 5 hours` +- `{prefix}close 2m30s` + +Custom close messages: +- `{prefix}close 2 hours The issue has been resolved.` +- `{prefix}close We will contact you once we find out more.` + +Silently close a thread (no message) +- `{prefix}close silently` +- `{prefix}close in 10m silently` + +Stop a thread from closing: +- `{prefix}close cancel`","Close the current thread. + +Close after a period of time: +- `{prefix}close in 5 hours` +- `{prefix}close 2m30s` + +Custom close messages: +- `{prefix}close 2 hours The issue has been resolved.` +- `{prefix}close We will contact you once we find out more.` + +Silently close a thread (no message) +- `{prefix}close silently` +- `{prefix}close in 10m silently` + +Stop a thread from closing: +- `{prefix}close cancel`","Cog: Modmail +Command: close" +"Notify a user or role when the next thread message received.","Notify a user or role when the next thread message received.","Cog: Modmail +Command: notify" +"Notify a user or role when the next thread message received. + +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`. +`user_or_role` may be a user ID, mention, name. role ID, mention, name, ""everyone"", or ""here"".","Notify a user or role when the next thread message received. + +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`. +`user_or_role` may be a user ID, mention, name. role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail +Command: notify" +"Un-notify a user, role, or yourself from a thread.","Un-notify a user, role, or yourself from a thread.","Cog: Modmail +Command: unnotify" +"Un-notify a user, role, or yourself from a thread. + +Leave `user_or_role` empty to un-notify yourself. +`@here` and `@everyone` can be substituted with `here` and `everyone`. +`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Un-notify a user, role, or yourself from a thread. + +Leave `user_or_role` empty to un-notify yourself. +`@here` and `@everyone` can be substituted with `here` and `everyone`. +`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail +Command: unnotify" +"Notify a user, role, or yourself for every thread message received.","Notify a user, role, or yourself for every thread message received.","Cog: Modmail +Command: subscribe" +"Notify a user, role, or yourself for every thread message received. + +You will be pinged for every thread message received until you unsubscribe. + +Leave `user_or_role` empty to subscribe yourself. +`@here` and `@everyone` can be substituted with `here` and `everyone`. +`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Notify a user, role, or yourself for every thread message received. + +You will be pinged for every thread message received until you unsubscribe. + +Leave `user_or_role` empty to subscribe yourself. +`@here` and `@everyone` can be substituted with `here` and `everyone`. +`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail +Command: subscribe" +"Unsubscribe a user, role, or yourself from a thread.","Unsubscribe a user, role, or yourself from a thread.","Cog: Modmail +Command: unsubscribe" +"Unsubscribe a user, role, or yourself from a thread. + +Leave `user_or_role` empty to unsubscribe yourself. +`@here` and `@everyone` can be substituted with `here` and `everyone`. +`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Unsubscribe a user, role, or yourself from a thread. + +Leave `user_or_role` empty to unsubscribe yourself. +`@here` and `@everyone` can be substituted with `here` and `everyone`. +`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail +Command: unsubscribe" +"Flags a Modmail thread as NSFW (not safe for work).","Flags a Modmail thread as NSFW (not safe for work).","Cog: Modmail +Command: nsfw" +"Flags a Modmail thread as NSFW (not safe for work).","Flags a Modmail thread as NSFW (not safe for work).","Cog: Modmail +Command: nsfw" +"Flags a Modmail thread as SFW (safe for work).","Flags a Modmail thread as SFW (safe for work).","Cog: Modmail +Command: sfw" +"Flags a Modmail thread as SFW (safe for work).","Flags a Modmail thread as SFW (safe for work).","Cog: Modmail +Command: sfw" +"Retrieves the link to the current thread's logs.","Retrieves the link to the current thread's logs.","Cog: Modmail +Command: loglink" +"Retrieves the link to the current thread's logs.","Retrieves the link to the current thread's logs.","Cog: Modmail +Command: loglink" +"Get previous Modmail thread logs of a member.","Get previous Modmail thread logs of a member.","Cog: Modmail +Command: logs" +"Get previous Modmail thread logs of a member. + +Leave `user` blank when this command is used within a +thread channel to show logs for the current recipient. +`user` may be a user ID, mention, or name.","Get previous Modmail thread logs of a member. + +Leave `user` blank when this command is used within a +thread channel to show logs for the current recipient. +`user` may be a user ID, mention, or name.","Cog: Modmail +Command: logs" +"Get all logs closed by the specified user.","Get all logs closed by the specified user.","Cog: Modmail +Command: logs closed-by" +"Get all logs closed by the specified user. + +If no `user` is provided, the user will be the person who sent this command. +`user` may be a user ID, mention, or name.","Get all logs closed by the specified user. + +If no `user` is provided, the user will be the person who sent this command. +`user` may be a user ID, mention, or name.","Cog: Modmail +Command: logs closed-by" +"Wipe a log entry from the database.","Wipe a log entry from the database.","Cog: Modmail +Command: logs delete" +"Wipe a log entry from the database.","Wipe a log entry from the database.","Cog: Modmail +Command: logs delete" +"Get all logs where the specified user has responded at least once.","Get all logs where the specified user has responded at least once.","Cog: Modmail +Command: logs responded" +"Get all logs where the specified user has responded at least once. + +If no `user` is provided, the user will be the person who sent this command. +`user` may be a user ID, mention, or name.","Get all logs where the specified user has responded at least once. + +If no `user` is provided, the user will be the person who sent this command. +`user` may be a user ID, mention, or name.","Cog: Modmail +Command: logs responded" +"Retrieve all logs that contain messages with your query.","Retrieve all logs that contain messages with your query.","Cog: Modmail +Command: logs search" +"Retrieve all logs that contain messages with your query. + +Provide a `limit` to specify the maximum number of logs the bot should find.","Retrieve all logs that contain messages with your query. + +Provide a `limit` to specify the maximum number of logs the bot should find.","Cog: Modmail +Command: logs search" +"Reply to a Modmail thread.","Reply to a Modmail thread.","Cog: Modmail +Command: reply" +"Reply to a Modmail thread. + +Supports attachments and images as well as +automatically embedding image URLs.","Reply to a Modmail thread. + +Supports attachments and images as well as +automatically embedding image URLs.","Cog: Modmail +Command: reply" +"Reply to a Modmail thread with variables.","Reply to a Modmail thread with variables.","Cog: Modmail +Command: freply" +"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.","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.","Cog: Modmail +Command: freply" +"Reply to a thread anonymously.","Reply to a thread anonymously.","Cog: Modmail +Command: areply" +"Reply to a thread anonymously. + +You can edit the anonymous user's name, +avatar and tag using the config command. + +Edit the `anon_username`, `anon_avatar_url` +and `anon_tag` config variables to do so.","Reply to a thread anonymously. + +You can edit the anonymous user's name, +avatar and tag using the config command. + +Edit the `anon_username`, `anon_avatar_url` +and `anon_tag` config variables to do so.","Cog: Modmail +Command: areply" +"Take a note about the current thread.","Take a note about the current thread.","Cog: Modmail +Command: note" +"Take a note about the current thread. + +Useful for noting context.","Take a note about the current thread. + +Useful for noting context.","Cog: Modmail +Command: note" +"Edit a message that was sent using the reply or anonreply command.","Edit a message that was sent using the reply or anonreply command.","Cog: Modmail +Command: edit" +"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. + +Note: attachments **cannot** be edited.","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. + +Note: attachments **cannot** be edited.","Cog: Modmail +Command: edit" +"Create a thread with a specified member.","Create a thread with a specified member.","Cog: Modmail +Command: contact" +"Create a thread with a specified member. + +If `category` is specified, the thread +will be created in that specified category. + +`category`, if specified, may be a category ID, mention, or name. +`user` may be a user ID, mention, or name.","Create a thread with a specified member. + +If `category` is specified, the thread +will be created in that specified category. + +`category`, if specified, may be a category ID, mention, or name. +`user` may be a user ID, mention, or name.","Cog: Modmail +Command: contact" +"Retrieve a list of blocked users.","Retrieve a list of blocked users.","Cog: Modmail +Command: blocked" +"Retrieve a list of blocked users.","Retrieve a list of blocked users.","Cog: Modmail +Command: blocked" +"Whitelist or un-whitelist a user from getting blocked.","Whitelist or un-whitelist a user from getting blocked.","Cog: Modmail +Command: blocked whitelist" +"Whitelist or un-whitelist a user from getting blocked. + +Useful for preventing users from getting blocked by account_age/guild_age restrictions.","Whitelist or un-whitelist a user from getting blocked. + +Useful for preventing users from getting blocked by account_age/guild_age restrictions.","Cog: Modmail +Command: blocked whitelist" +"Block a user from using Modmail.","Block a user from using Modmail.","Cog: Modmail +Command: block" +"Block a user from using Modmail. + +You may choose to set a time as to when the user will automatically be unblocked. + +Leave `user` blank when this command is used within a +thread channel to block the current recipient. +`user` may be a user ID, mention, or name. +`duration` may be a simple ""human-readable"" time text. See `{prefix}help close` for examples.","Block a user from using Modmail. + +You may choose to set a time as to when the user will automatically be unblocked. + +Leave `user` blank when this command is used within a +thread channel to block the current recipient. +`user` may be a user ID, mention, or name. +`duration` may be a simple ""human-readable"" time text. See `{prefix}help close` for examples.","Cog: Modmail +Command: block" +"Unblock a user from using Modmail.","Unblock a user from using Modmail.","Cog: Modmail +Command: unblock" +"Unblock a user from using Modmail. + +Leave `user` blank when this command is used within a +thread channel to unblock the current recipient. +`user` may be a user ID, mention, or name.","Unblock a user from using Modmail. + +Leave `user` blank when this command is used within a +thread channel to unblock the current recipient. +`user` may be a user ID, mention, or name.","Cog: Modmail +Command: unblock" +"Delete a message that was sent using the reply command or a note.","Delete a message that was sent using the reply command or a note.","Cog: Modmail +Command: delete" +"Delete a message that was sent using the reply command or a note. + +Deletes the previous message, unless a message ID is provided, +which in that case, deletes the message with that message ID. + +Notes can only be deleted when a note ID is provided.","Delete a message that was sent using the reply command or a note. + +Deletes the previous message, unless a message ID is provided, +which in that case, deletes the message with that message ID. + +Notes can only be deleted when a note ID is provided.","Cog: Modmail +Command: delete" +"Repair a thread broken by Discord.","Repair a thread broken by Discord.","Cog: Modmail +Command: repair" +"Repair a thread broken by Discord.","Repair a thread broken by Discord.","Cog: Modmail +Command: repair" +"Re-enables DM functionalities of Modmail.","Re-enables DM functionalities of Modmail.","Cog: Modmail +Command: enable" +"Re-enables DM functionalities of Modmail. + +Undo's the `{prefix}disable` command, all DM will be relayed after running this command.","Re-enables DM functionalities of Modmail. + +Undo's the `{prefix}disable` command, all DM will be relayed after running this command.","Cog: Modmail +Command: enable" +"Disable partial or full Modmail thread functions.","Disable partial or full Modmail thread functions.","Cog: Modmail +Command: disable" +"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`.","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`.","Cog: Modmail +Command: disable" +"Stop accepting new Modmail threads.","Stop accepting new Modmail threads.","Cog: Modmail +Command: disable new" +"Stop accepting new Modmail threads. + +No new threads can be created through DM.","Stop accepting new Modmail threads. + +No new threads can be created through DM.","Cog: Modmail +Command: disable new" +"Disables all DM functionalities of Modmail.","Disables all DM functionalities of Modmail.","Cog: Modmail +Command: disable all" +"Disables all DM functionalities of Modmail. + +No new threads can be created through DM nor no further DM messages will be relayed.","Disables all DM functionalities of Modmail. + +No new threads can be created through DM nor no further DM messages will be relayed.","Cog: Modmail +Command: disable all" +"Check if the DM functionalities of Modmail is enabled.","Check if the DM functionalities of Modmail is enabled.","Cog: Modmail +Command: isenable" +"Check if the DM functionalities of Modmail is enabled.","Check if the DM functionalities of Modmail is enabled.","Cog: Modmail +Command: isenable" +"Plugins expand Modmail functionality by allowing third-party addons. + +These addons could have a range of features from moderation to simply +making your life as a moderator easier! +Learn how to create a plugin yourself here: +https://github.com/kyb3r/modmail/wiki/Plugins","Plugins expand Modmail functionality by allowing third-party addons. + +These addons could have a range of features from moderation to simply +making your life as a moderator easier! +Learn how to create a plugin yourself here: +https://github.com/kyb3r/modmail/wiki/Plugins","Cog: Plugins" +"Manage plugins for Modmail.","Manage plugins for Modmail.","Cog: Plugins +Command: plugins" +"Manage plugins for Modmail.","Manage plugins for Modmail.","Cog: Plugins +Command: plugins" +"Install a new plugin for the bot.","Install a new plugin for the bot.","Cog: Plugins +Command: plugins add" +"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]`).","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]`).","Cog: Plugins +Command: plugins add" +"Remove an installed plugin of the bot.","Remove an installed plugin of the bot.","Cog: Plugins +Command: plugins remove" +"Remove an installed plugin of 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]`).","Remove an installed plugin of 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]`).","Cog: Plugins +Command: plugins remove" +"Update a plugin for the bot.","Update a plugin for the bot.","Cog: Plugins +Command: plugins update" +"Update a plugin for the bot. + +`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference +to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + +To update all plugins, do `{prefix}plugins update`.","Update a plugin for the bot. + +`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference +to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + +To update all plugins, do `{prefix}plugins update`.","Cog: Plugins +Command: plugins update" +"Show a list of currently loaded plugins.","Show a list of currently loaded plugins.","Cog: Plugins +Command: plugins loaded" +"Show a list of currently loaded plugins.","Show a list of currently loaded plugins.","Cog: Plugins +Command: plugins loaded" +"Shows a list of all approved plugins.","Shows a list of all approved plugins.","Cog: Plugins +Command: plugins registry" +"Shows a list of all approved plugins. + +Usage: +`{prefix}plugin registry` Details about all plugins. +`{prefix}plugin registry plugin-name` Details about the indicated plugin. +`{prefix}plugin registry page-number` Jump to a page in the registry.","Shows a list of all approved plugins. + +Usage: +`{prefix}plugin registry` Details about all plugins. +`{prefix}plugin registry plugin-name` Details about the indicated plugin. +`{prefix}plugin registry page-number` Jump to a page in the registry.","Cog: Plugins +Command: plugins registry" +"Shows a compact view of all plugins within the registry.","Shows a compact view of all plugins within the registry.","Cog: Plugins +Command: plugins registry compact" +"Shows a compact view of all plugins within the registry.","Shows a compact view of all plugins within the registry.","Cog: Plugins +Command: plugins registry compact" +"General commands that provide utility.","General commands that provide utility.","Cog: Utility" +"Shows the changelog of the Modmail.","Shows the changelog of the Modmail.","Cog: Utility +Command: changelog" +"Shows the changelog of the Modmail.","Shows the changelog of the Modmail.","Cog: Utility +Command: changelog" +"Shows information about this bot.","Shows information about this bot.","Cog: Utility +Command: about" +"Shows information about this bot.","Shows information about this bot.","Cog: Utility +Command: about" +"Shows a list of sponsors.","Shows a list of sponsors.","Cog: Utility +Command: sponsors" +"Shows a list of sponsors.","Shows a list of sponsors.","Cog: Utility +Command: sponsors" +"Shows the recent application logs of the bot.","Shows the recent application logs of the bot.","Cog: Utility +Command: debug" +"Shows the recent application logs of the bot.","Shows the recent application logs of the bot.","Cog: Utility +Command: debug" +"Posts application-logs to Hastebin.","Posts application-logs to Hastebin.","Cog: Utility +Command: debug hastebin" +"Posts application-logs to Hastebin.","Posts application-logs to Hastebin.","Cog: Utility +Command: debug hastebin" +"Clears the locally cached logs.","Clears the locally cached logs.","Cog: Utility +Command: debug clear" +"Clears the locally cached logs.","Clears the locally cached logs.","Cog: Utility +Command: debug clear" +"Set an activity status for the bot.","Set an activity status for the bot.","Cog: Utility +Command: activity" +"Set an activity status for the bot. + +Possible activity types: + - `playing` + - `streaming` + - `listening` + - `watching` + +When activity type is set to `listening`, +it must be followed by a ""to"": ""listening to..."" + +When activity type is set to `streaming`, you can set +the linked twitch page: +- `{prefix}config set twitch_url https://www.twitch.tv/somechannel/` + +To remove the current activity status: +- `{prefix}activity clear`","Set an activity status for the bot. + +Possible activity types: + - `playing` + - `streaming` + - `listening` + - `watching` + +When activity type is set to `listening`, +it must be followed by a ""to"": ""listening to..."" + +When activity type is set to `streaming`, you can set +the linked twitch page: +- `{prefix}config set twitch_url https://www.twitch.tv/somechannel/` + +To remove the current activity status: +- `{prefix}activity clear`","Cog: Utility +Command: activity" +"Set a status for the bot.","Set a status for the bot.","Cog: Utility +Command: status" +"Set a status for the bot. + +Possible status types: + - `online` + - `idle` + - `dnd` or `do not disturb` + - `invisible` or `offline` + +To remove the current status: +- `{prefix}status clear`","Set a status for the bot. + +Possible status types: + - `online` + - `idle` + - `dnd` or `do not disturb` + - `invisible` or `offline` + +To remove the current status: +- `{prefix}status clear`","Cog: Utility +Command: status" +"Pong! Returns your websocket latency.","Pong! Returns your websocket latency.","Cog: Utility +Command: ping" +"Pong! Returns your websocket latency.","Pong! Returns your websocket latency.","Cog: Utility +Command: ping" +"Change what the bot mentions at the start of each thread.","Change what the bot mentions at the start of each thread.","Cog: Utility +Command: mention" +"Change what the bot mentions at the start of each thread. + +Type only `{prefix}mention` to retrieve your current ""mention"" message.","Change what the bot mentions at the start of each thread. + +Type only `{prefix}mention` to retrieve your current ""mention"" message.","Cog: Utility +Command: mention" +"Change the prefix of the bot.","Change the prefix of the bot.","Cog: Utility +Command: prefix" +"Change the prefix of the bot. + +Type only `{prefix}prefix` to retrieve your current bot prefix.","Change the prefix of the bot. + +Type only `{prefix}prefix` to retrieve your current bot prefix.","Cog: Utility +Command: prefix" +"Modify changeable configuration variables for this bot.","Modify changeable configuration variables for this bot.","Cog: Utility +Command: config" +"Modify changeable configuration variables for this bot. + +Type `{prefix}config options` to view a list +of valid configuration variables. + +Type `{prefix}config help config-name` for info + on a config. + +To set a configuration variable: +- `{prefix}config set config-name value here` + +To remove a configuration variable: +- `{prefix}config remove config-name`","Modify changeable configuration variables for this bot. + +Type `{prefix}config options` to view a list +of valid configuration variables. + +Type `{prefix}config help config-name` for info + on a config. + +To set a configuration variable: +- `{prefix}config set config-name value here` + +To remove a configuration variable: +- `{prefix}config remove config-name`","Cog: Utility +Command: config" +"Return a list of valid configuration names you can change.","Return a list of valid configuration names you can change.","Cog: Utility +Command: config options" +"Return a list of valid configuration names you can change.","Return a list of valid configuration names you can change.","Cog: Utility +Command: config options" +"Set a configuration variable and its value.","Set a configuration variable and its value.","Cog: Utility +Command: config set" +"Set a configuration variable and its value.","Set a configuration variable and its value.","Cog: Utility +Command: config set" +"Delete a set configuration variable.","Delete a set configuration variable.","Cog: Utility +Command: config remove" +"Delete a set configuration variable.","Delete a set configuration variable.","Cog: Utility +Command: config remove" +"Show the configuration variables that are currently set.","Show the configuration variables that are currently set.","Cog: Utility +Command: config get" +"Show the configuration variables that are currently set. + +Leave `key` empty to show all currently set configuration variables.","Show the configuration variables that are currently set. + +Leave `key` empty to show all currently set configuration variables.","Cog: Utility +Command: config get" +"Show information on a specified configuration.","Show information on a specified configuration.","Cog: Utility +Command: config help" +"Show information on a specified configuration.","Show information on a specified configuration.","Cog: Utility +Command: config help" +"Create shortcuts to bot commands.","Create shortcuts to bot commands.","Cog: Utility +Command: alias" +"Create shortcuts to bot commands. + +When `{prefix}alias` is used by itself, this will retrieve +a list of alias that are currently set. `{prefix}alias-name` will show what the +alias point to. + +To use alias: + +First create an alias using: +- `{prefix}alias add alias-name other-command` + +For example: +- `{prefix}alias add r reply` +- Now you can use `{prefix}r` as an replacement for `{prefix}reply`. + +See also `{prefix}snippet`.","Create shortcuts to bot commands. + +When `{prefix}alias` is used by itself, this will retrieve +a list of alias that are currently set. `{prefix}alias-name` will show what the +alias point to. + +To use alias: + +First create an alias using: +- `{prefix}alias add alias-name other-command` + +For example: +- `{prefix}alias add r reply` +- Now you can use `{prefix}r` as an replacement for `{prefix}reply`. + +See also `{prefix}snippet`.","Cog: Utility +Command: alias" +"View the raw content of an alias.","View the raw content of an alias.","Cog: Utility +Command: alias raw" +"View the raw content of an alias.","View the raw content of an alias.","Cog: Utility +Command: alias raw" +"Add an alias.","Add an alias.","Cog: Utility +Command: alias add" +"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""`","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""`","Cog: Utility +Command: alias add" +"Remove an alias.","Remove an alias.","Cog: Utility +Command: alias remove" +"Remove an alias.","Remove an alias.","Cog: Utility +Command: alias remove" +"Edit an alias.","Edit an alias.","Cog: Utility +Command: alias edit" +"Edit an alias.","Edit an alias.","Cog: Utility +Command: alias edit" +"Set the permissions for Modmail commands.","Set the permissions for Modmail commands.","Cog: Utility +Command: permissions" +"Set the permissions for Modmail commands. + +You may set permissions based on individual command names, or permission +levels. + +Acceptable permission levels are: + - **Owner** [5] (absolute control over the bot) + - **Administrator** [4] (administrative powers such as setting activities) + - **Moderator** [3] (ability to block) + - **Supporter** [2] (access to core Modmail supporting functions) + - **Regular** [1] (most basic interactions such as help and about) + +By default, owner is set to the absolute bot owner and regular is `@everyone`. + +To set permissions, see `{prefix}help permissions add`; and to change permission level for specific +commands see `{prefix}help permissions override`. + +Note: You will still have to manually give/take permission to the Modmail +category to users/roles.","Set the permissions for Modmail commands. + +You may set permissions based on individual command names, or permission +levels. + +Acceptable permission levels are: + - **Owner** [5] (absolute control over the bot) + - **Administrator** [4] (administrative powers such as setting activities) + - **Moderator** [3] (ability to block) + - **Supporter** [2] (access to core Modmail supporting functions) + - **Regular** [1] (most basic interactions such as help and about) + +By default, owner is set to the absolute bot owner and regular is `@everyone`. + +To set permissions, see `{prefix}help permissions add`; and to change permission level for specific +commands see `{prefix}help permissions override`. + +Note: You will still have to manually give/take permission to the Modmail +category to users/roles.","Cog: Utility +Command: permissions" +"Change a permission level for a specific command.","Change a permission level for a specific command.","Cog: Utility +Command: permissions override" +"Change a permission level for a specific command. + +Examples: +- `{prefix}perms override reply administrator` +- `{prefix}perms override ""plugin enabled"" moderator` + +To undo a permission override, see `{prefix}help permissions remove`. + +Example: +- `{prefix}perms remove override reply` +- `{prefix}perms remove override plugin enabled` + +You can retrieve a single or all command level override(s), see`{prefix}help permissions get`.","Change a permission level for a specific command. + +Examples: +- `{prefix}perms override reply administrator` +- `{prefix}perms override ""plugin enabled"" moderator` + +To undo a permission override, see `{prefix}help permissions remove`. + +Example: +- `{prefix}perms remove override reply` +- `{prefix}perms remove override plugin enabled` + +You can retrieve a single or all command level override(s), see`{prefix}help permissions get`.","Cog: Utility +Command: permissions override" +"Add a permission to a command or a permission level.","Add a permission to a command or a permission level.","Cog: Utility +Command: permissions add" +"Add a permission to a command or a permission level. + +For sub commands, wrap the complete command name with quotes. +To find a list of permission levels, see `{prefix}help perms`. + +Examples: +- `{prefix}perms add level REGULAR everyone` +- `{prefix}perms add command reply @user` +- `{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.","Add a permission to a command or a permission level. + +For sub commands, wrap the complete command name with quotes. +To find a list of permission levels, see `{prefix}help perms`. + +Examples: +- `{prefix}perms add level REGULAR everyone` +- `{prefix}perms add command reply @user` +- `{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.","Cog: Utility +Command: permissions add" +"Remove permission to use a command, permission level, or command level override.","Remove permission to use a command, permission level, or command level override.","Cog: Utility +Command: permissions remove" +"Remove permission to use a command, permission level, or command level override. + +For sub commands, wrap the complete command name with quotes. +To find a list of permission levels, see `{prefix}help perms`. + +Examples: +- `{prefix}perms remove level REGULAR everyone` +- `{prefix}perms remove command reply @user` +- `{prefix}perms remove command ""plugin enabled"" @role` +- `{prefix}perms remove command help 984301093849028` +- `{prefix}perms remove override block` +- `{prefix}perms remove override ""snippet add""` + +Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Remove permission to use a command, permission level, or command level override. + +For sub commands, wrap the complete command name with quotes. +To find a list of permission levels, see `{prefix}help perms`. + +Examples: +- `{prefix}perms remove level REGULAR everyone` +- `{prefix}perms remove command reply @user` +- `{prefix}perms remove command ""plugin enabled"" @role` +- `{prefix}perms remove command help 984301093849028` +- `{prefix}perms remove override block` +- `{prefix}perms remove override ""snippet add""` + +Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Cog: Utility +Command: permissions remove" +"View the currently-set permissions.","View the currently-set permissions.","Cog: Utility +Command: permissions get" +"View the currently-set permissions. + +To find a list of permission levels, see `{prefix}help perms`. + +To view all command and level permissions: + +Examples: +- `{prefix}perms get @user` +- `{prefix}perms get 984301093849028` + +To view all users and roles of a command or level permission: + +Examples: +- `{prefix}perms get command reply` +- `{prefix}perms get command plugin remove` +- `{prefix}perms get level SUPPORTER` + +To view command level overrides: + +Examples: +- `{prefix}perms get override block` +- `{prefix}perms get override permissions add` + +Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","View the currently-set permissions. + +To find a list of permission levels, see `{prefix}help perms`. + +To view all command and level permissions: + +Examples: +- `{prefix}perms get @user` +- `{prefix}perms get 984301093849028` + +To view all users and roles of a command or level permission: + +Examples: +- `{prefix}perms get command reply` +- `{prefix}perms get command plugin remove` +- `{prefix}perms get level SUPPORTER` + +To view command level overrides: + +Examples: +- `{prefix}perms get override block` +- `{prefix}perms get override permissions add` + +Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Cog: Utility +Command: permissions get" +"Commands relating to logviewer oauth2 login authentication.","Commands relating to logviewer oauth2 login authentication.","Cog: Utility +Command: oauth" +"Commands relating to logviewer oauth2 login authentication. + +This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature.","Commands relating to logviewer oauth2 login authentication. + +This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature.","Cog: Utility +Command: oauth" +"Whitelist or un-whitelist a user or role to have access to logs.","Whitelist or un-whitelist a user or role to have access to logs.","Cog: Utility +Command: oauth whitelist" +"Whitelist or un-whitelist a user or role to have access to logs. + +`target` may be a role ID, name, mention, user ID, name, or mention.","Whitelist or un-whitelist a user or role to have access to logs. + +`target` may be a role ID, name, mention, user ID, name, or mention.","Cog: Utility +Command: oauth whitelist" +"Shows a list of users and roles that are whitelisted to view logs.","Shows a list of users and roles that are whitelisted to view logs.","Cog: Utility +Command: oauth show" +"Shows a list of users and roles that are whitelisted to view logs.","Shows a list of users and roles that are whitelisted to view logs.","Cog: Utility +Command: oauth show" +"Evaluates Python code.","Evaluates Python code.","Cog: Utility +Command: eval" +"Evaluates Python code.","Evaluates Python code.","Cog: Utility +Command: eval" +"Shows this help message.","Shows this help message.","Cog: Utility +Command: help" +"Shows this help message.","Shows this help message.","Cog: Utility +Command: help" diff --git a/plugins/registry.json b/plugins/registry.json index b15bb374dc..1c268d89be 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -170,7 +170,7 @@ "thumbnail_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png" }, "stats": { - "repository": "KarateWumpus/modmail-plugins", + "repository": "MiTonder/modmail-plugins", "branch": "master", "description": "Get useful stats directly in an embed about either the Modmail bot, a user or the server.", "bot_version": "2.24.1", @@ -190,37 +190,10 @@ "serverstats": { "repository": "dazvise/modmail-plugins", "branch": "master", - "description": "Voice channels containing interesting and accurate statistics about your server such as Member Count.", + "description": "Interesting and accurate statistics about your server.", "bot_version": "2.20.1", "title": "Server Stats", "icon_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png", "thumbnail_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png" - }, - "suggest": { - "repository": "realcyguy/modmail-plugins", - "branch": "master", - "description": "Send suggestions to a selected server! It even has moderation...", - "bot_version": "3.4.1", - "title": "Suggest stuff.", - "icon_url": "https://i.imgur.com/qtE7AH8.png", - "thumbnail_url": "https://i.imgur.com/qtE7AH8.png" - }, - "githubstats": { - "repository": "mischievousdev/modmail-plugins", - "branch": "master", - "description": "Github statistics in discord", - "bot_version": "2.20.1", - "title": "Github Stats", - "icon_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg", - "thumbnail_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg" - }, - "slowmode": { - "repository": "teen1/modmail-plugins", - "branch": "master", - "description": "Configure slow mode for your channels with Modmail!", - "bot_version": "2.20.1", - "title": "Slow Mode", - "icon_url": "https://cdn.discordapp.com/attachments/717029057635549274/717033838966210601/Slow_mode_-_icon.png", - "thumbnail_url": "https://cdn.discordapp.com/attachments/717029057635549274/717029110907666482/Slow_mode_plugin_-_thumbnail.png" } } diff --git a/poetry.lock b/poetry.lock index f4b23956b8..1841d893a1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,9 +13,6 @@ chardet = ">=2.0,<4.0" multidict = ">=4.0,<5.0" yarl = ">=1.0,<2.0" -[package.extras] -speedups = ["aiodns", "brotlipy", "cchardet"] - [[package]] category = "dev" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." @@ -57,12 +54,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "19.3.0" -[package.extras] -azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] -dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] -docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] - [[package]] category = "dev" description = "Security oriented static analyser for python code." @@ -92,9 +83,6 @@ attrs = ">=18.1.0" click = ">=6.5" toml = ">=0.9.4" -[package.extras] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] - [[package]] category = "main" description = "Universal encoding detector for Python 2 and 3" @@ -108,8 +96,8 @@ category = "dev" description = "Composable command line interface toolkit" name = "click" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "7.0" [[package]] category = "main" @@ -131,10 +119,6 @@ version = "1.2.5" aiohttp = ">=3.3.0,<3.6.0" websockets = ">=6.0,<7.0" -[package.extras] -docs = ["sphinx (1.7.4)", "sphinxcontrib-asyncio", "sphinxcontrib-websupport"] -voice = ["PyNaCl (1.3.0)"] - [[package]] category = "main" description = "DNS toolkit" @@ -143,10 +127,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.16.0" -[package.extras] -DNSSEC = ["pycryptodome", "ecdsa (>=0.13)"] -IDNA = ["idna (>=2.1)"] - [[package]] category = "main" description = "Emoji for Python" @@ -155,9 +135,6 @@ optional = false python-versions = "*" version = "0.5.4" -[package.extras] -dev = ["nose", "coverage", "coveralls"] - [[package]] category = "main" description = "Backport of the concurrent.futures package from Python 3.2" @@ -169,24 +146,24 @@ version = "3.1.1" [[package]] category = "dev" description = "Git Object Database" -name = "gitdb" +name = "gitdb2" optional = false -python-versions = ">=3.4" -version = "4.0.4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.0.6" [package.dependencies] -smmap = ">=3.0.1,<4" +smmap2 = ">=2.0.0" [[package]] category = "dev" description = "Python Git Library" name = "gitpython" optional = false -python-versions = ">=3.4" -version = "3.1.1" +python-versions = ">=3.0, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.0.5" [package.dependencies] -gitdb = ">=4.0.1,<5" +gitdb2 = ">=2.0.0" [[package]] category = "main" @@ -194,7 +171,7 @@ 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.9" +version = "2.8" [[package]] category = "main" @@ -215,12 +192,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "4.3.21" -[package.extras] -pipfile = ["pipreqs", "requirementslib"] -pyproject = ["toml"] -requirements = ["pipreqs", "pip-api"] -xdg_home = ["appdirs (>=1.4.0)"] - [[package]] category = "dev" description = "A fast and thorough lazy object proxy." @@ -255,7 +226,7 @@ description = "multidict implementation" name = "multidict" optional = false python-versions = ">=3.5" -version = "4.7.5" +version = "4.7.1" [[package]] category = "main" @@ -282,7 +253,7 @@ description = "Python Build Reasonableness" name = "pbr" optional = false python-versions = "*" -version = "5.4.5" +version = "5.4.4" [[package]] category = "dev" @@ -304,15 +275,7 @@ description = "Python driver for MongoDB " name = "pymongo" optional = true python-versions = "*" -version = "3.10.1" - -[package.extras] -encryption = ["pymongocrypt (<2.0.0)"] -gssapi = ["pykerberos"] -snappy = ["python-snappy"] -srv = ["dnspython (>=1.16.0,<1.17.0)"] -tls = ["ipaddress"] -zstd = ["zstandard"] +version = "3.10.0" [[package]] category = "main" @@ -331,34 +294,31 @@ description = "Add .env support to your django/flask apps in development and dep name = "python-dotenv" optional = false python-versions = "*" -version = "0.10.5" - -[package.extras] -cli = ["click (>=5.0)"] +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.*, !=3.4.*" -version = "5.3.1" +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.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.14.0" +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 = "smmap" +name = "smmap2" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.0.2" +version = "2.0.5" [[package]] category = "dev" @@ -366,7 +326,7 @@ description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false python-versions = "*" -version = "1.32.0" +version = "1.31.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" @@ -387,7 +347,7 @@ marker = "implementation_name == \"cpython\" and python_version < \"3.8\"" name = "typed-ast" optional = false python-versions = "*" -version = "1.4.1" +version = "1.4.0" [[package]] category = "main" @@ -425,347 +385,48 @@ version = "1.4.2" idna = ">=2.0" multidict = ">=4.0" -[extras] -mongodb = ["motor"] - [metadata] -content-hash = "793f86eb19d73f473f00883384ec965876c3930f41f94a171d2d1bc38f66903b" +content-hash = "fbe9e329f33e482854cff5bf05b006de9830c2d46bf3874e2ee4f8a8da0b1797" python-versions = "^3.7" -[metadata.files] -aiohttp = [ - {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_10_x86_64.whl", hash = "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5"}, - {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed"}, - {file = "aiohttp-3.5.4-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303"}, - {file = "aiohttp-3.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10"}, - {file = "aiohttp-3.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa"}, - {file = "aiohttp-3.5.4-cp35-cp35m-win32.whl", hash = "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4"}, - {file = "aiohttp-3.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72"}, - {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5"}, - {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12"}, - {file = "aiohttp-3.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6"}, - {file = "aiohttp-3.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6"}, - {file = "aiohttp-3.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d"}, - {file = "aiohttp-3.5.4-cp36-cp36m-win32.whl", hash = "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1"}, - {file = "aiohttp-3.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d"}, - {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300"}, - {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390"}, - {file = "aiohttp-3.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366"}, - {file = "aiohttp-3.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"}, - {file = "aiohttp-3.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55"}, - {file = "aiohttp-3.5.4-cp37-cp37m-win32.whl", hash = "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc"}, - {file = "aiohttp-3.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939"}, - {file = "aiohttp-3.5.4.tar.gz", hash = "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf"}, -] -appdirs = [ - {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, - {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, -] -astroid = [ - {file = "astroid-2.3.3-py3-none-any.whl", hash = "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"}, - {file = "astroid-2.3.3.tar.gz", hash = "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a"}, -] -async-timeout = [ - {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, - {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, -] -attrs = [ - {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, - {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, -] -bandit = [ - {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, - {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, -] -black = [ - {file = "black-19.3b0-py36-none-any.whl", hash = "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf"}, - {file = "black-19.3b0.tar.gz", hash = "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"}, -] -chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, -] -click = [ - {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, - {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, -] -colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, -] -"discord.py" = [ - {file = "discord.py-1.2.5-py3-none-any.whl", hash = "sha256:7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d"}, -] -dnspython = [ - {file = "dnspython-1.16.0-py2.py3-none-any.whl", hash = "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"}, - {file = "dnspython-1.16.0.zip", hash = "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01"}, -] -emoji = [ - {file = "emoji-0.5.4.tar.gz", hash = "sha256:60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174"}, -] -futures = [ - {file = "futures-3.1.1-py2-none-any.whl", hash = "sha256:c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"}, - {file = "futures-3.1.1-py3-none-any.whl", hash = "sha256:3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b"}, - {file = "futures-3.1.1.tar.gz", hash = "sha256:51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd"}, -] -gitdb = [ - {file = "gitdb-4.0.4-py3-none-any.whl", hash = "sha256:ba1132c0912e8c917aa8aa990bee26315064c7b7f171ceaaac0afeb1dc656c6a"}, - {file = "gitdb-4.0.4.tar.gz", hash = "sha256:6f0ecd46f99bb4874e5678d628c3a198e2b4ef38daea2756a2bfd8df7dd5c1a5"}, -] -gitpython = [ - {file = "GitPython-3.1.1-py3-none-any.whl", hash = "sha256:71b8dad7409efbdae4930f2b0b646aaeccce292484ffa0bc74f1195582578b3d"}, - {file = "GitPython-3.1.1.tar.gz", hash = "sha256:6d4f10e2aaad1864bb0f17ec06a2c2831534140e5883c350d58b4e85189dab74"}, -] -idna = [ - {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, - {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, -] -isodate = [ - {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"}, - {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"}, -] -isort = [ - {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, - {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, -] -lazy-object-proxy = [ - {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"}, - {file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"}, - {file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"}, - {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"}, - {file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"}, - {file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"}, - {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"}, - {file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"}, - {file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"}, - {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"}, - {file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"}, - {file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"}, - {file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"}, - {file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"}, - {file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -motor = [ - {file = "motor-2.1.0-py2-none-any.whl", hash = "sha256:599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909"}, - {file = "motor-2.1.0-py3-none-any.whl", hash = "sha256:97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a"}, - {file = "motor-2.1.0.tar.gz", hash = "sha256:756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4"}, -] -multidict = [ - {file = "multidict-4.7.5-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:fc3b4adc2ee8474cb3cd2a155305d5f8eda0a9c91320f83e55748e1fcb68f8e3"}, - {file = "multidict-4.7.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:42f56542166040b4474c0c608ed051732033cd821126493cf25b6c276df7dd35"}, - {file = "multidict-4.7.5-cp35-cp35m-win32.whl", hash = "sha256:7774e9f6c9af3f12f296131453f7b81dabb7ebdb948483362f5afcaac8a826f1"}, - {file = "multidict-4.7.5-cp35-cp35m-win_amd64.whl", hash = "sha256:c2c37185fb0af79d5c117b8d2764f4321eeb12ba8c141a95d0aa8c2c1d0a11dd"}, - {file = "multidict-4.7.5-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:e439c9a10a95cb32abd708bb8be83b2134fa93790a4fb0535ca36db3dda94d20"}, - {file = "multidict-4.7.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:85cb26c38c96f76b7ff38b86c9d560dea10cf3459bb5f4caf72fc1bb932c7136"}, - {file = "multidict-4.7.5-cp36-cp36m-win32.whl", hash = "sha256:620b37c3fea181dab09267cd5a84b0f23fa043beb8bc50d8474dd9694de1fa6e"}, - {file = "multidict-4.7.5-cp36-cp36m-win_amd64.whl", hash = "sha256:6e6fef114741c4d7ca46da8449038ec8b1e880bbe68674c01ceeb1ac8a648e78"}, - {file = "multidict-4.7.5-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a326f4240123a2ac66bb163eeba99578e9d63a8654a59f4688a79198f9aa10f8"}, - {file = "multidict-4.7.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dc561313279f9d05a3d0ffa89cd15ae477528ea37aa9795c4654588a3287a9ab"}, - {file = "multidict-4.7.5-cp37-cp37m-win32.whl", hash = "sha256:4b7df040fb5fe826d689204f9b544af469593fb3ff3a069a6ad3409f742f5928"}, - {file = "multidict-4.7.5-cp37-cp37m-win_amd64.whl", hash = "sha256:317f96bc0950d249e96d8d29ab556d01dd38888fbe68324f46fd834b430169f1"}, - {file = "multidict-4.7.5-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:b51249fdd2923739cd3efc95a3d6c363b67bbf779208e9f37fd5e68540d1a4d4"}, - {file = "multidict-4.7.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ae402f43604e3b2bc41e8ea8b8526c7fa7139ed76b0d64fc48e28125925275b2"}, - {file = "multidict-4.7.5-cp38-cp38-win32.whl", hash = "sha256:bb519becc46275c594410c6c28a8a0adc66fe24fef154a9addea54c1adb006f5"}, - {file = "multidict-4.7.5-cp38-cp38-win_amd64.whl", hash = "sha256:544fae9261232a97102e27a926019100a9db75bec7b37feedd74b3aa82f29969"}, - {file = "multidict-4.7.5.tar.gz", hash = "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e"}, -] -natural = [ - {file = "natural-0.2.0.tar.gz", hash = "sha256:18c83662d2d33fd7e6eee4e3b0d7366e1ce86225664e3127a2aaf0a3233f7df2"}, -] -parsedatetime = [ - {file = "parsedatetime-2.5-py2-none-any.whl", hash = "sha256:3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1"}, - {file = "parsedatetime-2.5.tar.gz", hash = "sha256:d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"}, -] -pbr = [ - {file = "pbr-5.4.5-py2.py3-none-any.whl", hash = "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"}, - {file = "pbr-5.4.5.tar.gz", hash = "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c"}, -] -pylint = [ - {file = "pylint-2.4.4-py3-none-any.whl", hash = "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"}, - {file = "pylint-2.4.4.tar.gz", hash = "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd"}, -] -pymongo = [ - {file = "pymongo-3.10.1-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee"}, - {file = "pymongo-3.10.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed"}, - {file = "pymongo-3.10.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6"}, - {file = "pymongo-3.10.1-cp27-cp27m-win32.whl", hash = "sha256:e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5"}, - {file = "pymongo-3.10.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe"}, - {file = "pymongo-3.10.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc"}, - {file = "pymongo-3.10.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764"}, - {file = "pymongo-3.10.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372"}, - {file = "pymongo-3.10.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32"}, - {file = "pymongo-3.10.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc"}, - {file = "pymongo-3.10.1-cp34-cp34m-win32.whl", hash = "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a"}, - {file = "pymongo-3.10.1-cp34-cp34m-win_amd64.whl", hash = "sha256:7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae"}, - {file = "pymongo-3.10.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_ppc64le.whl", hash = "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_s390x.whl", hash = "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf"}, - {file = "pymongo-3.10.1-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d"}, - {file = "pymongo-3.10.1-cp35-cp35m-win32.whl", hash = "sha256:c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57"}, - {file = "pymongo-3.10.1-cp35-cp35m-win_amd64.whl", hash = "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70"}, - {file = "pymongo-3.10.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464"}, - {file = "pymongo-3.10.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73"}, - {file = "pymongo-3.10.1-cp36-cp36m-win32.whl", hash = "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33"}, - {file = "pymongo-3.10.1-cp36-cp36m-win_amd64.whl", hash = "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6"}, - {file = "pymongo-3.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11"}, - {file = "pymongo-3.10.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951"}, - {file = "pymongo-3.10.1-cp37-cp37m-win32.whl", hash = "sha256:c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39"}, - {file = "pymongo-3.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b"}, - {file = "pymongo-3.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82"}, - {file = "pymongo-3.10.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be"}, - {file = "pymongo-3.10.1-cp38-cp38-win32.whl", hash = "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f"}, - {file = "pymongo-3.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac"}, - {file = "pymongo-3.10.1-py2.7-macosx-10.14-intel.egg", hash = "sha256:bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a"}, - {file = "pymongo-3.10.1-py2.7-win-amd64.egg", hash = "sha256:ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012"}, - {file = "pymongo-3.10.1-py2.7-win32.egg", hash = "sha256:f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a"}, - {file = "pymongo-3.10.1.tar.gz", hash = "sha256:993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa"}, -] -python-dateutil = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, -] -python-dotenv = [ - {file = "python-dotenv-0.10.5.tar.gz", hash = "sha256:f254bfd0c970d64ccbb6c9ebef3667ab301a71473569c991253a481f1c98dddc"}, - {file = "python_dotenv-0.10.5-py2.py3-none-any.whl", hash = "sha256:440c7c23d53b7d352f9c94d6f70860242c2f071cf5c029dd661ccb22d64ae42b"}, -] -pyyaml = [ - {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, - {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, - {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, - {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, - {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, -] -six = [ - {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, - {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, -] -smmap = [ - {file = "smmap-3.0.2-py2.py3-none-any.whl", hash = "sha256:52ea78b3e708d2c2b0cfe93b6fc3fbeec53db913345c26be6ed84c11ed8bebc1"}, - {file = "smmap-3.0.2.tar.gz", hash = "sha256:b46d3fc69ba5f367df96d91f8271e8ad667a198d5a28e215a6c3d9acd133a911"}, -] -stevedore = [ - {file = "stevedore-1.32.0-py2.py3-none-any.whl", hash = "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"}, - {file = "stevedore-1.32.0.tar.gz", hash = "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b"}, -] -toml = [ - {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, - {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, - {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, -] -typed-ast = [ - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, - {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, - {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, - {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, - {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, -] -uvloop = [ - {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, - {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"}, - {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"}, - {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"}, - {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"}, - {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"}, - {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"}, - {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, - {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, -] -websockets = [ - {file = "websockets-6.0-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d"}, - {file = "websockets-6.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484"}, - {file = "websockets-6.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e"}, - {file = "websockets-6.0-cp34-cp34m-win32.whl", hash = "sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454"}, - {file = "websockets-6.0-cp34-cp34m-win_amd64.whl", hash = "sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96"}, - {file = "websockets-6.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538"}, - {file = "websockets-6.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0"}, - {file = "websockets-6.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1"}, - {file = "websockets-6.0-cp35-cp35m-win32.whl", hash = "sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d"}, - {file = "websockets-6.0-cp35-cp35m-win_amd64.whl", hash = "sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6"}, - {file = "websockets-6.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf"}, - {file = "websockets-6.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d"}, - {file = "websockets-6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb"}, - {file = "websockets-6.0-cp36-cp36m-win32.whl", hash = "sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c"}, - {file = "websockets-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff"}, - {file = "websockets-6.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908"}, - {file = "websockets-6.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559"}, - {file = "websockets-6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4"}, - {file = "websockets-6.0-cp37-cp37m-win32.whl", hash = "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136"}, - {file = "websockets-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584"}, - {file = "websockets-6.0.tar.gz", hash = "sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c"}, -] -wrapt = [ - {file = "wrapt-1.11.2.tar.gz", hash = "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"}, -] -yarl = [ - {file = "yarl-1.4.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b"}, - {file = "yarl-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1"}, - {file = "yarl-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080"}, - {file = "yarl-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a"}, - {file = "yarl-1.4.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f"}, - {file = "yarl-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea"}, - {file = "yarl-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb"}, - {file = "yarl-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70"}, - {file = "yarl-1.4.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d"}, - {file = "yarl-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce"}, - {file = "yarl-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"}, - {file = "yarl-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce"}, - {file = "yarl-1.4.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b"}, - {file = "yarl-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae"}, - {file = "yarl-1.4.2-cp38-cp38-win32.whl", hash = "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462"}, - {file = "yarl-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6"}, - {file = "yarl-1.4.2.tar.gz", hash = "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b"}, -] +[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"] +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"] +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 a114573201..9c7122c417 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 99 -target-version = ['py37'] +target-version = ['py36'] include = '\.pyi?$' exclude = ''' ( @@ -21,8 +21,8 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.4.1' -description = "Modmail is similar to Reddit's Modmail, both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way." +version = '3.5.0-dev0' +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 ', @@ -54,5 +54,3 @@ black = {version = "=19.3b0", allows-prereleases = true} pylint = "^2.4" bandit = "^1.6" -[tool.poetry.extras] -mongodb = ["motor"] diff --git a/runtime.txt b/runtime.txt index 6919bf9ede..aefcfbece7 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.6 +python-3.7.5 diff --git a/translation_files.py b/translation_files.py new file mode 100644 index 0000000000..7d50c53417 --- /dev/null +++ b/translation_files.py @@ -0,0 +1,117 @@ +import csv +import glob +import re +import string + +from discord.ext import commands + +from core import translations +from cogs.modmail import Modmail +from cogs.plugins import Plugins +from cogs.utility import Utility + + +data = [('Identifier', 'English', 'Context')] +all_identifiers = [] +identifiers = {} + + +class FormatError(Exception): + def __init__(self, reason, string): + super().__init__(f'Unable to parse {reason}: {string}') + + +for filename in glob.glob('**/*.py') + glob.glob('*.py'): + if filename == 'translation_files.py': + continue + with open(filename, encoding='utf8') as f: + filedata = f.read() + regex_matches = re.findall(r'(?:^|[^A-z])_\(.+?(?:\'|\")\)+?', filedata, flags=re.DOTALL | re.MULTILINE) + + for i in regex_matches: + if "f'" in i or 'f"' in i: + print(FormatError('f-string', i)) + identifier = '' + read = False + ignore_inverted = False + newline = False + counter = 0 + mode = None + for n in range(len(i)): + triggered = False + x = i[n] + + if x in ("'", '"') and not ignore_inverted: + if not mode: + mode = x + if mode == x: + read = not read + triggered = True + + if x == '\n': + newline = True + + if read and not triggered: + newline = False + identifier += x + + if newline and x not in string.whitespace: + counter += 1 + if counter > 1: + break + else: + counter = 0 + + if x == '\\': + ignore_inverted = True + elif ignore_inverted: + ignore_inverted = False + + all_identifiers.append(identifier) + + filedata_lines = filedata.splitlines() + fullline = list(filter((lambda x: x.find(i.splitlines()[0]) != -1), filedata_lines))[0] + count = 0 + for nline, line in enumerate(filedata_lines): + if line == fullline: + count += 1 + if count == all_identifiers.count(identifier): + linenum = nline + 1 + break + + if identifier in identifiers.keys(): + if filename not in data[identifiers[identifier]][2]: + data[identifiers[identifier]][2] += f' {filename}/L{linenum}' + elif str(linenum) not in data[identifiers[identifier]][2]: + split_space = data[identifiers[identifier]][2].split(' ') + for nx, x in enumerate(split_space): + if filename in x: + split_space[nx] += f'/L{linenum}' + break + data[identifiers[identifier]][2] = (' ').join(split_space) + else: + data.append([identifier, identifier, f'File: {filename}/L{linenum}']) + identifiers[identifier] = len(data) - 1 + + print(filename) + +translations.init() +done = set() +bot = commands.Bot(command_prefix=None) +cogs = [Modmail(bot), Plugins(bot), Utility(bot)] +for i in cogs: + if i.description: + data.append([i.description, i.description, f'Cog: {i.__cog_name__}']) + + for cmd in i.walk_commands(): + if cmd not in done: + if cmd.short_doc: + print(cmd) + data.append([cmd.short_doc, cmd.short_doc, f'Cog: {i.__cog_name__}\nCommand: {cmd.qualified_name}']) + data.append([cmd.help, cmd.help, f'Cog: {i.__cog_name__}\nCommand: {cmd.qualified_name}']) + + done.add(cmd) + + +with open('languages/en.csv', 'w+') as f: + csv.writer(f, dialect='unix').writerows(data) From d74f8c82945c04f80336ec4752ed5e8628756c1b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 3 Jul 2020 18:24:15 +0800 Subject: [PATCH 0572/1221] black? --- bot.py | 4 +- cogs/modmail.py | 151 ++++++++++++++++++----------- cogs/plugins.py | 75 +++++++++------ cogs/utility.py | 221 +++++++++++++++++++++++++++---------------- core/translations.py | 6 +- translation_files.py | 50 ++++++---- 6 files changed, 317 insertions(+), 190 deletions(-) diff --git a/bot.py b/bot.py index ddbeec2d31..a379567b4d 100644 --- a/bot.py +++ b/bot.py @@ -549,7 +549,9 @@ def check_account_age(self, author: discord.Member) -> bool: logger.debug("Blocked due to account age, user %s.", author.name) if str(author.id) not in self.blocked_users: - new_reason = _("System Message: New Account. Required to wait for {time}.").format(time=delta) + new_reason = _("System Message: New Account. Required to wait for {time}.").format( + time=delta + ) self.blocked_users[str(author.id)] = new_reason return False diff --git a/cogs/modmail.py b/cogs/modmail.py index 7f5b2fe15b..76a28b3c9c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -41,12 +41,18 @@ async def setup(self, ctx): if ctx.guild != self.bot.modmail_guild: return await ctx.send( - _("You can only setup in the Modmail guild: {guild_name}.".format(guild_name=self.bot.modmail_guild)) + _( + "You can only setup in the Modmail guild: {guild_name}.".format( + guild_name=self.bot.modmail_guild + ) + ) ) if self.bot.main_category is not None: logger.debug("Can't re-setup server, main_category is found.") - return await ctx.send(_("{guild_name} is already set up.").format(guild_name=self.bot.modmail_guild)) + return await ctx.send( + _("{guild_name} is already set up.").format(guild_name=self.bot.modmail_guild) + ) if self.bot.modmail_guild is None: embed = discord.Embed( @@ -89,21 +95,27 @@ async def setup(self, ctx): embed = discord.Embed( title=_("Friendly Reminder"), - description=_("You may use the `{prefix}config set log_channel_id " - "` command to set up a custom log channel, then you can delete this default " - "{log_channel} log channel.").format(prefix=self.bot.prefix, log_channel=log_channel.mention), + description=_( + "You may use the `{prefix}config set log_channel_id " + "` command to set up a custom log channel, then you can delete this default " + "{log_channel} log channel." + ).format(prefix=self.bot.prefix, log_channel=log_channel.mention), color=self.bot.main_color, ) embed.add_field( name=_("Thanks for using the 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)!"), + 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)!" + ), ) embed.set_footer( - text=_('Type "{prefix}help" for a complete list of commands.').format(prefix=self.bot.prefix) + text=_('Type "{prefix}help" for a complete list of commands.').format( + prefix=self.bot.prefix + ) ) await log_channel.send(embed=embed) @@ -112,12 +124,14 @@ 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" - "Type:\n- `{prefix}permissions` and `{prefix}permissions add` " - "for more info on setting permissions.\n" - "- `{prefix}config help` for a list of available customizations.").format(prefix=self.bot.prefix) + _( + "**Successfully set up server.**\n" + "Consider setting permission levels " + "to give access to roles or users the ability to use Modmail.\n\n" + "Type:\n- `{prefix}permissions` and `{prefix}permissions add` " + "for more info on setting permissions.\n" + "- `{prefix}config help` for a list of available customizations." + ).format(prefix=self.bot.prefix) ) if not self.bot.config["command_permissions"] and not self.bot.config["level_permissions"]: @@ -224,7 +238,9 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte embed = discord.Embed( title=_("Error"), color=self.bot.error_color, - description=_("An alias with the same name already exists: `{name}`.").format(name=name), + description=_("An alias with the same name already exists: `{name}`.").format( + name=name + ), ) return await ctx.send(embed=embed) @@ -323,16 +339,16 @@ async def send_scheduled_close_message(self, ctx, after, silent=False): embed = discord.Embed( title=_("Scheduled close"), - description=_("This thread will close {silent}in {time}.").format(silent=silent, time=human_delta), + description=_("This thread will close {silent}in {time}.").format( + silent=silent, time=human_delta + ), color=self.bot.error_color, ) 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) @@ -430,15 +446,18 @@ async def notify( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} is already going to be mentioned.").format(mention=mention), + description=_("{mention} is already going to be mentioned.").format( + mention=mention + ), ) else: mentions.append(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=_("{mention} will be mentioned " - "on the next message received.").format(mention=mention), + description=_( + "{mention} will be mentioned " "on the next message received." + ).format(mention=mention), ) return await ctx.send(embed=embed) @@ -469,7 +488,9 @@ async def unnotify( if mention not in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} does not have a pending notification.").format(mention=mention), + description=_("{mention} does not have a pending notification.").format( + mention=mention + ), ) else: mentions.remove(mention) @@ -509,15 +530,18 @@ async def subscribe( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} is already subscribed to this thread.").format(mention=mention), + description=_("{mention} is already subscribed to this thread.").format( + mention=mention + ), ) else: mentions.append(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=_("{mention} will now be " - "notified of all messages received.").format(mention=mention), + description=_( + "{mention} will now be " "notified of all messages received." + ).format(mention=mention), ) return await ctx.send(embed=embed) @@ -548,14 +572,18 @@ async def unsubscribe( if mention not in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} is not already subscribed to this thread.").format(mention=mention), + description=_("{mention} is not already subscribed to this thread.").format( + mention=mention + ), ) else: mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=_("{mention} is now unsubscribed to this thread.").format(mention=mention), + description=_("{mention} is now unsubscribed to this thread.").format( + mention=mention + ), ) return await ctx.send(embed=embed) @@ -740,7 +768,9 @@ async def logs_responded(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} has not responded to any threads.").format(mention=getattr(user, 'mention', user.id)), + description=_("{mention} has not responded to any threads.").format( + mention=getattr(user, "mention", user.id) + ), ) return await ctx.send(embed=embed) @@ -896,8 +926,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) @@ -905,8 +934,9 @@ async def contact( if exists: embed = discord.Embed( color=self.bot.error_color, - description=_("A thread for this user already " - "exists in {mention}.").format(mention=exists.channel.mention), + description=_("A thread for this user already " "exists in {mention}.").format( + mention=exists.channel.mention + ), ) await ctx.channel.send(embed=embed) @@ -917,8 +947,9 @@ async def contact( embed = discord.Embed( title=_("Created Thread"), - description=_("Thread started by {author_mention} " - "for {user_mention}.").format(author_mention=ctx.author.mention, user_mention=user.mention), + description=_("Thread started by {author_mention} " "for {user_mention}.").format( + author_mention=ctx.author.mention, user_mention=user.mention + ), color=self.bot.main_color, ) await thread.wait_until_ready() @@ -935,9 +966,7 @@ async def blocked(self, ctx): """Retrieve a list of blocked users.""" embeds = [ - discord.Embed( - title=_("Blocked Users"), color=self.bot.main_color, description="" - ) + discord.Embed(title=_("Blocked Users"), color=self.bot.main_color, description="") ] users = [] @@ -1015,8 +1044,10 @@ async def blocked_whitelist(self, ctx, *, user: User = None): reason = msg[16:].strip().rstrip(".") embed = discord.Embed( title="Success", - description=_("{mention} was previously blocked internally for " - '"{reason}". {mention} is now whitelisted.').format(mention=mention, reason=reason), + description=_( + "{mention} was previously blocked internally for " + '"{reason}". {mention} is now whitelisted.' + ).format(mention=mention, reason=reason), color=self.bot.main_color, ) else: @@ -1057,7 +1088,9 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( title=_("Error"), - description=_("Cannot block {mention}, user is whitelisted.").format(mention=mention), + description=_("Cannot block {mention}, user is whitelisted.").format( + mention=mention + ), color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -1082,15 +1115,19 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi old_reason = msg.strip().rstrip(".") embed = discord.Embed( title=_("Success"), - description=_("{mention} was previously blocked {old_reason}.\n" - "{mention} is now blocked {reason}").format(mention=mention, old_reason=old_reason, reason=reason), + description=_( + "{mention} was previously blocked {old_reason}.\n" + "{mention} is now blocked {reason}" + ).format(mention=mention, old_reason=old_reason, reason=reason), color=self.bot.main_color, ) else: embed = discord.Embed( title=_("Success"), color=self.bot.main_color, - description=_("{mention} is now blocked {reason}").format(mention=mention, reason=reason), + description=_("{mention} is now blocked {reason}").format( + mention=mention, reason=reason + ), ) self.bot.blocked_users[str(user.id)] = reason await self.bot.config.update() @@ -1129,16 +1166,18 @@ async def unblock(self, ctx, *, user: User = None): reason = msg[16:].strip().rstrip(".") or "no reason" embed = discord.Embed( title="Success", - description=_("{mention} was previously blocked internally " - "{reason}.\n{mention} is no longer blocked.").format(mention=mention, reason=reason), + description=_( + "{mention} was previously blocked internally " + "{reason}.\n{mention} is no longer blocked." + ).format(mention=mention, reason=reason), color=self.bot.main_color, ) embed.set_footer( - text=_("However, if the original system block reason still applies, " - "{name} will be automatically blocked again. Use " - '"{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.').format( - name=name, prefix=self.bot.prefix, user_id=user.id - ) + text=_( + "However, if the original system block reason still applies, " + "{name} will be automatically blocked again. Use " + '"{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' + ).format(name=name, prefix=self.bot.prefix, user_id=user.id) ) else: embed = discord.Embed( @@ -1355,7 +1394,9 @@ async def disable_new(self, ctx): else: embed = discord.Embed( title=_("Success"), - description=_("Modmail will not create **new** threads, but existing threads will now be functioning."), + description=_( + "Modmail will not create **new** threads, but existing threads will now be functioning." + ), color=self.bot.main_color, ) self.bot.config["dm_disabled"] = 1 @@ -1404,7 +1445,9 @@ async def isenable(self, ctx): elif self.bot.config["dm_disabled"] == 2: embed = discord.Embed( title=_("All DM Disabled"), - description=_("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: diff --git a/cogs/plugins.py b/cogs/plugins.py index 8868df3e77..4d335607e1 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -104,7 +104,7 @@ def __init__(self, bot): self.bot.loop.create_task(self.populate_registry()) - if getattr(self.bot, 'config', None) and self.bot.config.get("enable_plugins"): + if getattr(self.bot, "config", None) and self.bot.config.get("enable_plugins"): self.bot.loop.create_task(self.initial_load_plugins()) else: logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") @@ -243,8 +243,10 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if required_version and self.bot.version < parse_version(required_version): embed = discord.Embed( - description=_("Your bot's version is too low. " - "This plugin requires version `{required_version}`.").format(required_version=required_version), + description=_( + "Your bot's version is too low. " + "This plugin requires version `{required_version}`." + ).format(required_version=required_version), color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -257,9 +259,11 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): plugin = Plugin.from_string(plugin_name) except InvalidPluginError: embed = discord.Embed( - description=_("Invalid plugin name, double check the plugin name " - "or use one of the following formats: " - "username/repo/plugin, username/repo/plugin@branch."), + description=_( + "Invalid plugin name, double check the plugin name " + "or use one of the following formats: " + "username/repo/plugin, username/repo/plugin@branch." + ), color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -292,8 +296,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) @@ -306,7 +309,9 @@ async def plugins_add(self, ctx, *, plugin_name: str): return await ctx.send(embed=embed) embed = discord.Embed( - description=_("Starting to download plugin from {plugin_link}...").format(plugin_link=plugin.link), + description=_("Starting to download plugin from {plugin_link}...").format( + plugin_link=plugin.link + ), color=self.bot.main_color, ) msg = await ctx.send(embed=embed) @@ -342,18 +347,25 @@ async def plugins_add(self, ctx, *, plugin_name: str): else: embed = discord.Embed( - description=_("Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*"), + description=_( + "Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*" + ), color=self.bot.main_color, ) else: embed = discord.Embed( - description=_("Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*") + "\n\n" + - _("This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " - "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot."), + description=_( + "Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*" + ) + + "\n\n" + + _( + "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot." + ), color=self.bot.main_color, ) return await msg.edit(embed=embed) @@ -399,8 +411,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) @@ -426,7 +437,9 @@ async def update_plugin(self, ctx, plugin_name): await self.load_plugin(plugin) logger.debug("Updated %s.", plugin_name) embed = discord.Embed( - description=_("Successfully updated {plugin_name}.").format(plugin_name=plugin.name), + description=_("Successfully updated {plugin_name}.").format( + plugin_name=plugin.name + ), color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -459,8 +472,10 @@ async def plugins_loaded(self, ctx): if not self.bot.config.get("enable_plugins"): embed = discord.Embed( - description=_("No plugins are loaded due to `ENABLE_PLUGINS=false`, " - "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot."), + description=_( + "No plugins are loaded due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot." + ), color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -531,15 +546,16 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N if not index and plugin_name is not None: embed = discord.Embed( color=self.bot.error_color, - description=_('Could not find a plugin with name "{plugin_name}" within the registry.').format(plugin_name=plugin_name), + description=_( + 'Could not find a plugin with name "{plugin_name}" within the registry.' + ).format(plugin_name=plugin_name), ) matches = get_close_matches(plugin_name, self.registry.keys()) 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) @@ -559,8 +575,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 {plugin_name}```", ) embed.set_author( @@ -579,8 +594,10 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N required_version = details.get("bot_version", False) if required_version and self.bot.version < parse_version(required_version): embed.set_footer( - text=_("Your bot is unable to install this plugin, " - "minimum required version is v{required_version}.").format(required_version=required_version) + text=_( + "Your bot is unable to install this plugin, " + "minimum required version is v{required_version}." + ).format(required_version=required_version) ) else: embed.set_footer(text=_("Your bot is able to install this plugin.")) diff --git a/cogs/utility.py b/cogs/utility.py index 2704e57f1b..9974deb803 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -75,8 +75,9 @@ async def format_cog_help(self, cog, *, no_cog=False): embed.set_author(name=name + continued, icon_url=bot.user.avatar_url) embed.set_footer( - text=_('Type "{prefix}{command} command" ' - "for more info on a specific command.").format(prefix=prefix, command=self.command_attrs["name"]) + text=_( + 'Type "{prefix}{command} command" ' "for more info on a specific command." + ).format(prefix=prefix, command=self.command_attrs["name"]) ) embeds.append(embed) return embeds @@ -154,8 +155,9 @@ async def send_group_help(self, group): embed.add_field(name=_("Sub Command(s)"), value=format_[:1024], inline=False) embed.set_footer( - text=_('Type "{prefix}{command} command" ' - "for more info on a command.").format(prefix=self.clean_prefix, command=self.command_attrs["name"]) + text=_('Type "{prefix}{command} command" ' "for more info on a command.").format( + prefix=self.clean_prefix, command=self.command_attrs["name"] + ) ) await self.get_destination().send(embed=embed) @@ -165,7 +167,8 @@ async def send_error_message(self, error): val = self.context.bot.snippets.get(command) if val is not None: embed = discord.Embed( - title=_("{command} is a snippet.").format(command=command), color=self.context.bot.main_color + title=_("{command} is a snippet.").format(command=command), + color=self.context.bot.main_color, ) embed.add_field(name=f"`{command}` will send:", value=val) return await self.get_destination().send(embed=embed) @@ -178,8 +181,10 @@ async def send_error_message(self, error): embed = discord.Embed( title=_("Error"), color=self.context.bot.error_color, - description=_("Alias `{command}` is invalid, this alias will now be deleted." - "This alias will now be deleted.").format(command=command), + description=_( + "Alias `{command}` is invalid, this alias will now be deleted." + "This alias will now be deleted." + ).format(command=command), ) embed.add_field(name=f"{command}` used to be:", value=val) self.context.bot.aliases.pop(command) @@ -187,21 +192,27 @@ async def send_error_message(self, error): else: if len(values) == 1: embed = discord.Embed( - title=_("{command} is an alias.").format(command=command), color=self.context.bot.main_color + title=_("{command} is an alias.").format(command=command), + color=self.context.bot.main_color, + ) + embed.add_field( + name=_("`{command}` points to:").format(command=command), value=values[0] ) - embed.add_field(name=_("`{command}` points to:").format(command=command), value=values[0]) else: embed = discord.Embed( title=_("{command} is an alias.").format(command=command), color=self.context.bot.main_color, - description=_("**`{command}` points to the following steps:**").format(command=command), + description=_("**`{command}` points to the following steps:**").format( + command=command + ), ) for i, val in enumerate(values, start=1): embed.add_field(name=_("Step") + f" {i}:", value=val) embed.set_footer( - text=_('Type "{prefix}{command} alias" ' - "for more details on aliases.").format(prefix=self.clean_prefix, command=self.command_attrs["name"]) + text=_('Type "{prefix}{command} alias" ' "for more details on aliases.").format( + prefix=self.clean_prefix, command=self.command_attrs["name"] + ) ) return await self.get_destination().send(embed=embed) @@ -224,8 +235,9 @@ async def send_error_message(self, error): else: embed.title = _("Cannot find command or category") embed.set_footer( - text=_('Type "{prefix}{command}" ' - "for a list of all available commands.").format(prefix=self.clean_prefix, command=self.command_attrs["name"]) + text=_('Type "{prefix}{command}" ' "for a list of all available commands.").format( + prefix=self.clean_prefix, command=self.command_attrs["name"] + ) ) await self.get_destination().send(embed=embed) @@ -263,7 +275,9 @@ async def changelog(self, ctx, version: str.lower = ""): return await ctx.send( embed=discord.Embed( color=self.bot.error_color, - description=_("The specified version `{version}` could not be found.").format(version=version), + description=_("The specified version `{version}` could not be found.").format( + version=version + ), ) ) @@ -279,7 +293,9 @@ async def changelog(self, ctx, version: str.lower = ""): finally: logger.warning("Failed to display changelog.", exc_info=True) await ctx.send( - _("View the changelog here: {url}").format(url=f'{changelog.latest_version.changelog_url}#v{version[::2]}') + _("View the changelog here: {url}").format( + url=f"{changelog.latest_version.changelog_url}#v{version[::2]}" + ) ) @commands.command(aliases=["info"]) @@ -295,9 +311,11 @@ async def about(self, ctx): ) embed.set_thumbnail(url=self.bot.user.avatar_url) - desc = _("This is an open source Discord bot that serves as a means for " - "members to easily communicate with server administrators in " - "an organised manner.") + desc = _( + "This is an open source Discord bot that serves as a means for " + "members to easily communicate with server administrators in " + "an organised manner." + ) embed.description = desc embed.add_field(name=_("Uptime"), value=self.bot.uptime) @@ -312,7 +330,9 @@ async def about(self, ctx): stable = next( filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions) ) - footer = _("You are on the prerelease version • the latest version is v{version}.").format(version=stable.version) + footer = _( + "You are on the prerelease version • the latest version is v{version}." + ).format(version=stable.version) elif self.bot.version < parse_version(latest.version): footer = _("A newer version is available v{version}.").format(version=latest.version) else: @@ -320,16 +340,20 @@ async def about(self, ctx): embed.add_field( name=_("Want Modmail in Your Server?"), - value=_("Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " - "and join our [Discord server](https://discord.gg/F34cRU8/)!"), + value=_( + "Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " + "and join our [Discord server](https://discord.gg/F34cRU8/)!" + ), inline=False, ) embed.add_field( name=_("Support the Developers"), - value=_("This bot is completely free for everyone. We rely on kind individuals " - "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " - "to keep this bot free forever!"), + value=_( + "This bot is completely free for everyone. We rely on kind individuals " + "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " + "to keep this bot free forever!" + ), inline=False, ) @@ -654,14 +678,14 @@ 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( title=_("Changed mention!"), - description=_('On thread creation the bot now says "{mention}".').format(mention=mention), + description=_('On thread creation the bot now says "{mention}".').format( + mention=mention + ), color=self.bot.main_color, ) self.bot.config["mention"] = mention @@ -745,7 +769,9 @@ async def config_set(self, ctx, key: str.lower, *, value: str): embed = discord.Embed( title=_("Success"), color=self.bot.main_color, - description=_("Set `{key}` to `{value}`.").format(key=key, value=self.bot.config[key]), + description=_("Set `{key}` to `{value}`.").format( + key=key, value=self.bot.config[key] + ), ) except InvalidConfigError as exc: embed = exc.embed @@ -798,9 +824,7 @@ async def config_get(self, ctx, *, key: str.lower = None): if key in keys: desc = _("`{key}` is set to `{value}`").format(key=key, value=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( @@ -809,18 +833,17 @@ async def config_get(self, ctx, *, key: str.lower = None): description=_("`{key}` is an invalid key.").format(key=key), ) embed.set_footer( - text=_('Type "{prefix}config options" for a list of config variables.').format(prefix=self.bot.prefix) + text=_('Type "{prefix}config options" for a list of config variables.').format( + prefix=self.bot.prefix + ) ) 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) embed.set_author(name="Current config(s):", icon_url=self.bot.user.avatar_url) config = self.bot.config.filter_default(self.bot.config) @@ -872,13 +895,13 @@ def fmt(val): if current_key == key: index = i embed = discord.Embed( - title=_("Configuration description on {current_key}:").format(current_key=current_key), + title=_("Configuration description on {current_key}:").format( + current_key=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"]: @@ -936,8 +959,10 @@ async def alias(self, ctx, *, name: str.lower = None): embed = discord.Embed( title=_("Error"), color=self.bot.error_color, - description=_("Alias `{name}` is invalid, it used to be `{value}`. " - "This alias will now be deleted.").format(name=name, value=escape_markdown(val)), + description=_( + "Alias `{name}` is invalid, it used to be `{value}`. " + "This alias will now be deleted." + ).format(name=name, value=escape_markdown(val)), ) embed.add_field(name=f"{name}` used to be:", value=utils.truncate(val, 1024)) self.bot.aliases.pop(name) @@ -946,7 +971,9 @@ async def alias(self, ctx, *, name: str.lower = None): if len(values) == 1: embed = discord.Embed( - title=_("Alias") + f' - "{name}":', description=values[0], color=self.bot.main_color + title=_("Alias") + f' - "{name}":', + description=values[0], + color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -967,7 +994,9 @@ async def alias(self, ctx, *, name: str.lower = None): color=self.bot.error_color, description=_("You dont have any aliases at the moment."), ) - embed.set_footer(text=_("Do {prefix}help alias for more commands.").format(prefix=self.bot.prefix)) + embed.set_footer( + text=_("Do {prefix}help alias for more commands.").format(prefix=self.bot.prefix) + ) embed.set_author(name=_("Aliases"), icon_url=ctx.guild.icon_url) return await ctx.send(embed=embed) @@ -1024,7 +1053,10 @@ async def make_alias(self, name, value, action): embed = discord.Embed(title=_("{action} alias").format(action), color=self.bot.main_color) if not multiple_alias: - embed.add_field(name=_("`{name}` points to:").format(name=name), value=utils.truncate(values[0], 1024)) + embed.add_field( + name=_("`{name}` points to:").format(name=name), + value=utils.truncate(values[0], 1024), + ) else: embed.description = _("`{name}` now points to the following steps:").format(name=name) @@ -1041,15 +1073,15 @@ async def make_alias(self, name, value, action): embed = discord.Embed(title="Error", color=self.bot.error_color) if multiple_alias: - embed.description = ( - _("The command you are attempting to point " - "to does not exist: `{command}`.").format(command=linked_command) - ) + embed.description = _( + "The command you are attempting to point " + "to does not exist: `{command}`." + ).format(command=linked_command) else: - embed.description = ( - _("The command you are attempting to point " - "to on step {number} does not exist: `{command}`.").format(number=i, command=linked_command) - ) + embed.description = _( + "The command you are attempting to point " + "to on step {number} does not exist: `{command}`." + ).format(number=i, command=linked_command) return embed else: @@ -1082,21 +1114,27 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed = discord.Embed( title=_("Error"), color=self.bot.error_color, - description=_("A command with the same name already exists: `{name}`.").format(name=name), + description=_("A command with the same name already exists: `{name}`.").format( + name=name + ), ) elif name in self.bot.aliases: embed = discord.Embed( title=_("Error"), color=self.bot.error_color, - description=_("Another alias with the same name already exists: `{name}`.").format(name=name), + description=_("Another alias with the same name already exists: `{name}`.").format( + name=name + ), ) elif name in self.bot.snippets: embed = discord.Embed( title=_("Error"), color=self.bot.error_color, - description=_("A snippet with the same name already exists: `{name}`.").format(name=name), + description=_("A snippet with the same name already exists: `{name}`.").format( + name=name + ), ) elif len(name) > 120: @@ -1228,7 +1266,9 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name embed = discord.Embed( title=_("Error"), color=self.bot.error_color, - description=_("The referenced level does not exist: `{level}`.").format(level=level_name), + description=_("The referenced level does not exist: `{level}`.").format( + level=level_name + ), ) else: logger.info( @@ -1242,8 +1282,9 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name embed = discord.Embed( title=_("Success"), color=self.bot.main_color, - description=_("Successfully set command permission level for " - "`{command}` to `{level}`.").format(command=command.qualified_name, level=level.name), + description=_( + "Successfully set command permission level for " "`{command}` to `{level}`." + ).format(command=command.qualified_name, level=level.name), ) return await ctx.send(embed=embed) @@ -1288,7 +1329,9 @@ async def permissions_add( embed = discord.Embed( title=_("Error"), color=self.bot.error_color, - description=_("The referenced {type} does not exist: `{name}`.").format(type=type_, name=name), + description=_("The referenced {type} does not exist: `{name}`.").format( + type=type_, name=name + ), ) return await ctx.send(embed=embed) @@ -1364,8 +1407,10 @@ async def permissions_remove( embed = discord.Embed( title=_("Error"), color=self.bot.error_color, - description=_("The command permission level was never overridden: `{name}`, " - "current permission level is {perm_name}.").format(name=name, perm_name=perm.name), + description=_( + "The command permission level was never overridden: `{name}`, " + "current permission level is {perm_name}." + ).format(name=name, perm_name=perm.name), ) else: logger.info("Restored command permission level for `%s`.", name) @@ -1375,7 +1420,9 @@ async def permissions_remove( embed = discord.Embed( title=_("Success"), color=self.bot.main_color, - description=_("Command permission level for `{name}` was successfully restored to {perm_name}.").format(name=name, perm_name=perm.name), + description=_( + "Command permission level for `{name}` was successfully restored to {perm_name}." + ).format(name=name, perm_name=perm.name), ) return await ctx.send(embed=embed) @@ -1389,7 +1436,9 @@ async def permissions_remove( embed = discord.Embed( title=_("Error"), color=self.bot.error_color, - description=_("The referenced level does not exist: `{name}`.").format(name=name), + description=_("The referenced level does not exist: `{name}`.").format( + name=name + ), ) return await ctx.send(embed=embed) name = level.name @@ -1525,12 +1574,16 @@ async def permissions_get( embeds = [ discord.Embed( - title=_("{mention} has permission with the following commands:").format(mention=mention), + title=_("{mention} has permission with the following commands:").format( + mention=mention + ), description=desc_cmd, color=self.bot.main_color, ), discord.Embed( - title=_("{mention} has permission with the following permission levels:").format(mention=mention), + title=_( + "{mention} has permission with the following permission levels:" + ).format(mention=mention), description=desc_level, color=self.bot.main_color, ), @@ -1556,7 +1609,9 @@ async def permissions_get( embeds.append( discord.Embed( title=_("Permission Overrides"), - description=_("You don't have any command level overrides at the moment."), + description=_( + "You don't have any command level overrides at the moment." + ), color=self.bot.error_color, ) ) @@ -1585,14 +1640,18 @@ async def permissions_get( embed = discord.Embed( title=_("Error"), color=self.bot.error_color, - description=_("The command permission level was never overridden: `{name}`, " - "current permission level is {perm_name}.").format(name=name, perm_name=perm.name), + description=_( + "The command permission level was never overridden: `{name}`, " + "current permission level is {perm_name}." + ).format(name=name, perm_name=perm.name), ) else: embed = discord.Embed( title=_("Success"), color=self.bot.main_color, - description=_('Permission override for command "{name}" is "{perm_name}".').format(name=name, perm_name=perm.name), + description=_( + 'Permission override for command "{name}" is "{perm_name}".' + ).format(name=name, perm_name=perm.name), ) return await ctx.send(embed=embed) @@ -1674,12 +1733,12 @@ async def oauth_whitelist(self, ctx, target: Union[discord.Role, utils.User]): target = self.bot.get_user(target.id) or self.bot.modmail_guild.get_role(target.id) if removed: - embed.description = ( - _("Un-whitelisted {target_mention} to view logs.").format(target_mention=target.mention) + embed.description = _("Un-whitelisted {target_mention} to view logs.").format( + target_mention=target.mention ) else: - embed.description = ( - _("Whitelisted {target_mention} to view logs.").format(target_mention=target.mention) + embed.description = _("Whitelisted {target_mention} to view logs.").format( + target_mention=target.mention ) await ctx.send(embed=embed) @@ -1704,12 +1763,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/translations.py b/core/translations.py index 218b5f91ae..98a3327386 100644 --- a/core/translations.py +++ b/core/translations.py @@ -5,13 +5,13 @@ class Translator: def __init__(self): - self.language = os.getenv('language', 'en') + self.language = os.getenv("language", "en") self.texts = {} self.generate_texts() def generate_texts(self): - with open(f'languages/{self.language}.csv', encoding='utf8') as f: - reader = csv.reader(f, dialect='unix') + with open(f"languages/{self.language}.csv", encoding="utf8") as f: + reader = csv.reader(f, dialect="unix") for n, row in enumerate(reader): if n != 0: diff --git a/translation_files.py b/translation_files.py index 7d50c53417..07c6979f63 100644 --- a/translation_files.py +++ b/translation_files.py @@ -11,27 +11,29 @@ from cogs.utility import Utility -data = [('Identifier', 'English', 'Context')] +data = [("Identifier", "English", "Context")] all_identifiers = [] identifiers = {} class FormatError(Exception): def __init__(self, reason, string): - super().__init__(f'Unable to parse {reason}: {string}') + super().__init__(f"Unable to parse {reason}: {string}") -for filename in glob.glob('**/*.py') + glob.glob('*.py'): - if filename == 'translation_files.py': +for filename in glob.glob("**/*.py") + glob.glob("*.py"): + if filename == "translation_files.py": continue - with open(filename, encoding='utf8') as f: + with open(filename, encoding="utf8") as f: filedata = f.read() - regex_matches = re.findall(r'(?:^|[^A-z])_\(.+?(?:\'|\")\)+?', filedata, flags=re.DOTALL | re.MULTILINE) + regex_matches = re.findall( + r"(?:^|[^A-z])_\(.+?(?:\'|\")\)+?", filedata, flags=re.DOTALL | re.MULTILINE + ) for i in regex_matches: if "f'" in i or 'f"' in i: - print(FormatError('f-string', i)) - identifier = '' + print(FormatError("f-string", i)) + identifier = "" read = False ignore_inverted = False newline = False @@ -48,7 +50,7 @@ def __init__(self, reason, string): read = not read triggered = True - if x == '\n': + if x == "\n": newline = True if read and not triggered: @@ -62,7 +64,7 @@ def __init__(self, reason, string): else: counter = 0 - if x == '\\': + if x == "\\": ignore_inverted = True elif ignore_inverted: ignore_inverted = False @@ -81,16 +83,16 @@ def __init__(self, reason, string): if identifier in identifiers.keys(): if filename not in data[identifiers[identifier]][2]: - data[identifiers[identifier]][2] += f' {filename}/L{linenum}' + data[identifiers[identifier]][2] += f" {filename}/L{linenum}" elif str(linenum) not in data[identifiers[identifier]][2]: - split_space = data[identifiers[identifier]][2].split(' ') + split_space = data[identifiers[identifier]][2].split(" ") for nx, x in enumerate(split_space): if filename in x: - split_space[nx] += f'/L{linenum}' + split_space[nx] += f"/L{linenum}" break - data[identifiers[identifier]][2] = (' ').join(split_space) + data[identifiers[identifier]][2] = (" ").join(split_space) else: - data.append([identifier, identifier, f'File: {filename}/L{linenum}']) + data.append([identifier, identifier, f"File: {filename}/L{linenum}"]) identifiers[identifier] = len(data) - 1 print(filename) @@ -101,17 +103,25 @@ def __init__(self, reason, string): cogs = [Modmail(bot), Plugins(bot), Utility(bot)] for i in cogs: if i.description: - data.append([i.description, i.description, f'Cog: {i.__cog_name__}']) + data.append([i.description, i.description, f"Cog: {i.__cog_name__}"]) for cmd in i.walk_commands(): if cmd not in done: if cmd.short_doc: print(cmd) - data.append([cmd.short_doc, cmd.short_doc, f'Cog: {i.__cog_name__}\nCommand: {cmd.qualified_name}']) - data.append([cmd.help, cmd.help, f'Cog: {i.__cog_name__}\nCommand: {cmd.qualified_name}']) + data.append( + [ + cmd.short_doc, + cmd.short_doc, + f"Cog: {i.__cog_name__}\nCommand: {cmd.qualified_name}", + ] + ) + data.append( + [cmd.help, cmd.help, f"Cog: {i.__cog_name__}\nCommand: {cmd.qualified_name}"] + ) done.add(cmd) -with open('languages/en.csv', 'w+') as f: - csv.writer(f, dialect='unix').writerows(data) +with open("languages/en.csv", "w+") as f: + csv.writer(f, dialect="unix").writerows(data) From a27f8bcf988f0d0cc3f7474bce32cb68af4394a8 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 8 Jul 2020 23:30:30 -0700 Subject: [PATCH 0573/1221] dpy 1.3.4 --- Pipfile | 2 +- Pipfile.lock | 49 ++++++++++++++++++++++---------------------- requirements.min.txt | 4 ++-- runtime.txt | 2 +- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/Pipfile b/Pipfile index 16bbac8e49..7a51fffd39 100644 --- a/Pipfile +++ b/Pipfile @@ -21,7 +21,7 @@ parsedatetime = "==2.6" aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = "==1.3.3" +"discord.py" = "==1.3.4" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 9a650f4af4..57570d4062 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bfb85b7c7862e17e905d739f0fb63248d3bf446035f32e3afac619f6d2c5a0ba" + "sha256": "2d28437167e3e207b978644be0e6cacbd6b46faa445da13b6f27b6fe630aac28" }, "pipfile-spec": 6, "requires": { @@ -59,10 +59,10 @@ }, "certifi": { "hashes": [ - "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1", - "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" ], - "version": "==2020.4.5.2" + "version": "==2020.6.20" }, "chardet": { "hashes": [ @@ -81,17 +81,18 @@ }, "discord.py": { "hashes": [ - "sha256:406871b06d86c3dc49fba63238519f28628dac946fef8a0e22988ff58ec05580", - "sha256:ad00e34c72d2faa8db2157b651d05f3c415d7d05078e7e41dc9e8dc240051beb" + "sha256:1b546a32c0cd83d949392a71e5b06e30e19d1067246e3826d32ae9b8b3d06c1e", + "sha256:8ef58d6fc1e66903bc00ae79c4c09a38aa71043e88a83da4d2e8b9b1c9f9b9e2" ], "index": "pypi", - "version": "==1.3.3" + "version": "==1.3.4" }, "distlib": { "hashes": [ - "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" + "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", + "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" ], - "version": "==0.3.0" + "version": "==0.3.1" }, "dnspython": { "hashes": [ @@ -117,19 +118,19 @@ }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9" + "version": "==2.10" }, "importlib-metadata": { "hashes": [ - "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545", - "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958" + "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", + "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" ], "markers": "python_version < '3.8'", - "version": "==1.6.1" + "version": "==1.7.0" }, "isodate": { "hashes": [ @@ -265,11 +266,11 @@ }, "python-dotenv": { "hashes": [ - "sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7", - "sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74" + "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", + "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" ], "index": "pypi", - "version": "==0.13.0" + "version": "==0.14.0" }, "six": { "hashes": [ @@ -297,11 +298,11 @@ }, "virtualenv": { "hashes": [ - "sha256:a116629d4e7f4d03433b8afa27f43deba09d48bc48f5ecefa4f015a178efb6cf", - "sha256:a730548b27366c5e6cbdf6f97406d861cccece2e22275e8e1a757aeff5e00c70" + "sha256:c11a475400e98450403c0364eb3a2d25d42f71cf1493da64390487b666de4324", + "sha256:e10cc66f40cbda459720dfe1d334c4dc15add0d80f09108224f171006a97a172" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.0.21" + "version": "==20.0.26" }, "virtualenv-clone": { "hashes": [ @@ -559,11 +560,11 @@ }, "stevedore": { "hashes": [ - "sha256:001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9", - "sha256:471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691" + "sha256:609912b87df5ad338ff8e44d13eaad4f4170a65b79ae9cb0aa5632598994a1b7", + "sha256:c4724f8d7b8f6be42130663855d01a9c2414d6046055b5a65ab58a0e38637688" ], "markers": "python_version >= '3.6'", - "version": "==2.0.0" + "version": "==2.0.1" }, "toml": { "hashes": [ diff --git a/requirements.min.txt b/requirements.min.txt index e3ba980fc1..28550f1e50 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -6,7 +6,7 @@ aiohttp==3.6.2 async-timeout==3.0.1 attrs==19.3.0 chardet==3.0.4 -discord.py==1.3.3 +discord.py==1.3.4 dnspython==1.16.0 emoji==0.5.4 future==0.18.2 @@ -18,7 +18,7 @@ natural==0.2.0 parsedatetime==2.6 pymongo==3.10.1 python-dateutil==2.8.1 -python-dotenv==0.13.0 +python-dotenv==0.14.0 six==1.15.0 websockets==8.1 yarl==1.4.2 diff --git a/runtime.txt b/runtime.txt index 257b314f5f..05802392e0 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.6 \ No newline at end of file +python-3.7.7 \ No newline at end of file From 86f959c443e3d679d9a03707771aca0b1a180696 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 8 Jul 2020 23:38:30 -0700 Subject: [PATCH 0574/1221] Use py 3.7.8 --- runtime.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime.txt b/runtime.txt index 05802392e0..423adc28cf 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.7 \ No newline at end of file +python-3.7.8 \ No newline at end of file From 53a5176a58d22ef96e529019b0313902bd129730 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 8 Jul 2020 23:59:55 -0700 Subject: [PATCH 0575/1221] Freeze into requirements.txt --- pyproject.toml | 6 +++--- requirements.txt | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 requirements.txt diff --git a/pyproject.toml b/pyproject.toml index 54f4efb4e7..b8547d752b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,9 +36,9 @@ keywords = ['discord', 'modmail'] [tool.poetry.dependencies] python = "^3.7" -"discord.py" = "=1.3.3" -uvloop = {version=">=0.12.0", sys_platform = "!= 'win32'"} -python-dotenv = "^0.10.3" +"discord.py" = "=1.3.4" +uvloop = {version = ">=0.12.0", sys_platform = "!= 'win32'"} +python-dotenv = ">=0.10.3" parsedatetime = "^2.6" dnspython = "^1.16" isodate = "^0.6.0" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..4df684c56d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,37 @@ +aiohttp==3.6.2 +appdirs==1.4.4 +async-timeout==3.0.1 +attrs==19.3.0 +certifi==2020.6.20 +chardet==3.0.4 +colorama==0.4.3 +dadjokes==1.3 +discord.py==1.3.4 +distlib==0.3.1 +dnspython==1.16.0 +emoji==0.5.4 +filelock==3.0.12 +idna==2.10 +importlib-metadata==1.7.0 +isodate==0.6.0 +motor==2.1.0 +multidict==4.7.6 +natural==0.2.0 +parsedatetime==2.6 +pipenv==2020.6.2 +pymongo==3.10.1 +python-box==4.2.3 +python-dateutil==2.8.1 +python-dotenv==0.14.0 +requests==2.24.0 +ruamel.yaml==0.16.10 +ruamel.yaml.clib==0.2.0 +six==1.15.0 +toml==0.10.1 +urllib3==1.25.9 +uvloop==0.14.0 +virtualenv==20.0.26 +virtualenv-clone==0.5.4 +websockets==8.1 +yarl==1.4.2 +zipp==3.1.0 From e849892a0112c040b820242277332a30b8ab1e3f Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 9 Jul 2020 00:01:52 -0700 Subject: [PATCH 0576/1221] Dont use pipenv in procfile --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Procfile b/Procfile index 5ae4640def..29cff6d9d1 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -worker: pipenv run bot +worker: python bot.py From 494abba75bdfc2298ed2e8d70a79d2a60ba078a6 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 9 Jul 2020 00:04:14 -0700 Subject: [PATCH 0577/1221] Done --- requirements.txt | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4df684c56d..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,37 +0,0 @@ -aiohttp==3.6.2 -appdirs==1.4.4 -async-timeout==3.0.1 -attrs==19.3.0 -certifi==2020.6.20 -chardet==3.0.4 -colorama==0.4.3 -dadjokes==1.3 -discord.py==1.3.4 -distlib==0.3.1 -dnspython==1.16.0 -emoji==0.5.4 -filelock==3.0.12 -idna==2.10 -importlib-metadata==1.7.0 -isodate==0.6.0 -motor==2.1.0 -multidict==4.7.6 -natural==0.2.0 -parsedatetime==2.6 -pipenv==2020.6.2 -pymongo==3.10.1 -python-box==4.2.3 -python-dateutil==2.8.1 -python-dotenv==0.14.0 -requests==2.24.0 -ruamel.yaml==0.16.10 -ruamel.yaml.clib==0.2.0 -six==1.15.0 -toml==0.10.1 -urllib3==1.25.9 -uvloop==0.14.0 -virtualenv==20.0.26 -virtualenv-clone==0.5.4 -websockets==8.1 -yarl==1.4.2 -zipp==3.1.0 From 178ea9c0ac56a345cf1ddea48e3bcc1524f83136 Mon Sep 17 00:00:00 2001 From: Taaku18 <45324516+Taaku18@users.noreply.github.com> Date: Thu, 9 Jul 2020 00:12:53 -0700 Subject: [PATCH 0578/1221] Dump 3.5.0 --- CHANGELOG.md | 7 +++++-- bot.py | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0956794786..9d3d127944 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 do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.5.0dev5 +# v3.5.0 + +Fixed discord.py issue. ### Added @@ -23,7 +25,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Deleting channel manually will now close the thread. - Deleting messages will no longer cause the bot to produce warnings. - Plugins will automatically be removed when it fails to load. -- Moved all database-related activities to clients.py under MongoDBClient, with possible future hook for additional database support. +- Moved all database-related activities to clients.py under MongoDBClient, with possible future hook for additional database support. +- `bot.db` is deprecated in favour of `bot.api.db` and will be removed in the future. - Deprecated `bot.plugin_db.get_partition` in favour of `bot.api.get_plugin_partition` (not final). - Deprecated `MONGO_URI` config var (but will keep support in the future) in favour of `CONNECTION_URI` and `DATABASE_TYPE`. Right now there is one supported database - "mongodb", which is the default. diff --git a/bot.py b/bot.py index bba04ff50d..9e48c0f342 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.5.0-dev5" +__version__ = "3.5.0" import asyncio @@ -148,6 +148,11 @@ def api(self) -> ApiClient: raise RuntimeError return self._api + @property + def db(self): + # deprecated + return self.api.db + async def get_prefix(self, message=None): return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] From 846dfbd0ca44c66165ea33cf83bb96bc2ef0ae2b Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Thu, 9 Jul 2020 06:24:58 -0700 Subject: [PATCH 0579/1221] Eval was disabled by default by accident, reverted --- core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.py b/core/config.py index f945557718..4f54c2b29c 100644 --- a/core/config.py +++ b/core/config.py @@ -113,7 +113,7 @@ class ConfigManager: # bot "token": None, "enable_plugins": True, - "enable_eval": False, + "enable_eval": True, # github access token for private repositories "github_token": None, # Logging From 8db53571d66d72e7c3a4c5481afb9ed1f5b74b6d Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 10 Jul 2020 00:09:45 +0200 Subject: [PATCH 0580/1221] run this when you use pm2 on your vps --- modmail.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 modmail.sh diff --git a/modmail.sh b/modmail.sh new file mode 100644 index 0000000000..2dbc7218ff --- /dev/null +++ b/modmail.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +pipenv run python3 bot.py \ No newline at end of file From 1b75995913db9aa9349facf1607c19381b16f8f2 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 10 Jul 2020 00:10:05 +0200 Subject: [PATCH 0581/1221] Update .env.example --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index b3554bf9c9..14bdf060bf 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,4 @@ TOKEN=MyBotToken LOG_URL=https://logviewername.herokuapp.com/ GUILD_ID=1234567890 OWNERS=Owner1ID,Owner2ID,Owner3ID -MONGO_URI=mongodb+srv://mongodburi +CONNECTION_URI=mongodb+srv://mongodburi From cc4ba8b788277ca1d7c40afb52d88ec9907a4311 Mon Sep 17 00:00:00 2001 From: kuro <46526001+WebKide@users.noreply.github.com> Date: Thu, 23 Jul 2020 20:57:45 +0100 Subject: [PATCH 0582/1221] added translate :wave: this command conflicts with Translator... --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index b15bb374dc..b36f89feec 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -222,5 +222,14 @@ "title": "Slow Mode", "icon_url": "https://cdn.discordapp.com/attachments/717029057635549274/717033838966210601/Slow_mode_-_icon.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/717029057635549274/717029110907666482/Slow_mode_plugin_-_thumbnail.png" + }, + "translate": { + "repository": "WebKide/modmail-plugins", + "branch": "master", + "description": "(∩`-´)⊃━☆゚.*・。゚ translate text from one language to another (defaults to English)\n\nGet full list of available languages at: https://github.com/WebKide/modmail-plugins/blob/master/translate/langs.json\n\nThis command conflicts with Translator-plugin", + "bot_version": "3.5.0", + "title": "Translate", + "icon_url": "https://i.imgur.com/yeHFKgl.png", + "thumbnail_url": "https://i.imgur.com/yeHFKgl.png" } } From 7100d9755a78372444ce050770bfa5a5a63404dd Mon Sep 17 00:00:00 2001 From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com> Date: Thu, 6 Aug 2020 19:16:55 +0600 Subject: [PATCH 0583/1221] Add publish plugin to registry (#2835) Update registry.json --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index b15bb374dc..6e9ada2150 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -222,5 +222,14 @@ "title": "Slow Mode", "icon_url": "https://cdn.discordapp.com/attachments/717029057635549274/717033838966210601/Slow_mode_-_icon.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/717029057635549274/717029110907666482/Slow_mode_plugin_-_thumbnail.png" + }, + "publish": { + "repository": "codeinteger6/modmail-plugins", + "branch": "master", + "description": "Publish messages sent in announcement channels.", + "bot_version": "3.5.0", + "title": "Publish", + "icon_url": "https://user-images.githubusercontent.com/44692189/89184422-96de3600-d5ba-11ea-98ea-d096aa385ad5.png", + "thumbnail_url": "https://user-images.githubusercontent.com/44692189/89184422-96de3600-d5ba-11ea-98ea-d096aa385ad5.png" } } From c836787ad3494f2ac7290123bb617f53331fcf95 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 7 Aug 2020 03:06:24 +0200 Subject: [PATCH 0584/1221] Update README.md (#2837) and Add music plugin --- README.md | 2 +- cogs/modmail.py | 4 ++-- plugins/registry.json | 13 +++++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4b36c5a3d2..76c2a4f107 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
diff --git a/cogs/modmail.py b/cogs/modmail.py index 5c4de73e26..9ec5d5604f 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -540,7 +540,7 @@ 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 subscribed to this thread.", ) else: mentions.remove(mention) @@ -1355,7 +1355,7 @@ async def isenable(self, ctx): else: embed = discord.Embed( title="Enabled", - description="Modmail is accepting all DM messages.", + description="Modmail now is accepting all DM messages.", color=self.bot.main_color, ) diff --git a/plugins/registry.json b/plugins/registry.json index 6e9ada2150..5a12d84191 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -7,8 +7,17 @@ "title": "Profanity Filter Plugin", "icon_url": "https://i.imgur.com/951szZ3.jpg", "thumbnail_url": "https://i.imgur.com/951szZ3.jpg" - }, - "dragory-migrate": { + }, + "music": { + "repository": "lorenzo132/modmail-plugins", + "branch": "master", + "description": "Play your favourite music on your bot! Note: Only works on VPS follow this guide: https://gist.github.com/lorenzo132/5ef328e5dfcfaec19cb81dc7a63eaffa", + "bot_version": "2.20.1", + "title": "Music", + "icon_url": "https://i.imgur.com/R2olclk.png", + "thumbnail_url": "https://i.imgur.com/xuoQjPu.gif" + }, + "dragory-migrate": { "repository": "kyb3r/modmail-plugins", "branch": "master", "description": "Migrate your logs from Dragory's modmail bot to this one with a simple command. Added at the request of users.", From c954dbab43c8b923a1f4a4b221f8217bd25e8114 Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sun, 9 Aug 2020 13:18:42 -0400 Subject: [PATCH 0585/1221] Remove stale GitHub workflow --- .github/workflows/stale.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index d0811bfd30..0000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Close Stale Issues" - -on: - schedule: - - cron: "0 0 * * *" - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - 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: 'priority: high,approved,security,bug' From a61f431e546ede1e5be20644a4aee01817a4e372 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 21 Aug 2020 23:23:56 +0800 Subject: [PATCH 0586/1221] Deps --- Pipfile | 4 +- Pipfile.lock | 374 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 225 insertions(+), 153 deletions(-) diff --git a/Pipfile b/Pipfile index 7a51fffd39..0872abf3e2 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,7 @@ bandit = "==1.6.2" colorama = ">=0.4.0" python-dateutil = ">=2.7.0" emoji = ">=0.2" -uvloop = {version=">=0.12.0", sys_platform = "!= 'win32'"} +uvloop = {version = ">=0.12.0",sys_platform = "!= 'win32'"} motor = ">=2.0.0" natural = "==0.2.0" isodate = ">=0.6.0" @@ -22,6 +22,8 @@ aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" "discord.py" = "==1.3.4" +pynacl = "*" +cffi = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 57570d4062..807d74cb97 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2d28437167e3e207b978644be0e6cacbd6b46faa445da13b6f27b6fe630aac28" + "sha256": "397b06fdc5c4560667c7d3b2b8323ffd6ac3961fc561a470ccf99d8a5c234ffb" }, "pipfile-spec": 6, "requires": { @@ -46,16 +46,14 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", + "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==19.3.0" + "version": "==20.1.0" }, "certifi": { "hashes": [ @@ -64,6 +62,40 @@ ], "version": "==2020.6.20" }, + "cffi": { + "hashes": [ + "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e", + "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c", + "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e", + "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1", + "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4", + "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2", + "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c", + "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0", + "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798", + "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1", + "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4", + "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731", + "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4", + "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c", + "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487", + "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e", + "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f", + "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123", + "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c", + "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b", + "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650", + "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad", + "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75", + "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82", + "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7", + "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15", + "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa", + "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281" + ], + "index": "pypi", + "version": "==1.14.2" + }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", @@ -104,10 +136,10 @@ }, "emoji": { "hashes": [ - "sha256:60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174" + "sha256:e42da4f8d648f8ef10691bc246f682a1ec6b18373abfd9be10ec0b398823bd11" ], "index": "pypi", - "version": "==0.5.4" + "version": "==0.6.0" }, "filelock": { "hashes": [ @@ -121,7 +153,6 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "importlib-metadata": { @@ -142,12 +173,10 @@ }, "motor": { "hashes": [ - "sha256:599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909", - "sha256:756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4", - "sha256:97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a" + "sha256:659ad13c2e2dca19807fbb6d2bb62e4c60f99bb0d43110a6abb8fdd12b644a64" ], "index": "pypi", - "version": "==2.1.0" + "version": "==2.2.0" }, "multidict": { "hashes": [ @@ -169,7 +198,6 @@ "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], - "markers": "python_version >= '3.5'", "version": "==4.7.6" }, "natural": { @@ -189,72 +217,98 @@ }, "pipenv": { "hashes": [ - "sha256:7dd49a7345fa5da7843907bab42a996dece474e45dd309dbd7619739dc60478b", - "sha256:cc289dc78feaa14def4e1723b0b4d85ff628c8e5f740eeeb68197b90dafa8dff" + "sha256:448ac3a36443db633d52a2359cac15ecbc4f429eab4ddd420697602b721d1c5a", + "sha256:eff0e10eadb330f612edfa5051d3d8e775e9e0e918c3c50361da703bd0daa035" ], "index": "pypi", - "version": "==2020.6.2" + "version": "==2020.8.13" + }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "version": "==2.20" }, "pymongo": { "hashes": [ - "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", - "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", - "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", - "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", - "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", - "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103", - "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", - "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", - "sha256:2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", - "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", - "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", - "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", - "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", - "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", - "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", - "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", - "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", - "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", - "sha256:4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", - "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", - "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", - "sha256:56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", - "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", - "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", - "sha256:619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", - "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", - "sha256:63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113", - "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", - "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", - "sha256:7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae", - "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", - "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", - "sha256:993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", - "sha256:9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", - "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", - "sha256:a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", - "sha256:a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", - "sha256:a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", - "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", - "sha256:ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012", - "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", - "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", - "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", - "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", - "sha256:bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a", - "sha256:c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", - "sha256:c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", - "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", - "sha256:c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", - "sha256:da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", - "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", - "sha256:e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", - "sha256:e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", - "sha256:e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", - "sha256:f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a", - "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606" - ], - "version": "==3.10.1" + "sha256:03dc64a9aa7a5d405aea5c56db95835f6a2fa31b3502c5af1760e0e99210be30", + "sha256:05fcc6f9c60e6efe5219fbb5a30258adb3d3e5cbd317068f3d73c09727f2abb6", + "sha256:076a7f2f7c251635cf6116ac8e45eefac77758ee5a77ab7bd2f63999e957613b", + "sha256:137e6fa718c7eff270dbd2fc4b90d94b1a69c9e9eb3f3de9e850a7fd33c822dc", + "sha256:1f865b1d1c191d785106f54df9abdc7d2f45a946b45fd1ea0a641b4f982a2a77", + "sha256:213c445fe7e654621c6309e874627c35354b46ef3ee807f5a1927dc4b30e1a67", + "sha256:25e617daf47d8dfd4e152c880cd0741cbdb48e51f54b8de9ddbfe74ecd87dd16", + "sha256:3d9bb1ba935a90ec4809a8031efd988bdb13cdba05d9e9a3e9bf151bf759ecde", + "sha256:40696a9a53faa7d85aaa6fd7bef1cae08f7882640bad08c350fb59dee7ad069b", + "sha256:421aa1b92c291c429668bd8d8d8ec2bd00f183483a756928e3afbf2b6f941f00", + "sha256:4437300eb3a5e9cc1a73b07d22c77302f872f339caca97e9bf8cf45eca8fa0d2", + "sha256:455f4deb00158d5ec8b1d3092df6abb681b225774ab8a59b3510293b4c8530e3", + "sha256:475a34a0745c456ceffaec4ce86b7e0983478f1b6140890dff7b161e7bcd895b", + "sha256:4797c0080f41eba90404335e5ded3aa66731d303293a675ff097ce4ea3025bb9", + "sha256:4ae23fbbe9eadf61279a26eba866bbf161a6f7e2ffad14a42cf20e9cb8e94166", + "sha256:4b32744901ee9990aa8cd488ec85634f443526def1e5190a407dc107148249d7", + "sha256:50127b13b38e8e586d5e97d342689405edbd74ad0bd891d97ee126a8c7b6e45f", + "sha256:50531caa7b4be1c4ed5e2d5793a4e51cc9bd62a919a6fd3299ef7c902e206eab", + "sha256:68220b81850de8e966d4667d5c325a96c6ac0d6adb3d18935d6e3d325d441f48", + "sha256:689142dc0c150e9cb7c012d84cac2c346d40beb891323afb6caf18ec4caafae0", + "sha256:6a15e2bee5c4188369a87ed6f02de804651152634a46cca91966a11c8abd2550", + "sha256:7122ffe597b531fb065d3314e704a6fe152b81820ca5f38543e70ffcc95ecfd4", + "sha256:7307024b18266b302f4265da84bb1effb5d18999ef35b30d17592959568d5c0a", + "sha256:7a4a6f5b818988a3917ec4baa91d1143242bdfece8d38305020463955961266a", + "sha256:83c5a3ecd96a9f3f11cfe6dfcbcec7323265340eb24cc996acaecea129865a3a", + "sha256:890b0f1e18dbd898aeb0ab9eae1ab159c6bcbe87f0abb065b0044581d8614062", + "sha256:8deda1f7b4c03242f2a8037706d9584e703f3d8c74d6d9cac5833db36fe16c42", + "sha256:8ea13d0348b4c96b437d944d7068d59ed4a6c98aaa6c40d8537a2981313f1c66", + "sha256:91e96bf85b7c07c827d339a386e8a3cf2e90ef098c42595227f729922d0851df", + "sha256:96782ebb3c9e91e174c333208b272ea144ed2a684413afb1038e3b3342230d72", + "sha256:9755c726aa6788f076114dfdc03b92b03ff8860316cca00902cce88bcdb5fedd", + "sha256:9dbab90c348c512e03f146e93a5e2610acec76df391043ecd46b6b775d5397e6", + "sha256:9ee0eef254e340cc11c379f797af3977992a7f2c176f1a658740c94bf677e13c", + "sha256:9fc17fdac8f1973850d42e51e8ba6149d93b1993ed6768a24f352f926dd3d587", + "sha256:a2787319dc69854acdfd6452e6a8ba8f929aeb20843c7f090e04159fc18e6245", + "sha256:b7c522292407fa04d8195032493aac937e253ad9ae524aab43b9d9d242571f03", + "sha256:bd312794f51e37dcf77f013d40650fe4fbb211dd55ef2863839c37480bd44369", + "sha256:c0d660a186e36c526366edf8a64391874fe53cf8b7039224137aee0163c046df", + "sha256:c4869141e20769b65d2d72686e7a7eb141ce9f3168106bed3e7dcced54eb2422", + "sha256:cc4057f692ac35bbe82a0a908d42ce3a281c9e913290fac37d7fa3bd01307dfb", + "sha256:cccf1e7806f12300e3a3b48f219e111000c2538483e85c869c35c1ae591e6ce9", + "sha256:ce208f80f398522e49d9db789065c8ad2cd37b21bd6b23d30053474b7416af11", + "sha256:d0565481dc196986c484a7fb13214fc6402201f7fb55c65fd215b3324962fe6c", + "sha256:d1b3366329c45a474b3bbc9b9c95d4c686e03f35da7fd12bc144626d1f2a7c04", + "sha256:d226e0d4b9192d95079a9a29c04dd81816b1ce8903b8c174a39224fe978547cb", + "sha256:d38b35f6eef4237b1d0d8e845fc1546dad85c55eba447e28c211da8c7ef9697c", + "sha256:d64c98277ea80e4484f1332ab107e8dfd173a7dcf1bdbf10a9cccc97aaab145f", + "sha256:d9de8427a5601799784eb0e7fa1b031aa64086ce04de29df775a8ca37eedac41", + "sha256:e6a15cf8f887d9f578dd49c6fb3a99d53e1d922fdd67a245a67488d77bf56eb2", + "sha256:e8c446882cbb3774cd78c738c9f58220606b702b7c1655f1423357dc51674054", + "sha256:e8d188ee39bd0ffe76603da887706e4e7b471f613625899ddf1e27867dc6a0d3", + "sha256:ef76535776c0708a85258f6dc51d36a2df12633c735f6d197ed7dfcaa7449b99", + "sha256:f6efca006a81e1197b925a7d7b16b8f61980697bb6746587aad8842865233218" + ], + "version": "==3.11.0" + }, + "pynacl": { + "hashes": [ + "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4", + "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4", + "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574", + "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d", + "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25", + "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f", + "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505", + "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122", + "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7", + "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420", + "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f", + "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96", + "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6", + "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514", + "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff", + "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80" + ], + "index": "pypi", + "version": "==1.4.0" }, "python-dateutil": { "hashes": [ @@ -277,9 +331,17 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, + "typing-extensions": { + "hashes": [ + "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", + "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", + "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" + ], + "markers": "python_version < '3.8'", + "version": "==3.7.4.2" + }, "uvloop": { "hashes": [ "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", @@ -298,18 +360,16 @@ }, "virtualenv": { "hashes": [ - "sha256:c11a475400e98450403c0364eb3a2d25d42f71cf1493da64390487b666de4324", - "sha256:e10cc66f40cbda459720dfe1d334c4dc15add0d80f09108224f171006a97a172" + "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", + "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.0.26" + "version": "==20.0.31" }, "virtualenv-clone": { "hashes": [ "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.5.4" }, "websockets": { @@ -337,38 +397,35 @@ "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" ], - "markers": "python_full_version >= '3.6.1'", "version": "==8.1" }, "yarl": { "hashes": [ - "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" - ], - "markers": "python_version >= '3.5'", - "version": "==1.4.2" + "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", + "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", + "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", + "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", + "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", + "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", + "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", + "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", + "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", + "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", + "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", + "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", + "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", + "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", + "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", + "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", + "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + ], + "version": "==1.5.1" }, "zipp": { "hashes": [ "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" ], - "markers": "python_version >= '3.6'", "version": "==3.1.0" } }, @@ -385,16 +442,14 @@ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], - "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", + "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==19.3.0" + "version": "==20.1.0" }, "bandit": { "hashes": [ @@ -417,32 +472,44 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "index": "pypi", + "version": "==0.4.3" + }, "gitdb": { "hashes": [ "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], - "markers": "python_version >= '3.4'", "version": "==4.0.5" }, "gitpython": { "hashes": [ - "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a", - "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac" + "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", + "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" + ], + "version": "==3.1.7" + }, + "importlib-metadata": { + "hashes": [ + "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", + "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" ], - "markers": "python_version >= '3.4'", - "version": "==3.1.3" + "markers": "python_version < '3.8'", + "version": "==1.7.0" }, "isort": { "hashes": [ - "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", - "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + "sha256:60a1b97e33f61243d12647aaaa3e6cc6778f5eb9f42997650f1cc975b6008750", + "sha256:d488ba1c5a2db721669cc180180d5acf84ebdc5af7827f7aaeaa75f73cf0e2b8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.3.21" + "version": "==5.4.2" }, "lazy-object-proxy": { "hashes": [ @@ -468,7 +535,6 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -494,11 +560,11 @@ }, "pylint": { "hashes": [ - "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", - "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" + "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", + "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" ], "index": "pypi", - "version": "==2.5.3" + "version": "==2.6.0" }, "pyyaml": { "hashes": [ @@ -518,36 +584,35 @@ }, "regex": { "hashes": [ - "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a", - "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938", - "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29", - "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae", - "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387", - "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a", - "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf", - "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610", - "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9", - "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5", - "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3", - "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89", - "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded", - "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754", - "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f", - "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868", - "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd", - "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910", - "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3", - "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac", - "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c" - ], - "version": "==2020.6.8" + "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", + "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", + "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", + "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", + "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", + "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", + "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", + "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", + "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", + "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", + "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", + "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", + "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", + "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", + "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", + "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", + "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", + "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", + "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", + "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", + "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + ], + "version": "==2020.7.14" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "smmap": { @@ -555,16 +620,14 @@ "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "stevedore": { "hashes": [ - "sha256:609912b87df5ad338ff8e44d13eaad4f4170a65b79ae9cb0aa5632598994a1b7", - "sha256:c4724f8d7b8f6be42130663855d01a9c2414d6046055b5a65ab58a0e38637688" + "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5", + "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "version": "==3.2.0" }, "toml": { "hashes": [ @@ -597,7 +660,7 @@ "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], - "markers": "python_version < '3.8' and implementation_name == 'cpython'", + "markers": "implementation_name == 'cpython' and python_version < '3.8'", "version": "==1.4.1" }, "wrapt": { @@ -605,6 +668,13 @@ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], "version": "==1.12.1" + }, + "zipp": { + "hashes": [ + "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", + "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + ], + "version": "==3.1.0" } } } From 53ae48c69d7a4d65dc1e3777ef5b57859346b1bf Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 21 Aug 2020 23:25:59 +0800 Subject: [PATCH 0587/1221] Deps --- runtime.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime.txt b/runtime.txt index 423adc28cf..050dcb9387 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.8 \ No newline at end of file +python-3.7.9 \ No newline at end of file From e88c5755b1b614658924bb557c345a955d17c7d0 Mon Sep 17 00:00:00 2001 From: Vincysuper07 <52707876+Vincysuper07@users.noreply.github.com> Date: Sun, 30 Aug 2020 09:01:40 +0200 Subject: [PATCH 0588/1221] Add a "no command description" in the help command. --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index bd04bd6a09..b5be17fe6d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -47,7 +47,7 @@ async def format_cog_help(self, cog, *, no_cog=False): else: format_ = f"`[{perm_level}] {prefix + cmd.qualified_name}` " - format_ += f"- {cmd.short_doc}\n" + format_ += f"- {cmd.short_doc}\n" if not cmd.short_doc=="" else "- No description." if not format_.strip(): continue if len(format_) + len(formats[-1]) >= 1024: From 84adf5474c715466b6b7f2ef486f20e7c6465318 Mon Sep 17 00:00:00 2001 From: Vincysuper07 <52707876+Vincysuper07@users.noreply.github.com> Date: Sun, 30 Aug 2020 09:08:02 +0200 Subject: [PATCH 0589/1221] Reformatting... --- cogs/utility.py | 202 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 150 insertions(+), 52 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index b5be17fe6d..d2d78fb9ed 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -47,7 +47,11 @@ async def format_cog_help(self, cog, *, no_cog=False): else: format_ = f"`[{perm_level}] {prefix + cmd.qualified_name}` " - format_ += f"- {cmd.short_doc}\n" if not cmd.short_doc=="" else "- No description." + format_ += ( + f"- {cmd.short_doc}\n" + if not cmd.short_doc == "" + else "- No description." + ) if not format_.strip(): continue if len(format_) + len(formats[-1]) >= 1024: @@ -67,7 +71,11 @@ 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( @@ -88,7 +96,11 @@ 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) @@ -97,12 +109,16 @@ 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): @@ -183,7 +199,8 @@ async def send_error_message(self, error): else: if len(values) == 1: embed = discord.Embed( - title=f"{command} is an alias.", color=self.context.bot.main_color + title=f"{command} is an alias.", + color=self.context.bot.main_color, ) embed.add_field(name=f"`{command}` points to:", value=values[0]) else: @@ -214,7 +231,9 @@ async def send_error_message(self, error): closest = get_close_matches(command, choices) if closest: - embed.add_field(name="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( @@ -307,11 +326,12 @@ 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) - ) - footer = ( - f"You are on the prerelease version • the latest version is v{stable.version}." + filter( + lambda v: not parse_version(v.version).is_prerelease, + changelog.versions, + ) ) + footer = f"You are on the prerelease version • the latest version is v{stable.version}." elif self.bot.version < parse_version(latest.version): footer = f"A newer version is available v{latest.version}." else: @@ -366,7 +386,8 @@ 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,14 +442,17 @@ 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"] @@ -459,7 +483,8 @@ 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", ): @@ -522,7 +547,9 @@ 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() @@ -559,10 +586,14 @@ 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") @@ -571,7 +602,9 @@ async def set_presence(self, *, status=None, activity_type=None, activity_messag activity_type = self.bot.config.get("activity_type") url = None - activity_message = (activity_message or self.bot.config["activity_message"]).strip() + activity_message = ( + activity_message or self.bot.config["activity_message"] + ).strip() if activity_type is not None and not activity_message: logger.warning( 'No activity message found whilst activity is provided, defaults to "Modmail".' @@ -587,7 +620,9 @@ async def set_presence(self, *, status=None, activity_type=None, activity_messag 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) @@ -649,7 +684,9 @@ 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( @@ -744,7 +781,9 @@ 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)) @@ -766,7 +805,9 @@ 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)) @@ -787,7 +828,9 @@ 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( @@ -804,7 +847,9 @@ async def config_get(self, ctx, *, key: str.lower = None): 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) + 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(): @@ -832,7 +877,8 @@ 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) @@ -855,10 +901,13 @@ 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"]: @@ -907,7 +956,9 @@ 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) @@ -919,14 +970,18 @@ 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=utils.truncate(val, 1024)) + 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( - 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) @@ -944,9 +999,12 @@ 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_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) @@ -974,7 +1032,9 @@ async def alias_raw(self, ctx, *, name: str.lower): 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 + title=f'Raw alias - "{name}":', + description=f"```\n{val}```", + color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -992,7 +1052,9 @@ async def make_alias(self, name, value, action): if len(values) > 25: embed = discord.Embed( - title="Error", description="Too many steps, max=25.", color=self.bot.error_color + title="Error", + description="Too many steps, max=25.", + color=self.bot.error_color, ) return embed @@ -1003,7 +1065,9 @@ async def make_alias(self, name, value, action): 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=utils.truncate(values[0], 1024)) + 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:" @@ -1176,7 +1240,9 @@ 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. @@ -1215,7 +1281,9 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name 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( @@ -1287,7 +1355,9 @@ 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", @@ -1384,13 +1454,21 @@ 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", @@ -1440,7 +1518,11 @@ def _get_perm(self, ctx, name, type_): @permissions.command(name="get", usage="[@user] or [command/level/override] [name]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get( - self, ctx, user_or_role: Union[discord.Role, utils.User, str], *, name: str = None + self, + ctx, + user_or_role: Union[discord.Role, utils.User, str], + *, + name: str = None, ): """ View the currently-set permissions. @@ -1490,7 +1572,9 @@ 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 @@ -1540,10 +1624,14 @@ 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 @@ -1599,7 +1687,9 @@ 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: @@ -1608,7 +1698,9 @@ 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")) @@ -1650,7 +1742,9 @@ 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 {target.mention} to view logs." @@ -1678,8 +1772,12 @@ 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) From ca02cb12b9fd9f81ccb1cc85e303eeccb7786426 Mon Sep 17 00:00:00 2001 From: Vincysuper07 <52707876+Vincysuper07@users.noreply.github.com> Date: Sun, 20 Sep 2020 09:09:45 +0200 Subject: [PATCH 0590/1221] Fix missing new line. --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index d2d78fb9ed..c4480985c3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -50,7 +50,7 @@ async def format_cog_help(self, cog, *, no_cog=False): format_ += ( f"- {cmd.short_doc}\n" if not cmd.short_doc == "" - else "- No description." + else "- No description.\n" ) if not format_.strip(): continue From 9030155a1fa5dbe21ef68e1861a871f071b29e80 Mon Sep 17 00:00:00 2001 From: Forcellrus Date: Tue, 29 Sep 2020 12:21:13 +0200 Subject: [PATCH 0591/1221] Add MODMAIL_GUILD_ID variable in example This variable was missing from the env example file for quite some time now. --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index 14bdf060bf..44c91c59c7 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ TOKEN=MyBotToken LOG_URL=https://logviewername.herokuapp.com/ GUILD_ID=1234567890 +MODMAIL_GUILD_ID=1234567890 OWNERS=Owner1ID,Owner2ID,Owner3ID CONNECTION_URI=mongodb+srv://mongodburi From b5fba82f8399f94945c61ad21396d08c892eedca Mon Sep 17 00:00:00 2001 From: Robin Mahieu <42642013+robinmahieu@users.noreply.github.com> Date: Sun, 4 Oct 2020 14:41:02 +0200 Subject: [PATCH 0592/1221] Remove plugins from Papier (#2858) --- plugins/registry.json | 45 ------------------------------------------- 1 file changed, 45 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 5a12d84191..4a79456010 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -26,15 +26,6 @@ "icon_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png", "thumbnail_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png" }, - "autorole": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "Easily auto-assign a role to a user when they join your server.", - "bot_version": "2.20.1", - "title": "Autorole Plugin", - "icon_url": "https://i.imgur.com/67bEi82.png", - "thumbnail_url": "https://i.imgur.com/67bEi82.png" - }, "anti-steal-close": { "repository": "officialpiyush/modmail-plugins", "branch": "master", @@ -43,42 +34,6 @@ "icon_url": "https://i.imgur.com/LovxyV3.png", "thumbnail_url": "https://i.imgur.com/LovxyV3.png" }, - "embedder": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "Easily make embeds for a nicer presence.", - "bot_version": "2.20.1", - "title": "Embedder Plugin", - "icon_url": "https://i.imgur.com/l3uzJwD.png", - "thumbnail_url": "https://i.imgur.com/l3uzJwD.png" - }, - "leveling": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "A leveling system for your server: see who's active and who's not.", - "bot_version": "2.20.1", - "title": "Leveling Plugin", - "icon_url": "https://i.imgur.com/VuZ60QX.png", - "thumbnail_url": "https://i.imgur.com/VuZ60QX.png" - }, - "purger": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "Delete multiple messages at a time.", - "bot_version": "2.20.1", - "title": "Purger Plugin", - "icon_url": "https://i.imgur.com/HnC42jM.png", - "thumbnail_url": "https://i.imgur.com/HnC42jM.png" - }, - "supporters": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "Let your users know who is part of the support team.", - "bot_version": "2.20.1", - "title": "Supporters Plugin", - "icon_url": "https://i.imgur.com/1QXRutA.png", - "thumbnail_url": "https://i.imgur.com/1QXRutA.png" - }, "announcement": { "repository": "officialpiyush/modmail-plugins", "branch": "master", From 2a55dc674798c15b73cb0769f0c713b98a1529eb Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sun, 11 Oct 2020 04:24:27 +0200 Subject: [PATCH 0593/1221] Remove lorenzo sponsor message per request remove sponsor server, still am a sponsor --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 76c2a4f107..40cbd7b1e4 100644 --- a/README.md +++ b/README.md @@ -166,10 +166,6 @@ Special thanks to our sponsors for supporting the project. - - - - Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins From 50c440f104eb5b89373822ed2fbed35bbac7ac40 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Sun, 11 Oct 2020 13:55:48 +0800 Subject: [PATCH 0594/1221] Update bot.py --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 9e48c0f342..daa62d0516 100644 --- a/bot.py +++ b/bot.py @@ -1214,7 +1214,7 @@ async def post_metadata(self): else: data.update({"owner_name": info.owner.name, "owner_id": info.owner.id, "team": False}) - async with self.session.post("https://api.logviewer.tech/metadata", json=data): + async with self.session.post("https://api.modmail.dev/metadata", json=data): logger.debug("Uploading metadata to Modmail server.") async def before_post_metadata(self): From eb80fed05e7a7e6fc6c6e59693bbb1a8e416a8a2 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 15:06:52 +0800 Subject: [PATCH 0595/1221] Fix errors on windows selfhost --- bot.py | 10 +++++++--- cogs/plugins.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index 9e48c0f342..976c01744f 100644 --- a/bot.py +++ b/bot.py @@ -89,9 +89,13 @@ def uptime(self) -> str: def startup(self): logger.line() - logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") - logger.info("││││ │ │││││├─┤││") - logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") + if os.name != 'nt': + logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") + logger.info("││││ │ │││││├─┤││") + logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") + else: + logger.info("MODMAIL") + logger.info("MODMAIL") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") logger.line() diff --git a/cogs/plugins.py b/cogs/plugins.py index df8259736c..6203edab67 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -199,7 +199,7 @@ async def load_plugin(self, plugin): venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env user_install = " --user" if not venv else "" proc = await asyncio.create_subprocess_shell( - f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", + f"\"{sys.executable}\" -m pip install --upgrade{user_install} -r {req_txt} -q -q --user", stderr=PIPE, stdout=PIPE, ) @@ -497,7 +497,7 @@ async def plugins_reset(self, ctx): logger.warning("Removing %s.", entry.name) embed = discord.Embed( - description=f"Successfully purged all plugins from the bot.", color=self.bot.main_color + description="Successfully purged all plugins from the bot.", color=self.bot.main_color ) return await ctx.send(embed=embed) From 6f05aa8129d634d1c4616230d5f21d24ddcd83eb Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 15:23:12 +0800 Subject: [PATCH 0596/1221] Add avatar to closed thread messages, resolve #2828 --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 960dafe0f7..ffe9c212df 100644 --- a/core/thread.py +++ b/core/thread.py @@ -361,7 +361,7 @@ async def _close( event = "Thread Closed as Scheduled" if scheduled else "Thread Closed" # embed.set_author(name=f"Event: {event}", url=log_url) - embed.set_footer(text=f"{event} by {_closer}") + embed.set_footer(text=f"{event} by {_closer}", icon_url=closer.avatar_url) embed.timestamp = datetime.utcnow() tasks = [self.bot.config.update()] From 17e287c8579322531a6e5f6d0564a1970890fcf6 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 15:27:15 +0800 Subject: [PATCH 0597/1221] Add nsfw indicator to log messages, resolve #2792 --- core/thread.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index ffe9c212df..b300e9fbbc 100644 --- a/core/thread.py +++ b/core/thread.py @@ -316,6 +316,7 @@ async def _close( { "open": False, "closed_at": str(datetime.utcnow()), + "nsfw": self.channel.nsfw, "close_message": message if not silent else None, "closer": { "id": str(closer.id), @@ -338,8 +339,13 @@ async def _close( sneak_peak = content.replace("\n", "") else: sneak_peak = "No content" + + if self.channel.nsfw: + _nsfw = 'NSFW-' + else: + _nsfw = '' - desc = f"[`{log_data['key']}`]({log_url}): " + desc = f"[`{_nsfw}{log_data['key']}`]({log_url}): " desc += truncate(sneak_peak, max=75 - 13) else: desc = "Could not resolve log url." From d70fd49a3fc668eb278764296b6aa53cfd6c8d07 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 15:34:04 +0800 Subject: [PATCH 0598/1221] Add thread_move_title config --- core/config.py | 1 + core/config_help.json | 14 ++++++++++++-- core/thread.py | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/core/config.py b/core/config.py index 4f54c2b29c..f2bb54a5c8 100644 --- a/core/config.py +++ b/core/config.py @@ -57,6 +57,7 @@ class ConfigManager: "thread_close_title": "Thread Closed", "thread_close_response": "{closer.mention} has closed this Modmail thread.", "thread_self_close_response": "You have closed this Modmail thread.", + "thread_move_title": "Thread Moved", "thread_move_notify": False, "thread_move_response": "This thread has been moved.", "disabled_new_thread_title": "Not Delivered", diff --git a/core/config_help.json b/core/config_help.json index db9218d2b3..9778f49f8c 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -350,6 +350,16 @@ "See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`." ] }, + "thread_move_title": { + "default": "Thread Moved", + "description": "The title of the message embed when a thread is moved.", + "examples": [ + "`{prefix}config set thread_move_title Thread transferred to another channel!" + ], + "notes": [ + "See also: `thread_move_notify`, `thread_move_response`." + ] + }, "thread_move_notify": { "default": "No", "description": "Notify the recipient if the thread was moved.", @@ -358,7 +368,7 @@ "`{prefix}config set thread_move_notify no`" ], "notes": [ - "See also: `thread_move_response`." + "See also: `thread_move_title`, `thread_move_response`." ] }, "thread_move_response": { @@ -369,7 +379,7 @@ ], "notes": [ "Only has an effect when `thread_move_notify` is on.", - "See also: `thread_move_notify`." + "See also: `thread_move_title`, `thread_move_notify`." ] }, "disabled_new_thread_title": { diff --git a/core/thread.py b/core/thread.py index b300e9fbbc..23ab81ad0d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -341,9 +341,9 @@ async def _close( sneak_peak = "No content" if self.channel.nsfw: - _nsfw = 'NSFW-' + _nsfw = "NSFW-" else: - _nsfw = '' + _nsfw = "" desc = f"[`{_nsfw}{log_data['key']}`]({log_url}): " desc += truncate(sneak_peak, max=75 - 13) From 1aa7dfc1d42c6b16565186e96b1b372f40295fa9 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 15:39:26 +0800 Subject: [PATCH 0599/1221] Add thread_move_title config --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 9ec5d5604f..a8551410e4 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -305,7 +305,7 @@ async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = if self.bot.config["thread_move_notify"] and not silent: embed = discord.Embed( - title="Thread Moved", + title=self.bot.config["thread_move_title"], description=self.bot.config["thread_move_response"], color=self.bot.main_color, ) From a3edc421f083c15878547310fcb910789c1db827 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 15:51:10 +0800 Subject: [PATCH 0600/1221] Allow mention command to accept ids/names, resolve #2796 --- cogs/utility.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index bd04bd6a09..deca773af1 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -638,7 +638,7 @@ async def ping(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) - async def mention(self, ctx, *, mention: str = None): + async def mention(self, ctx, *mention: Union[discord.Role, discord.Member]): """ Change what the bot mentions at the start of each thread. @@ -647,11 +647,12 @@ async def mention(self, ctx, *, mention: str = None): # TODO: ability to disable mention. current = self.bot.config["mention"] - if mention is None: + if not mention: embed = discord.Embed( title="Current mention:", color=self.bot.main_color, description=str(current) ) else: + mention = " ".join(i.mention for i in mention) embed = discord.Embed( title="Changed mention!", description=f'On thread creation the bot now says "{mention}".', From 1f846fa4cb16082985c15240b5767fe47399bee7 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 15:51:19 +0800 Subject: [PATCH 0601/1221] Formatting --- cogs/plugins.py | 2 +- core/thread.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 6203edab67..835aad49a9 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -199,7 +199,7 @@ async def load_plugin(self, plugin): venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env user_install = " --user" if not venv else "" proc = await asyncio.create_subprocess_shell( - f"\"{sys.executable}\" -m pip install --upgrade{user_install} -r {req_txt} -q -q --user", + f'"{sys.executable}" -m pip install --upgrade{user_install} -r {req_txt} -q -q --user', stderr=PIPE, stdout=PIPE, ) diff --git a/core/thread.py b/core/thread.py index 23ab81ad0d..9c388389e0 100644 --- a/core/thread.py +++ b/core/thread.py @@ -339,7 +339,7 @@ async def _close( sneak_peak = content.replace("\n", "") else: sneak_peak = "No content" - + if self.channel.nsfw: _nsfw = "NSFW-" else: From 732de02a3c321842eeb17fdc7c54fdcbc0d0fef0 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:07:20 +0800 Subject: [PATCH 0602/1221] Change move command to consume for category --- cogs/modmail.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index a8551410e4..b7489688cc 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -284,22 +284,43 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") await ctx.send(embed=embed) - @commands.command() + @commands.command(usage=' [options]') @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() - async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None): + async def move(self, ctx, *, arguments): """ Move a thread to another category. `category` may be a category ID, mention, or name. - `specifics` is a string which takes in arguments on how to perform the move. Ex: "silently" + `options` is a string which takes in arguments on how to perform the move. Ex: "silently" """ + split_args = arguments.strip('"').split(' ') + + # manually parse arguments, consumes as much of args as possible for category + for i in range(len(split_args)): + try: + if i == 0: + fmt = arguments + else: + fmt = ' '.join(split_args[:-i]) + + category = await commands.CategoryChannelConverter().convert(ctx, fmt) + except commands.BadArgument: + if i == len(split_args) - 1: + # last one + raise + pass + else: + break + + options = ' '.join(arguments.split(' ')[-i:]) + thread = ctx.thread silent = False - if specifics: + if options: silent_words = ["silent", "silently"] - silent = any(word in silent_words for word in specifics.split()) + silent = any(word in silent_words for word in options.split()) await thread.channel.edit(category=category, sync_permissions=True) From 8efb4ba95015092b2a02da56ae161099976aff96 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:09:12 +0800 Subject: [PATCH 0603/1221] Use --diff in CI, resolve #2816 --- .github/workflows/lints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index c84c9af773..52a538bef7 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -31,4 +31,4 @@ jobs: continue-on-error: true - name: Black and flake8 run: | - black . --check + black . --diff From 72e9522ce74343e48a4d39f4747a126bcdebaabd Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:09:48 +0800 Subject: [PATCH 0604/1221] Formatting --- cogs/modmail.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index b7489688cc..44f0e9dcde 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -284,7 +284,7 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") await ctx.send(embed=embed) - @commands.command(usage=' [options]') + @commands.command(usage=" [options]") @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() async def move(self, ctx, *, arguments): @@ -294,7 +294,7 @@ async def move(self, ctx, *, arguments): `category` may be a category ID, mention, or name. `options` is a string which takes in arguments on how to perform the move. Ex: "silently" """ - split_args = arguments.strip('"').split(' ') + split_args = arguments.strip('"').split(" ") # manually parse arguments, consumes as much of args as possible for category for i in range(len(split_args)): @@ -302,7 +302,7 @@ async def move(self, ctx, *, arguments): if i == 0: fmt = arguments else: - fmt = ' '.join(split_args[:-i]) + fmt = " ".join(split_args[:-i]) category = await commands.CategoryChannelConverter().convert(ctx, fmt) except commands.BadArgument: @@ -313,7 +313,7 @@ async def move(self, ctx, *, arguments): else: break - options = ' '.join(arguments.split(' ')[-i:]) + options = " ".join(arguments.split(" ")[-i:]) thread = ctx.thread silent = False From fa3efa186fbd0184e5a7eb17bb14cedb49bb2f4b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:10:32 +0800 Subject: [PATCH 0605/1221] Bump discord.py to 1.5.1, intent explicit support --- Pipfile | 6 +- Pipfile.lock | 341 ++++++++++++++++++++++--------------------- bot.py | 7 +- pyproject.toml | 2 +- requirements.min.txt | 2 +- runtime.txt | 2 +- 6 files changed, 181 insertions(+), 179 deletions(-) diff --git a/Pipfile b/Pipfile index 0872abf3e2..d468866095 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ verify_ssl = true black = "==19.10b0" pylint = "*" bandit = "==1.6.2" +flake8 = "*" [packages] colorama = ">=0.4.0" @@ -21,12 +22,9 @@ parsedatetime = "==2.6" aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = "==1.3.4" +"discord.py" = "==1.5.1" pynacl = "*" cffi = "*" -[requires] -python_version = "3.7" - [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 807d74cb97..682d7252b5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "397b06fdc5c4560667c7d3b2b8323ffd6ac3961fc561a470ccf99d8a5c234ffb" + "sha256": "0dba33486b4cc030061af10a203dbf804d4984f09da44200d4a0a97c6ed924ce" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -18,21 +16,22 @@ "default": { "aiohttp": { "hashes": [ - "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", - "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", - "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", - "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", - "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", - "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", - "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", - "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", - "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", - "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", - "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", - "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe", + "sha256:206c0ccfcea46e1bddc91162449c20c72f308aebdcef4977420ef329c8fcc599", + "sha256:2ad493de47a8f926386fa6d256832de3095ba285f325db917c7deae0b54a9fc8", + "sha256:319b490a5e2beaf06891f6711856ea10591cfe84fe9f3e71a721aa8f20a0872a", + "sha256:470e4c90da36b601676fe50c49a60d34eb8c6593780930b1aa4eea6f508dfa37", + "sha256:60f4caa3b7f7a477f66ccdd158e06901e1d235d572283906276e3803f6b098f5", + "sha256:66d64486172b032db19ea8522328b19cfb78a3e1e5b62ab6a0567f93f073dea0", + "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c", + "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645", + "sha256:797456399ffeef73172945708810f3277f794965eb6ec9bd3a0c007c0476be98", + "sha256:a885432d3cabc1287bcf88ea94e1826d3aec57fd5da4a586afae4591b061d40d", + "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81", + "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5" ], "index": "pypi", - "version": "==3.6.2" + "version": "==3.6.3" }, "appdirs": { "hashes": [ @@ -46,14 +45,16 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { "hashes": [ - "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", - "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "version": "==20.1.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" }, "certifi": { "hashes": [ @@ -64,37 +65,45 @@ }, "cffi": { "hashes": [ - "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e", - "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c", - "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e", - "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1", - "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4", - "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2", - "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c", - "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0", - "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798", - "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1", - "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4", - "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731", - "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4", - "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c", - "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487", - "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e", - "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f", - "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123", - "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c", - "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b", - "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650", - "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad", - "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75", - "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82", - "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7", - "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15", - "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa", - "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281" + "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", + "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", + "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", + "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", + "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", + "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", + "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", + "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", + "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", + "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", + "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", + "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", + "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", + "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", + "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", + "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", + "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", + "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", + "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", + "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", + "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", + "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", + "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", + "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", + "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", + "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", + "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", + "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", + "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", + "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", + "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", + "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", + "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", + "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", + "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", + "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" ], "index": "pypi", - "version": "==1.14.2" + "version": "==1.14.3" }, "chardet": { "hashes": [ @@ -105,19 +114,19 @@ }, "colorama": { "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], "index": "pypi", - "version": "==0.4.3" + "version": "==0.4.4" }, "discord.py": { "hashes": [ - "sha256:1b546a32c0cd83d949392a71e5b06e30e19d1067246e3826d32ae9b8b3d06c1e", - "sha256:8ef58d6fc1e66903bc00ae79c4c09a38aa71043e88a83da4d2e8b9b1c9f9b9e2" + "sha256:2367359e31f6527f8a936751fc20b09d7495dd6a76b28c8fb13d4ca6c55b7563", + "sha256:def00dc50cf36d21346d71bc89f0cad8f18f9a3522978dc18c7796287d47de8b" ], "index": "pypi", - "version": "==1.3.4" + "version": "==1.5.1" }, "distlib": { "hashes": [ @@ -153,16 +162,9 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, - "importlib-metadata": { - "hashes": [ - "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", - "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" - ], - "markers": "python_version < '3.8'", - "version": "==1.7.0" - }, "isodate": { "hashes": [ "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", @@ -173,10 +175,11 @@ }, "motor": { "hashes": [ - "sha256:659ad13c2e2dca19807fbb6d2bb62e4c60f99bb0d43110a6abb8fdd12b644a64" + "sha256:428d94750123d19fcd0a89b8671ff9b4656f205217bad9f44161748c64c5fc80", + "sha256:f1692b760d834707e3477996ce8d407af8cd61c1a2abedbf81c22ef14675e61a" ], "index": "pypi", - "version": "==2.2.0" + "version": "==2.3.0" }, "multidict": { "hashes": [ @@ -198,6 +201,7 @@ "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], + "markers": "python_version >= '3.5'", "version": "==4.7.6" }, "natural": { @@ -228,6 +232,7 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pymongo": { @@ -250,6 +255,7 @@ "sha256:4b32744901ee9990aa8cd488ec85634f443526def1e5190a407dc107148249d7", "sha256:50127b13b38e8e586d5e97d342689405edbd74ad0bd891d97ee126a8c7b6e45f", "sha256:50531caa7b4be1c4ed5e2d5793a4e51cc9bd62a919a6fd3299ef7c902e206eab", + "sha256:63a5387e496a98170ffe638b435c0832c0f2011a6f4ff7a2880f17669fff8c03", "sha256:68220b81850de8e966d4667d5c325a96c6ac0d6adb3d18935d6e3d325d441f48", "sha256:689142dc0c150e9cb7c012d84cac2c346d40beb891323afb6caf18ec4caafae0", "sha256:6a15e2bee5c4188369a87ed6f02de804651152634a46cca91966a11c8abd2550", @@ -286,6 +292,7 @@ "sha256:ef76535776c0708a85258f6dc51d36a2df12633c735f6d197ed7dfcaa7449b99", "sha256:f6efca006a81e1197b925a7d7b16b8f61980697bb6746587aad8842865233218" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.11.0" }, "pynacl": { @@ -294,6 +301,7 @@ "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4", "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574", "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d", + "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634", "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25", "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f", "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505", @@ -302,6 +310,7 @@ "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420", "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f", "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96", + "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6", "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6", "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514", "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff", @@ -331,17 +340,9 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, - "typing-extensions": { - "hashes": [ - "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", - "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", - "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" - ], - "markers": "python_version < '3.8'", - "version": "==3.7.4.2" - }, "uvloop": { "hashes": [ "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", @@ -354,51 +355,25 @@ "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" ], - "index": "pypi", "markers": "sys_platform != 'win32'", "version": "==0.14.0" }, "virtualenv": { "hashes": [ - "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc", - "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b" + "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", + "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" ], - "version": "==20.0.31" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.1.0" }, "virtualenv-clone": { "hashes": [ "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.5.4" }, - "websockets": { - "hashes": [ - "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", - "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", - "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", - "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", - "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", - "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", - "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", - "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", - "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", - "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", - "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", - "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", - "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", - "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", - "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", - "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", - "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", - "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", - "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", - "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", - "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", - "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" - ], - "version": "==8.1" - }, "yarl": { "hashes": [ "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", @@ -419,14 +394,8 @@ "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" ], + "markers": "python_version >= '3.5'", "version": "==1.5.1" - }, - "zipp": { - "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" - ], - "version": "==3.1.0" } }, "develop": { @@ -442,14 +411,16 @@ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], + "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "attrs": { "hashes": [ - "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a", - "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "version": "==20.1.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" }, "bandit": { "hashes": [ @@ -472,44 +443,48 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "colorama": { "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], "index": "pypi", - "version": "==0.4.3" + "version": "==0.4.4" + }, + "flake8": { + "hashes": [ + "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", + "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" + ], + "index": "pypi", + "version": "==3.8.4" }, "gitdb": { "hashes": [ "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], + "markers": "python_version >= '3.4'", "version": "==4.0.5" }, "gitpython": { "hashes": [ - "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", - "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" + "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b", + "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8" ], - "version": "==3.1.7" - }, - "importlib-metadata": { - "hashes": [ - "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", - "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" - ], - "markers": "python_version < '3.8'", - "version": "==1.7.0" + "markers": "python_version >= '3.4'", + "version": "==3.1.11" }, "isort": { "hashes": [ - "sha256:60a1b97e33f61243d12647aaaa3e6cc6778f5eb9f42997650f1cc975b6008750", - "sha256:d488ba1c5a2db721669cc180180d5acf84ebdc5af7827f7aaeaa75f73cf0e2b8" + "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7", + "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58" ], - "version": "==5.4.2" + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==5.6.4" }, "lazy-object-proxy": { "hashes": [ @@ -535,6 +510,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -553,10 +529,27 @@ }, "pbr": { "hashes": [ - "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c", - "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8" + "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", + "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" ], - "version": "==5.4.5" + "markers": "python_version >= '2.6'", + "version": "==5.5.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", + "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.6.0" + }, + "pyflakes": { + "hashes": [ + "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", + "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.2.0" }, "pylint": { "hashes": [ @@ -584,35 +577,42 @@ }, "regex": { "hashes": [ - "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", - "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", - "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", - "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", - "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", - "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", - "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", - "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", - "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", - "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", - "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", - "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", - "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", - "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", - "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", - "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", - "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", - "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", - "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", - "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", - "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" - ], - "version": "==2020.7.14" + "sha256:0cb23ed0e327c18fb7eac61ebbb3180ebafed5b9b86ca2e15438201e5903b5dd", + "sha256:1a065e7a6a1b4aa851a0efa1a2579eabc765246b8b3a5fd74000aaa3134b8b4e", + "sha256:1a511470db3aa97432ac8c1bf014fcc6c9fbfd0f4b1313024d342549cf86bcd6", + "sha256:1c447b0d108cddc69036b1b3910fac159f2b51fdeec7f13872e059b7bc932be1", + "sha256:2278453c6a76280b38855a263198961938108ea2333ee145c5168c36b8e2b376", + "sha256:240509721a663836b611fa13ca1843079fc52d0b91ef3f92d9bba8da12e768a0", + "sha256:4e21340c07090ddc8c16deebfd82eb9c9e1ec5e62f57bb86194a2595fd7b46e0", + "sha256:570e916a44a361d4e85f355aacd90e9113319c78ce3c2d098d2ddf9631b34505", + "sha256:59d5c6302d22c16d59611a9fd53556554010db1d47e9df5df37be05007bebe75", + "sha256:6a46eba253cedcbe8a6469f881f014f0a98819d99d341461630885139850e281", + "sha256:6f567df0601e9c7434958143aebea47a9c4b45434ea0ae0286a4ec19e9877169", + "sha256:781906e45ef1d10a0ed9ec8ab83a09b5e0d742de70e627b20d61ccb1b1d3964d", + "sha256:8469377a437dbc31e480993399fd1fd15fe26f382dc04c51c9cb73e42965cc06", + "sha256:8cd0d587aaac74194ad3e68029124c06245acaeddaae14cb45844e5c9bebeea4", + "sha256:97a023f97cddf00831ba04886d1596ef10f59b93df7f855856f037190936e868", + "sha256:a973d5a7a324e2a5230ad7c43f5e1383cac51ef4903bf274936a5634b724b531", + "sha256:af360e62a9790e0a96bc9ac845d87bfa0e4ee0ee68547ae8b5a9c1030517dbef", + "sha256:b706c70070eea03411b1761fff3a2675da28d042a1ab7d0863b3efe1faa125c9", + "sha256:bfd7a9fddd11d116a58b62ee6c502fd24cfe22a4792261f258f886aa41c2a899", + "sha256:c30d8766a055c22e39dd7e1a4f98f6266169f2de05db737efe509c2fb9c8a3c8", + "sha256:c53dc8ee3bb7b7e28ee9feb996a0c999137be6c1d3b02cb6b3c4cba4f9e5ed09", + "sha256:c95d514093b80e5309bdca5dd99e51bcf82c44043b57c34594d9d7556bd04d05", + "sha256:d43cf21df524283daa80ecad551c306b7f52881c8d0fe4e3e76a96b626b6d8d8", + "sha256:d62205f00f461fe8b24ade07499454a3b7adf3def1225e258b994e2215fd15c5", + "sha256:e289a857dca3b35d3615c3a6a438622e20d1bf0abcb82c57d866c8d0be3f44c4", + "sha256:e5f6aa56dda92472e9d6f7b1e6331f4e2d51a67caafff4d4c5121cadac03941e", + "sha256:f4b1c65ee86bfbf7d0c3dfd90592a9e3d6e9ecd36c367c884094c050d4c35d04" + ], + "version": "==2020.10.23" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "smmap": { @@ -620,14 +620,16 @@ "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "stevedore": { "hashes": [ - "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5", - "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633" + "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62", + "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0" ], - "version": "==3.2.0" + "markers": "python_version >= '3.6'", + "version": "==3.2.2" }, "toml": { "hashes": [ @@ -640,27 +642,35 @@ "hashes": [ "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", + "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", "version": "==1.4.1" }, "wrapt": { @@ -668,13 +678,6 @@ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], "version": "==1.12.1" - }, - "zipp": { - "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" - ], - "version": "==3.1.0" } } } diff --git a/bot.py b/bot.py index 976c01744f..10c4b940cb 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.5.0" +__version__ = "3.6.0" import asyncio @@ -53,7 +53,8 @@ class ModmailBot(commands.Bot): def __init__(self): - super().__init__(command_prefix=None) # implemented in `get_prefix` + intents = discord.Intents.all() + super().__init__(command_prefix=None, intents=intents) # implemented in `get_prefix` self._session = None self._api = None self.metadata_loop = None @@ -89,7 +90,7 @@ def uptime(self) -> str: def startup(self): logger.line() - if os.name != 'nt': + if os.name != "nt": logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") logger.info("││││ │ │││││├─┤││") logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") diff --git a/pyproject.toml b/pyproject.toml index b8547d752b..3f598a4640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ keywords = ['discord', 'modmail'] [tool.poetry.dependencies] python = "^3.7" -"discord.py" = "=1.3.4" +"discord.py" = "=1.5.1" uvloop = {version = ">=0.12.0", sys_platform = "!= 'win32'"} python-dotenv = ">=0.10.3" parsedatetime = "^2.6" diff --git a/requirements.min.txt b/requirements.min.txt index 28550f1e50..9fd4adf1a3 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -6,7 +6,7 @@ aiohttp==3.6.2 async-timeout==3.0.1 attrs==19.3.0 chardet==3.0.4 -discord.py==1.3.4 +discord.py==1.5.1 dnspython==1.16.0 emoji==0.5.4 future==0.18.2 diff --git a/runtime.txt b/runtime.txt index 050dcb9387..67068f10fe 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.9 \ No newline at end of file +python-3.9.0 \ No newline at end of file From e49e1709adfcf0829bc625773d6393e2a14a08f6 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:17:10 +0800 Subject: [PATCH 0606/1221] v3.6 changelog --- CHANGELOG.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d3d127944..17951f6a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. +# v3.6.0 + +### Added + +- Added `thread_move_title` to specify title of thread moved embed. +- Mark NSFW logs in log message. ([GH #2792](https://github.com/kyb3r/modmail/issues/2792)) +- Icon for moderator that closed the thread in log message. ([GH #2828](https://github.com/kyb3r/modmail/issues/2828)) +- Ability to set mentions via user/role ID ([GH #2796](https://github.com/kyb3r/modmail/issues/2796)) + +### Changed + +- `?move` now consumes rest in category name, which means `?move Long Category Name` works without quotes! + +### Internal + +- Bump discord.py version to 1.5.1 +- Explicitly state intents used for connection +- Use `--diff` for black CI instead of `--check` ([GH#2816](https://github.com/kyb3r/modmail/issues/2816)) # v3.5.0 @@ -14,7 +32,7 @@ Fixed discord.py issue. ### Added - A confirmation when you manually delete a thread message embed. -- Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. (GH #2803) +- Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. ([GH #2803](https://github.com/kyb3r/modmail/issues/2803)) - Added `?plugins reset` command to completely reset everything related to plugins. This will fix some problems caused by broken plugins in the file system. - Support private GitHub repos for plugins (thanks to @officialpiyush pr#2767) From 4a32c0a978f22c022945f3f7a82fef9d0f4ab07c Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:18:23 +0800 Subject: [PATCH 0607/1221] Remove piyush translator plugin --- plugins/registry.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 4a79456010..38b6f20efa 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -79,15 +79,6 @@ "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, - "translator": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "You can auto translate thread msgs using this plugin or translate text fromm any language to English!", - "bot_version": "2.20.1", - "title": "Translator Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, "welcomer": { "repository": "fourjr/modmail-plugins", "branch": "master", From 2aa0f842e8a40181300ce165737eaaef2e247859 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:22:43 +0800 Subject: [PATCH 0608/1221] Black Styling --- cogs/utility.py | 202 +++++++++++++----------------------------------- 1 file changed, 52 insertions(+), 150 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 1f89a0447a..c9e124b04c 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -47,11 +47,7 @@ async def format_cog_help(self, cog, *, no_cog=False): else: format_ = f"`[{perm_level}] {prefix + cmd.qualified_name}` " - format_ += ( - f"- {cmd.short_doc}\n" - if not cmd.short_doc == "" - else "- No description.\n" - ) + format_ += f"- {cmd.short_doc}\n" if not cmd.short_doc == "" else "- No description.\n" if not format_.strip(): continue if len(format_) + len(formats[-1]) >= 1024: @@ -71,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( @@ -96,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) @@ -109,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): @@ -199,8 +183,7 @@ async def send_error_message(self, error): else: if len(values) == 1: embed = discord.Embed( - title=f"{command} is an alias.", - color=self.context.bot.main_color, + title=f"{command} is an alias.", color=self.context.bot.main_color ) embed.add_field(name=f"`{command}` points to:", value=values[0]) else: @@ -231,9 +214,7 @@ async def send_error_message(self, error): closest = get_close_matches(command, choices) if closest: - embed.add_field( - name="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( @@ -326,12 +307,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: @@ -386,8 +366,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: @@ -442,17 +421,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"] @@ -483,8 +459,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", ): @@ -547,9 +522,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() @@ -586,14 +559,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") @@ -602,9 +571,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".' @@ -620,9 +587,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) @@ -684,9 +649,7 @@ async def mention(self, ctx, *mention: Union[discord.Role, discord.Member]): if not mention: 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: mention = " ".join(i.mention for i in mention) @@ -782,9 +745,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)) @@ -806,9 +767,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)) @@ -829,9 +788,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( @@ -848,9 +805,7 @@ async def config_get(self, ctx, *, key: str.lower = None): 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 - ) + 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(): @@ -878,8 +833,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) @@ -902,13 +856,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"]: @@ -957,9 +908,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) @@ -971,18 +920,14 @@ 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=utils.truncate(val, 1024) - ) + 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( - 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) @@ -1000,12 +945,9 @@ 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.", - ) - embed.set_footer( - text=f'Do "{self.bot.prefix}help alias" for more commands.' + 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) return await ctx.send(embed=embed) @@ -1033,9 +975,7 @@ async def alias_raw(self, ctx, *, name: str.lower): 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, + title=f'Raw alias - "{name}":', description=f"```\n{val}```", color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -1053,9 +993,7 @@ async def make_alias(self, name, value, action): if len(values) > 25: embed = discord.Embed( - title="Error", - description="Too many steps, max=25.", - color=self.bot.error_color, + title="Error", description="Too many steps, max=25.", color=self.bot.error_color ) return embed @@ -1066,9 +1004,7 @@ async def make_alias(self, name, value, action): 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=utils.truncate(values[0], 1024) - ) + 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:" @@ -1241,9 +1177,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. @@ -1282,9 +1216,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( @@ -1356,9 +1288,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", @@ -1455,21 +1385,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", @@ -1519,11 +1441,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. @@ -1573,9 +1491,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 @@ -1625,14 +1541,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 @@ -1688,9 +1600,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: @@ -1699,9 +1609,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")) @@ -1743,9 +1651,7 @@ 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 {target.mention} to view logs." @@ -1773,12 +1679,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) From 2d3d690a645f822a9fd379f616a7ddcee86e574b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:23:49 +0800 Subject: [PATCH 0609/1221] add PR#2845 in changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17951f6a24..3763968747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,12 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `thread_move_title` to specify title of thread moved embed. - Mark NSFW logs in log message. ([GH #2792](https://github.com/kyb3r/modmail/issues/2792)) - Icon for moderator that closed the thread in log message. ([GH #2828](https://github.com/kyb3r/modmail/issues/2828)) -- Ability to set mentions via user/role ID ([GH #2796](https://github.com/kyb3r/modmail/issues/2796)) +- Ability to set mentions via user/role ID. ([GH #2796](https://github.com/kyb3r/modmail/issues/2796)) ### Changed - `?move` now consumes rest in category name, which means `?move Long Category Name` works without quotes! +- `?help` shows "No command description" if no description provided. ([PR #2845](https://github.com/kyb3r/modmail/pull/2845)) ### Internal From 3d6c657ec7aeae29a2a51f4e1ec9e6daf8ccd266 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:28:02 +0800 Subject: [PATCH 0610/1221] Add to app.json --- app.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app.json b/app.json index b6a833b1ef..cd5f25825c 100644 --- a/app.json +++ b/app.json @@ -11,6 +11,10 @@ "description": "The id for the server you are hosting this bot for.", "required": true }, + "MODMAIL_GUILD_ID": { + "description": "The ID of the discord server where the threads channels should be created (receiving server). Default to GUILD_ID.", + "required": false + }, "OWNERS": { "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", "required": true From 7f4f3eda393792d4918398ecc9a920a229b4867d Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sat, 10 Oct 2020 21:14:46 +0200 Subject: [PATCH 0611/1221] Update SPONSORS.json Remove sponsor server, still am sponsor --- SPONSORS.json | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index d30134e015..ceed37be23 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -26,42 +26,5 @@ } ] } - }, - { - "embed": { - "title": "Hey there!", - "description": "Nice to see you here! You can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <- and also join our [Discord](https://discord.gg/zaeVCaV)!", - "url": "https://discord.gg/zaeVCaV", - "color": 13003681, - "footer": { - "icon_url": "https://imgur.com/Mrc9pLd.gif", - "text": "everyone is a clown" - }, - "thumbnail": { - "url": "https://imgur.com/Mrc9pLd.gif" - }, - "image": { - "url": "https://imgur.com/ZUFiL6b.gif" - }, - "author": { - "name": "Roomieofficial", - "url": "https://discord.gg/zaeVCaV", - "icon_url": "https://imgur.com/6hBkt7Z.png" - }, - "fields": [ - { - "name": "What is all this about 🤔", - "value": "We are mainly focused on everything that has anything to do with music or singing! " - }, - { - "name": "Youtube 🙄", - "value": "U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads." - }, - { - "name": "Discord 😁", - "value": "Make sure to join our [Discord](https://discord.gg/zaeVCaV) We have weekly events, 24/7 chats and more!" - } - ] - } } ] From 7da970c17666b26149fbda4586fa5a79ec8b2c99 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:31:02 +0800 Subject: [PATCH 0612/1221] lorenzo132: add mediaonly plugin --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 335cd25458..00a637875c 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -25,6 +25,15 @@ "title": "Dragory Logs Migration", "icon_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png", "thumbnail_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png" + }, + "media-only": { + "repository": "lorenzo132/modmail-plugins", + "branch": "master", + "description": "Make a channel mediaonly, only the following mediatypes will be accepted `.png` / `.gif` / `.jpg` / `.mp4`/ `.jpeg`", + "bot_version": "2.20.1", + "title": "Media-only", + "icon_url": "https://i.imgur.com/ussAoIi.png", + "thumbnail_url": "https://i.imgur.com/ussAoIi.png" }, "anti-steal-close": { "repository": "officialpiyush/modmail-plugins", From 2c5a72b899a474067f11e31bda0d41b4ec7a3dff Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:31:26 +0800 Subject: [PATCH 0613/1221] Formatting --- plugins/registry.json | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 00a637875c..5c343ebcff 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -7,8 +7,8 @@ "title": "Profanity Filter Plugin", "icon_url": "https://i.imgur.com/951szZ3.jpg", "thumbnail_url": "https://i.imgur.com/951szZ3.jpg" - }, - "music": { + }, + "music": { "repository": "lorenzo132/modmail-plugins", "branch": "master", "description": "Play your favourite music on your bot! Note: Only works on VPS follow this guide: https://gist.github.com/lorenzo132/5ef328e5dfcfaec19cb81dc7a63eaffa", @@ -16,8 +16,8 @@ "title": "Music", "icon_url": "https://i.imgur.com/R2olclk.png", "thumbnail_url": "https://i.imgur.com/xuoQjPu.gif" - }, - "dragory-migrate": { + }, + "dragory-migrate": { "repository": "kyb3r/modmail-plugins", "branch": "master", "description": "Migrate your logs from Dragory's modmail bot to this one with a simple command. Added at the request of users.", @@ -25,8 +25,8 @@ "title": "Dragory Logs Migration", "icon_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png", "thumbnail_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png" - }, - "media-only": { + }, + "media-only": { "repository": "lorenzo132/modmail-plugins", "branch": "master", "description": "Make a channel mediaonly, only the following mediatypes will be accepted `.png` / `.gif` / `.jpg` / `.mp4`/ `.jpeg`", @@ -36,12 +36,12 @@ "thumbnail_url": "https://i.imgur.com/ussAoIi.png" }, "anti-steal-close": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Don't let anyone steal ya close.", - "title": "Anti Steal Close", - "icon_url": "https://i.imgur.com/LovxyV3.png", - "thumbnail_url": "https://i.imgur.com/LovxyV3.png" + "repository": "officialpiyush/modmail-plugins", + "branch": "master", + "description": "Don't let anyone steal ya close.", + "title": "Anti Steal Close", + "icon_url": "https://i.imgur.com/LovxyV3.png", + "thumbnail_url": "https://i.imgur.com/LovxyV3.png" }, "announcement": { "repository": "officialpiyush/modmail-plugins", @@ -114,7 +114,7 @@ "title": "Backup Database (backupdb)", "icon_url": "https://images.ionadev.ml/b/nKAlOC4.jpg", "thumbnail_url": "https://images.ionadev.ml/b/nKAlOC4.jpg" - }, + }, "colors": { "repository": "Taaku18/modmail-plugins", "branch": "master", @@ -124,7 +124,7 @@ "icon_url": "https://cdn1.iconfinder.com/data/icons/weather-19/32/rainbow-512.png", "thumbnail_url": "https://i.imgur.com/fSxnc9W.jpg" }, - "fun": { + "fun": { "repository": "TheKinG2149/modmail-plugins", "branch": "master", "description": "Some fun commands like 8ball, dadjokes", @@ -132,7 +132,7 @@ "title": "Fun", "icon_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png" - }, + }, "stats": { "repository": "KarateWumpus/modmail-plugins", "branch": "master", @@ -178,7 +178,7 @@ "icon_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg", "thumbnail_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg" }, - "slowmode": { + "slowmode": { "repository": "teen1/modmail-plugins", "branch": "master", "description": "Configure slow mode for your channels with Modmail!", @@ -187,7 +187,7 @@ "icon_url": "https://cdn.discordapp.com/attachments/717029057635549274/717033838966210601/Slow_mode_-_icon.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/717029057635549274/717029110907666482/Slow_mode_plugin_-_thumbnail.png" }, - "publish": { + "publish": { "repository": "codeinteger6/modmail-plugins", "branch": "master", "description": "Publish messages sent in announcement channels.", @@ -196,7 +196,7 @@ "icon_url": "https://user-images.githubusercontent.com/44692189/89184422-96de3600-d5ba-11ea-98ea-d096aa385ad5.png", "thumbnail_url": "https://user-images.githubusercontent.com/44692189/89184422-96de3600-d5ba-11ea-98ea-d096aa385ad5.png" }, - "translate": { + "translate": { "repository": "WebKide/modmail-plugins", "branch": "master", "description": "(∩`-´)⊃━☆゚.*・。゚ translate text from one language to another (defaults to English)\n\nGet full list of available languages at: https://github.com/WebKide/modmail-plugins/blob/master/translate/langs.json\n\nThis command conflicts with Translator-plugin", @@ -205,4 +205,4 @@ "icon_url": "https://i.imgur.com/yeHFKgl.png", "thumbnail_url": "https://i.imgur.com/yeHFKgl.png" } -} +} \ No newline at end of file From e57a519819d1e0d37716e1f267f839cd2344c22a Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:42:33 +0800 Subject: [PATCH 0614/1221] Finalise 3.6 changelog --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3763968747..4069bf5586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?move` now consumes rest in category name, which means `?move Long Category Name` works without quotes! - `?help` shows "No command description" if no description provided. ([PR #2845](https://github.com/kyb3r/modmail/pull/2845)) +### Fixed +- Unicode errors raised during windows selfhosting + ### Internal - Bump discord.py version to 1.5.1 diff --git a/pyproject.toml b/pyproject.toml index 3f598a4640..c1f32e7c2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.5.0' +version = '3.6.0' description = "Modmail is similar to Reddit's 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 e3a2bbff2502dc112b8164d2c469c2a452e5dc0a Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:44:08 +0800 Subject: [PATCH 0615/1221] Fix startup printing --- bot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot.py b/bot.py index 46cfa4b772..af0187f290 100644 --- a/bot.py +++ b/bot.py @@ -96,7 +96,6 @@ def startup(self): logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") else: logger.info("MODMAIL") - logger.info("MODMAIL") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") logger.line() From 2d99efe3fe530f8cd9d669147fb1f26dd8e11501 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 17:02:09 +0800 Subject: [PATCH 0616/1221] Fix bug with help message --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index c9e124b04c..b0d96ea094 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -47,7 +47,7 @@ async def format_cog_help(self, cog, *, no_cog=False): else: format_ = f"`[{perm_level}] {prefix + cmd.qualified_name}` " - format_ += f"- {cmd.short_doc}\n" if not cmd.short_doc == "" else "- No description.\n" + format_ += f"- {cmd.short_doc}\n" if cmd.short_doc else "- *No description.*\n" if not format_.strip(): continue if len(format_) + len(formats[-1]) >= 1024: From 5d14ef13ea478f7517ae0d405e99582c62f00695 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 17:19:24 +0800 Subject: [PATCH 0617/1221] Update version number --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 40cbd7b1e4..1a34338e8a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
From 1772910f4dfc3547db5d8add086bdf8b32ef86bb Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 17:27:06 +0800 Subject: [PATCH 0618/1221] 3.6.1 - error message if no priv. intents granted --- CHANGELOG.md | 8 ++++++++ bot.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4069bf5586..beddd66a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. +# v3.6.1 + +### Added + +- Proper error message if privileged intents not explicitly granted to bot. + + # v3.6.0 ### Added @@ -29,6 +36,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Explicitly state intents used for connection - Use `--diff` for black CI instead of `--check` ([GH#2816](https://github.com/kyb3r/modmail/issues/2816)) + # v3.5.0 Fixed discord.py issue. diff --git a/bot.py b/bot.py index af0187f290..0755b859bd 100644 --- a/bot.py +++ b/bot.py @@ -167,6 +167,8 @@ def run(self, *args, **kwargs): pass except discord.LoginFailure: logger.critical("Invalid token") + except discord.PrivilegedIntentsRequired: + logger.critical('Privileged intents are not explicitly granted in the discord developers dashboard.') except Exception: logger.critical("Fatal exception", exc_info=True) finally: From 690078dba8c89c442e869de6271ec229bb1dffe7 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 17:28:15 +0800 Subject: [PATCH 0619/1221] 3.6.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a34338e8a..b22bf75f79 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
From 9994cf6a88da045ab61198c5576dd4454b1bc205 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 18:15:35 +0800 Subject: [PATCH 0620/1221] Fix pyproject.toml --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c1f32e7c2e..0d47eb844a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ keywords = ['discord', 'modmail'] [tool.poetry.dependencies] python = "^3.7" "discord.py" = "=1.5.1" -uvloop = {version = ">=0.12.0", sys_platform = "!= 'win32'"} +uvloop = {version = ">=0.12.0", markers = "sys_platform != 'win32'"} python-dotenv = ">=0.10.3" parsedatetime = "^2.6" dnspython = "^1.16" @@ -50,7 +50,7 @@ colorama = "^0.4.3" aiohttp = ">=3.6.0,<3.7.0" [tool.poetry.dev-dependencies] -black = {version = "=19.10b0", allows-prereleases = true} +black = {version = "=19.10b0", allow-prereleases = true} pylint = "^2.4" bandit = "^1.6" From 7808be033e570f71e1f0e39dffde6d7d1ae60ab9 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 18:29:15 +0800 Subject: [PATCH 0621/1221] Fix issue with plugins and clean pipfile --- CHANGELOG.md | 7 +++++ Pipfile | 2 -- Pipfile.lock | 76 +------------------------------------------------ README.md | 2 +- bot.py | 6 ++-- cogs/plugins.py | 2 +- 6 files changed, 14 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beddd66a28..04779442b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. +# v3.6.2 + +### Added + +- Proper error message if privileged intents not explicitly granted to bot. + + # v3.6.1 ### Added diff --git a/Pipfile b/Pipfile index d468866095..01485b1e63 100644 --- a/Pipfile +++ b/Pipfile @@ -23,8 +23,6 @@ aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" "discord.py" = "==1.5.1" -pynacl = "*" -cffi = "*" [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 682d7252b5..005ce37f67 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0dba33486b4cc030061af10a203dbf804d4984f09da44200d4a0a97c6ed924ce" + "sha256": "0491618cb8bd6d70e4ab3337c23b72fc3b1d5f9ca0603d8be6b890b661039102" }, "pipfile-spec": 6, "requires": {}, @@ -63,48 +63,6 @@ ], "version": "==2020.6.20" }, - "cffi": { - "hashes": [ - "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d", - "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b", - "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4", - "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f", - "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3", - "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579", - "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537", - "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e", - "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05", - "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171", - "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca", - "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522", - "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c", - "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc", - "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d", - "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808", - "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828", - "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869", - "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d", - "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9", - "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0", - "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc", - "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15", - "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c", - "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a", - "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3", - "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1", - "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768", - "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d", - "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b", - "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e", - "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d", - "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730", - "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394", - "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1", - "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591" - ], - "index": "pypi", - "version": "==1.14.3" - }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", @@ -227,14 +185,6 @@ "index": "pypi", "version": "==2020.8.13" }, - "pycparser": { - "hashes": [ - "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", - "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.20" - }, "pymongo": { "hashes": [ "sha256:03dc64a9aa7a5d405aea5c56db95835f6a2fa31b3502c5af1760e0e99210be30", @@ -295,30 +245,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.11.0" }, - "pynacl": { - "hashes": [ - "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4", - "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4", - "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574", - "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d", - "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634", - "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25", - "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f", - "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505", - "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122", - "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7", - "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420", - "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f", - "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96", - "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6", - "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6", - "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514", - "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff", - "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80" - ], - "index": "pypi", - "version": "==1.4.0" - }, "python-dateutil": { "hashes": [ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", diff --git a/README.md b/README.md index b22bf75f79..6173484788 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
diff --git a/bot.py b/bot.py index 0755b859bd..15ef85429e 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.6.0" +__version__ = "3.6.2" import asyncio @@ -168,7 +168,9 @@ def run(self, *args, **kwargs): except discord.LoginFailure: logger.critical("Invalid token") except discord.PrivilegedIntentsRequired: - logger.critical('Privileged intents are not explicitly granted in the discord developers dashboard.') + logger.critical( + "Privileged intents are not explicitly granted in the discord developers dashboard." + ) except Exception: logger.critical("Fatal exception", exc_info=True) finally: diff --git a/cogs/plugins.py b/cogs/plugins.py index 835aad49a9..3330d407ad 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -199,7 +199,7 @@ async def load_plugin(self, plugin): venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env user_install = " --user" if not venv else "" proc = await asyncio.create_subprocess_shell( - f'"{sys.executable}" -m pip install --upgrade{user_install} -r {req_txt} -q -q --user', + f'"{sys.executable}" -m pip install --upgrade{user_install} -r {req_txt} -q -q', stderr=PIPE, stdout=PIPE, ) From 8c5df2c5f2cd9968dd9609840eaa45a2de1ddb08 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 18:30:41 +0800 Subject: [PATCH 0622/1221] 3.6.2 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04779442b1..5248d772ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,9 @@ however, insignificant breaking changes do not guarantee a major version bump, s # v3.6.2 -### Added +### Fixed -- Proper error message if privileged intents not explicitly granted to bot. +- Plugins downloading requirements in virtual environments # v3.6.1 From 714f5316d8e647af1c2862b635eedd000eeccf80 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 28 Oct 2020 22:26:41 +0800 Subject: [PATCH 0623/1221] Cleanup folder structures --- .../CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md => .github/CONTRIBUTING.md | 0 .lint.py | 17 - .pylintrc | 511 ------------------ app.json | 66 +-- core/clients.py | 4 +- 6 files changed, 35 insertions(+), 563 deletions(-) rename CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md (100%) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (100%) delete mode 100644 .lint.py delete mode 100644 .pylintrc diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/.lint.py b/.lint.py deleted file mode 100644 index 9d29372fd8..0000000000 --- a/.lint.py +++ /dev/null @@ -1,17 +0,0 @@ -if __name__ == "__main__": - import sys - from os import listdir - from os.path import join - - from pylint.lint import Run - - THRESHOLD = 9.75 - - cogs = [join("cogs", c) for c in listdir("cogs") if c.endswith(".py")] - core = [join("core", c) for c in listdir("core") if c.endswith(".py")] - - results = Run(["bot.py", *cogs, *core], do_exit=False) - - score = results.linter.stats["global_note"] - if score <= THRESHOLD: - sys.exit(1) diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 21087a91f7..0000000000 --- a/.pylintrc +++ /dev/null @@ -1,511 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=0 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - metaclass-assignment, - missing-docstring, # No doc-string - no-name-in-module, # No name 'file' in module 'core' - fixme, - too-many-public-methods, - too-many-locals, - too-many-statements, - too-many-branches, - too-many-instance-attributes, - too-many-arguments, - too-few-public-methods, - too-many-lines, - line-too-long, - bad-continuation, - invalid-name, - logging-too-many-args - - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, while `new` is for `{}` formatting. -logging-format-style=new - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package.. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - TODO, - BUG - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=99 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - ex, - _, - id, - db, - f, - dt, - ch, - ts - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=yes - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement. -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception". -overgeneral-exceptions=BaseException diff --git a/app.json b/app.json index cd5f25825c..41697cf064 100644 --- a/app.json +++ b/app.json @@ -1,35 +1,35 @@ { - "name": "Modmail", - "description": "An easy to install Modmail bot for Discord - DM to contact mods!", - "repository": "https://github.com/kyb3r/modmail", - "env": { - "TOKEN": { - "description": "Your discord bot's token.", - "required": true - }, - "GUILD_ID": { - "description": "The id for the server you are hosting this bot for.", - "required": true - }, - "MODMAIL_GUILD_ID": { - "description": "The ID of the discord server where the threads channels should be created (receiving server). Default to GUILD_ID.", - "required": false - }, - "OWNERS": { - "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", - "required": true - }, - "CONNECTION_URI": { - "description": "The connection URI for your database.", - "required": true - }, - "DATABASE_TYPE": { - "description": "The type of your database. There is only one supported database at the moment - MongoDB (default).", - "required": false - }, - "LOG_URL": { - "description": "The url of the log viewer app for viewing self-hosted logs.", - "required": true + "name": "Modmail", + "description": "An easy to install Modmail bot for Discord - DM to contact mods!", + "repository": "https://github.com/kyb3r/modmail", + "env": { + "TOKEN": { + "description": "Your discord bot's token.", + "required": true + }, + "GUILD_ID": { + "description": "The id for the server you are hosting this bot for.", + "required": true + }, + "MODMAIL_GUILD_ID": { + "description": "The ID of the discord server where the threads channels should be created (receiving server). Default to GUILD_ID.", + "required": false + }, + "OWNERS": { + "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", + "required": true + }, + "CONNECTION_URI": { + "description": "The connection URI for your database.", + "required": true + }, + "DATABASE_TYPE": { + "description": "The type of your database. There is only one supported database at the moment - MongoDB (default).", + "required": false + }, + "LOG_URL": { + "description": "The url of the log viewer app for viewing self-hosted logs.", + "required": true + } } - } -} +} \ No newline at end of file diff --git a/core/clients.py b/core/clients.py index 1e6008e042..03921d5a17 100644 --- a/core/clients.py +++ b/core/clients.py @@ -164,7 +164,7 @@ def __init__(self, bot): db = AsyncIOMotorClient(mongo_uri).modmail_bot except ConfigurationError as e: logger.critical( - "Your MONGO_URI might be copied wrong, try re-copying from the source again. " + "Your MongoDB CONNECTION_URI might be copied wrong, try re-copying from the source again. " "Otherwise noted in the following message:" ) logger.critical(e) @@ -210,7 +210,7 @@ async def validate_database_connection(self): if "OperationFailure" in message: logger.critical( - "This is due to having invalid credentials in your MONGO_URI. " + "This is due to having invalid credentials in your MongoDB CONNECTION_URI. " "Remember you need to substitute `` with your actual password." ) logger.critical( From e65997ad2bd0f16a6fa440d41c21b13c21d0b3a9 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sun, 1 Nov 2020 17:45:02 +0800 Subject: [PATCH 0624/1221] Improve plugin install error messages --- cogs/plugins.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 3330d407ad..227cfc8270 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -166,6 +166,17 @@ async def download_plugin(self, plugin, force=False): async with self.bot.session.get(plugin.url, headers=headers) as resp: logger.debug("Downloading %s.", plugin.url) raw = await resp.read() + + try: + raw = await resp.text() + except UnicodeDecodeError: + pass + else: + if raw == 'Not Found': + raise InvalidPluginError('Plugin not found') + else: + raise InvalidPluginError('Invalid download recieved, non-bytes object') + plugin_io = io.BytesIO(raw) if not plugin.cache_path.parent.exists(): plugin.cache_path.parent.mkdir(parents=True) @@ -321,11 +332,11 @@ async def plugins_add(self, ctx, *, plugin_name: str): try: await self.download_plugin(plugin, force=True) - except Exception: + except Exception as e: logger.warning("Unable to download plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description="Failed to download plugin, check logs for error.", + description=f"Failed to download plugin, check logs for error.\n{type(e)}: {e}", color=self.bot.error_color, ) @@ -340,11 +351,11 @@ async def plugins_add(self, ctx, *, plugin_name: str): try: await self.load_plugin(plugin) - except Exception: + except Exception as e: logger.warning("Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description="Failed to download plugin, check logs for error.", + description=f"Failed to download plugin, check logs for error.\n{type(e)}: {e}", color=self.bot.error_color, ) From 39e3bc32854f365db24ee7517a7f9d844acc3042 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sun, 1 Nov 2020 17:51:33 +0800 Subject: [PATCH 0625/1221] Changelog --- CHANGELOG.md | 8 +++++++- bot.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5248d772ee..57b9d415b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. +# v3.6.3-dev0 + +### Improved + +- Plugins installations have clearer error messages + # v3.6.2 ### Fixed @@ -41,7 +47,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Bump discord.py version to 1.5.1 - Explicitly state intents used for connection -- Use `--diff` for black CI instead of `--check` ([GH#2816](https://github.com/kyb3r/modmail/issues/2816)) +- Use `--diff` for black CI instead of `--check` ([GH #2816](https://github.com/kyb3r/modmail/issues/2816)) # v3.5.0 diff --git a/bot.py b/bot.py index 15ef85429e..ab5ded6c53 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.6.2" +__version__ = "3.6.3-dev0" import asyncio From c605d68a5e73214499fe5c09446c40ca0df8e8cd Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sun, 1 Nov 2020 17:55:11 +0800 Subject: [PATCH 0626/1221] Force push to dev --- .env.example | 3 +- .../CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md => .github/CONTRIBUTING.md | 17 +- .github/ISSUE_TEMPLATE/command-request.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/workflows/lints.yml | 2 +- .github/workflows/stale.yml | 17 - .lint.py | 17 - .pylintrc | 511 ------- CHANGELOG.md | 333 +++-- Pipfile | 14 +- Pipfile.lock | 621 +++++---- Procfile | 2 +- README.md | 50 +- SPONSORS.json | 37 - app.json | 58 +- bot.py | 266 ++-- cogs/modmail.py | 385 +++--- cogs/plugins.py | 184 ++- cogs/utility.py | 438 +++--- core/clients.py | 180 ++- core/config.py | 11 +- core/config_help.json | 14 +- core/paginator.py | 2 +- core/thread.py | 64 +- core/time.py | 2 +- core/translations.py | 25 - languages/en.csv | 1172 ----------------- modmail.sh | 3 + plugins/registry.json | 133 +- poetry.lock | 157 ++- pyproject.toml | 20 +- requirements.min.txt | 28 +- runtime.txt | 2 +- translation_files.py | 127 -- 35 files changed, 1638 insertions(+), 3261 deletions(-) rename CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md (100%) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (83%) delete mode 100644 .github/workflows/stale.yml delete mode 100644 .lint.py delete mode 100644 .pylintrc delete mode 100644 core/translations.py delete mode 100644 languages/en.csv create mode 100644 modmail.sh delete mode 100644 translation_files.py diff --git a/.env.example b/.env.example index b3554bf9c9..44c91c59c7 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ TOKEN=MyBotToken LOG_URL=https://logviewername.herokuapp.com/ GUILD_ID=1234567890 +MODMAIL_GUILD_ID=1234567890 OWNERS=Owner1ID,Owner2ID,Owner3ID -MONGO_URI=mongodb+srv://mongodburi +CONNECTION_URI=mongodb+srv://mongodburi diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 83% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index a95e344610..568e5f2175 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,33 +15,32 @@ We use GitHub to host code, to track issues and feature requests, as well as acc ## We Use [Git Flow](https://atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) ![Simple Image Of A Git Flow Workflow](https://nvie.com/img/hotfix-branches@2x.png) -When contributing to this project please make sure you follow this and name your branches appropriately! +When contributing to this project, please make sure you follow this and name your branches appropriately! ## All Code Changes Happen Through Pull Requests Make sure you know how Git Flow works before contributing! Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 1. Fork the repo and create your branch from `master` or `development` according to Git Flow. -2. If you've added code that should be tested, add tests. -3. If you've changed APIs, update the documentation. -4. Ensure the test suite passes. -5. Make sure your code lints. -6. Issue that pull request! +2. Update the CHANGELOG. +3. If you've changed `core/*` or `bot.py`, mark changelog as "BREAKING" since plugins may break. +4. Make sure your code passes the lint checks. +5. Create Issues and pull requests! ## Any contributions you make will be under the GNU Affero General Public License v3.0 In short, when you submit code changes, your submissions are understood to be under the same [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project. Feel free to contact the maintainers if that's a concern. ## Report bugs using [Github Issues](https://github.com/kyb3r/modmail/issues) -We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! +We use GitHub issues to track public bugs. Report a bug by [opening a new Issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! ## Write bug reports with detail, background, and sample code **Great Bug Reports** tend to have: -- A quick summary and/or background +- A quick summary and background - Steps to reproduce - Be specific! - What you expected would happen -- What actually happens +- What *actually* happens - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) diff --git a/.github/ISSUE_TEMPLATE/command-request.md b/.github/ISSUE_TEMPLATE/command-request.md index 44902f2cf3..d3c1673a6b 100644 --- a/.github/ISSUE_TEMPLATE/command-request.md +++ b/.github/ISSUE_TEMPLATE/command-request.md @@ -1,7 +1,7 @@ --- name: Command request about: Request a new command -title: "[COMMAND-REQUEST] your title here" +title: "your title here" labels: command-request assignees: '' diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 48b986344d..7cd7a506cd 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: Suggest an idea for this project -title: "[FEATURE-REQUEST] your title here" +title: "your title here" labels: feature-request assignees: '' diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index c84c9af773..52a538bef7 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -31,4 +31,4 @@ jobs: continue-on-error: true - name: Black and flake8 run: | - black . --check + black . --diff diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index b3003ccf92..0000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Close Stale Issues" - -on: - schedule: - - cron: "0 0 * * *" - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - 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/.lint.py b/.lint.py deleted file mode 100644 index 9d29372fd8..0000000000 --- a/.lint.py +++ /dev/null @@ -1,17 +0,0 @@ -if __name__ == "__main__": - import sys - from os import listdir - from os.path import join - - from pylint.lint import Run - - THRESHOLD = 9.75 - - cogs = [join("cogs", c) for c in listdir("cogs") if c.endswith(".py")] - core = [join("core", c) for c in listdir("core") if c.endswith(".py")] - - results = Run(["bot.py", *cogs, *core], do_exit=False) - - score = results.linter.stats["global_note"] - if score <= THRESHOLD: - sys.exit(1) diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 21087a91f7..0000000000 --- a/.pylintrc +++ /dev/null @@ -1,511 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=0 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - metaclass-assignment, - missing-docstring, # No doc-string - no-name-in-module, # No name 'file' in module 'core' - fixme, - too-many-public-methods, - too-many-locals, - too-many-statements, - too-many-branches, - too-many-instance-attributes, - too-many-arguments, - too-few-public-methods, - too-many-lines, - line-too-long, - bad-continuation, - invalid-name, - logging-too-many-args - - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, while `new` is for `{}` formatting. -logging-format-style=new - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package.. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - TODO, - BUG - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=99 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - ex, - _, - id, - db, - f, - dt, - ch, - ts - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=yes - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement. -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception". -overgeneral-exceptions=BaseException diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c89216f4..57b9d415b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,93 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); -however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). +however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. +# v3.6.3-dev0 -# v3.5.0-dev0 +### Improved + +- Plugins installations have clearer error messages + +# v3.6.2 + +### Fixed + +- Plugins downloading requirements in virtual environments + + +# v3.6.1 + +### Added + +- Proper error message if privileged intents not explicitly granted to bot. + + +# v3.6.0 + +### Added + +- Added `thread_move_title` to specify title of thread moved embed. +- Mark NSFW logs in log message. ([GH #2792](https://github.com/kyb3r/modmail/issues/2792)) +- Icon for moderator that closed the thread in log message. ([GH #2828](https://github.com/kyb3r/modmail/issues/2828)) +- Ability to set mentions via user/role ID. ([GH #2796](https://github.com/kyb3r/modmail/issues/2796)) + +### Changed + +- `?move` now consumes rest in category name, which means `?move Long Category Name` works without quotes! +- `?help` shows "No command description" if no description provided. ([PR #2845](https://github.com/kyb3r/modmail/pull/2845)) + +### Fixed +- Unicode errors raised during windows selfhosting + +### Internal + +- Bump discord.py version to 1.5.1 +- Explicitly state intents used for connection +- Use `--diff` for black CI instead of `--check` ([GH #2816](https://github.com/kyb3r/modmail/issues/2816)) + + +# v3.5.0 + +Fixed discord.py issue. + +### Added + +- A confirmation when you manually delete a thread message embed. +- Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. ([GH #2803](https://github.com/kyb3r/modmail/issues/2803)) +- Added `?plugins reset` command to completely reset everything related to plugins. This will fix some problems caused by broken plugins in the file system. +- Support private GitHub repos for plugins (thanks to @officialpiyush pr#2767) + +### Changed + +- Bump discord.py version to v1.3.3. +- Renamed `bot.owner_ids` to `bot.bot_owner_ids` as the attribute is now defined internally for team support. +- Deleting channel manually will now close the thread. +- Deleting messages will no longer cause the bot to produce warnings. +- Plugins will automatically be removed when it fails to load. +- Moved all database-related activities to clients.py under MongoDBClient, with possible future hook for additional database support. +- `bot.db` is deprecated in favour of `bot.api.db` and will be removed in the future. +- Deprecated `bot.plugin_db.get_partition` in favour of `bot.api.get_plugin_partition` (not final). +- Deprecated `MONGO_URI` config var (but will keep support in the future) in favour of `CONNECTION_URI` and `DATABASE_TYPE`. Right now there is one supported database - "mongodb", which is the default. + +### Fixed + +- Plugins not loading in Windows OS. Now uses proactor event loop for asyncio which should fix this. + + +# v3.4.1 + +### Fixed + +- Masked a bunch of noise errors when deleting messages. +- Added more checks for deleting messages. + +### Breaking + +- `thread_initiate` will be dispatched at the beginning of the setup process. +- `thread_create` is dispatched when the thread is registered as a thread by Modmail (i.e., when channel topic is edited). +- `thread_ready` is dispatched when a thread finishes its setup steps. -Translations WIP. # v3.4.0 @@ -26,35 +107,35 @@ Translations WIP. - 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). -- Recipient get feedback when they edit message. +- Recipients get feedback when they edit their messages. - Chained delete for DMs now comes with a message. - poetry (in case someone needs it). ### Changed - The look of alias and snippet when previewing. -- Message ID of the thread embed is saved in DB, instead of the original message. +- The database now saves the message ID of the thread embed, 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. +- A hard limit of a maximum of 25 steps for aliases. - `?disable` is now `?disable new`. ### 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. +- Fixed many 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 add / remove. +- Chained reaction add/remove. - Chained delete for thread channels. ### Internal -- Commit to black format line width max = 99, consistent with pylint. -- Alias parser is rewritten without shlex. +- Commit to black format line width max = 99, consistent with PyLint. +- No longer requires shlex for alias parsing. - New checks with thread create / find. -- No more flake8 and travis. +- No more flake8 and Travis. # v3.3.2 @@ -78,19 +159,19 @@ Translations WIP. - Three new config vars: - `enable_plugins` (yes/no default yes) - - When set to no, plugins will not be loaded into the bot. + - When set to no, Modmail will not load plugins. - `error_color` (color format, defaults discord red) - The color of error messages. - `anon_reply_without_command` (yes/no default no) (Thanks to papiersnipper PR#288) - When set, all non-command messages sent to thread channels are forwarded to the recipient anonymously without the need of `?anonreply`. - This config takes precedence over `reply_without_command`. -- `?logs responded [user]` command, it will show all logs that the user has sent an reply. (Thanks to papiersnipper PR#288) +- `?logs responded [user]` command. It will show all the logs that the user has sent a reply. (Thanks to papiersnipper PR#288) - `user` when not provided, defaults to the user who ran the command. -- Open threads in limbo now auto closes if the channel cannot be found. This check is done every time the bot restarts. +- Open threads in limbo now auto-close if Modmail cannot find the channel. Modmail does this check every time the bot restarts. - Ability to disable new threads from getting created. - - `?disable` + - `?disable`. - Ability to fully disable Modmail DM. - - `?disable all` + - `?disable all`. - To re-enable DM: `?enable`, and to see the current status: `?isenable`. - This disabled Modmail interface is customizable with the following config vars: - `disabled_new_thread_title` @@ -104,33 +185,33 @@ Translations WIP. ### Changed -- `?contact` no longer send the "thread created" message to where the command is ran, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) +- `?contact` no longer send the "thread created" message to where the command was run, instead, it's now sent to the newly created thread channel. (Thanks to DAzVise) - Automatically delete notes command `?note` when there're no attachments attached. - Embed author links used to be inaccessible in many cases, now: - `?anonreply`, `?reply`, and `?note` in the thread channel will link to the sender's profile. - - `?reply` and recipient's DM will also link the sender's profile. + - `?reply` and the recipient's DM will also link the sender's profile. - `?anonreply` in DM channel will link to the first channel of the main guild. - Plugins update (mostly internal). - - `git` is no longer used to install plugins, it now downloads through zip files. + - `git` is no longer used to install plugins; it now downloads through zip files. - `?plugins enabled` renamed to `?plugins loaded` while `enabled` is still an alias to that command. - Reorganized plugins folder structure. - Logging / plugin-related messages change. - - Updating one plugin will not update all other plugins (plugins are no longer separated by repos, but the plugin name itself). -- Help command is in alphabetical order grouped by permissions. -- Notes are no longer always blurple, it's set to `MAIN_COLOR` now. + - Updating one plugin will not update other plugins; repositories no longer separate plugins, but the plugin name itself. +- The help command is in alphabetical order grouped by permissions. +- Notes are no longer always blurple; it's set to `MAIN_COLOR` now. - Added `?plugins update` for updating all installed plugins. - Reintroduce flake8 and use bandit for security issues detection. -- Add travis checks for 3.6 in Linux and 3.7 for MacOS and Windows. -- Eval commands are logged in debug logs. +- Add Travis checks for 3.6 in Linux and 3.7 for macOS and Windows. +- Debug logs not logs eval commands. - Presence updates 30 minutes instead of 45 now. -- Fixed an assortment of problems to do with block. +- Fixed an assortment of problems to do with `?block`. - Existing aliases can be used when creating new aliases. (Thanks to papiersnipper PR#402) ### Internal - Reworked `config.get` and `config.set`, it feeds through the converters before setting/getting. - To get/set the raw value, access through `config[]`. -- Prerelease naming scheme is now `x.x.x-devN`. +- The prerelease naming scheme is now `x.x.x-devN`. - `trigger_typing` has been moved to `core.utils.trigger_typing`, the original location is deprecated. - Simpler status and activity logic. - New logging logic. @@ -143,9 +224,9 @@ Security update! - Supporter permission users used to be able to "hack" snippets to reveal all your config vars, including your token and MongoURI. - Implemented some changes to address this bug: - - All customizable variables used in snippets, close messages, etc, using the `{}` syntax, now forbids chaining 2 or more attributes and attributes that starts with `_`. -- It is advised to update to this version. -- If you felt your credentials have been leaked, consider changing your bot token / mongo uri. + - All customizable variables used in snippets, close messages, etc., using the `{}` syntax, now forbids chaining two or more attributes and attributes that start with `_`. +- We advise you to update to this version. +- If you felt your credentials had been leaked, consider changing your bot token / MongoURI. # v3.2.1 @@ -175,7 +256,7 @@ Security update! ### Internal -- Use regex to parse Changes, Added, Fixed, etc and description. +- Use regex to parse Changes, Added, Fixed, etc. and description. - Adds `PermissionLevel.INVALID` when commands don't have a permission level. # v3.1.1 @@ -195,33 +276,33 @@ Security update! ### Added - `?sfw`, mark a thread as "safe for work", undos `?nsfw`. -- New config variable, `thread_auto_close_silently`, when set to a truthy value, no message will be sent when thread is auto-closed. +- New config variable, `thread_auto_close_silently`, when set to a truthy value, no message will be sent when a thread is auto-closed. - New configuration variable `thread_self_closable_creation_footer` — the footer when `recipient_thread_close` is enabled. - Added a minimalistic version of requirements.txt (named requirements.min.txt) that contains only the absolute minimum of Modmail. - For users having trouble with pipenv or any other reason. -- Multi-step alias, see `?help alias add`. Public beta testing, might be unstable. +- Multi-step alias, see `?help alias add`. Public beta testing might be unstable. - Misc commands without cogs are now displayed in `?help`. - `?help` works for alias and snippets. - `?config help ` shows a help embed for the configuration. -- Support setting permissions for sub commands. -- Support numbers (1-5) as substitutes for Permission Level REGULAR - OWNER in `?perms` sub commands. +- Support setting permissions for subcommands. +- Support numbers (1-5) as substitutes for Permission Level REGULAR - OWNER in `?perms` subcommands. ### Changes - `thread_auto_close_response` has a configurable variable `{timeout}`. - `?snippet` is now the default command name instead of `?snippets` (`?snippets` is still usable). This is to make this consistent with `?alias`/`?aliases`. -- `colorama` is no longer a necessity, this is due to some unsupported OS. -- Changelog command can now take a version argument to jump straight to specified version. +- `colorama` is no longer a necessity; this is due to some unsupported OS. +- Changelog command can now take a version argument to jump straight to the specified version. - `?plugin enabled` results are now sorted alphabetically. -- `?plugin registry` results are now sorted alphabetically, helps user find plugins more easily. +- `?plugin registry` results are now sorted alphabetically, helps users find plugins more easily. - `?plugin registry page-number` plugin registry can specify a page number for quick access. - A reworked interface for `?snippet` and `?alias`. - Add an `?snippet raw ` command for viewing the raw content of a snippet (escaped markdown). - - Add an `?alias raw ` command for viewing the raw content of a alias (escaped markdown). + - Add an `?alias raw ` command for displaying the raw content of an alias (escaped markdown). - The placeholder channel for the streaming status changed to https://www.twitch.tv/discordmodmail/. - Removed unclear `rm` alias for some `remove` commands. - Paginate `?config options`. -- All users configured with a permission level greater than REGULAR has access to the main Modmail category. +- All users configured with a permission level higher than REGULAR has access to the main Modmail category. - Category overrides also changes when a level is removed or added to a user or role. - `@everyone` is now accepted for `?perms add`. @@ -229,12 +310,12 @@ Security update! - `?notify` no longer carries over to the next thread. - `discord.NotFound` errors for `on_raw_reaction_add`. -- `mod_typing` ~~and `user_typing`~~ (`user_typing` is now by-design to show) will no longer show when user is blocked. +- `mod_typing` ~~and `user_typing`~~ (`user_typing` is now by-design to show) will no longer show when the user is blocked. - Better `?block` usage message. -- Resolves errors when message was sent by mods after thread is closed somehow. +- Resolved errors when mods sent messages after a thread is closed somehow. - Recipient join/leave server messages are limited to only the guild set by `GUILD_ID`. -- When creating snippets and aliases, it now checks if another snippets/aliases with the same name exists. -- Was looking for `config.json` in the wrong directory. +- When creating snippets and aliases, it now checks if other snippets/aliases with the same name exist. +- Modmail looked for `config.json` in the wrong directory. ### Internal @@ -254,13 +335,13 @@ Security update! ### Added - New commands, `?alias edit ` and `?snippets edit `. - - They can be used to edit aliases and snippets respectively. + - They can be used to edit aliases and snippets, respectively. # v3.0.2 ### Added -- New command, `?blocked whitelist `, this command prevents users from getting blocked by any means. +- A new command, `?blocked whitelist `, this command prevents users from getting blocked by any means. ### Changed @@ -270,30 +351,30 @@ Security update! ### Fixed -- A lot of bugs with `thread_auto_close` 😅 +- Many bugs with `thread_auto_close`. # v3.0.0 ### Added -- Sponsors command that will list sponsors. -- An alert will now be sent to the log channel if a thread channel fails to create. This could be due to a variety of problems such as insufficient permissions or the category channel limit is met. +- `?sponsors` command will list sponsors. +- An alert will now be sent to the log channel if a thread channel fails to create. This could be due to a variety of problems such as insufficient permissions, or the category channel limit is met. - Threads will close automatically after some time when `thread_auto_close` is set. -- Custom closing message can be set with `thread_auto_close_response`. +- Custom closing messages can be configured with `thread_auto_close_response`. ### Breaking Changes -- Removed autoupdate functionality and the `?update` command in favour of the [Pull app](https://github.com/apps/pull). +- Removed auto-update functionality and the `?update` command in favor of the [Pull app](https://github.com/apps/pull). Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/updating) ### Changed -- Channel names now can contain unicode characters. -- Debug logs are now located in a unique file for each bot. (Internal change) +- Channel names now can contain Unicode characters. +- Debug logs are now located in a different file for each bot. (Internal change) - Default cogs always appear first in the help command now. ### Fixed -- Editing notes now works, minor bug with edit command is fixed. +- Editing notes now work, minor bug with edit command is fixed. - Bug in the `?oauth` command where the response message fails to send when an ID is provided. - Plugin requirement installation now works in virtual environments @@ -308,7 +389,7 @@ Fixed a bug with branches and `?plugin update`. ### Added -Branch support for `?plugin add` and in registry. Typically for developers. +Branch support for `?plugin add` and in the registry. Typically for developers. # v2.23.0 @@ -316,11 +397,11 @@ Branch support for `?plugin add` and in registry. Typically for developers. Added a "Mutual servers" field to the genesis embed if: a) The user is not in the main guild. -b) The user shares more than 1 server with the bot. +b) The user shares more than one server with the bot. ### Changed -Notes taken with the `?note` command are now automatically pinned within the thread channel. +Notes with the `?note` command are now automatically pinned within the thread channel. # v2.22.0 @@ -362,14 +443,14 @@ Add your plugin in the `plugins/registry.json` file in the main repository. This update contains mostly internal changes. - Implemented support for the new discord.py v1.1.1. - Improved help text for most commands. - - Completely revamped help command, few user changes. - - Removed abc (internal). + - Completely revamped help command, few users changes. + - Removed ABC (internal). # v2.20.0 ### What's new? -New `oauth` whitelist command which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app check the logviewer [repo](https://github.com/kyb3r/logviewer). +New `?oauth whitelist` command, which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app, check the logviewer [repo](https://github.com/kyb3r/logviewer). # v2.19.1 @@ -377,7 +458,7 @@ New `oauth` whitelist command which allows you to whitelist users so they can lo - Ability to force an update despite having the same version number. Helpful to keep up-to-date with the latest GitHub commit. - `?update force`. -- Plugin developers now have a new event called `on_plugin_ready`, this is coroutine is awaited when all plugins are loaded. Use `on_plugin_ready` instead of `on_ready` since `on_ready` will not get called in plugins. +- Plugin developers now have a new event called `on_plugin_ready`; this is a coroutine and is awaited when all plugins are loaded. Use `on_plugin_ready` instead of `on_ready` since `on_ready` will not get called in plugins. # v2.19.0 @@ -398,17 +479,17 @@ Fix the teams permission bug. ### Changed -Commands now have better error messages, instead of just sending the help message for a command when an argument fails to be converted to its specified object, the bot now says things like "User 'bob' not found" instead. +Commands now have better error messages. Instead of sending the help message for a command when an argument fails to be converted, the bot now says like "User 'bob' not found" instead. # v2.18.1 -Un-deprecated the `OWNERS` config variable to support discord developer team accounts. +Un-deprecated the `OWNERS` config variable to support Discord developer team accounts. # v2.18.0 ### New Permissions System -- A brand new permission system! Replacing the old guild-based permissions (ie. manage channels, manage messages), the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. +- A brand new permission system! Replaced the old guild-based permissions (i.e., manage channels, manage messages), with the new system enables you to customize your desired permission level specific to a command or a group of commands for a role or user. - There are five permission levels: - Owner [5] - Administrator [4] @@ -427,7 +508,7 @@ You may add a role or user to a permission group through any of the following me The same applies to individual commands permissions: - `?permissions add command command-name @member#1234` -- ... and the other methods listed above. +- and the other methods listed above. To revoke permission, use `remove` instead of `add`. @@ -440,11 +521,11 @@ By default, all newly set up Modmail will have `OWNER` set to the owner of the b ### Breaking When updating to this version, all prior permission settings with guild-based permissions will be invalidated. You will need to convert to the above system. -`OWNERS` will also get removed, you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. +`OWNERS` will also get removed; you will need to set owners through `?permissions add level owner 212931293123129` or any way listed above. ### New Command -- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer requires manage messages permission to recall thread messages. +- A `?delete` command, which is an alternative to manually deleting a message. This command is created to no longer require "manage messages" permission to recall thread messages. ### Changed @@ -465,28 +546,28 @@ Stricter fallback genesis embed search. ### Changed -How modmail checks if a channel is a thread: +How Modmail checks if a channel is a thread: -1. The bot first checks if the channel topic is in the format `User ID: xxxx`, this means it is a thread. -2. If a channel topic is not found, the bot searches through the message history of a channel to find the thread creation embed. This step should never yield a thread for a normal user, but in the case of another bot messing up the channel topic (happened to a user before) this extra step was added. +1. The bot first checks if the channel topic is in the format `User ID: XXXX`, this means it is a thread. +2. If a channel topic is not found, the bot searches through the message history of a channel to find the thread creation embed. This step should never yield a thread for an average user. Still, in the case of another bot messing up the channel topic (happened to a user before), this extra step was added. # v2.17.0 ### What's new? -Added a config option `reply_without_command` which when present, enables the bot to forward any message sent in a thread channel to the recipient. (Replying without using a command) +Added a config option `reply_without_command`, which, when present, enables the bot to forward any message sent in a thread channel to the recipient. (Replying without using a command) To enable this functionality, do `?config set reply_without_command true` and to disable it, use `?config del reply_without_command`. ### Changed -The `move` command now only requires `manage_messages` perms instead of `manage_channels` +The `move` command now only requires `manage_messages` perms instead of `manage_channels`. # v2.16.1 ### Fixed -An issue where a scheduled close would not execute over a long period of time if the recipient no shares any servers with the bot. +An issue where a scheduled close would not execute over a long time if the recipient no shares any servers with the bot. # v2.16.0 @@ -516,8 +597,8 @@ Added the ability to change the default close message via the introduction of tw They will be provided by string variables that you can incorporate into them: - `closer` - the user object that closed the thread. -- `logkey` - the key for the thread logs e.g. (`5219ccc82ad4`) -- `loglink` - the full link to the thread logs e.g. (`https://logwebsite.com/logs/5219ccc82ad4`) +- `logkey` - the key for the thread logs, e.g. (`5219ccc82ad4`) +- `loglink` - the full link to the thread logs, e.g. (`https://logwebsite.com/logs/5219ccc82ad4`) Example usage would be: ``?config set thread_close_message {closer.mention} closed the thread, here is the link to your logs: [**`{logkey}`**]({loglink})`` @@ -546,9 +627,9 @@ You now have complete control of the look of the thread creation and close embed ### What's new? -Added the ability to disable the `sent_emoji` and `blocked_emoji` when a user messages modmail. +Added the ability to disable the `sent_emoji` and `blocked_emoji` when a user messages Modmail. -You can do this via `?config set sent_emoji disable` +You can do this via `?config set sent_emoji disable`. ### Fixed @@ -565,17 +646,17 @@ Added image link in title in case discord fails to embed an image. - Introduced a new configuration variable `account_age` for setting a minimum account creation age. - Users blocked by this reason will be stored in `blocked` along with other reasons for being blocked. - `account_age` needs to be an ISO-8601 Duration Format (examples: `P12DT3H` 12 days and 3 hours, `P3Y5M` 3 years and 5 months `PT4H14M999S` 4 hours 14 minutes and 999 seconds). https://en.wikipedia.org/wiki/ISO_8601#Durations. - - You can set `account_age` using `config set account_age time` where "time" can be a simple human readable time string or an ISO-8601 Duration Format string. + - You can set `account_age` using `config set account_age time` where "time" can be a simple human-readable time string or an ISO-8601 Duration Format string. ### Changed -- `block` reason cannot start with `System Message: ` as it is now reserved for internal user blocking. -- `block`, like `close`, now supports a block duration (temp blocking). +- `?block` reason cannot start with `System Message: ` as it is now reserved for internal user blocking. +- `?block`, like `?close`, now supports a block duration (temp blocking). # v2.13.10 ### Fixed - Fixed an issue where status and activity do not work if they were modified wrongly in the database. - - This was especially an issue for older Modmail users, as the old `status` configuration variable clashes with the new `status` variable. + - This was primarily an issue for older Modmail users, as the old `status` configuration variable clashes with the new `status` variable. # v2.13.9 @@ -594,7 +675,7 @@ Added image link in title in case discord fails to embed an image. ### What's new? - The ability to enable typing interactions. - - If you want the bot to type in the thread channel if the user is also typing, add the config variable `user_typing` and set it to "yes" or "true". use `config del` to disable the functionality. The same thing in reverse is also possible if you want the user to see the bot type when someone is typing in the thread channel add the `mod_typing` config variable. + - If you want the bot to type in the thread channel if the user is also typing, add the config variable `user_typing` and set it to "yes" or "true". Use `config del` to disable the functionality. The same thing in reverse is also possible if you want the user to see the bot type when someone is typing in the thread channel add the `mod_typing` config variable. - New `status` command, change the bot's status to `online`, `idle`, `dnd`, `invisible`, or `offline`. - To remove the status (change it back to default), use `status clear`. - This also introduces a new internal configuration variable: `status`. Possible values are `online`, `idle`, `dnd`, `invisible`, and `offline`. @@ -611,15 +692,15 @@ Added image link in title in case discord fails to embed an image. ### What's new? - You will no longer need to view your bot debug logs from Heroku. `debug` will show you the recent logs within 24h through a series of embeds. - - If you don't mind your data (may or may not be limited to: user ID, guild ID, bot name) be on the internet, `debug hastebin` will upload a formatted logs file to https://hasteb.in. + - If you don't mind your data (may or may not be limited to user ID, guild ID, bot name) be on the internet, `debug hastebin` will upload a formatted logs file to https://hasteb.in. - `debug clear` will clear the locally cached logs. - - Local logs are automatically cleared at least once every 27h for bots hosted on Heroku. + - Local logs are automatically erased at least once every 27h for bots hosted on Heroku. ### Fixed -- Will no longer show `Unclosed client session` and `Task was destroyed but it is pending!` when the bot terminates. +- Will no longer show `Unclosed client session` and `Task was destroyed, but it is pending!` when the bot terminates. - `thread.create` is now synchronous so that the first message sent can be queued to be sent as soon as a thread is created. - This fixes a problem where if multiple messages are sent in quick succession, the first message sent (which triggers the thread creation) is not sent in order. -- Trying to reply to someone who has DMs disabled or has blocked the bot is now handled and the bot will send a message saying so. +- Trying to reply to someone who has DMs disabled or has blocked the bot is now handled, and the bot will send a message saying so. ### Changed - `print` is replaced by logging. @@ -653,14 +734,14 @@ Added image link in title in case discord fails to embed an image. ### What's new? - Plugins: - - Think of it like addons! Anyone (with the skills) can create a plugin, make it public and distribute it. Add a welcome message to Modmail, or moderation commands? It's all up to your imagination! Have a niche feature request that you think only your server would benefit from? Plugins are your go-to! + - Think of it like addons! Anyone (with the skills) can create a plugin, make it public and distribute it. Add a welcome message to Modmail, or moderation commands? It's all up to your imagination! Have a niche feature request that you think only your server would benefit? Plugins are your go-to! - [Creating Plugins Documentation](https://github.com/kyb3r/modmail/wiki/Plugins). # v2.12.5 ### Fixed -- `config del` command will now work properly on self-hosted db bots. +- `config del` command will now work correctly on self-hosted DB bots. # v2.12.4 @@ -674,14 +755,14 @@ Added image link in title in case discord fails to embed an image. ### Fixed - Patched a bug where `logs` sub-commands were accessible by anyone. -- Patched a bug where an error was raised if there was an open thread where the recipient had left the server. +- Patched a bug where an error was raised when a thread is open where the recipient left the server. Huge thanks to Sasiko for reporting these issues. # v2.12.2 ### Fixed -- Fixed a bug in self-hosted `update` command. +- Fixed a bug in self-hosted `?update` command. # v2.12.1 @@ -692,12 +773,12 @@ Huge thanks to Sasiko for reporting these issues. # v2.12.0 ### Important -**In the future, the Modmail API (https://modmail.tk) will be deprecated. This is due to the fact that we are providing a free service without getting anything in return, and thus we do not have the resources to scale to accommodate for more users. +**In the future, the Modmail API (https://modmail.tk) will be deprecated. This is because we are providing free service without getting anything in return. Thus we do not have the resources to scale to accommodate more users. We recommend using your own database for logs. In the future you will soon get a `backup` command so you can download all your pre-existing data and migrate to your own database.** ### Changed - A lot of painful code cleanup, which is good for us (the developers), but shouldn't affect you. -- The appearance of the `logs` command. Should be clearer with better info now. +- The appearance of the `?logs` command. It should be clearer with better info now. - Bot owners get access to all commands regardless of server permissions. - Blocked users no longer receive a message, only the blocked emoji will be sent. @@ -705,10 +786,10 @@ We recommend using your own database for logs. In the future you will soon get a - **Note:** The following commands only work if you are self-hosting your logs. We recommend you to use your own database. - Log search queries, in the form of two new commands. - `logs search [query]` - this searches all log messages for a query string. -- `logs closed-by [user]` this returns all logs closed by a certain user +- `logs closed-by [user]` this returns all logs closed by a particular user ### Fixed -- `activity listening to music` no longer result in two "to"s ("listening to to music"). +- `activity listening to music` no longer results in two "to"s ("listening to to music"). - This may require you to change your activity message to accommodate this fix. - A problem where `main_category_id` and `log_channel_id` weren't updated when their corresponding channel or category get deleted. @@ -731,7 +812,7 @@ We recommend using your own database for logs. In the future you will soon get a ### What's new? - `anonreply` command to anonymously reply to the recipient. -The username of the anonymous user defaults to the `mod_tag` (the footer text of a mod reply message). The avatar defaults the guild icon URL. However you can change both of these via the `anon_username`, `anon_avatar_url` and `anon_tag` config variables. +The username of the anonymous user defaults to the `mod_tag` (the footer text of a mod reply message) — the avatar defaults to the guild icon URL. However, you can change both of these via the `anon_username`, `anon_avatar_url`, and `anon_tag` config variables. ### Changed - Your bot now logs all messages sent in a thread channel, including discussions that take place. You can now toggle to view them in the log viewer app. @@ -756,7 +837,7 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of - All commands are now blurple instead of green. ### Fixed -- Bug where the close command wouldn't work if you didnt configure a log channel. +- Bug where the close command wouldn't work if you didn't configure a log channel. ### What's new? - Ability to set your own custom `mod_color` and `recipient_color` for the thread message embeds. @@ -773,13 +854,13 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of # v2.9.0 ### What's new? -- New command `note` will add a system message to your thread logs. This is useful for noting the context of a conversation. +- New command `note` will add a system message to your thread logs. - - This is useful for noting the context of a conversation. # v2.8.1 ### Fixed - Fixed bug where thread logs were getting duplicated when using the `contact` command. -- Fixed bug where the wrong key was used for logs which caused some `log` command log links to point to an HTTP 404 Not Found. +- Fixed bug where the wrong key was used for logs, which caused some `log` command log links to point to an HTTP 404 Not Found. - A minor oversight from commit 1ba74d9. # v2.8.0 @@ -796,7 +877,7 @@ The username of the anonymous user defaults to the `mod_tag` (the footer text of ### Security Thread channels will now default to being private (`@everyone`'s read message perms set to `false`). - If the thread creation category could not be resolved. - - This will save you from some trouble if for whatever reason your configuration gets messed up. + - This will save you from some trouble if, for whatever reason, your configuration gets messed up. # v2.7.1 @@ -808,20 +889,20 @@ Thread channels will now default to being private (`@everyone`'s read message pe ### Note -- If your Modmail bot was set up a long time ago, you may experience an issue where messages were sent outside of the category. +- If your Modmail bot was set up a long time ago, you might experience an issue where messages were sent outside of the category. - To fix this, set `main_category_id` to the ID of the Modmail category. # v2.7.0 ### Changed -- `move` command now syncs thread channel permissions with the category that it was moved to. +- `move` command now syncs thread channel permissions with the destination category. - `contact` command now supports an optional category argument (where the thread channel will be created). # v2.6.3 ### Fixes -- Fixed small issue with finding thread. +- Fixed small issue with finding threads. # v2.6.2 @@ -841,8 +922,8 @@ Thread channels will now default to being private (`@everyone`'s read message pe ### Changed - Log URLs are moved to their own collection. - Log URLs are now `https://logs.modmail.tk/LOGKEY`, no more numbers before the log key. -- We still support the numbers so as to not break everyone's URLs so quickly but both work at the moment. -- This is a huge change to the backend logging and there might be migration errors. If so, please contact us in our [discord server](https://discord.gg/2fMbf2N). +- We still support the numbers to not break everyone's URLs so quickly, but both work at the moment. +- This is a huge change to the backend logging, and there might be migration errors. If so, please contact us in our [Discord server](https://discord.gg/2fMbf2N). # v2.5.2 @@ -852,13 +933,13 @@ Thread channels will now default to being private (`@everyone`'s read message pe # v2.5.1 ### Fixes -- Emergency patch to save config. +- Emergency patch to save configs. # v2.5.0 ### Background - Bots hosted by Heroku restart at least once every 27 hours. -- During this period, local caches are deleted, which results in the inability to set the scheduled close time to longer than 24 hours. This update resolves this issue. +- During this period, local caches will be deleted, which results in the inability to set the scheduled close time to longer than 24 hours. This update resolves this issue. - [PR #135](https://github.com/kyb3r/modmail/pull/135) ### Changed @@ -876,7 +957,7 @@ Fixed activity setting due to flawed logic in `config.get()` function. # v2.4.4 ### Fixed -Fixed a bug in activity command where it would fail to set the activity on bot restart if the activity type was `playing`. +Fixed a bug in the `?activity` command where it would fail to set the activity on bot restart if the activity type was `playing`. # v2.4.3 @@ -891,17 +972,17 @@ Fixed a bug in activity command where it would fail to set the activity on bot r # v2.4.1 ### Fixed -- Small bug in `activity` command. +- Small bug in `?activity` command. # v2.4.0 ### What's new? -- Added the `activity` command for setting the activity -- [PR #131](https://github.com/kyb3r/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening` and `streaming`). +- Added the `?activity` command for setting the activity +- [PR #131](https://github.com/kyb3r/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening`, and `streaming`). ### Removed - Removed the deprecated `status` command. -- This also means you will have to reset your bot status with the `activity` command, as `status` command is removed. +- This also means you will have to reset your bot status with the `?activity` command, as the `?status` command was removed. # v2.3.0 @@ -922,7 +1003,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### What's new? - Notify command `notify [role]`. - Notify a given role or yourself to the next thread message received. - - Once a thread message is received you will be pinged once only. + - Once a thread message is received, you will be pinged once only. - Subscribe command `sub [role]` / `unsub [role]`. - Subscribes yourself or a given role to be notified when thread messages are received. @@ -939,15 +1020,15 @@ Fixed a bug in activity command where it would fail to set the activity on bot r # v2.1.0 ### What's new? -- Ability to set a custom thread creation response message. +- Ability to set a custom thread-creation-response message. - Via `config set thread_creation_response [message]`. ### Changed -- Improve logs command format. -- Improve thread log channel message to have more relevant info. +- Improve `?logs` command format. +- Improve thread log channel messages to have more relevant info. - Improve close command. - - You now can close the thread after a delay and use a custom thread close message. - - You also now have the ability to close a thread silently. + - You can now close the thread after a delay and use a custom thread close message. + - You also now can close a thread silently. # v2.0.10 @@ -962,7 +1043,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Fixes - Support multiple images and file attachments in one message. -- This is only possible on mobile so its good to handle it in code. +- This is only possible on mobile, so its good to handle it in code. # v2.0.8 @@ -973,7 +1054,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r - You can do this via the `config set main_category_id ` command. ### Changed -- You now have the ability to supply a reason when blocking a user. +- You can now supply a reason when blocking a user. - Blocked users are now stored in the database instead of in the channel topic. - This means you can delete the top channel in the Modmail category now (after migrating the currently blocked users). @@ -984,8 +1065,8 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Changed - `update` command now shows the latest changes directly from CHANGELOG.md. -- Auto update messages also show the latest changes from the GitHub repo. -- Removed "latest changes" section from the `about` command. +- Auto-update messages also show the latest changes from the GitHub repo. +- Removed the "latest changes" section from the `about` command. # v2.0.6 @@ -998,20 +1079,20 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Changed - `alias` command now checks if you are adding a valid alias-command combo. -- Deleting a channel manually will now correctly close the thread and post logs. +- Manually deleting a channel will now correctly close the thread and post logs. # v2.0.4 ### Fixed -- Fixed a one-off bug where the channel topic disappears, but Modmail operations should still continue. +- Fixed a one-off bug where the channel topic disappears, but Modmail operations should continue. - Fixed `linked_message_id` issues. # v2.0.3 ### Fixed -- Thread creation embed now shows the correct number of past logs. +- The thread creation embed now shows the correct number of past logs. - If using a separate server setup, roles in the info embed now are shown as names instead of mentions. - - This is due to the fact that you can't mention roles across servers. + - This is because you can't mention roles across servers. # v2.0.2 @@ -1023,7 +1104,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r ### Changed - Improved `block` / `unblock` commands. - - They now take a wider range of arguments: usernames, nicknames, mentions and user IDs. + - They now take a more comprehensive range of arguments: usernames, nicknames, mentions, and user IDs. ### Fixed - Setup command now configures permissions correctly so that the bot will always be able to see the main operations category. @@ -1031,7 +1112,7 @@ Fixed a bug in activity command where it would fail to set the activity on bot r # v2.0.0 This release introduces the use of our centralized [API service](https://github.com/kyb3r/webserver) to enable dynamic configuration, auto-updates, and thread logs. -To use this release you must acquire an API token from https://modmail.tk. +To use this release, you must acquire an API token from https://modmail.tk. Read the updated installation guide [here](https://github.com/kyb3r/modmail/wiki/installation). ### Changed diff --git a/Pipfile b/Pipfile index 9173c3a5fe..01485b1e63 100644 --- a/Pipfile +++ b/Pipfile @@ -4,27 +4,25 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -black = "==19.3b0" +black = "==19.10b0" pylint = "*" bandit = "==1.6.2" +flake8 = "*" [packages] colorama = ">=0.4.0" python-dateutil = ">=2.7.0" emoji = ">=0.2" -uvloop = {version=">=0.12.0", sys_platform = "!= 'win32'"} +uvloop = {version = ">=0.12.0",sys_platform = "!= 'win32'"} motor = ">=2.0.0" natural = "==0.2.0" isodate = ">=0.6.0" dnspython = "~=1.16.0" -parsedatetime = "==2.5" -aiohttp = "<3.6.0,>=3.3.0" +parsedatetime = "==2.6" +aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = "==1.2.5" - -[requires] -python_version = "3.7" +"discord.py" = "==1.5.1" [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 06d263645b..005ce37f67 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "c2eb0898f236534a02cb1c198d74c82fed052b4445e39f99c1af3e58d22aa435" + "sha256": "0491618cb8bd6d70e4ab3337c23b72fc3b1d5f9ca0603d8be6b890b661039102" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -18,52 +16,52 @@ "default": { "aiohttp": { "hashes": [ - "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55", - "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed", - "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10", - "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5", - "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1", - "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939", - "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390", - "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa", - "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc", - "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5", - "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d", - "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf", - "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6", - "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72", - "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12", - "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366", - "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4", - "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300", - "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d", - "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303", - "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6", - "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889" + "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe", + "sha256:206c0ccfcea46e1bddc91162449c20c72f308aebdcef4977420ef329c8fcc599", + "sha256:2ad493de47a8f926386fa6d256832de3095ba285f325db917c7deae0b54a9fc8", + "sha256:319b490a5e2beaf06891f6711856ea10591cfe84fe9f3e71a721aa8f20a0872a", + "sha256:470e4c90da36b601676fe50c49a60d34eb8c6593780930b1aa4eea6f508dfa37", + "sha256:60f4caa3b7f7a477f66ccdd158e06901e1d235d572283906276e3803f6b098f5", + "sha256:66d64486172b032db19ea8522328b19cfb78a3e1e5b62ab6a0567f93f073dea0", + "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c", + "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645", + "sha256:797456399ffeef73172945708810f3277f794965eb6ec9bd3a0c007c0476be98", + "sha256:a885432d3cabc1287bcf88ea94e1826d3aec57fd5da4a586afae4591b061d40d", + "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81", + "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5" ], "index": "pypi", - "version": "==3.5.4" + "version": "==3.6.3" + }, + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "version": "==19.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" }, "certifi": { "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" ], - "version": "==2019.11.28" + "version": "==2020.6.20" }, "chardet": { "hashes": [ @@ -74,18 +72,26 @@ }, "colorama": { "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], "index": "pypi", - "version": "==0.4.3" + "version": "==0.4.4" }, "discord.py": { "hashes": [ - "sha256:7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d" + "sha256:2367359e31f6527f8a936751fc20b09d7495dd6a76b28c8fb13d4ca6c55b7563", + "sha256:def00dc50cf36d21346d71bc89f0cad8f18f9a3522978dc18c7796287d47de8b" ], "index": "pypi", - "version": "==1.2.5" + "version": "==1.5.1" + }, + "distlib": { + "hashes": [ + "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", + "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" + ], + "version": "==0.3.1" }, "dnspython": { "hashes": [ @@ -97,17 +103,25 @@ }, "emoji": { "hashes": [ - "sha256:60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174" + "sha256:e42da4f8d648f8ef10691bc246f682a1ec6b18373abfd9be10ec0b398823bd11" ], "index": "pypi", - "version": "==0.5.4" + "version": "==0.6.0" + }, + "filelock": { + "hashes": [ + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + ], + "version": "==3.0.12" }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "isodate": { "hashes": [ @@ -119,34 +133,34 @@ }, "motor": { "hashes": [ - "sha256:599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909", - "sha256:756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4", - "sha256:97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a" + "sha256:428d94750123d19fcd0a89b8671ff9b4656f205217bad9f44161748c64c5fc80", + "sha256:f1692b760d834707e3477996ce8d407af8cd61c1a2abedbf81c22ef14675e61a" ], "index": "pypi", - "version": "==2.1.0" + "version": "==2.3.0" }, "multidict": { "hashes": [ - "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" + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + ], + "markers": "python_version >= '3.5'", + "version": "==4.7.6" }, "natural": { "hashes": [ @@ -157,78 +171,79 @@ }, "parsedatetime": { "hashes": [ - "sha256:3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", - "sha256:d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667" + "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", + "sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b" ], "index": "pypi", - "version": "==2.5" + "version": "==2.6" }, "pipenv": { "hashes": [ - "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330", - "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e", - "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846" + "sha256:448ac3a36443db633d52a2359cac15ecbc4f429eab4ddd420697602b721d1c5a", + "sha256:eff0e10eadb330f612edfa5051d3d8e775e9e0e918c3c50361da703bd0daa035" ], "index": "pypi", - "version": "==2018.11.26" + "version": "==2020.8.13" }, "pymongo": { "hashes": [ - "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" + "sha256:03dc64a9aa7a5d405aea5c56db95835f6a2fa31b3502c5af1760e0e99210be30", + "sha256:05fcc6f9c60e6efe5219fbb5a30258adb3d3e5cbd317068f3d73c09727f2abb6", + "sha256:076a7f2f7c251635cf6116ac8e45eefac77758ee5a77ab7bd2f63999e957613b", + "sha256:137e6fa718c7eff270dbd2fc4b90d94b1a69c9e9eb3f3de9e850a7fd33c822dc", + "sha256:1f865b1d1c191d785106f54df9abdc7d2f45a946b45fd1ea0a641b4f982a2a77", + "sha256:213c445fe7e654621c6309e874627c35354b46ef3ee807f5a1927dc4b30e1a67", + "sha256:25e617daf47d8dfd4e152c880cd0741cbdb48e51f54b8de9ddbfe74ecd87dd16", + "sha256:3d9bb1ba935a90ec4809a8031efd988bdb13cdba05d9e9a3e9bf151bf759ecde", + "sha256:40696a9a53faa7d85aaa6fd7bef1cae08f7882640bad08c350fb59dee7ad069b", + "sha256:421aa1b92c291c429668bd8d8d8ec2bd00f183483a756928e3afbf2b6f941f00", + "sha256:4437300eb3a5e9cc1a73b07d22c77302f872f339caca97e9bf8cf45eca8fa0d2", + "sha256:455f4deb00158d5ec8b1d3092df6abb681b225774ab8a59b3510293b4c8530e3", + "sha256:475a34a0745c456ceffaec4ce86b7e0983478f1b6140890dff7b161e7bcd895b", + "sha256:4797c0080f41eba90404335e5ded3aa66731d303293a675ff097ce4ea3025bb9", + "sha256:4ae23fbbe9eadf61279a26eba866bbf161a6f7e2ffad14a42cf20e9cb8e94166", + "sha256:4b32744901ee9990aa8cd488ec85634f443526def1e5190a407dc107148249d7", + "sha256:50127b13b38e8e586d5e97d342689405edbd74ad0bd891d97ee126a8c7b6e45f", + "sha256:50531caa7b4be1c4ed5e2d5793a4e51cc9bd62a919a6fd3299ef7c902e206eab", + "sha256:63a5387e496a98170ffe638b435c0832c0f2011a6f4ff7a2880f17669fff8c03", + "sha256:68220b81850de8e966d4667d5c325a96c6ac0d6adb3d18935d6e3d325d441f48", + "sha256:689142dc0c150e9cb7c012d84cac2c346d40beb891323afb6caf18ec4caafae0", + "sha256:6a15e2bee5c4188369a87ed6f02de804651152634a46cca91966a11c8abd2550", + "sha256:7122ffe597b531fb065d3314e704a6fe152b81820ca5f38543e70ffcc95ecfd4", + "sha256:7307024b18266b302f4265da84bb1effb5d18999ef35b30d17592959568d5c0a", + "sha256:7a4a6f5b818988a3917ec4baa91d1143242bdfece8d38305020463955961266a", + "sha256:83c5a3ecd96a9f3f11cfe6dfcbcec7323265340eb24cc996acaecea129865a3a", + "sha256:890b0f1e18dbd898aeb0ab9eae1ab159c6bcbe87f0abb065b0044581d8614062", + "sha256:8deda1f7b4c03242f2a8037706d9584e703f3d8c74d6d9cac5833db36fe16c42", + "sha256:8ea13d0348b4c96b437d944d7068d59ed4a6c98aaa6c40d8537a2981313f1c66", + "sha256:91e96bf85b7c07c827d339a386e8a3cf2e90ef098c42595227f729922d0851df", + "sha256:96782ebb3c9e91e174c333208b272ea144ed2a684413afb1038e3b3342230d72", + "sha256:9755c726aa6788f076114dfdc03b92b03ff8860316cca00902cce88bcdb5fedd", + "sha256:9dbab90c348c512e03f146e93a5e2610acec76df391043ecd46b6b775d5397e6", + "sha256:9ee0eef254e340cc11c379f797af3977992a7f2c176f1a658740c94bf677e13c", + "sha256:9fc17fdac8f1973850d42e51e8ba6149d93b1993ed6768a24f352f926dd3d587", + "sha256:a2787319dc69854acdfd6452e6a8ba8f929aeb20843c7f090e04159fc18e6245", + "sha256:b7c522292407fa04d8195032493aac937e253ad9ae524aab43b9d9d242571f03", + "sha256:bd312794f51e37dcf77f013d40650fe4fbb211dd55ef2863839c37480bd44369", + "sha256:c0d660a186e36c526366edf8a64391874fe53cf8b7039224137aee0163c046df", + "sha256:c4869141e20769b65d2d72686e7a7eb141ce9f3168106bed3e7dcced54eb2422", + "sha256:cc4057f692ac35bbe82a0a908d42ce3a281c9e913290fac37d7fa3bd01307dfb", + "sha256:cccf1e7806f12300e3a3b48f219e111000c2538483e85c869c35c1ae591e6ce9", + "sha256:ce208f80f398522e49d9db789065c8ad2cd37b21bd6b23d30053474b7416af11", + "sha256:d0565481dc196986c484a7fb13214fc6402201f7fb55c65fd215b3324962fe6c", + "sha256:d1b3366329c45a474b3bbc9b9c95d4c686e03f35da7fd12bc144626d1f2a7c04", + "sha256:d226e0d4b9192d95079a9a29c04dd81816b1ce8903b8c174a39224fe978547cb", + "sha256:d38b35f6eef4237b1d0d8e845fc1546dad85c55eba447e28c211da8c7ef9697c", + "sha256:d64c98277ea80e4484f1332ab107e8dfd173a7dcf1bdbf10a9cccc97aaab145f", + "sha256:d9de8427a5601799784eb0e7fa1b031aa64086ce04de29df775a8ca37eedac41", + "sha256:e6a15cf8f887d9f578dd49c6fb3a99d53e1d922fdd67a245a67488d77bf56eb2", + "sha256:e8c446882cbb3774cd78c738c9f58220606b702b7c1655f1423357dc51674054", + "sha256:e8d188ee39bd0ffe76603da887706e4e7b471f613625899ddf1e27867dc6a0d3", + "sha256:ef76535776c0708a85258f6dc51d36a2df12633c735f6d197ed7dfcaa7449b99", + "sha256:f6efca006a81e1197b925a7d7b16b8f61980697bb6746587aad8842865233218" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==3.11.0" }, "python-dateutil": { "hashes": [ @@ -240,18 +255,19 @@ }, "python-dotenv": { "hashes": [ - "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093", - "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544" + "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", + "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" ], "index": "pypi", - "version": "==0.10.3" + "version": "==0.14.0" }, "six": { "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.13.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" }, "uvloop": { "hashes": [ @@ -265,94 +281,72 @@ "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" ], - "index": "pypi", "markers": "sys_platform != 'win32'", "version": "==0.14.0" }, "virtualenv": { "hashes": [ - "sha256:116655188441670978117d0ebb6451eb6a7526f9ae0796cc0dee6bd7356909b0", - "sha256:b57776b44f91511866594e477dd10e76a6eb44439cdd7f06dcd30ba4c5bd854f" + "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", + "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" ], - "version": "==16.7.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.1.0" }, "virtualenv-clone": { "hashes": [ - "sha256:532f789a5c88adf339506e3ca03326f20ee82fd08ee5586b44dc859b5b4468c5", - "sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557" - ], - "version": "==0.5.3" - }, - "websockets": { - "hashes": [ - "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", - "sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6", - "sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1", - "sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538", - "sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4", - "sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908", - "sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0", - "sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d", - "sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c", - "sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d", - "sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c", - "sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb", - "sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf", - "sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e", - "sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96", - "sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584", - "sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484", - "sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d", - "sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559", - "sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff", - "sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454" - ], - "version": "==6.0" + "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", + "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.5.4" }, "yarl": { "hashes": [ - "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" + "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", + "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", + "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", + "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", + "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", + "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", + "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", + "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", + "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", + "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", + "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", + "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", + "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", + "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", + "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", + "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", + "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + ], + "markers": "python_version >= '3.5'", + "version": "==1.5.1" } }, "develop": { "appdirs": { "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" ], - "version": "==1.4.3" + "version": "==1.4.4" }, "astroid": { "hashes": [ - "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", - "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" + "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", + "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], - "version": "==2.3.3" + "markers": "python_version >= '3.5'", + "version": "==2.4.2" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" ], - "version": "==19.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" }, "bandit": { "hashes": [ @@ -364,39 +358,59 @@ }, "black": { "hashes": [ - "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", - "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", + "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" ], "index": "pypi", - "version": "==19.3b0" + "version": "==19.10b0" }, "click": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" + }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "index": "pypi", + "version": "==0.4.4" + }, + "flake8": { + "hashes": [ + "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", + "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" ], - "version": "==7.0" + "index": "pypi", + "version": "==3.8.4" }, - "gitdb2": { + "gitdb": { "hashes": [ - "sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", - "sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b" + "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", + "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], - "version": "==2.0.6" + "markers": "python_version >= '3.4'", + "version": "==4.0.5" }, "gitpython": { "hashes": [ - "sha256:9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", - "sha256:c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245" + "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b", + "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8" ], - "version": "==3.0.5" + "markers": "python_version >= '3.4'", + "version": "==3.1.11" }, "isort": { "hashes": [ - "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", - "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7", + "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58" ], - "version": "==4.3.21" + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==5.6.4" }, "lazy-object-proxy": { "hashes": [ @@ -422,6 +436,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -431,96 +446,164 @@ ], "version": "==0.6.1" }, + "pathspec": { + "hashes": [ + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + ], + "version": "==0.8.0" + }, "pbr": { "hashes": [ - "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", - "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488" + "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", + "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" ], - "version": "==5.4.4" + "markers": "python_version >= '2.6'", + "version": "==5.5.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", + "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.6.0" + }, + "pyflakes": { + "hashes": [ + "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", + "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.2.0" }, "pylint": { "hashes": [ - "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", - "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" + "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", + "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" ], "index": "pypi", - "version": "==2.4.4" + "version": "==2.6.0" }, "pyyaml": { "hashes": [ - "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" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" + ], + "version": "==5.3.1" + }, + "regex": { + "hashes": [ + "sha256:0cb23ed0e327c18fb7eac61ebbb3180ebafed5b9b86ca2e15438201e5903b5dd", + "sha256:1a065e7a6a1b4aa851a0efa1a2579eabc765246b8b3a5fd74000aaa3134b8b4e", + "sha256:1a511470db3aa97432ac8c1bf014fcc6c9fbfd0f4b1313024d342549cf86bcd6", + "sha256:1c447b0d108cddc69036b1b3910fac159f2b51fdeec7f13872e059b7bc932be1", + "sha256:2278453c6a76280b38855a263198961938108ea2333ee145c5168c36b8e2b376", + "sha256:240509721a663836b611fa13ca1843079fc52d0b91ef3f92d9bba8da12e768a0", + "sha256:4e21340c07090ddc8c16deebfd82eb9c9e1ec5e62f57bb86194a2595fd7b46e0", + "sha256:570e916a44a361d4e85f355aacd90e9113319c78ce3c2d098d2ddf9631b34505", + "sha256:59d5c6302d22c16d59611a9fd53556554010db1d47e9df5df37be05007bebe75", + "sha256:6a46eba253cedcbe8a6469f881f014f0a98819d99d341461630885139850e281", + "sha256:6f567df0601e9c7434958143aebea47a9c4b45434ea0ae0286a4ec19e9877169", + "sha256:781906e45ef1d10a0ed9ec8ab83a09b5e0d742de70e627b20d61ccb1b1d3964d", + "sha256:8469377a437dbc31e480993399fd1fd15fe26f382dc04c51c9cb73e42965cc06", + "sha256:8cd0d587aaac74194ad3e68029124c06245acaeddaae14cb45844e5c9bebeea4", + "sha256:97a023f97cddf00831ba04886d1596ef10f59b93df7f855856f037190936e868", + "sha256:a973d5a7a324e2a5230ad7c43f5e1383cac51ef4903bf274936a5634b724b531", + "sha256:af360e62a9790e0a96bc9ac845d87bfa0e4ee0ee68547ae8b5a9c1030517dbef", + "sha256:b706c70070eea03411b1761fff3a2675da28d042a1ab7d0863b3efe1faa125c9", + "sha256:bfd7a9fddd11d116a58b62ee6c502fd24cfe22a4792261f258f886aa41c2a899", + "sha256:c30d8766a055c22e39dd7e1a4f98f6266169f2de05db737efe509c2fb9c8a3c8", + "sha256:c53dc8ee3bb7b7e28ee9feb996a0c999137be6c1d3b02cb6b3c4cba4f9e5ed09", + "sha256:c95d514093b80e5309bdca5dd99e51bcf82c44043b57c34594d9d7556bd04d05", + "sha256:d43cf21df524283daa80ecad551c306b7f52881c8d0fe4e3e76a96b626b6d8d8", + "sha256:d62205f00f461fe8b24ade07499454a3b7adf3def1225e258b994e2215fd15c5", + "sha256:e289a857dca3b35d3615c3a6a438622e20d1bf0abcb82c57d866c8d0be3f44c4", + "sha256:e5f6aa56dda92472e9d6f7b1e6331f4e2d51a67caafff4d4c5121cadac03941e", + "sha256:f4b1c65ee86bfbf7d0c3dfd90592a9e3d6e9ecd36c367c884094c050d4c35d04" + ], + "version": "==2020.10.23" }, "six": { "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.13.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" }, - "smmap2": { + "smmap": { "hashes": [ - "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", - "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a" + "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", + "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], - "version": "==2.0.5" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==3.0.4" }, "stevedore": { "hashes": [ - "sha256:01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", - "sha256:e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14" + "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62", + "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0" ], - "version": "==1.31.0" + "markers": "python_version >= '3.6'", + "version": "==3.2.2" }, "toml": { "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" ], - "version": "==0.10.0" + "version": "==0.10.1" }, "typed-ast": { "hashes": [ - "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", - "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", - "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", - "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", - "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", - "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", - "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", - "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", - "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", - "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", - "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", - "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", - "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", - "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", - "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", - "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", - "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", - "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", - "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", - "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" - ], - "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.0" + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", + "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + ], + "version": "==1.4.1" }, "wrapt": { "hashes": [ - "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" ], - "version": "==1.11.2" + "version": "==1.12.1" } } } diff --git a/Procfile b/Procfile index 5ae4640def..29cff6d9d1 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -worker: pipenv run bot +worker: python bot.py diff --git a/README.md b/README.md index 52e4c4abd8..6173484788 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- +
@@ -31,10 +31,6 @@ Made with Python 3.7 - - - - @@ -50,25 +46,25 @@ ## What is Modmail? -Modmail is similar to Reddit's Modmail both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. +Modmail is similar to Reddit's Modmail, both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included! ## How does it work? -When a member sends a direct message to the bot, Modmail will create a channel or "thread" within an isolated category. All further DM messages will automatically relay to that channel, for any available staff can respond within the channel. +When a member sends a direct message to the bot, Modmail will create a channel or "thread" into a designated category. All further DM messages will automatically relay to that channel; any available staff can respond within the channel. -All threads are logged and you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.logviewer.tech/example). +Our Logviewer will save the threads so you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.logviewer.tech/example). ## Features * **Highly Customisable:** * Bot activity, prefix, category, log channel, etc. * Command permission system. - * Interface elements (color, responses, reactions, etc). + * Interface elements (color, responses, reactions, etc.). * Snippets and *command aliases*. * Minimum duration for accounts to be created before allowed to contact Modmail (`account_age`). - * Minimum duration for members to be in the guild before allowed to contact Modmail (`guild_age`). + * Minimum length for members to be in the guild before allowed to contact Modmail (`guild_age`). * **Advanced Logging Functionality:** * When you close a thread, Modmail will generate a [log link](https://logs.logviewer.tech/example) and post it to your log channel. @@ -90,11 +86,11 @@ This list is ever-growing thanks to active development and our exceptional contr Where can I find the Modmail bot invite link? -Unfortunately, due to how this bot functions, it cannot be invited. This is to ensure the individuality to your server and grant you full control over your bot and data. Nonetheless, you can easily obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time)... +Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time). ### Heroku -This bot can be hosted on Heroku. +You can host this bot on Heroku. Installation via Heroku is possible with your web browser alone. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. @@ -108,11 +104,11 @@ To configure automatic updates: ### Hosting for Patreons -If you don't want to go through the trouble of setting up your very own Modmail bot, and/or want to support this project, we offer the all inclusive installation, hosting and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! +If you don't want to go through the trouble of setting up your very own Modmail bot or wish to support this project, we got a solution for you! We offer the complete installation, hosting, and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! ### Locally -Local hosting of Modmail is also possible, first you will need [`Python 3.7`](https://www.python.org/downloads/). +Local hosting of Modmail is also possible. First, you will need [`Python 3.7`](https://www.python.org/downloads/release/python-376/). Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. @@ -147,7 +143,7 @@ You can build your own Docker image: $ docker build . --tag=modmail ``` -or run directly from a pre-built version from https://hub.docker.com/. Currently there are two community release of Modmail: +Or run directly from a pre-built version from https://hub.docker.com/. - Kyber's: @@ -155,22 +151,12 @@ or run directly from a pre-built version from https://hub.docker.com/. Currently $ docker pull kyb3rr/modmail ``` -- Taku's: - -```console -$ docker pull taaku18/modmail -# You can also choose one of the following: -$ docker pull taaku18/modmail:dev -$ docker pull taaku18/modmail: ( ex: 3.2.0, 3.2, etc.) -``` - And to run your docker image: ```console -$ docker run --env-file .env user/modmail +$ docker run --env-file .env kyb3rr/modmail ``` -- Replace `user/modmail` with `kyb3rr/modmail`, `taaku18/modmail`, `taaku18/modmail:3.2`, etc as above. -- `.env` should be the path to your env file, you can also supply a path: `/path/to/.env`. +- `.env` should be the path to your env file; you can also supply a path: `/path/to/.env`. ## Sponsors @@ -180,18 +166,14 @@ Special thanks to our sponsors for supporting the project. - - - - Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins Modmail supports the use of third-party plugins to extend or add functionalities to the bot. -This allows niche features as well as anything else outside of the scope of the core functionality of Modmail. +Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail. -A list of third-party plugins can be found using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. +You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). @@ -199,6 +181,6 @@ Plugins requests and support is available in our [Modmail Plugins Server](https: ## Contributing -Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our contribution [guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! diff --git a/SPONSORS.json b/SPONSORS.json index d30134e015..ceed37be23 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -26,42 +26,5 @@ } ] } - }, - { - "embed": { - "title": "Hey there!", - "description": "Nice to see you here! You can support us by subscribing on youtube -> [Youtube](https://www.youtube.com/user/RoomieOfficial) <- and also join our [Discord](https://discord.gg/zaeVCaV)!", - "url": "https://discord.gg/zaeVCaV", - "color": 13003681, - "footer": { - "icon_url": "https://imgur.com/Mrc9pLd.gif", - "text": "everyone is a clown" - }, - "thumbnail": { - "url": "https://imgur.com/Mrc9pLd.gif" - }, - "image": { - "url": "https://imgur.com/ZUFiL6b.gif" - }, - "author": { - "name": "Roomieofficial", - "url": "https://discord.gg/zaeVCaV", - "icon_url": "https://imgur.com/6hBkt7Z.png" - }, - "fields": [ - { - "name": "What is all this about 🤔", - "value": "We are mainly focused on everything that has anything to do with music or singing! " - }, - { - "name": "Youtube 🙄", - "value": "U will get great content if you follow our [Youtube](https://www.youtube.com/user/RoomieOfficial) with weekly uploads." - }, - { - "name": "Discord 😁", - "value": "Make sure to join our [Discord](https://discord.gg/zaeVCaV) We have weekly events, 24/7 chats and more!" - } - ] - } } ] diff --git a/app.json b/app.json index 76fe320a30..41697cf064 100644 --- a/app.json +++ b/app.json @@ -1,27 +1,35 @@ { - "name": "Modmail", - "description": "An easy to install Modmail bot for Discord - DM to contact mods!", - "repository": "https://github.com/kyb3r/modmail", - "env": { - "TOKEN": { - "description": "Your discord bot's token.", - "required": true - }, - "GUILD_ID": { - "description": "The id for the server you are hosting this bot for.", - "required": true - }, - "OWNERS": { - "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", - "required": true - }, - "MONGO_URI": { - "description": "Mongo DB connection URI for self-hosting your data.", - "required": true - }, - "LOG_URL": { - "description": "The url of the log viewer app for viewing self-hosted logs.", - "required": true + "name": "Modmail", + "description": "An easy to install Modmail bot for Discord - DM to contact mods!", + "repository": "https://github.com/kyb3r/modmail", + "env": { + "TOKEN": { + "description": "Your discord bot's token.", + "required": true + }, + "GUILD_ID": { + "description": "The id for the server you are hosting this bot for.", + "required": true + }, + "MODMAIL_GUILD_ID": { + "description": "The ID of the discord server where the threads channels should be created (receiving server). Default to GUILD_ID.", + "required": false + }, + "OWNERS": { + "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", + "required": true + }, + "CONNECTION_URI": { + "description": "The connection URI for your database.", + "required": true + }, + "DATABASE_TYPE": { + "description": "The type of your database. There is only one supported database at the moment - MongoDB (default).", + "required": false + }, + "LOG_URL": { + "description": "The url of the log viewer app for viewing self-hosted logs.", + "required": true + } } - } -} +} \ No newline at end of file diff --git a/bot.py b/bot.py index a379567b4d..ab5ded6c53 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.5.0-dev0" +__version__ = "3.6.3-dev0" import asyncio @@ -18,8 +18,6 @@ from aiohttp import ClientSession from emoji import UNICODE_EMOJI -from motor.motor_asyncio import AsyncIOMotorClient -from pymongo.errors import ConfigurationError from pkg_resources import parse_version @@ -31,8 +29,8 @@ except ImportError: pass -from core import checks, translations -from core.clients import ApiClient, PluginDatabaseClient +from core import checks +from core.clients import ApiClient, PluginDatabaseClient, MongoDBClient from core.config import ConfigManager from core.utils import human_join, normalize_alias from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging @@ -46,10 +44,17 @@ if not os.path.exists(temp_dir): os.mkdir(temp_dir) +if sys.platform == "win32": + try: + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + except AttributeError: + logger.error("Failed to use WindowsProactorEventLoopPolicy.", exc_info=True) + class ModmailBot(commands.Bot): def __init__(self): - super().__init__(command_prefix=None) # implemented in `get_prefix` + intents = discord.Intents.all() + super().__init__(command_prefix=None, intents=intents) # implemented in `get_prefix` self._session = None self._api = None self.metadata_loop = None @@ -66,22 +71,7 @@ def __init__(self): self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") self._configure_logging() - mongo_uri = self.config["mongo_uri"] - if mongo_uri is None: - logger.critical("A Mongo URI is necessary for the bot to function.") - raise RuntimeError - - try: - self.db = AsyncIOMotorClient(mongo_uri).modmail_bot - except ConfigurationError as e: - logger.critical( - "Your MONGO_URI might be copied wrong, try re-copying from the source again. " - "Otherwise noted in the following message:" - ) - logger.critical(e) - sys.exit(0) - - self.plugin_db = PluginDatabaseClient(self) + self.plugin_db = PluginDatabaseClient(self) # Deprecated self.startup() @property @@ -100,9 +90,12 @@ def uptime(self) -> str: def startup(self): logger.line() - logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") - logger.info("││││ │ │││││├─┤││") - logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") + if os.name != "nt": + logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") + logger.info("││││ │ │││││├─┤││") + logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") + else: + logger.info("MODMAIL") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") logger.line() @@ -152,9 +145,18 @@ def session(self) -> ClientSession: @property def api(self) -> ApiClient: if self._api is None: - self._api = ApiClient(self) + if self.config["database_type"].lower() == "mongodb": + self._api = MongoDBClient(self) + else: + logger.critical("Invalid database type.") + raise RuntimeError return self._api + @property + def db(self): + # deprecated + return self.api.db + async def get_prefix(self, message=None): return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] @@ -165,6 +167,10 @@ def run(self, *args, **kwargs): pass except discord.LoginFailure: logger.critical("Invalid token") + except discord.PrivilegedIntentsRequired: + logger.critical( + "Privileged intents are not explicitly granted in the discord developers dashboard." + ) except Exception: logger.critical("Fatal exception", exc_info=True) finally: @@ -180,7 +186,7 @@ def run(self, *args, **kwargs): logger.error(" - Shutting down bot - ") @property - def owner_ids(self): + def bot_owner_ids(self): owner_ids = self.config["owners"] if owner_ids is not None: owner_ids = set(map(int, str(owner_ids).split(","))) @@ -192,7 +198,7 @@ def owner_ids(self): return owner_ids async def is_owner(self, user: discord.User) -> bool: - if user.id in self.owner_ids: + if user.id in self.bot_owner_ids: return True return await super().is_owner(user) @@ -370,37 +376,16 @@ def command_perm(self, command_name: str) -> PermissionLevel: async def on_connect(self): try: - await self.validate_database_connection() + await self.api.validate_database_connection() except Exception: logger.debug("Logging out due to failed database connection.") return await self.logout() logger.debug("Connected to gateway.") await self.config.refresh() - await self.setup_indexes() + await self.api.setup_indexes() self._connected.set() - async def setup_indexes(self): - """Setup text indexes so we can use the $search operator""" - coll = self.db.logs - index_name = "messages.content_text_messages.author.name_text_key_text" - - index_info = await coll.index_information() - - # Backwards compatibility - old_index = "messages.content_text_messages.author.name_text" - if old_index in index_info: - logger.info("Dropping old index: %s", old_index) - await coll.drop_index(old_index) - - if index_name not in index_info: - 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")] - ) - logger.debug("Successfully configured and verified database indexes.") - async def on_ready(self): """Bot startup, sets uptime.""" @@ -416,7 +401,8 @@ 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.bot_owner_ids ) logger.info("Owners: %s", owners) logger.info("Prefix: %s", self.prefix) @@ -469,7 +455,7 @@ async def on_ready(self): { "open": False, "closed_at": str(datetime.utcnow()), - "close_message": _("Channel has been deleted, no closer found."), + "close_message": "Channel has been deleted, no closer found.", "closer": { "id": str(self.user.id), "name": self.user.name, @@ -498,6 +484,17 @@ async def on_ready(self): self.metadata_loop.before_loop(self.before_post_metadata) self.metadata_loop.start() + other_guilds = [ + guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild} + ] + if any(other_guilds): + logger.warning( + "The bot is in more servers other than the main and staff server." + "This may cause data compromise (%s).", + ", ".join(guild.name for guild in other_guilds), + ) + logger.warning("If the external servers are valid, you may ignore this message.") + async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() @@ -549,9 +546,7 @@ def check_account_age(self, author: discord.Member) -> bool: logger.debug("Blocked due to account age, user %s.", author.name) if str(author.id) not in self.blocked_users: - new_reason = _("System Message: New Account. Required to wait for {time}.").format( - time=delta - ) + new_reason = f"System Message: New Account. Required to wait for {delta}." self.blocked_users[str(author.id)] = new_reason return False @@ -617,7 +612,7 @@ def check_manual_blocked(self, author: discord.Member) -> bool: return False async def _process_blocked(self, message): - x, 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 @@ -937,7 +932,7 @@ async def on_typing(self, channel, user, _): return await thread.recipient.trigger_typing() - async def handle_reaction_events(self, payload, *, add): + async def handle_reaction_events(self, payload): user = self.get_user(payload.user_id) if user.bot: return @@ -963,7 +958,7 @@ async def handle_reaction_events(self, payload, *, add): if not thread: return if ( - add + payload.event_type == "REACTION_ADD" and message.embeds and str(reaction) == str(close_emoji) and self.config.get("recipient_thread_close") @@ -987,14 +982,14 @@ async def handle_reaction_events(self, payload, *, add): if not thread: return try: - x, linked_message = await thread.find_linked_messages( + _, 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 - if add: + if payload.event_type == "REACTION_ADD": if await self.add_reaction(linked_message, reaction): await self.add_reaction(message, reaction) else: @@ -1005,28 +1000,15 @@ async def handle_reaction_events(self, payload, *, add): logger.warning("Failed to remove reaction: %s", e) async def on_raw_reaction_add(self, payload): - await self.handle_reaction_events(payload, add=True) + await self.handle_reaction_events(payload) async def on_raw_reaction_remove(self, payload): - await self.handle_reaction_events(payload, add=False) + await self.handle_reaction_events(payload) async def on_guild_channel_delete(self, channel): if channel.guild != self.modmail_guild: return - 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 - # TODO: waiting for dpy - logger.warning("Failed to retrieve audit log: %s.", e) - return - - if mod == self.user: - return - if isinstance(channel, discord.CategoryChannel): if self.main_category == channel: logger.debug("Main category was deleted.") @@ -1043,6 +1025,19 @@ async def on_guild_channel_delete(self, channel): await self.config.update() return + audit_logs = self.modmail_guild.audit_logs( + limit=10, action=discord.AuditLogAction.channel_delete + ) + entry = await audit_logs.find(lambda a: int(a.target.id) == channel.id) + + if entry is None: + logger.debug("Cannot find the audit log entry for channel delete of %d.", channel.id) + return + + mod = entry.user + if mod == self.user: + return + thread = await self.threads.find(channel=channel) if thread and thread.channel == channel: logger.debug("Manually closed channel %s.", channel.name) @@ -1070,20 +1065,60 @@ 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): - thread = await self.threads.find(channel=message.channel) + + if message.is_system(): + return + + if isinstance(message.channel, discord.DMChannel): + if message.author == self.user: + return + thread = await self.threads.find(recipient=message.author) + if not thread: + return try: - await thread.delete_message(message) + message = await thread.find_linked_message_from_dm(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) + if str(e) != "Thread channel message not found.": + logger.debug("Failed to find linked message to delete: %s", e) + return embed = message.embeds[0] embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=embed.footer.icon_url) await message.edit(embed=embed) + return + + if message.author != self.user: + return + + thread = await self.threads.find(channel=message.channel) + if not thread: + return + + audit_logs = self.modmail_guild.audit_logs( + limit=10, action=discord.AuditLogAction.message_delete + ) + + entry = await audit_logs.find(lambda a: a.target == self.user) + + if entry is None: + return + + try: + await thread.delete_message(message, note=False) + embed = discord.Embed( + description="Successfully deleted message.", color=self.main_color + ) + except ValueError as e: + if str(e) not in {"DM message not found.", "Malformed thread message."}: + logger.debug("Failed to find linked message to delete: %s", e) + embed = discord.Embed( + description="Failed to delete message.", color=self.error_color + ) + else: + return + except discord.NotFound: + return + embed.set_footer(text=f"Message ID: {message.id} from {message.author}.") + return await message.channel.send(embed=embed) async def on_bulk_message_delete(self, messages): await discord.utils.async_all(self.on_message_delete(msg) for msg in messages) @@ -1096,10 +1131,13 @@ async def on_message_edit(self, before, after): if isinstance(after.channel, discord.DMChannel): thread = await self.threads.find(recipient=before.author) + if not thread: + return + try: await thread.edit_dm_message(after, after.content) except ValueError: - x, blocked_emoji = await self.retrieve_emoji() + _, blocked_emoji = await self.retrieve_emoji() await self.add_reaction(after, blocked_emoji) else: embed = discord.Embed( @@ -1147,45 +1185,17 @@ async def on_command_error(self, context, exception): corrected_permission_level.name, ) logger.warning("CheckFailure: %s", exception) + elif isinstance(exception, commands.DisabledCommand): + logger.info( + "DisabledCommand: %s is trying to run eval but it's disabled", context.author.name + ) else: logger.error("Unexpected exception:", exc_info=exception) - async def validate_database_connection(self): - try: - await self.db.command("buildinfo") - except Exception as exc: - logger.critical("Something went wrong while connecting to the database.") - message = f"{type(exc).__name__}: {str(exc)}" - logger.critical(message) - - if "ServerSelectionTimeoutError" in message: - logger.critical( - "This may have been caused by not whitelisting " - "IPs correctly. Make sure to whitelist all " - "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" - ) - - if "OperationFailure" in message: - logger.critical( - "This is due to having invalid credentials in your MONGO_URI. " - "Remember you need to substitute `` with your actual password." - ) - logger.critical( - "Be sure to URL encode your username and password (not the entire URL!!), " - "https://www.urlencoder.io/, if this issue persists, try changing your username and password " - "to only include alphanumeric characters, no symbols." - "" - ) - raise - else: - logger.debug("Successfully connected to the database.") - logger.line("debug") - async def post_metadata(self): - owner = (await self.application_info()).owner + info = await self.application_info() + data = { - "owner_name": str(owner), - "owner_id": owner.id, "bot_id": self.user.id, "bot_name": str(self.user), "avatar_url": str(self.user.avatar_url), @@ -1199,7 +1209,20 @@ async def post_metadata(self): "last_updated": str(datetime.utcnow()), } - async with self.session.post("https://api.logviewer.tech/metadata", json=data): + if info.team is not None: + data.update( + { + "owner_name": info.team.owner.name + if info.team.owner is not None + else "No Owner", + "owner_id": info.team.owner_id, + "team": True, + } + ) + else: + data.update({"owner_name": info.owner.name, "owner_id": info.owner.id, "team": False}) + + async with self.session.post("https://api.modmail.dev/metadata", json=data): logger.debug("Uploading metadata to Modmail server.") async def before_post_metadata(self): @@ -1220,7 +1243,6 @@ def main(): except ImportError: pass - translations.init() bot = ModmailBot() bot.run() diff --git a/cogs/modmail.py b/cogs/modmail.py index 76a28b3c9c..44f0e9dcde 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -41,23 +41,17 @@ async def setup(self, ctx): if ctx.guild != self.bot.modmail_guild: return await ctx.send( - _( - "You can only setup in the Modmail guild: {guild_name}.".format( - guild_name=self.bot.modmail_guild - ) - ) + f"You can only setup in the Modmail guild: {self.bot.modmail_guild}." ) if self.bot.main_category is not None: logger.debug("Can't re-setup server, main_category is found.") - return await ctx.send( - _("{guild_name} is already set up.").format(guild_name=self.bot.modmail_guild) - ) + return await ctx.send(f"{self.bot.modmail_guild} is already set up.") if self.bot.modmail_guild is None: embed = discord.Embed( - title=_("Error"), - description=_("Modmail functioning guild not found."), + title="Error", + description="Modmail functioning guild not found.", color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -94,29 +88,21 @@ async def setup(self, ctx): ) embed = discord.Embed( - title=_("Friendly Reminder"), - description=_( - "You may use the `{prefix}config set log_channel_id " - "` command to set up a custom log channel, then you can delete this default " - "{log_channel} log channel." - ).format(prefix=self.bot.prefix, log_channel=log_channel.mention), + title="Friendly Reminder", + description=f"You may use the `{self.bot.prefix}config set log_channel_id " + "` command to set up a custom log channel, then you can delete this default " + f"{log_channel.mention} log channel.", color=self.bot.main_color, ) embed.add_field( - name=_("Thanks for using the 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)!" - ), + 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: and if you are " + "feeling extra generous, buy us coffee on [Patreon](https://patreon.com/kyber) :heart:!", ) - embed.set_footer( - text=_('Type "{prefix}help" for a complete list of commands.').format( - prefix=self.bot.prefix - ) - ) + 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 @@ -124,20 +110,18 @@ 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" - "Type:\n- `{prefix}permissions` and `{prefix}permissions add` " - "for more info on setting permissions.\n" - "- `{prefix}config help` for a list of available customizations." - ).format(prefix=self.bot.prefix) + "**Successfully set up server.**\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." ) 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) + for owner_id in self.bot.bot_owner_ids: + await self.bot.update_perms(PermissionLevel.OWNER, owner_id) @commands.group(aliases=["snippets"], invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -186,7 +170,7 @@ async def snippet(self, ctx, *, name: str.lower = None): 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) + embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -228,27 +212,25 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte """ if name in self.bot.snippets: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("Snippet `{name}` already exists.").format(name=name), + description=f"Snippet `{name}` already exists.", ) return await ctx.send(embed=embed) if name in self.bot.aliases: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("An alias with the same name already exists: `{name}`.").format( - name=name - ), + description=f"An alias that shares the same name exists: `{name}`.", ) return await ctx.send(embed=embed) if len(name) > 120: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("Snippet names cannot be longer than 120 characters."), + description="Snippet names cannot be longer than 120 characters.", ) return await ctx.send(embed=embed) @@ -269,9 +251,9 @@ async def snippet_remove(self, ctx, *, name: str.lower): if name in self.bot.snippets: embed = discord.Embed( - title=_("Removed snippet"), + title="Removed snippet", color=self.bot.main_color, - description=_("Snippet `{name}` is now deleted.").format(name=name), + description=f"Snippet `{name}` is now deleted.", ) self.bot.snippets.pop(name) await self.bot.config.update() @@ -294,42 +276,63 @@ async def snippet_edit(self, ctx, name: str.lower, *, value): await self.bot.config.update() embed = discord.Embed( - title=_("Edited snippet"), + title="Edited snippet", color=self.bot.main_color, - description=f_('`{name}` will now send "{value}".'), + description=f'`{name}` will now send "{value}".', ) else: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") await ctx.send(embed=embed) - @commands.command() + @commands.command(usage=" [options]") @checks.has_permissions(PermissionLevel.MODERATOR) @checks.thread_only() - async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None): + async def move(self, ctx, *, arguments): """ Move a thread to another category. `category` may be a category ID, mention, or name. - `specifics` is a string which takes in arguments on how to perform the move. Ex: "silently" + `options` is a string which takes in arguments on how to perform the move. Ex: "silently" """ + split_args = arguments.strip('"').split(" ") + + # manually parse arguments, consumes as much of args as possible for category + for i in range(len(split_args)): + try: + if i == 0: + fmt = arguments + else: + fmt = " ".join(split_args[:-i]) + + category = await commands.CategoryChannelConverter().convert(ctx, fmt) + except commands.BadArgument: + if i == len(split_args) - 1: + # last one + raise + pass + else: + break + + options = " ".join(arguments.split(" ")[-i:]) + thread = ctx.thread silent = False - if specifics: + if options: silent_words = ["silent", "silently"] - silent = any(word in silent_words for word in specifics.split()) + silent = any(word in silent_words for word in options.split()) await thread.channel.edit(category=category, sync_permissions=True) if self.bot.config["thread_move_notify"] and not silent: embed = discord.Embed( - title=_("Thread Moved"), + title=self.bot.config["thread_move_title"], description=self.bot.config["thread_move_response"], color=self.bot.main_color, ) await thread.recipient.send(embed=embed) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) async def send_scheduled_close_message(self, ctx, after, silent=False): @@ -338,17 +341,15 @@ async def send_scheduled_close_message(self, ctx, after, silent=False): silent = "*silently* " if silent else "" embed = discord.Embed( - title=_("Scheduled close"), - description=_("This thread will close {silent}in {time}.").format( - silent=silent, time=human_delta - ), + title="Scheduled close", + description=f"This thread will close {silent}in {human_delta}.", color=self.bot.error_color, ) if after.arg and not silent: - embed.add_field(name=_("Message"), value=after.arg) + 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) @@ -382,21 +383,20 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): close_after = (after.dt - now).total_seconds() if after else 0 message = after.arg if after else None - silent = str(message).lower() in {_("silent"), _("silently")} - cancel = str(message).lower() == _("cancel") + silent = str(message).lower() in {"silent", "silently"} + cancel = str(message).lower() == "cancel" if cancel: 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( color=self.bot.error_color, - description=_("This thread has not already been scheduled to close."), + description="This thread has not already been scheduled to close.", ) return await ctx.send(embed=embed) @@ -446,18 +446,14 @@ async def notify( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} is already going to be mentioned.").format( - mention=mention - ), + description=f"{mention} is already going to be mentioned.", ) else: mentions.append(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=_( - "{mention} will be mentioned " "on the next message received." - ).format(mention=mention), + description=f"{mention} will be mentioned on the next message received.", ) return await ctx.send(embed=embed) @@ -488,16 +484,13 @@ async def unnotify( if mention not in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} does not have a pending notification.").format( - mention=mention - ), + description=f"{mention} does not have a pending notification.", ) else: mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( - color=self.bot.main_color, - description=_("{mention} will no longer be notified.").format(mention=mention), + color=self.bot.main_color, description=f"{mention} will no longer be notified." ) return await ctx.send(embed=embed) @@ -530,18 +523,14 @@ async def subscribe( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} is already subscribed to this thread.").format( - mention=mention - ), + description=f"{mention} is not subscribed to this thread.", ) else: mentions.append(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=_( - "{mention} will now be " "notified of all messages received." - ).format(mention=mention), + description=f"{mention} will now be notified of all messages received.", ) return await ctx.send(embed=embed) @@ -572,18 +561,14 @@ async def unsubscribe( if mention not in mentions: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} is not already subscribed to this thread.").format( - mention=mention - ), + description=f"{mention} is not subscribed to this thread.", ) else: mentions.remove(mention) await self.bot.config.update() embed = discord.Embed( color=self.bot.main_color, - description=_("{mention} is now unsubscribed to this thread.").format( - mention=mention - ), + description=f"{mention} is now unsubscribed from this thread.", ) return await ctx.send(embed=embed) @@ -593,7 +578,7 @@ async def unsubscribe( async def nsfw(self, ctx): """Flags a Modmail thread as NSFW (not safe for work).""" await ctx.channel.edit(nsfw=True) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -602,7 +587,7 @@ async def nsfw(self, ctx): async def sfw(self, ctx): """Flags a Modmail thread as SFW (safe for work).""" await ctx.channel.edit(nsfw=False) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -635,26 +620,24 @@ def format_log_embeds(self, logs, avatar_url): embed.add_field(name="Created", value=duration(created_at, now=datetime.utcnow())) closer = entry.get("closer") if closer is None: - closer_msg = _("Unknown") + closer_msg = "Unknown" else: closer_msg = f"<@{closer['id']}>" - embed.add_field(name=_("Closed By"), value=closer_msg) + embed.add_field(name="Closed By", value=closer_msg) if entry["recipient"]["id"] != entry["creator"]["id"]: - embed.add_field(name=_("Created by"), value=f"<@{entry['creator']['id']}>") + 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. - embed.add_field(name=_("Link"), value=log_url) + embed.add_field(name="Link", value=log_url) else: logger.debug("Invalid log entry: no closer.") - embed.add_field(name=_("Log Key"), value=f"`{entry['key']}`") + embed.add_field(name="Log Key", value=f"`{entry['key']}`") - embed.set_footer(text=_("Recipient ID") + ": " + str(entry["recipient"]["id"])) + embed.set_footer(text="Recipient ID: " + str(entry["recipient"]["id"])) embeds.append(embed) return embeds @@ -685,7 +668,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) @@ -707,18 +690,13 @@ 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)} - - projection = {"messages": {"$slice": 5}} - - entries = await self.bot.db.logs.find(query, projection).to_list(None) - + entries = await self.bot.api.search_closed_by(user.id) embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) 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) @@ -737,14 +715,14 @@ async def logs_delete(self, ctx, key_or_link: str): if not success: embed = discord.Embed( - title=_("Error"), - description=_("Log entry `{key}` not found.").format(key=key), + title="Error", + description=f"Log entry `{key}` not found.", color=self.bot.error_color, ) else: embed = discord.Embed( title="Success", - description=_("Log entry `{key}` successfully deleted.").format(key=key), + description=f"Log entry `{key}` successfully deleted.", color=self.bot.main_color, ) @@ -768,9 +746,7 @@ async def logs_responded(self, ctx, *, user: User = None): if not embeds: embed = discord.Embed( color=self.bot.error_color, - description=_("{mention} has not responded to any threads.").format( - mention=getattr(user, "mention", user.id) - ), + description=f"{getattr(user, 'mention', user.id)} has not responded to any threads.", ) return await ctx.send(embed=embed) @@ -788,22 +764,14 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): await ctx.trigger_typing() - query = { - "guild_id": str(self.bot.guild_id), - "open": False, - "$text": {"$search": f'"{query}"'}, - } - - projection = {"messages": {"$slice": 5}} - - entries = await self.bot.db.logs.find(query, projection).to_list(limit) + entries = await self.bot.api.search_by_text(query, limit) embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) 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) @@ -896,13 +864,13 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): except ValueError: return await ctx.send( embed=discord.Embed( - title=_("Failed"), - description=_("Cannot find a message to edit."), + title="Failed", + description="Cannot find a message to edit.", color=self.bot.error_color, ) ) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -926,7 +894,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) @@ -934,9 +902,8 @@ async def contact( if exists: embed = discord.Embed( color=self.bot.error_color, - description=_("A thread for this user already " "exists in {mention}.").format( - mention=exists.channel.mention - ), + description="A thread for this user already " + f"exists in {exists.channel.mention}.", ) await ctx.channel.send(embed=embed) @@ -946,15 +913,13 @@ async def contact( logger.info("Contacting user %s when Modmail DM is disabled.", user) embed = discord.Embed( - title=_("Created Thread"), - description=_("Thread started by {author_mention} " "for {user_mention}.").format( - author_mention=ctx.author.mention, user_mention=user.mention - ), + title="Created Thread", + description=f"Thread started by {ctx.author.mention} for {user.mention}.", color=self.bot.main_color, ) await thread.wait_until_ready() await thread.channel.send(embed=embed) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) await asyncio.sleep(3) await ctx.message.delete() @@ -965,9 +930,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 = [] @@ -986,10 +949,10 @@ async def blocked(self, ctx): embed = embeds[0] for mention, reason in users: - line = mention + f" - {reason or _('No Reason Provided')}\n" + line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title=_("Blocked Users") + " " + _("(Continued)"), + title="Blocked Users (Continued)", color=self.bot.main_color, description=line, ) @@ -997,7 +960,7 @@ async def blocked(self, ctx): else: embed.description += line else: - embeds[0].description = _("Currently there are no blocked users.") + embeds[0].description = "Currently there are no blocked users." session = EmbedPaginatorSession(ctx, *embeds) await session.run() @@ -1023,8 +986,8 @@ async def blocked_whitelist(self, ctx, *, user: User = None): if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( - title=_("Success"), - description=_("{mention} is no longer whitelisted.").format(mention=mention), + title="Success", + description=f"{mention} is no longer whitelisted.", color=self.bot.main_color, ) self.bot.blocked_whitelisted_users.remove(str(user.id)) @@ -1044,10 +1007,8 @@ async def blocked_whitelist(self, ctx, *, user: User = None): reason = msg[16:].strip().rstrip(".") embed = discord.Embed( title="Success", - description=_( - "{mention} was previously blocked internally for " - '"{reason}". {mention} is now whitelisted.' - ).format(mention=mention, reason=reason), + description=f"{mention} was previously blocked internally for " + f'"{reason}". {mention} is now whitelisted.', color=self.bot.main_color, ) else: @@ -1087,10 +1048,8 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi if str(user.id) in self.bot.blocked_whitelisted_users: embed = discord.Embed( - title=_("Error"), - description=_("Cannot block {mention}, user is whitelisted.").format( - mention=mention - ), + title="Error", + description=f"Cannot block {mention}, user is whitelisted.", color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -1114,20 +1073,16 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi if str(user.id) in self.bot.blocked_users and msg: old_reason = msg.strip().rstrip(".") embed = discord.Embed( - title=_("Success"), - description=_( - "{mention} was previously blocked {old_reason}.\n" - "{mention} is now blocked {reason}" - ).format(mention=mention, old_reason=old_reason, reason=reason), + title="Success", + description=f"{mention} was previously blocked {old_reason}.\n" + f"{mention} is now blocked {reason}", color=self.bot.main_color, ) else: embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("{mention} is now blocked {reason}").format( - mention=mention, reason=reason - ), + description=f"{mention} is now blocked {reason}", ) self.bot.blocked_users[str(user.id)] = reason await self.bot.config.update() @@ -1166,30 +1121,24 @@ async def unblock(self, ctx, *, user: User = None): reason = msg[16:].strip().rstrip(".") or "no reason" embed = discord.Embed( title="Success", - description=_( - "{mention} was previously blocked internally " - "{reason}.\n{mention} is no longer blocked." - ).format(mention=mention, reason=reason), + 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, " - "{name} will be automatically blocked again. Use " - '"{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' - ).format(name=name, prefix=self.bot.prefix, user_id=user.id) + text="However, if the original system block reason still applies, " + f"{name} will be automatically blocked again. " + f'Use "{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' ) else: embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("{mention} is no longer blocked.").format(mention=mention), + description=f"{mention} is no longer blocked.", ) else: embed = discord.Embed( - title=_("Error"), - description=_("{mention} is not blocked.").format(mention=mention), - color=self.bot.error_color, + title="Error", description=f"{mention} is not blocked.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -1209,17 +1158,18 @@ async def delete(self, ctx, message_id: int = None): thread = ctx.thread try: - await thread.delete_message(message_id) - except ValueError: + await thread.delete_message(message_id, note=True) + except ValueError as e: + logger.warning("Failed to delete message: %s.", e) return await ctx.send( embed=discord.Embed( - title=_("Failed"), - description=_("Cannot find a message to delete."), + title="Failed", + description="Cannot find a message to delete.", color=self.bot.error_color, ) ) - sent_emoji, x = await self.bot.retrieve_emoji() + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @commands.command() @@ -1341,20 +1291,15 @@ async def enable(self, ctx): Undo's the `{prefix}disable` command, all DM will be relayed after running this command. """ + embed = discord.Embed( + title="Success", + description="Modmail will now accept all DM messages.", + color=self.bot.main_color, + ) if self.bot.config["dm_disabled"] != 0: - embed = discord.Embed( - title=_("Success"), - description=_("Modmail will now accept **all** DM messages."), - color=self.bot.main_color, - ) self.bot.config["dm_disabled"] = 0 await self.bot.config.update() - else: - embed = discord.Embed( - description=_("Modmail is already accepting all DM messages."), - color=self.bot.error_color, - ) return await ctx.send(embed=embed) @@ -1378,27 +1323,12 @@ async def disable_new(self, ctx): No new threads can be created through DM. """ + embed = discord.Embed( + title="Success", + description="Modmail will not create any new threads.", + color=self.bot.main_color, + ) if self.bot.config["dm_disabled"] < 1: - embed = discord.Embed( - title=_("Success"), - description=_("Modmail will not create any **new** threads."), - color=self.bot.main_color, - ) - self.bot.config["dm_disabled"] = 1 - await self.bot.config.update() - elif self.bot.config["dm_disabled"] == 1: - embed = discord.Embed( - description=_("Modmail is already not creating any new threads."), - color=self.bot.error_color, - ) - else: - embed = discord.Embed( - title=_("Success"), - description=_( - "Modmail will not create **new** threads, but existing threads will now be functioning." - ), - color=self.bot.main_color, - ) self.bot.config["dm_disabled"] = 1 await self.bot.config.update() @@ -1412,20 +1342,15 @@ async def disable_all(self, ctx): No new threads can be created through DM nor no further DM messages will be relayed. """ + embed = discord.Embed( + title="Success", + description="Modmail will not accept any DM messages.", + color=self.bot.main_color, + ) - if self.bot.config["dm_disabled"] < 2: - embed = discord.Embed( - title=_("Success"), - description=_("Modmail will not accept **any** DM messages."), - color=self.bot.main_color, - ) + if self.bot.config["dm_disabled"] != 2: self.bot.config["dm_disabled"] = 2 await self.bot.config.update() - else: - embed = discord.Embed( - description=_("Modmail is already not accepting any DM messages."), - color=self.bot.error_color, - ) return await ctx.send(embed=embed) @@ -1438,22 +1363,20 @@ async def isenable(self, ctx): if self.bot.config["dm_disabled"] == 1: embed = discord.Embed( - title=_("New Threads Disabled"), - description=_("Modmail is not creating new threads."), + title="New Threads Disabled", + 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=_( - "Modmail is not accepting any DM messages for new and existing threads." - ), + title="All DM Disabled", + 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=_("Modmail is accepting all DM messages."), + title="Enabled", + description="Modmail now is accepting all DM messages.", color=self.bot.main_color, ) diff --git a/cogs/plugins.py b/cogs/plugins.py index 4d335607e1..227cfc8270 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -104,7 +104,7 @@ def __init__(self, bot): self.bot.loop.create_task(self.populate_registry()) - if getattr(self.bot, "config", None) and self.bot.config.get("enable_plugins"): + if self.bot.config.get("enable_plugins"): self.bot.loop.create_task(self.initial_load_plugins()) else: logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") @@ -136,7 +136,12 @@ async def initial_load_plugins(self): await self.download_plugin(plugin) await self.load_plugin(plugin) except Exception: - logger.error("Error when loading plugin %s.", plugin, exc_info=True) + self.bot.config["plugins"].remove(plugin_name) + logger.error( + "Error when loading plugin %s. Plugin removed from config.", + plugin, + exc_info=True, + ) continue logger.debug("Finished loading all plugins.") @@ -152,11 +157,26 @@ async def download_plugin(self, plugin, force=False): if plugin.cache_path.exists() and not force: plugin_io = plugin.cache_path.open("rb") logger.debug("Loading cached %s.", plugin.cache_path) - else: - async with self.bot.session.get(plugin.url) as resp: + headers = {} + github_token = self.bot.config["github_token"] + if github_token is not None: + headers["Authorization"] = f"token {github_token}" + + async with self.bot.session.get(plugin.url, headers=headers) as resp: logger.debug("Downloading %s.", plugin.url) raw = await resp.read() + + try: + raw = await resp.text() + except UnicodeDecodeError: + pass + else: + if raw == 'Not Found': + raise InvalidPluginError('Plugin not found') + else: + raise InvalidPluginError('Invalid download recieved, non-bytes object') + plugin_io = io.BytesIO(raw) if not plugin.cache_path.parent.exists(): plugin.cache_path.parent.mkdir(parents=True) @@ -187,10 +207,10 @@ async def load_plugin(self, plugin): if req_txt.exists(): # Install PIP requirements - venv = hasattr(sys, "real_prefix") # in a virtual env + venv = hasattr(sys, "real_prefix") or hasattr(sys, "base_prefix") # in a virtual env user_install = " --user" if not venv else "" proc = await asyncio.create_subprocess_shell( - f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", + f'"{sys.executable}" -m pip install --upgrade{user_install} -r {req_txt} -q -q', stderr=PIPE, stdout=PIPE, ) @@ -227,7 +247,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if not self._ready_event.is_set(): embed = discord.Embed( - description=_("Plugins are still loading, please try again later."), + description="Plugins are still loading, please try again later.", color=self.bot.main_color, ) await ctx.send(embed=embed) @@ -243,10 +263,8 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if required_version and self.bot.version < parse_version(required_version): embed = discord.Embed( - description=_( - "Your bot's version is too low. " - "This plugin requires version `{required_version}`." - ).format(required_version=required_version), + description="Your bot's version is too low. " + f"This plugin requires version `{required_version}`.", color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -259,11 +277,9 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): plugin = Plugin.from_string(plugin_name) except InvalidPluginError: embed = discord.Embed( - description=_( - "Invalid plugin name, double check the plugin name " - "or use one of the following formats: " - "username/repo/plugin, username/repo/plugin@branch." - ), + description="Invalid plugin name, double check the plugin name " + "or use one of the following formats: " + "username/repo/plugin, username/repo/plugin@branch.", color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -296,33 +312,31 @@ 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) if plugin.name in self.bot.cogs: # another class with the same name embed = discord.Embed( - description=_("Cannot install this plugin (dupe cog name)."), + description="Cannot install this plugin (dupe cog name).", color=self.bot.error_color, ) return await ctx.send(embed=embed) embed = discord.Embed( - description=_("Starting to download plugin from {plugin_link}...").format( - plugin_link=plugin.link - ), + description=f"Starting to download plugin from {plugin.link}...", color=self.bot.main_color, ) msg = await ctx.send(embed=embed) try: await self.download_plugin(plugin, force=True) - except Exception: + except Exception as e: logger.warning("Unable to download plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=_("Failed to download plugin, check logs for error."), + description=f"Failed to download plugin, check logs for error.\n{type(e)}: {e}", color=self.bot.error_color, ) @@ -337,35 +351,28 @@ async def plugins_add(self, ctx, *, plugin_name: str): try: await self.load_plugin(plugin) - except Exception: + except Exception as e: logger.warning("Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=_("Failed to download plugin, check logs for error."), + description=f"Failed to download plugin, check logs for error.\n{type(e)}: {e}", color=self.bot.error_color, ) else: embed = discord.Embed( - description=_( - "Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*" - ), + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*", color=self.bot.main_color, ) else: embed = discord.Embed( - description=_( - "Successfully installed plugin.\n" - "*Friendly reminder, plugins have absolute control over your bot. " - "Please only install plugins from developers you trust.*" - ) - + "\n\n" - + _( - "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " - "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot." - ), + description="Successfully installed plugin.\n" + "*Friendly reminder, plugins have absolute control over your bot. " + "Please only install plugins from developers you trust.*\n\n" + "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.", color=self.bot.main_color, ) return await msg.edit(embed=embed) @@ -385,7 +392,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description=_("Plugin is not installed."), color=self.bot.error_color + description="Plugin is not installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -411,7 +418,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) @@ -423,25 +430,33 @@ async def update_plugin(self, ctx, plugin_name): if str(plugin) not in self.bot.config["plugins"]: embed = discord.Embed( - description=_("Plugin is not installed."), color=self.bot.error_color + description="Plugin is not installed.", color=self.bot.error_color ) return await ctx.send(embed=embed) async with ctx.typing(): + embed = discord.Embed( + description=f"Successfully updated {plugin.name}.", color=self.bot.main_color + ) await self.download_plugin(plugin, force=True) if self.bot.config.get("enable_plugins"): try: self.bot.unload_extension(plugin.ext_string) except commands.ExtensionError: logger.warning("Plugin unload fail.", exc_info=True) - await self.load_plugin(plugin) - logger.debug("Updated %s.", plugin_name) - embed = discord.Embed( - description=_("Successfully updated {plugin_name}.").format( - plugin_name=plugin.name - ), - color=self.bot.main_color, - ) + try: + await self.load_plugin(plugin) + except Exception: + embed = discord.Embed( + description=f"Failed to update {plugin.name}. This plugin will now be removed from your bot.", + color=self.bot.error_color, + ) + self.bot.config["plugins"].remove(plugin_name) + logger.debug("Failed to update %s. Removed plugin from config.", plugin_name) + else: + logger.debug("Updated %s.", plugin_name) + else: + logger.debug("Updated %s.", plugin_name) return await ctx.send(embed=embed) @plugins.command(name="update") @@ -458,11 +473,45 @@ 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"]: + for plugin_name in list(self.bot.config["plugins"]): await self.update_plugin(ctx, plugin_name) else: await self.update_plugin(ctx, plugin_name) + @plugins.command(name="reset") + @checks.has_permissions(PermissionLevel.OWNER) + async def plugins_reset(self, ctx): + """ + Reset all plugins for the bot. + + Deletes all cache and plugins from config and unloads from the bot. + """ + logger.warning("Purging plugins.") + for ext in list(self.bot.extensions): + if not ext.startswith("plugins."): + continue + try: + logger.error("Unloading plugin: %s.", ext) + self.bot.unload_extension(ext) + except Exception: + logger.error("Failed to unload plugin: %s.", ext) + self.bot.config["plugins"].clear() + + cache_path = Path(__file__).absolute().parent.parent / "temp" / "plugins-cache" + if cache_path.exists(): + logger.warning("Removing cache path.") + shutil.rmtree(cache_path) + + for entry in os.scandir(Path(__file__).absolute().parent.parent / "plugins"): + if entry.is_dir(): + shutil.rmtree(entry.path) + logger.warning("Removing %s.", entry.name) + + embed = discord.Embed( + description="Successfully purged all plugins from the bot.", color=self.bot.main_color + ) + return await ctx.send(embed=embed) + @plugins.command(name="loaded", aliases=["enabled", "installed"]) @checks.has_permissions(PermissionLevel.OWNER) async def plugins_loaded(self, ctx): @@ -472,25 +521,22 @@ async def plugins_loaded(self, ctx): if not self.bot.config.get("enable_plugins"): embed = discord.Embed( - description=_( - "No plugins are loaded due to `ENABLE_PLUGINS=false`, " - "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot." - ), + description="No plugins are loaded due to `ENABLE_PLUGINS=false`, " + "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.", color=self.bot.error_color, ) return await ctx.send(embed=embed) if not self._ready_event.is_set(): embed = discord.Embed( - description=_("Plugins are still loading, please try again later."), + description="Plugins are still loading, please try again later.", color=self.bot.main_color, ) return await ctx.send(embed=embed) if not self.loaded_plugins: embed = discord.Embed( - description=_("There are no plugins currently loaded."), - color=self.bot.error_color, + description="There are no plugins currently loaded.", color=self.bot.error_color ) return await ctx.send(embed=embed) @@ -510,7 +556,7 @@ async def plugins_loaded(self, ctx): embeds = [] for page in pages: embed = discord.Embed( - title=_("Loaded plugins:"), description=page, color=self.bot.main_color + title="Loaded plugins:", description=page, color=self.bot.main_color ) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) @@ -546,16 +592,14 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N if not index and plugin_name is not None: embed = discord.Embed( color=self.bot.error_color, - description=_( - 'Could not find a plugin with name "{plugin_name}" within the registry.' - ).format(plugin_name=plugin_name), + description=f'Could not find a plugin with name "{plugin_name}" within the registry.', ) matches = get_close_matches(plugin_name, self.registry.keys()) 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) @@ -575,7 +619,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( @@ -594,13 +638,11 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N required_version = details.get("bot_version", False) if required_version and self.bot.version < parse_version(required_version): embed.set_footer( - text=_( - "Your bot is unable to install this plugin, " - "minimum required version is v{required_version}." - ).format(required_version=required_version) + text="Your bot is unable to install this plugin, " + f"minimum required version is v{required_version}." ) else: - embed.set_footer(text=_("Your bot is able to install this plugin.")) + embed.set_footer(text="Your bot is able to install this plugin.") embeds.append(embed) @@ -655,7 +697,7 @@ async def plugins_registry_compact(self, ctx): for page in pages: embed = discord.Embed(color=self.bot.main_color, description=page) - embed.set_author(name=_("Plugin Registry"), icon_url=self.bot.user.avatar_url) + embed.set_author(name="Plugin Registry", icon_url=self.bot.user.avatar_url) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) diff --git a/cogs/utility.py b/cogs/utility.py index 9974deb803..b0d96ea094 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -47,7 +47,7 @@ async def format_cog_help(self, cog, *, no_cog=False): else: format_ = f"`[{perm_level}] {prefix + cmd.qualified_name}` " - format_ += f"- {_(cmd.short_doc)}\n" + format_ += f"- {cmd.short_doc}\n" if cmd.short_doc else "- *No description.*\n" if not format_.strip(): continue if len(format_) + len(formats[-1]) >= 1024: @@ -58,26 +58,21 @@ async def format_cog_help(self, cog, *, no_cog=False): embeds = [] for format_ in formats: description = ( - _(cog.description) or _("No description.") + cog.description or "No description." if not no_cog - else _("Miscellaneous commands without a category.") + else "Miscellaneous commands without a category." ) embed = discord.Embed(description=f"*{description}*", color=bot.main_color) - embed.add_field(name=_("Commands"), value=format_ or _("No commands.")) + 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") - ) + continued = " (Continued)" if embeds else "" + 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( - text=_( - 'Type "{prefix}{command} command" ' "for more info on a specific command." - ).format(prefix=prefix, command=self.command_attrs["name"]) + text=f'Type "{prefix}{self.command_attrs["name"]} command" ' + "for more info on a specific command." ) embeds.append(embed) return embeds @@ -117,19 +112,19 @@ async def _get_help_embed(self, topic): if perm_level is not PermissionLevel.INVALID: perm_level = f"{perm_level.name} [{perm_level}]" else: - perm_level = _("NONE") + perm_level = "NONE" embed = discord.Embed( title=f"`{self.get_command_signature(topic)}`", color=self.context.bot.main_color, - description=self.process_help_msg(_(topic.help)), + description=self.process_help_msg(topic.help), ) return embed, perm_level async def send_command_help(self, command): topic = await self._get_help_embed(command) if topic is not None: - topic[0].set_footer(text=_("Permission level: {level}").format(level=topic[1])) + topic[0].set_footer(text=f"Permission level: {topic[1]}") await self.get_destination().send(embed=topic[0]) async def send_group_help(self, group): @@ -137,7 +132,7 @@ async def send_group_help(self, group): if topic is None: return embed = topic[0] - embed.add_field(name=_("Permission Level"), value=topic[1], inline=False) + embed.add_field(name="Permission Level", value=topic[1], inline=False) format_ = "" length = len(group.commands) @@ -151,13 +146,12 @@ async def send_group_help(self, group): branch = "└─" else: branch = "├─" - format_ += f"`{branch} {command.name}` - {_(command.short_doc)}\n" + format_ += f"`{branch} {command.name}` - {command.short_doc}\n" - embed.add_field(name=_("Sub Command(s)"), value=format_[:1024], inline=False) + embed.add_field(name="Sub Command(s)", value=format_[:1024], inline=False) embed.set_footer( - text=_('Type "{prefix}{command} command" ' "for more info on a command.").format( - prefix=self.clean_prefix, command=self.command_attrs["name"] - ) + text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' + "for more info on a command." ) await self.get_destination().send(embed=embed) @@ -167,8 +161,7 @@ async def send_error_message(self, error): val = self.context.bot.snippets.get(command) if val is not None: embed = discord.Embed( - title=_("{command} is a snippet.").format(command=command), - color=self.context.bot.main_color, + 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) @@ -179,12 +172,10 @@ async def send_error_message(self, error): if not values: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.context.bot.error_color, - description=_( - "Alias `{command}` is invalid, this alias will now be deleted." - "This alias will now be deleted." - ).format(command=command), + 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) @@ -192,34 +183,28 @@ async def send_error_message(self, error): else: if len(values) == 1: embed = discord.Embed( - title=_("{command} is an alias.").format(command=command), - color=self.context.bot.main_color, - ) - embed.add_field( - name=_("`{command}` points to:").format(command=command), value=values[0] + 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=_("{command} is an alias.").format(command=command), + title=f"{command} is an alias.", color=self.context.bot.main_color, - description=_("**`{command}` points to the following steps:**").format( - command=command - ), + description=f"**`{command}` points to the following steps:**", ) for i, val in enumerate(values, start=1): - embed.add_field(name=_("Step") + f" {i}:", value=val) + embed.add_field(name=f"Step {i}:", value=val) embed.set_footer( - text=_('Type "{prefix}{command} alias" ' "for more details on aliases.").format( - prefix=self.clean_prefix, command=self.command_attrs["name"] - ) + text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" ' + "for more details on aliases." ) return await self.get_destination().send(embed=embed) logger.warning("CommandNotFound: %s", error) embed = discord.Embed(color=self.context.bot.error_color) - embed.set_footer(text=_('Command/Category "{command}" not found.').format(command=command)) + embed.set_footer(text=f'Command/Category "{command}" not found.') choices = set() @@ -229,15 +214,12 @@ async def send_error_message(self, error): closest = get_close_matches(command, choices) if closest: - embed.add_field( - name=_("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.title = "Cannot find command or category" embed.set_footer( - text=_('Type "{prefix}{command}" ' "for a list of all available commands.").format( - prefix=self.clean_prefix, command=self.command_attrs["name"] - ) + text=f'Type "{self.clean_prefix}{self.command_attrs["name"]}" ' + "for a list of all available commands." ) await self.get_destination().send(embed=embed) @@ -251,12 +233,15 @@ def __init__(self, bot): self.bot.help_command = ModmailHelpCommand( verify_checks=False, command_attrs={ - "help": _("Shows this help message."), + "help": "Shows this help message.", "checks": [checks.has_permissions_predicate(PermissionLevel.REGULAR)], }, ) self.bot.help_command.cog = self self.loop_presence.start() # pylint: disable=no-member + if not self.bot.config.get("enable_eval"): + self.eval_.enabled = False + logger.info("Eval disabled. enable_eval=False") def cog_unload(self): self.bot.help_command = self._original_help_command @@ -275,9 +260,7 @@ async def changelog(self, ctx, version: str.lower = ""): return await ctx.send( embed=discord.Embed( color=self.bot.error_color, - description=_("The specified version `{version}` could not be found.").format( - version=version - ), + description=f"The specified version `{version}` could not be found.", ) ) @@ -293,9 +276,7 @@ async def changelog(self, ctx, version: str.lower = ""): finally: logger.warning("Failed to display changelog.", exc_info=True) await ctx.send( - _("View the changelog here: {url}").format( - url=f"{changelog.latest_version.changelog_url}#v{version[::2]}" - ) + f"View the changelog here: {changelog.latest_version.changelog_url}#v{version[::2]}" ) @commands.command(aliases=["info"]) @@ -305,23 +286,21 @@ async def about(self, ctx): """Shows information about this bot.""" embed = discord.Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) embed.set_author( - name=_("Modmail - About"), + name="Modmail - About", icon_url=self.bot.user.avatar_url, url="https://discord.gg/F34cRU8", ) embed.set_thumbnail(url=self.bot.user.avatar_url) - desc = _( - "This is an open source Discord bot that serves as a means for " - "members to easily communicate with server administrators in " - "an organised manner." - ) + desc = "This is an open source Discord bot that serves as a means for " + desc += "members to easily communicate with server administrators in " + desc += "an organised manner." embed.description = desc - embed.add_field(name=_("Uptime"), value=self.bot.uptime) - embed.add_field(name=_("Latency"), value=f"{self.bot.latency * 1000:.2f} ms") - embed.add_field(name=_("Version"), value=f"`{self.bot.version}`") - embed.add_field(name=_("Authors"), value="`kyb3r`, `Taki`, `fourjr`") + embed.add_field(name="Uptime", value=self.bot.uptime) + embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms") + embed.add_field(name="Version", value=f"`{self.bot.version}`") + embed.add_field(name="Authors", value="`kyb3r`, `Taki`, `fourjr`") changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version @@ -330,30 +309,26 @@ async def about(self, ctx): stable = next( filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions) ) - footer = _( - "You are on the prerelease version • the latest version is v{version}." - ).format(version=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 = _("A newer version is available v{version}.").format(version=latest.version) + footer = f"A newer version is available v{latest.version}." else: - footer = _("You are up to date with the latest version.") + footer = "You are up to date with the latest version." embed.add_field( - name=_("Want Modmail in Your Server?"), - value=_( - "Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " - "and join our [Discord server](https://discord.gg/F34cRU8/)!" - ), + name="Want Modmail in Your Server?", + value="Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " + "and join our [Discord server](https://discord.gg/F34cRU8/)!", inline=False, ) embed.add_field( - name=_("Support the Developers"), - value=_( - "This bot is completely free for everyone. We rely on kind individuals " - "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " - "to keep this bot free forever!" - ), + name="Support the Developers", + value="This bot is completely free for everyone. We rely on kind individuals " + "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " + "to keep this bot free forever!", inline=False, ) @@ -400,10 +375,10 @@ async def debug(self, ctx): if not logs: embed = discord.Embed( color=self.bot.main_color, - title=_("Debug Logs:"), - description=_("You don't have any logs at the moment."), + title="Debug Logs:", + description="You don't have any logs at the moment.", ) - embed.set_footer(text=_("Go to Heroku to see your logs.")) + embed.set_footer(text="Go to Heroku to see your logs.") return await ctx.send(embed=embed) messages = [] @@ -429,7 +404,7 @@ async def debug(self, ctx): messages.append(msg) embed = discord.Embed(color=self.bot.main_color) - embed.set_footer(text=_("Debug logs - Navigate using the reactions below.")) + embed.set_footer(text="Debug logs - Navigate using the reactions below.") session = MessagePaginatorSession(ctx, *messages, embed=embed) session.current = len(messages) - 1 @@ -461,7 +436,7 @@ async def debug_hastebin(self, ctx): logger.error(data["message"]) raise embed = discord.Embed( - title=_("Debug Logs"), + title="Debug Logs", color=self.bot.main_color, description=f"{haste_url}/" + key, ) @@ -469,9 +444,9 @@ 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.")) + embed.set_footer(text="Go to Heroku to see your logs.") await ctx.send(embed=embed) @debug.command(name="clear", aliases=["wipe"]) @@ -491,7 +466,7 @@ async def debug_clear(self, ctx): pass await ctx.send( embed=discord.Embed( - color=self.bot.main_color, description=_("Cached logs are now cleared.") + color=self.bot.main_color, description="Cached logs are now cleared." ) ) @@ -522,7 +497,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): self.bot.config.remove("activity_message") await self.bot.config.update() await self.set_presence() - embed = discord.Embed(title=_("Activity Removed"), color=self.bot.main_color) + embed = discord.Embed(title="Activity Removed", color=self.bot.main_color) return await ctx.send(embed=embed) if not message: @@ -533,7 +508,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="activity")) - activity, x = await self.set_presence( + activity, _ = await self.set_presence( activity_type=activity_type, activity_message=message ) @@ -541,15 +516,13 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): self.bot.config["activity_message"] = activity.name await self.bot.config.update() - msg = _("Activity set to: {name} ").format(name=activity.type.name.capitalize()) + msg = f"Activity set to: {activity.type.name.capitalize()} " if activity.type == ActivityType.listening: - msg += _("to {name}.").format(name=activity.name) + msg += f"to {activity.name}." 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() @@ -571,7 +544,7 @@ async def status(self, ctx, *, status_type: str.lower): self.bot.config.remove("status") await self.bot.config.update() await self.set_presence() - embed = discord.Embed(title=_("Status Removed"), color=self.bot.main_color) + embed = discord.Embed(title="Status Removed", color=self.bot.main_color) return await ctx.send(embed=embed) status_type = status_type.replace(" ", "_") @@ -580,15 +553,13 @@ async def status(self, ctx, *, status_type: str.lower): except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="status")) - x, status = await self.set_presence(status=status) + _, status = await self.set_presence(status=status) self.bot.config["status"] = status.value await self.bot.config.update() - msg = _("Status set to: {value}.").format(value=status.value) - embed = discord.Embed( - title=_("Status Changed"), description=msg, color=self.bot.main_color - ) + msg = f"Status set to: {status.value}." + embed = discord.Embed(title="Status Changed", description=msg, color=self.bot.main_color) return await ctx.send(embed=embed) async def set_presence(self, *, status=None, activity_type=None, activity_message=None): @@ -659,7 +630,7 @@ async def before_loop_presence(self): async def ping(self, ctx): """Pong! Returns your websocket latency.""" embed = discord.Embed( - title=_("Pong! Websocket Latency:"), + title="Pong! Websocket Latency:", description=f"{self.bot.ws.latency * 1000:.4f} ms", color=self.bot.main_color, ) @@ -667,7 +638,7 @@ async def ping(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) - async def mention(self, ctx, *, mention: str = None): + async def mention(self, ctx, *mention: Union[discord.Role, discord.Member]): """ Change what the bot mentions at the start of each thread. @@ -676,16 +647,15 @@ async def mention(self, ctx, *, mention: str = None): # TODO: ability to disable mention. current = self.bot.config["mention"] - if mention is None: + if not mention: 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: + mention = " ".join(i.mention for i in mention) embed = discord.Embed( - title=_("Changed mention!"), - description=_('On thread creation the bot now says "{mention}".').format( - mention=mention - ), + title="Changed mention!", + description=f'On thread creation the bot now says "{mention}".', color=self.bot.main_color, ) self.bot.config["mention"] = mention @@ -704,14 +674,14 @@ async def prefix(self, ctx, *, prefix=None): current = self.bot.prefix embed = discord.Embed( - title=_("Current prefix"), color=self.bot.main_color, description=f"{current}" + title="Current prefix", color=self.bot.main_color, description=f"{current}" ) if prefix is None: await ctx.send(embed=embed) else: - embed.title = _("Changed prefix!") - embed.description = _("Set prefix to `{prefix}`").format(prefix=prefix) + embed.title = "Changed prefix!" + embed.description = f"Set prefix to `{prefix}`" self.bot.config["prefix"] = prefix await self.bot.config.update() await ctx.send(embed=embed) @@ -746,7 +716,7 @@ async def config_options(self, ctx): f"`{name}`" for name in takewhile(lambda x: x is not None, names) ) embed = discord.Embed( - title=_("Available configuration keys:"), + title="Available configuration keys:", color=self.bot.main_color, description=description, ) @@ -767,22 +737,18 @@ async def config_set(self, ctx, key: str.lower, *, value: str): self.bot.config.set(key, value) await self.bot.config.update() embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("Set `{key}` to `{value}`.").format( - key=key, value=self.bot.config[key] - ), + description=f"Set `{key}` to `{self.bot.config[key]}`.", ) except InvalidConfigError as exc: embed = exc.embed else: embed = discord.Embed( - title=_("Error"), - color=self.bot.error_color, - description=_("{key} is an invalid key.").format(key=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)) + embed.add_field(name="Valid keys", value=", ".join(valid_keys)) return await ctx.send(embed=embed) @@ -795,18 +761,16 @@ async def config_remove(self, ctx, *, key: str.lower): self.bot.config.remove(key) await self.bot.config.update() embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("`{key}` had been reset to default.").format(key=key), + description=f"`{key}` had been reset to default.", ) else: embed = discord.Embed( - title=_("Error"), - color=self.bot.error_color, - description=_("{key} is an invalid key.").format(key=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)) + embed.add_field(name="Valid keys", value=", ".join(valid_keys)) return await ctx.send(embed=embed) @@ -822,28 +786,25 @@ async def config_get(self, ctx, *, key: str.lower = None): if key: if key in keys: - desc = _("`{key}` is set to `{value}`").format(key=key, value=self.bot.config[key]) + 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( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("`{key}` is an invalid key.").format(key=key), + description=f"`{key}` is an invalid key.", ) embed.set_footer( - text=_('Type "{prefix}config options" for a list of config variables.').format( - prefix=self.bot.prefix - ) + text=f'Type "{self.bot.prefix}config options" for a list of config variables.' ) else: embed = discord.Embed( color=self.bot.main_color, - description=_("Here is a list of currently " "set configuration variable(s)."), + 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) embed.set_author(name="Current config(s):", icon_url=self.bot.user.avatar_url) config = self.bot.config.filter_default(self.bot.config) @@ -866,9 +827,9 @@ async def config_help(self, ctx, key: str.lower = None): key, {**self.bot.config.public_keys, **self.bot.config.protected_keys} ) embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("`{key}` is an invalid key.").format(key=key), + description=f"`{key}` is an invalid key.", ) if closest: embed.add_field( @@ -880,9 +841,9 @@ async def config_help(self, ctx, key: str.lower = None): if key is not None and key not in config_help: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("No help details found for `{key}`.").format(key=key), + description=f"No help details found for `{key}`.", ) return await ctx.send(embed=embed) @@ -895,24 +856,21 @@ def fmt(val): if current_key == key: index = i embed = discord.Embed( - title=_("Configuration description on {current_key}:").format( - current_key=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"]: example_text += f"- {fmt(example)}\n" - embed.add_field(name=_("Example(s):"), value=example_text, inline=False) + embed.add_field(name="Example(s):", value=example_text, inline=False) note_text = "" for note in info["notes"]: note_text += f"- {fmt(note)}\n" if note_text: - embed.add_field(name=_("Note(s):"), value=note_text, inline=False) + embed.add_field(name="Note(s):", value=note_text, inline=False) if info.get("image") is not None: embed.set_image(url=fmt(info["image"])) @@ -957,12 +915,10 @@ async def alias(self, ctx, *, name: str.lower = None): if not values: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_( - "Alias `{name}` is invalid, it used to be `{value}`. " - "This alias will now be deleted." - ).format(name=name, value=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=utils.truncate(val, 1024)) self.bot.aliases.pop(name) @@ -971,9 +927,7 @@ async def alias(self, ctx, *, name: str.lower = None): if len(values) == 1: embed = discord.Embed( - title=_("Alias") + f' - "{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) @@ -991,13 +945,10 @@ 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."), - ) - embed.set_footer( - text=_("Do {prefix}help alias for more commands.").format(prefix=self.bot.prefix) + color=self.bot.error_color, description="You dont have any aliases at the moment." ) - embed.set_author(name=_("Aliases"), icon_url=ctx.guild.icon_url) + 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) embeds = [] @@ -1005,7 +956,7 @@ async def alias(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = utils.format_description(i, names) embed = discord.Embed(color=self.bot.main_color, description=description) - embed.set_author(name=_("Command Aliases"), icon_url=ctx.guild.icon_url) + embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -1033,9 +984,9 @@ async def make_alias(self, name, value, action): values = utils.parse_alias(value) if not values: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("Invalid multi-step alias, try wrapping each steps in quotes."), + 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 embed @@ -1050,15 +1001,12 @@ async def make_alias(self, name, value, action): multiple_alias = len(values) > 1 - embed = discord.Embed(title=_("{action} alias").format(action), color=self.bot.main_color) + embed = discord.Embed(title=f"{action} alias", color=self.bot.main_color) if not multiple_alias: - embed.add_field( - name=_("`{name}` points to:").format(name=name), - value=utils.truncate(values[0], 1024), - ) + embed.add_field(name=f"`{name}` points to:", value=utils.truncate(values[0], 1024)) else: - embed.description = _("`{name}` now points to the following steps:").format(name=name) + embed.description = f"`{name}` now points to the following steps:" for i, val in enumerate(values, start=1): view = StringView(val) @@ -1073,21 +1021,21 @@ async def make_alias(self, name, value, action): embed = discord.Embed(title="Error", color=self.bot.error_color) if multiple_alias: - embed.description = _( + embed.description = ( "The command you are attempting to point " - "to does not exist: `{command}`." - ).format(command=linked_command) + f"to does not exist: `{linked_command}`." + ) else: - embed.description = _( + embed.description = ( "The command you are attempting to point " - "to on step {number} does not exist: `{command}`." - ).format(number=i, command=linked_command) + f"to on step {i} does not exist: `{linked_command}`." + ) return embed else: save_aliases.append(val) if multiple_alias: - embed.add_field(name=_("Step") + f" {i}:", value=utils.truncate(val, 1024)) + 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() @@ -1112,36 +1060,30 @@ async def alias_add(self, ctx, name: str.lower, *, value): embed = None if self.bot.get_command(name): embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("A command with the same name already exists: `{name}`.").format( - name=name - ), + description=f"A command with the same name already exists: `{name}`.", ) elif name in self.bot.aliases: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("Another alias with the same name already exists: `{name}`.").format( - name=name - ), + description=f"Another alias with the same name already exists: `{name}`.", ) elif name in self.bot.snippets: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("A snippet with the same name already exists: `{name}`.").format( - name=name - ), + description=f"A snippet with the same name already exists: `{name}`.", ) elif len(name) > 120: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("Alias names cannot be longer than 120 characters."), + description="Alias names cannot be longer than 120 characters.", ) if embed is None: @@ -1158,9 +1100,9 @@ async def alias_remove(self, ctx, *, name: str.lower): await self.bot.config.update() embed = discord.Embed( - title=_("Removed alias"), + title="Removed alias", color=self.bot.main_color, - description=_("Successfully deleted `{name}`.").format(name=name), + description=f"Successfully deleted `{name}`.", ) else: embed = utils.create_not_found_embed(name, self.bot.aliases.keys(), "Alias") @@ -1264,11 +1206,9 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name level = self._parse_level(level_name) if level is PermissionLevel.INVALID: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("The referenced level does not exist: `{level}`.").format( - level=level_name - ), + description=f"The referenced level does not exist: `{level_name}`.", ) else: logger.info( @@ -1280,11 +1220,10 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name await self.bot.config.update() embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_( - "Successfully set command permission level for " "`{command}` to `{level}`." - ).format(command=command.qualified_name, level=level.name), + description="Successfully set command permission level for " + f"`{command.qualified_name}` to `{level.name}`.", ) return await ctx.send(embed=embed) @@ -1327,11 +1266,9 @@ async def permissions_add( if not check: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("The referenced {type} does not exist: `{name}`.").format( - type=type_, name=name - ), + description=f"The referenced {type_} does not exist: `{name}`.", ) return await ctx.send(embed=embed) @@ -1354,9 +1291,9 @@ async def permissions_add( await self.bot.main_category.set_permissions(key, read_messages=True) embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("Permission for `{name}` was successfully updated.").format(name=name), + description=f"Permission for `{name}` was successfully updated.", ) return await ctx.send(embed=embed) @@ -1405,12 +1342,10 @@ async def permissions_remove( if level is None: perm = self.bot.command_perm(name) embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_( - "The command permission level was never overridden: `{name}`, " - "current permission level is {perm_name}." - ).format(name=name, perm_name=perm.name), + description=f"The command permission level was never overridden: `{name}`, " + f"current permission level is {perm.name}.", ) else: logger.info("Restored command permission level for `%s`.", name) @@ -1418,11 +1353,9 @@ async def permissions_remove( await self.bot.config.update() perm = self.bot.command_perm(name) embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_( - "Command permission level for `{name}` was successfully restored to {perm_name}." - ).format(name=name, perm_name=perm.name), + description=f"Command permission level for `{name}` was successfully restored to {perm.name}.", ) return await ctx.send(embed=embed) @@ -1434,11 +1367,9 @@ async def permissions_remove( level = self._parse_level(name) if level is PermissionLevel.INVALID: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_("The referenced level does not exist: `{name}`.").format( - name=name - ), + description=f"The referenced level does not exist: `{name}`.", ) return await ctx.send(embed=embed) name = level.name @@ -1463,9 +1394,9 @@ async def permissions_remove( await self.bot.main_category.set_permissions(member, overwrite=None) embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_("Permission for `{name}` was successfully updated.").format(name=name), + description=f"Permission for `{name}` was successfully updated.", ) return await ctx.send(embed=embed) @@ -1476,8 +1407,8 @@ def _get_perm(self, ctx, name, type_): permissions = self.bot.config["level_permissions"].get(name, []) if not permissions: embed = discord.Embed( - title=_("Permission entries for {type} `{name}`:").format(type=type_, name=name), - description=_("No permission entries found."), + title=f"Permission entries for {type_} `{name}`:", + description="No permission entries found.", color=self.bot.main_color, ) else: @@ -1501,7 +1432,7 @@ def _get_perm(self, ctx, name, type_): values.append(str(perm)) embed = discord.Embed( - title=_("Permission entries for {type} `{name}`:").format(type=type_, name=name), + title=f"Permission entries for {type_} `{name}`:", description=", ".join(values), color=self.bot.main_color, ) @@ -1564,26 +1495,22 @@ async def permissions_get( desc_cmd = ( ", ".join(map(lambda x: f"`{x}`", cmds)) if cmds - else _("No permission entries found.") + else "No permission entries found." ) desc_level = ( ", ".join(map(lambda x: f"`{x}`", levels)) if levels - else _("No permission entries found.") + else "No permission entries found." ) embeds = [ discord.Embed( - title=_("{mention} has permission with the following commands:").format( - mention=mention - ), + title=f"{mention} has permission with the following commands:", description=desc_cmd, color=self.bot.main_color, ), discord.Embed( - title=_( - "{mention} has permission with the following permission levels:" - ).format(mention=mention), + title=f"{mention} has permission with the following permission levels:", description=desc_level, color=self.bot.main_color, ), @@ -1608,10 +1535,8 @@ async def permissions_get( if not overrides: embeds.append( discord.Embed( - title=_("Permission Overrides"), - description=_( - "You don't have any command level overrides at the moment." - ), + title="Permission Overrides", + description="You don't have any command level overrides at the moment.", color=self.bot.error_color, ) ) @@ -1625,7 +1550,7 @@ async def permissions_get( color=self.bot.main_color, description=description ) embed.set_author( - name=_("Permission Overrides"), icon_url=ctx.guild.icon_url + name="Permission Overrides", icon_url=ctx.guild.icon_url ) embeds.append(embed) @@ -1638,20 +1563,16 @@ async def permissions_get( perm = self.bot.command_perm(name) if level is None: embed = discord.Embed( - title=_("Error"), + title="Error", color=self.bot.error_color, - description=_( - "The command permission level was never overridden: `{name}`, " - "current permission level is {perm_name}." - ).format(name=name, perm_name=perm.name), + description=f"The command permission level was never overridden: `{name}`, " + f"current permission level is {perm.name}.", ) else: embed = discord.Embed( - title=_("Success"), + title="Success", color=self.bot.main_color, - description=_( - 'Permission override for command "{name}" is "{perm_name}".' - ).format(name=name, perm_name=perm.name), + description=f'Permission override for command "{name}" is "{perm.name}".', ) return await ctx.send(embed=embed) @@ -1727,19 +1648,14 @@ async def oauth_whitelist(self, ctx, target: Union[discord.Role, utils.User]): await self.bot.config.update() embed = discord.Embed(color=self.bot.main_color) - embed.title = _("Success") + embed.title = "Success" if not hasattr(target, "mention"): target = self.bot.get_user(target.id) or self.bot.modmail_guild.get_role(target.id) - if removed: - embed.description = _("Un-whitelisted {target_mention} to view logs.").format( - target_mention=target.mention - ) - else: - embed.description = _("Whitelisted {target_mention} to view logs.").format( - target_mention=target.mention - ) + embed.description = ( + f"{'Un-w' if removed else 'W'}hitelisted {target.mention} to view logs." + ) await ctx.send(embed=embed) @@ -1761,9 +1677,9 @@ async def oauth_show(self, ctx): roles.append(role) embed = discord.Embed(color=self.bot.main_color) - embed.title = _("Oauth Whitelist") + embed.title = "Oauth Whitelist" - embed.add_field(name=_("Users"), value=" ".join(u.mention for u in users) 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/clients.py b/core/clients.py index 54cc28c9be..03921d5a17 100644 --- a/core/clients.py +++ b/core/clients.py @@ -1,18 +1,21 @@ import secrets +import sys from datetime import datetime from json import JSONDecodeError -from typing import Union +from typing import Union, Optional from discord import Member, DMChannel, TextChannel, Message from aiohttp import ClientResponseError, ClientResponse +from motor.motor_asyncio import AsyncIOMotorClient +from pymongo.errors import ConfigurationError from core.models import getLogger logger = getLogger(__name__) -class RequestClient: +class ApiClient: """ This class represents the general request class for all type of clients. @@ -29,8 +32,9 @@ class RequestClient: The bot's current running `ClientSession`. """ - def __init__(self, bot): + def __init__(self, bot, db): self.bot = bot + self.db = db self.session = bot.session async def request( @@ -74,16 +78,152 @@ async def request( except (JSONDecodeError, ClientResponseError): return await resp.text() - -class ApiClient(RequestClient): - @property - def db(self): - return self.bot.db - @property def logs(self): return self.db.logs + async def setup_indexes(self): + return NotImplemented + + async def validate_database_connection(self): + return NotImplemented + + async def get_user_logs(self, user_id: Union[str, int]) -> list: + return NotImplemented + + async def get_latest_user_logs(self, user_id: Union[str, int]): + return NotImplemented + + async def get_responded_logs(self, user_id: Union[str, int]) -> list: + return NotImplemented + + async def get_open_logs(self) -> list: + return NotImplemented + + async def get_log(self, channel_id: Union[str, int]) -> dict: + return NotImplemented + + async def get_log_link(self, channel_id: Union[str, int]) -> str: + return NotImplemented + + async def create_log_entry( + self, recipient: Member, channel: TextChannel, creator: Member + ) -> str: + return NotImplemented + + async def delete_log_entry(self, key: str) -> bool: + return NotImplemented + + async def get_config(self) -> dict: + return NotImplemented + + async def update_config(self, data: dict): + return NotImplemented + + async def edit_message(self, message_id: Union[int, str], new_content: str) -> None: + return NotImplemented + + async def append_log( + self, + message: Message, + *, + message_id: str = "", + channel_id: str = "", + type_: str = "thread_message", + ) -> dict: + return NotImplemented + + async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: + return NotImplemented + + async def search_closed_by(self, user_id: Union[int, str]): + return NotImplemented + + async def search_by_text(self, text: str, limit: Optional[int]): + return NotImplemented + + def get_plugin_partition(self, cog): + return NotImplemented + + +class MongoDBClient(ApiClient): + def __init__(self, bot): + mongo_uri = bot.config["connection_uri"] + if mongo_uri is None: + mongo_uri = bot.config["mongo_uri"] + if mongo_uri is not None: + logger.warning( + "You're using the old config MONGO_URI, " + "consider switching to the new CONNECTION_URI config." + ) + else: + logger.critical("A Mongo URI is necessary for the bot to function.") + raise RuntimeError + + try: + db = AsyncIOMotorClient(mongo_uri).modmail_bot + except ConfigurationError as e: + logger.critical( + "Your MongoDB CONNECTION_URI might be copied wrong, try re-copying from the source again. " + "Otherwise noted in the following message:" + ) + logger.critical(e) + sys.exit(0) + + super().__init__(bot, db) + + async def setup_indexes(self): + """Setup text indexes so we can use the $search operator""" + coll = self.db.logs + index_name = "messages.content_text_messages.author.name_text_key_text" + + index_info = await coll.index_information() + + # Backwards compatibility + old_index = "messages.content_text_messages.author.name_text" + if old_index in index_info: + logger.info("Dropping old index: %s", old_index) + await coll.drop_index(old_index) + + if index_name not in index_info: + 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")] + ) + logger.debug("Successfully configured and verified database indexes.") + + async def validate_database_connection(self): + try: + await self.db.command("buildinfo") + except Exception as exc: + logger.critical("Something went wrong while connecting to the database.") + message = f"{type(exc).__name__}: {str(exc)}" + logger.critical(message) + + if "ServerSelectionTimeoutError" in message: + logger.critical( + "This may have been caused by not whitelisting " + "IPs correctly. Make sure to whitelist all " + "IPs (0.0.0.0/0) https://i.imgur.com/mILuQ5U.png" + ) + + if "OperationFailure" in message: + logger.critical( + "This is due to having invalid credentials in your MongoDB CONNECTION_URI. " + "Remember you need to substitute `` with your actual password." + ) + logger.critical( + "Be sure to URL encode your username and password (not the entire URL!!), " + "https://www.urlencoder.io/, if this issue persists, try changing your username and password " + "to only include alphanumeric characters, no symbols." + "" + ) + raise + else: + logger.debug("Successfully connected to the database.") + logger.line("debug") + async def get_user_logs(self, user_id: Union[str, int]) -> list: query = {"recipient.id": str(user_id), "guild_id": str(self.bot.guild_id)} projection = {"messages": {"$slice": 5}} @@ -245,6 +385,26 @@ async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: {"channel_id": str(channel_id)}, {"$set": data}, return_document=True ) + async def search_closed_by(self, user_id: Union[int, str]): + return await self.logs.find( + {"guild_id": str(self.bot.guild_id), "open": False, "closer.id": str(user_id)}, + {"messages": {"$slice": 5}}, + ).to_list(None) + + async def search_by_text(self, text: str, limit: Optional[int]): + return await self.bot.db.logs.find( + { + "guild_id": str(self.bot.guild_id), + "open": False, + "$text": {"$search": f'"{text}"'}, + }, + {"messages": {"$slice": 5}}, + ).to_list(limit) + + def get_plugin_partition(self, cog): + cls_name = cog.__class__.__name__ + return self.db.plugins[cls_name] + class PluginDatabaseClient: def __init__(self, bot): @@ -252,4 +412,4 @@ def __init__(self, bot): def get_partition(self, cog): cls_name = cog.__class__.__name__ - return self.bot.db.plugins[cls_name] + return self.bot.api.db.plugins[cls_name] diff --git a/core/config.py b/core/config.py index a0eda4a2dc..f2bb54a5c8 100644 --- a/core/config.py +++ b/core/config.py @@ -57,6 +57,7 @@ class ConfigManager: "thread_close_title": "Thread Closed", "thread_close_response": "{closer.mention} has closed this Modmail thread.", "thread_self_close_response": "You have closed this Modmail thread.", + "thread_move_title": "Thread Moved", "thread_move_notify": False, "thread_move_response": "This thread has been moved.", "disabled_new_thread_title": "Not Delivered", @@ -81,7 +82,7 @@ class ConfigManager: "activity_type": None, "status": None, # dm_disabled 0 = none, 1 = new threads, 2 = all threads - # TODO: use emum + # TODO: use enum "dm_disabled": 0, "oauth_whitelist": [], # moderation @@ -107,12 +108,17 @@ class ConfigManager: "log_url": "https://example.com/", "log_url_prefix": "/logs", "mongo_uri": None, + "database_type": "mongodb", + "connection_uri": None, # replace mongo uri in the future "owners": None, # bot "token": None, + "enable_plugins": True, + "enable_eval": True, + # github access token for private repositories + "github_token": None, # Logging "log_level": "INFO", - "enable_plugins": True, } colors = {"mod_color", "recipient_color", "main_color", "error_color"} @@ -128,6 +134,7 @@ class ConfigManager: "thread_auto_close_silently", "thread_move_notify", "enable_plugins", + "enable_eval", } special_types = {"status", "activity_type"} diff --git a/core/config_help.json b/core/config_help.json index db9218d2b3..9778f49f8c 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -350,6 +350,16 @@ "See also: `thread_close_title`, `thread_close_footer`, `thread_close_response`." ] }, + "thread_move_title": { + "default": "Thread Moved", + "description": "The title of the message embed when a thread is moved.", + "examples": [ + "`{prefix}config set thread_move_title Thread transferred to another channel!" + ], + "notes": [ + "See also: `thread_move_notify`, `thread_move_response`." + ] + }, "thread_move_notify": { "default": "No", "description": "Notify the recipient if the thread was moved.", @@ -358,7 +368,7 @@ "`{prefix}config set thread_move_notify no`" ], "notes": [ - "See also: `thread_move_response`." + "See also: `thread_move_title`, `thread_move_response`." ] }, "thread_move_response": { @@ -369,7 +379,7 @@ ], "notes": [ "Only has an effect when `thread_move_notify` is on.", - "See also: `thread_move_notify`." + "See also: `thread_move_title`, `thread_move_notify`." ] }, "disabled_new_thread_title": { diff --git a/core/paginator.py b/core/paginator.py index 45c059f844..7ba1c98b60 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -176,7 +176,7 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: """ self.running = False - sent_emoji, x = await self.ctx.bot.retrieve_emoji() + sent_emoji, _ = await self.ctx.bot.retrieve_emoji() await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji) if delete: diff --git a/core/thread.py b/core/thread.py index 0926a0acd0..9c388389e0 100644 --- a/core/thread.py +++ b/core/thread.py @@ -72,15 +72,13 @@ def ready(self) -> bool: def ready(self, flag: bool): if flag: self._ready_event.set() - self.bot.dispatch("thread_ready", self) + self.bot.dispatch("thread_create", self) else: self._ready_event.clear() async def setup(self, *, creator=None, category=None): """Create the thread channel and other io related initialisation tasks""" - - self.bot.dispatch("thread_create", self) - + self.bot.dispatch("thread_initiate", self) recipient = self.recipient # in case it creates a channel outside of category @@ -175,6 +173,7 @@ async def send_recipient_genesis_message(): await self.bot.add_reaction(msg, close_emoji) await asyncio.gather(send_genesis_message(), send_recipient_genesis_message()) + self.bot.dispatch("thread_ready", self) def _format_info_embed(self, user, log_url, log_count, color): """Get information about a member of a server @@ -317,6 +316,7 @@ async def _close( { "open": False, "closed_at": str(datetime.utcnow()), + "nsfw": self.channel.nsfw, "close_message": message if not silent else None, "closer": { "id": str(closer.id), @@ -340,7 +340,12 @@ async def _close( else: sneak_peak = "No content" - desc = f"[`{log_data['key']}`]({log_url}): " + if self.channel.nsfw: + _nsfw = "NSFW-" + else: + _nsfw = "" + + desc = f"[`{_nsfw}{log_data['key']}`]({log_url}): " desc += truncate(sneak_peak, max=75 - 13) else: desc = "Could not resolve log url." @@ -362,7 +367,7 @@ async def _close( event = "Thread Closed as Scheduled" if scheduled else "Thread Closed" # embed.set_author(name=f"Event: {event}", url=log_url) - embed.set_footer(text=f"{event} by {_closer}") + embed.set_footer(text=f"{event} by {_closer}", icon_url=closer.avatar_url) embed.timestamp = datetime.utcnow() tasks = [self.bot.config.update()] @@ -457,9 +462,14 @@ async def find_linked_messages( message_id: typing.Optional[int] = None, either_direction: bool = False, message1: discord.Message = None, + note: bool = True, ) -> typing.Tuple[discord.Message, typing.Optional[discord.Message]]: if message1 is not None: - if not message1.embeds or not message1.embeds[0].author.url: + if ( + not message1.embeds + or not message1.embeds[0].author.url + or message1.author != self.bot.user + ): raise ValueError("Malformed thread message.") elif message_id is not None: @@ -469,13 +479,18 @@ async def find_linked_messages( raise ValueError("Thread message not found.") if not ( - message1.embeds and message1.embeds[0].author.url and message1.embeds[0].color + message1.embeds + and message1.embeds[0].author.url + and message1.embeds[0].color + and message1.author == self.bot.user ): raise ValueError("Thread message not found.") if message1.embeds[0].color.value == self.bot.main_color and message1.embeds[ 0 ].author.name.startswith("Note"): + if not note: + raise ValueError("Thread message not found.") return message1, None if message1.embeds[0].color.value != self.bot.mod_color and not ( @@ -495,6 +510,8 @@ async def find_linked_messages( and message1.embeds[0].color.value == self.bot.recipient_color ) ) + and message1.embeds[0].author.url.split("#")[-1].isdigit() + and message1.author == self.bot.user ): break else: @@ -516,7 +533,7 @@ async def find_linked_messages( if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id: return message1, msg except ValueError: - raise ValueError("DM message not found.") + continue raise ValueError("DM message not found.") async def edit_message(self, message_id: typing.Optional[int], message: str) -> None: @@ -537,16 +554,13 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> await asyncio.gather(*tasks) - async def delete_message(self, message: typing.Union[int, discord.Message] = None) -> None: - try: - 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 - + async def delete_message( + self, message: typing.Union[int, discord.Message] = None, note: bool = True + ) -> None: + if isinstance(message, discord.Message): + message1, message2 = await self.find_linked_messages(message1=message, note=note) + else: + message1, message2 = await self.find_linked_messages(message, note=note) tasks = [] if not isinstance(message, discord.Message): tasks += [message1.delete()] @@ -571,12 +585,12 @@ async def find_linked_message_from_dm(self, message, either_direction=False): return linked_message msg_id = url.split("#")[-1] - try: - if int(msg_id) == message.id: - return linked_message - except ValueError: - raise ValueError("Malformed dm channel message.") - raise ValueError("DM channel message not found.") + if not msg_id.isdigit(): + continue + msg_id = int(msg_id) + if int(msg_id) == message.id: + return linked_message + raise ValueError("Thread channel message not found.") async def edit_dm_message(self, message: discord.Message, content: str) -> None: try: diff --git a/core/time.py b/core/time.py index cc30d84f30..331e26349f 100644 --- a/core/time.py +++ b/core/time.py @@ -134,7 +134,7 @@ def convert(self, ctx, argument): # foo date time # first the first two cases: - dt, status, begin, end, x = elements[0] + dt, status, begin, end, _ = elements[0] if not status.hasDateOrTime: return self.check_constraints(self.now, argument) diff --git a/core/translations.py b/core/translations.py deleted file mode 100644 index 98a3327386..0000000000 --- a/core/translations.py +++ /dev/null @@ -1,25 +0,0 @@ -import builtins -import csv -import os - - -class Translator: - def __init__(self): - self.language = os.getenv("language", "en") - self.texts = {} - self.generate_texts() - - def generate_texts(self): - with open(f"languages/{self.language}.csv", encoding="utf8") as f: - reader = csv.reader(f, dialect="unix") - - for n, row in enumerate(reader): - if n != 0: - self.texts[row[0]] = row[1] - - def translate(self, identifier): - return self.texts.get(identifier, identifier) - - -def init(): - builtins._ = Translator().translate diff --git a/languages/en.csv b/languages/en.csv deleted file mode 100644 index 388091bab5..0000000000 --- a/languages/en.csv +++ /dev/null @@ -1,1172 +0,0 @@ -"Identifier","English","Context" -"You can only setup in the Modmail guild: {guild_name}.","You can only setup in the Modmail guild: {guild_name}.","File: cogs\modmail.py/L44" -"{guild_name} is already set up.","{guild_name} is already set up.","File: cogs\modmail.py/L49" -"Error","Error","File: cogs\modmail.py/L53/L217/L225/L233/L712/L1059/L1151 cogs\utility.py/L168/L748/L774/L802/L822/L892/L979/L1045/L1085/L1092/L1099/L1125/L1245/L1316/L1378/L1559" -"Modmail functioning guild not found.","Modmail functioning guild not found.","File: cogs\modmail.py/L54" -"Friendly Reminder","Friendly Reminder","File: cogs\modmail.py/L91" -"You may use the `{prefix}config set log_channel_id ` command to set up a custom log channel, then you can delete this default {log_channel} log channel.","You may use the `{prefix}config set log_channel_id ` command to set up a custom log channel, then you can delete this default {log_channel} log channel.","File: cogs\modmail.py/L92" -"Thanks for using the bot!","Thanks for using the bot!","File: cogs\modmail.py/L99" -"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)!","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)!","File: cogs\modmail.py/L100" -"Type ""{prefix}help"" for a complete list of commands.","Type ""{prefix}help"" for a complete list of commands.","File: cogs\modmail.py/L106" -"**Successfully set up server.**\nConsider setting permission levels to give access to roles or users the ability to use Modmail.\n\nType:\n- `{prefix}permissions` and `{prefix}permissions add` for more info on setting permissions.\n- `{prefix}config help` for a list of available customizations.","**Successfully set up server.**\nConsider setting permission levels to give access to roles or users the ability to use Modmail.\n\nType:\n- `{prefix}permissions` and `{prefix}permissions add` for more info on setting permissions.\n- `{prefix}config help` for a list of available customizations.","File: cogs\modmail.py/L115" -"Snippets","Snippets","File: cogs\modmail.py/L175" -"Snippet `{name}` already exists.","Snippet `{name}` already exists.","File: cogs\modmail.py/L219" -"An alias with the same name already exists: `{name}`.","An alias with the same name already exists: `{name}`.","File: cogs\modmail.py/L227" -"Snippet names cannot be longer than 120 characters.","Snippet names cannot be longer than 120 characters.","File: cogs\modmail.py/L235" -"Removed snippet","Removed snippet","File: cogs\modmail.py/L256" -"Snippet `{name}` is now deleted.","Snippet `{name}` is now deleted.","File: cogs\modmail.py/L258" -"Edited snippet","Edited snippet","File: cogs\modmail.py/L281" -"Thread Moved","Thread Moved","File: cogs\modmail.py/L310" -"Scheduled close","Scheduled close","File: cogs\modmail.py/L325" -"This thread will close {silent}in {time}.","This thread will close {silent}in {time}.","File: cogs\modmail.py/L326" -"Message","Message","File: cogs\modmail.py/L331" -"Closing will be cancelled if a thread message is sent.","Closing will be cancelled if a thread message is sent.","File: cogs\modmail.py/L334" -"silent","silent","File: cogs\modmail.py/L369" -"silently","silently","File: cogs\modmail.py/L369" -"cancel","cancel","File: cogs\modmail.py/L370" -"Scheduled close has been cancelled.","Scheduled close has been cancelled.","File: cogs\modmail.py/L378" -"This thread has not already been scheduled to close.","This thread has not already been scheduled to close.","File: cogs\modmail.py/L383" -"{mention} is already going to be mentioned.","{mention} is already going to be mentioned.","File: cogs\modmail.py/L433" -"{mention} will be mentioned on the next message received.","{mention} will be mentioned on the next message received.","File: cogs\modmail.py/L440" -"{mention} does not have a pending notification.","{mention} does not have a pending notification.","File: cogs\modmail.py/L472" -"{mention} will no longer be notified.","{mention} will no longer be notified.","File: cogs\modmail.py/L479" -"{mention} is already subscribed to this thread.","{mention} is already subscribed to this thread.","File: cogs\modmail.py/L512" -"{mention} will now be notified of all messages received.","{mention} will now be notified of all messages received.","File: cogs\modmail.py/L519" -"{mention} is not already subscribed to this thread.","{mention} is not already subscribed to this thread.","File: cogs\modmail.py/L551" -"{mention} is now unsubscribed to this thread.","{mention} is now unsubscribed to this thread.","File: cogs\modmail.py/L558" -"Unknown","Unknown","File: cogs\modmail.py/L610" -"Closed By","Closed By","File: cogs\modmail.py/L613" -"Created by","Created by","File: cogs\modmail.py/L616" -"Preview","Preview","File: cogs\modmail.py/L619" -"Link","Link","File: cogs\modmail.py/L624" -"Log Key","Log Key","File: cogs\modmail.py/L627" -"Recipient ID","Recipient ID","File: cogs\modmail.py/L629" -"This user does not have any previous logs.","This user does not have any previous logs.","File: cogs\modmail.py/L660" -"No log entries have been found for that query","No log entries have been found for that query","File: cogs\modmail.py/L693" -"Log entry `{key}` not found.","Log entry `{key}` not found.","File: cogs\modmail.py/L713" -"Log entry `{key}` successfully deleted.","Log entry `{key}` successfully deleted.","File: cogs\modmail.py/L719" -"{mention} has not responded to any threads.","{mention} has not responded to any threads.","File: cogs\modmail.py/L743" -"No log entries have been found for that query.","No log entries have been found for that query.","File: cogs\modmail.py/L776" -"Failed","Failed","File: cogs\modmail.py/L869/L1177" -"Cannot find a message to edit.","Cannot find a message to edit.","File: cogs\modmail.py/L870" -"Cannot start a thread with a bot.","Cannot start a thread with a bot.","File: cogs\modmail.py/L900" -"A thread for this user already exists in {mention}.","A thread for this user already exists in {mention}.","File: cogs\modmail.py/L908" -"Created Thread","Created Thread","File: cogs\modmail.py/L919" -"Thread started by {author_mention} for {user_mention}.","Thread started by {author_mention} for {user_mention}.","File: cogs\modmail.py/L920" -"Blocked Users","Blocked Users","File: cogs\modmail.py/L939/L960" -"No Reason Provided","No Reason Provided","File: cogs\modmail.py/L960" -"(Continued)","(Continued)","File: cogs\modmail.py/L963 cogs\utility.py/L67" -"Currently there are no blocked users.","Currently there are no blocked users.","File: cogs\modmail.py/L971" -"Success","Success","File: cogs\modmail.py/L997/L1084/L1091/L1308/L1344/L1357/L1377/L1358 cogs\utility.py/L725/L759/L1231/L1291/L1367/L1392/L1559/L1595" -"{mention} is no longer whitelisted.","{mention} is no longer whitelisted.","File: cogs\modmail.py/L998" -"{mention} was previously blocked internally for ","{mention} was previously blocked internally for ","File: cogs\modmail.py/L1018" -"Cannot block {mention}, user is whitelisted.","Cannot block {mention}, user is whitelisted.","File: cogs\modmail.py/L1060" -"{mention} was previously blocked {old_reason}.\n{mention} is now blocked {reason}","{mention} was previously blocked {old_reason}.\n{mention} is now blocked {reason}","File: cogs\modmail.py/L1085" -"{mention} is now blocked {reason}","{mention} is now blocked {reason}","File: cogs\modmail.py/L1093" -"{mention} was previously blocked internally {reason}.\n{mention} is no longer blocked.","{mention} was previously blocked internally {reason}.\n{mention} is no longer blocked.","File: cogs\modmail.py/L1132" -"However, if the original system block reason still applies, {name} will be automatically blocked again. Use ","However, if the original system block reason still applies, {name} will be automatically blocked again. Use ","File: cogs\modmail.py/L1137" -"{mention} is no longer blocked.","{mention} is no longer blocked.","File: cogs\modmail.py/L1147" -"{mention} is not blocked.","{mention} is not blocked.","File: cogs\modmail.py/L1152" -"Cannot find a message to delete.","Cannot find a message to delete.","File: cogs\modmail.py/L1178" -"Modmail will now accept **all** DM messages.","Modmail will now accept **all** DM messages.","File: cogs\modmail.py/L1309" -"Modmail is already accepting all DM messages.","Modmail is already accepting all DM messages.","File: cogs\modmail.py/L1316" -"Modmail will not create any **new** threads.","Modmail will not create any **new** threads.","File: cogs\modmail.py/L1345" -"Modmail is already not creating any new threads.","Modmail is already not creating any new threads.","File: cogs\modmail.py/L1352" -"Modmail will not create **new** threads, but existing threads will now be functioning.","Modmail will not create **new** threads, but existing threads will now be functioning.","File: cogs\modmail.py/L1358" -"Modmail will not accept **any** DM messages.","Modmail will not accept **any** DM messages.","File: cogs\modmail.py/L1378" -"Modmail is already not accepting any DM messages.","Modmail is already not accepting any DM messages.","File: cogs\modmail.py/L1385" -"New Threads Disabled","New Threads Disabled","File: cogs\modmail.py/L1400" -"Modmail is not creating new threads.","Modmail is not creating new threads.","File: cogs\modmail.py/L1401" -"All DM Disabled","All DM Disabled","File: cogs\modmail.py/L1406" -"Modmail is not accepting any DM messages for new and existing threads.","Modmail is not accepting any DM messages for new and existing threads.","File: cogs\modmail.py/L1407" -"Enabled","Enabled","File: cogs\modmail.py/L1412" -"Modmail is accepting all DM messages.","Modmail is accepting all DM messages.","File: cogs\modmail.py/L1413" -"Plugins are still loading, please try again later.","Plugins are still loading, please try again later.","File: cogs\plugins.py/L230/L470" -"Your bot's version is too low. This plugin requires version `{required_version}`.","Your bot's version is too low. This plugin requires version `{required_version}`.","File: cogs\plugins.py/L246" -"Invalid plugin name, double check the plugin name or use one of the following formats: username/repo/plugin, username/repo/plugin@branch.","Invalid plugin name, double check the plugin name or use one of the following formats: username/repo/plugin, username/repo/plugin@branch.","File: cogs\plugins.py/L260" -"This plugin is already installed.","This plugin is already installed.","File: cogs\plugins.py/L295" -"Cannot install this plugin (dupe cog name).","Cannot install this plugin (dupe cog name).","File: cogs\plugins.py/L303" -"Starting to download plugin from {plugin_link}...","Starting to download plugin from {plugin_link}...","File: cogs\plugins.py/L309" -"Failed to download plugin, check logs for error.","Failed to download plugin, check logs for error.","File: cogs\plugins.py/L320" -"Successfully installed plugin.\n*Friendly reminder, plugins have absolute control over your bot. Please only install plugins from developers you trust.*","Successfully installed plugin.\n*Friendly reminder, plugins have absolute control over your bot. Please only install plugins from developers you trust.*","File: cogs\plugins.py/L345" -"This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.","This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.","File: cogs\plugins.py/L355" -"Plugin is not installed.","Plugin is not installed.","File: cogs\plugins.py/L376/L415" -"The plugin is successfully uninstalled.","The plugin is successfully uninstalled.","File: cogs\plugins.py/L402" -"Successfully updated {plugin_name}.","Successfully updated {plugin_name}.","File: cogs\plugins.py/L429" -"No plugins are loaded due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.","No plugins are loaded due to `ENABLE_PLUGINS=false`, to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.","File: cogs\plugins.py/L462" -"There are no plugins currently loaded.","There are no plugins currently loaded.","File: cogs\plugins.py/L477" -"Loaded plugins:","Loaded plugins:","File: cogs\plugins.py/L498" -"Could not find a plugin with name ""{plugin_name}"" within the registry.","Could not find a plugin with name ""{plugin_name}"" within the registry.","File: cogs\plugins.py/L534" -"Perhaps you meant:","Perhaps you meant:","File: cogs\plugins.py/L541 cogs\utility.py/L211" -"Installation","Installation","File: cogs\plugins.py/L562" -"Your bot is unable to install this plugin, minimum required version is v{required_version}.","Your bot is unable to install this plugin, minimum required version is v{required_version}.","File: cogs\plugins.py/L582" -"Your bot is able to install this plugin.","Your bot is able to install this plugin.","File: cogs\plugins.py/L586" -"Plugin Registry","Plugin Registry","File: cogs\plugins.py/L641" -" - if not format_.strip(): - continue - if len(format_) + len(formats[-1]) >= 1024: - formats.append(format_) - else: - formats[-1] += format_ - - embeds = [] - for format_ in formats: - description = ( - _(cog.description) or _()"," - if not format_.strip(): - continue - if len(format_) + len(formats[-1]) >= 1024: - formats.append(format_) - else: - formats[-1] += format_ - - embeds = [] - for format_ in formats: - description = ( - _(cog.description) or _()","File: cogs\utility.py/L50" -"Miscellaneous commands without a category.","Miscellaneous commands without a category.","File: cogs\utility.py/L63" -"Commands","Commands","File: cogs\utility.py/L67" -"No commands.","No commands.","File: cogs\utility.py/L67" -"Help","Help","File: cogs\utility.py/L71" -"Miscellaneous Commands","Miscellaneous Commands","File: cogs\utility.py/L73" -"Type ""{prefix}{command} command"" ","Type ""{prefix}{command} command"" ","File: cogs\utility.py/L78/L153" -"NONE","NONE","File: cogs\utility.py/L119" -"","","File: cogs\utility.py/L124" -"Permission Level","Permission Level","File: cogs\utility.py/L139" -" - - embed.add_field(name=_()"," - - embed.add_field(name=_()","File: cogs\utility.py/L153" -"{command} is a snippet.","{command} is a snippet.","File: cogs\utility.py/L168" -"Alias `{command}` is invalid, this alias will now be deleted.This alias will now be deleted.","Alias `{command}` is invalid, this alias will now be deleted.This alias will now be deleted.","File: cogs\utility.py/L181" -"{command} is an alias.","{command} is an alias.","File: cogs\utility.py/L190/L192" -"`{command}` points to:","`{command}` points to:","File: cogs\utility.py/L192" -"**`{command}` points to the following steps:**","**`{command}` points to the following steps:**","File: cogs\utility.py/L197" -"Step","Step","File: cogs\utility.py/L200/L1045" -"Type ""{prefix}{command} alias"" ","Type ""{prefix}{command} alias"" ","File: cogs\utility.py/L203" -"Command/Category ""{command}"" not found.","Command/Category ""{command}"" not found.","File: cogs\utility.py/L211" -"Cannot find command or category","Cannot find command or category","File: cogs\utility.py/L225" -"Type ""{prefix}{command}"" ","Type ""{prefix}{command}"" ","File: cogs\utility.py/L227" -"Shows this help message.","Shows this help message.","File: cogs\utility.py/L242" -"The specified version `{version}` could not be found.","The specified version `{version}` could not be found.","File: cogs\utility.py/L266" -"View the changelog here: {url}","View the changelog here: {url}","File: cogs\utility.py/L282" -"Modmail - About","Modmail - About","File: cogs\utility.py/L292" -"This is an open source Discord bot that serves as a means for members to easily communicate with server administrators in an organised manner.","This is an open source Discord bot that serves as a means for members to easily communicate with server administrators in an organised manner.","File: cogs\utility.py/L298" -"Uptime","Uptime","File: cogs\utility.py/L303" -"Latency","Latency","File: cogs\utility.py/L304" -"Version","Version","File: cogs\utility.py/L305" -"Authors","Authors","File: cogs\utility.py/L306" -"You are on the prerelease version • the latest version is v{version}.","You are on the prerelease version • the latest version is v{version}.","File: cogs\utility.py/L315" -"A newer version is available v{version}.","A newer version is available v{version}.","File: cogs\utility.py/L317" -"You are up to date with the latest version.","You are up to date with the latest version.","File: cogs\utility.py/L319" -"Want Modmail in Your Server?","Want Modmail in Your Server?","File: cogs\utility.py/L322" -"Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) and join our [Discord server](https://discord.gg/F34cRU8/)!","Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) and join our [Discord server](https://discord.gg/F34cRU8/)!","File: cogs\utility.py/L323" -"Support the Developers","Support the Developers","File: cogs\utility.py/L329" -"This bot is completely free for everyone. We rely on kind individuals like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) to keep this bot free forever!","This bot is completely free for everyone. We rely on kind individuals like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) to keep this bot free forever!","File: cogs\utility.py/L330" -"Debug Logs:","Debug Logs:","File: cogs\utility.py/L379" -"You don't have any logs at the moment.","You don't have any logs at the moment.","File: cogs\utility.py/L380" -"Go to Heroku to see your logs.","Go to Heroku to see your logs.","File: cogs\utility.py/L382/L450" -"Debug logs - Navigate using the reactions below.","Debug logs - Navigate using the reactions below.","File: cogs\utility.py/L408" -"Debug Logs","Debug Logs","File: cogs\utility.py/L440" -"Something's wrong. We're unable to upload your logs to hastebin.","Something's wrong. We're unable to upload your logs to hastebin.","File: cogs\utility.py/L448" -"Cached logs are now cleared.","Cached logs are now cleared.","File: cogs\utility.py/L470" -"Activity Removed","Activity Removed","File: cogs\utility.py/L501" -"Activity set to: {name} ","Activity set to: {name} ","File: cogs\utility.py/L520" -"to {name}.","to {name}.","File: cogs\utility.py/L522" -"Activity Changed","Activity Changed","File: cogs\utility.py/L527" -"Status Removed","Status Removed","File: cogs\utility.py/L550" -"Status set to: {value}.","Status set to: {value}.","File: cogs\utility.py/L564" -"Status Changed","Status Changed","File: cogs\utility.py/L566" -"Pong! Websocket Latency:","Pong! Websocket Latency:","File: cogs\utility.py/L638" -"Current mention:","Current mention:","File: cogs\utility.py/L657" -"Changed mention!","Changed mention!","File: cogs\utility.py/L663" -"On thread creation the bot now says ""{mention}"".","On thread creation the bot now says ""{mention}"".","File: cogs\utility.py/L664" -"Current prefix","Current prefix","File: cogs\utility.py/L683" -"Changed prefix!","Changed prefix!","File: cogs\utility.py/L689" -"Set prefix to `{prefix}`","Set prefix to `{prefix}`","File: cogs\utility.py/L690" -"Available configuration keys:","Available configuration keys:","File: cogs\utility.py/L725" -"Set `{key}` to `{value}`.","Set `{key}` to `{value}`.","File: cogs\utility.py/L748" -"{key} is an invalid key.","{key} is an invalid key.","File: cogs\utility.py/L756/L780" -"Valid keys","Valid keys","File: cogs\utility.py/L759/L783" -"`{key}` had been reset to default.","`{key}` had been reset to default.","File: cogs\utility.py/L774" -"`{key}` is set to `{value}`","`{key}` is set to `{value}`","File: cogs\utility.py/L799" -"Config variable","Config variable","File: cogs\utility.py/L802" -"`{key}` is an invalid key.","`{key}` is an invalid key.","File: cogs\utility.py/L809/L822" -"Type ""{prefix}config options"" for a list of config variables.","Type ""{prefix}config options"" for a list of config variables.","File: cogs\utility.py/L812" -"Here is a list of currently set configuration variable(s).","Here is a list of currently set configuration variable(s).","File: cogs\utility.py/L818" -"Current config(s):","Current config(s):","File: cogs\utility.py/L822" -"No help details found for `{key}`.","No help details found for `{key}`.","File: cogs\utility.py/L862" -"Configuration description on {current_key}:","Configuration description on {current_key}:","File: cogs\utility.py/L875" -"Information:","Information:","File: cogs\utility.py/L880" -"Example(s):","Example(s):","File: cogs\utility.py/L886" -"Note(s):","Note(s):","File: cogs\utility.py/L892" -"Alias `{name}` is invalid, it used to be `{value}`. This alias will now be deleted.","Alias `{name}` is invalid, it used to be `{value}`. This alias will now be deleted.","File: cogs\utility.py/L939" -"Alias","Alias","File: cogs\utility.py/L949" -"You dont have any aliases at the moment.","You dont have any aliases at the moment.","File: cogs\utility.py/L968" -"Do {prefix}help alias for more commands.","Do {prefix}help alias for more commands.","File: cogs\utility.py/L970" -"Aliases","Aliases","File: cogs\utility.py/L971" -"Command Aliases","Command Aliases","File: cogs\utility.py/L979" -"Invalid multi-step alias, try wrapping each steps in quotes.","Invalid multi-step alias, try wrapping each steps in quotes.","File: cogs\utility.py/L1009" -"{action} alias","{action} alias","File: cogs\utility.py/L1024" -"`{name}` points to:","`{name}` points to:","File: cogs\utility.py/L1027" -"`{name}` now points to the following steps:","`{name}` now points to the following steps:","File: cogs\utility.py/L1029" -"The command you are attempting to point to does not exist: `{command}`.","The command you are attempting to point to does not exist: `{command}`.","File: cogs\utility.py/L1045" -"The command you are attempting to point to on step {number} does not exist: `{command}`.","The command you are attempting to point to on step {number} does not exist: `{command}`.","File: cogs\utility.py/L1045" -"A command with the same name already exists: `{name}`.","A command with the same name already exists: `{name}`.","File: cogs\utility.py/L1085" -"Another alias with the same name already exists: `{name}`.","Another alias with the same name already exists: `{name}`.","File: cogs\utility.py/L1092" -"A snippet with the same name already exists: `{name}`.","A snippet with the same name already exists: `{name}`.","File: cogs\utility.py/L1099" -"Alias names cannot be longer than 120 characters.","Alias names cannot be longer than 120 characters.","File: cogs\utility.py/L1106" -"Removed alias","Removed alias","File: cogs\utility.py/L1123" -"Successfully deleted `{name}`.","Successfully deleted `{name}`.","File: cogs\utility.py/L1125" -"The referenced level does not exist: `{level}`.","The referenced level does not exist: `{level}`.","File: cogs\utility.py/L1231" -"Successfully set command permission level for `{command}` to `{level}`.","Successfully set command permission level for `{command}` to `{level}`.","File: cogs\utility.py/L1245" -"The referenced {type} does not exist: `{name}`.","The referenced {type} does not exist: `{name}`.","File: cogs\utility.py/L1291" -"Permission for `{name}` was successfully updated.","Permission for `{name}` was successfully updated.","File: cogs\utility.py/L1316/L1419" -"The command permission level was never overridden: `{name}`, current permission level is {perm_name}.","The command permission level was never overridden: `{name}`, current permission level is {perm_name}.","File: cogs\utility.py/L1367/L1559" -"Command permission level for `{name}` was successfully restored to {perm_name}.","Command permission level for `{name}` was successfully restored to {perm_name}.","File: cogs\utility.py/L1378" -"The referenced level does not exist: `{name}`.","The referenced level does not exist: `{name}`.","File: cogs\utility.py/L1392" -"Permission entries for {type} `{name}`:","Permission entries for {type} `{name}`:","File: cogs\utility.py/L1430/L1455" -"No permission entries found.","No permission entries found.","File: cogs\utility.py/L1431/L1523" -"{mention} has permission with the following commands:","{mention} has permission with the following commands:","File: cogs\utility.py/L1528" -"{mention} has permission with the following permission levels:","{mention} has permission with the following permission levels:","File: cogs\utility.py/L1533" -"Permission Overrides","Permission Overrides","File: cogs\utility.py/L1558/L1559" -"You don't have any command level overrides at the moment.","You don't have any command level overrides at the moment.","File: cogs\utility.py/L1559" -"Permission override for command ""{name}"" is ""{perm_name}"".","Permission override for command ""{name}"" is ""{perm_name}"".","File: cogs\utility.py/L1595" -"Un-whitelisted {target_mention} to view logs.","Un-whitelisted {target_mention} to view logs.","File: cogs\utility.py/L1678" -"Whitelisted {target_mention} to view logs.","Whitelisted {target_mention} to view logs.","File: cogs\utility.py/L1682" -"Oauth Whitelist","Oauth Whitelist","File: cogs\utility.py/L1705" -"Users","Users","File: cogs\utility.py/L1708" -"None","None","File: cogs\utility.py/L1708" -"Channel has been deleted, no closer found.","Channel has been deleted, no closer found.","File: bot.py/L472" -"System Message: New Account. Required to wait for {time}.","System Message: New Account. Required to wait for {time}.","File: bot.py/L552" -"Commands directly related to Modmail functionality.","Commands directly related to Modmail functionality.","Cog: Modmail" -"Sets up a server for Modmail.","Sets up a server for Modmail.","Cog: Modmail -Command: setup" -"Sets up a server for Modmail. - -You only need to run this command -once after configuring Modmail.","Sets up a server for Modmail. - -You only need to run this command -once after configuring Modmail.","Cog: Modmail -Command: setup" -"Create pre-defined messages for use in threads.","Create pre-defined messages for use in threads.","Cog: Modmail -Command: snippet" -"Create pre-defined messages for use in threads. - -When `{prefix}snippet` is used by itself, this will retrieve -a list of snippets that are currently set. `{prefix}snippet-name` will show what the -snippet point to. - -To create a snippet: -- `{prefix}snippet add snippet-name A pre-defined text.` - -You can use your snippet in a thread channel -with `{prefix}snippet-name`, the message ""A pre-defined text."" -will be sent to the recipient. - -Currently, there is not a built-in anonymous snippet command; however, a workaround -is available using `{prefix}alias`. Here is how: -- `{prefix}alias add snippet-name anonreply A pre-defined anonymous text.` - -See also `{prefix}alias`.","Create pre-defined messages for use in threads. - -When `{prefix}snippet` is used by itself, this will retrieve -a list of snippets that are currently set. `{prefix}snippet-name` will show what the -snippet point to. - -To create a snippet: -- `{prefix}snippet add snippet-name A pre-defined text.` - -You can use your snippet in a thread channel -with `{prefix}snippet-name`, the message ""A pre-defined text."" -will be sent to the recipient. - -Currently, there is not a built-in anonymous snippet command; however, a workaround -is available using `{prefix}alias`. Here is how: -- `{prefix}alias add snippet-name anonreply A pre-defined anonymous text.` - -See also `{prefix}alias`.","Cog: Modmail -Command: snippet" -"View the raw content of a snippet.","View the raw content of a snippet.","Cog: Modmail -Command: snippet raw" -"View the raw content of a snippet.","View the raw content of a snippet.","Cog: Modmail -Command: snippet raw" -"Add a snippet.","Add a snippet.","Cog: Modmail -Command: snippet add" -"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. -```","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. -```","Cog: Modmail -Command: snippet add" -"Remove a snippet.","Remove a snippet.","Cog: Modmail -Command: snippet remove" -"Remove a snippet.","Remove a snippet.","Cog: Modmail -Command: snippet remove" -"Edit a snippet.","Edit a snippet.","Cog: Modmail -Command: snippet edit" -"Edit a snippet. - -To edit a multi-word snippet name, use quotes: ``` -{prefix}snippet edit ""two word"" this is a new two word snippet. -```","Edit a snippet. - -To edit a multi-word snippet name, use quotes: ``` -{prefix}snippet edit ""two word"" this is a new two word snippet. -```","Cog: Modmail -Command: snippet edit" -"Move a thread to another category.","Move a thread to another category.","Cog: Modmail -Command: move" -"Move a thread to another category. - -`category` may be a category ID, mention, or name. -`specifics` is a string which takes in arguments on how to perform the move. Ex: ""silently""","Move a thread to another category. - -`category` may be a category ID, mention, or name. -`specifics` is a string which takes in arguments on how to perform the move. Ex: ""silently""","Cog: Modmail -Command: move" -"Close the current thread.","Close the current thread.","Cog: Modmail -Command: close" -"Close the current thread. - -Close after a period of time: -- `{prefix}close in 5 hours` -- `{prefix}close 2m30s` - -Custom close messages: -- `{prefix}close 2 hours The issue has been resolved.` -- `{prefix}close We will contact you once we find out more.` - -Silently close a thread (no message) -- `{prefix}close silently` -- `{prefix}close in 10m silently` - -Stop a thread from closing: -- `{prefix}close cancel`","Close the current thread. - -Close after a period of time: -- `{prefix}close in 5 hours` -- `{prefix}close 2m30s` - -Custom close messages: -- `{prefix}close 2 hours The issue has been resolved.` -- `{prefix}close We will contact you once we find out more.` - -Silently close a thread (no message) -- `{prefix}close silently` -- `{prefix}close in 10m silently` - -Stop a thread from closing: -- `{prefix}close cancel`","Cog: Modmail -Command: close" -"Notify a user or role when the next thread message received.","Notify a user or role when the next thread message received.","Cog: Modmail -Command: notify" -"Notify a user or role when the next thread message received. - -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`. -`user_or_role` may be a user ID, mention, name. role ID, mention, name, ""everyone"", or ""here"".","Notify a user or role when the next thread message received. - -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`. -`user_or_role` may be a user ID, mention, name. role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail -Command: notify" -"Un-notify a user, role, or yourself from a thread.","Un-notify a user, role, or yourself from a thread.","Cog: Modmail -Command: unnotify" -"Un-notify a user, role, or yourself from a thread. - -Leave `user_or_role` empty to un-notify yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Un-notify a user, role, or yourself from a thread. - -Leave `user_or_role` empty to un-notify yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail -Command: unnotify" -"Notify a user, role, or yourself for every thread message received.","Notify a user, role, or yourself for every thread message received.","Cog: Modmail -Command: subscribe" -"Notify a user, role, or yourself for every thread message received. - -You will be pinged for every thread message received until you unsubscribe. - -Leave `user_or_role` empty to subscribe yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Notify a user, role, or yourself for every thread message received. - -You will be pinged for every thread message received until you unsubscribe. - -Leave `user_or_role` empty to subscribe yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail -Command: subscribe" -"Unsubscribe a user, role, or yourself from a thread.","Unsubscribe a user, role, or yourself from a thread.","Cog: Modmail -Command: unsubscribe" -"Unsubscribe a user, role, or yourself from a thread. - -Leave `user_or_role` empty to unsubscribe yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Unsubscribe a user, role, or yourself from a thread. - -Leave `user_or_role` empty to unsubscribe yourself. -`@here` and `@everyone` can be substituted with `here` and `everyone`. -`user_or_role` may be a user ID, mention, name, role ID, mention, name, ""everyone"", or ""here"".","Cog: Modmail -Command: unsubscribe" -"Flags a Modmail thread as NSFW (not safe for work).","Flags a Modmail thread as NSFW (not safe for work).","Cog: Modmail -Command: nsfw" -"Flags a Modmail thread as NSFW (not safe for work).","Flags a Modmail thread as NSFW (not safe for work).","Cog: Modmail -Command: nsfw" -"Flags a Modmail thread as SFW (safe for work).","Flags a Modmail thread as SFW (safe for work).","Cog: Modmail -Command: sfw" -"Flags a Modmail thread as SFW (safe for work).","Flags a Modmail thread as SFW (safe for work).","Cog: Modmail -Command: sfw" -"Retrieves the link to the current thread's logs.","Retrieves the link to the current thread's logs.","Cog: Modmail -Command: loglink" -"Retrieves the link to the current thread's logs.","Retrieves the link to the current thread's logs.","Cog: Modmail -Command: loglink" -"Get previous Modmail thread logs of a member.","Get previous Modmail thread logs of a member.","Cog: Modmail -Command: logs" -"Get previous Modmail thread logs of a member. - -Leave `user` blank when this command is used within a -thread channel to show logs for the current recipient. -`user` may be a user ID, mention, or name.","Get previous Modmail thread logs of a member. - -Leave `user` blank when this command is used within a -thread channel to show logs for the current recipient. -`user` may be a user ID, mention, or name.","Cog: Modmail -Command: logs" -"Get all logs closed by the specified user.","Get all logs closed by the specified user.","Cog: Modmail -Command: logs closed-by" -"Get all logs closed by the specified user. - -If no `user` is provided, the user will be the person who sent this command. -`user` may be a user ID, mention, or name.","Get all logs closed by the specified user. - -If no `user` is provided, the user will be the person who sent this command. -`user` may be a user ID, mention, or name.","Cog: Modmail -Command: logs closed-by" -"Wipe a log entry from the database.","Wipe a log entry from the database.","Cog: Modmail -Command: logs delete" -"Wipe a log entry from the database.","Wipe a log entry from the database.","Cog: Modmail -Command: logs delete" -"Get all logs where the specified user has responded at least once.","Get all logs where the specified user has responded at least once.","Cog: Modmail -Command: logs responded" -"Get all logs where the specified user has responded at least once. - -If no `user` is provided, the user will be the person who sent this command. -`user` may be a user ID, mention, or name.","Get all logs where the specified user has responded at least once. - -If no `user` is provided, the user will be the person who sent this command. -`user` may be a user ID, mention, or name.","Cog: Modmail -Command: logs responded" -"Retrieve all logs that contain messages with your query.","Retrieve all logs that contain messages with your query.","Cog: Modmail -Command: logs search" -"Retrieve all logs that contain messages with your query. - -Provide a `limit` to specify the maximum number of logs the bot should find.","Retrieve all logs that contain messages with your query. - -Provide a `limit` to specify the maximum number of logs the bot should find.","Cog: Modmail -Command: logs search" -"Reply to a Modmail thread.","Reply to a Modmail thread.","Cog: Modmail -Command: reply" -"Reply to a Modmail thread. - -Supports attachments and images as well as -automatically embedding image URLs.","Reply to a Modmail thread. - -Supports attachments and images as well as -automatically embedding image URLs.","Cog: Modmail -Command: reply" -"Reply to a Modmail thread with variables.","Reply to a Modmail thread with variables.","Cog: Modmail -Command: freply" -"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.","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.","Cog: Modmail -Command: freply" -"Reply to a thread anonymously.","Reply to a thread anonymously.","Cog: Modmail -Command: areply" -"Reply to a thread anonymously. - -You can edit the anonymous user's name, -avatar and tag using the config command. - -Edit the `anon_username`, `anon_avatar_url` -and `anon_tag` config variables to do so.","Reply to a thread anonymously. - -You can edit the anonymous user's name, -avatar and tag using the config command. - -Edit the `anon_username`, `anon_avatar_url` -and `anon_tag` config variables to do so.","Cog: Modmail -Command: areply" -"Take a note about the current thread.","Take a note about the current thread.","Cog: Modmail -Command: note" -"Take a note about the current thread. - -Useful for noting context.","Take a note about the current thread. - -Useful for noting context.","Cog: Modmail -Command: note" -"Edit a message that was sent using the reply or anonreply command.","Edit a message that was sent using the reply or anonreply command.","Cog: Modmail -Command: edit" -"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. - -Note: attachments **cannot** be edited.","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. - -Note: attachments **cannot** be edited.","Cog: Modmail -Command: edit" -"Create a thread with a specified member.","Create a thread with a specified member.","Cog: Modmail -Command: contact" -"Create a thread with a specified member. - -If `category` is specified, the thread -will be created in that specified category. - -`category`, if specified, may be a category ID, mention, or name. -`user` may be a user ID, mention, or name.","Create a thread with a specified member. - -If `category` is specified, the thread -will be created in that specified category. - -`category`, if specified, may be a category ID, mention, or name. -`user` may be a user ID, mention, or name.","Cog: Modmail -Command: contact" -"Retrieve a list of blocked users.","Retrieve a list of blocked users.","Cog: Modmail -Command: blocked" -"Retrieve a list of blocked users.","Retrieve a list of blocked users.","Cog: Modmail -Command: blocked" -"Whitelist or un-whitelist a user from getting blocked.","Whitelist or un-whitelist a user from getting blocked.","Cog: Modmail -Command: blocked whitelist" -"Whitelist or un-whitelist a user from getting blocked. - -Useful for preventing users from getting blocked by account_age/guild_age restrictions.","Whitelist or un-whitelist a user from getting blocked. - -Useful for preventing users from getting blocked by account_age/guild_age restrictions.","Cog: Modmail -Command: blocked whitelist" -"Block a user from using Modmail.","Block a user from using Modmail.","Cog: Modmail -Command: block" -"Block a user from using Modmail. - -You may choose to set a time as to when the user will automatically be unblocked. - -Leave `user` blank when this command is used within a -thread channel to block the current recipient. -`user` may be a user ID, mention, or name. -`duration` may be a simple ""human-readable"" time text. See `{prefix}help close` for examples.","Block a user from using Modmail. - -You may choose to set a time as to when the user will automatically be unblocked. - -Leave `user` blank when this command is used within a -thread channel to block the current recipient. -`user` may be a user ID, mention, or name. -`duration` may be a simple ""human-readable"" time text. See `{prefix}help close` for examples.","Cog: Modmail -Command: block" -"Unblock a user from using Modmail.","Unblock a user from using Modmail.","Cog: Modmail -Command: unblock" -"Unblock a user from using Modmail. - -Leave `user` blank when this command is used within a -thread channel to unblock the current recipient. -`user` may be a user ID, mention, or name.","Unblock a user from using Modmail. - -Leave `user` blank when this command is used within a -thread channel to unblock the current recipient. -`user` may be a user ID, mention, or name.","Cog: Modmail -Command: unblock" -"Delete a message that was sent using the reply command or a note.","Delete a message that was sent using the reply command or a note.","Cog: Modmail -Command: delete" -"Delete a message that was sent using the reply command or a note. - -Deletes the previous message, unless a message ID is provided, -which in that case, deletes the message with that message ID. - -Notes can only be deleted when a note ID is provided.","Delete a message that was sent using the reply command or a note. - -Deletes the previous message, unless a message ID is provided, -which in that case, deletes the message with that message ID. - -Notes can only be deleted when a note ID is provided.","Cog: Modmail -Command: delete" -"Repair a thread broken by Discord.","Repair a thread broken by Discord.","Cog: Modmail -Command: repair" -"Repair a thread broken by Discord.","Repair a thread broken by Discord.","Cog: Modmail -Command: repair" -"Re-enables DM functionalities of Modmail.","Re-enables DM functionalities of Modmail.","Cog: Modmail -Command: enable" -"Re-enables DM functionalities of Modmail. - -Undo's the `{prefix}disable` command, all DM will be relayed after running this command.","Re-enables DM functionalities of Modmail. - -Undo's the `{prefix}disable` command, all DM will be relayed after running this command.","Cog: Modmail -Command: enable" -"Disable partial or full Modmail thread functions.","Disable partial or full Modmail thread functions.","Cog: Modmail -Command: disable" -"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`.","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`.","Cog: Modmail -Command: disable" -"Stop accepting new Modmail threads.","Stop accepting new Modmail threads.","Cog: Modmail -Command: disable new" -"Stop accepting new Modmail threads. - -No new threads can be created through DM.","Stop accepting new Modmail threads. - -No new threads can be created through DM.","Cog: Modmail -Command: disable new" -"Disables all DM functionalities of Modmail.","Disables all DM functionalities of Modmail.","Cog: Modmail -Command: disable all" -"Disables all DM functionalities of Modmail. - -No new threads can be created through DM nor no further DM messages will be relayed.","Disables all DM functionalities of Modmail. - -No new threads can be created through DM nor no further DM messages will be relayed.","Cog: Modmail -Command: disable all" -"Check if the DM functionalities of Modmail is enabled.","Check if the DM functionalities of Modmail is enabled.","Cog: Modmail -Command: isenable" -"Check if the DM functionalities of Modmail is enabled.","Check if the DM functionalities of Modmail is enabled.","Cog: Modmail -Command: isenable" -"Plugins expand Modmail functionality by allowing third-party addons. - -These addons could have a range of features from moderation to simply -making your life as a moderator easier! -Learn how to create a plugin yourself here: -https://github.com/kyb3r/modmail/wiki/Plugins","Plugins expand Modmail functionality by allowing third-party addons. - -These addons could have a range of features from moderation to simply -making your life as a moderator easier! -Learn how to create a plugin yourself here: -https://github.com/kyb3r/modmail/wiki/Plugins","Cog: Plugins" -"Manage plugins for Modmail.","Manage plugins for Modmail.","Cog: Plugins -Command: plugins" -"Manage plugins for Modmail.","Manage plugins for Modmail.","Cog: Plugins -Command: plugins" -"Install a new plugin for the bot.","Install a new plugin for the bot.","Cog: Plugins -Command: plugins add" -"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]`).","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]`).","Cog: Plugins -Command: plugins add" -"Remove an installed plugin of the bot.","Remove an installed plugin of the bot.","Cog: Plugins -Command: plugins remove" -"Remove an installed plugin of 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]`).","Remove an installed plugin of 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]`).","Cog: Plugins -Command: plugins remove" -"Update a plugin for the bot.","Update a plugin for the bot.","Cog: Plugins -Command: plugins update" -"Update a plugin for the bot. - -`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference -to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). - -To update all plugins, do `{prefix}plugins update`.","Update a plugin for the bot. - -`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference -to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). - -To update all plugins, do `{prefix}plugins update`.","Cog: Plugins -Command: plugins update" -"Show a list of currently loaded plugins.","Show a list of currently loaded plugins.","Cog: Plugins -Command: plugins loaded" -"Show a list of currently loaded plugins.","Show a list of currently loaded plugins.","Cog: Plugins -Command: plugins loaded" -"Shows a list of all approved plugins.","Shows a list of all approved plugins.","Cog: Plugins -Command: plugins registry" -"Shows a list of all approved plugins. - -Usage: -`{prefix}plugin registry` Details about all plugins. -`{prefix}plugin registry plugin-name` Details about the indicated plugin. -`{prefix}plugin registry page-number` Jump to a page in the registry.","Shows a list of all approved plugins. - -Usage: -`{prefix}plugin registry` Details about all plugins. -`{prefix}plugin registry plugin-name` Details about the indicated plugin. -`{prefix}plugin registry page-number` Jump to a page in the registry.","Cog: Plugins -Command: plugins registry" -"Shows a compact view of all plugins within the registry.","Shows a compact view of all plugins within the registry.","Cog: Plugins -Command: plugins registry compact" -"Shows a compact view of all plugins within the registry.","Shows a compact view of all plugins within the registry.","Cog: Plugins -Command: plugins registry compact" -"General commands that provide utility.","General commands that provide utility.","Cog: Utility" -"Shows the changelog of the Modmail.","Shows the changelog of the Modmail.","Cog: Utility -Command: changelog" -"Shows the changelog of the Modmail.","Shows the changelog of the Modmail.","Cog: Utility -Command: changelog" -"Shows information about this bot.","Shows information about this bot.","Cog: Utility -Command: about" -"Shows information about this bot.","Shows information about this bot.","Cog: Utility -Command: about" -"Shows a list of sponsors.","Shows a list of sponsors.","Cog: Utility -Command: sponsors" -"Shows a list of sponsors.","Shows a list of sponsors.","Cog: Utility -Command: sponsors" -"Shows the recent application logs of the bot.","Shows the recent application logs of the bot.","Cog: Utility -Command: debug" -"Shows the recent application logs of the bot.","Shows the recent application logs of the bot.","Cog: Utility -Command: debug" -"Posts application-logs to Hastebin.","Posts application-logs to Hastebin.","Cog: Utility -Command: debug hastebin" -"Posts application-logs to Hastebin.","Posts application-logs to Hastebin.","Cog: Utility -Command: debug hastebin" -"Clears the locally cached logs.","Clears the locally cached logs.","Cog: Utility -Command: debug clear" -"Clears the locally cached logs.","Clears the locally cached logs.","Cog: Utility -Command: debug clear" -"Set an activity status for the bot.","Set an activity status for the bot.","Cog: Utility -Command: activity" -"Set an activity status for the bot. - -Possible activity types: - - `playing` - - `streaming` - - `listening` - - `watching` - -When activity type is set to `listening`, -it must be followed by a ""to"": ""listening to..."" - -When activity type is set to `streaming`, you can set -the linked twitch page: -- `{prefix}config set twitch_url https://www.twitch.tv/somechannel/` - -To remove the current activity status: -- `{prefix}activity clear`","Set an activity status for the bot. - -Possible activity types: - - `playing` - - `streaming` - - `listening` - - `watching` - -When activity type is set to `listening`, -it must be followed by a ""to"": ""listening to..."" - -When activity type is set to `streaming`, you can set -the linked twitch page: -- `{prefix}config set twitch_url https://www.twitch.tv/somechannel/` - -To remove the current activity status: -- `{prefix}activity clear`","Cog: Utility -Command: activity" -"Set a status for the bot.","Set a status for the bot.","Cog: Utility -Command: status" -"Set a status for the bot. - -Possible status types: - - `online` - - `idle` - - `dnd` or `do not disturb` - - `invisible` or `offline` - -To remove the current status: -- `{prefix}status clear`","Set a status for the bot. - -Possible status types: - - `online` - - `idle` - - `dnd` or `do not disturb` - - `invisible` or `offline` - -To remove the current status: -- `{prefix}status clear`","Cog: Utility -Command: status" -"Pong! Returns your websocket latency.","Pong! Returns your websocket latency.","Cog: Utility -Command: ping" -"Pong! Returns your websocket latency.","Pong! Returns your websocket latency.","Cog: Utility -Command: ping" -"Change what the bot mentions at the start of each thread.","Change what the bot mentions at the start of each thread.","Cog: Utility -Command: mention" -"Change what the bot mentions at the start of each thread. - -Type only `{prefix}mention` to retrieve your current ""mention"" message.","Change what the bot mentions at the start of each thread. - -Type only `{prefix}mention` to retrieve your current ""mention"" message.","Cog: Utility -Command: mention" -"Change the prefix of the bot.","Change the prefix of the bot.","Cog: Utility -Command: prefix" -"Change the prefix of the bot. - -Type only `{prefix}prefix` to retrieve your current bot prefix.","Change the prefix of the bot. - -Type only `{prefix}prefix` to retrieve your current bot prefix.","Cog: Utility -Command: prefix" -"Modify changeable configuration variables for this bot.","Modify changeable configuration variables for this bot.","Cog: Utility -Command: config" -"Modify changeable configuration variables for this bot. - -Type `{prefix}config options` to view a list -of valid configuration variables. - -Type `{prefix}config help config-name` for info - on a config. - -To set a configuration variable: -- `{prefix}config set config-name value here` - -To remove a configuration variable: -- `{prefix}config remove config-name`","Modify changeable configuration variables for this bot. - -Type `{prefix}config options` to view a list -of valid configuration variables. - -Type `{prefix}config help config-name` for info - on a config. - -To set a configuration variable: -- `{prefix}config set config-name value here` - -To remove a configuration variable: -- `{prefix}config remove config-name`","Cog: Utility -Command: config" -"Return a list of valid configuration names you can change.","Return a list of valid configuration names you can change.","Cog: Utility -Command: config options" -"Return a list of valid configuration names you can change.","Return a list of valid configuration names you can change.","Cog: Utility -Command: config options" -"Set a configuration variable and its value.","Set a configuration variable and its value.","Cog: Utility -Command: config set" -"Set a configuration variable and its value.","Set a configuration variable and its value.","Cog: Utility -Command: config set" -"Delete a set configuration variable.","Delete a set configuration variable.","Cog: Utility -Command: config remove" -"Delete a set configuration variable.","Delete a set configuration variable.","Cog: Utility -Command: config remove" -"Show the configuration variables that are currently set.","Show the configuration variables that are currently set.","Cog: Utility -Command: config get" -"Show the configuration variables that are currently set. - -Leave `key` empty to show all currently set configuration variables.","Show the configuration variables that are currently set. - -Leave `key` empty to show all currently set configuration variables.","Cog: Utility -Command: config get" -"Show information on a specified configuration.","Show information on a specified configuration.","Cog: Utility -Command: config help" -"Show information on a specified configuration.","Show information on a specified configuration.","Cog: Utility -Command: config help" -"Create shortcuts to bot commands.","Create shortcuts to bot commands.","Cog: Utility -Command: alias" -"Create shortcuts to bot commands. - -When `{prefix}alias` is used by itself, this will retrieve -a list of alias that are currently set. `{prefix}alias-name` will show what the -alias point to. - -To use alias: - -First create an alias using: -- `{prefix}alias add alias-name other-command` - -For example: -- `{prefix}alias add r reply` -- Now you can use `{prefix}r` as an replacement for `{prefix}reply`. - -See also `{prefix}snippet`.","Create shortcuts to bot commands. - -When `{prefix}alias` is used by itself, this will retrieve -a list of alias that are currently set. `{prefix}alias-name` will show what the -alias point to. - -To use alias: - -First create an alias using: -- `{prefix}alias add alias-name other-command` - -For example: -- `{prefix}alias add r reply` -- Now you can use `{prefix}r` as an replacement for `{prefix}reply`. - -See also `{prefix}snippet`.","Cog: Utility -Command: alias" -"View the raw content of an alias.","View the raw content of an alias.","Cog: Utility -Command: alias raw" -"View the raw content of an alias.","View the raw content of an alias.","Cog: Utility -Command: alias raw" -"Add an alias.","Add an alias.","Cog: Utility -Command: alias add" -"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""`","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""`","Cog: Utility -Command: alias add" -"Remove an alias.","Remove an alias.","Cog: Utility -Command: alias remove" -"Remove an alias.","Remove an alias.","Cog: Utility -Command: alias remove" -"Edit an alias.","Edit an alias.","Cog: Utility -Command: alias edit" -"Edit an alias.","Edit an alias.","Cog: Utility -Command: alias edit" -"Set the permissions for Modmail commands.","Set the permissions for Modmail commands.","Cog: Utility -Command: permissions" -"Set the permissions for Modmail commands. - -You may set permissions based on individual command names, or permission -levels. - -Acceptable permission levels are: - - **Owner** [5] (absolute control over the bot) - - **Administrator** [4] (administrative powers such as setting activities) - - **Moderator** [3] (ability to block) - - **Supporter** [2] (access to core Modmail supporting functions) - - **Regular** [1] (most basic interactions such as help and about) - -By default, owner is set to the absolute bot owner and regular is `@everyone`. - -To set permissions, see `{prefix}help permissions add`; and to change permission level for specific -commands see `{prefix}help permissions override`. - -Note: You will still have to manually give/take permission to the Modmail -category to users/roles.","Set the permissions for Modmail commands. - -You may set permissions based on individual command names, or permission -levels. - -Acceptable permission levels are: - - **Owner** [5] (absolute control over the bot) - - **Administrator** [4] (administrative powers such as setting activities) - - **Moderator** [3] (ability to block) - - **Supporter** [2] (access to core Modmail supporting functions) - - **Regular** [1] (most basic interactions such as help and about) - -By default, owner is set to the absolute bot owner and regular is `@everyone`. - -To set permissions, see `{prefix}help permissions add`; and to change permission level for specific -commands see `{prefix}help permissions override`. - -Note: You will still have to manually give/take permission to the Modmail -category to users/roles.","Cog: Utility -Command: permissions" -"Change a permission level for a specific command.","Change a permission level for a specific command.","Cog: Utility -Command: permissions override" -"Change a permission level for a specific command. - -Examples: -- `{prefix}perms override reply administrator` -- `{prefix}perms override ""plugin enabled"" moderator` - -To undo a permission override, see `{prefix}help permissions remove`. - -Example: -- `{prefix}perms remove override reply` -- `{prefix}perms remove override plugin enabled` - -You can retrieve a single or all command level override(s), see`{prefix}help permissions get`.","Change a permission level for a specific command. - -Examples: -- `{prefix}perms override reply administrator` -- `{prefix}perms override ""plugin enabled"" moderator` - -To undo a permission override, see `{prefix}help permissions remove`. - -Example: -- `{prefix}perms remove override reply` -- `{prefix}perms remove override plugin enabled` - -You can retrieve a single or all command level override(s), see`{prefix}help permissions get`.","Cog: Utility -Command: permissions override" -"Add a permission to a command or a permission level.","Add a permission to a command or a permission level.","Cog: Utility -Command: permissions add" -"Add a permission to a command or a permission level. - -For sub commands, wrap the complete command name with quotes. -To find a list of permission levels, see `{prefix}help perms`. - -Examples: -- `{prefix}perms add level REGULAR everyone` -- `{prefix}perms add command reply @user` -- `{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.","Add a permission to a command or a permission level. - -For sub commands, wrap the complete command name with quotes. -To find a list of permission levels, see `{prefix}help perms`. - -Examples: -- `{prefix}perms add level REGULAR everyone` -- `{prefix}perms add command reply @user` -- `{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.","Cog: Utility -Command: permissions add" -"Remove permission to use a command, permission level, or command level override.","Remove permission to use a command, permission level, or command level override.","Cog: Utility -Command: permissions remove" -"Remove permission to use a command, permission level, or command level override. - -For sub commands, wrap the complete command name with quotes. -To find a list of permission levels, see `{prefix}help perms`. - -Examples: -- `{prefix}perms remove level REGULAR everyone` -- `{prefix}perms remove command reply @user` -- `{prefix}perms remove command ""plugin enabled"" @role` -- `{prefix}perms remove command help 984301093849028` -- `{prefix}perms remove override block` -- `{prefix}perms remove override ""snippet add""` - -Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Remove permission to use a command, permission level, or command level override. - -For sub commands, wrap the complete command name with quotes. -To find a list of permission levels, see `{prefix}help perms`. - -Examples: -- `{prefix}perms remove level REGULAR everyone` -- `{prefix}perms remove command reply @user` -- `{prefix}perms remove command ""plugin enabled"" @role` -- `{prefix}perms remove command help 984301093849028` -- `{prefix}perms remove override block` -- `{prefix}perms remove override ""snippet add""` - -Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Cog: Utility -Command: permissions remove" -"View the currently-set permissions.","View the currently-set permissions.","Cog: Utility -Command: permissions get" -"View the currently-set permissions. - -To find a list of permission levels, see `{prefix}help perms`. - -To view all command and level permissions: - -Examples: -- `{prefix}perms get @user` -- `{prefix}perms get 984301093849028` - -To view all users and roles of a command or level permission: - -Examples: -- `{prefix}perms get command reply` -- `{prefix}perms get command plugin remove` -- `{prefix}perms get level SUPPORTER` - -To view command level overrides: - -Examples: -- `{prefix}perms get override block` -- `{prefix}perms get override permissions add` - -Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","View the currently-set permissions. - -To find a list of permission levels, see `{prefix}help perms`. - -To view all command and level permissions: - -Examples: -- `{prefix}perms get @user` -- `{prefix}perms get 984301093849028` - -To view all users and roles of a command or level permission: - -Examples: -- `{prefix}perms get command reply` -- `{prefix}perms get command plugin remove` -- `{prefix}perms get level SUPPORTER` - -To view command level overrides: - -Examples: -- `{prefix}perms get override block` -- `{prefix}perms get override permissions add` - -Do not ping `@everyone` for granting permission to everyone, use ""everyone"" or ""all"" instead.","Cog: Utility -Command: permissions get" -"Commands relating to logviewer oauth2 login authentication.","Commands relating to logviewer oauth2 login authentication.","Cog: Utility -Command: oauth" -"Commands relating to logviewer oauth2 login authentication. - -This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature.","Commands relating to logviewer oauth2 login authentication. - -This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature.","Cog: Utility -Command: oauth" -"Whitelist or un-whitelist a user or role to have access to logs.","Whitelist or un-whitelist a user or role to have access to logs.","Cog: Utility -Command: oauth whitelist" -"Whitelist or un-whitelist a user or role to have access to logs. - -`target` may be a role ID, name, mention, user ID, name, or mention.","Whitelist or un-whitelist a user or role to have access to logs. - -`target` may be a role ID, name, mention, user ID, name, or mention.","Cog: Utility -Command: oauth whitelist" -"Shows a list of users and roles that are whitelisted to view logs.","Shows a list of users and roles that are whitelisted to view logs.","Cog: Utility -Command: oauth show" -"Shows a list of users and roles that are whitelisted to view logs.","Shows a list of users and roles that are whitelisted to view logs.","Cog: Utility -Command: oauth show" -"Evaluates Python code.","Evaluates Python code.","Cog: Utility -Command: eval" -"Evaluates Python code.","Evaluates Python code.","Cog: Utility -Command: eval" -"Shows this help message.","Shows this help message.","Cog: Utility -Command: help" -"Shows this help message.","Shows this help message.","Cog: Utility -Command: help" diff --git a/modmail.sh b/modmail.sh new file mode 100644 index 0000000000..4f29170f0a --- /dev/null +++ b/modmail.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +pipenv run python3 bot.py \ No newline at end of file diff --git a/plugins/registry.json b/plugins/registry.json index 1c268d89be..5c343ebcff 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -8,6 +8,15 @@ "icon_url": "https://i.imgur.com/951szZ3.jpg", "thumbnail_url": "https://i.imgur.com/951szZ3.jpg" }, + "music": { + "repository": "lorenzo132/modmail-plugins", + "branch": "master", + "description": "Play your favourite music on your bot! Note: Only works on VPS follow this guide: https://gist.github.com/lorenzo132/5ef328e5dfcfaec19cb81dc7a63eaffa", + "bot_version": "2.20.1", + "title": "Music", + "icon_url": "https://i.imgur.com/R2olclk.png", + "thumbnail_url": "https://i.imgur.com/xuoQjPu.gif" + }, "dragory-migrate": { "repository": "kyb3r/modmail-plugins", "branch": "master", @@ -17,58 +26,22 @@ "icon_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png", "thumbnail_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png" }, - "autorole": { - "repository": "papiersnipper/modmail-plugins", + "media-only": { + "repository": "lorenzo132/modmail-plugins", "branch": "master", - "description": "Easily auto-assign a role to a user when they join your server.", + "description": "Make a channel mediaonly, only the following mediatypes will be accepted `.png` / `.gif` / `.jpg` / `.mp4`/ `.jpeg`", "bot_version": "2.20.1", - "title": "Autorole Plugin", - "icon_url": "https://i.imgur.com/67bEi82.png", - "thumbnail_url": "https://i.imgur.com/67bEi82.png" + "title": "Media-only", + "icon_url": "https://i.imgur.com/ussAoIi.png", + "thumbnail_url": "https://i.imgur.com/ussAoIi.png" }, "anti-steal-close": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Don't let anyone steal ya close.", - "title": "Anti Steal Close", - "icon_url": "https://i.imgur.com/LovxyV3.png", - "thumbnail_url": "https://i.imgur.com/LovxyV3.png" - }, - "embedder": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "Easily make embeds for a nicer presence.", - "bot_version": "2.20.1", - "title": "Embedder Plugin", - "icon_url": "https://i.imgur.com/l3uzJwD.png", - "thumbnail_url": "https://i.imgur.com/l3uzJwD.png" - }, - "leveling": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "A leveling system for your server: see who's active and who's not.", - "bot_version": "2.20.1", - "title": "Leveling Plugin", - "icon_url": "https://i.imgur.com/VuZ60QX.png", - "thumbnail_url": "https://i.imgur.com/VuZ60QX.png" - }, - "purger": { - "repository": "papiersnipper/modmail-plugins", + "repository": "officialpiyush/modmail-plugins", "branch": "master", - "description": "Delete multiple messages at a time.", - "bot_version": "2.20.1", - "title": "Purger Plugin", - "icon_url": "https://i.imgur.com/HnC42jM.png", - "thumbnail_url": "https://i.imgur.com/HnC42jM.png" - }, - "supporters": { - "repository": "papiersnipper/modmail-plugins", - "branch": "master", - "description": "Let your users know who is part of the support team.", - "bot_version": "2.20.1", - "title": "Supporters Plugin", - "icon_url": "https://i.imgur.com/1QXRutA.png", - "thumbnail_url": "https://i.imgur.com/1QXRutA.png" + "description": "Don't let anyone steal ya close.", + "title": "Anti Steal Close", + "icon_url": "https://i.imgur.com/LovxyV3.png", + "thumbnail_url": "https://i.imgur.com/LovxyV3.png" }, "announcement": { "repository": "officialpiyush/modmail-plugins", @@ -115,15 +88,6 @@ "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" }, - "translator": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "You can auto translate thread msgs using this plugin or translate text fromm any language to English!", - "bot_version": "2.20.1", - "title": "Translator Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, "welcomer": { "repository": "fourjr/modmail-plugins", "branch": "master", @@ -150,7 +114,7 @@ "title": "Backup Database (backupdb)", "icon_url": "https://images.ionadev.ml/b/nKAlOC4.jpg", "thumbnail_url": "https://images.ionadev.ml/b/nKAlOC4.jpg" - }, + }, "colors": { "repository": "Taaku18/modmail-plugins", "branch": "master", @@ -160,7 +124,7 @@ "icon_url": "https://cdn1.iconfinder.com/data/icons/weather-19/32/rainbow-512.png", "thumbnail_url": "https://i.imgur.com/fSxnc9W.jpg" }, - "fun": { + "fun": { "repository": "TheKinG2149/modmail-plugins", "branch": "master", "description": "Some fun commands like 8ball, dadjokes", @@ -168,9 +132,9 @@ "title": "Fun", "icon_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png" - }, + }, "stats": { - "repository": "MiTonder/modmail-plugins", + "repository": "KarateWumpus/modmail-plugins", "branch": "master", "description": "Get useful stats directly in an embed about either the Modmail bot, a user or the server.", "bot_version": "2.24.1", @@ -190,10 +154,55 @@ "serverstats": { "repository": "dazvise/modmail-plugins", "branch": "master", - "description": "Interesting and accurate statistics about your server.", + "description": "Voice channels containing interesting and accurate statistics about your server such as Member Count.", "bot_version": "2.20.1", "title": "Server Stats", "icon_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png", "thumbnail_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png" + }, + "suggest": { + "repository": "realcyguy/modmail-plugins", + "branch": "master", + "description": "Send suggestions to a selected server! It even has moderation...", + "bot_version": "3.4.1", + "title": "Suggest stuff.", + "icon_url": "https://i.imgur.com/qtE7AH8.png", + "thumbnail_url": "https://i.imgur.com/qtE7AH8.png" + }, + "githubstats": { + "repository": "mischievousdev/modmail-plugins", + "branch": "master", + "description": "Github statistics in discord", + "bot_version": "2.20.1", + "title": "Github Stats", + "icon_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg", + "thumbnail_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg" + }, + "slowmode": { + "repository": "teen1/modmail-plugins", + "branch": "master", + "description": "Configure slow mode for your channels with Modmail!", + "bot_version": "2.20.1", + "title": "Slow Mode", + "icon_url": "https://cdn.discordapp.com/attachments/717029057635549274/717033838966210601/Slow_mode_-_icon.png", + "thumbnail_url": "https://cdn.discordapp.com/attachments/717029057635549274/717029110907666482/Slow_mode_plugin_-_thumbnail.png" + }, + "publish": { + "repository": "codeinteger6/modmail-plugins", + "branch": "master", + "description": "Publish messages sent in announcement channels.", + "bot_version": "3.5.0", + "title": "Publish", + "icon_url": "https://user-images.githubusercontent.com/44692189/89184422-96de3600-d5ba-11ea-98ea-d096aa385ad5.png", + "thumbnail_url": "https://user-images.githubusercontent.com/44692189/89184422-96de3600-d5ba-11ea-98ea-d096aa385ad5.png" + }, + "translate": { + "repository": "WebKide/modmail-plugins", + "branch": "master", + "description": "(∩`-´)⊃━☆゚.*・。゚ translate text from one language to another (defaults to English)\n\nGet full list of available languages at: https://github.com/WebKide/modmail-plugins/blob/master/translate/langs.json\n\nThis command conflicts with Translator-plugin", + "bot_version": "3.5.0", + "title": "Translate", + "icon_url": "https://i.imgur.com/yeHFKgl.png", + "thumbnail_url": "https://i.imgur.com/yeHFKgl.png" } -} +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 1841d893a1..4aa8248546 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,13 +4,13 @@ description = "Async http client/server framework (asyncio)" name = "aiohttp" optional = false python-versions = ">=3.5.3" -version = "3.5.4" +version = "3.6.2" [package.dependencies] async-timeout = ">=3.0,<4.0" attrs = ">=17.3.0" chardet = ">=2.0,<4.0" -multidict = ">=4.0,<5.0" +multidict = ">=4.5,<5.0" yarl = ">=1.0,<2.0" [[package]] @@ -19,20 +19,20 @@ description = "A small Python module for determining appropriate platform-specif name = "appdirs" optional = false python-versions = "*" -version = "1.4.3" +version = "1.4.4" [[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" +python-versions = ">=3.5" +version = "2.4.1" [package.dependencies] lazy-object-proxy = ">=1.4.0,<1.5.0" six = ">=1.12,<2.0" -wrapt = ">=1.11.0,<1.12.0" +wrapt = ">=1.11,<2.0" [package.dependencies.typed-ast] python = "<3.8" @@ -75,13 +75,16 @@ description = "The uncompromising code formatter." name = "black" optional = false python-versions = ">=3.6" -version = "19.3b0" +version = "19.10b0" [package.dependencies] appdirs = "*" attrs = ">=18.1.0" click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" toml = ">=0.9.4" +typed-ast = ">=1.4.0" [[package]] category = "main" @@ -96,8 +99,8 @@ 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" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" [[package]] category = "main" @@ -109,15 +112,15 @@ version = "0.4.3" [[package]] category = "main" -description = "A python wrapper for the Discord API" +description = "A Python wrapper for the Discord API" name = "discord.py" optional = false python-versions = ">=3.5.3" -version = "1.2.5" +version = "1.3.3" [package.dependencies] -aiohttp = ">=3.3.0,<3.6.0" -websockets = ">=6.0,<7.0" +aiohttp = ">=3.6.0,<3.7.0" +websockets = ">=6.0,<7.0 || >7.0,<8.0 || >8.0,<8.0.1 || >8.0.1,<9.0" [[package]] category = "main" @@ -146,24 +149,24 @@ version = "3.1.1" [[package]] category = "dev" description = "Git Object Database" -name = "gitdb2" +name = "gitdb" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.6" +python-versions = ">=3.4" +version = "4.0.5" [package.dependencies] -smmap2 = ">=2.0.0" +smmap = ">=3.0.1,<4" [[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" +python-versions = ">=3.4" +version = "3.1.3" [package.dependencies] -gitdb2 = ">=2.0.0" +gitdb = ">=4.0.1,<5" [[package]] category = "main" @@ -171,7 +174,7 @@ 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" +version = "2.9" [[package]] category = "main" @@ -226,7 +229,7 @@ description = "multidict implementation" name = "multidict" optional = false python-versions = ">=3.5" -version = "4.7.1" +version = "4.7.6" [[package]] category = "main" @@ -245,7 +248,15 @@ description = "Parse human-readable date/time text." name = "parsedatetime" optional = false python-versions = "*" -version = "2.5" +version = "2.6" + +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.8.0" [[package]] category = "dev" @@ -253,7 +264,7 @@ description = "Python Build Reasonableness" name = "pbr" optional = false python-versions = "*" -version = "5.4.4" +version = "5.4.5" [[package]] category = "dev" @@ -261,13 +272,14 @@ description = "python code static checker" name = "pylint" optional = false python-versions = ">=3.5.*" -version = "2.4.4" +version = "2.5.2" [package.dependencies] -astroid = ">=2.3.0,<2.4" +astroid = ">=2.4.0,<=2.5" colorama = "*" isort = ">=4.2.5,<5" mccabe = ">=0.6,<0.7" +toml = ">=0.7.1" [[package]] category = "main" @@ -275,7 +287,7 @@ description = "Python driver for MongoDB " name = "pymongo" optional = true python-versions = "*" -version = "3.10.0" +version = "3.10.1" [[package]] category = "main" @@ -294,43 +306,50 @@ description = "Add .env support to your django/flask apps in development and dep name = "python-dotenv" optional = false python-versions = "*" -version = "0.10.3" +version = "0.10.5" [[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" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.3.1" + +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2020.6.7" [[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" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" [[package]] category = "dev" description = "A pure Python implementation of a sliding window memory map manager" -name = "smmap2" +name = "smmap" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.5" +version = "3.0.4" [[package]] category = "dev" description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false -python-versions = "*" -version = "1.31.0" +python-versions = ">=3.6" +version = "2.0.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" -six = ">=1.10.0" [[package]] category = "dev" @@ -338,16 +357,15 @@ description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" optional = false python-versions = "*" -version = "0.10.0" +version = "0.10.1" [[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" +version = "1.4.1" [[package]] category = "main" @@ -362,8 +380,8 @@ category = "main" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" name = "websockets" optional = false -python-versions = ">=3.4" -version = "6.0" +python-versions = ">=3.6.1" +version = "8.1" [[package]] category = "dev" @@ -371,7 +389,7 @@ description = "Module for decorators, wrappers and monkey patching." name = "wrapt" optional = false python-versions = "*" -version = "1.11.2" +version = "1.12.1" [[package]] category = "main" @@ -385,48 +403,53 @@ version = "1.4.2" idna = ">=2.0" multidict = ">=4.0" +[extras] +mongodb = ["motor"] + [metadata] -content-hash = "fbe9e329f33e482854cff5bf05b006de9830c2d46bf3874e2ee4f8a8da0b1797" +content-hash = "68d5c15e62c4bf5f65fd3b0a9a2586b15557f724c3de5b756534bacd52cbfe40" 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"] +aiohttp = ["1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", "259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", "2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", "32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", "344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", "460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", "4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", "50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", "6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", "65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", "ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", "b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"] +appdirs = ["7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", "a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"] +astroid = ["4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", "d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"] async-timeout = ["0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"] attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] bandit = ["336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", "41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"] -black = ["09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", "68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"] +black = ["1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", "c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"] chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] -click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] +click = ["d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"] colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"] -"discord.py" = ["7c843b523bb011062b453864e75c7b675a03faf573c58d14c9f096e85984329d"] +"discord.py" = ["406871b06d86c3dc49fba63238519f28628dac946fef8a0e22988ff58ec05580", "ad00e34c72d2faa8db2157b651d05f3c415d7d05078e7e41dc9e8dc240051beb"] dnspython = ["36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", "f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"] emoji = ["60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174"] futures = ["3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b", "51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd", "c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"] -gitdb2 = ["1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", "96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"] -gitpython = ["9c2398ffc3dcb3c40b27324b316f08a4f93ad646d5a6328cafbb871aa79f5e42", "c155c6a2653593ccb300462f6ef533583a913e17857cfef8fc617c246b6dc245"] -idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] +gitdb = ["91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"] +gitpython = ["e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a", "ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"] +idna = ["7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"] 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"] +multidict = ["1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", "275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", "3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", "4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", "5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", "51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", "5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", "6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", "7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", "9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", "c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", "d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", "e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", "f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", "fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", "fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"] natural = ["18c83662d2d33fd7e6eee4e3b0d7366e1ce86225664e3127a2aaf0a3233f7df2"] -parsedatetime = ["3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", "d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"] -pbr = ["139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b", "61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"] -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"] +parsedatetime = ["4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", "cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b"] +pathspec = ["7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", "da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"] +pbr = ["07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c", "579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"] +pylint = ["b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", "dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b"] +pymongo = ["01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", "0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", "1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", "18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", "19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", "20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103", "26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", "26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", "2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", "2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", "316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", "31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", "334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", "358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", "3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", "444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", "47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", "4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", "4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", "53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", "568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", "56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", "5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", "61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", "619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", "6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", "63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113", "6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", "7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", "7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae", "80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", "95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", "993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", "9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", "a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", "a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", "a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", "a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", "a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", "ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012", "ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", "b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", "b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", "bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", "bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a", "c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", "c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", "c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", "c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", "da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", "dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", "e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", "e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", "e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", "f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a", "f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606"] 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"] +python-dotenv = ["440c7c23d53b7d352f9c94d6f70860242c2f071cf5c029dd661ccb22d64ae42b", "f254bfd0c970d64ccbb6c9ebef3667ab301a71473569c991253a481f1c98dddc"] +pyyaml = ["06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", "69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", "b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"] +regex = ["150125da109fccdcc8fec3b0b386b2a5d6ca7cff076f8b622486d1ca868b0c10", "163bc0805e46acfa098dfc8c0b07f371577d505f603e48afc425ff475cdac3a5", "20c513893ff80bdbe4b4ce11ea2e93d49481f05b270595d82af69ffc402010a6", "21fc17cb868c4264f0813f992f46f9ae6fc8c309d4741091de4153bd1f6a6176", "2c928bc8e0c453d73dffa3193a6e37ee752ea36df0dd4601e21024d98274dfad", "2d9beca70e36f9c60d679e108c5fe49f3d4da79d13a13f91e5e759443bd954f9", "5735f26cacdb50b3d6d35ebf8fdeb504bd8b381e2d079d2d9f12ce534fc14ecd", "6edc5c190248d3b612f2cca45448cf8ebc3621d41afcd1c5708853cbb1dbb3b3", "7606dba82435429641efe4fbc580574942f89cf2b9c5c1f8bc1eab2bacbf7e8b", "8d1ee3796795e609ef7a3a5a35eaf4728038d986aa12c06b3fd1b92ee81911f4", "8d9bb2d90e23c51aacbc58c1a11320f49b335cd67a91986cdbebcc3e843e4de8", "97d414c41f19fd2362e493810caa8445c05e0a2d63a14081c972aad66284a8d2", "9e37502817225ee99d91d8418f5119e98c380b00e772d06915690c05290f32ee", "af7209b2fcc79ee2b0ad4ea080d70bb748450ec4f282cc9e864861e469b1072e", "c0849b0864ff451f04c8afb5fc28e9ed592262e03debdd227cf0f53e04a55dcd", "c4ac9215650688e78dea29b46adbdafb7b85058eebe92ef6ea848e14466c915f", "dcda6d4e1bbfc939b177c237aee41c9678eaaf71df482688f8986e8251e12345", "dd8501b8d9ea1aba53c4bc7d47bc72933f9b4213d534cf400f16c1431f51c8ba", "ec0e509ed1877ff1cbc6f0864689bb60384a303502c4d72d9a635f8a4676fd3f", "f6c8c3f56fef719180464855346e6e80971b86dfd9e5a0e356664b5baca53072", "ffd4f80602490a309064cf2b203e220d581c51660e01055c64bf5da450485ee6"] +six = ["30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"] +smmap = ["54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"] +stevedore = ["001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9", "471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691"] +toml = ["926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", "bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"] +typed-ast = ["0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", "249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", "4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", "6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", "bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"] 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"] +websockets = ["0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", "1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", "20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", "295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", "2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", "3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", "3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", "3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", "4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", "5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", "5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", "751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", "7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", "965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", "9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", "9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", "c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", "c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", "ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", "d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", "e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", "f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"] +wrapt = ["b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"] 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 9c7122c417..0d47eb844a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 99 -target-version = ['py36'] +target-version = ['py37'] include = '\.pyi?$' exclude = ''' ( @@ -21,8 +21,8 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.5.0-dev0' -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.' +version = '3.6.0' +description = "Modmail is similar to Reddit's 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 ', @@ -36,10 +36,10 @@ 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" +"discord.py" = "=1.5.1" +uvloop = {version = ">=0.12.0", markers = "sys_platform != 'win32'"} +python-dotenv = ">=0.10.3" +parsedatetime = "^2.6" dnspython = "^1.16" isodate = "^0.6.0" natural = "^0.2.0" @@ -47,10 +47,12 @@ 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" +aiohttp = ">=3.6.0,<3.7.0" [tool.poetry.dev-dependencies] -black = {version = "=19.3b0", allows-prereleases = true} +black = {version = "=19.10b0", allow-prereleases = true} pylint = "^2.4" bandit = "^1.6" +[tool.poetry.extras] +mongodb = ["motor"] diff --git a/requirements.min.txt b/requirements.min.txt index dded194188..9fd4adf1a3 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -1,24 +1,24 @@ -# Generated as of October, 2019 +# Generated as of June, 2020 # This is the bare minimum requirements.txt for running Modmail. # To install requirements.txt run: pip install -r requirements.min.txt -aiohttp==3.5.4 +aiohttp==3.6.2 async-timeout==3.0.1 attrs==19.3.0 chardet==3.0.4 -discord.py==1.2.5 +discord.py==1.5.1 dnspython==1.16.0 emoji==0.5.4 -future==0.18.1 -idna==2.8 +future==0.18.2 +idna==2.9 isodate==0.6.0 -motor==2.0.0 -multidict==4.5.2 +motor==2.1.0 +multidict==4.7.6 natural==0.2.0 -parsedatetime==2.4 -pymongo==3.9.0 -python-dateutil==2.8.0 -python-dotenv==0.10.3 -six==1.12.0 -websockets==6.0 -yarl==1.3.0 +parsedatetime==2.6 +pymongo==3.10.1 +python-dateutil==2.8.1 +python-dotenv==0.14.0 +six==1.15.0 +websockets==8.1 +yarl==1.4.2 diff --git a/runtime.txt b/runtime.txt index aefcfbece7..67068f10fe 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.7.5 +python-3.9.0 \ No newline at end of file diff --git a/translation_files.py b/translation_files.py deleted file mode 100644 index 07c6979f63..0000000000 --- a/translation_files.py +++ /dev/null @@ -1,127 +0,0 @@ -import csv -import glob -import re -import string - -from discord.ext import commands - -from core import translations -from cogs.modmail import Modmail -from cogs.plugins import Plugins -from cogs.utility import Utility - - -data = [("Identifier", "English", "Context")] -all_identifiers = [] -identifiers = {} - - -class FormatError(Exception): - def __init__(self, reason, string): - super().__init__(f"Unable to parse {reason}: {string}") - - -for filename in glob.glob("**/*.py") + glob.glob("*.py"): - if filename == "translation_files.py": - continue - with open(filename, encoding="utf8") as f: - filedata = f.read() - regex_matches = re.findall( - r"(?:^|[^A-z])_\(.+?(?:\'|\")\)+?", filedata, flags=re.DOTALL | re.MULTILINE - ) - - for i in regex_matches: - if "f'" in i or 'f"' in i: - print(FormatError("f-string", i)) - identifier = "" - read = False - ignore_inverted = False - newline = False - counter = 0 - mode = None - for n in range(len(i)): - triggered = False - x = i[n] - - if x in ("'", '"') and not ignore_inverted: - if not mode: - mode = x - if mode == x: - read = not read - triggered = True - - if x == "\n": - newline = True - - if read and not triggered: - newline = False - identifier += x - - if newline and x not in string.whitespace: - counter += 1 - if counter > 1: - break - else: - counter = 0 - - if x == "\\": - ignore_inverted = True - elif ignore_inverted: - ignore_inverted = False - - all_identifiers.append(identifier) - - filedata_lines = filedata.splitlines() - fullline = list(filter((lambda x: x.find(i.splitlines()[0]) != -1), filedata_lines))[0] - count = 0 - for nline, line in enumerate(filedata_lines): - if line == fullline: - count += 1 - if count == all_identifiers.count(identifier): - linenum = nline + 1 - break - - if identifier in identifiers.keys(): - if filename not in data[identifiers[identifier]][2]: - data[identifiers[identifier]][2] += f" {filename}/L{linenum}" - elif str(linenum) not in data[identifiers[identifier]][2]: - split_space = data[identifiers[identifier]][2].split(" ") - for nx, x in enumerate(split_space): - if filename in x: - split_space[nx] += f"/L{linenum}" - break - data[identifiers[identifier]][2] = (" ").join(split_space) - else: - data.append([identifier, identifier, f"File: {filename}/L{linenum}"]) - identifiers[identifier] = len(data) - 1 - - print(filename) - -translations.init() -done = set() -bot = commands.Bot(command_prefix=None) -cogs = [Modmail(bot), Plugins(bot), Utility(bot)] -for i in cogs: - if i.description: - data.append([i.description, i.description, f"Cog: {i.__cog_name__}"]) - - for cmd in i.walk_commands(): - if cmd not in done: - if cmd.short_doc: - print(cmd) - data.append( - [ - cmd.short_doc, - cmd.short_doc, - f"Cog: {i.__cog_name__}\nCommand: {cmd.qualified_name}", - ] - ) - data.append( - [cmd.help, cmd.help, f"Cog: {i.__cog_name__}\nCommand: {cmd.qualified_name}"] - ) - - done.add(cmd) - - -with open("languages/en.csv", "w+") as f: - csv.writer(f, dialect="unix").writerows(data) From 3cfba283e885e6bd58b313664b9ace27feee9cc8 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 4 Nov 2020 23:54:35 +0800 Subject: [PATCH 0627/1221] Plain replies (#2872) --- CHANGELOG.md | 5 ++++- bot.py | 17 +++++++++++++---- cogs/modmail.py | 28 ++++++++++++++++++++++++++++ cogs/plugins.py | 6 +++--- core/config.py | 2 ++ core/config_help.json | 15 +++++++++++++-- core/thread.py | 40 ++++++++++++++++++++++++++++++++++++---- 7 files changed, 99 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b9d415b2..7b9260338d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.6.3-dev0 +# v3.6.3-dev1 + +### Added +- Plain replies: added commands `preply`, `pareply`, added config `plain_reply_without_command`. Only works from mod to users. ### Improved diff --git a/bot.py b/bot.py index ab5ded6c53..b442e89a25 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.6.3-dev0" +__version__ = "3.6.3-dev1" import asyncio @@ -896,10 +896,19 @@ async def process_commands(self, message): thread = await self.threads.find(channel=ctx.channel) if thread is not None: + anonymous = False + plain = False if self.config.get("anon_reply_without_command"): - await thread.reply(message, anonymous=True) - elif self.config.get("reply_without_command"): - await thread.reply(message) + anonymous = True + if self.config.get("plain_reply_without_command"): + plain = True + + if ( + self.config.get("reply_without_command") + or self.config.get("anon_reply_without_command") + or self.config.get("plain_reply_without_command") + ): + await thread.reply(message, anonymous=anonymous, plain=plain) else: await self.api.append_log(message, type_="internal") elif ctx.invoked_with: diff --git a/cogs/modmail.py b/cogs/modmail.py index 44f0e9dcde..4745ccd448 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -831,6 +831,34 @@ async def areply(self, ctx, *, msg: str = ""): async with ctx.typing(): await ctx.thread.reply(ctx.message, anonymous=True) + @commands.command(aliases=["plainreply"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def preply(self, ctx, *, msg: str = ""): + """ + Reply to a Modmail thread with a plain message. + + Supports attachments and images as well as + automatically embedding image URLs. + """ + ctx.message.content = msg + async with ctx.typing(): + await ctx.thread.reply(ctx.message, plain=True) + + @commands.command(aliases=["plainanonreply", "plainanonymousreply"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def pareply(self, ctx, *, msg: str = ""): + """ + Reply to a Modmail thread with a plain message and anonmymously. + + Supports attachments and images as well as + automatically embedding image URLs. + """ + ctx.message.content = msg + async with ctx.typing(): + await ctx.thread.reply(ctx.message, anonymous=True, plain=True) + @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() diff --git a/cogs/plugins.py b/cogs/plugins.py index 227cfc8270..e85907838f 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -172,10 +172,10 @@ async def download_plugin(self, plugin, force=False): except UnicodeDecodeError: pass else: - if raw == 'Not Found': - raise InvalidPluginError('Plugin not found') + if raw == "Not Found": + raise InvalidPluginError("Plugin not found") else: - raise InvalidPluginError('Invalid download recieved, non-bytes object') + raise InvalidPluginError("Invalid download recieved, non-bytes object") plugin_io = io.BytesIO(raw) if not plugin.cache_path.parent.exists(): diff --git a/core/config.py b/core/config.py index f2bb54a5c8..61f3b1ed5c 100644 --- a/core/config.py +++ b/core/config.py @@ -39,6 +39,7 @@ class ConfigManager: "thread_cooldown": isodate.Duration(), "reply_without_command": False, "anon_reply_without_command": False, + "plain_reply_without_command": False, # logging "log_channel_id": None, # threads @@ -130,6 +131,7 @@ class ConfigManager: "mod_typing", "reply_without_command", "anon_reply_without_command", + "plain_reply_without_command", "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", diff --git a/core/config_help.json b/core/config_help.json index 9778f49f8c..2466de613c 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -139,7 +139,7 @@ "`{prefix}config set reply_without_command no`" ], "notes": [ - "See also: `anon_reply_without_command`." + "See also: `anon_reply_without_command`, `plain_reply_without_command`." ] }, "anon_reply_without_command": { @@ -150,7 +150,18 @@ "`{prefix}config set anon_reply_without_command no`" ], "notes": [ - "See also: `reply_without_command`." + "See also: `reply_without_command`, `plain_reply_without_command`." + ] + }, + "plain_reply_without_command": { + "default": "Disabled", + "description": "Setting this configuration will make all non-command messages sent in the thread channel to be forwarded to the recipient in a plain form without the need of `{prefix}reply`.", + "examples": [ + "`{prefix}config set plain_reply_without_command yes`", + "`{prefix}config set plain_reply_without_command no`" + ], + "notes": [ + "See also: `reply_without_command`, `anon_reply_without_command`." ] }, "log_channel_id": { diff --git a/core/thread.py b/core/thread.py index 9c388389e0..adbfed2e67 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1,4 +1,5 @@ import asyncio +import io import re import typing from datetime import datetime, timedelta @@ -619,7 +620,9 @@ async def note(self, message: discord.Message) -> None: return msg - async def reply(self, message: discord.Message, anonymous: bool = False) -> None: + async def reply( + self, message: discord.Message, anonymous: bool = False, plain: bool = False + ) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) if not any(g.get_member(self.id) for g in self.bot.guilds): @@ -635,7 +638,11 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None try: await self.send( - message, destination=self.recipient, from_mod=True, anonymous=anonymous + message, + destination=self.recipient, + from_mod=True, + anonymous=anonymous, + plain=plain, ) except Exception: logger.error("Message delivery failed:", exc_info=True) @@ -653,7 +660,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False) -> None else: # Send the same thing in the thread channel. msg = await self.send( - message, destination=self.channel, from_mod=True, anonymous=anonymous + message, destination=self.channel, from_mod=True, anonymous=anonymous, plain=plain ) tasks.append( @@ -688,6 +695,7 @@ async def send( from_mod: bool = False, note: bool = False, anonymous: bool = False, + plain: bool = False, ) -> None: self.bot.loop.create_task( @@ -851,7 +859,31 @@ async def send( else: mentions = None - msg = await destination.send(mentions, embed=embed) + if plain: + if from_mod and not isinstance(destination, discord.TextChannel): + # Plain to user + if embed.footer.text: + plain_message = f"**({embed.footer.text}) " + else: + plain_message = "**" + plain_message += f"{embed.author.name}:** {embed.description}" + files = [] + for i in embed.fields: + if "Image" in i.name: + async with self.bot.session.get( + i.field[i.field.find("http") : -1] + ) as resp: + stream = io.BytesIO(await resp.read()) + files.append(discord.File(stream)) + + msg = await destination.send(plain_message, files=files) + else: + # Plain to mods + embed.set_footer(text="[PLAIN] " + embed.footer.text) + msg = await destination.send(mentions, embed=embed) + + else: + msg = await destination.send(mentions, embed=embed) if additional_images: self.ready = False From feebbefe918d05fe2ca7a06b5e6b0576d7130ea6 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 00:38:02 +0800 Subject: [PATCH 0628/1221] React to start thread --- CHANGELOG.md | 8 +++++++- cogs/modmail.py | 45 +++++++++++++++++++++++++++++++++++++------ core/config.py | 3 +++ core/config_help.json | 20 +++++++++++++++++++ 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b9260338d..b2b6eac90c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,13 @@ however, insignificant breaking changes do not guarantee a major version bump, s # v3.6.3-dev1 ### Added -- Plain replies: added commands `preply`, `pareply`, added config `plain_reply_without_command`. Only works from mod to users. + +- Plain replies: added commands `preply`, `pareply`, added config `plain_reply_without_command`. Only works from mod to users. ([#2872](https://github.com/kyb3r/modmail/issues/2872)) +- React to start a thread, added configs `react_to_contact_message`, `react_to_contact_emoji`. + +### Fixed + +- `?contact` now sends members a DM ### Improved diff --git a/cogs/modmail.py b/cogs/modmail.py index 4745ccd448..a5b79fe0c9 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -909,6 +909,7 @@ async def contact( user: Union[discord.Member, discord.User], *, category: discord.CategoryChannel = None, + manual_trigger = True ): """ Create a thread with a specified member. @@ -924,7 +925,7 @@ async def contact( embed = discord.Embed( color=self.bot.error_color, description="Cannot start a thread with a bot." ) - return await ctx.send(embed=embed) + return await ctx.send(embed=embed, delete_afer=3) exists = await self.bot.threads.find(recipient=user) if exists: @@ -933,13 +934,21 @@ async def contact( description="A thread for this user already " f"exists in {exists.channel.mention}.", ) - await ctx.channel.send(embed=embed) + await ctx.channel.send(embed=embed, delete_after=3) else: 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) + if ctx.author.id == user.id: + description = "You have opened a Modmail thread." + else: + description = f"{ctx.author.name} has opened a Modmail thread." + em = discord.Embed(title="New Thread", description=description, color=self.bot.main_color, timestamp=datetime.utcnow()) + em.set_footer(icon_url=ctx.author.avatar_url) + await user.send(embed=em) + embed = discord.Embed( title="Created Thread", description=f"Thread started by {ctx.author.mention} for {user.mention}.", @@ -947,10 +956,34 @@ async def contact( ) await thread.wait_until_ready() await thread.channel.send(embed=embed) - sent_emoji, _ = await self.bot.retrieve_emoji() - await self.bot.add_reaction(ctx.message, sent_emoji) - await asyncio.sleep(3) - await ctx.message.delete() + + if manual_trigger: + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + await asyncio.sleep(5) + await ctx.message.delete() + + @commands.Cog.listener() + async def on_raw_reaction_add(self, payload): + react_message_id = int(self.bot.config.get("react_to_contact_message")) + react_message_emoji = self.bot.config.get("react_to_contact_emoji") + if all((react_message_id, react_message_emoji)): + if payload.message_id == react_message_id: + if payload.emoji.is_unicode_emoji(): + emoji_fmt = payload.emoji.name + else: + emoji_fmt = f'<:{payload.emoji.name}:{payload.emoji.id}>' + + if emoji_fmt == react_message_emoji: + channel = self.bot.get_channel(payload.channel_id) + member = channel.guild.get_member(payload.user_id) + message = await channel.fetch_message(payload.message_id) + await message.remove_reaction(payload.emoji, member) + + ctx = await self.bot.get_context(message) + ctx.author = member + await ctx.invoke(self.contact, user=member, manual_trigger=False) + @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) diff --git a/core/config.py b/core/config.py index 61f3b1ed5c..528b22c483 100644 --- a/core/config.py +++ b/core/config.py @@ -75,6 +75,9 @@ class ConfigManager: "anon_username": None, "anon_avatar_url": None, "anon_tag": "Response", + # react to contact + "react_to_contact_message": None, + "react_to_contact_emoji": "\u2705", } private_keys = { diff --git a/core/config_help.json b/core/config_help.json index 2466de613c..a444f8a4ac 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -533,6 +533,26 @@ ], "image": "https://i.imgur.com/SKOC42Z.png" }, + "react_to_contact_message": { + "default": "None", + "description": "A message ID where reactions are tracked. If the `react_to_contact_emoji` is added, the bot opens a thread with them.", + "examples": [ + "`{prefix}config set react_to_contact_message 773575608814534717`" + ], + "notes": [ + "See also: `react_to_contact_emoji`" + ] + }, + "react_to_contact_emoji": { + "default": "\u2705", + "description": "An emoji which is tracked in `react_to_contact_message`", + "examples": [ + "`{prefix}config set react_to_contact_emoji 773575608814534717`" + ], + "notes": [ + "See also: `react_to_contact_message \u2705`" + ] + }, "modmail_guild_id": { "default": "Fallback on `GUILD_ID`", "description": "The ID of the discord server where the threads channels should be created (receiving server).", From edd37365ae3afe41c0967bb76b7d9c72affe9d10 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 00:46:39 +0800 Subject: [PATCH 0629/1221] Add config to mention mods after move , resolve #215 --- CHANGELOG.md | 3 ++- cogs/modmail.py | 5 +++++ core/config.py | 2 ++ core/config_help.json | 15 +++++++++++++-- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b6eac90c..ca5739d8b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,9 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Added -- Plain replies: added commands `preply`, `pareply`, added config `plain_reply_without_command`. Only works from mod to users. ([#2872](https://github.com/kyb3r/modmail/issues/2872)) +- Plain replies: added commands `preply`, `pareply`, added config `plain_reply_without_command`. Only works from mod to users. ([GH#2872](https://github.com/kyb3r/modmail/issues/2872)) - React to start a thread, added configs `react_to_contact_message`, `react_to_contact_emoji`. +- Mention mods after move: added config `thread_move_notify_mods` ([GH#215](https://github.com/kyb3r/modmail/issues/215)) ### Fixed diff --git a/cogs/modmail.py b/cogs/modmail.py index a5b79fe0c9..b19edd9f69 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -332,6 +332,11 @@ async def move(self, ctx, *, arguments): ) await thread.recipient.send(embed=embed) + if self.bot.config["thread_move_notify_mods"]: + mention = self.bot.config["mention"] + await thread.channel.send(f'{mention}, thread has been moved.') + + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) diff --git a/core/config.py b/core/config.py index 528b22c483..51efd82e48 100644 --- a/core/config.py +++ b/core/config.py @@ -60,6 +60,7 @@ class ConfigManager: "thread_self_close_response": "You have closed this Modmail thread.", "thread_move_title": "Thread Moved", "thread_move_notify": False, + "thread_move_notify_mods": False, "thread_move_response": "This thread has been moved.", "disabled_new_thread_title": "Not Delivered", "disabled_new_thread_response": "We are not accepting new threads.", @@ -138,6 +139,7 @@ class ConfigManager: "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", + "thread_move_notify_mods", "enable_plugins", "enable_eval", } diff --git a/core/config_help.json b/core/config_help.json index a444f8a4ac..ab97ba13cf 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -368,7 +368,7 @@ "`{prefix}config set thread_move_title Thread transferred to another channel!" ], "notes": [ - "See also: `thread_move_notify`, `thread_move_response`." + "See also: `thread_move_notify`, `thread_move_notify_mods`, `thread_move_response`." ] }, "thread_move_notify": { @@ -379,7 +379,18 @@ "`{prefix}config set thread_move_notify no`" ], "notes": [ - "See also: `thread_move_title`, `thread_move_response`." + "See also: `thread_move_title`, `thread_move_response`, `thread_move_notify_mods`." + ] + }, + "thread_move_notify_mods": { + "default": "No", + "description": "Notify mods again after the thread is moved", + "examples": [ + "`{prefix}config set thread_move_notify_mods yes`", + "`{prefix}config set thread_move_notify_mods no`" + ], + "notes": [ + "See also: `thread_move_title`, `thread_move_response`, `thread_move_notify`." ] }, "thread_move_response": { From 306a158bc69e55008354238671149eb45e5e466b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 00:56:51 +0800 Subject: [PATCH 0630/1221] transfer reactions config var, resolve #2783 --- CHANGELOG.md | 5 +++-- bot.py | 6 ++++-- core/config.py | 2 ++ core/config_help.json | 10 +++++++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5739d8b0..ffe2d03e03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,10 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Added -- Plain replies: added commands `preply`, `pareply`, added config `plain_reply_without_command`. Only works from mod to users. ([GH#2872](https://github.com/kyb3r/modmail/issues/2872)) +- Plain replies: added commands `preply`, `pareply`, added config `plain_reply_without_command`. Only works from mod to users. ([GH #2872](https://github.com/kyb3r/modmail/issues/2872)) - React to start a thread, added configs `react_to_contact_message`, `react_to_contact_emoji`. -- Mention mods after move: added config `thread_move_notify_mods` ([GH#215](https://github.com/kyb3r/modmail/issues/215)) +- Mention mods after move: added config `thread_move_notify_mods` ([GH #215](https://github.com/kyb3r/modmail/issues/215)) +- Added `transfer_reactions` config var ([GH #2763](https://github.com/kyb3r/modmail/issues/2763)) ### Fixed diff --git a/bot.py b/bot.py index b442e89a25..de5b7c7672 100644 --- a/bot.py +++ b/bot.py @@ -1009,10 +1009,12 @@ async def handle_reaction_events(self, payload): logger.warning("Failed to remove reaction: %s", e) async def on_raw_reaction_add(self, payload): - await self.handle_reaction_events(payload) + if self.config["transfer_reactions"]: + await self.handle_reaction_events(payload) async def on_raw_reaction_remove(self, payload): - await self.handle_reaction_events(payload) + if self.config["transfer_reactions"]: + await self.handle_reaction_events(payload) async def on_guild_channel_delete(self, channel): if channel.guild != self.modmail_guild: diff --git a/core/config.py b/core/config.py index 51efd82e48..e376d2048e 100644 --- a/core/config.py +++ b/core/config.py @@ -68,6 +68,7 @@ class ConfigManager: "disabled_current_thread_title": "Not Delivered", "disabled_current_thread_response": "We are not accepting any messages.", "disabled_current_thread_footer": "Please try again later...", + "transfer_reactions": True, # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), @@ -140,6 +141,7 @@ class ConfigManager: "thread_auto_close_silently", "thread_move_notify", "thread_move_notify_mods", + "transfer_reactions", "enable_plugins", "enable_eval", } diff --git a/core/config_help.json b/core/config_help.json index ab97ba13cf..0c2f68e579 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -558,12 +558,20 @@ "default": "\u2705", "description": "An emoji which is tracked in `react_to_contact_message`", "examples": [ - "`{prefix}config set react_to_contact_emoji 773575608814534717`" + "`{prefix}config set react_to_contact_emoji \u2705`" ], "notes": [ "See also: `react_to_contact_message \u2705`" ] }, + "transfer_reactions": { + "default": "Yes", + "description": "Transfer users reactions to mods and vice versa", + "examples":[ + "`{prefix}config set transfer_reactions no" + ], + "notes": [] + }, "modmail_guild_id": { "default": "Fallback on `GUILD_ID`", "description": "The ID of the discord server where the threads channels should be created (receiving server).", From d0bcaf16776c5f22cd0da6026cd8be062de04ba7 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 01:08:21 +0800 Subject: [PATCH 0631/1221] close_on_leave config, resolves #2757 --- bot.py | 11 +++++++---- core/config.py | 2 ++ core/config_help.json | 8 ++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/bot.py b/bot.py index de5b7c7672..80c6b6597f 100644 --- a/bot.py +++ b/bot.py @@ -1059,10 +1059,13 @@ async def on_member_remove(self, member): return thread = await self.threads.find(recipient=member) if thread: - embed = discord.Embed( - description="The recipient has left the server.", color=self.error_color - ) - await thread.channel.send(embed=embed) + if self.config["close_on_leave"]: + await thread.close(closer=member.guild.me, message="The recipient has left the server.", silent=True) + else: + embed = discord.Embed( + description="The recipient has left the server.", color=self.error_color + ) + await thread.channel.send(embed=embed) async def on_member_join(self, member): if member.guild != self.guild: diff --git a/core/config.py b/core/config.py index e376d2048e..132ac27ee0 100644 --- a/core/config.py +++ b/core/config.py @@ -69,6 +69,7 @@ class ConfigManager: "disabled_current_thread_response": "We are not accepting any messages.", "disabled_current_thread_footer": "Please try again later...", "transfer_reactions": True, + "close_on_leave": False, # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), @@ -142,6 +143,7 @@ class ConfigManager: "thread_move_notify", "thread_move_notify_mods", "transfer_reactions", + "close_on_leave", "enable_plugins", "enable_eval", } diff --git a/core/config_help.json b/core/config_help.json index 0c2f68e579..b8c626b4b8 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -572,6 +572,14 @@ ], "notes": [] }, + "close_on_leave": { + "default": "No", + "description": "Closes a modmail thread upon user leave automatically", + "examples":[ + "`{prefix}config set close_on_leave yes" + ], + "notes": [] + }, "modmail_guild_id": { "default": "Fallback on `GUILD_ID`", "description": "The ID of the discord server where the threads channels should be created (receiving server).", From 90e11cae82ab63844c8adbab06760ba03ecc2f16 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 01:11:30 +0800 Subject: [PATCH 0632/1221] Formatting --- CHANGELOG.md | 13 +++++++------ bot.py | 6 +++++- cogs/modmail.py | 15 +++++++++------ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe2d03e03..1132d2b0f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,15 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Added -- Plain replies: added commands `preply`, `pareply`, added config `plain_reply_without_command`. Only works from mod to users. ([GH #2872](https://github.com/kyb3r/modmail/issues/2872)) -- React to start a thread, added configs `react_to_contact_message`, `react_to_contact_emoji`. -- Mention mods after move: added config `thread_move_notify_mods` ([GH #215](https://github.com/kyb3r/modmail/issues/215)) -- Added `transfer_reactions` config var ([GH #2763](https://github.com/kyb3r/modmail/issues/2763)) +- Plain replies functionality. Added commands `preply`, `pareply` and config `plain_reply_without_command`. ([GH #2872](https://github.com/kyb3r/modmail/issues/2872)) +- Added `react_to_contact_message`, `react_to_contact_emoji` to allow users to create threads by reacting to a message. +- Added `thread_move_notify_mods` to mention all mods again after moving thread. ([GH #215](https://github.com/kyb3r/modmail/issues/215)) +- Added `transfer_reactions` to link reactions between mods and users. ([GH #2763](https://github.com/kyb3r/modmail/issues/2763)) +- Added `close_on_leave` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/kyb3r/modmail/issues/2757)) ### Fixed -- `?contact` now sends members a DM +- `?contact` now sends members a DM. ### Improved @@ -27,7 +28,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed -- Plugins downloading requirements in virtual environments +- Plugins downloading requirements in virtual environments. # v3.6.1 diff --git a/bot.py b/bot.py index 80c6b6597f..6613735843 100644 --- a/bot.py +++ b/bot.py @@ -1060,7 +1060,11 @@ async def on_member_remove(self, member): thread = await self.threads.find(recipient=member) if thread: if self.config["close_on_leave"]: - await thread.close(closer=member.guild.me, message="The recipient has left the server.", silent=True) + await thread.close( + closer=member.guild.me, + message="The recipient has left the server.", + silent=True, + ) else: embed = discord.Embed( description="The recipient has left the server.", color=self.error_color diff --git a/cogs/modmail.py b/cogs/modmail.py index b19edd9f69..73445ecba0 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -334,8 +334,7 @@ async def move(self, ctx, *, arguments): if self.bot.config["thread_move_notify_mods"]: mention = self.bot.config["mention"] - await thread.channel.send(f'{mention}, thread has been moved.') - + await thread.channel.send(f"{mention}, thread has been moved.") sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -914,7 +913,7 @@ async def contact( user: Union[discord.Member, discord.User], *, category: discord.CategoryChannel = None, - manual_trigger = True + manual_trigger=True, ): """ Create a thread with a specified member. @@ -950,7 +949,12 @@ async def contact( description = "You have opened a Modmail thread." else: description = f"{ctx.author.name} has opened a Modmail thread." - em = discord.Embed(title="New Thread", description=description, color=self.bot.main_color, timestamp=datetime.utcnow()) + em = discord.Embed( + title="New Thread", + description=description, + color=self.bot.main_color, + timestamp=datetime.utcnow(), + ) em.set_footer(icon_url=ctx.author.avatar_url) await user.send(embed=em) @@ -977,7 +981,7 @@ async def on_raw_reaction_add(self, payload): if payload.emoji.is_unicode_emoji(): emoji_fmt = payload.emoji.name else: - emoji_fmt = f'<:{payload.emoji.name}:{payload.emoji.id}>' + emoji_fmt = f"<:{payload.emoji.name}:{payload.emoji.id}>" if emoji_fmt == react_message_emoji: channel = self.bot.get_channel(payload.channel_id) @@ -989,7 +993,6 @@ async def on_raw_reaction_add(self, payload): ctx.author = member await ctx.invoke(self.contact, user=member, manual_trigger=False) - @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing From 9f8d8741c9f470ff54029ba8dd3fa7bb59851d8e Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 14:04:29 +0800 Subject: [PATCH 0633/1221] Added alert_on_mention, resolve #2833 --- CHANGELOG.md | 1 + bot.py | 18 ++++++++++++++++++ core/config.py | 2 ++ core/config_help.json | 10 ++++++++++ 4 files changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1132d2b0f0..f159aa15a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `thread_move_notify_mods` to mention all mods again after moving thread. ([GH #215](https://github.com/kyb3r/modmail/issues/215)) - Added `transfer_reactions` to link reactions between mods and users. ([GH #2763](https://github.com/kyb3r/modmail/issues/2763)) - Added `close_on_leave` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/kyb3r/modmail/issues/2757)) +- Added `alert_on_mention` to mention mods upon a bot mention. ([GH #2833](https://github.com/kyb3r/modmail/issues/2833)) ### Fixed diff --git a/bot.py b/bot.py index 6613735843..5642cb21ce 100644 --- a/bot.py +++ b/bot.py @@ -862,6 +862,24 @@ async def on_message(self, message): await self.wait_for_connected() if message.type == discord.MessageType.pins_add and message.author == self.user: await message.delete() + + if ( + (f"<@{self.user.id}" in message.content or f"<@!{self.user.id}" in message.content) + and self.config["alert_on_mention"] + and not message.author.bot + ): + if len(message.content) > 50: + extra = "..." + else: + extra = "" + em = discord.Embed( + title="Bot mention", + description=f"[Jump URL]({message.jump_url})\n{message.content[:50]}{extra}", + color=self.main_color, + timestamp=datetime.utcnow(), + ) + await self.log_channel.send(content=self.config["mention"], embed=em) + await self.process_commands(message) async def process_commands(self, message): diff --git a/core/config.py b/core/config.py index 132ac27ee0..936278d67b 100644 --- a/core/config.py +++ b/core/config.py @@ -70,6 +70,7 @@ class ConfigManager: "disabled_current_thread_footer": "Please try again later...", "transfer_reactions": True, "close_on_leave": False, + "alert_on_mention": False, # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), @@ -144,6 +145,7 @@ class ConfigManager: "thread_move_notify_mods", "transfer_reactions", "close_on_leave", + "alert_on_mention", "enable_plugins", "enable_eval", } diff --git a/core/config_help.json b/core/config_help.json index b8c626b4b8..acb2834e53 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -580,6 +580,16 @@ ], "notes": [] }, + "alert_on_mention": { + "default": "No", + "description": "Mentions all mods (mention) in logs channel when bot is mentioned", + "examples":[ + "`{prefix}config set alert_on_mention yes" + ], + "notes": [ + "See also: `mention`" + ] + }, "modmail_guild_id": { "default": "Fallback on `GUILD_ID`", "description": "The ID of the discord server where the threads channels should be created (receiving server).", From 5cb85a22ab929405b31f3f0b7b5df1c4a0b3d529 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 15:27:41 +0800 Subject: [PATCH 0634/1221] Added confirm thread creation, resolve #2773 --- CHANGELOG.md | 1 + bot.py | 17 ++--- core/config.py | 7 ++ core/config_help.json | 50 ++++++++++++++ core/thread.py | 150 ++++++++++++++++++++++++++++++++---------- 5 files changed, 183 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f159aa15a1..839c915071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `transfer_reactions` to link reactions between mods and users. ([GH #2763](https://github.com/kyb3r/modmail/issues/2763)) - Added `close_on_leave` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/kyb3r/modmail/issues/2757)) - Added `alert_on_mention` to mention mods upon a bot mention. ([GH #2833](https://github.com/kyb3r/modmail/issues/2833)) +- Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_description`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/kyb3r/modmail/issues/2773)) ### Fixed diff --git a/bot.py b/bot.py index 5642cb21ce..82344544c9 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 = await self.threads.create(message.author) + thread = await self.threads.create(message.author, message=message) else: if self.config["dm_disabled"] == 2: embed = discord.Embed( @@ -756,13 +756,14 @@ async def process_dm_modmail(self, message: discord.Message) -> None: await self.add_reaction(message, blocked_emoji) return await message.channel.send(embed=embed) - 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) + if not thread.cancelled: + 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): """ diff --git a/core/config.py b/core/config.py index 936278d67b..4e6b145924 100644 --- a/core/config.py +++ b/core/config.py @@ -82,6 +82,12 @@ class ConfigManager: # react to contact "react_to_contact_message": None, "react_to_contact_emoji": "\u2705", + # confirm thread creation + "confirm_thread_creation": False, + "confirm_thread_creation_title": "Confirm thread creation", + "confirm_thread_creation_description": "React to confirm thread creation which will directly contact the moderators", + "confirm_thread_creation_accept": "\u2705", + "confirm_thread_creation_deny": "\U0001F6AB", } private_keys = { @@ -146,6 +152,7 @@ class ConfigManager: "transfer_reactions", "close_on_leave", "alert_on_mention", + "confirm_thread_creation", "enable_plugins", "enable_eval", } diff --git a/core/config_help.json b/core/config_help.json index acb2834e53..c986be3ded 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -590,6 +590,56 @@ "See also: `mention`" ] }, + "confirm_thread_creation": { + "default": "No", + "description": "Ensure users confirm that they want to create a new thread", + "examples":[ + "`{prefix}config set confirm_thread_creation yes" + ], + "notes": [ + "See also: `confirm_thread_creation_title`, `confirm_thread_creation_description`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" + ] + }, + "confirm_thread_creation_title": { + "default": "Confirm thread creation", + "description": "Title for the embed message sent to users to confirm a thread creation", + "examples":[ + "`{prefix}config set confirm_thread_creation_title Are you sure you want to create a new thread?" + ], + "notes": [ + "See also: `confirm_thread_creation`, `confirm_thread_creation_description`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" + ] + }, + "confirm_thread_creation_description": { + "default": "React to confirm thread creation which will directly contact the moderators", + "description": "Description for the embed message sent to users to confirm a thread creation", + "examples":[ + "`{prefix}config set confirm_thread_creation_description React to confirm" + ], + "notes": [ + "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" + ] + }, + "confirm_thread_creation_accept": { + "default": "\u2705", + "description": "Emoji to accept a thread creation", + "examples":[ + "`{prefix}config set confirm_thread_creation_accept \u2611" + ], + "notes": [ + "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_description`, confirm_thread_creation_deny`" + ] + }, + "confirm_thread_creation_deny": { + "default": "\u2705", + "description": "Emoji to accept deny thread creation", + "examples":[ + "`{prefix}config set confirm_thread_creation_deny \u26D4" + ], + "notes": [ + "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_description`, confirm_thread_creation_accept`" + ] + }, "modmail_guild_id": { "default": "Fallback on `GUILD_ID`", "description": "The ID of the discord server where the threads channels should be created (receiving server).", diff --git a/core/thread.py b/core/thread.py index adbfed2e67..c844014462 100644 --- a/core/thread.py +++ b/core/thread.py @@ -39,19 +39,25 @@ def __init__( self._channel = channel self.genesis_message = None self._ready_event = asyncio.Event() + self.wait_tasks = [] self.close_task = None self.auto_close_task = None + self._cancelled = False def __repr__(self): 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.""" - # timeout after 3 seconds + # timeout after 30 seconds + task = asyncio.create_task(asyncio.wait_for(self._ready_event.wait(), timeout=20)) + self.wait_tasks.append(task) try: - await asyncio.wait_for(self._ready_event.wait(), timeout=3) + await task except asyncio.TimeoutError: - return + pass + + self.wait_tasks.remove(task) @property def id(self) -> int: @@ -77,6 +83,17 @@ def ready(self, flag: bool): else: self._ready_event.clear() + @property + def cancelled(self) -> bool: + return self._cancelled + + @cancelled.setter + def cancelled(self, flag: bool): + self._cancelled = flag + if flag: + for i in self.wait_tasks: + i.cancel() + async def setup(self, *, creator=None, category=None): """Create the thread channel and other io related initialisation tasks""" self.bot.dispatch("thread_initiate", self) @@ -312,22 +329,25 @@ async def _close( self.bot.config["notification_squad"].pop(str(self.id), None) # Logging - log_data = await self.bot.api.post_log( - self.channel.id, - { - "open": False, - "closed_at": str(datetime.utcnow()), - "nsfw": self.channel.nsfw, - "close_message": message if not silent else None, - "closer": { - "id": str(closer.id), - "name": closer.name, - "discriminator": closer.discriminator, - "avatar_url": str(closer.avatar_url), - "mod": True, + if self.channel: + log_data = await self.bot.api.post_log( + self.channel.id, + { + "open": False, + "closed_at": str(datetime.utcnow()), + "nsfw": self.channel.nsfw, + "close_message": message if not silent else None, + "closer": { + "id": str(closer.id), + "name": closer.name, + "discriminator": closer.discriminator, + "avatar_url": str(closer.avatar_url), + "mod": True, + }, }, - }, - ) + ) + else: + log_data = None if isinstance(log_data, dict): prefix = self.bot.config["log_url_prefix"].strip("/") @@ -950,15 +970,20 @@ async def find( 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 + try: + await thread.wait_until_ready() + except asyncio.CancelledError: + logger.warning("Thread for %s cancelled, abort creating", recipient) + return thread + else: + 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}" @@ -1000,6 +1025,7 @@ async def create( self, recipient: typing.Union[discord.Member, discord.User], *, + message: discord.Message = None, creator: typing.Union[discord.Member, discord.User] = None, category: discord.CategoryChannel = None, ) -> Thread: @@ -1008,14 +1034,19 @@ async def create( # 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) + try: + await thread.wait_until_ready() + except asyncio.CancelledError: + logger.warning("Thread for %s cancelled, 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) - ) + else: + 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) @@ -1035,6 +1066,57 @@ async def create( self.bot.config.set("fallback_category_id", category.id) await self.bot.config.update() + if message and self.bot.config["confirm_thread_creation"]: + confirm = await message.channel.send( + embed=discord.Embed( + title=self.bot.config["confirm_thread_creation_title"], + description=self.bot.config["confirm_thread_creation_description"], + color=self.bot.main_color, + ) + ) + accept_emoji = self.bot.config["confirm_thread_creation_accept"] + deny_emoji = self.bot.config["confirm_thread_creation_deny"] + await confirm.add_reaction(accept_emoji) + await asyncio.sleep(0.2) + await confirm.add_reaction(deny_emoji) + try: + r, _ = await self.bot.wait_for( + "reaction_add", + check=lambda r, u: u.id == message.author.id + and r.message.id == confirm.id + and r.message.channel.id == confirm.channel.id + and r.emoji in (accept_emoji, deny_emoji), + timeout=20, + ) + except asyncio.TimeoutError: + thread.cancelled = True + + await confirm.remove_reaction(accept_emoji, self.bot.user) + await asyncio.sleep(0.2) + await confirm.remove_reaction(deny_emoji, self.bot.user) + await message.channel.send( + embed=discord.Embed( + title="Cancelled", description="Timed out", color=self.bot.error_color + ) + ) + del self.cache[recipient.id] + return thread + else: + if r.emoji == deny_emoji: + thread.cancelled = True + + await confirm.remove_reaction(accept_emoji, self.bot.user) + await asyncio.sleep(0.2) + await confirm.remove_reaction(deny_emoji, self.bot.user) + await message.channel.send( + embed=discord.Embed( + title="Cancelled", color=self.bot.error_color + ) + ) + del self.cache[recipient.id] + return thread + + self.bot.loop.create_task(thread.setup(creator=creator, category=category)) return thread From 3eee193a78fb49b6b7905ecff6d0904f9828c6fe Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 15:58:55 +0800 Subject: [PATCH 0635/1221] Support gyazo links in image embed, resolve #282 --- CHANGELOG.md | 3 ++- bot.py | 2 +- core/thread.py | 21 ++++++++++++--------- core/utils.py | 19 +++++++++++++++---- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 839c915071..55bbf33fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.6.3-dev1 +# v3.6.3-dev2 ### Added @@ -17,6 +17,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `close_on_leave` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/kyb3r/modmail/issues/2757)) - Added `alert_on_mention` to mention mods upon a bot mention. ([GH #2833](https://github.com/kyb3r/modmail/issues/2833)) - Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_description`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/kyb3r/modmail/issues/2773)) +- Support Gyazo image links in message embeds. ([GH #282](https://github.com/kyb3r/modmail/issues/282)) ### Fixed diff --git a/bot.py b/bot.py index 82344544c9..716a66c598 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.6.3-dev1" +__version__ = "3.6.3-dev2" import asyncio diff --git a/core/thread.py b/core/thread.py index c844014462..2ec55b8214 100644 --- a/core/thread.py +++ b/core/thread.py @@ -56,7 +56,7 @@ async def wait_until_ready(self) -> None: await task except asyncio.TimeoutError: pass - + self.wait_tasks.remove(task) @property @@ -86,7 +86,7 @@ def ready(self, flag: bool): @property def cancelled(self) -> bool: return self._cancelled - + @cancelled.setter def cancelled(self, flag: bool): self._cancelled = flag @@ -793,11 +793,15 @@ async def send( attachments.append(attachment) image_urls = re.findall( - r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + r"http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", message.content, ) - image_urls = [(url, None) for url in image_urls if is_image_url(url)] + image_urls = [ + (is_image_url(url, convert_size=False), None) + for url in image_urls + if is_image_url(url, convert_size=False) + ] images.extend(image_urls) embedded_image = False @@ -1043,7 +1047,9 @@ async def create( 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) + 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) ) @@ -1109,14 +1115,11 @@ async def create( await asyncio.sleep(0.2) await confirm.remove_reaction(deny_emoji, self.bot.user) await message.channel.send( - embed=discord.Embed( - title="Cancelled", color=self.bot.error_color - ) + embed=discord.Embed(title="Cancelled", color=self.bot.error_color) ) del self.cache[recipient.id] return thread - self.bot.loop.create_task(thread.setup(creator=creator, category=category)) return thread diff --git a/core/utils.py b/core/utils.py index a4f14182a5..a648f04f1a 100644 --- a/core/utils.py +++ b/core/utils.py @@ -117,7 +117,7 @@ def format_preview(messages: typing.List[typing.Dict[str, typing.Any]]): return out or "No Messages" -def is_image_url(url: str) -> bool: +def is_image_url(url: str, **kwargs) -> bool: """ Check if the URL is pointing to an image. @@ -131,10 +131,18 @@ def is_image_url(url: str) -> bool: bool Whether the URL is a valid image URL. """ - return bool(parse_image_url(url)) + if url.startswith("https://gyazo.com") or url.startswith("http://gyazo.com"): + # gyazo support + url = re.sub( + r"(http[s]?:\/\/)((?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)", + r"\1i.\2.png", + url, + ) + return parse_image_url(url, **kwargs) -def parse_image_url(url: str) -> str: + +def parse_image_url(url: str, *, convert_size=True) -> str: """ Convert the image URL into a sized Discord avatar. @@ -152,7 +160,10 @@ def parse_image_url(url: str) -> str: url = parse.urlsplit(url) if any(url.path.lower().endswith(i) for i in types): - return parse.urlunsplit((*url[:3], "size=128", url[-1])) + if convert_size: + return parse.urlunsplit((*url[:3], "size=128", url[-1])) + else: + return parse.urlunsplit(url) return "" From 1968f0ac385daad5f7520550cfe57ea3dad1f2e5 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 15:59:21 +0800 Subject: [PATCH 0636/1221] Support silent arg in contact --- CHANGELOG.md | 1 + cogs/modmail.py | 36 ++++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55bbf33fe3..5ed07feeac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `alert_on_mention` to mention mods upon a bot mention. ([GH #2833](https://github.com/kyb3r/modmail/issues/2833)) - Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_description`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/kyb3r/modmail/issues/2773)) - Support Gyazo image links in message embeds. ([GH #282](https://github.com/kyb3r/modmail/issues/282)) +- Added `silent` argument to `?contact` to restore old behaviour. ### Fixed diff --git a/cogs/modmail.py b/cogs/modmail.py index 73445ecba0..fa420034bc 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -905,14 +905,14 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) - @commands.command() + @commands.command(usage=" [category] [options]") @checks.has_permissions(PermissionLevel.SUPPORTER) async def contact( self, ctx, user: Union[discord.Member, discord.User], *, - category: discord.CategoryChannel = None, + category: Union[discord.CategoryChannel, str] = None, manual_trigger=True, ): """ @@ -923,7 +923,13 @@ async def contact( `category`, if specified, may be a category ID, mention, or name. `user` may be a user ID, mention, or name. + `options` can be `silent` """ + silent = False + if isinstance(category, str): + if "silent" in category or "silently" in category: + silent = True + category = None if user.bot: embed = discord.Embed( @@ -945,18 +951,20 @@ async def contact( if self.bot.config["dm_disabled"] >= 1: logger.info("Contacting user %s when Modmail DM is disabled.", user) - if ctx.author.id == user.id: - description = "You have opened a Modmail thread." - else: - description = f"{ctx.author.name} has opened a Modmail thread." - em = discord.Embed( - title="New Thread", - description=description, - color=self.bot.main_color, - timestamp=datetime.utcnow(), - ) - em.set_footer(icon_url=ctx.author.avatar_url) - await user.send(embed=em) + if not silent: + if ctx.author.id == user.id: + description = "You have opened a Modmail thread." + else: + description = f"{ctx.author.name} has opened a Modmail thread." + + em = discord.Embed( + title="New Thread", + description=description, + color=self.bot.main_color, + timestamp=datetime.utcnow(), + ) + em.set_footer(icon_url=ctx.author.avatar_url) + await user.send(embed=em) embed = discord.Embed( title="Created Thread", From 0222231263a157981c68e02459c469fa1fabdd86 Mon Sep 17 00:00:00 2001 From: Vincysuper07 <52707876+Vincysuper07@users.noreply.github.com> Date: Thu, 5 Nov 2020 10:25:16 +0100 Subject: [PATCH 0637/1221] Replace moderation plugin (#2873) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reè * Confused branch once again --- plugins/registry.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 5c343ebcff..55ce288847 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -143,13 +143,13 @@ "thumbnail_url": "http://www.pngmart.com/files/7/Statistics-PNG-Clipart.png" }, "moderation": { - "repository": "xPolar/modmail-plugins", - "branch": "master", + "repository": "Vincysuper07/modmail-plugins", + "branch": "main", "description": "Moderate your server with Modmail, bring the Mod to Modmail!", "bot_version": "3.0.3", "title": "Moderate your server", - "icon_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png", - "thumbnail_url": "https://cdn.discordapp.com/attachments/539943767562780704/601485194196680704/wGFmzZq.png" + "icon_url": "https://cdn.discordapp.com/attachments/759829573654544454/773535811143598110/ad2e4d6e7b90ca6005a5038e22b099cc.png", + "thumbnail_url": "https://cdn.discordapp.com/attachments/759829573654544454/773535811143598110/ad2e4d6e7b90ca6005a5038e22b099cc.png" }, "serverstats": { "repository": "dazvise/modmail-plugins", @@ -205,4 +205,4 @@ "icon_url": "https://i.imgur.com/yeHFKgl.png", "thumbnail_url": "https://i.imgur.com/yeHFKgl.png" } -} \ No newline at end of file +} From 0df4ab4d296bacdc3bbab98365f2f334c01d76fe Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 17:26:51 +0800 Subject: [PATCH 0638/1221] change moderation bot version to 3.6.2 --- plugins/registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 55ce288847..f5e119912e 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -146,7 +146,7 @@ "repository": "Vincysuper07/modmail-plugins", "branch": "main", "description": "Moderate your server with Modmail, bring the Mod to Modmail!", - "bot_version": "3.0.3", + "bot_version": "3.6.2", "title": "Moderate your server", "icon_url": "https://cdn.discordapp.com/attachments/759829573654544454/773535811143598110/ad2e4d6e7b90ca6005a5038e22b099cc.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/759829573654544454/773535811143598110/ad2e4d6e7b90ca6005a5038e22b099cc.png" From aba9b71a5568daeac3ea4448141704e4a589b178 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 18:43:35 +0800 Subject: [PATCH 0639/1221] add countdowns plugin --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index f5e119912e..ce2b76afc0 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -204,5 +204,14 @@ "title": "Translate", "icon_url": "https://i.imgur.com/yeHFKgl.png", "thumbnail_url": "https://i.imgur.com/yeHFKgl.png" + }, + "countdowns": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Setup a countdown voice channel in your server!", + "bot_version": "3.6.2", + "title": "Countdowns", + "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", + "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" } } From 59eabf8d55d3989035b70f99f721a41cd69da9b9 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 23:25:35 +0800 Subject: [PATCH 0640/1221] Help and help all, resolve #2847 --- CHANGELOG.md | 1 + cogs/utility.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed07feeac..44fa86d9eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_description`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/kyb3r/modmail/issues/2773)) - Support Gyazo image links in message embeds. ([GH #282](https://github.com/kyb3r/modmail/issues/282)) - Added `silent` argument to `?contact` to restore old behaviour. +- If `?help` is sent, bot does checks on every command, `?help all` restores old behaviour. ([GH #2847](https://github.com/kyb3r/modmail/issues/2847)) ### Fixed diff --git a/cogs/utility.py b/cogs/utility.py index b0d96ea094..e3c0d12b66 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -31,6 +31,19 @@ class ModmailHelpCommand(commands.HelpCommand): + async def command_callback(self, ctx, *, command=None): + """Ovrwrites original command_callback to ensure `help` without any arguments + returns with checks, `help all` returns without checks""" + if command is None: + self.verify_checks = True + else: + self.verify_checks = False + + if command == 'all': + command = None + + return await super().command_callback(ctx, command=command) + async def format_cog_help(self, cog, *, no_cog=False): bot = self.context.bot prefix = self.clean_prefix @@ -64,6 +77,9 @@ async def format_cog_help(self, cog, *, no_cog=False): ) embed = discord.Embed(description=f"*{description}*", color=bot.main_color) + if not format_: + continue + embed.add_field(name="Commands", value=format_ or "No commands.") continued = " (Continued)" if embeds else "" @@ -231,7 +247,6 @@ def __init__(self, bot): self.bot = bot self._original_help_command = bot.help_command self.bot.help_command = ModmailHelpCommand( - verify_checks=False, command_attrs={ "help": "Shows this help message.", "checks": [checks.has_permissions_predicate(PermissionLevel.REGULAR)], From e7ba21bed2859e20eb74d07b9fefa04e8bfe9175 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 23:46:45 +0800 Subject: [PATCH 0641/1221] Role blocking, resolve #2753 --- CHANGELOG.md | 1 + bot.py | 37 +++++++++++++++++++++++++ cogs/modmail.py | 72 +++++++++++++++++++++++++++++++++++++++---------- cogs/utility.py | 4 +-- core/config.py | 1 + 5 files changed, 99 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44fa86d9eb..a558003e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Support Gyazo image links in message embeds. ([GH #282](https://github.com/kyb3r/modmail/issues/282)) - Added `silent` argument to `?contact` to restore old behaviour. - If `?help` is sent, bot does checks on every command, `?help all` restores old behaviour. ([GH #2847](https://github.com/kyb3r/modmail/issues/2847)) +- Added a way to block roles. ([GH #2753](https://github.com/kyb3r/modmail/issues/2753)) ### Fixed diff --git a/bot.py b/bot.py index 716a66c598..bb0c4b853f 100644 --- a/bot.py +++ b/bot.py @@ -324,6 +324,10 @@ def main_category(self) -> typing.Optional[discord.CategoryChannel]: def blocked_users(self) -> typing.Dict[str, str]: return self.config["blocked"] + @property + def blocked_roles(self) -> typing.Dict[str, str]: + return self.config["blocked_roles"] + @property def blocked_whitelisted_users(self) -> typing.List[str]: return self.config["blocked_whitelist"] @@ -578,6 +582,36 @@ def check_guild_age(self, author: discord.Member) -> bool: return False return True + def check_manual_blocked_roles(self, author: discord.Member) -> bool: + for r in author.roles: + if str(r.id) in self.blocked_roles: + + blocked_reason = self.blocked_roles.get(str(r.id)) or "" + now = datetime.utcnow() + + # 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, + ) + + 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 + + return True + def check_manual_blocked(self, author: discord.Member) -> bool: if str(author.id) not in self.blocked_users: return True @@ -656,6 +690,9 @@ async def is_blocked( if not self.check_manual_blocked(author): return True + if not self.check_manual_blocked_roles(author): + return True + await self.config.update() return False diff --git a/cogs/modmail.py b/cogs/modmail.py index fa420034bc..45e77f2eda 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -7,6 +7,7 @@ import discord from discord.ext import commands +from discord.role import Role from discord.utils import escape_markdown from dateutil import parser @@ -1009,6 +1010,7 @@ async def blocked(self, ctx): embeds = [discord.Embed(title="Blocked Users", color=self.bot.main_color, description="")] + roles = [] users = [] for id_, reason in self.bot.blocked_users.items(): @@ -1022,6 +1024,11 @@ async def blocked(self, ctx): except discord.NotFound: users.append((id_, reason)) + for id_, reason in self.bot.blocked_roles.items(): + role = self.bot.guild.get_role(int(id_)) + if role: + roles.append((role.mention, reason)) + if users: embed = embeds[0] @@ -1039,7 +1046,29 @@ async def blocked(self, ctx): else: embeds[0].description = "Currently there are no blocked users." + embeds.append( + discord.Embed(title="Blocked Roles", color=self.bot.main_color, description="") + ) + + if roles: + embed = embeds[-1] + + for mention, reason in roles: + line = mention + f" - {reason or 'No Reason Provided'}\n" + if len(embed.description) + len(line) > 2048: + embed = discord.Embed( + title="Blocked Roles (Continued)", + color=self.bot.main_color, + description=line, + ) + embeds.append(embed) + else: + embed.description += line + else: + embeds[-1].description = "Currently there are no blocked roles." + session = EmbedPaginatorSession(ctx, *embeds) + await session.run() @blocked.command(name="whitelist") @@ -1100,7 +1129,13 @@ async def blocked_whitelist(self, ctx, *, user: User = None): @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): + async def block( + self, + ctx, + user_or_role: Union[User, discord.Role] = None, + *, + after: UserFriendlyTime = None, + ): """ Block a user from using Modmail. @@ -1112,24 +1147,25 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi `duration` may be a simple "human-readable" time text. See `{prefix}help close` for examples. """ - if user is None: + if user_or_role is None: thread = ctx.thread if thread: - user = thread.recipient + user_or_role = thread.recipient elif after is None: raise commands.MissingRequiredArgument(SimpleNamespace(name="user")) else: raise commands.BadArgument(f'User "{after.arg}" not found.') - mention = getattr(user, "mention", f"`{user.id}`") + mention = getattr(user_or_role, "mention", f"`{user_or_role.id}`") - if str(user.id) in self.bot.blocked_whitelisted_users: - embed = discord.Embed( - title="Error", - description=f"Cannot block {mention}, user is whitelisted.", - color=self.bot.error_color, - ) - return await ctx.send(embed=embed) + if not isinstance(user_or_role, discord.Role): + if str(user_or_role.id) in self.bot.blocked_whitelisted_users: + embed = discord.Embed( + title="Error", + description=f"Cannot block {mention}, user is whitelisted.", + color=self.bot.error_color, + ) + return await ctx.send(embed=embed) reason = f"by {escape_markdown(ctx.author.name)}#{ctx.author.discriminator}" @@ -1143,11 +1179,15 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi reason += "." - msg = self.bot.blocked_users.get(str(user.id)) + if isinstance(user_or_role, discord.Role): + msg = self.bot.blocked_roles.get(str(user_or_role.id)) + else: + msg = self.bot.blocked_users.get(str(user_or_role.id)) + if msg is None: msg = "" - if str(user.id) in self.bot.blocked_users and msg: + if msg: old_reason = msg.strip().rstrip(".") embed = discord.Embed( title="Success", @@ -1161,7 +1201,11 @@ async def block(self, ctx, user: Optional[User] = None, *, after: UserFriendlyTi color=self.bot.main_color, description=f"{mention} is now blocked {reason}", ) - self.bot.blocked_users[str(user.id)] = reason + + if isinstance(user_or_role, discord.Role): + self.bot.blocked_roles[str(user_or_role.id)] = reason + else: + self.bot.blocked_users[str(user_or_role.id)] = reason await self.bot.config.update() return await ctx.send(embed=embed) diff --git a/cogs/utility.py b/cogs/utility.py index e3c0d12b66..8658767d9f 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -38,8 +38,8 @@ async def command_callback(self, ctx, *, command=None): self.verify_checks = True else: self.verify_checks = False - - if command == 'all': + + if command == "all": command = None return await super().command_callback(ctx, command=command) diff --git a/core/config.py b/core/config.py index 4e6b145924..8e093ecc1d 100644 --- a/core/config.py +++ b/core/config.py @@ -101,6 +101,7 @@ class ConfigManager: "oauth_whitelist": [], # moderation "blocked": {}, + "blocked_roles": {}, "blocked_whitelist": [], "command_permissions": {}, "level_permissions": {}, From ef4abaf1dbc666b44fe2b807f1a4b44297e87604 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 5 Nov 2020 23:51:30 +0800 Subject: [PATCH 0642/1221] Add functionality to unblock roles --- cogs/modmail.py | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 45e77f2eda..543a18af0f 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1158,14 +1158,16 @@ async def block( mention = getattr(user_or_role, "mention", f"`{user_or_role.id}`") - if not isinstance(user_or_role, discord.Role): - if str(user_or_role.id) in self.bot.blocked_whitelisted_users: - embed = discord.Embed( - title="Error", - description=f"Cannot block {mention}, user is whitelisted.", - color=self.bot.error_color, - ) - return await ctx.send(embed=embed) + if ( + not isinstance(user_or_role, discord.Role) + and str(user_or_role.id) in self.bot.blocked_whitelisted_users + ): + embed = discord.Embed( + title="Error", + description=f"Cannot block {mention}, user is whitelisted.", + color=self.bot.error_color, + ) + return await ctx.send(embed=embed) reason = f"by {escape_markdown(ctx.author.name)}#{ctx.author.discriminator}" @@ -1213,7 +1215,7 @@ async def block( @commands.command() @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing - async def unblock(self, ctx, *, user: User = None): + async def unblock(self, ctx, *, user_or_role: Union[User, Role] = None): """ Unblock a user from using Modmail. @@ -1222,18 +1224,21 @@ async def unblock(self, ctx, *, user: User = None): `user` may be a user ID, mention, or name. """ - if user is None: + if user_or_role is None: thread = ctx.thread if thread: - user = thread.recipient + user_or_role = thread.recipient else: raise commands.MissingRequiredArgument(SimpleNamespace(name="user")) - mention = getattr(user, "mention", f"`{user.id}`") - name = getattr(user, "name", f"`{user.id}`") + mention = getattr(user_or_role, "mention", f"`{user_or_role.id}`") + name = getattr(user_or_role, "name", f"`{user_or_role.id}`") - if str(user.id) in self.bot.blocked_users: - msg = self.bot.blocked_users.pop(str(user.id)) or "" + if ( + not isinstance(user_or_role, discord.Role) + and str(user_or_role.id) in self.bot.blocked_users + ): + msg = self.bot.blocked_users.pop(str(user_or_role.id)) or "" await self.bot.config.update() if msg.startswith("System Message: "): @@ -1249,7 +1254,7 @@ async def unblock(self, ctx, *, user: User = None): embed.set_footer( text="However, if the original system block reason still applies, " f"{name} will be automatically blocked again. " - f'Use "{self.bot.prefix}blocked whitelist {user.id}" to whitelist the user.' + f'Use "{self.bot.prefix}blocked whitelist {user_or_role.id}" to whitelist the user.' ) else: embed = discord.Embed( @@ -1257,6 +1262,18 @@ async def unblock(self, ctx, *, user: User = None): color=self.bot.main_color, description=f"{mention} is no longer blocked.", ) + elif ( + isinstance(user_or_role, discord.Role) + and str(user_or_role.id) in self.bot.blocked_roles + ): + msg = self.bot.blocked_roles.pop(str(user_or_role.id)) or "" + await self.bot.config.update() + + embed = discord.Embed( + title="Success", + color=self.bot.main_color, + description=f"{mention} is no longer blocked.", + ) else: embed = discord.Embed( title="Error", description=f"{mention} is not blocked.", color=self.bot.error_color From 66856ca9244fc44012048368719d05dbb207a167 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Nov 2020 00:04:09 +0800 Subject: [PATCH 0643/1221] cooldown_thread_title and cooldown_thread_response config var, resolve #2865 --- CHANGELOG.md | 5 +++-- bot.py | 6 +++--- core/config.py | 4 +++- core/config_help.json | 35 +++++++++++++++++++++++++++++------ core/thread.py | 2 +- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a558003e84..74e5398b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.6.3-dev2 +# v3.7.0-dev3 ### Added @@ -16,11 +16,12 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `transfer_reactions` to link reactions between mods and users. ([GH #2763](https://github.com/kyb3r/modmail/issues/2763)) - Added `close_on_leave` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/kyb3r/modmail/issues/2757)) - Added `alert_on_mention` to mention mods upon a bot mention. ([GH #2833](https://github.com/kyb3r/modmail/issues/2833)) -- Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_description`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/kyb3r/modmail/issues/2773)) +- Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/kyb3r/modmail/issues/2773)) - Support Gyazo image links in message embeds. ([GH #282](https://github.com/kyb3r/modmail/issues/282)) - Added `silent` argument to `?contact` to restore old behaviour. - If `?help` is sent, bot does checks on every command, `?help all` restores old behaviour. ([GH #2847](https://github.com/kyb3r/modmail/issues/2847)) - Added a way to block roles. ([GH #2753](https://github.com/kyb3r/modmail/issues/2753)) +- Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/kyb3r/modmail/issues/2865)) ### Fixed diff --git a/bot.py b/bot.py index bb0c4b853f..0ffbd59b51 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.6.3-dev2" +__version__ = "3.7.0-dev3" import asyncio @@ -753,8 +753,8 @@ async def process_dm_modmail(self, message: discord.Message) -> None: 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.", + title=self.config["cooldown_thread_title"], + description=self.config["cooldown_thread_response"].format(delta=delta), color=self.error_color, ) ) diff --git a/core/config.py b/core/config.py index 8e093ecc1d..c4570f93f9 100644 --- a/core/config.py +++ b/core/config.py @@ -62,6 +62,8 @@ class ConfigManager: "thread_move_notify": False, "thread_move_notify_mods": False, "thread_move_response": "This thread has been moved.", + "cooldown_thread_title": "Message not sent!", + "cooldown_thread_response": "You must wait for {delta} before you can contact me again.", "disabled_new_thread_title": "Not Delivered", "disabled_new_thread_response": "We are not accepting new threads.", "disabled_new_thread_footer": "Please try again later...", @@ -85,7 +87,7 @@ class ConfigManager: # confirm thread creation "confirm_thread_creation": False, "confirm_thread_creation_title": "Confirm thread creation", - "confirm_thread_creation_description": "React to confirm thread creation which will directly contact the moderators", + "confirm_thread_response": "React to confirm thread creation which will directly contact the moderators", "confirm_thread_creation_accept": "\u2705", "confirm_thread_creation_deny": "\U0001F6AB", } diff --git a/core/config_help.json b/core/config_help.json index c986be3ded..27860c5b2e 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -404,6 +404,29 @@ "See also: `thread_move_title`, `thread_move_notify`." ] }, + "cooldown_thread_title": { + "default": "Message not sent!", + "description": "The title of the message embed when the user has a cooldown before creating a new thread.", + "examples": [ + "`{prefix}config set cooldown_thread_title Error`" + ], + "notes": [ + "Only has an effect when `thread_cooldown` is set", + "See also: `cooldown_thread_response`." + ] + }, + "cooldown_thread_response": { + "default": "You must wait for {delta} before you can contact me again.", + "description": "The description of the message embed when the user has a cooldown before creating a new thread.", + "examples": [ + "`{prefix}config set cooldown_thread_response Be patient! You are on cooldown, wait {delta} more." + ], + "notes": [ + "Only has an effect when `thread_cooldown` is set", + "Must have a {delta} included which will be replaced with the duration of time.", + "See also: `cooldown_thread_title`." + ] + }, "disabled_new_thread_title": { "default": "Not Delivered.", "description": "The title of the message embed when Modmail new thread creation is disabled and user tries to create a new thread.", @@ -597,7 +620,7 @@ "`{prefix}config set confirm_thread_creation yes" ], "notes": [ - "See also: `confirm_thread_creation_title`, `confirm_thread_creation_description`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" + "See also: `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" ] }, "confirm_thread_creation_title": { @@ -607,14 +630,14 @@ "`{prefix}config set confirm_thread_creation_title Are you sure you want to create a new thread?" ], "notes": [ - "See also: `confirm_thread_creation`, `confirm_thread_creation_description`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" + "See also: `confirm_thread_creation`, `confirm_thread_response`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" ] }, - "confirm_thread_creation_description": { + "confirm_thread_response": { "default": "React to confirm thread creation which will directly contact the moderators", "description": "Description for the embed message sent to users to confirm a thread creation", "examples":[ - "`{prefix}config set confirm_thread_creation_description React to confirm" + "`{prefix}config set confirm_thread_response React to confirm" ], "notes": [ "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" @@ -627,7 +650,7 @@ "`{prefix}config set confirm_thread_creation_accept \u2611" ], "notes": [ - "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_description`, confirm_thread_creation_deny`" + "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, confirm_thread_creation_deny`" ] }, "confirm_thread_creation_deny": { @@ -637,7 +660,7 @@ "`{prefix}config set confirm_thread_creation_deny \u26D4" ], "notes": [ - "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_description`, confirm_thread_creation_accept`" + "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, confirm_thread_creation_accept`" ] }, "modmail_guild_id": { diff --git a/core/thread.py b/core/thread.py index 2ec55b8214..0d8e61929d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1076,7 +1076,7 @@ async def create( confirm = await message.channel.send( embed=discord.Embed( title=self.bot.config["confirm_thread_creation_title"], - description=self.bot.config["confirm_thread_creation_description"], + description=self.bot.config["confirm_thread_response"], color=self.bot.main_color, ) ) From 06a0a87aa4b1f655d460028e97163eba46971e31 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Nov 2020 00:18:53 +0800 Subject: [PATCH 0644/1221] Add ?selfcontact resolves #2762 --- CHANGELOG.md | 1 + cogs/modmail.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74e5398b5e..90e4b7c9d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - If `?help` is sent, bot does checks on every command, `?help all` restores old behaviour. ([GH #2847](https://github.com/kyb3r/modmail/issues/2847)) - Added a way to block roles. ([GH #2753](https://github.com/kyb3r/modmail/issues/2753)) - Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/kyb3r/modmail/issues/2865)) +- Added `?selfcontact` to allow users to open a thread. ([GH #2762](https://github.com/kyb3r/modmail/issues/2762)) ### Fixed diff --git a/cogs/modmail.py b/cogs/modmail.py index 543a18af0f..01b60e6709 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -906,6 +906,11 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) + @commands.command() + @checks.has_permissions(PermissionLevel.REGULAR) + async def selfcontact(self, ctx): + await ctx.invoke(self.contact, user=ctx.author) + @commands.command(usage=" [category] [options]") @checks.has_permissions(PermissionLevel.SUPPORTER) async def contact( From 8da1e20246d81b35f44acd89f06e829f77f48b10 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Nov 2020 01:04:04 +0800 Subject: [PATCH 0645/1221] Fix bugs introduced in -dev3 --- CHANGELOG.md | 2 +- bot.py | 2 +- cogs/modmail.py | 2 +- cogs/utility.py | 2 +- core/config_help.json | 22 ++++++++++++---------- core/thread.py | 6 ++---- core/utils.py | 10 +++++++++- 7 files changed, 27 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e4b7c9d2..59b1d521f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.7.0-dev3 +# v3.7.0-dev4 ### Added diff --git a/bot.py b/bot.py index 0ffbd59b51..38cf135352 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev3" +__version__ = "3.7.0-dev4" import asyncio diff --git a/cogs/modmail.py b/cogs/modmail.py index 01b60e6709..d51d210f8a 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -988,7 +988,7 @@ async def contact( @commands.Cog.listener() async def on_raw_reaction_add(self, payload): - react_message_id = int(self.bot.config.get("react_to_contact_message")) + react_message_id = tryint(self.bot.config.get("react_to_contact_message")) react_message_emoji = self.bot.config.get("react_to_contact_emoji") if all((react_message_id, react_message_emoji)): if payload.message_id == react_message_id: diff --git a/cogs/utility.py b/cogs/utility.py index 8658767d9f..e355e2dd65 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -863,7 +863,7 @@ async def config_help(self, ctx, key: str.lower = None): return await ctx.send(embed=embed) def fmt(val): - return val.format(prefix=self.bot.prefix, bot=self.bot) + return val.format(prefix=self.bot.prefix, bot=self.bot, delta='{delta}') index = 0 embeds = [] diff --git a/core/config_help.json b/core/config_help.json index 27860c5b2e..32d8cb58ac 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -419,7 +419,7 @@ "default": "You must wait for {delta} before you can contact me again.", "description": "The description of the message embed when the user has a cooldown before creating a new thread.", "examples": [ - "`{prefix}config set cooldown_thread_response Be patient! You are on cooldown, wait {delta} more." + "`{prefix}config set cooldown_thread_response Be patient! You are on cooldown, wait {delta} more.`" ], "notes": [ "Only has an effect when `thread_cooldown` is set", @@ -591,7 +591,7 @@ "default": "Yes", "description": "Transfer users reactions to mods and vice versa", "examples":[ - "`{prefix}config set transfer_reactions no" + "`{prefix}config set transfer_reactions no`" ], "notes": [] }, @@ -599,7 +599,7 @@ "default": "No", "description": "Closes a modmail thread upon user leave automatically", "examples":[ - "`{prefix}config set close_on_leave yes" + "`{prefix}config set close_on_leave yes`" ], "notes": [] }, @@ -607,7 +607,7 @@ "default": "No", "description": "Mentions all mods (mention) in logs channel when bot is mentioned", "examples":[ - "`{prefix}config set alert_on_mention yes" + "`{prefix}config set alert_on_mention yes`" ], "notes": [ "See also: `mention`" @@ -617,7 +617,7 @@ "default": "No", "description": "Ensure users confirm that they want to create a new thread", "examples":[ - "`{prefix}config set confirm_thread_creation yes" + "`{prefix}config set confirm_thread_creation yes`" ], "notes": [ "See also: `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" @@ -627,7 +627,7 @@ "default": "Confirm thread creation", "description": "Title for the embed message sent to users to confirm a thread creation", "examples":[ - "`{prefix}config set confirm_thread_creation_title Are you sure you want to create a new thread?" + "`{prefix}config set confirm_thread_creation_title Are you sure you want to create a new thread?`" ], "notes": [ "See also: `confirm_thread_creation`, `confirm_thread_response`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" @@ -637,7 +637,7 @@ "default": "React to confirm thread creation which will directly contact the moderators", "description": "Description for the embed message sent to users to confirm a thread creation", "examples":[ - "`{prefix}config set confirm_thread_response React to confirm" + "`{prefix}config set confirm_thread_response React to confirm`" ], "notes": [ "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" @@ -647,19 +647,21 @@ "default": "\u2705", "description": "Emoji to accept a thread creation", "examples":[ - "`{prefix}config set confirm_thread_creation_accept \u2611" + "`{prefix}config set confirm_thread_creation_accept \u2611`" ], "notes": [ + "Has no effect unless `confirm_thread_creation` is set", "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, confirm_thread_creation_deny`" ] }, "confirm_thread_creation_deny": { - "default": "\u2705", + "default": "\uD83D\uDEAB", "description": "Emoji to accept deny thread creation", "examples":[ - "`{prefix}config set confirm_thread_creation_deny \u26D4" + "`{prefix}config set confirm_thread_creation_deny \u26D4`" ], "notes": [ + "Has no effect unless `confirm_thread_creation` is set", "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, confirm_thread_creation_accept`" ] }, diff --git a/core/thread.py b/core/thread.py index 0d8e61929d..1d1fc2f65c 100644 --- a/core/thread.py +++ b/core/thread.py @@ -50,7 +50,7 @@ def __repr__(self): async def wait_until_ready(self) -> None: """Blocks execution until the thread is fully set up.""" # timeout after 30 seconds - task = asyncio.create_task(asyncio.wait_for(self._ready_event.wait(), timeout=20)) + task = asyncio.create_task(asyncio.wait_for(self._ready_event.wait(), timeout=25)) self.wait_tasks.append(task) try: await task @@ -346,8 +346,6 @@ async def _close( }, }, ) - else: - log_data = None if isinstance(log_data, dict): prefix = self.bot.config["log_url_prefix"].strip("/") @@ -393,7 +391,7 @@ async def _close( tasks = [self.bot.config.update()] - if self.bot.log_channel is not None: + if self.bot.log_channel is not None and self.channel is not None: tasks.append(self.bot.log_channel.send(embed=embed)) # Thread closed message diff --git a/core/utils.py b/core/utils.py index a648f04f1a..d3ddb2fccd 100644 --- a/core/utils.py +++ b/core/utils.py @@ -29,6 +29,7 @@ "trigger_typing", "escape_code_block", "format_channel_name", + "tryint", ] @@ -117,7 +118,7 @@ def format_preview(messages: typing.List[typing.Dict[str, typing.Any]]): return out or "No Messages" -def is_image_url(url: str, **kwargs) -> bool: +def is_image_url(url: str, **kwargs) -> str: """ Check if the URL is pointing to an image. @@ -327,3 +328,10 @@ def format_channel_name(author, guild, exclude_channel=None): counter += 1 return new_name + + +def tryint(x): + try: + return int(x) + except ValueError: + return x From 5dea0bd32de1ab0d47de4503f82e395697ecb87b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Nov 2020 01:10:20 +0800 Subject: [PATCH 0646/1221] More elegant solution for unseenformatter --- cogs/utility.py | 17 ++++++++--------- core/models.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index e355e2dd65..0e9aaac753 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1,31 +1,30 @@ import asyncio import inspect import os -import traceback import random +import traceback from contextlib import redirect_stdout from datetime import datetime from difflib import get_close_matches -from io import StringIO, BytesIO -from itertools import zip_longest, takewhile +from io import BytesIO, StringIO +from itertools import takewhile, zip_longest from json import JSONDecodeError, loads from textwrap import indent from types import SimpleNamespace from typing import Union import discord +from aiohttp import ClientResponseError from discord.enums import ActivityType, Status from discord.ext import commands, tasks from discord.ext.commands.view import StringView - -from aiohttp import ClientResponseError from pkg_resources import parse_version -from core import checks +from core import checks, utils from core.changelog import Changelog -from core.models import InvalidConfigError, PermissionLevel, getLogger +from core.models import InvalidConfigError, PermissionLevel, UnseenFormatter, getLogger from core.paginator import EmbedPaginatorSession, MessagePaginatorSession -from core import utils + logger = getLogger(__name__) @@ -863,7 +862,7 @@ async def config_help(self, ctx, key: str.lower = None): return await ctx.send(embed=embed) def fmt(val): - return val.format(prefix=self.bot.prefix, bot=self.bot, delta='{delta}') + return UnseenFormatter().format(val, prefix=self.bot.prefix, bot=self.bot) index = 0 embeds = [] diff --git a/core/models.py b/core/models.py index e19086b198..12365f24da 100644 --- a/core/models.py +++ b/core/models.py @@ -174,3 +174,14 @@ def get_field(self, field_name, args, kwargs): except (IndexError, KeyError): pass return "", first + + +class UnseenFormatter(Formatter): + def get_value(self, key, args, kwds): + if isinstance(key, str): + try: + return kwds[key] + except KeyError: + return "{" + key + "}" + else: + return Formatter.get_value(key, args, kwds) From 91153d33449fba3fba012057478476727f7318be Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Nov 2020 01:16:24 +0800 Subject: [PATCH 0647/1221] Fix errors in -v3/4 --- bot.py | 2 +- core/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 38cf135352..c06c8914a7 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev4" +__version__ = "3.7.0-dev5" import asyncio diff --git a/core/utils.py b/core/utils.py index d3ddb2fccd..72c96c00f6 100644 --- a/core/utils.py +++ b/core/utils.py @@ -333,5 +333,5 @@ def format_channel_name(author, guild, exclude_channel=None): def tryint(x): try: return int(x) - except ValueError: + except (ValueError, TypeError): return x From 55bf442df3de9da2be3916471b342df3f331cbad Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Nov 2020 01:17:05 +0800 Subject: [PATCH 0648/1221] up version in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b1d521f1..c8e5cc6da6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.7.0-dev4 +# v3.7.0-dev5 ### Added From 3657ad1efca7c61cecf3ce18f0fcf5a68826dd62 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Nov 2020 13:19:41 +0800 Subject: [PATCH 0649/1221] Close on thread reason config var-dev6 --- CHANGELOG.md | 4 ++-- bot.py | 6 +++--- core/config.py | 1 + core/config_help.json | 19 ++++++++++++++++--- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e5cc6da6..3317ecd03a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.7.0-dev5 +# v3.7.0-dev6 ### Added @@ -14,7 +14,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `react_to_contact_message`, `react_to_contact_emoji` to allow users to create threads by reacting to a message. - Added `thread_move_notify_mods` to mention all mods again after moving thread. ([GH #215](https://github.com/kyb3r/modmail/issues/215)) - Added `transfer_reactions` to link reactions between mods and users. ([GH #2763](https://github.com/kyb3r/modmail/issues/2763)) -- Added `close_on_leave` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/kyb3r/modmail/issues/2757)) +- Added `close_on_leave`, `close_on_leave_reason` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/kyb3r/modmail/issues/2757)) - Added `alert_on_mention` to mention mods upon a bot mention. ([GH #2833](https://github.com/kyb3r/modmail/issues/2833)) - Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/kyb3r/modmail/issues/2773)) - Support Gyazo image links in message embeds. ([GH #282](https://github.com/kyb3r/modmail/issues/282)) diff --git a/bot.py b/bot.py index c06c8914a7..57a511233e 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev5" +__version__ = "3.7.0-dev6" import asyncio @@ -1118,12 +1118,12 @@ async def on_member_remove(self, member): if self.config["close_on_leave"]: await thread.close( closer=member.guild.me, - message="The recipient has left the server.", + message=self.config["close_on_leave_reason"], silent=True, ) else: embed = discord.Embed( - description="The recipient has left the server.", color=self.error_color + description=self.config["close_on_leave_reason"], color=self.error_color ) await thread.channel.send(embed=embed) diff --git a/core/config.py b/core/config.py index c4570f93f9..91c50d66f5 100644 --- a/core/config.py +++ b/core/config.py @@ -72,6 +72,7 @@ class ConfigManager: "disabled_current_thread_footer": "Please try again later...", "transfer_reactions": True, "close_on_leave": False, + "close_on_leave_reason": "The recipient has left the server.", "alert_on_mention": False, # moderation "recipient_color": str(discord.Color.gold()), diff --git a/core/config_help.json b/core/config_help.json index 32d8cb58ac..cc941048c5 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -601,7 +601,20 @@ "examples":[ "`{prefix}config set close_on_leave yes`" ], - "notes": [] + "notes": [ + "See also: `close_on_leave_reason`." + ] + }, + "close_on_leave_reason": { + "default": "The recipient has left the server.", + "description": "Reason for closing the thread once member leaves", + "examples":[ + "`{prefix}config set close_on_leave_reason Member left`" + ], + "notes": [ + "This has no effect unless `close_on_leave` is set.", + "See also: `close_on_leave`." + ] }, "alert_on_mention": { "default": "No", @@ -650,7 +663,7 @@ "`{prefix}config set confirm_thread_creation_accept \u2611`" ], "notes": [ - "Has no effect unless `confirm_thread_creation` is set", + "This has no effect unless `confirm_thread_creation` is set", "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, confirm_thread_creation_deny`" ] }, @@ -661,7 +674,7 @@ "`{prefix}config set confirm_thread_creation_deny \u26D4`" ], "notes": [ - "Has no effect unless `confirm_thread_creation` is set", + "This has no effect unless `confirm_thread_creation` is set", "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, confirm_thread_creation_accept`" ] }, From 2aca7ff9fc6066ed490c43ce70439a7bf89c0945 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Nov 2020 01:15:08 +0800 Subject: [PATCH 0650/1221] Custom dpy version with stickers, support stickers/pins --- Pipfile | 29 +-- Pipfile.lock | 416 +++++++++++++++++++--------------------- bot.py | 3 + core/thread.py | 12 +- discord.py-1.5.2.tar.gz | Bin 0 -> 649387 bytes pyproject.toml | 2 +- requirements.min.txt | 2 +- 7 files changed, 230 insertions(+), 234 deletions(-) create mode 100644 discord.py-1.5.2.tar.gz diff --git a/Pipfile b/Pipfile index 01485b1e63..fb20f42c21 100644 --- a/Pipfile +++ b/Pipfile @@ -10,19 +10,26 @@ bandit = "==1.6.2" flake8 = "*" [packages] -colorama = ">=0.4.0" -python-dateutil = ">=2.7.0" -emoji = ">=0.2" -uvloop = {version = ">=0.12.0",sys_platform = "!= 'win32'"} -motor = ">=2.0.0" +aiohttp = "==3.6.2" +async-timeout = "==3.0.1" +attrs = "==19.3.0" +chardet = "==3.0.4" +"discord.py" = {path = "discord.py-1.5.2.tar.gz"} +dnspython = "==1.16.0" +emoji = "==0.5.4" +future = "==0.18.2" +idna = "==2.9" +isodate = "==0.6.0" +motor = "==2.1.0" +multidict = "==4.7.6" natural = "==0.2.0" -isodate = ">=0.6.0" -dnspython = "~=1.16.0" parsedatetime = "==2.6" -aiohttp = ">=3.6.0,<3.7.0" -python-dotenv = ">=0.10.3" -pipenv = "*" -"discord.py" = "==1.5.1" +pymongo = "==3.10.1" +python-dateutil = "==2.8.1" +python-dotenv = "==0.14.0" +six = "==1.15.0" +websockets = "==8.1" +yarl = "==1.4.2" [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 005ce37f67..d88503c8d8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0491618cb8bd6d70e4ab3337c23b72fc3b1d5f9ca0603d8be6b890b661039102" + "sha256": "2be2574267d4b75a621fe0a7202531936a02f5bc0cd0a79addf757344a3acb4d" }, "pipfile-spec": 6, "requires": {}, @@ -16,82 +16,55 @@ "default": { "aiohttp": { "hashes": [ - "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe", - "sha256:206c0ccfcea46e1bddc91162449c20c72f308aebdcef4977420ef329c8fcc599", - "sha256:2ad493de47a8f926386fa6d256832de3095ba285f325db917c7deae0b54a9fc8", - "sha256:319b490a5e2beaf06891f6711856ea10591cfe84fe9f3e71a721aa8f20a0872a", - "sha256:470e4c90da36b601676fe50c49a60d34eb8c6593780930b1aa4eea6f508dfa37", - "sha256:60f4caa3b7f7a477f66ccdd158e06901e1d235d572283906276e3803f6b098f5", - "sha256:66d64486172b032db19ea8522328b19cfb78a3e1e5b62ab6a0567f93f073dea0", - "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c", - "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645", - "sha256:797456399ffeef73172945708810f3277f794965eb6ec9bd3a0c007c0476be98", - "sha256:a885432d3cabc1287bcf88ea94e1826d3aec57fd5da4a586afae4591b061d40d", - "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81", - "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5" + "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", + "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", + "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", + "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", + "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", + "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", + "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", + "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", + "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", + "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", + "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", + "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" ], "index": "pypi", - "version": "==3.6.3" - }, - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" + "version": "==3.6.2" }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", + "index": "pypi", "version": "==3.0.1" }, "attrs": { "hashes": [ - "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", - "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.2.0" - }, - "certifi": { - "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" - ], - "version": "==2020.6.20" + "index": "pypi", + "version": "==19.3.0" }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" ], + "index": "pypi", "version": "==3.0.4" }, - "colorama": { + "discord-py": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:5aac5a89eb8d70ba7bb5721ad9b495b437cc46328bab6a9170a4e0f8a568a9e8" ], - "index": "pypi", - "version": "==0.4.4" + "path": "./discord.py-1.5.2.tar.gz", + "version": "==1.5.2" }, "discord.py": { - "hashes": [ - "sha256:2367359e31f6527f8a936751fc20b09d7495dd6a76b28c8fb13d4ca6c55b7563", - "sha256:def00dc50cf36d21346d71bc89f0cad8f18f9a3522978dc18c7796287d47de8b" - ], - "index": "pypi", - "version": "==1.5.1" - }, - "distlib": { - "hashes": [ - "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", - "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" - ], - "version": "==0.3.1" + "path": "discord.py-1.5.2.tar.gz" }, "dnspython": { "hashes": [ @@ -103,25 +76,25 @@ }, "emoji": { "hashes": [ - "sha256:e42da4f8d648f8ef10691bc246f682a1ec6b18373abfd9be10ec0b398823bd11" + "sha256:60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174" ], "index": "pypi", - "version": "==0.6.0" + "version": "==0.5.4" }, - "filelock": { + "future": { "hashes": [ - "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", - "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" ], - "version": "==3.0.12" + "index": "pypi", + "version": "==0.18.2" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" + "index": "pypi", + "version": "==2.9" }, "isodate": { "hashes": [ @@ -133,11 +106,12 @@ }, "motor": { "hashes": [ - "sha256:428d94750123d19fcd0a89b8671ff9b4656f205217bad9f44161748c64c5fc80", - "sha256:f1692b760d834707e3477996ce8d407af8cd61c1a2abedbf81c22ef14675e61a" + "sha256:599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909", + "sha256:756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4", + "sha256:97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a" ], "index": "pypi", - "version": "==2.3.0" + "version": "==2.1.0" }, "multidict": { "hashes": [ @@ -159,7 +133,7 @@ "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], - "markers": "python_version >= '3.5'", + "index": "pypi", "version": "==4.7.6" }, "natural": { @@ -177,73 +151,67 @@ "index": "pypi", "version": "==2.6" }, - "pipenv": { - "hashes": [ - "sha256:448ac3a36443db633d52a2359cac15ecbc4f429eab4ddd420697602b721d1c5a", - "sha256:eff0e10eadb330f612edfa5051d3d8e775e9e0e918c3c50361da703bd0daa035" - ], - "index": "pypi", - "version": "==2020.8.13" - }, "pymongo": { "hashes": [ - "sha256:03dc64a9aa7a5d405aea5c56db95835f6a2fa31b3502c5af1760e0e99210be30", - "sha256:05fcc6f9c60e6efe5219fbb5a30258adb3d3e5cbd317068f3d73c09727f2abb6", - "sha256:076a7f2f7c251635cf6116ac8e45eefac77758ee5a77ab7bd2f63999e957613b", - "sha256:137e6fa718c7eff270dbd2fc4b90d94b1a69c9e9eb3f3de9e850a7fd33c822dc", - "sha256:1f865b1d1c191d785106f54df9abdc7d2f45a946b45fd1ea0a641b4f982a2a77", - "sha256:213c445fe7e654621c6309e874627c35354b46ef3ee807f5a1927dc4b30e1a67", - "sha256:25e617daf47d8dfd4e152c880cd0741cbdb48e51f54b8de9ddbfe74ecd87dd16", - "sha256:3d9bb1ba935a90ec4809a8031efd988bdb13cdba05d9e9a3e9bf151bf759ecde", - "sha256:40696a9a53faa7d85aaa6fd7bef1cae08f7882640bad08c350fb59dee7ad069b", - "sha256:421aa1b92c291c429668bd8d8d8ec2bd00f183483a756928e3afbf2b6f941f00", - "sha256:4437300eb3a5e9cc1a73b07d22c77302f872f339caca97e9bf8cf45eca8fa0d2", - "sha256:455f4deb00158d5ec8b1d3092df6abb681b225774ab8a59b3510293b4c8530e3", - "sha256:475a34a0745c456ceffaec4ce86b7e0983478f1b6140890dff7b161e7bcd895b", - "sha256:4797c0080f41eba90404335e5ded3aa66731d303293a675ff097ce4ea3025bb9", - "sha256:4ae23fbbe9eadf61279a26eba866bbf161a6f7e2ffad14a42cf20e9cb8e94166", - "sha256:4b32744901ee9990aa8cd488ec85634f443526def1e5190a407dc107148249d7", - "sha256:50127b13b38e8e586d5e97d342689405edbd74ad0bd891d97ee126a8c7b6e45f", - "sha256:50531caa7b4be1c4ed5e2d5793a4e51cc9bd62a919a6fd3299ef7c902e206eab", - "sha256:63a5387e496a98170ffe638b435c0832c0f2011a6f4ff7a2880f17669fff8c03", - "sha256:68220b81850de8e966d4667d5c325a96c6ac0d6adb3d18935d6e3d325d441f48", - "sha256:689142dc0c150e9cb7c012d84cac2c346d40beb891323afb6caf18ec4caafae0", - "sha256:6a15e2bee5c4188369a87ed6f02de804651152634a46cca91966a11c8abd2550", - "sha256:7122ffe597b531fb065d3314e704a6fe152b81820ca5f38543e70ffcc95ecfd4", - "sha256:7307024b18266b302f4265da84bb1effb5d18999ef35b30d17592959568d5c0a", - "sha256:7a4a6f5b818988a3917ec4baa91d1143242bdfece8d38305020463955961266a", - "sha256:83c5a3ecd96a9f3f11cfe6dfcbcec7323265340eb24cc996acaecea129865a3a", - "sha256:890b0f1e18dbd898aeb0ab9eae1ab159c6bcbe87f0abb065b0044581d8614062", - "sha256:8deda1f7b4c03242f2a8037706d9584e703f3d8c74d6d9cac5833db36fe16c42", - "sha256:8ea13d0348b4c96b437d944d7068d59ed4a6c98aaa6c40d8537a2981313f1c66", - "sha256:91e96bf85b7c07c827d339a386e8a3cf2e90ef098c42595227f729922d0851df", - "sha256:96782ebb3c9e91e174c333208b272ea144ed2a684413afb1038e3b3342230d72", - "sha256:9755c726aa6788f076114dfdc03b92b03ff8860316cca00902cce88bcdb5fedd", - "sha256:9dbab90c348c512e03f146e93a5e2610acec76df391043ecd46b6b775d5397e6", - "sha256:9ee0eef254e340cc11c379f797af3977992a7f2c176f1a658740c94bf677e13c", - "sha256:9fc17fdac8f1973850d42e51e8ba6149d93b1993ed6768a24f352f926dd3d587", - "sha256:a2787319dc69854acdfd6452e6a8ba8f929aeb20843c7f090e04159fc18e6245", - "sha256:b7c522292407fa04d8195032493aac937e253ad9ae524aab43b9d9d242571f03", - "sha256:bd312794f51e37dcf77f013d40650fe4fbb211dd55ef2863839c37480bd44369", - "sha256:c0d660a186e36c526366edf8a64391874fe53cf8b7039224137aee0163c046df", - "sha256:c4869141e20769b65d2d72686e7a7eb141ce9f3168106bed3e7dcced54eb2422", - "sha256:cc4057f692ac35bbe82a0a908d42ce3a281c9e913290fac37d7fa3bd01307dfb", - "sha256:cccf1e7806f12300e3a3b48f219e111000c2538483e85c869c35c1ae591e6ce9", - "sha256:ce208f80f398522e49d9db789065c8ad2cd37b21bd6b23d30053474b7416af11", - "sha256:d0565481dc196986c484a7fb13214fc6402201f7fb55c65fd215b3324962fe6c", - "sha256:d1b3366329c45a474b3bbc9b9c95d4c686e03f35da7fd12bc144626d1f2a7c04", - "sha256:d226e0d4b9192d95079a9a29c04dd81816b1ce8903b8c174a39224fe978547cb", - "sha256:d38b35f6eef4237b1d0d8e845fc1546dad85c55eba447e28c211da8c7ef9697c", - "sha256:d64c98277ea80e4484f1332ab107e8dfd173a7dcf1bdbf10a9cccc97aaab145f", - "sha256:d9de8427a5601799784eb0e7fa1b031aa64086ce04de29df775a8ca37eedac41", - "sha256:e6a15cf8f887d9f578dd49c6fb3a99d53e1d922fdd67a245a67488d77bf56eb2", - "sha256:e8c446882cbb3774cd78c738c9f58220606b702b7c1655f1423357dc51674054", - "sha256:e8d188ee39bd0ffe76603da887706e4e7b471f613625899ddf1e27867dc6a0d3", - "sha256:ef76535776c0708a85258f6dc51d36a2df12633c735f6d197ed7dfcaa7449b99", - "sha256:f6efca006a81e1197b925a7d7b16b8f61980697bb6746587aad8842865233218" + "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", + "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", + "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", + "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", + "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", + "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103", + "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", + "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", + "sha256:2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", + "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", + "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", + "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", + "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", + "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", + "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", + "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", + "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", + "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", + "sha256:4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", + "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", + "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", + "sha256:56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", + "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", + "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", + "sha256:619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", + "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", + "sha256:63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113", + "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", + "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", + "sha256:7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae", + "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", + "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", + "sha256:993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", + "sha256:9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", + "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", + "sha256:a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", + "sha256:a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", + "sha256:a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", + "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", + "sha256:ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012", + "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", + "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", + "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", + "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", + "sha256:bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a", + "sha256:c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", + "sha256:c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", + "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", + "sha256:c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", + "sha256:da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", + "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", + "sha256:e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", + "sha256:e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", + "sha256:e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", + "sha256:f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a", + "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==3.11.0" + "index": "pypi", + "version": "==3.10.1" }, "python-dateutil": { "hashes": [ @@ -266,62 +234,59 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "index": "pypi", "version": "==1.15.0" }, - "uvloop": { - "hashes": [ - "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", - "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", - "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", - "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", - "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", - "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", - "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", - "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", - "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" - ], - "markers": "sys_platform != 'win32'", - "version": "==0.14.0" - }, - "virtualenv": { - "hashes": [ - "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", - "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" + "websockets": { + "hashes": [ + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.1.0" - }, - "virtualenv-clone": { - "hashes": [ - "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", - "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.5.4" + "index": "pypi", + "version": "==8.1" }, "yarl": { "hashes": [ - "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", - "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", - "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", - "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", - "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", - "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", - "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", - "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", - "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", - "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", - "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", - "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", - "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", - "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", - "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", - "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", - "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + "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" ], - "markers": "python_version >= '3.5'", - "version": "==1.5.1" + "index": "pypi", + "version": "==1.4.2" } }, "develop": { @@ -342,11 +307,11 @@ }, "attrs": { "hashes": [ - "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", - "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.2.0" + "index": "pypi", + "version": "==19.3.0" }, "bandit": { "hashes": [ @@ -377,7 +342,7 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "index": "pypi", + "markers": "platform_system == 'Windows' and sys_platform == 'win32'", "version": "==0.4.4" }, "flake8": { @@ -503,42 +468,58 @@ }, "regex": { "hashes": [ - "sha256:0cb23ed0e327c18fb7eac61ebbb3180ebafed5b9b86ca2e15438201e5903b5dd", - "sha256:1a065e7a6a1b4aa851a0efa1a2579eabc765246b8b3a5fd74000aaa3134b8b4e", - "sha256:1a511470db3aa97432ac8c1bf014fcc6c9fbfd0f4b1313024d342549cf86bcd6", - "sha256:1c447b0d108cddc69036b1b3910fac159f2b51fdeec7f13872e059b7bc932be1", - "sha256:2278453c6a76280b38855a263198961938108ea2333ee145c5168c36b8e2b376", - "sha256:240509721a663836b611fa13ca1843079fc52d0b91ef3f92d9bba8da12e768a0", - "sha256:4e21340c07090ddc8c16deebfd82eb9c9e1ec5e62f57bb86194a2595fd7b46e0", - "sha256:570e916a44a361d4e85f355aacd90e9113319c78ce3c2d098d2ddf9631b34505", - "sha256:59d5c6302d22c16d59611a9fd53556554010db1d47e9df5df37be05007bebe75", - "sha256:6a46eba253cedcbe8a6469f881f014f0a98819d99d341461630885139850e281", - "sha256:6f567df0601e9c7434958143aebea47a9c4b45434ea0ae0286a4ec19e9877169", - "sha256:781906e45ef1d10a0ed9ec8ab83a09b5e0d742de70e627b20d61ccb1b1d3964d", - "sha256:8469377a437dbc31e480993399fd1fd15fe26f382dc04c51c9cb73e42965cc06", - "sha256:8cd0d587aaac74194ad3e68029124c06245acaeddaae14cb45844e5c9bebeea4", - "sha256:97a023f97cddf00831ba04886d1596ef10f59b93df7f855856f037190936e868", - "sha256:a973d5a7a324e2a5230ad7c43f5e1383cac51ef4903bf274936a5634b724b531", - "sha256:af360e62a9790e0a96bc9ac845d87bfa0e4ee0ee68547ae8b5a9c1030517dbef", - "sha256:b706c70070eea03411b1761fff3a2675da28d042a1ab7d0863b3efe1faa125c9", - "sha256:bfd7a9fddd11d116a58b62ee6c502fd24cfe22a4792261f258f886aa41c2a899", - "sha256:c30d8766a055c22e39dd7e1a4f98f6266169f2de05db737efe509c2fb9c8a3c8", - "sha256:c53dc8ee3bb7b7e28ee9feb996a0c999137be6c1d3b02cb6b3c4cba4f9e5ed09", - "sha256:c95d514093b80e5309bdca5dd99e51bcf82c44043b57c34594d9d7556bd04d05", - "sha256:d43cf21df524283daa80ecad551c306b7f52881c8d0fe4e3e76a96b626b6d8d8", - "sha256:d62205f00f461fe8b24ade07499454a3b7adf3def1225e258b994e2215fd15c5", - "sha256:e289a857dca3b35d3615c3a6a438622e20d1bf0abcb82c57d866c8d0be3f44c4", - "sha256:e5f6aa56dda92472e9d6f7b1e6331f4e2d51a67caafff4d4c5121cadac03941e", - "sha256:f4b1c65ee86bfbf7d0c3dfd90592a9e3d6e9ecd36c367c884094c050d4c35d04" - ], - "version": "==2020.10.23" + "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a", + "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f", + "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb", + "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5", + "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de", + "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c", + "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0", + "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c", + "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64", + "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53", + "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12", + "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740", + "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c", + "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd", + "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504", + "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427", + "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b", + "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e", + "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582", + "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0", + "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c", + "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9", + "sha256:9b6305295b6591e45f069d3553c54d50cc47629eb5c218aac99e0f7fafbf90a1", + "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0", + "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf", + "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898", + "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd", + "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d", + "sha256:bd904c0dec29bbd0769887a816657491721d5f545c29e30fd9d7a1a275dc80ab", + "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f", + "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e", + "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786", + "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b", + "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de", + "sha256:c454ad88e56e80e44f824ef8366bb7e4c3def12999151fd5c0ea76a18fe9aa3e", + "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789", + "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520", + "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa", + "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b", + "sha256:de7fd57765398d141949946c84f3590a68cf5887dac3fc52388df0639b01eda4", + "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625", + "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d", + "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26" + ], + "version": "==2020.10.28" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "index": "pypi", "version": "==1.15.0" }, "smmap": { @@ -559,10 +540,11 @@ }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "version": "==0.10.1" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" }, "typed-ast": { "hashes": [ diff --git a/bot.py b/bot.py index 57a511233e..daf1f593c1 100644 --- a/bot.py +++ b/bot.py @@ -746,6 +746,9 @@ async def process_dm_modmail(self, message: discord.Message) -> None: if blocked: return sent_emoji, blocked_emoji = await self.retrieve_emoji() + + if message.type != discord.MessageType.default: + return thread = await self.threads.find(recipient=message.author) if thread is None: diff --git a/core/thread.py b/core/thread.py index 1d1fc2f65c..426b76489d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -780,7 +780,7 @@ async def send( url=f"https://discordapp.com/users/{author.id}#{message.id}", ) - ext = [(a.url, a.filename) for a in message.attachments] + ext = [(a.url, a.filename, False) for a in message.attachments] images = [] attachments = [] @@ -796,11 +796,12 @@ async def send( ) image_urls = [ - (is_image_url(url, convert_size=False), None) + (is_image_url(url, convert_size=False), None, False) for url in image_urls if is_image_url(url, convert_size=False) ] images.extend(image_urls) + images.extend((str(i.image_url), f'{i.name} Sticker', True) for i in message.stickers) embedded_image = False @@ -809,11 +810,14 @@ async def send( additional_images = [] additional_count = 1 - for url, filename in images: + for url, filename, is_sticker in images: 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})") + if is_sticker: + embed.add_field(name=filename, value=f"\u200b") + else: + embed.add_field(name="Image", value=f"[{filename}]({url})") embedded_image = True elif filename is not None: if note: diff --git a/discord.py-1.5.2.tar.gz b/discord.py-1.5.2.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..79619c9a8e36cfde75e22f715d15cf4a4bdcbdc0 GIT binary patch literal 649387 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
    9I&RraX-kD2 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

    -O5Ah2?Whk$Eqb;`t%vP zI#g{zUod%=vZdKp4_9fo-a{Z6&!@IO|5?f7XFrhW3@cpGp z3>Vvg5?-4mv<1JPWTJ-0mrKI3Jj~$eB(Wwb{=GX%v?L9$pSTQafaTygV(m;Z-^ky) zg3k`ZzUEnzk%eWwNhA^vqMCCluWvAWsP{orNt1CG1F&ZYAsSbFi8ar0Lbfu7&Nc3t zLb88_Xu}Z|aB+HvUc|C}eDJYm#9=?igrI&hwd6s`Ccm&ADsX>kyJl=`;Ed$e z^;doG%8duuqH`CT3>a!&CTKXIoV}b(-FnL(fxZ3b`8+5rmcHgDiWi*d6%@ z-J$W{F$ga}I3VFww_A~p(|dZ~OgD@{Ks-Cc=4t+?#Y719@se$}7of;IU_KaCTgqVrf&X~d(AV4-= zaMidSsn^Zdqp!M=*xVs#vh2$s9+o(2rU9#C2m~iEqKon>qHBTh8`QOs;n--J@@qzU zS!&M6PSR#YFYmZiFAwH43{w2A0`Y}2^=0#QK`mupf;ZTAo#}69at^JS_9rPB9;wY+KpA~9FS9U>KjMUP{+97 z&k5-0k717NwLe6EG*~?K(fwhDF9g>r3OJ5tH0i1)9`ht)+RD8I;xs~cg=O@o(&45n zt)HGLc!obN`m`!ZWlk!q+(|_NloLf!80ZiB5|Lk@gvZTZ#E;Q@=R_rQ<%;^f7#GUK zl^m;N*U7GxSHl#PjhTjXW&aoHlAiN)QPVZ7#<8 z)2kCky{axOLw-BFdx$!;j63vK;6VcfN00TgNbps6zeD&yrsy|zP;PX<10nwJQzr|R zHq3fTj}xCpovq3To3_f$(L4RIBeh@C!n@v`q->cu}fpRpyG( zv_{@13M;y6s_#_fWtieTX%otHVW|JAWx@;q_!ozrH7px`Rxc zkG7bPlD#g!{9?O0u5F&~*fRXdQ)c;GU46uc$8&YZqu8OU@0~S|vWUCj#(Ikhw0u8(Cn6B3!klQ`CRp>;1R9L%#SEMuZ}ool!2Zv(^ zVWx=g)y}Z41eIBnCy3j1IcF&tV_R5X^O_HpwJWc(w+j1N{EJKt$=kJSiH?ka*=~cf zM>Ab^;&n#~MG{X0l%@e*XH;aYTWt4c>6BldQM8`AR-}k+(H!|it7`&_6@3ZxsAO8| zu5qdHp9(Q@mk4kV|FyVS4}_;(#unhbiq@L3lSw}Sox62XSJ;K|A`E|!y_+pR z-h?@Phg~E~g~Z=be@`28ElslsJtdxMCX~d&J}Uvrj5wc;JOMdov?(UM#1edny1@4A zN4*uiCx`dPatQ#jnVN93&NNq^Iwq$&Gh_MA$VW8rl*vFXqrsQucoDO08)l}*Zke{I z?kX`8KMrP07<_?t4B+n_qv7wP0{(VK4qN=QP_kT4m4M!Fv#}lw)B{XU{Z-}cLk+Ca0*tF!iHkP4K)EIjXE4^x zEV3vsH_I!`@+Zym-bfghLHDE3}GLHqDam9i_%eyNNq>5W3-LhSr5U9A9b(|C(R82#c7W4P+T zpkyNqv-Aj{b+HM>nG84P7ofl0AjZvVTr-*nC1LVyw&yQB2%Rgkgl7Du$7vJl006*K z028n?;jL@hlFS6K=-^bP*;JB-u?SW88c$?Pq3Gj2M zD$s%pjaiR~_}yiLfwKn!$-{Qq&LMA)CN&`+SA3P*6EGZ;!6`A*Le$Ny*|!>wJe7)w zP+s~D9?h9aSMU2{8R7v5OnQ#h{7Xo0Eltodguu%s6UDFYA~NGs2SkHOdY*v*4l|da zGg5`=b~##!{HNChWkB!sLx6&JGPJl=tylq@f4dhEImjZ#ajX2F6x(qn2ySKi5+R8)^X!<5W5B8W}%}c@*2+K zOoZ$SND1UY_if-Amt(y{?0r~#5;iu+!IQY=eCjt=OUVB6v0eD&lm$4R0hTwY9}ZpH zMxf_)VOh|=&~WU6(GRr<54ZyQEdY{0ZNC~zed(b8obD@C5b$AKyXk*_q(?+3(0hVZ zh07-P08u*FuGCkjOEbRN&!Oq)?q5*b2C+?Lm)OU&o*0}2EiGLvr9YKDRP}kfnKnk; zR62lC&31{KtLTXAq4hjlt;n7a&=tQvgz5fF=^Q#K-+hA0S_FSu)l1hqQPxz=mJQKI zKw`H0V`l)3JLLKyGXr}$?1yH9r$T=wb9X=|?4$~4sFiO}J8h!UiY5S1C$Bb*YHoqz zYgw~9)_!uHTsZ3!Irf&Ndew}D{eJj12WwgYq4?{8=q{QOs-hsk zpr3v{? zrXhIiq3{6hxkx*J-6?G8cZ43v#Hb#DQDG0z#0m+4Ry`J4^;l@tW1&@#g;qTlTJ>0H z)nlU7x}B-f1Sb}AC;psbtr4|%H_b{_pIFS>uwEqzdHvTU4nS4X|KK;Vi1M8`PVAR# zH46aM-s1Qd=^3n{Wh>tv+K$+UG!KQoi#K2T1tMa_vZWUcakQhz3rK!Z%+)D7B(ZUy z+u6&hY+hyXSX)B-r7949lVH$Z)q75pb_a~bZxv+aovmh?wnPD>p?%n18Yl%U85ZjM zXQfzsbi}k^UwuOdT2hdbt%5~dJ zJ-9Xq5y(fJJzM=L3>TTgY~Dnl9ZToXgVA$&a^s^cxm+;F%kTAwNTT6X#*>n)a6(21yov|K_L^`2Fen*$cCO(tKHiXP_VI^Ws8R z&U$g*11w%LQ4snM^P1cEZzYDX+Z<0hZn}jlK!tdSSAa*#`!88EXpR}3@6L{MJ&hB_ z={ChjETtHz*@Ux>{uHe4#wrAu0ux5vYKN+_*PECth@pfr-1JOq*hA@<@(jk^w|7*576th$q4>PTl4miv7jHkSHPG%2=RQ# zn+)h}+5Ihh_E5#Htc|BI=EUBrFjayTO=v=6GZIJ8*B04J&BNXkNiMS;h&(JJnS$s~ z*aXqP)+UI47LIz;!dq=^9}C&1$(MEw2ZK7(Hf( zYhW6aH#Dn^8EgKK2cZS$7eKX7rV=FN?jH|@t5{pYrE^%gw$t9MRm|S9ciOD4{D>1pm z6}X5KmHib{j#whN!;TV@ZZ8pOo{$A@p5fS`8d{NRxH&{(vNunapP`oN+vuzO<1&7W zNi(+olKrD@7@sY1%JrTC$S5rd3`^j~+H5PoqlLTmgVwEP%*`O}C&7E763~r+o(y>R zALHQIbdqK`eFys-M**y1&rG>A-xBAuf0h&?j%HYEPoTxRtRO~oF_tA zb)J&1PgcBtNht$l}5U#)fG@unkfVZ2F+jUEPPp?1a_DgLGNFNdWxm zF2X}p)oubO+MrxAaS0VZbh%uzpZ4%WGQH5r`*a5kUYZ0PVuQc7$o5e9Q^>c>V{yIS)CyX%X!44^n8U0iyQy)DdAV+KL$AudT%~0 z%UHpA(YYAI@KZA$1)0udj`EU+RU$n+sWol{7b;fZSb0ZiDD$$1CwG8O%F=okdZm5| z^CDwWFAEd6Jhg!yfyWmopBT=BW-M`+f>?&>@ZC&YK>tkCkIdF4;$kE|YqAti!Lfg) zzl+DN5||SRw`#W)e-7x$$;Nf`1m@UnLQ7l$h*Y>>P`epyMdWF%u9Z1@-;f-0(vxkP zX*{y;FOsF155{c6BCX0M(=vo5t{g#qY~&qy^?wDE7b;e^VOoqDn*&>E1?OTrVcJrb zrM68r!;0_-!2|yjHAEz1kG%dnlS~&^8&wrM1Cpjzh4JIVAJEIj!i;GY44-mhsg7MH zSTdIyo5pW_ZxX!37q5k-ow0bbOVR0?jznouD4zo_?JU%E*i290cwS|T@NejP2HCF2zMAx{=D5^YIN>tLPFI_LOgo`CCC8krL z6UIVo?ODiGpvh{AjoL6IW4_q=;9aJ4ej-vYYeZ7cv#Yv*qx4{f0>u%+8tN_PDM+XO z<1=DztK2KF*_&bq`3T7Mzve*8AH!IB_9A2YkwEf8`>`+HYN*#iRg26Vgi7l*sX5>~ z(^~&H4mtkStkC2gSJN1jZKD38U>7V0mx0tr)EF3z1W@^df3%JwRhjzyXW)-3SQp9% zPc&w>M2DhJw8oZ5a^X|lW@M9Ax9drEcWafvN?@dN?3#>-V5C_{L?t@EzeE`J9QVf# z)>c^vfaVwla{W^@!=3#W#*qEuxh{U#)*Ii_*%D9K;A7u6C1w||t3JjTmAwQhmVTl{ zGY+|a;{@;(GyZv!-S%>xFo=MTnbWq^|7L>dOc!rrg?2F`?xCX+1GQcOLCz@KAy0sJ z!#)Jw6p6KwY=o#IwEGu&KVZ}tY{5L+_@@jEnYA#Fyvkjs#*58ta+_$Y%x!gmfw0vw zj#!8cAEoVDxKok0+vtb}4QE$C>RV_yf1*mg3yh+lXj?U@L-D;ykf^?%r@NC;%A*8t zp+^e&x+DKded(k94avG=z1~fW+ah~l8}{uX`Ia72rKH0n;cVm}11UwmekV`g*4rdl z#T|esVW(=Xgl8YD$l&3d08Q=rfM826!Lhesj9l;TD`90r6`U)k3I~Wv7#rswO-trq+CUKJ9kdlTziygq&|VEidlV@np$Sc}c4(-|@P9 zM<2ujvx9m{u0JipGC0z|Lj)(ior5ZVk&aa@91>x( z^75rfgx#tf+^6~aptvd@MIxBwM=n`F*KIl19a(Y#+1*obZOCL|2jzx;I}4YIJ^Dd( zuN=8H4JDIHv>?&BpcRs=^My-!gQh1VhtvOX7G8iu8M!`Fz>|0P$t={dRzNPCDQ|p^ zvk+x!7lOTC!IEH5h%dRATA}^^5=*G1U5arF^zG{$@^-&XmHMLnvLY``G9Z-~C4ne| z(k`7i9bZ@EAd8MmIJSpNeu3Z?5l`$%0Xnd(VP+{4#!Pg2ETYq65uF~3==4}br^g~X zJr>dFv4~EOMRa-u(Fvtp$tW4h|F4VQ@_)GKVsX!!i<|uH|NY{|=-XlHH#nA!b#O=I z7JmHF8CCByiVjL=2$<9u(54cE)G)Lr`(H(+nNyw7iwHFf#JGpPVYxoLg^BtdvUZE!zMU+K@eUj-7>-?K32u&J1-SusWRES6;Lz2j%%nHk~Oc-*lVdqnTh z&YD=T!+L?4w`i)!Qg1Jmx3BsE$BkM!SKeOp1CBvb2KuRjK=HPq-sjf`74K=PLNS7F ziM9;QH0WdMbg_u);YmF>cPOL!hSuD~0jNUZoqqig`&kk1OB-kb^0nm&y;8^vn?yU} zArpZHx?LGW;NznZ^39jGFa80%leAzsD3s0UWH2vCGix}B-XWwRsWco~rlR$%%7wX1 zp)kGE;HmlvRa=B?J(_P%q$8L|NQot|b z=S+{-xL6EmTwI)+_c>lsumBi0?Q+sA?N+{cJO|Lr%tKf z_82_7p6345-VI^kO7!Snf*HbLNX<|W#JXy4wRD8U86sRYEltr>%l}{Ao~L@-kOk9OFV;uG`q*>Vr>Tf- z5DAT6v_5oOKD;&`4heyTMNiS{Rlt??;lq;9kFUT<=5R*!&*t#~{YCzWlc`xJv)lp@ zh$vNh#yaoB@G0^$`*1ptW<8pkaE#;2`JOXlq`R&gI0UIiMX$VJo>1d$W3#sV3HHvo zDxd9<5Bf`wX!jk*0H|)kam@ITzT=1&&>qz7LLsx9jv@8 z35`QVNI&E+Js2=nIf`=q!H60-C^z0?ntT&EHPOalQoo1?a1YxeppV{W$_v2<7D-S{ zST}YpAH6YkKOm1vn&pQJ?QA$qNbUm6(@k6FJ?xgsPO|KQ(aI65E}usm_6Dg>^>wV7 zfH+u!%zrPyXAh}}-;`tJ8AuT%ICS3xQ~THz&_NPJA=Dj#44tvIac3qFZozri5Php0 z^Rb2Vnz>g5jjEqIXiL`|yUKS!e{an=#n+|3qsXC?dMThsP-+YK_SAUf?Jnt@;ybWv zKfORFa8)n8{*D|xWxk|8KpS{Zq(^>{>4oPQ%~#)`x@)TSF&2hb8*HVC^cAMd5Bj#y zaB!}((yo+xiwGqLBE(MX%QvD!j*}RR*kPu4kB9jiu51)P5&V5 zvGh1PhKAGj@*PW-EYaJN1J|5A>fMdt$ke}d#VHU4CT|~~vd7Nnulx&)iKaKvquCC- zjkb%OZBgR+X-PQ!E=#}JKjKpY6`!AXAmvc7r9F00e&t2$4R29{FAL-Adiz!3^<*f zf(->Gr|?5OoxMGc+y3k=(n-L9#=IR2`g$VU<{L$iK-`b)Ghc;txHRGSc5=Gc!}f)y z&Lb!hC$!r#0TtvE15PMwQYbatKB16VPBRI}w_1CiD&vupXvOVRy zY)6H)KVF2+FiZ8MHJ$8d}Ep`{kjepB1dq}a9?LZ7_=A{Yblp045E#{S8amQxE&M8IRz-)bI_ z%X2L&2!=B{s+Y*;`0b$a9fyW|j%Qdy8895Ph-JBvEgwrAyhNJb9MZexlC}r0hL$dy z(Q)2j$jsc80Z8=zX^swFcI<%ME=Mr9iOsNIzC{gbSxJs9jlPiaVTg^drhQt zbh`q2f8cD-0%Q6Wi;QW}fJo{1R#5M#2?vCGud`0kcgZCa9{f0VX62pqz%Puz`VmsY zdCZUPTZg`0<&Qg~3M(H9k=s!ZS~(s2Gm-s5E?4NRQ}yZVL=%xGvKa+I80Efj>|M)$ zD>ol*-6vDW2sxEd9X;R2W@E3}L|BZNOZ*yUl#NHsARAAZF*bf}hS>PM8DZmjGr-1c z@VtXdhP=J(3qd{B$2wV~mn~k5e%91Jp;61Nv3|mtPZAVsjit;hecfpnBqcB0dsTd0 z3W0d#?d4Z1$q&Y>_r>rL4D|KbUbfIk2|p&&g{kj?WRrKyDmQ+PcNG1kYBZJ)ayp`S z^vFH>^L;Sr>(n1PZO6w3qzAKN z`n?@0)xHK1)Tq@M#|hLq=(B1({6~&a>YqskwmkkIp?Z^?W38jE$wqUlYfe?S>E({P zw9Qs*15N@6r$a<|H_{VdeUBFR&omw_M-nAteR)02M<;d8tV)sg=$)b_Rj+XB1yqw# zckH67I$?^!(@v;%?aFH$bcfm3!VNISjc@vd<10LZaBt&7;RJ!Fm`G02+n4%D3G0U- zSY~cXk3m#%Yc?tJ_ePidJ=yBlieP9twapQ4D}?R(Tv(JX?oV7IyoLF7nHFxML#xuI zHe3<=yb(3`-ASw}8pkMGYmN0-5e-%O{+s0<$>wcha)D@OeTiH56T*wR#&E^N{=|5H z>oFmh6ZxWF6JeyE7jCUj6eIe?IlaI0+=L~w3H-52f7TuGqE{5c{DUshoZi`aPSN)z z2kc7nW0x}SO=ax`yIfxe@oqd;HU?XI);7AFJto_Rh_9i>o!dBT9YHidvCfZ~$?OP) zDc!w(9ZAI%B+NT|4*BbX@-t0HRW{cs+QlSpIFamiojrie-AFTzJlY*EBdHuw5UqM= zcLqjf$d=-VAkM|f<|G@SR{ej8(D!ovKRU5t5}bLl*^eZ9&7+g-p)axaY}&;(Gwot~ zPTEC6{bt(5JxcfC$5dEZbQ1!N1%k%mE-`CPG*Je)^|2Cizz#)rgv zP0_SVZa6|c?mow+SJ8oDIn<%(=gifo*%+%3tR#X$jc?*;<(=nM{fQJW;tyX-vE2Av zUbVbppG|HYpuy>nuvc#U2>=(xSa3m$FDct5OrGyoqkTaHAzIPqtCnYbh>HK`4i~O= z)71&O`ue2gUIMY};7LwEzirj-IM?U*b*x^d7@8D}Gt>%&yMys3#0G5-c-vULFC~L{ z{<5roKM;240W-&-qB|NyFa+f$4z9QAPzMR^C= zBA=s4jg@mCzYNW7zn09nh&#qw;~1&5M&1+Aw4HigTT0#RVCBsc4_*(qL*6Uza4euN zTg?vXh9Ze<1&K8u)Sbg#k?a3n659tS)LaBHqcq*mrx?a1Z)jkkVU%5+6()etxOMHgca@xP-`xf95CqJ+a9+YeW7&kl!BeRtOqged7^QCwZV8u*; zp1YRC9dz>=ptbzMnybnhUfaVTzQ*{{CxYa}{*G!q<(Kb&+C4~zKY4Bp_6;U5{@Pvv z4rkkDeA*>J?0bK`+et&!m&^$eGdo8{_h;MkJG%l~Q!8qlE`zvvx*juMO-}}_{m1qGs0$XeykAg#Jx?4}Mnk}Mj2)y6 zay7z$p{4v=(NbJ=iwf>=EihKCC(iD90i%C_J}hrT1DJQw{w9}bw-4=IHP9)EhHV&- ze18dUrhGt8i4^&xsHG3J5c;^w0i2TrXhM2e!U`*mdBiWD@aqG?c%)1`d>-=J(u4FE zXypSE(RR{QdqsKPx9f;U)b9DVJ2x?9i{j9LZ4u{rwWb!vlVac$_B1DEemd zoSgt-=Q)4;@0FKrL>2h&YrnDn*$IkwpHi3ne)x*7`t^QQKc}4SAtfEJxa2FyznG;5FlHj$j_7)_~V*GxvAXgmjeYVF?_@v zCC`oH9yELoi-$U6T|dfG&hGac&Pw`Li^i}497L$uA+(IPNQA;@UHP$JYG<2!y+e25 zWb(=P_7L_{yt_34w*2}Q-e4#fu#G5X6G|vi@k~-dd7HF^DsNwy@#mRqnRY_k@{TTCb}9rt2^8(OSIawhkEKUnm*1xA`&+BcB|Ta#KX6#4yF<8Zr@KRR zcaWEsKJ2m6Hy<9P@=tMbgo=z!A_?f%<-geR&1ZD|*)jUFvvq8>e4!h+`{;vyx;ca! zSRsA(akV_Oi~jf0|F@|0Fm2hxxIQVbI6O%_tnG*<3f3|(ut%0@@w@KT!i)Og7D9Xe z7952yUItkEdD^PqA|`IdfzO+;_Wn-#-$o#65B2$g9Z&a|!ycmI75gUvY|_mh>Pzh& z#i*Kqf!&{=KmXj%mu(@qgY(QEl{kdo&fUo1TP@$$f(HEhdJgNb9cHJC&V8SV*UXa5zM!SH*Dc;13Uec!SfdPFQYm54h{B) ztLI>f{3dpZ_K&p?j~`LO*Yeb?c%Kenp#W=gqe39c(E5Y%kd{p6A-GY|t^(7gmF?s2 zcdW(V;98*8UPi6WCTd;L>9bb1lQHcFbak3B?asZp@22~ce7~FShw_i_1?>E%UfM51 z6MMzYA-ef~60#D-GSk%!=j#Pbpxm`g_3d9B(85dR5M7~x@CiE_6wY93A;Dz-*x?MV zYh=#Os=Q?5M6e-t_go-Ijy-Fdw6K3!z^JbWPS)ZCA1Vq0x+;Q#vPCeNc0{T2o43wo1b9|cjmTKaINXbPUXaVIJP-$``Pk@bxt#-$!y`n#{mjmj(C&zw!ieKqvu6g4p z97^v|_>9WWtQSPZY7ggM!i<L+9u#KpfAlUD*Kq8S*cm;nCFFL9$>Zv0mHcI%b;`va|n%!!_ufYERBl8 z(x^Brjf%t4s5mT*io?>VI4q5dgET6d@!U2MxhQGlUfzP4e9BD*#|tDZE>;tezfSEuzy; zI_)Pe(hKTU51=I$CQjjNMFjC8;DAQ)wT-J8IF?QrN$(jN8eJbeGtR&DGx{t?);N6@a$d~YWX}}(tm)#{C=kC}ip1}) zOT_PUE5z^LD#h=M)#A6Q0S%v|jPh_&?P%QSC#G&ooN_rBIi!>#Yjs79oq|rb03Hc@ z;2*M9ZllN^E4Wt5DC{QPiIRVd7KvqI_!MEzR>K&fg1?BgGIrHW0yS(RbW{<$5bopT zUe`#d(`cxsBvltRUe&4bvMAe>Nr=e6Qi|S)$Ye?H@4BEhEA^$#TyQV04g@fUL7QQ&wMn^OK; zUUHt7#MzWpFDPuvnlSuX)d_#fd-W}%M=#i+u*MswUc^Hi+osdcZyofbHSmUhI?PeB z&9#q(e3V9pRtT`vnrk&w9V$FNPD|KfN)*_LH}lXO3wPrpR@5OnCI2@$Fz7%(7r8o- zDHwVd9QiW90IhH+ye>~~!l!{)maB~|Y7=FwmQxzHep-kIN(Z$iqDsxcKP@zDTT;5Q z>DO4AG(0xQ)#-N_%H=}GHevjmUii#NeP(_cp`(eQ!78KhTH=U#Eg@>I<`6=(EzpEU z47;MIbZQ!=jcvlQ8@5R@##w<0d9fVfx=k%7Eba`qg_7P%ZK_E3TT8lBuEw|;AL!vP z{uLf>W7}J~I?Vgoc9?}{s2J)^#r$y9XFQ3wR4i1T%>C$$a`hB+YkHQI{#6gr#-$)a zBrtGj0o~YOSExP4f`mA4se+&kqM6<_6}MZz%+GbjFHujS__56zZILB$c#Kt}C}Lw@ zY%lQUt~kh0kzV_xPFhq_4&n^9RVP(^EO-xP;tiEh)Qed!=xT;qc!IY;)09+;W)V9e z6nlC#?sK7uhL;+BHSGmU*LPH9i(5Xc>x+=C57^RyLDcmjODgnx$csjfXZ;g3Un}oA zFp6rvUtgZW1G|w>>(!_+l3gt<_*Vi<7Sv%3l&Ft)6gE3MSW|c4>~+S%gz@+^)bJgZ zi8RZ+*>|6$)E3o|`(EDK~gcSQ$0i10xqyaCg$7&bertAb7e)!%Jt`v9xQWq#J0(WJUh?i$i* z3o4wpJ}}eI;cs9NHGKzxQ(lEM%aDipuPIHkPFNQNqntvRRp1HGpSW2f8&-Ner$=nk z|EiOsLzQ-9efR5B`#i%&AroAYbqbzE(L>l6>|kf4u-jwglD!dThkQh&sFvl;A^kFN z$M&TDkK3l8n+WAfw3bZ`oly8TTs7ZMv_MsPh9AYZ&H_50 zu*mRivZ1_`rXPgSN7MC#5&v#$+{#n&tk-%Z5u(58k40M)#ZVMQL; zO7#Lgr+*ZK>6wZ4$++a1ZsRE=b6(@ABKS$Fx@Df(VCf5!k4RtGj{3!gzZLd~FL>X9 z@1I;TN)_S}0<9uv+<>%U80IsiXG#u{?qG-q-8Icfe7&v%EcCB}9t!L~4qFLSW@vB12!7lEPyI|em^C=>wR8Yc<94IS0Cv1iq9TXrhw3ydX1 zC!glS^1Ucr4vGG)5s)d|ppc;nqmv@eq7j^67xRLGo3UVo;)G?95)yb0i^TV6|M1VJYa zomSDqcKa(mdK1Oc(6`Wb4>Ou12L+1|$I~pf0(wyds~4j4C~UX-FkDX+*C7$@BP0@Z zsw?0T-cqcKijWA!5y}t@C;3Lg&Kef$U7}_Gm0|*sVruo1Hdski?`ETBH$Yp?&gm%+ zA*X$A9(%;|)$<5!$F9K}<*O$UaEB3a&%5w>?==X|j*GYihX}SEsG5BAEjt!Gg#}Nz z={H7fhS$hya?-7_97ZRVf;hMGLpS`qtEOdBcvIhr653m?%oAwJm6ntvJv&z?8 zwDkK@PmhVu_TXn{=(E?cH=|fFs;6k67=;7j98blQ zc+ws=gZ|VX(bg_tRi|b20`CPpC~EhVEZdVDE&U3o543H7MOzVC6e;ShhyoEdC`&5f zRVB-yGpw}tGuftEVFm~jgZXo8#iRdP7|8Z%(p!DD23eo1>ho-X`rU!%98A#yY@dw! z7;i;8bYVYLE5YUNJBDSxa2RR6tYp zNKBd(f?2?{Hc_G7st~xj9D!o9XphRUyXG0W6NND0jOxZPp|mVg2Rb7>O7;k*MH|un zP&0^snFsFHpl3b)Ws9Rc>I8N$0*vuw5WIlT%ONB1vB`SqujDDZ%mu>X?mpx1lPHZU(i?JQWO5`i8M>)RG1)c3 z42_IJ3t*8JPm?+DwjdYNT*#64Z8ETr(B-;fY%A;6gl}lbiqxa>`}26@W<#L^cMEc$ zRj4CZbw*Ua7=UHVSZPg=VRxXdK#@9Ce)n+Nhy8p%e0N@PwnE!+&9$Y6+!nJyzS5fz zQ-l(Ls9Eq)(lNz2VGlgYr5h+#Wf^$C>)%$cTnQ)>aT-kYNwV2ThS10Y;#;v{s|Nh< zLbVSUR$(^@GO9jBc6C%uP%xinCpuRJA_X8+_&)^7(QOeR zz=L78QMAGxIbl=u+zoE&ysGB~q4pQZ!PDxzKM#?G5V0|>hN7!gS^bd znxd)rgPeLz#4dO>XzsxJgMtgQJ`apa&{;(Ht^$OOU>@I`fje4X(xVu;x6k8w+js-z zL?G>ozNAy(btQPZNq6`12Kz`NA&hkJ7?mPjL-VpqGtSTB@P{{4B5W-=h<6xE!M{}( z>`{Pv4AVq3$u^G%zp+A~O&W0|=L|5nQrONMMb-Q;bQ47nqN{Zd4uN58D1< z2YHO(mDtM$cqoYuTbd>hCsmM#H^lKrUucJ?Q{ia_sdO4pHv~^XsQI1)p4!FJsp4q? zJe>_sg%+p*wLob1|7D7!Z6c8{pxP&E`h)#rYJNNE1!vU)Uo#t+1R*&3%abY+=sfB6 zQMo`~Vi+xB1L{lIra@auwPP7M3rM+SR(_I5G{A122EsCIXF&7^Wd3cL-#-<^YxB<+ zDZe`K#3`I04;)dPhovYK&I5#}j3E-0?^6kpv;c~+oBwk$6~Ed)Q?K&rzn(+<%ioa8 zcMi&Yh>sHPIjT2dDHG>T_%{It1;lX|wK+!;coZ^G4*6f5k8B5$L3i4@f$P*T)f6Nd!> zc-FTd^xV7!HuaSj4d zOb6<#a9h2Z3;RTFdWP6g+|igQ*9;A%)=W=+zhs3yfVX;cdg^^Mht z(OD=r2kq@09{o9dZk~aG4;M$^0w zzN5qO&Nz>dA?`q&HgbP(!ELiCXNiDZ!%Yv(o-@|c#K)y7}ODp>YK015e;MK>hAigRzV^Bp(JWI^tJyn ztdnaz#G!sa6<$OM;*bO^I zk-Am=7l`t_t{5O#g>U{@21UbDFv6WK29zR~FJz?Y@`zZvrM88JZMS8c*Ypygmz&7s z#D6npo%eU5g|~Ya_f191yrvfwX#6htVi7)bXncNwX7R&TBY@egn)#+`vQ_-1 z&Yr3@nyQs*6!w1TLpjxmc7Ci?FNYTH!e>ysRs_#x!!uyHib~)a-fPfu3G9saq^?d6 zPRz(oAr4;KYfqnrlB|?PA5FfgQn9pBd~m$A7eID_R|>}-yhhnk;J6U*do5PsiufkV zLfD{!VXNRrf?LqHAWoBpkeQt%Svp# zSZq*noh{E@QH@x@Tj54r9+A^{p-aF&V1eAb@R*5SP3#Wsyql@x zQq$#B0h8;tsYKOG4qRMe7}aE$d_t>eF@K4_ZB!lNWwkH? z$lZJZfre@~dx`5I!Oh|ah<`xw&8#__X0iW_y}mJH%wF&2uWP)kO`T4vKRtxI$7?JH z!o0juFt5N1yWaO?y8I7O8Bk|K?+r{$&D7p@w zD^?e(`1X+xJrOJV4;y(_@#>E7_;vq(7hkxXNQI*$>-cC#T%`l7dCmTv?xhQbe52 zrq9=SSzO`A6--nK#HI{`s?LMoIC}{Lhi0yf#Iv@6BqBxV^gB$y?-p5V7fwb@iz?Eg zK%eBvS{}0o$Lhap9XL}d2}L`_SLe~LxGY6dpW$@518pG8EgnXfRZdJ*;D@iF`n{8d zenqF<%Nk)CfRSHr_ZT;t&q;V{88$^BU;|pA*ry+jBEuUl)=O_)Hcz(|@K)zr$bQU# zefOV$c!Ad|#EsvZV=R=O9vGa*ZMX19?44T@iEjwC+wM*ro-DQ1K$9$E{euc?+TS>p zbUA0PQ=Cy>9*+R8IB-}#(99zT_J_hU)6N3-tMK+fuM~(&SBJX>10Sxxal#2s1>hsY z2VM+~1Aq;s2#8YI{u8jPFo~OmT8zAEsT=9o0x22hgKffiZM=VHg4CE3=O#p)6Q~>0 zVL_)*A=|G-KAf&aMrZMUfE{3*mHt9N2N$5}xtgAP8;KfFZa?-ML=WzntHo^JN+gVp zMHwIjLvTM#RoYeq?*5E)7X7_$TO z4n!lz7`Alpad7{)c_r^3UbXcTnBA+7}r6r ziQr1e2L|2Lc>)dD2uk>r>?|sbbOUG(k0aQH0RRuY5elmS1!f-VoJP%8M?V*T zFfHGd!-`Js(f??L)TfkjrTJ?-R!9er{?~lrWz&|4o04sEBA2xLmHeb7R)56`)1g#n zV?+rnYZXRTTknx#fTX9PCz-wPGK?>WkvVj9X3JeEvscS9RQA%Fc zsT+qgo19B{7hcc8!qiLLbRF1!K@w_>+$C6KdJ>S?_9J*d&HCgmS?WM@1b&lGpC7>2 zQ^iU6kw>E7`k{82ZEp4gI@uhSbDCM` z2NH>g8en6#ozhoyKP!r5EyUEG zbARxx6>ZcF7Y!8Z^miJM1Lteym&u`&p-WrX%;rZiQddN#gUghi4+OZV;_6=CBeJz# z=Xwi6Pi8$!v0`r=_>Gjoj2ko4^z*!*Ir?NEWS)vn43ulzim@_kQPQX6>S%pN0lqINtn|ZmWtfsj z1@@D+0j@^wvluv$&Fo-LVmyD<+2XA@y2zOJY~M8O|0Qmz4-3e~Jqar0CWYYh9#5I_)UFdm$hju~&JWVxk% z@Uj0$@TmVMu1?;{g15mQb-l$lp1=gUK?hQkeieWeK?H-oG(IC`P@X|styl_`l^!>r z7)|N7d2``WS2G@=-!z9-J~fMhU86`;_nMiM{&6gxxa|5=#<C4F zzO}D|;$4E+({29V>dAT{+C^#kN@@l#Vj`m)vSQUuZsE~Dbhs{)?Fc&~m?bkKGMHJn zP-;oVi{3Fw=AFUzHFrVD-W79J_`VS~vjWGmst2=tKb8;l?z}G4w*;h48N|*3lXphq z*@`oq`hJ)7df;T>SdOm{?Spu9xEsm#@`hOzV`zl@m6o>C<_+a2!+-;#cz30ffW%WwlP~y8;D~1a{V<4^6`;q{Kd#^f85BLAX1e972yvE*< z`9YvT#@cH}J<@w>=mZOvA?Dd3&AG@nh>Z5bFr`KGaH%8*PGuUlZ4~N!C7-xWLHznS z>J2N-d89KKn$tnmZ@>?>@1^(mV3Z<2DZ#ff%1zm*hxfRRWp`$Z{mNdtN(6u{8O2%X zH+MiOT&^3$3aE%)(N9B4!67xG@K4D<9mQNgD0bVjgkA@vQ|h7-Lf2O)IDH+f8kB z4FAxie;Xg{n2&viu4lnOMo6^qUEUx|du8Vy#%EDhajeG(<|6`=Yxa%`pWaRJ5#(zk z65Aoe<5T!0eeQ4W>ymmzl!2M20J@vJ_v=HuqD%kt58_cq4%ur5ILqFH>DB! zg*MC!!vgaH4$?>l@L;?>%;YmWgTezwR1DgS{`I;Q<;W4c{UR}tS{lfa zd=n*Z-u?8z+Whj+g>`;38X0$?Pn|JM_|Zf@%v@+Z{BsOEbHy9~A>kpWb#vYPVqq8v z!^#FEAX$Q;S{lXxJkn7s8#u)*%9WC|(*~?ozv}ApzfKu8DTEFtW;^Ns8>L0I@WW^H zY2^B2>C-g6PqADIIeD1qS&CkqOB6vTza}q#txYWu$J(E;adkL_)Di#N0IAEo75-Dv zrzbL+DR+&!qTD}K=54IL>k_nHWGT*OW7hXv``O|%0Xmo|u}y6h9$ADiV8IA#jf-b% z`W6xMfV!mU4;68Bnr5g)8jqkVD)#+-BMJS^ImlCAnFW-xYT8N6=oDOyH$&pOa}u6M zh>K$hCn&y#>tpm_Wg9=CGh`ntt_zxByG>zUVBWSrif*AGr?ie~1pjQ3Mo`q~F_vM@ zx8+e5Tr@c&V|8l`jbVgGpB{K3ch_XnqfKU>=Hz&I@pNB4^bHH@#Ra@Lj|KmPy(*qo z{asWO$I0Jg1_r_gB3aer;m^fLyZK&W!pq;r!x=E@8JfO4=H{(^SLx~uS-(36HiXpX z8bw8=kzcy*FoRq$z1E?}?Qdj6@<$?a|YMz*F!ekhw_FiM&6n~a6??!P% z*T@^mpiCreiUtLYF%K;U^S;46G%SUA{)WF!ujOm2tDR6Q;MmP- zjBQbK)WQqqera5Ox#A*MD_h)8fXU0I`HS^CbHudX0KyziYw6~(_!OyhH#Q9Gbq2j+ zp0iS!k12I-^8;D?IZR1a34klB|A@}y{ctM4GJ7_HkDdt!BfeW|hGJRcqTcwX13OCK zz6rCtQ_RTq79S{K`k1;d2rvE9m8qoH^sK_tx@zl6hK}6NA#+x zdT|c5xxI~!*^CAEx&K4jN{Z_f9`_i&{34kx-kW4bRE#c-x^^!fY&#tL-bFmp_c7Zz zpG*yLY`^a&*sJ5Lv-L`|@z)vrU*Ry79}JqC;3DPEk05k%Y6gJMiHRsf3R z@BlW%8qSg%uYikhcx|K>f9w*?xZ&^GxV2J1c9G?D;3S+HyB zO=R|q9fk+AvFI?7Iu-iY!?wemjGv6}eE@IEIxPqGqEI(t9<~FIOAWK&KPVVec-NP$ zN`p|TR$50D)?Pj8yxlj$ZVa*iG zk5s3gfU>>tmKuwF8H(uMX*Gi}0)%~;3D{KnO%Zn9)l`Dq|}%q5IJVFOZ6D(r-j;MYyLEnumBGl@fc;ZeT!1N2G4SM zbTuk+dj5C4M0u6R&JW|!ym1LWWmaz6OcZf*GSBi6FYnxSoi&r+d1VAu?X#<^P5@RZ z(qP(wCBvv*)m1q#;9n3DyEg}&$xG3%T{emn-im!**t(HpnI5CG)&})h^b~;>mD$&( z<&#F&KS7c`QdD*w@}EqSV?3(?{QDq~Rh18-zX@v0C~RzAu9oGs3kE}l$pY-{=ADAi z_!BzRdE>zJ|H$Q^3R^7~04|5s|DFtqLr_mA%#_oZm(Yyd&*Zn3DZwiCM&wKY9Nwo@ zbST^-TEf=J#04F~UI5=tjY8wGZ(_T}4#0Pz2k>N7l3xFbKyAe>Dv+~MU7ZHdE;H>@ zUOY!vhk3CAD5l5Uj6P-!g1!N!=ITLM+~kIYEt-am&8LQw^14=`p1=(=ZmZmMobZOO z#xW^T5m2tl)m3&_wTWcOR%f-T!vdU7AV||0LGz{&Y`?9zl)`x4l@RaYtTc?zJvK!f#xrUsO?lL+AGi4{fDB@{>>NoDOKWZW8+QJ`Aeg2<2n2)G zAdZQ>-ALFl2Tul9*%fD8zhrV0P1b=;d87=dOOXxHYv19$sdTCC;NIsLbx=Z<%{x5osn6 zCwvWhZGH#imA*QH(n3!>36DSv<{}kfL6j5cF2c)D5w=%64MWkIx4|`UHH+f{zS58@ zAXfADLnvWbTTR6@HPn~mUQ9A8Hd?Ix9JIKFG8203z+2|TuDWX!`nf#EeCAUWCObUc%)loBg?UBU3a0rA#^_kV0TS=!@as zArqYgvjVgEmU%5yIJQSmTXfSRn4j6SZ^Dq#dXq5c572xRT?bEZrg#a!tYJvl*y->H zz&&h#KTB0|+PpyCxqiXW!92uTq1Y}oTVa6nw%DP_NCKEnVC-OfA?83fk5_eC44!%8 zg_M6KDvb*GKFr5FBlbq=Xriz?6!u>WGTID&I1ru~=#9f9&xjrTEsV1?e+F=!<$zWL zdkXF)eU>370&(dlIfa%!s_(zX>)%qEse+mzlIZw;en{baeP6E`3NHuF=d1AiyLe)| zxh8=C!M22h&A!~41kMH{0%xcR`4z-D&=RZtGvY%tR-C{*)t(AG#~-`vW06vNMl81! zIdc};EFx6TXq#`t6W09SbNIg(@O8O--Bd%H$p76QI2H%Enh}VU^UIKk7}GA(j78v!hD-XMk0!}*o5 zD^5}Z`DLgXEGOqDPyR#lg$XuHuR^C!aPc1oF%WZNiP-07v}CIEhh?^9FDWzRz+Ne~ z=A`HG<~G*{y$H4@V}ujDf3J`ThgG5?Bo0i4Y3%-&1x(5 z2z|LZbCl>b4Y>^??!#o5UmZ!0k7a#NbUUSeOOi7nU@7hU!^Bu&$QXp9W%?6ZtjN;w z!7byDqpQOcnz;*ptxD6hdB>7frb(aZ!Uc_rN@M4MMwwNpAcXnz7EN(9h_q`?f zrnX{>$0(j{{aQmT)~w$W{EfB$7jl;Sp@XsSdPhN^$Vu(s;sOh<3c~`f?kqC>iFA1Q z;@*6ldXtO2G1dQz@)N0iDV4wC^W{G`HK^|f)?Z+^oum@TmpMAQAF{HSP#abgMguy% z)Bvjo#cO{>N(C^f!FZ`dCSgPOYpc4OHQR>WoR~uU#H(A1hJ($dNe2GUP zn(-^(+svv!mS_j0^GM%Rh8c>RI69>i-CMcA4QkjQ|q*!Z}F=P>=!FHxLTXG}fbHcwb zqc~{Hhx%`v0rg|jabwmFM2mB(d5<$H>oZ>QAVb>^_1a!>_VH#Ob;gR}CMb12J#f){ zG-1&*A_6a?oGB#vSJAWm<>@fkzTeP2PW#I!kM?b%dkjJMaz+(U!Y42j_Nx=1 z+xoLcZabakx0t8MP$shESf(LxQCfdRST*L3&XCis>^DIBne}_ArLJ>+?v$CRICLN_~HB&!jqCB zlAXeIekEGPJ1X0L3^y!b7Nl}N{;8DVtvKP4dOgOp8VqR&sN+c+5E?5!u04y+7@;9O zl~JeP|6wSx=3hM0N1Bnh6BFjS(TXk|NtzVq4dZ5?N1l|uY496L{Pav9G3q}R7?IX} z1PlGTp^joV`qp4K+7}tiIJ6bPek_FR98bmTlpzys38>!;g+2T*bZ*@ZN&G^o^*mC$ zr?Kr{^=)ykx~Ci^!ZqK0y_4Z43f;Kx8k5z>V^q}NpfeV zCd}uyd5mRGj@e6LbZ(rlu5~vF{Ks}nqQ4QFSLza~^KyCE%5%8S|1LV8Xs&bqy{Uer z^B?2&hh9s6IQ?__!#_^7^oN`6(^Yh%5-P-o|5bmO=G&IkAnxObRCL^Y{r^^pxByQw z8>-MBqCcAv!sIR3aBj^=Rt}cAf!rC)*v(^l#lx}D!%4m3d5_R54oj9^vDk*=Q20ZY z3jfPrZ-pVfOpzKYCk8%@gD5=il10&Ocp8^3Ia=&c_y~%M`UxDvFpzy44(mHH4?iM9_l)s86a6hN;)@cJg}Lb zA{NGfHEoTTP$O%^Gt}R-1u%sZ-a&CXLj}Y&3dkFM8>9SXbWPkjhx9=YYi)r#t8iNY zeHW*a9yPlyfQS9-zJ#?k-iiaMzFl!P^Iu?K1@egmq?u8Kj!uCWZpBY*`1(#?Y+^!m|H$JR6A{5Y;004hv21 z9f#61y&w(4d*8QIbOnlFnt&s*pAV;NeE$PAzTZ6b8IAAC1}t$(=zGma&~5JJq#rBq zl6i+b&_@DEBt8*{nzPdtW%WDhxXU0eBn$qIA`J1zPC=gsb671d@qu1=ZNTgG4d`xdY6Bq9oz%0rzG;P0G=I=_X#vy5DjP`ToY_?*Q?Zjr0Qcl~@LSHzcx zn>-c0=K&E&?XC`tl#l@CsYR|aF={8E&ZvT^^aih^UXue>)O?%{shMjnd#8|})8v!n zIR;;T0m>*vB%_p&jIx3bGT^a=XGor)SwTuZ6o?nHOD-d%)yy<|Nkx-*xv-Bu)V79&`r=Sc-DQxKxdIJRKFxKNn&u|cE%JFme zd8`PZFGWh)EWE~*tJhpBGO)gk1eWLc_~m3QF9Nw{c%f5%v8q44dtgY>k_ zM3D)^v2whyyKTh1TT)bn9-%054C~*+93P%z_kVz0h%m<~B#_tRGnw5F!rcA6Z05)K z?E5z8S}R`tD2Ht@=-k6M{#+?#u&vF(ZWb~uqrQwO2ebbhUpRbpc!8D?1h5q#)5qyB zLnS4}rr6ewHk8wftRoC#@;WXao$k23Qmp7VvBvshkqGvJCAHFmnYf49%v-NbbtZ^x zH;S*A#rE`Bw&c^53Y+;ltHo$0Fo|fozUKlIm>E5*vL!vNcCLV-Fx=h)^M6k(et

    Sz2z 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

    2z7U%M!UkoWC&SG?D1= z&U~k4#$_s+c}5N$PNu)dK!F}a6vt>zG`bGV^0JBM7!fRj2&D}ysY(&OfJEQ@=>r87 zAO~5tfTX9KhB!$#3s1_B5vNY~ui==WC*di*Nm~9CY!F8ulb)(DvzoJ;S0wqQV9$3? zLeUFYFnVs9O`s>n@MAngJ>|zqr3V_$Y)FH9kIGu5dtEB8VWWFG+|u17<}z&o$}pd( zC1ujT(uxWzjf7je7ep`h+NREBw$Y$wpsB=iZS1NSV9l5shdTsnl{|n_ji>Q@_n0w$=>aKtZ8G;O^J(z76A>Kn zgE<}HZw1Sj)rwxz0=Es3Sv}gDFgq+(&OJFF@+Uez-|zPyRw^1Y01our0oK8|5s`xI?@j z-32P2l2#lO`u+jx#O@BKbnhXOZo8z>Syxj!Y56T85^Pq*3GUhK4lovRw^X{{6X?ZO z8NaB8UsiD&@9)6kK;(M&-FBh1lpcC)5YfKu#dkk4IM=C7Jc-4Kv!IWjK)spEolBdH zcKY1u3DlXW{3{GhqcJ#25IFRuKHCENm!=$QJ$;&IAJuz^h$&T$Wgp?xdp>31LIu*i zoT#S11x_AMf1R+^S<4tC&x+xvT1zk*EuQi^Px(pa;mQbek9EYO5hx(`y|Om687=2B zOPGU+HA=v+NSARRrK>jwSWe!@9y~PaG;lA2J1~0_nBv}}^q<~A0_UX&K8`-|*ezJ^ zd4(^FVCEf`Mt>?T-@q$IS?d1dMm!JX7ia|=k}hC6CyV~F?C(c7?fqOaGVucgBVrPS zAi7}{qyO%9G3wA#t3+x2M*o@!Y@m{R6Y@PTHcV?o0dsB_Dz12I6>o)@nb{m};^n_I z`y0naljBX1R=5n;=$FOJ8Qm%^-_pa%LiQTb)6&-X%%WAQ1u_fSjefRP+J{d#bf8-u z%CHHT2CiHzR3lAtpi>sRZ3yI<^+w=@HX?*%KS(6`l$!v(5HpTmwy;l@$^SR6HtN13 z)XpMCTf^!8`f9%CSY$5NX3~E@>WN&D5YKpOY7(^|q4wbe``28GqGt_fUgCeP!&A^6 zF(~&N-e4wxE>N?3{BOj@A-~zVK5Fb+vi9i^y4F#q|G!qa}{SrM+dI zG?j$!=);dWfhJZBJzgM*xd0{Rl4{xqfW_-Dk;mMt!xKlETGt_!0c$Ko|81ie)2N8) zp$^Jl80Fb=SB-OJ<(02z3&<)t(0!#8952p~4tTX+=UjtPS0A{BjUw|Iu73RI;FI0? zTip50;(SY>dbGsH!=nvNz>Fv}58?wzR&*NtM_i75K0rw{Il0D=>$7G@HSkft0;Y0QYvcq7&zBsa7e7i65FAmsz$iQn=f)gbOxN1n8_6=6yLBn7(DmRj+0J}Zo~N4;}YB!WXCR%%23p2h+-2V3xe9ToR1>yNVV z#X^fb#wv;Pq8x^IF$Yjyb~z`XJ!auiL3+q}1pk?t@e}c;l$YaWRYHAAy@%7!473RS zj|6t+J1xe6-pZ9Y++szW*i;FHx~>m}vrP?wwt=|~72VP|ap;P#>+mSu)yM|qis(4y zf2&edGUm(?v1FSOE@@TDxZ%tQl_@WZhgB(yhsO_jwu`w-$;yVuPO6L-8-^Qmn8O-n zng<%b%U1s@tiv$o2}A?Jx6$x%dTgBG!;LYUxz&h(B)-M7H_AVh)#>l_^Th&iF5hP}A4NJ|b+5^*L5Wy8oR(ck0q2uH(hn zLG#do;uxqh8EPoR zoBlGq8IGrMc!H8dKdCrpk8w*=Nl$1=p*EV6JrbH7IObXn=vSEuuvZ+R)GbgHHWwi*b0b4)edW`LU=JGY!NdJ2*@b$}~Nt*=8 zGJms*euQ^_27Orw;_xHg(I_d>Er>&W1$$;-X$ZGP8y&s^e(xKQJAyci&vGN|H4#l* zC0nzrD623g^Aor#g15)K196+en*dk?0^hdTz znRi+7kV6kSvRaZSD>L%cq5>lIB(r31R3;Uu&!Dxe3p;`R9O#?N_}ZX8!$H)|>oW#AQKQ>d$EG0_P>6=FM(`Mo2?B!z``-9ZU$d z$*E3l!tbbpa^Nh{^Gl@DF-l&H8S|t1OyY za10`7&|(~4TGv!y8dH0s;u?o^PHuc78Rae98*#ERUd{ zw9RUhb8D&Z+*eWCX~+tEg&KUANWqJRQ(SbcdSQ)_T5Up!o_$o@$jpZBQ2H_Uu#)`* zwYOffcV0kb;V6M~h`{+YmJpdUNO0W0k8&Z@;jCuKj$;y1fuDX8yJ)`4cCNQFnjdlG zltv&ftp1ycZ*Ope6*z9nZ~m~H`x^l}MO~8X0ARDrw|SKYMo!3Wa5}I9uB;GEzn{oC z3kRltKD$HjNVmC#_TObYs55ocQylr;PaBk3?et?C2W2cn6xSNanA#Xd+{~@U#uS%e zrHx>Jo)pxthlina!{ZacrBrjAyRA;8=>_N6F7+8qn3Rg)P@p;oD*Uk2PWu@r3_kRf z@!Hg6ll!6SPK8stOw}Td*k2MaElmp?pDRB88v_>lm*=bP?wV40O1@>GklZ2^)h0kf~mv+_dN z=nQpVe>TiAs+ri7_-w07*#`fz#krB-fu#?|!tEiAvd@)U@62t4`QiNdc@M2{9sq_o zK7l_}sZ(i%^&^$E6V_aDge4fk&*dNm^KAvD^K8M@crY4GDYMNF9(ZycT-+ABZlx}{ zl-1U3;SILHC16ZW$x4j@gjlg@u1smlhk|D9N;f9F$uFfU=4Z|kdbY=5V zNH7XPxpbsgz1vc(ggmDG^etH&5Qoggru}>xOmoTE<8|!dUE3j{m}aKm3oBgNanS#k z8~$VOii^!rqxT!v;&*D^SIju#$-Zkp>i|o$rtIfyi|tS(hU^zA_7L&WTIit%;E5(n z_VYrQXpxq~`BNI5`bDddsM7LCl7Sk#I9c2SVy6oYKw9x(vT^6Wua%FKSXhWS(8owJ z+x`TeW%6kv~}n*G)S6>MwOkUmV9m8#v{sX z17FyR8}x4ak96_U9_R-|=6s00TO!Mcg6qLb$lau#31$JSGe^v2psC zAKIw}U($b{ODpntk!S-=3&*rDP3<^eoOuKb)#1|^C|c6yVXSDFaB4W3M)y}q*jzWR%WT<}XEH3?icE%OTZzfAY@1^;EZY{C49m7< zCd0C=%4CS1-@^CIIUL@%RsdBzJ}g+Rm~Af2TqLdDVNOcYN=g2_%O7F$Oln`QSN(w% zPVCwQ6JL@c#G-}oe7?0;7|mK~dzVC#OV-MM1s!K;cJ^MxDxbRC?A6A+-AvD>YBvYY z9JOnY5Fhn>unx_)4a1p5jd(fY&wC6iZ8sjWPV5o-_V^1$U%pp;+~TDL2~90FeM0hY zV4wV}?~{MUj;%ue-KC8i?opb=dVksEUdOh$xZI9?6rVJnl9s>0^C9E9J>s}mTK;Q( zu|;~SmM?Yb$=)Nas840%0!)tBaS(3M@2w`LVfoY!Hrj>BaYS07n|X3n^Q{8gk=#Qy~L3|j&t~ax?XJQsLlyerfXJ`hlIV?aYzcT zM+CKUvkm??7IE7NN|*08F7ST1TL^~iAMEC_PchMO+7@7F<+Ck3829s++XKa>FK9T% zTw*w0BZucm=4E^Wdq;I|Sc&r!Wq3()}$Dd;3F*YS( zJZ>`o{~3?0l)mxsYT0LYvIyRE!O#RQH8fbzs{iw_#Ozye7xb7Ms7v-ZzLtW2g=rzi z#%Hs@w!WHO4Sh5_Fh*}Si>3J(%_7Nu5SWAB0Vc0D93unZi^9#x0$^bQbR5BFbRnW*xVJzU_qOv=iH@F5;A(oZ2O>aqy`O_JV8?M{i2I@X$%0 z_JvS}XMk|;8hPQwaGcv{)@3Qc2L_&Nz;>qoN>TqrceYv_fVXGkUZW#7L z*(R$QrzV?{^l?D^3Umr5UH=WTn!5fI27HQIWu;bDS{SvU&wq>W81>PNrke34O6X?H zt&G3Wv+(o{v-jv!TMlgd?Tn>~-Or7j#|z}kT76H7_(7jjA4DmLpUP#^i#d4lp^eW& zjOr8p!R-1S3_DjXt${=0*%sj$8~1OqQzx=+6QTOxk{NBJ7iPR**sLOptYm$i&(5{D zGD6;q&fCZ<2FsWX;#qiS<8vr5uP@0O$X;tYs{X zBddh(5&j1G)sbYS(WMoc?hK3zk!DfjZ;!m)SD%{tgwq7i(E-B$w?1_q{pLW~Kcy0? z60l0t?5EM{b+^6p$*31@74FKyTgXxCyIMZGOIEU%@N?S#9|7Z6ENF+hhj0jYg@+{C4G}oL{GO(vy5c zqwHw$jevLH*=8EA&O8iNmg$1Jg+Ju_k7ngG&)W^K`)?Z!k8DfuuZaiumX#eu!Nnx} zqFMjT&CUKXkpgHT+O!sMWY&Fn4Htypvj_6DHp(!WGQ>T&JwW?wf$w%6eb#Xvkdwbf z_;Rb$TB2DuaKWiVoQU-;D`Pa|R#MNn=+C+|b5gay-cQ}*;&k806CqFN@h0)u8O{3i z2cTERZL~dzW_A96+e`nhM|N2WMYDF(Fa7il3^+WD3i~Z8(GbQYbqqa*e-`bVmEg8O zcZ%=oV6*Q^KCfhsjL$b+9c-Lq3QmpjXX-l#l9B=i$-V*g93CLho$R|3Ago34#E4@? z>I7iWa8L*Y{ z+R|AvIoIV0G%m{XcjQsLdBdVvW9JzB1>h+7d}<{fec(6&(u{v54H{Tbyj z1)`QmZ!0=eZX6W~C`mL+Xz!BgwJftC%g1oQ%9Kr;Is2-Xw&nlkJ3n zx>vv+xKoJBrdy*~58Mx_A_b>l0%skW%`AKMUVs+q>u%_aUro(XqDqSDD(q2QV|&6f zBRGX$M10z)^uDXGcOJd(4d*eQO=^(pby8&(D(#s)j0u!CO#d->UCWrW2jjnAFTDX9 zccD*S;6GOo_n*tYjfx5$@MX%{kJj~bBdbrna=Z|M`Vjbk09XdGN;ZEZ<~=qk12vCm zY(dY*w6?IrRefrk(Vt5->@8`$Yq++TSepGkqo zp{{9DO0q$pIIVMZIMvpT@ZhGYzie#UDG zr-rEMy=Lr}m0xG4qi22AF3OR(9Wtxiv;ROvJ`0VZC64-D-X93|3_ zAdrIJV#u|+bn4OGeb^xRU#H%Bz?lqd{D{mNl(c*u%a0tX@=1Ouk{J4h7C+}YHic8p z(u(J0(3sb~N{4QT@g)id?XHM&1Jwc2d(&yQ>O_k`x7Dj!{C{vs@0+}Eo;zEt)Oaek zqS|@X=VD2&@#K<9-ryyo3D`4z*E!&RSE-ql+@myl)4D08NAffpgzndZKLCatqgDm` zsWC3;z5W%!E~fILdYGjiLe}qro3)G|@4_nTew`)6E8k!yzK-7M&wV2XO2RmErov~E z08K!$zmlpKmKTx~s($omg+vt+5~)z&S8PKVZtB4@sS<#4g{k-!tY!1L4+gWH=5?%M znWRb{5y+QuFoeR#XrlmUi{k+nuHmx(0l2k03V*>r5E#7hS$gz8Ol7hHR&12%dl;g; zlwZcnDqKrxeGe8shb-eL3kaxR8*c{NSO$$+_+vb4Rbe*rZYx}cKR5R+JhAo(IB#yl zmTT#sH_{ByS-a3*Vhw!bTB9}&u$_?W*=reKb)bRAF1~OB9~WP~=%@dR?@Y&;;`?^` z&MXnG1(-u@eaX?E8Ho9~Sxrs)9%h_>gdF_BmqSCkt_gb8XN87P`BSO@Rv8-WU5+87 z881xBZv7sFWmYq#{~(V|K}Vc9FL3P;OzeXkp7;AaxhLosJw!&=4NP+>KQ;sFLzQV} z)}||yzeQA_^{6s(4DI8t($u77R83Yw6^H3_PDR&*36p64VrmOkeL%44VuMxRVmQRF z`dIbl7_0iPkFja@uo!>bZLn!j607R#2g&4U8`_h|ucgedzKMcY>td|% zyLiE$2*&gM#o*T%5@Ajs9_E?xX5yJUVnAl%qhRQh%+ONsf3ZXvOzn=B{Z5RjC(~zx zsfF|VZv;<^qpZ^gS;;`)ry$eyb&!>>oLjs>xYMqg#jTJgro02ATwCg7_itbU*AI(B`lnuP05{h93KiPSErMcDzzvYvW@DMSh(cG@l~m))^RBQ%V}Kv8-h1| z*Q!>>F8|=jU23Ldzb_XqPO41Z-Qqt(&`N0$*l48CDOv}!}3~5yq@$(9`x?(NP+iXr>5G+@lGyNp9F;5K}=6wHTP-L zRIi$vJWgn4Kj+=wS7S{{#C9|&YDU{w^koaFMCwed{z?`|dw}Mf|4VAvQGBdVT{l+f zIQ6OT8BfuykEc@0el5mNO?_o7x8f?Uv_AFeu|n*L>S;fa@xoYi@pEK4P^JHxL|fPw z-;v1W&T*s{=T}!aO590FK2vRe^}LG5hYe1q-8MA+3P3!!= zQo~Hcr1w1os7twlFm0XF6|d%Pu)EZvbdRIatArL>ASE453kO572jb7s{OU{L&C=#j zzk4W8QOm-IWHrl-pmq?Uddx89@IO4GlJXG*{9v->nF-rw#oDQKHcf{E;Hkqe5P# z<`GK@?3kM#Nu-6?4ED#%r`=yx>JGL_XA1ZR0|P~@z)2uRfoMrsE+0RRbTF57KrF$$ zDx>|IwP?Wb}-h4qOI3rcW5{ zowhwTT;D(opJO1qb=)DzT@%~#y5hP-B3=;Mo3))k#aisM#QKYt%Uqi8#DAcCej?@j z@M|ujzx6`;t3|f6jUoN|J}syJczD%p|3`q2+(NAzOMNW$5Dg=QcQ{7>ci?FM{rlY|a$%A+i4TB= zJ#vj)5$0ckv?O^HKk?UvpUb%+yeysBKYJBRncB<$Ci*-e<@#(zut_MHCre9f@DH3a zI_1#u9HnstEuKkf4N6nR;iRPYCt($n*5I2&y(teiFM6^=vSxgkngGv z*wPgoblD18yAPJx3OZnTSRmO^jh(8YK0jdaQxHyH)sG*gB&iMcIC1ZnsH&n1mHOAx z#GgskS&{B7N*8%shU4dXUm~R?GwvkuN9x|br;m4 z53!1Du>$P-xC^A5X5CsJTT4SCb3I2sNTA;$&~Hu$n~f$3Zzq9S+#rYa9HnLidQfqM z$+%T%N((tjRO56sFry>l97S$T` z^LzEXBte7s>n@NoP=qGJ|N}^TPm(?0+sczlU`nB=G|MkJv)+p#y?}VIBI^d}NH~iR6 z6&0S?472*n@}*SyOQ7H|(+Dk^_%e0lW~kRqf&!@@Y!;X; zhv?k`de`#Np-^jkt9~d%eLH`!mHuB09Zd)@&xQOAP2+hMJd1q#iVSXHVRREne`8GZ z1pVXucwiHc++O{*Av6o#(C;zwY>VeHhU@Q}c(@kxaP9pdh54Tnhy988IQo?tf*MAB zDU9i!5U=la`UO*CuV5&up=6UWHRx@x{tC*z=0XlIOj(J`;$vAW#!qSD5fr1B)?&z& z`$?`82AidoykAfNv>%@l7$lw*lLlRGRxG%$){qzQy5)#7aFeu#tcNGg2Hi(BJ ztx4IMRwJ#c306ZOxzcI^9VQk=^sBPQXNIQRA(f%Vz@}lfaeun%$d?z{E5~fxe6|HJ z7corM1Ux*Cxfp#~RPeA<4n=a57Ns?j0~(bb>ywI-lG5sY8Nn^=zd~=RQX2)*Xd4T*go(V zJs08mn}Erz=uCnMM&|acm76=8mkx7=puGC)G8wBrUJ(+%SQ^xU39o_pxn(r+`J_iq=g71U? z!3{e~{hqX#nC1q?Cud5C+d>Q&o1**lD5VXT_ruQ;)vp(|{{nF3ou+G=D~{krMGZ(5 zUfi3ArzObb`zFY;({u^4bPtG;rRcmMOITPr;k;N1)`%JAvONHmWNtN=)9}a;EC93V zmlQb0Wlt?Etwsw;egK%UNLIpLQk0W56j+o3H&{-k0c5+sn1 zB#<%)hB6mY1Mz3k+}mT#1s}HYE%}TwFK;9|?COUn4I0>zS$z@ zn|Wks6z=^OXg_-&aal(=IWfNGWy`xMUS6{A7Rq-vcm^a{^)EwfwKEY2TuBgpOZd+3d4a(klA|FuFU$Df8K8`XIRHSEP=)8!ET> z%$3dl?8<6N+$;sZgU!L2MdRZ=9?>IM_aoBC!a05R*B7l)G_Bc<}_ya zYS1y~9LAi(n8$>HTMg|u;e7L*d;Z<~JZJ5vXjbY_^}gLzEA+ySe*&KVxW3lkRV(m{ z8&5wu{>S%Jd4Vx_#tvM{M)1-NrwMhHjTt*^|1?YUR~C*KILS!-%EqewS=Bbm2e!H5<(M^}o;f4}Z<8mOr&o3wPDH zV_SZke@X%UTkdH!tMjpHqd$W>himl2I=}rWXB2jbD*m`rt@Z7Z`SxgCz831}f@?0= z`meQNg?%j;?Eh8V{@Qr6?w*56aTbdZMeWx+`#sLASL+jCNYLtPp>>W`T^Wz-Nn-4@ zuk(cYH0THZt-fw(KgHa+)o$IsTDd6G^UB>iwf*`(Q(NI-9vl5vRn|1K^p^-vbwv8! zuP%Kk(n}&eB+?xsT_w_4A{{HzT#*hEX-|<-k+u?PW0BSq={KlLUx@UQNDtMf-}?_N zS8Sgp(y=1V73naMhKV##q#Z=+BvMO}>WK6`73~)3C6OKy=?;;u66q|FjumOHNQa3u ztTq+j-yGAd?y`K#e+BRSQJmU`k!qjDsYM%iCTraT#E4(bnQ5XQ`Rnzy@kZX5*OS`! zHZZ=by&RnXM+9H{mP^biZyn=tRs4dwggsR9E`A+aYh+Xqj~ z;3Go;hCC)2CXdH)f3bEpFLWV-WAIe%U837_={o*(*YQ{C+}+2ywXY)Z(7H`!ZE#)t zvPA8Lji^Bfu!!GxU>7%^Uw@r!tJulAyMNuUuua^_om_Fh6W81D_2fU+KjFDgqW@S= zZ55hK&zs9Gse1HRXtv~&@?q*v5;PDRzJ~{N_v)EH37WZbML&%?& z@7u}$<4eqcQ*VvG9dBR#V*lXumA^fQKW|;Tt46GUe;Duka#B3)M*esCe{<0^G(2&T*j z|M5DPy~ymPclEb`Rvq|5VgY<_-TFiL)43QC5zKQtZsQX@m*GHO=G(WcZAU`<{Qlnl zoYbV$w7gXN_{_|-Op=^9BrQEB%f3ykwyoN@wQcW~(9UD%u%U78UL-3gE-pSRizKGz z#wI5YwU5nA$VrJ$%_c)~h7F6)w9igUv(HM2O-{xmvg0#TW0OUmacP;EIqBK)L+!J& zGvi}ZNM?LSPJC9jeQH{^ePT*_ay-u_e(1k)&dQF>jwdPcDQTGn_LyN>Tx@n?TB`l9 z*u>=cp?|A?qJJI#GekO4q%%aiOr+aHdR(Me{lE7c3n}p%L`kIZyMKZ&1^GL$7Km9-O` z3HdXA;g940eC%y;d>Kk5iV5P`ozJz75`&VDG8SbT$|97_C>Kz4I%!D6+DdfnrNwzE z;jZQit-5ty@53lT_&=AjlP7x6lP69H9uQ?6?+CYX<&L*seNiyJvag!DKS#;yh<+=s zS2F#}P4{_t)KvXZ=UScc_htNs4I9S&TlFB<4X{vCqv6k2QydLl4(o7eG= z{3-`yyiUODKmE(geer)8+pHYydK{0d7acXd>h#!KB)c&-0p|%>4EL(0Rz0-Dd)|#rdSh9=Yrjs7BENAy8|IdI$>S7 z|3+{-E?57N1K(}=XFJ9O@OuNqtInRab>U^gpY;)>Sq8G_Sg*h9o7=liSGTLUZI%Sq z@jRw$UD~KeU3#HsUHU_xy3{MQF1;UCm-ZM?m+D2-rNtsWB2p4rm)eWeOQfA6|LTuO z|2sne@ACioBK@!Yul#>3n!@MS75=z^@W;6iZ;E1$QXj<%#Ttdj*L;>*I~0E1v?)q6 z6lea~QxqN^x}mh@&ztfw8KpgnC;z}I|I`ZAR(;t}k9|=1M~vR0yyJIi zQ20k3q2`Bx%}&mD^c$7dZQl|kFo*f z6Mt16Nw5&*9ZDYzP`9AeM^cn%^ScXJE(WP5QMw>Ws!;ev)`KV^NSLcAi5R$kMfnlQ z(*}dxvnV-OIa>^LkD?SHncPg!PAE%}RIz5LGbGk?l-(AHJ0#dZOVksR%(Ma84do&> zOx+MMg@l{i2r-GI8`Bsui^MCp$N!Ohlbhf^B;XoHJQhiK5xw`5gS1DVMgzx*###keCJNksd{I+9Ek~&|5!?1Z{)L{1H9HufStSY$JNf zA)T?!NK}6$YE>}mq8sW2$r^!VebN(k)f;uz7j=h(?SqDR7lt|=fVz!D9V2NsAZg=~ zv<^txJLmWNiCclh%|IjeN8&1xxUNWCYb34_5?33ED%UO@krVY zNZL#soI>54 zK^-AUzamNbx1ZY}LC+%aa&Dtek(@`7oCS|j*GS9iWKN!Px@N1^x|J#NIf> zoi*aDhGPuz^ayeE39+L>oRlLrbP)&EXm^Home8h;(1rzQFNU3HRS!HLb#9INRVCwk zKJG{Td_q0cEW_imUAkC*>q}VP6aGDU+;1k40wgUU5&a`2GA>%|&`9mXjBjRtvSt4RvFYZ6wdNvhUql5saRiM6>FQFYZK zRpnab(Oxa`>4O%jX|GMBTy3JeOPg4K(Iya}L&_)UkVhAENP&YcfplHskHi{hp+~Go z=#iQ#J>uU;K_2BR$fwH+qVm=!NL9mn zWZbNJ#JavQQOz>ObrbSvsR{Yi!j#nPG9^-1GopLlj96!y6ELtK<*O~oqrm#4;7NS~ zQ!Riwhty2zOC-Bc zqWg1yV%;;G!1Dp5ydsi3>KKjfA4p)~VB#MZi~5fv)>np-nkB=Cf7)>JC}0HnWRpx( z)hV@o^%}&f0{cTW5Anx-O@V#Z8tjXzuy2vDpKwLKE0DA0$hmSH*c3SExVDpsR4kFI z(-K+Zu0a$_G>E?);$@R2QS{RyYxK2={}pXguGS&0`*exok{*$a^obN_Kor*uiEDN} zQtn|w{2Q8)HO(!EBEgbWoozt;Q*4OBw=r4c=Rm4T9EtSanYgZRK@`hdlkyuLqqE8xG6Oc#z7pqA5h#!dSPZdNFJ)cMu zRuZYhW}+Cnm$)`KOv=N~6aS{S$(oVRiK5^$snXY%{C%w@g_*0gW}3HDwXd5b&5xE` z-P0sR`ADgJ`YfqDah;?vsFGZZFH6$?7gE(79gQ`&?KBj>cx(7qglbfUq-!W@CTXl$ zyH>+L?5IY$?h_5yUlf{(=`A#+yq=m;L8hi+(QHlE-_)Ari4QdW8`aZVqwl4q@QKr^ z+BQ|ozeug6nDd*~8WUUXs%Jg5CHIlquJ?CnD~z9Om+y4cDZf8ZN3n2@j_czKI?_oq z-KxHQb=Q>5&{Yh&r0f5tpRJ(d>Di_zQ(l;`ewig?_ z9(iG`2#GT(?{Uqf{B%E4#jVq(uA_UJNk>kbRc-5MzNYszbHz__7XAlbT2!qot*=Nj zv0S6S($asXuT{C}Wh>W7*$ou+>RU@ocUeol1~*hRQP{XT?yxBzJgkxbO>5gVYtPs! zPETlD)j!zIzpKJt@$*UhHA{YUsIrc2A~kn!>Uv$nQQ>pdvAoZ=W;6JBjTkk?KGPKY z$tZLnji#UZ4rwd8N9klW`l8b;VU2FuojARW;Fb!X7)|}>Z=dL2zIfYU?ybj$o!)5F z%X#10Sam(!qP4Rz2T)T>o-hqz1VGh+oi6@+qYQc={>O78Q*bnv;4a1PU^hv=+@x) zRKt)>2j=zJvTSfz(aY#4x4AQ8tmf$ryZT{I(x?L)GPYaa%VSLjmfE5Zl7W6e1&*;_ zog@-(s6n3OX_C3iv`Ks9vs=0mxpUu~I2>q1p6_%aIn%vJL~saktxh1#K8`2K!#|Tg zPEW}e^On+$bH&n_H#eoDefw+dZgEeezQrWX5WP^XstnNfZ`Dud_wrJp1+HA$1aUZg{ysUfLS)+bYUG$pUU zc$1nwQADbqNQ`sNk_^^Xf_b~7s_Q8l#-|5sW=vV6Ra2zYk#_Xco4VkO{_EHQ^ekYNjy}dinsm(=TNwcpMy6B^xHnp=r$s6p6vVWpIq`qd zjQIBoA{Cv=i0OwLL<-E3r0J0wrqV9WiuU`o{im+h^Vd0LXu5U2S%vYuhEl`aW>OE` zb`^`lgG?8$kMUnsQQowVB{_&=iPi2TGWY!zk~DsaWc1574Lhl~w!>9hg-5cx@x^5; z8mvyxb9Vh&;#;)+QIu_-?f6Yg+>UOTQbfi-w3fVQ4bo6rUeVefm#4Vna>&#{vz>iT z@JO$c@bDNGwMnD=inGp#(SwYheA{57n)s|k`OXK!tuz-@RT($?&bL{ z7J1xJg#L&=nJ10~HRx+;p-+{7flimp{HuoiS}(>Xf4#T!Z|?7q|9F3Z-~GuK@oD|q z3#t+D$945GSn^}t^+ru`FDhTew@YT$Z7-W%msX0jOtp9A2RU+qo*JoTP%yDi!>ep@Q5v6?6|(L273e-1Jbvye2AGZlZ$f zucff?X(>#;R0_wdN+E20DYTkZ3jI{2a44Y^#`Y?OnH@^uWusD$GBcmRt3mn zldc7Dq(K3EdYBJIKj%Zw@_fh&$%m&6@?q<}JUF=_4?5=M!6(QAeZ4$LJ)H~9X5~Wb z=v;uwZ^t)7m zpHiV;WGYk|q{7&JDX=m*1#BCoz{_LFU{jC`OP!LT?0gdJRV6|17D?cDX#`9v8v$*b zkAU{)6JbVCB7`(egrUcV!m@RQ|mxVSd~Vqy{?SStaht{w&+-G)Jj-{N8RV-)D44B_gDt^vu=&9dFc~=n&e{%vx4U8?uU{-gKaYVGV`HGFT?|C+ z91O>M4u(Y!2EoOmL6B}b2vS!KgmWDS!n{+_a40q!B7Tp8K4YU`qg51S{uBudy&|F2 zkqB@OkAQIx20*W@0iYrSz-n?hG_Vebkt@O=taTX7*xesGcIpq^PlUqZ&`{WTy&p(J z`vH5}7oMc|1=agLFtVr*ys7C8r$_aMhPu7s-1uHlZO{v*P3{RZ>-7Z1)DU=HF9iIj z^Z;$69^f;vJKWLh4xh$!gHamYKvmimUVI9M)7in`_%aBd4-W#@yMb^mG7!l5E-=4a z7g%4_8QOYvhSnQ9!Lp{EVDg**cxe~_N6P%6;qQL%V3;4cTm^X04Zvm}g%d3)d|04_ zg@#Iyi+myasSo4~@B#fp9l_S4BWzgg4SNl|At=ua`rP$|Ydt*S-i{8CWZwa@$G3;C zuiJrcWILEqmCTDXC8 zZc9kI+yXqkTEN6P%^~5VE1U>-g*`i5ply8@@W^rocE$-Vxj8|>gl4e%p(DfwIKrBR zO`+st6S&p437lT<03kXK5H;8yKJK)GW+rxUH=!}OGg~mRw1x8IMlgE64K!?E1D#SD z0@-g3eJrh^!-xj3MQsIDCRUITX9>%<*N0r)`fz7}1>9d_4ytN1*w)Pq%I2BEqbDZt z*vkY)k28k77wf^4ruE=kx)JEhhOkZF5URrr;MOvIhoYy{^%J!j z_mNf^f1rD2zoR;Kztf*qzM+d+yr#cxen~6Zzo650|3)ACKBM#46S~I#F|B{#A-&b< z0oD8E9UsTWz{bZ@FHgpO#;sCAQ~j>a4T$ zM7=X~_SjSO+}9JdB>Om>`}8PvjyXbIE*_$byB?$y)CcHq?)&MX<(1UhlF^6b)YR?M zUivz54{dvG7k%AzCvClZJAL8&Gi^OG^rIqT^g@xpX#l*PTW0B+sO-=cZEw z&k8zj{#3fCW-{#)H;FDgG=WyQj;HpM#?js{#?U6cN7LC`M$*k@<@DymQo3e!5j~KQ zPg^_Z&=(IfsMd;9niZQweH#y_L#_>_kAIA%r+W{g4Gp5`iC@C$!;(;%)S(X@_#%X| zmEGv{=pcH{tTUZ`#E+gTQPQ%u9qGo09q7P$9&~buI}IRiv}CI*4H)V~E9y6;sfX<7 zuepurH79GDa;ZL@GscWYwKJxB?i$dY)AVR4)uuC_N%WQbSEbH`_sWdBFO?o0pD24( z+*O`_^s92a@}g36#wlgxqr=KKK2^#EQ}-wj+});3Zofe}f7~i%|Er6Yi<{3?mKIG{ zzCS)uY0zkla@^%&WngZaa-hW!<)tm5%H2JKlqS!-l@G?YRQffsQ?^u_Dz}DfDYxHy z?i*uu-Z!_~E?gD^*v7v8sU%J+*c30|5Tkh>9zWX$4B+NYv0s;D>dXVW;$|E zyh3if(@@@{XCmK-Fqh}7wUnP$Tgzv<*~&rl?B$+Mn#y-PonC&o{XvS1rFQn>gN=<>e3L6HOk;UP~X##O|r= zxZs(*tif}+aOMkno$)K#W5R3MOzW*2Sn|7k`rSKuZORAv>iv)MfT(KO2rfhSr z8H;l=XB#h?vx?Cc%*dlYv%6ED9hqXu^n9(@jmK6j_{Rn;*x#C6|IM0d&1uLg18kV_ za~t+*RwEYYXUm2^v1Q*X8nb2{?b!7@cC6`mdsf}rfepUo!1@+9VHX^lvU`V`GF6fz zTW#EoMQ>}y7KS;o;h&rsoA1n)C|#JrEf-d!a%Cm<&Dp5R=FBvv1#_-x!G2lTl0Eiu zW7Ds?u>-lSSb=$K_VcFJEUUXa+xgI)smj~1<2G&C-0f}I)1Dqo{m6rvm9=A!t=co# z)Sg)ec3`R3JFqU9o@{}x7t2`S#SXfAvsu4*vxngw+195W*_UD;w!fY)(_G@qs@#>V zx>CtD^rr0BTa=AT1-9}FFuw_Yte=%XyS?0>-ESShV)h2Geu15se5Mmy6xo@*zt@@V zPwv9B-*sX8iUZkW%^)^@LJ(VM63m*-3TE~;UD=eyU0GVwZmeomH#XL_JKMLeJBxGc z!3sC_U>dDMSoOvbHl$Tg=DndOD{aw>d93Nh`aAVz50~|3_Z#%9gfp@>oCVkpV82Zrz`jWl zEI%uPMcj{I%R(brpI;)`Fpns9cWD$mU>wc#OQPA%C(-Ok|ACBF4P*^k4r1Z62C+{y zgILY*!EE^1!OX)uh7DN~!`|w|vIohrtmC;@_Nv1WrZ;~GEBiKtMa0Ij=?CIi%VtAa zv#~>2>BFJSGcca@Tp7;}Y7S%bVu!Jl@-Q~YCV_?IC9rwt5?E)q;jH_(;cVmW;VjKN zkxi^fWLi%W*;N?96thOKna@VBBEKZIZ)OsUc$~y~`6RO?lapEZTgj}KdkWi7mcruC zq_7;jRCX&Rl`Y+y%J%A|u~z-l*vBPl%;vW=_G5>1mQa??rX5XZ#%3ApVMGQqUz)*Y zKgwVkZkcRJW+rpmnaQNjnXE0%V!O(+*v$P|>=(^!*0pmsYce{U^*)fz&Ph4!C&*#v zigQ@+-8szRT@LHwp3BrDa@h|na@ndIxy;fsk6jJPV=qVNvGBcl%=C30vvta6g^~HJ z`ILOtqAH)2yv=70%?g-hSOJS3Tfp9IFJRa17cevPLN?E@kY%M5vW0UCndQMkcI#y! z`)FOnVmlSFhRH>&$;={FxVMPe-z{QRdc`c-t(d*)Q_Rli6f^C)#cb@}V%GmgG0XZ^ z%x*OcTFUPDma^6TOWB^3Qf4!z zlpUR4%6{2W%8U+|vSq)PvN119*>(*TYg=E%zBs9vxtEIN2CJC!02OmgP_fh;71JK2 zVvnb)nDqh``*F334c@9^+51%N@Cq(EX`ci~b4EbdBZ=VMAQ9%r4~MkP39#qm zFes+OU{`rOr0pLHE67lY^o@gxoFUL-eJo759Rm?&F|g5pFjS-t0?oMtL9Z$rmOPAt z(~2mV(>xMB2S>oyp#xxXX*fLiF$~VE?GJJ4P*{4hA58kSFE~Hy139mHL&1k$(C$l5 zSobXiZhh+k7ru0dNgum`{+q7Q_en6s-wcAFvw`rrstb(X+!-D$=mhrT0-$4>KeX%b z2j<=YR~l0Iq1qRmuK2*u+d4vr$=rr;nF~A{t!bu5V+w zPi#TC!3OSxH-z)V8mxb^f}cYyVeM-R_&UQJwze~a6X#8!cZM+p7}bLns|}&p&j3zd zR=~UzJ$NGNf_kPlSUYKf<_--g?JPl~Q&lg&K>JvS7`#rt?@-6N27IieZP6udSrOiKGq$_?qM^D~3O*fr5LHq7LO4;&5^xMP( zv_WPiwG2~J; z%%)SlXVN8_6|~`r$y9sB1e!Ep91UwUnx4E_Mt49FHDTFwuTLsHx;=r0xW&?lWs$VT zB9!(S6GG$O2GYR6e$?owH$CR=L8r}XNi(aP(UH-1^xVD%G}p$As`3q}bU};SMt@Oe z8ogAy?z^KLHvEFJr`chp-k#mcU4z#vM{6!tPG4D})D0Z1G&`1}EY$9&te{@X6*;!b zFKacF$JD2N!c8~g4JRH&P}II3s8 zqUtw0BGgX{v(*h3jZ+7FovA(&xlBEF$0l{SiCS$r{Fr*a`m*|f$pdv_>>G9brW$p( z5*>NCgOOZ)z+8Slw4vNe%Ryeh)LHJQY$cmqZYQ5h^^wPF1<2NOg5?6&Uh>Rc{pGMO z(elRAL*(({iSniEX>!@%Tsi+%u^hUwTrPh$TK01OLC%bwC_B!ZB6m4kA)nOwQQp>Z zj{G8dzMQ>qk*qwiRQCI{Le6shNj@F2PVSnsQNA{Bi<~e2EZf}PDetM-BX_rz<-=Z8 zaG70=(5*PMSKSD$$- ztB*gEpB{K2=kI>=x@sus~fZ9@9o%$&JIjb(S#NM+LSpuHe+8$I5CaQ&dmL-3me_8IjhcU!KQ9$ z$>6yg({*mm?hSHhXJ)ox$BwjRr>Z^JwdU>F;{hGmhtZx)W0M!tz3k02zjkD=?S0wl z0419@gtA^`z@E(aW5c)mvqL8Wm~^)jvwGK=Y3c;BUo3)Hm_snz(V{E6@b1ng zD|;{ngs{%ECsX?LVz1iwW|6J>umZ=vtXqSA?3f~yefSv4PCe+)Le7M-^j+c1bI|~{ zpge*t84}5;e-z8Hjb{EIqS>-j1KG^wgP29`VAdoshV3zrWmj*;GS%uKY)wiW3vGkG z$(y0fX-hoonlg+%a7thwZzQm?>BCu3=S24SZ6Z6cWCSzpp2RBOB(a;bliARYDXjOk z6t*NUmGv`AW5d>_p^uWz)|^Xc7l&l9s8<=RV^Jo{(9B|Af5>7t4YHZ(q--`zk;9Tk z=dh_?au~@(U*Uc(I~1D78tl(wTU+L{88h?Q-cR|=CAxrJ-CMv;HYj8oS%s|NSRrd| zSHv7Mir9e4B6ifUnB|5Qv%w3C*~mM^>~@n9mKR&Xf)r$3urDCofRm>z(#aiU4Sbl|yef&wq)~Z!(>S+~Qc~ixn<9IymcNH`J zjQ`^p{1nIE2Om|i>Y)nUj;f$|i3)y9RzZQA3R>PPg)Ngy!N9E)psED=b}ND4BgNq2 zQw$%L6@j{@5X$-#LhzgdXnY|b6x#V<-XaeI19M@0SPt}z&IYgWEXWPY1ar3xXrz}8 zi>{=?%0(&Q7nuyD21$^#H4&_P4To8GhQXV}c%YBtU}E$TczP%XQk(~aQ|Ul(IUWUL z>P14o-~q5aKMW==4h4I4U&uY%8Wq#Rkx=i6t~2ZVoe6o4|_OMo?sK0N#Fj@O79rJQ}9~ZhsX3u&_M zY)U{JT`qe&=+Q0XrP6t}h6+bklZb7Cryhf9z z#ywZ=SD#Z}tG7#eGkl@)^W2fjed~KE;~O>#f+X)Ukv_mzi^=qiUy z2$N4PiIaP8NSC{8Dv^!)jgc1}nJ621O_$A<&XI>{E|%G#mGYIX>*U0mEpm&_yW}fn zvfO<8FY>RqkI5@F&&pkFFUvV@H{{nI_vBpnCvs(zm-1x&ck=v4)$;jh!o2)6*{`-b zZ2mhv7J1l!eVAU4g$^)fy{#eYrI|)3On3c(yT1Np)cBv>e&a8BQ$Q#+4nO z--6AsXvOx9ac531+p?X#+q3>_J=wj_-mF^}UzR$SGEe5mCceZt&Z-N`=@7)O_2|ly zBD*t_!69s0WG_ZT`mlK&`mt@6{aNPoFm`ay0QO^PBs=33&4xS~$h@WvW`1t5ETM7; zThMhVJ9Q+UJyRyIdp`|ls}&=db4(HoU7yVCUZk+XW@&6d-*mPvJA)-m%4Dj!S?tmL zZ1&@f99CME%VrJ9!#*&dwbm(Miw+kstI|R?)3t~h9YbG#a5201q?k2GEMfFU3G)jq zWsO#pvJ>x1SqN3J1%)cMW1Wi4J*#4!ep9iZk;AKyzccTtV8J{UoN!cu*T_=%y0rvg zXE8LNS_E%97Q%%+`Cw|C2k$*|;9~nM&{Jf<>n*A9$vqjSjZK8s+lE2EopCUFMhx@} z9td47MSvy^gBE3dA%0;`7&NOJoJb0U5sd@j(?%uuHTDLhly=Z|L2DSdzBxQw)C?fo z9#Y(FAoh5DFzRIj!*}aLl7Tj~n)Hp{xbTL)e)Ev39$%xYcAla?4B1b6KHE;4hpwT0 z7B8S*PfevM4@S~cN0aD~#eJxIZ%=yub|d<_zXlyL{Is%|%u_mk4p!Q%*z5Z?FKq8n z?k|@8^*xGzTKN6W zAAZpD)u_$C0p_nqH2y;wOa6!XzpAUhPu;tJ?wG$xZSMb=f8qZu|J#_&xF7U<_)9<9 zBnI=h$Grc!4Q0juv;6s+i4BM&)?5o~-iWx6c9^>bmeP`VVg5$MnlvMAux+NqmbjAk zL_z8k2jWJ&u@3b}L*hi*5*=bj8k6Rv1JNgzqzP$7I$}+Yi4Ack9z>Uz6Fbs^coG9* zMVgYq#+r2{u~;VutWO`3h_!Sj{v?8AV2y3Dwjm@Q+t7?CNq>@pHFqOj$Uu^Vb#I9E z?nZ`SjhkR?`;rmZhUO%IM3PKwPh)ILPcjT!=tL+9BdOS~RwR%NBDvTy8*Ei~5{K<+ zif!pflCXs>NGB3Svaq#w*wS7k0o(6PfP|AYY;|iALQbsnD+t~hg*!G#^7i@KBZ1DaY(fCtJuJ z)Lnbj+mGZRYOD)tYbCjeI_yuzknQ9FYTFw%J(nCs{dXk=WDU8BT8|*($!_ulwXH-= zFC-^X{~@G=Y#=vK>jTMTq9)H#?`=`%)5(6+b^vO6899gg?@LCKt>iB1y#wle7CD64 z4n$3_B9~DAVPq`XK^~%|I--^4kz;7PZlsW`CD+hmkz@kdL!P3gC|YR|Ifb_CNlM8^ zauY2!h)f|ed4V?bKzq#~2hdWT&`Qh6d9+?_t0jZXs_AiFj^`It+bk4M%#sx zabzcXgjV)J3(qIV(eB+z5m`rmMQcZqiDWN%hE@i&@M3Zr?cR&1$R=_Ntv#4bC9L)d z^cb|Qy;LUIqfKuUKWT~NhgK$1j8rM_8h^{XrTG}T?Bc>Wj#nN?2LR3`|2dP|g zKrG!M{?by(AJHXAu~L)CQ6H zn)H#jNPQ5Grc#Dv!>LbE0lJ?S%QhUVwJJMg; zF7-#G*Ozjni;^v3_y$o*3ne9@`YRbIsilF4Wm~C4x*-&ubx>5_8^%?>NGPGCf^?@y zx02G`vC`7gvA`)NFOE@q8DUKve$l<)(TKq0+oi+E43!p?(AgWGB(~+aeWEAfj!x$18&!g=u z;Vwe=XNH9t|2~%%CmvVsCI+kMJF7K#4*TNC|2lzBUNrXIO}^$qf_d0jtCZ09x_sQB zhC4b%2z3y3FI$1(LEIc^i%Yz!b*xmZ(#8;#T+(amkdF!lGfHvq3sSk%}As%HGFj!pro=G%-7 zGp1K5EB*HndNcD)>lyttmIj&i4!VwfC3Q~o+?oW2%%t*|>Ey5?9)nA%C=k`wj50HM zsy+RlTr$1z?lJqT8B?s7eqQtHzWnYXBVjrEwYWaSu!Yk+%MU>l1cR8`AHRVDUvx&T z^ZkU5)*BWgS8~F_^@M^~SNvvk*^sio_l5K}b+CfXcU?M2H)ZCZsQ~q}D~_P@%&Ix0 zO7cGyaqH1RTKOMm3fwKr^QNKhP00ij;4P!vbP+3zhx#kwky`0@$Ks@P8%j)p@pzIJ z{m@sW_w3rYzFBzG!Pb{Q+kA9?7_y1uj0KS7$&7kAwR|mO9Qx$!Ol2h%bZne~Bg!$B z#C=)!cgiKUT#$yqeL|P|X|pBuz60>@m_3qP_`Rv+(ER_~BI(p9ST@e_sb*F!IrE;GKSQF|m=)(TgUwJ0ziO_Lz#~T|UH# zOBPP&TO!y_Zm@W~7bXmTLUS-{#ycks9(+z58v)!8?>7if&Xm>dq~!-O0M=EIbnO6J3ioKt4V?;Lg}NMMdV6C^aJf(a6lbHW6P$zf-P zB;?pLLsD`om?0TCC(Ne5sfn3P{i*qwO#j=NBqHSM#TO>z>cdwcb8NudmSQR8FS~`D&I!~yzOi)? z%_{D@S3iH9Ub;;fb!NLHQ%<&wPQq6A?ww9A9|b+UyfWI_eI|^IDW4I42Jj4M!f2i1 z8L?-KpYfY8s>FNd*;C2+%(Jgj<(X%1rQ;aPWh)*YU8hiBd4S$BBW9iH_l zjAx!bQ6$ei`=S`1dG!-O_lH-)xZw}iG^w}p0EcZ7CZ zcZK#^_k^IWP$5_=jBm4XlW(hWi*LJen{TIahi|uWmv66ej}O`i<%2cCNH=viQ#O}2 zNw;*jQnr@1NVj#jQ?{43Nq2O2Qg)VhNOyI2Q+AhjN%wU3QudbiNTIsW6zCF^6s8MH zfi1zvHw`y4Hdi;vw+y#3wpO>uw+**5wpX{wcMNwjc2;-DcMW$lc2{@F_YC(kDl*Xq z*M1i0-Vq^u`!D33I@NcJ2=7^?z2wz8)#qfH4`ro&ONzybPI7X6HS3eXw;DNkEZ34j zoyAj>L=X8AU7Gvp*hmc%JeFh0ppN2dN}}8Ro=(lBbZmsi9}JfBZ|KIw|L}-Bb6){8 zHzTnh|NNuH^zr}4iuvgOj~mk`;GZz&W56aFrr+<)H<+m3n*^ACe>TZ5QO`FNn1247 zte7bOO>RuTfK6dcRKPyo*K0hghd200j8N2nQctgPyIxK|{oarMdaXkBz>1F~2}Qk? zdU~DPg*N^4XCLp|bu`t30zQ&46h$cYgq_=kKKP!F@M=r^ z&w0^TxDM4LJ3fj86ZAF^^tuOtw$=6L9QGCNLG>t#kD|l`;Rk{+djS7!iTj^3euF1d zJu2a&7%@SFfgtQ20Qy#!|GCvScoWs55k86&6GR#adfNkdy(J!SUi1yVNcCuskK)G! zkq3fsdjJ?)T>+K;41uv8P{`1+QzI@exd*)JYhe5d{MbP&2v42#jLa)ndw6QP}0>6t0RTvrL z{Dkb?1yjN}j9VPI{mU-Thp%)hH@wc_du@|9pM-9~N}M0WUA9Uih37D$vO2xbmRBYV zm8k8cruzh0n~p_24`;?hCd(>LMA~HOp$SSUVvYwYWT16+NlLgj8%7tIAw3o)B~UBLG~(Z66_{oVc1St7h47BWX6S@6rcnnzJS$vCzP zbr|(C0MTn3{ejIw{$9O==ddevN*F2x8vV79L)fT34m4Z-1?NEpf~7m?M82o{Okp7 z;tuhoD#b>tx|jBP!SRqXlO+F|n!Y0ZZ~3BPvN3Wk0@uMzHU!}CjdArV_tY2dK(S4A z#6Nh7Ib`Mly37LMw;_+@%94kPq)5NicPwTuAx`_npTfiEa^Xg8|C8)a4dwZ^B;YCqu_FWW{N&8v-EX=NmzilfmtNu z`0a8(`wJR_fQEPsk=Esx(5OR93Me72@?}VSa^|;rF47E**4~mD(XH<1mKH*SgoU0# zlH2i^`?=uYLzMt3RvKbH_K1K|$%|hP60cGyYc8bOnk@Asej0=u1_lcgg>B+tiy= zjj@m1lX?y|6uO7C(9IBKUpX|p%Uf6kYP8UaL$s#$Yq4e4HM%1=G^=+iuo-+byJII= znypibz+6sjj%dK6uy?cI3V;3Q@|BeI zw9lW*J?_4OvTrMlj>{t&Y+xc|3Y203xq!*d7aO=0nI{!WIPVh7wC+=}YSfTNjjenQ z4PT~{3nMA@GGYnEJ-=>H{P9~)sgsm6vsUg8oR$`}4wKC7_4YW*zqXt0nU>J!13?qG zO8v`sO}JunsK4nNmkrE3B!)v3KZ%#p@%#2oecST&ELdZU3f)tk@60sdf2{YJUQONW zecCxT`Pks$+Yp93Ep53^AgPYmV)AF~XW-Z`>eIgDYdXoNE1oJ-)ZAIg?Z(2Bb(^P3 zQsxqWn97Q#u;Y)k)I!ee-q=azA(&MLzlXyKdF&Nb=1~}k-19Z!+Stj4aXv;XRF8Ct z{2l8WtBS~lCraN%oD!{Z*XlIdCMgt-?g!_QX{T`Tu9kQ8|Lx|x!}>JfM!M!#zDPPb zot2lE`iE=nwJRn5Iy((j>8xt@ZsR>i(736=wcSdU)z9pgYrU;O7}D;amh68t5hg1R zW-tgU6{cyYDDeX)D9y%g);|(>|YBRrN%w&Seq8jhJed78by~CA=Q!98Oxm`L{ z>Q?clHGaqnv662lYlx8BaBR{kUJ-#-M9gMil-#FUh=q3{FcBExp1SvW*!}x#40QaB z>8m({{FdE%w%SRLF&9UNJsJCT;#Np*IUPCQVz%WOi(L-GX!$$*$pfLwuNrd#B z(qFD_uFUN=26QV=Xd|n2@(RbGi(&f3#G4aVJF`y`hC@qqRKyj(^UIh8dtu(s6MS}D zB!#-WK*H%h-rrpq)&o*bWi zO5?FU++QkRzRQSs;wd^8?*O36E~Y^@(x9sFln{Z-8kB1dF((zE`kvL=irrPcU zZ+D(hu<3{6z3UIS5Xvfz5OxRrs-<%ByArw0u2hZ2+mhFj;!r~lM4eC7e%{Mz%TcRoRuV8ae??*BGRFLIxie1Rj<(K{I!DCM;1{238@y$gbxR=X3mSEbO{a{QzA)Os}z2 z@+Da8I?_=n`cvu7zWSD|<)iQYkJZ)Itn0{DKhKS^r*O*#?cb-e@!raSPo0z?Xq%XH zt9duz#=2IuJpB6B|H*Dc&BBaPCCgAvjmuVyL&Q(N!KOF(Q=%{nE31ljR{NU85AD-p zta!$;G+xlcBYjyCX>{6W6VxLPS!eds!^C|rkiQ~D*uquK(sbazW!nX%6O zWD(xy9;*#LIe7CF%xGSL3${E-F;32UoF#3x@5xWC17!#Uz1)jB#iqNTLOSkVgdcjY z3|Ij}3i9fSzSU6Y%4jYHgz^$=mQQ11V#@2iBx2Q7Ws{p*${qM2nfq5NlHI1d?>%q# z$2Qxlefjv#C!jk?5c>8Rvh%J3wc+7=`rx@?WjVn;DZ>4{EN?094^3x#`Zj6{M|T-6 z4z^R6eaxq24CXm>OO6}&%|P;@aZaAjmbwrRwdi$-w2q<6MIX%SeQ?q%W6z$hhzIRY zyxy3sswnj_li>Vmuhm~s=&hc#l2}ID&*&Y-T;l2bqgqwg_=LN{RC?gZABC=yxMi$rF&;oRW4%E2v9;yzu0ggwosA7>&Y z6=-@Sk;uDMA=jU>Tag=nD{Al+$0m|!0O))wHxTV;v2dcx>wGh+g5RSOpbo$4mK1JW zZwA?3+?++NjxaqkxFMq=`aK*tm%R~_WZ_?MNhtN>e(m4p^ylQfk?74tA|;XB6B`|g zHNHkRQj-<^!X)f^^{9mtdY!xS3<4q^*YkfUF(YAMgClIZ(Be&#CC7yFPkK{aY+X(G zLw`UzEZX91y`IHQ+Yqi3;O*t{bAFB@GhefZHegD2=FDKrAV*h|y8h)6dktaqdoE00 zra6!fCSIuF`Fv49#5*r=lHAk#BS}i^x-?f(u*cg%E6NFQqFD#*CsN%l`At)45DwG~h1s^Y7dR5Rcq;2NVQZ&x_K^6Qf1 z*4M4GnZcQzZPN>n>Z7#tw-Q3w!U~OY51}QxUKKX_y8mGH${mFD`s!?-U zIBR|USL<}<6c4Q!6$7rcBu@V3aykOF!21Ta!Z%aHn~poK`)X1bhnQXXP6ZKTBRQYA zd2UzO2hWzht|_yhKP>@e3D=(pJ3!NpXFV>{>aP#@Eg=s^An{-O0)~mL4ui3)vW-!n zUcoAq#eGm=Wg(6~`Vw%uGJvID@rc!{CpChZVQ(#5wa9sR%B=c_B{e%nZTUM7Jk zHzcN?+CB`tq&PiN{Jj3%wd|L4^UBWtx`qB#9neDmHmlso0JQ98^K_7bM9GVvt;dH% zrNy;ESLz)X7lp;_HY;aw-bD8($sP?gP&ak}G-_J`1W=*#rFv9_(5F|-`g3Jki}XA; zS=mJ!eFUUT?;qR_1r!jDJCZqbpT&yP59IC(CLS(w&du>YF@yX*tNwj6O-^B%OY3;& zvp1B7)}IHEU|-7i85L&bA>a8E?lQFyMuvAHTu-;Uh_{1o!t9*vbA^CN9?|-{%jEB5 zxppeY8!Erb#ZVNfH}}y!op($!M}9VI-@1P{+WWa2!I{D3b_l1SM(q$3DPo-w)l%II z1|UGXwN(6hyQ6sB9 z?LZ~G$816A0|g)NTW(eo67i?PZw9@RMwo5GxBYEBl4#4vQ(<8mMyP6fJdO-u&mVUK zZsKeiAH|s&yr0jJe^8%9L?QL+0?Vo4g6Ah?MLLt2kSL3K8EctThu#N=wP<{awKgj8Oi%6M{LZ*wm;6e^IOgzg#>YWGZS%k@pkcwB?L3q3wrI} zzDQq6B=aj#vhe+RB9<>%ETjUJsxi3D75oJ#a7}8O9a0I?)D8S}c?I*9w{Fif7Z}+L zeZQh7r2j~@m#Q3xu7DvIJYqStzr>AY@waBM|I58}{K~m?eRn5G==7V0zodKOmKTc7 z8cECrC>0sG*CvW9MZ(!p^ozTTyA@og?13;5b8=>k;^{rdFsD5M5egROg~$LoQp(JH zv~#1^oHnlZ{c5IW3o|_Wi~85>GJvzF*N#&UZ~HySIzJ}Zf0$Urr2L)6oZkYsHBBhTHee0}i#3fo9a(*GN;eNE;gNB){%sw{Y#)YOYoYF~ zRUS*9rr2}Lj_$&*;ABwquW*?}G&q=H4B>Z`wbOF9G4iotf6nZCD9gbLr3cC#HDRmq zc0>$KMkOV))g6%&j>lulN}^)9xs!Z%0Uz`H8tgAJD>r%#J3$ zf41L(WaB+~9Tm*=ba%K^y~_!^(s5V;K?mM&rx%_YBV|=mYF^wLHz!+}E)>#U?G1%p zTrD1pTrL_#em~uW&fE6EOTrgUM7%GIvsQch^IW>NVl|*u>wf!_22}-?xPa@A*KU2s zb6N&>*=^}oHXGW4p9TCTnvS?{Glx2!WPWZh$p~&9E#;s2K{BuIkWH3RCkbe>Dj7l` zFG!^s$!YtSnqSsCvu9<+*5Qj55>I3$EgEEJegLJ68qc+f4USb`c5WA&`5IWvh0~RG z&ZEcWSb2Km@s}TpW99Tm9_05574Oo7fbNuEIQ-VJ=LYF-3{u;J0NEyH&y}GS>&}~1 z7-wT2Tikzil7A9&L)F%=bpd=Wx8^EWVy1?fHyj~1E6cXgm*bg2@MuzTuiI@c1HYWx z;ZKlB}FRPCf!^bFrPZabwzs?`e{@Jg|GECVV0fk2IQL$Yjcy= zdawk_7Vph)p2bSDm%*o{Z3KZJ4o>^_#XUnWhnV|c0k3nzgCVz78A4!M0|CrQf{l6I z^of>9G}A(U+Ux1ZdPQx8DLBW$`Q_l}`ZAZ9xBwrbUC6|4PQ_5a+t!JkaxLewV1nWG zbJ@&0d7kD)TzYg57e9KH;j2i~Rwq>&!5Hfls}JYd@dtILuj8-EaH1>ZH%Lf`I@=#% zEp?#dkE`=#vm~br*EO0qw`S(o*@F(pU0tVJ!9`(JMI^?>`~UQ)EiH!j(~o#@>JQxyaJgVcmzBcT@Opb2D{Gn=@kHUc4!?`J z&C`mH;ZMG%u1~9fPHaggBXB-&RTs^S*1263D!VsoL!Ajqbcd?$e@vXWWFQvrxd-vg zeaNPp0$6Jau3C;kvj9D9`1T=c{_4G%MDXpc1FZ0;xd}_WkI%8rd?oOy+iV~+DP={d zrGB~AGe**p&K}v?&3KLf`H?>}@zad$Bhz6&$o)Zvs zwd~rFT-rI9se4vuCnOkWuYgrpvaE2L_F_gnU@))LYB{ZowG#k8QBTqgY!K#NmoqzZ z*`&i4ZEGf<{WXDPRa~UXkpQbhuZqe00X8r+(byV7lz0X(+7K46ZV2+T(JyQP9WrX+ zk!Wv2g7+nfh(gCQr225~yW+Ql2|@70|}TMB+B zh%CLE3Y8rJWM?6R1Va)!S}tcd3kyZmZ;yRsE<>Xtr(UhE3eg~)+81YKw}jW z9v1kC#aXFNS>O1s&ZHcQvhH)WrYTxTA`xqQJOZb1u93-k2_e>3MH_g%>OC}EGJO*Z z?p2&ikQD9=ltSzek2PofXlTUIk-DGVS9R(JuG8O%=eZ+%JnSYPw|os!Vi1EML!lfO z7ni)-qd6~ANPjplsQYZX5oqUK5!3C95f!OyoR#Euu`S)8UA9nab#nrtg{LEVXUzNY zPG%ilA1}+pZ5qVre7p0{tIxwUEZL1a#e6}^6aDjt$TvN~Z^=JqeAhT2C~?td434A_ zWbhgBuSW{nWRz*20lI$3yGp?Wmzb|&VC|qTlx2gXorC_-x2J=l=j@$irM)ugOhaN1 zF_tvuPWAB0#`{*tsBIxL(Aj*e`ZkLAi>tyz%#6+=uKjlk@xCgvQHIEhF>_PHt|}Bn zsGzGY(~xbd=vza3#}y*DL{hj7l5*I}VsGK)w2r(2E>66?uZ>R+NM!Ed+1OYX$pI<= zT}ndTy`-3uM#LZAbEo1b;yA!LtQ#~BQQdZl8a1;DMP$m18DBEgGbJ)w1;R2N1PTR% z1%!mxophm_u=4@G244~cnY)v_X&X+Y0pI#!u|ghg<9Z{)e${@ad~czyBFg}`daN?%(dr?eCd=)%bN-W%p#Ob63Dcrbee475EbH z)2wuc^-N1w=Pi>8yRe$R)=Yu<2m;yPFKsN%{l*155Ix|Eza3a|kgNA_yG2&T&w9yI zg=T!_Wr46cRTvNdOOl*@=j(Tv`cyZo@*K-S(Ot`3>^M@kiHaeA_RwC*4pNj-w2HLf z{n!cki{XF=o-djwn-4pDXHx)CvacX6TeitXNnx;UyEKbXhZ~9JreUKSVx7V*Oep~7 zJ#i~Si}J=CbC(T|*iNa<*t#5koE3zcSn?K6&bCzrPxa+2?pj1`apNWBo0`8`_S$Qm z{g|^EeV&}~1lUJo^halEFYpe^mIQKyUWV_#uimd3(QhPvTC!s=DlG-)g;Vk11C)t+ znpfqGEzrCCCs|375|hCnd7>)}TzgFq8bC|2Q8$0O3D-CT_1W1=CSIfrRL(3HjK05j zvdK^dI^-beRr-j2Vac-K@B5MAQ#GVi-$PBA1?9i6&@S&3DKm6KM)!IGad$a}nUq6I zrqXHvP;soudE&Zco5slD1&(b+zfsD|yxUh+rz_{iW%X793OwU&sJ}|k|6pg%8-;sL zD2_+SeT+Lfa2C<9klY|%$Cr2#uS;0rEi}{qH;6!7RSj^Vqf8DOiTECdKR{v&u%XNa zdYA0QEToiK#Jt+>P3s>q&9pv$IkB_VE-pX^K1GsN5Smj&j{w zx2PD8n8m4kmBb`(apFiJOHhCOOOM>xT-WO?A zC;bGKR>x1C+YEIkwN+&z42~4WeyDs_Fm4FqaddNCQew~ckq)-TBOr@2j5P!1fwFF+ z_%iGk#TIWZZ%XjNMv1$MNTbf`ik zApYttPs(VWnQB;L&xiqS1Tba658C-+3!~u%7%N%5AIb(WNf@ z&a`r2haSyRe9ElBwM(aD{!B&gs?b7?Z!XLM??52Ak# zI#WoZrbR{a8Wjm}9*vJ}9}Ps=Id8hf4K%MallE z6Ue^(yP4}796S}Po?a5Bvc=*3J1JhD()u~hr#~I}4|kvh?!1-Z+_x4d+7fqm^YZ~D zm!k4_1n3lbds11le~hke3FA^rNc6oHieGwDthq;4^8WR2i#Kmw({~Jr?jWmXykCqP zoO|8Sw^j~G%J3eEc?`R|(>byg5WC5b?b_G=IjiCu0aJZ)TR<38RkezhEyig+KAt*1 z#k^QsFOxYMv4H>vUlVAQD6+i+!(VYkD*>H|Wh7%Y2cYNZreR?veui+k5}$rdtP$^b z(34G|%@uV+b&iVYRWiAx@6l~1pF`dIV2=jDrZ~R7m zMxFEeF#qmUbHRN&-1N6e$gl5TRXTPf?uUW`ZY1~#p6975sMZ^Q|8VG5VEjt?uz=bi z%jJ5Kl4+*>`C(SioD{@K4odnnF#AO{|5oQ{l<0ACuy@=2X@TMfNOW}AoQ%>JT^Wb| z`^ypRTX`|XX%$gFbToQJ`P;mW7{kX)S~wDZ-Mm~)8x%ou!Fss> zP0Gq1V}o4DjGq%aR*H}%Vq$8SHz^+xxkrbaJKi0y#j=|xDgemCBY_LsjY0#;FCnPr ze?Gh}acAA0HI5Qf3=9qX1_P$P(_N={y}ZW=8s2j0nz;_A5PaYogi>PF)$#8%N!-Uz ze}QIL`X4^I@1J!B$J-TUPRF$AXJUX4P3}Hm0D;_QQ_4NwB?prv-$B5lTvsXKh zco&_Fids2O(rVzaN>SXbqt?$zw?)_~&6&{_?)usC$#cB^_JTryrVn9Tm%4sBn(Rp_ zdKGZDKlb+Q)mgy0OJU}XANOscht;^knG-u}hcI@a*yS5bF%rs^`~NU_t^^%tCPDe-Q7IDr_9N1#xh^a1l0bO0>WNQsdMrg zt9lJKi|TU}Lrc!S=E?@GhjgI8(X%%zVo&v;<2vAmZ8iff*yjs*sSn@y9@Evm#9r!+ z`koPh+ako2)gpT1BkOpQATt_)VFGNhNwqP1v+~hW2DrC*WBmLJ{GRFLel3v}&6~Bz z>RN{)21gB7nIeZUt}5}5!XWdN5lvzxXn@Jcl8%X6SZVR5To^E>m9+j{tve`+ z?y>8G!5+e8G!e*IrOBsJrIrS*(vZR8XE{lCP`7Eb*MbH+=%8xNLuULyNng&L8ZW`~ zVtZSi7zvZG=W`~7Xla7hE)%79wmPnYSl+SQgUTLDd{@FAEeCUvV_T_@&Uy6A`cva2 zv=k`66^=1-BBqic*$Jvl==3RAT|23m@iXe2$^ZL36;h>nkYW4{K?=2J2p9Yk*Q>a5 zmGUU+Q`oOdt-(Od-FvH$ve5m-vam0q2Cg-+8_i`E=%Y{wOd{g;2*s)E>Pu>=%i(f~ ztg-3@ZvIP{a58OHO8cx3Ey!&k)AE^5x^nEsF^&74gPpXgmp!!sX~12Muuo}DX&lSm zqfk6}rNS^-k5=dFCL3(K80J;m9sa>&Bk1Bjhn0J>C&ao%s$|7>ou!g&_TUdCcSD!B zi!%rhn&`_}$Wp?8_25To8FZIC7NCVpNv?X}o=reYJ?q5!p}$7nC&2Ter!NHkOYL>e zSXoGK3U=E&#g%WgsqmJ(6qmLxQ$>d;%T}F86B&U>i_U9YQ-Vmy<%*#z_mQSd(h93rrub z)B45|2DlI$eOzfl7ZHZ3|8(1xevB+AS>C#*_k4~n{JcS@WKmmAkh=UK>}}Z&PhoGY zbwq`;(ACb?!C06(_jC6$%gf`ykt0tt6-0=;;Xru%vGG529+#E9B?9;R?`v)Ol-x7N{H8D;`^OFjKv zOefF^?bo~~c_Mpvk$AgIaIU~63gu^_60!Ibxe$w(OIUQH4HDrbVnho`?ZomaM~kui3|9u{6ep5UA``E}t6kr{A-a zvmpfX)xKcw?_Ta+zD2%8-v8IVLJ2{fJSQF;C56cxn7R=UUK)Qx094^Br}ep{p4f)A zAsq7Us(?>*rFa}-F3cdita~4Z9CW?==bH5g@iqGp_yck`>$diAvm@mTg4$;vdMRy- zozY*Tm{0o6p|F}Anf)^b9#UDN7++UpV_cN|fNc*fQHV8i;co7uEoVXBUGCla^%=MP zvi8du!;{&SQzH*gt_QETNP&k#m|TAPO|=!7hYJUA7Pxe#OxW!ZRON^CJn@igvi=PhuMYKUur}J4-SPx_zdyb7D|Hg@6Ki^ z;`W*mACap8n*xPTa8Jvdq5otc7GLs+luU@91cUDFp_&Xuax?_a0omscDmP@9A<4oVHc2;zKo@$KD%K>(_LVEN^XGM+f0qYOhwa= znM$U)9CTcOJqE7FGC9R#ak;T}135+b8r^_=X5W}^N}r)mMxTiEhWCZn`?_t5_p?`S z?_Kyf!<>NWeRMW)Dc2ryW5bVh&^v!R=;w!xL}|IP#eO-(t2?=|jhGU8IYoMN3+wl^ zERq{I%z+VXDyFqmqW{zd9$hksSRmSxE!8mjnpj&CnNQ$bgt=O^NTCnuL zt7HsWXwk@8W1et#on?)@T*q8v$L9t<|jh$6y>fjUmJ^m=Jex1ZX=NtoSUyZ^;mcbYU{flbE4CZX$)TqEm54!|T$VhD+?y?QSy19!m-;qlJ<(%yF2<*e^|%--u?reLhmpsuE0F zY+Lh=Uk=>McvAU-nXNXt-W82c?-hTveA9wq}E9$J(N}`vO8H8h_7C)Kh$WBY}bsxu6r=>PQZb&qEAZ?pM{LHc2f2% zk3Q8+{kfC8x4E60p$l25(&^Bn)xGeUPuK8wvUsPmuGt7xv^yR(D(Ns-E*Kj3rjup% z9(GIz9f(N^@e45-(PKBsCorT@dfl|IcG*=>)y)Z31@%nWvB08h8T^02V0b$f#&19* zuG|O(GHrS`M9Jl!u{)=M>bWv-jF#KI2yKv!0PY z=dKE?P{p5|06Ew~mV6xlwDY9hCBt8i0$jf%qS`u3GmiA**z`|%zSghYd;GDomK{$k z{VOxd?p&IcR?so&*_Q9)JKg7TRd$*fAuyBkYljFy7LOgz0wpZDcYhCxGCEM+!n6uvCbPaml$V7*7u-8BIU@ga%sK;|(%oz9XuHojJMZQAZ(!Hz(y|s<1Srsq_R@9l{SOGh4b`iE|7t%xfsnpGQ@^6_AbUJ;=>Vp2=6*jkBezb{RSKEH7Oc{z1MiN4FJO(?n>#U9mTc7rdH1o?+O_{? z$q$fs^6vUIylS!Tt496r(2jP7)nuDddYUIB&g-%Tbj=e3w?13p$jT2^OlR=82Ka05vIH0)MKl-b8{^Y01A zPkNw@({%kH5`0IXxA>n`kl_RbJpag=Mre#HTwvqo)ife{4;=nFJgm()>Kl${7!A!= z&>j{&Ch1sV`Pe61=&SU|LP10at|VqgUj(ovlyht;un#*qgnLY6aF!~L7!wzRuhy%6 zII;>b!HZm@ycwP=?G?xnvSfe;uU#)I`vR4NuhSpnrP)sa(!W|JN3Ou~ZD86rAR_Hy ziH17fE0Ukp_@Dr8D6ML*q&{lLpZ(wBc?-x4gyKzz?9rpgGQ$TG8e5hP*LAPsI_f<+iYS3nellBn zO7Bw<|jm1j0X*hGrSz+1R`=!dc{1%Qwo(@qzLgfzQPk!|puSC_> zK#>)o-Kx3rvAVi6-55Q;Xd;w*s3)Eyz_>^Up^%-d!Vl^ZrBcQ634+O27`I#&lBm$^ zSzstGCUnILw0HiqHMBNz7-nZV)3fMj-tMqT)pSxKAAs0(>QhpQKZ zUwkc-dlbQCZh*EWDTc@QRBkKyWfgnGHuL;FmOzi5ue*+gUf3(tvCUFTPd<^` z@oo?MkHjAr9lxmSzh7-)6)Y%^S9i8@^`qRPtil)}u})9+?IlQ6+)Ay1fS(h>wpaZm z>s4mX-+63_vS$_>v5anZ#2;^YV>^Ob<-3>&e~ONAhBDOUO3@S1 zyB8!9u=Y$T>nBW>+3Aaw+ZAp8xqd&HnVs8Vbe~2$9q|iwfc$h>rrX~*p8fSzxK2&m z1q`!+ew4MsdZWgVDG(SCivoE@b;8A^xMco0})cEW8TN=H7 z7W+Y+i2@|6%f0aOA8Fk7y*+l;73U{+tnDv}D1FY85t^$J1^P$

    luAa|*bTAU63X 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

    W5Nq=aQxt0yqxKV@=NwMvYtGa8hLK}uD&=T_GE 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

    )A^}}-s0o=K~Vg}@yok9 zI)M`1EN5;04PL1vZJgMsB1Yvs4b=H?Q!Kz8Kbwzl?qAryY3!L(8J%WeFHLE=uI8x! z`K@Rn2hjuM+R%%{+ecTPn5SARW*M5@AzRuY`xMqs@RiHU;96j}Dgek}Y-fcD(~Oxhqn;`MBQWBD&W`R%7a^`I^gHy_(6>COd!I=`=q$ zv}L!;`EevH()hTlC(e|*OddM-@x9rxzulPKYsg%8Hgek1;mTA?ehP;*2gEh3^&8Ov zb9FAkbuPQ>>?JcZHWR{sG}#_#)e>h<=QXjE0(aFW`bwLNe?*04KyH7tg4!{y67DSi zm68;SI4_6uPjS_#QWws}hT*IoZYZDCKw|6c#``7igXd-;O6;3lZCP>Tkgq7o#wY!k zgd+(moBDMC(iGagE~9SPtr}0M8{y|_P@2l-uN!}p9zXL;oIJUHq_GrKnHn$-YXAiw8q)@eiWCv!il2agS~a>Do83bgw-froT8|KBSbdN14srHA z*F*(w@4mO*9)EWM+a%b{@~!pW{7(I77C#rKGX4<~N8Sfp=rUE5|JTl|`XOO}%=d*> zCFMkaPs7pez5}JQKt-y;lETnSPTqY_-y0T?P4iv6pT|>6{g#21U^_^^_Z1~+d*VlX z*R`B@p^{6Tjpdq!t(GselHwF8vfXGjjuQeXrdhYqhQgL~Y78MAEYvn2qWWxbLZt0ET(nZuYtaCr*F>IyDc3i;3Tr2*)wK#3eAwl_ktDL7%cNu%h z54AIK?tl@onz`ok{uLyxb48{BCSshl-+TYwI?&)_C3_GLhNbAlGviH`gZiz3{d&jK z;D^=2i(RvVtQ+&nUu>r$KleMACoWNY&MNQWc`E zOmmY*W5hdwZ__sJ>YiR9n-hQ@xu z7^Pg&rT?78k?|iH+H*MN#uNBb>@h-nx=P%%$@SL3Y}}MFp+oB>stfB`V{o`QBQ?EP z#vL5t2T?4!p2SdGrA-Un;;!4J&sK8WN?l9^%AlkE+R(vDV%W}`b040G2I&$>S-a8I z-Hc6O^LlCjEz|8QJiHtYG`XGoTzE7(Algz5A!U@i?Z;ttCzAwH6kb~$h`_qV|F*M~ zyW9kW_KpWfUY7N7iwmxRc7$2a-pZca*bKY7AFrvG1i_pp=Er^hU~=)%(niI#1~f%5 zzRqpt#NQeeKjJs*y3l#JTA5qGQK8hA=31zgFwZ)XoZ!Ga9QJMn(Y&7J0$aH)-R4XZ znh1JIl^^V{KGClXBni~qUlh|s)ZhI%O4N(#T9QgRLb?7t>Y(_rU@2P^0!?h?YI8;U zE+Y$gJYUj%zKHSg&Yx2XTRUjcG$u)cI?cgUPid+vZ2SJT5@Ykk!0U0-+BH~Y2U>KK zl-0MWvP%`iVI3>Z{P^^qS%}qbG_)4-ya_ zW77Ss;4_y!U71o)9w|asvmSqbQC0-wbVAEXPEYSL+M_S3z;GyiPyIs}?%@$Wb549{ zX&2&}OukF!mc=iXT!r(oe%+pVpbgfzDhx_FWV`JysUX32uZqSVFh@r^(Mxqz-mCBPjxPAHaq1Cu7OTF2E!o zSp$ON;;_NgYTSlJ?UhuV&NXcNSAkmrD`V3~3MC(MN4Y`fQSZESxY7JJ!=8uNt0XpP zc|JdGFt|GmL+!7eI%_wzJ3Hn zM!`UP>@A7}-9^fXw+dRg@EsM@xiVHm5(GnSXE+M^eL8MaI?x)FjJGWx zfG$3y?$P!U3{*KHV!>hsnanLM2U0zTv6&{e4#bffx%~ZP(+hV~!ESa?;y~gZ>-eUZ z+T5gUV1@|WcN>1F{%>a8zBsZl|1qSMiIwhzY9R(cP~76d35R=`w%Ri9 z&6}Z*+Bcf?I^rK^QIa)8qbJX zwUQV*`F7FsyRd_8yrRX6{safG*6Y7+-~t`Q{5A1+(4N@NS`uSX^K8V zZV4;)ZROgk`QXuwU0oV`Bp;R-3jc?LO7Bc2;6=oIcekIDn`^yoKYoVx=~(C1q)2JAnfV{$)8Mm<9m)H#9| zhf{~d8}|c#PT!;jHxcXh8Zv5*1abZCRx+8v!~P3Ok|-9`>KdQ4G0IDd!A+t`K{X+$ zwVXj9WhIW!>^lk9seUHf0=7j^ydf9EwHfB}mS?dKesKyAqqJ6$SmeHJq9%`UUI{#&+Ya|~K=UX!X+ zMSDnIcGHNa=-Q(}!Aq^$_OJ}Ia3=y&YOPtJuol9>?DOpMijDhA`Pd+Rl%u#f;-d8K zjEzthiZ{%0)u0${h-?DP7^Tq`)U6Hg7km(Qj zSFC=;$x%Mw)*=F#aWQ+HYU~H~wJgN9<$!oNOZUT>%*a0?6kE(Rr3;7GFm#@ z1tJLL&snU>2DR2dh_vBP+NS!-9SS#v9=S94rGct$?sR$A{RZn|w?!Y}<|%LNAMB|Gf16H9?tD|aYw-`;fa1z@5w zRdy=YCq%5MVxorZ&gFJV5Iq5H-J;%p;^b(CbM(0b2bB%DEON8{`OH|iVmDE^ehIPP zNH0IWzYDW|x}3=KtyDKQjLBo%OEi0Qc8N3}JP zdI^Hu9*`s$c>UU^67?YM%DF1G5>3~wBaG;o@fxUfdNb%LWDbht?ok(g*`N}if$_$a z@)8T#munAD3K$ z-E1lIEXj@C(xhA}CNGwNyU&L<)%juFlsa%y{{vS*sJ|lVodv!xqteErpi=J(4Szz3 z3W60KdB{A46|^Q%v3EQToFI|z!ByPAFAAR>qu2(QSY((@l@6rtpN0;3cH^c%9B292 z^QNP(#LuE@DB#CvdxT#DzC zidyeSQaH>kq2soEzN?JsKR&(qxf zEckCK##iC5c1PW>@dzyPl-Jy^vEbbn(7ab5pMlMvAHtq(L+~Sy)1^CR z`~ICg#Zg}9qT!7xo z{CMb-S<3P5vBW3vSZYJcQS-iHyTbM^9EUM;@K9Godv}^{>nz~ec?sBdea$!zY40i^ zP>IJJ*puw5@kHEFVY`usTKd(N$P}$lAw7(8VI&QU7J&>QakoS126@-2RAk<$U^}!{ zi>IqZpA=LgZ^bK@pjXT9Z61U_`wM!<$IH%_8XA#8F(n$w2k(Guw?d-ExvMbzS-VnD z)|Q}ZLkYTuWUp^dg>_bv#Qf;fuMNTG(#HHvDn<<@qmw*A=l)7771t_%YhyFu6)Jtc z|43;G`$?{q?%EW+;Z>An5rzbD`YZ6*3K*Cxr8DJYSXlFDVIATq8wR(#_9zsiSCtQP313-Ug068Qweyo>5jEoMP=eWKs>qJotbx@N-vPDr^^p~afYqik>bj}V=k z(yI#%R2<XX=HkQdu=^qlhFS0;o<6bKORTm91Zq~>3cpoW z>u@QbC>JosjMC35^Qjxa`5w^PES`Kb z)PH@a(SpdYB=Yqi5cBHyL{Ki1v{KC0@> z8=srGfe{C8)TzdHD(UWQo2hX(Y11xK;~FI(#VE7}rERdRubYLR*_O5$P1_Ji?~LU1 zaziR$p~cpHx9!K>(qdmx5P{4jKnNfvL}7+Nh}P;1Q4FBuFUk9SpL6eIfXH?~zqfu$ z?%Z?k`TLybJkL4L^L^^qO$L-e@J8*WP!Tk;dU+CFA1dM)B99VNA%R#QR;&II9096~ zxs%J(URqc9EaP-%Dg*j(M*X@;T$Dyd6>?FbLX@Fb(u$^kT+oBuMAa)cZ&J0FYHrHpF8yva&0iVmK*`MJy5~(!vV(D@Z+=05A)wV7KKNKnrbq62=?L#|+w3MmSY)#i%>GFLP5#o;3?LpTuhRNfj zild^DTJy{65rkJ;gK%`lS_`!qYt+XVKV_-Fz8nDR;8SRV_xiN3P%?EHQg6GPJBT7> zQN*^QJ=cbh)ZV!P%oXP+1#O~59JS-$HkFmqDk`~;a&}e#Au~&9CwjgQp_T$?gQpOW z0a3~(9Jaq^%szkaW5x`*?%`m?|9E?9*r*_@{oHwbAkqflz|STqugI4^8i{wll0MYw z5unEJ|7N)n6Bu~Y`Q|cG)4puZv@i4iwHJDszC3?S`t-bB^8g1Mg($V0#NU#8N3^Dz zc_XH0hJQdD)8A4$#*`uAKKG6gGwD$}hRh-w#=0>a)i?2dMoFu&{B_!X=E-5*Zcusb z18zRx2IBO6uIOpiDCUT*vqvb&{J{vc)X4_5uq1m#+@%^N)9H^(v?p3mqkW>?BmpV0 zsnL3 zXic}jESaa`9%#WKrH{mhd8WEdoVTJ#_*-tjZx0dM+)-l|wW55^Xz(>-`Br0A-peqO zXD=G>%I$MtF5GqdqI|7{G_P4cY{i$k9~6>mlFx#1aKDURNxan3_P0y5CdTWd95v<5iwZlOMCxM4i=&-3?r-fH!;KFrhN*%`eKodtQShS}qYoj<;(CR;??HaoT1~>g= zz*w+H+jh?GmpE!44xG0S2Pgx2M!E>&kI6U%F}?~k7VK2I{X?_|0yA}MCafj&!Rae* z>m02cg#q(+{IlYg@CA89`PF^B*H<*s>>G=`{ys5N<#b~aEmESo2fSL-VbX0o(XCk4h<(vS^th$n3dw3BAj34V;BDTY9_CXlnx#u| z-z{HNk)84*rFJR>kqR;EckCs%C{No~oKxW>UMPL9sNsIv2WI9Ni}E&@o?iyqHPfS7 zUveGuRZ?EpKpUV!C5nAJ8w}I(Rp0lTbH`|LCBFkRo{GA`SBtcUxs2rG*^Mc;>O)P_ z7TIMh6(ab%)>*m8kZc%_Z~gKO9|J*hG(+O?KjXCU`2S?#@o&GK#Zp=qC`*PRC~lqA z&KHGK0o^pqb7}9=8nnEejecZz(H!nICNX>E3=(kuLE@MckTN?bhhI9h(EX#9hnYjP z^v8IYaPetTD}Ei~Uvq8|`=dQmIB-p#S-4kHNspMdneCAIly1Rh|8Zl{+Z*`wt6eO5 z&i0HK3$_bN$@LG(B>zFalpjyt%2)NoBK9*1V)V@^;S1uv=e34bzEz^xNa0=0ikO~H z8TUP}+_xX$xfks52ivu#(yR<)ft%FGg<6y6nNFfIBx~a~IzIiR}E#K6d z77(Mm2EkT9VM8FMu`Gpn;C?W^F?o33&vz7*7dz6nv+|}y+j2m1JndX%`J>tzHa!Z`yUoUe=lWy)$8Qf3w`6H{(X-+dLg0*LiADPX$GrRyPCA>K z#Vp!8+hGsWAARfEwC*gY@tf72>d=-!ZOeU! z@Nafzr%|T{PGehS1tBI}@e0l1M{)@Dn3i_`7`axo9&xFl5uya}{D? z2L1gV{k>&8n1j~vhQm6x>>6a+dL`>~>>{3%Ty>E>v;Hb}wdEV^EJ3vB1rdu`xlDr3 zCU3iF>&p!B!>6YHjvW$QsBn5H=dG&G zRKH9u!a1Hwgz2Pziik{0of$@+PZ>flP0w6jb?-;ty2h-ToMDsSUTw=82;Qu+i=)1T{(Tj2s zeSn07wT!Ob&I&O}k7nrmG6HU2&;%W@VJ{}Jf+Qz;&aM#4(5^Y*NbSQIzJkbZd`#`D zyeEUYFf+q+f8Kw37mBx5>a}vEidVJLGqTsQlm` z^*uRhO3wnz;0J2k99P|7@pKrlG}jIG{5>98K6AWoaOFRx9wUoVkB;T3$CAzb7&>0% zrrfUpDdR&-zsK0@iWfmf<-+>=mY4Z-+?HzaE zA8lT9Kp(k|RD_wvTHI|5{taR zjlKm#MWtQnORk4-LItGpM^K-KT8_AxJ{WM9evYk>WSAVUPjAUdMBXR8>V?P+e0z+0 z^u%ZX2DX;eidLOZZuo%GC1xhX%=c)O=jeMp-+DN?^2pej)aONuXs>&(5@)3A83mh1 zg2b>;Iik3~u=+;$X^0mYu{-2~V6I~n#BZNvT(*^Fi6c{lftQO&bBv^oTMiSBSO5tC z;wtTyX$HSDyZDSCshBUgya@m7%M!1NFpRFMmx^bAr72o^-4()PN7}w8uI`R|eTj`H z6dCH81&$Rbt=B)n>y(^Pv(I|{7`+zF`KQZ>Mp#>7B(9IEyh9ChH7`n9t{cD2v;Fa` zMlR(RUe-R#S7_iLs0c3TvLdiP_Z;d%-Dq8+ z`3GC|)z?{B-WsWkI-3j0@K00OJ6F$0i)KxuVmi&Qc>VVLJG`KA0}%J$FWWgL?LO_bFR0L z!DzB_>G_K<;4OlYz-V_n`X^rbkY@`Jcd8?OS83&=Y~)EEg(}_1@`Lst%^JxM==U4? z*c3@q#DBcnnj)w->msc>C{-H&M&NV8^o$F`EzNSs9&4UP)ViY>POg$@-@wpvQYiB6e(?YT{07wSMR*EdPMlJ&o}QuEA+|HVGF-VCgdT34e0*@aO;oh!`HXleE<{P?!K%K z&I$U!w)G<;m)hUdy$3G@GsNbo=1%@2r0IkyZzF{?1U5oGT)cJ+*m(LSKA+N0JL1CD z$$S}5<13@@@rBYKj=-Hqb%&u zld2f*@A7y0hs353H)vf5GIDR&=YE)Irt&WgN+PQ z4U~xg?g7174a9@5p(H8=rcCr0#Oq5=(7w7YCfut$>4j5$}QY z-pMBoGAxHvnzEBZ|8dEiuAaQ%>dEV_p4@o#v~MFDRNB35?5SZ0wn|EMygA4I;^lzDd}JJS|f^DiAH@<{CGInct# zC-G5BE?`<};6i{|(zbPrv%s*G=xhgYkRNx>^TG!FthOyK4$A;9K+dA1^G=wl??;rK z36eY?pOp3SN$!tNa(#S~dS#M~!Q;G%QcQIU+fJ7Ivcp~4nr{<<-(Km<(bg=;kcR37 z5gOh(aAu&tEH9D-vo0d`0vQ5v2_v|*JH$ zAD`s<_$2kpq}L>2Ohd{6i7BD2iOFHBZQv- zrM{dyM#qS`u7W@qZmYUp^oauSf{MJp;i;$&BAd2?L{sc`#G#|18l9z zS6U&|@8M{1D|2_6DPod;24Gqr&5vb;q&0=^{HI0+FPVXbBKmeS*b9dp>x=fdEmO9I zh6*;Dn{1I`rez2f;XU^=64RDp~-8dy)SrVxUG7cNc6{UC(_f* z_1JN-mT`OtRLh3#8SIBLA8+RC16kUdcMu*te zPqTcz%6-08)*9$ndALhHYYiW$>}UdZQC;vp?Mdn%u_X=lANM!=2gJI~a*y21Thd-7 z&>QTs_(fd70fp1khr$&FV}UVZiCdMmjDqo>t>L3%koO5Cp0 zIxZ=+%(3&+_M)_HB)Mbo5`0~v>4NvuT=DYd)kersY=0mI)9nK(XLK;rTFyyCwt>W< zf1qgfCtzOzW9OO;@?Jx97?2hQ#j9y}!B_Umy&Iozc|$pX^Rtu0RBDpThAn$-S`d0K z75*|*%6(M0m&hbHI;w&9Y#=t#F{*9r5U+s*z|q)3YRFmkj02w$33TYgW5E-^dx8;K zGJ*@St;~Gb1+r908@38@3mor^FKgS5inB~_Oj*{&=s11Fu1;UCUwsnWubzD9>dE`B zp1kMk$&m?@rJuD^iXcdCw@Bhqi;{LuKuOE`*+UZS%oQn~23I_2Wg3(c9vZJPMgFa_>uC*3r+{+cZN@0dVwD>|4I}&&8n#mGjEF4RJtJ} z`sNg|-#;MFxRxIAeI6%U_!rE(*;H>bi>~AAS<^EqIy7a18{?Pdjqw40AGC?+z!bJJ zsSxgO@@3G3Ag3ihp9Q@h@cOjy9r7aFD{@7(YTry=W{==w|01~Q!yaLZ@pZL7-hW&i zjt)+%duQxU7J320Gi3dwfvN%`*gPzma?(YmR?SLA}Uf^v-*QIVa*D=VIq9tQ`J#UzrPW|F_Z+S1--s1|M6wbM)kRoL_2Ym*85ON& z-ivbAt)g+K!V=JUFlpOj2%SY;Rz-}%*Pdd~ZlH2ZH5V!!!-%ztY3yXI_hZl-J29bP zE2@Isx?E%nOsLA6@Ebh2dC(nGl+z5nMYs-;l3M9=NdpdKD~AWh(i8!ep?j4+w?#ys z2F(NV(kE^(zM&fE*EWf&LevC#v5+lFflDqk=86YgqF0=jZ51)Rw&~3xWFj??+z=0s zIHR_`cg%>au%4DpdrC}bBwktH0Fyk-hY04qu$P+(yn}1~bx;U-wqQ#&trkS+t zg})Fq@VXXcs}QqCt+J-o4O{t#)AF~#SC3YKS|K>Fx9ST>I?UF>8==tdDUy83;?JaU zk-ATI0`3u+brEMVklbE02gsgLWOs7(LB}1aCUWEIVuX$nJW7u*6>LL z`=jY&N^POJk(*kze3VF_34VbvL$_+K5ORlfD#pf$a37CKCC1UsJ6rF;~Q)IO%| zZc{o6j`aRbMPr~(^sks4h-sxQq7}-R)9n*QjWG2mGOjJwb}LFE(7obVASO<&=nnL0 zrTau{?;E!KyyMxQab}ej=ufG!vhP>8jW_S8mu_s8J09S?f-w^qE2Ll{48IR}=uy#Rv}4!+g?t(I!F^FVKRf4sA~ zH`e&z18@`bxx-!6bLxg`HOs?l#qFY-QRGBie`iH>a5Ce{`OkIsu-*C5rANMQh1xzx z>tnLRQG27<7ag6??Ud@DLMnO~*Pcb;V-IG=3-${4!_;%O-{P2ldj1Ff1l z75Yud-J_=aPIllfx$~S^Ic0Rm8Msj&#Dz0)SB^`Vo$i0JXcU%wTGM^>ykJCYx+en> zJf{OTzG>DF!Z9?>Q8F)@D`N*(oLS3N=Ni9xA3fzcQNnR4UQ36)PABMQn%+s z%EidGUk=GK=8TzMmVCl(Mv1rDZ2Z*+W4IQY1cbAqQ)9{#5h6`MZ57kTe|RG%9KfCZ z3I9>?QS{<;;w-;8cMBXhGVb`d|4<-7Ro1S?QJ~{iSx1@}zsw|s0?LC~;)D`W1@Nlh zB0ghZ!TazVX)WmYw{Fj2@G-bs_y^2+un)@Zd`G&FoZX1+e=w_mP-|+J_Rwq!ZF*nj zP%K^Awu9J~4p>L#d>RH87k!f?J##+CM>`&V-}GFoH7rEUNp`E~9MUi5XtSDmcFKLF z&Q08Q5r=ni$6&ZkYxsuBw@7nXSjgTO?U|0NcG1Fe`fpB87e}!{`r(hf_~xV9=JN_k zFpttcS?*i-3ZuUR%ZQ{WD&N)rm*_jw{b$VbO;rMtBkXMCdMaj&C}JMk=5`pRLwW)# zRGF`g4U<*i9^^6m@I&$PB~68~%IY!bby3s=12*d~!rP98oW6rQL@999eWiDM{M zBKgB4N=jY1KI7v%maLxY-^(-N+Q`;e<$Pu7Ls`cQSRQn&srjw**` zx6YwkeL$s4b*uA|ZncUxNPF#-73-Z2eVDqmWkiB;>(nh(RFCKh^iI9KhVi{}w9pqb zSc$`15Bh9C56>}{jEKZWF#tXEu(o9{b-S7XHjA z$E>dREA}Xa<68a0(9$VWKPYma)#QF?`O;7bnmKNB@^2{f6^eb<#39CI(t33y)Jw@D}KP#|$ruNLB z_7q`klfO>)w>EWqdM(&DwiOKbX`+OPC^;WiC%Y!h@|q@wkR{`icxkdce1A;d(JW?F zhGf;`Q8q(Z&*{xt{W&b8k3=H9;mNzDdwVhKF0}HJ|BlcUI80$z7WNWr0-H+yuVMRs zWIQuRvrqDxx1&G|%V5JCyYsl#@V_xyvCbZ_nLGEXj5^16yc2lGtgcK+5Xn_68Sxf?4K}5*Zf(s%_83A6_>$dRNa1s8&*)z{;QN^9D5Jr1*?Z@K*K{)5psiJi5;N5;v!c3>+PEkZkS z?XltyRznQw4=+wj^f<*qbW180jV(MLQ-HAXryfPuCvA`kD9fN7-6oQEX8V8KlWYO+P#;w?>GNX+jlnA zzGc+D+o>6swoi_fxyc8s7i-)QWC6E%1rf98_r$GzRNtr}Oq$Gn?z&Ri@Mhsa|06KeEmUSiC&TJiFEhdPP8+|kYAU;Dl$aEKZH?yKqZz1u1n%1;e*)I;f zJbq^BF-t#zNBcyesZG0ozt-do?APww56_rU@oI&*HrT{kP6-PWP^cGDSNKZDPYTA3 z)QEY_dXl95N!6dls5H{}+?xR>@(g6~%;OyFN1s%1wtdPzS+M2rO5eid`Eh$cS}+T> z(0(~84Vem`pOq0QJ(yv7zXOS;mo%O);Ew2QpfmU(&u-am4&|t}?Uss;;RjzP)*R%cq^!UJ8lv8ayg%nxx$@N?xUtE9K?L=-PKy#3u$~$9d>}%cKHClam3IoC=YlHPl;&?dFd%*pQpuo^ojY z43UOvmSjxAk$e-4`NJ9P?Bs8e!8p6L`(xYDkd6JrBqg;-Cn#+mQH@5LDAB)H>( ztPt*n<0n9Nyd9OLdH_?=Vq&O&3*`UFTqR{g?jNj+<|%91weTBIWW zIvO2vcMcS^S^0k*HABUJc&h3V|AD%T?9BD+D9PiyOR~nqlRj*z>;n?(Pcj}d;E<2` zTS-VulB~ZQV8ymCh^aP-r*m+_8VU4@C*dTanTY0QdOjs$fzw*(Gw|+z63{Qqw>w`( zV1mvbt?A;%{)^Fz8f{k{7e$^=LT8F6aias3p+hv=5#NrH5}AZW)`7tBKknSxt|Xj!zCf(z6tm!LSvq}I5J@>}G5yOyt8!tq;bkUAyXv!Y zu&Zvd#tow{ecv$pE_zFgB3S)D9Ql>MyvoQY0r=Z!6-D(8r|Q!}10-lu3OwsV@6j`) z+p**q=%3o}o9d@b&mSbMVX$$eX1cQj2daKx*4##q{}703A-Hh{PH7=1*MYbeYL)_V zXc}g)Ef5cGZheyP%efp}MfzA&mh@Y^w(60WVSbz}g5wU?!@G4%zWwaDeKeQiOqFtd zS}BXy*`-{#IG*2S)hbg?TtxZV7SUW;Moy}X|C7o&ahoHlHrG#(GdnG3B9-&u|EA29 zshouto}Z1^2VA}ylB#$rjbCA?Vuohphth>(4dQUhaAN2s#bI#o^??j6 zbR$1FwNMiSXDP;$D5L7TN`&Pd8Goq|(o>W_#L}Y;rqJ}<1 zGy3G7Rzxe!zt8kIO!W!A55pG!IOWjycrCPgju$HtbqX0W(DHA}zE0YG$~sUf_X0V& z8Hh-s-o;tp&!XMVkZm6?LY9d1Kd66oQ9q%q65;Y=OZ&C^uP&{MvL-*cELd9Vyzjlq zEWW}VoUf4Q+dmi;vn=%sWiL$qf{kCf^-GUru{Cv5rf)4@>*I$1v+}X1ZYKwt>Y*lW z?~(V`?i@xl+(gi!JBwQcU|1Soz6 zK-=@II4^Jr$2==>(ZWsWV{2XXMLB5Pm&Ka^wU;$TTpr4bl@{wxDpPM!I)*2{n9@UB zsY*g{u2bI&Fhkjh*`v5J!P;EYI?9sq9Ah=lj$h82>AuS>3C$PsC8gn|kri&My@eLo zd_)i}Bifk{h^~bewIt8tXl^x=jJOV2zJ?&9Nd1~i+~uy@15&)gKig&7D`*Ydq-Q%0 z+H#D)Hy&3_^&Y-GFZ4kQBvD#xdT=z&cH!XCNZl{T#%RQrcJTwkb|_J^+_GDk=Jfj5 zaN$-Gye!Lw#?QiX0sl&$92V%J3X#AR;r4y(S$u`Ki6yLz0B`}qKd>wj3{ohhF@o!g z3oY3{Ea`miQ5naH`#y=>1}yA+aQ1HcKJKI;VdcWA0Ba-d{$i58SEXLVJ%7L9_K8^| z@CHl&#dZcmIougGBU*i+2estE3s zs4K=wHcFlq6Menk;`;@Cz^8>KXJSjZUJHE(hnx%c8?&GP-FLlP8 za;M8iRXGo{pr&t55{Of57HUTILT9zAZF`L`s1Mrs&BmNE=Ny;m{YS0oN2zc7VLXl- z2xHlN=Q6ihbhFkpcZ#-+BeWxi_*o89{ zyve3^AjdA)BW6X$&o>ufzK!MW7UI@|J#yZ`ofjN3Jx5IULk8`Q1!;ggqgvDVGB`|) z>ABrls7^3VF0=3kt?3)8{Z%JugVyvhW4@aw=!cWEZPFOKA1VM%&^cpSmRa;kt?6qn zdv=anejYSC9@9fp^Svn(Obse>8}n)8hh#0VLd3k>MAb~g+<5J<;K#<%g1*MZ=A?%| z0P)iH)NU4i(FjIv4N~_n7|Tjb_t&fjHC=Z}n~G(dGA8JM(Z~0H#(4i_YsULu`0@SE z|8M)BNB!s0hA-*A*7Q|uy+oXskAX4YN3?i(1aL`_oqq*IE^xy%!dMVf7NjWh54d5N zGCXb{VBBZ(8sagy9ejTXlnJ7fghmTPH8#MC-I;AXkJAR{vJ^!P@^$vyN$5_|^eY$b z26!hp7rNxSbzM0T?5W|bc1xBqWdc@J0jqk{xiDAOo1Lnc%kM_L zi;3aLdOtZqz2(k1`LbT`1obX-f(TLXEfdsR=3F>K)|)dii=6MAQ$+Rt&6O;Y>hpmN zOP$M#Q!JACBXM+&1q~MY+5hJ(@@F8(9}bMigO(BGZu?JV90z_P`JssA2PrwM&l#5) zyg#>PhALl{xiH%(&KAM90SyTYkC`eXsLph;9py~7MC(t92`5ZotIm3zw(`IN+Q%1I z`}phPDDGs%f>>%3zYeYZHZH6$&>pTeoMKp~ibieQK4a0_rl(l+@%ot=Jxxp8{YK;| z-0KjdUYLPpLvNb#2K%-5jXm_J&`a8@w+!=SxZl{vA5>4`+;d#YsaIgaB0-6^_}L;zqPYmpvE_Y1X?NAXKPJ2!M1E%pyzNeM<#Kg7tyjbibo}8x^vm|-6Ia(S`MW0 z76|;2pMC|`PfJ_y7si#a*G!ms*~)&_8IrfUeU^^!`wf{m`y{pC;L`p0%g%nL>W2lTa)3`EqUaNhYL_1Q&qrC^ z5S>dq@TeR&y`P6E0rx9Q9QU7QgYu=33o%4Ap|bfwhPzSTAc;JB@fv+EW7%^{J1$Oa zy9hY0R@fc|*#7VrQoGCN%XnBysHCevXQ@V5RF|m6jAV)GEI_S`YL6YEfaxQ7?WJc5 z&@sYCs&B0uYGU+lRPPlX`dibZgPB^BYPPQfO}87Wxt*Sr_VX_0wn z&S&Y#-`+A#Us>GVO1`3MsO7NkzFec!mtPPO>Q8Vj6b)X`5s$t0f3>PUWgL$ZQ!3+l z88u;tS@H>olRiz8_vVa3X4fWR=@E`pR~Uf*xh8OMc{vCBDr4)K+};~f z@~D1VHRh^;c|T$fS0$|XvlR7q^r2gbzS;G4WQsbr9i_c``4D16SUGq_sclOSgk8PpRQ9SO7QCx~82{2%72#Kt3{RcpF?6fsu^IuoQ9AEo&w zS}Ad4(9Gu6YB*d#mF1}@G^lLO<&SG~_Twh4Hm%lzF0*>H;{&s5 zhniO3Uc0`8U7uNU=fssUgt&}XpKTu^*zrNy3c3m+z58wX@Rb}p%p$*k9z8CIBxl(1 zBwA6nLCDX1M(ES zll=gf&rAF-`i%0Yms6AO z97aBZ8S2HXx^oAp~OSD~mO>7%}VYN8@nKi(M;gM4yMSvvQJ$3&D* zroS4sd~CepEP^HXtk4IQo5wmv>)u6hUUAlnm)y!hdfCt4C*xlzt)1`sPiaj@s85x) zMs)zLAcG`;2MWd%_={8u-0BzI3dfl<#$#&uLhZM;UENK#<@%{AFWU%tML)H%m2m_* z53_N6n>MGP?a*ZqXKK{2)u{N=3llV|pM6lMQ6F5^qGN2v8;+15wu{N@{=MNY@vy3e z-na^MHqDf_YoPPbX{hacMO31;4`yw&$fQr(ayXq#4qr|tUYkVDXj{(v2S6lel*1sB zY1NNOsP-E=rPoQ_sEi41NJ(fhhlI?rBR|4;a6ICg=VH1+VZjlm5+rnAWc+Qacsw&A zFY$ffj!VO{4F%e@h7OK{tThZ;SjVyz+#qdD5Js{oP9Pk4Tgfg53 zkS>(s7E**sS*JDd5z`(m46KIa2NI7W&{^vh?Y*m0@sW?xZ$Kaay%le+g&LIn|5EXZ zY|LZLiq~r69*fcU2$J zvB%!n*w)hHKWOVTyuEeeU${`vlDQF1Cmuz6N%a-9f#cSf_@)`NU|-cP=$q*UF+adF zRL$hznJ<<5rq$0=h^vz9>(U1^Sr;PxyT06~X)k$w_0xk9oy2D3)TlN3fTo2pg%ynz z+U^!{TyJ$g7I`o-G+E!PRzwpne|!BsW3)3f;bzr;`=XdNB9)7q$7F;aiS?}dC)@D5 zw*UD{(|dn^iF{WuW_x^;-YSja5i$*yX4zAuHNQs&?DER^$ORE%4ZhU*m`69@6cct`BUQmsV&>d(5bbx1@cPX^@ zcd5SE9SPF#;*nTe?6&uy^)*04I~k8>_R+6cTRahNtNj@`2P4%RVN4Cf*i55N}KVNJ`MQKT;X{4$m>Y>(L%(xvw~Ur1py- z)aY9o;-vo#n;wC6y(Z_U+|riF)D8{afC{AS{CF4jzRXv|ccY{`5HVycagqe% z(Ca;Mugkm#eaNZ0i)~8awhW=8PylK`mA^UG7{U0^{ zO;tG^XbrwW^Xa7d1YC(u#i|M|d8f2f$+1@JBLI`mz8IGAFD4A>Dxq)Gr zp-SpKlgB*aoSzf5y@Kdl?iF_%dgLZxk0SCDn+^|LBly*dhg$WWH%TKQgCR`!g`Aa~ zYftvK()10P*vcPS*rwK#ELE|-Q~0}zMt$Ix+CR$d;D)^$dZ%7?{B);u@#%!2jgaPfzRyZ6}h_6n?=V-y248FQc;?)WXRTBp$tC5jt6ML^`BszYNcUTR1QUUwvr`h&q)0Zw! z%CbK|u63Un&m}8LPkkvDv(hLRX}KA$k+V`{gn?0tgOWYCv)%==y-#b{25zw)g!eMy z-IzOl%SZ?@U*x!g>XFva`ein>Bq{jke`{_Ew*yI;%SG-RxUG*0rx9L>s%EN2nB$qv zbQ$4#esCM%)%=iUgd6$6V}#f7L$(p#zz<#{yon!jj29VhyomJT7aevu+q4e8+DP*e zUMA6xa}Yh~KLL99QW&VeFW-qcjN$D}TSv^YFeUs14bBJyH?`iJP z3>MqCeb)Suejn|bWQH<}vSrjZ>J0{&t4QmZ7uea)^l;Okv3qXe{-}0F#PMCDIC^iW zXhbnL*q;Cug<+%0C1d^d-vU&f$YjHptf@5s)%l$CyTnTvbcKAI(_gsg#q;Se-1K5= z`im?pIA7|8M_54#?HAefB9i{XOE31MzsR8%`_o_e=*6M*7rFGJef$ODufodn_HyRf zk1{S)F?D{0l4W zAw62b2vz$_B9Ht}GI9(L_fGz!?JD*f4|#A9Qj^sq`a%)LP71w4{R`~$J&P=>_f2NE zb>Ac{l;M2?`cW~@5SIdPB zwZ`q^dzN+V#heJQRvgw!lIjE4Dc;)FK1rE@oW% zQnrD_AixE~Kj+t+`Im+6amy|2% zZllAZI~0>nd-5bO{6iUS*Pg=g9WvrWUV6a|rYhZla!ik-;tof#JiRDw=E+JvILb~~ zzDbP6i(13~A{{jI!WXJO#qAFjPP(kIe~TAkmUq@;l^V~KFqZhr%Y=^gmkS*|LV=%0 zSns#T*VrU^VYtx3vNl4Y1o%c62>3*@SmpI$;M?0v?Ew#B(pqa93%exi-ROW*I4_Z4 z((wd+*M@w*!3W{x@+L4hV0Fd(lC)sV;~3F>aP2LFJ5Cb`TEr>b&@wyxyy4$=uX z@oPCg1(?yo6bmd3FbdEo(T!(-&<~uER=rhGS^_c6Y|L;tNK-2{*Ja`Z_LtLkX84HmOKhTNFN>}*5s$YQqFkXx~sony#7Sj_era`P3l za}BwVirIO5pg&1u7*CF5IUN4OsrU}!CwOuGw}3IK)Lx?9vqHRx-IKmpV(SPzCoftX zX%>g^0yGxvGA^j$8`e0aG$GjO>)Gjh2+<51FGxioQz+8uBmEXDP~rQBccILY4*va{O^C6!wWc6N(tol-9^x|BC-!pgNT`PMBNA1*!)_Utg488u+RZwbWOJA@5{M}?BbCje5M&36@~<+ zFPx0^X3_uM z+hjA)I8*Jjw6~P9V zg))>*Tnafeta#qwqGc@ZWLbq+dgBc1R<#%fHhp4(P~g(Frued!?wOtnF)j}u>_sp> z%;_ypRJl_5&-$ZgN!Y`qI;XMCm6=*wrDIdL_^8h~uA1KK}K1 znJ=$?E#^l*t)R>L5b0IG-AK-oFdJ6MHI_@5)psiPKW@2XHf{b&CFjRUbT}D*eDldz za_r(|9#HmqYniXgsFdfe*u-GSuSk5=nL)rG)mv4}Y~nxf;%4LtFQ)>zL~rp>lA36sBpgYSBv~l!NsOCN0b+Qa3}iaSqsCLdM48Mp!@lNM^O%O@XwP!QW-X3Q zRf){vi4)QIfv%*s>K|#8PsX|j*{yQi{vmZTn!bL$lX?B-Pi9TB2nR5AGElzz* zqdkL0_w>o*k5B48+pAVIo{SUC7GlwFh-6$+dX=x#Dm3x0w@Sl{-z-~xrN|rwb&*9 z@ZO0)*!t7ggDY=fQC*+&Ifr#D>YaQE|Lo$%X)mpXP7<5yrlD#ds?c`Rwp~VxXPvJY zeb3CI_uX%H>Cel+QF$^fS3J{0?@zSF+rZq6wKJv))2FD{v8i5^ za=J@@l=5}A#ir)qcamnq+rSz-FaP#=koaAUSln~0W`%jUzWLcgT+|Q zwNJ|@SEAuo-n7>tgjGGC$ES5TZ9SB`cs(q0*?|+oky_z2&6}FD=Lxow34U(Ag{0yt~ z3k+)7%Kod^8ECD=)jxrn0asJx!{(HDstAosT$d`Iaw*MbW!P%6S@iQj--;&EQ|<7R zmZoa^-3ETlf&tpyx^hp3eP5AhtERl`#xrwi$itD~*H6Z*%<;@TdKKG_PiRAM#WVBi z_en3wPv2FTDcKppn=%VAiG%UX8ThDvCw&ylEaG=KWv8ucejL_8UZ43wqcx|NHBo-z zR}v8&g|VbSnU-W(_Ly>I=iQvzonTU~TgRv#IWoMjRB~ZiRypSO{TXuqMD)g!pWr=V zzTi!nj|KE~@@b*aH^`@Qp>L8;i-rEIe0o&qzq!tmTJ`7TQ>Eajcy)U~K1@;-jpJ=G z|Ma#5+gnDwHDbvhR>P`YR$HYgoA@$h%(Z+IVfCI@4LX;JVaK8|m~8mSkmn5GqbxW} z%s{uPZ<1v1<-K0#gMI6K=crf0+qmRqm}E0mE=v~3R@27RE2{3n|5G{%}Yi zo1(1(4#1diS259xCC`m!X$?QeK>oPcta?5YFD}v}0Kte&i5C~f=BmWEi}Pc1UHF?9 zo9o8k+}PYK{Po4=dhjl zRbz7t>8~R;7u!SeNNjFVcQm%7DE{q{z&F~$-RADd$}v-Y5}SFA&l7zYN3AZiXuYMMW?Lt(i`hw{#wxs=dvO>I4}{$=iM=!5zfb8_PSR)R3L^3A;w7P6 zTv4B2&HHlfEeIXv8k2Vt}~b6 zUzXXQ_*(o-LF82OiG}eR-xM4JOxfzjQHLu|9X(adIIEsgbfGjzm1jLXYvxl~T$nhYS-!^ERM4 z@d&id*x%J-5gIpqAy^Pk zbgg_|o(kiu*wl4WG=es?5|zc;h@M+uDML?f-AStpR$(h&X1T;zDTAd>x(3!bRu-+P zy{nEpy4osx12-5wjb(0Pog(({sKYMv$6D#6?&HiAjTr)fhsp^)Gn8?Els-%o{gQq`rDIaO%@jW6V3LDR(T66YT7zPr>wYLyIFEUGB9FX#Z z3QMfE*b+6q5K+X=#1r8Q@o4M-t(-Dn3C9uI5 zpv^WzHfu6R%<@c!HJTpS` zaw@6m7g_96s(d0r`Kf(W-@#b%2&!zQ_O+#-2hv))gC{g$+53$*uO)bnMhpC-5SEH^ z$Y4#^CHWFD?>)@(_VccV>SL$7+bA`^`#|^E*lma8+I1!V@slj?7sC`S4r75x?I#&e z1!ikk4&W>rCVC+`Kn;!IC2e9XN4D=Cn4*PGjN;U$H{0H?aoWkIjFa$hMWe+~=%X2m zz880-Gw8W!aiES_`p`fv<_PV926+WV*kvX$Rqfwg1XbtZu zsh!4jH(Je&Wya8dYIHRC9xH=#kHo&Xay#!Y*6x=$kpxS_t{98PrhZ6&FT*u3> z`+yeO1nto-MITbSm~+|t+Z}JOX9Lnd#~Z7#l^%?}8K=)s>~nJGYYb)J!gniZsmAP>NR-j1_drl>(9t)3gxDM&dnj zTAL|?ZDoRGYh;UHyNF=Fj?4%6gqPN)d5^+pJ9$36MX^XN=Mqw*rPX}^DYS79rjZ#e zob=%-%vHj;giMZJ9OtFX?JZ83W1oDbGfazPUCt4a*>o5xK?=p}Pq3 zPAjH^DvndPeRxs@O}DZQn7kJi}x$rJwyi|GW0?GP;h7WrbgRm++AGY!Il_<`;> zV^dEsCB4U#WUpl`mE>o*i^8bkmmal^P^kn$V?PmCX=EI7f> zk6T$CcGg0xeJ9~p#aw~~PwajNlHV9hSD9E{n$M;+<-SK(HZeVEp^pH-EcdIfRS-4{ zF2ts~o<@h<_;+g7(>90lJS{oYQ;QTw*D44L-Gh9n^mD7?BCseGW2e3l550inoY(=j zevYPZ#@Z?+gN@GZQ}WCKUY}hsI`F;#r`U5VcAku(G%ya*NWOxAOpQJxukr$GGG4&X{wccVdC;PVT!+%vA+Dof&X;x4BIZf>P3oeJ#0! z{Y&DRBOuS;OMTGsw59rE(?M4uEy8sn3Q=xMxfQH}=o2hZw2T8Kro@JB2s(L^RBQ?)@1;%BV;8Mq6a zuCVm~QfcQ{?Ucv*^atma^2#aSEc}UClx;y)&!B9NI$6Cn^LAS&qf4B8E`Wo03Dh{d zm-pdHCkEWIH|D#+f0LVQ9wjbzLhmCone}cftG?4$MX9Rw3BU|v`+@yP zpDu(g*^H`d>-L}r($Y)nMUUD|^7u@3Ag!Hxgs*}V|8aV}iN&t;HnwS3wsD~=mDio# z$Y&rQKq#0!4;3%EOaZL99x69nbHc0W15B$|w=Zh$S2oAr`*~O{zzqec* zclx$6kjxtcIC$#mGYhwvm1;KDbNEPna_yXW)WkA~(|Rv)C)Jj?w@z~2^L2YRDVM+*TI;co_@;z#K zc)0mGqe$LC-2Kl)KtK1g;#m8If{5+UG1D&lNozYQIC84wfgI~?`P0Qv;M#+|)`oF~ zKMTiiPAJ3h+fe}aVsAC&7Rob0)GbOl5O}CjETa*N&4G=2`YOu5) zH!CZxSTjnu5E5qwa`tjV+7iVrseZcp{jgiQ@Ua9U5|T+ElYl@1YG(+6fIm88Py-^F z{Qf-e=iHeDw7$CkeE&%1{y6vic+PX4U*|c`Q=u}x_|=JRHm&+jD{qEzywi_4`@Bq$ zfKqS^6>Yi?VsG|`q;|O?sYQVA`fad}0K_wjm6TGU{tU?t{(y38PLP6BsK^F;3xaR1oX_xi#)L+WO zBo+|WH<5t(ui$j-l~MMj{{Oebw~dco*5L-(pT{I-Az1}nzJV?#DgtNN<0mnf)I#jZ zF?n#?wEvj<_fn^VLWo(jFk(0JX{ZV#y@JT(*Ubk&?-GfzjB#gw0F2uUd_6@JTGqjz z$OG~Mk*AU7qYBTLyH+GCu)mJ)x8nQ!MqU^LT){?}!!*x;d-ZZxnuMy$tlIRJ#eTwi zi+Lt9-^_>ScdatFsv)~rk}xc@m@w4tArigO1%>kx`n;6sGsjJedq3>P`3=9Tf+g@r z@hYuyA0Q;pzR4b%{YZA}GhJH2S&0AEt{K1?Vrf4u93PMUa{Tgq#c)L~H1qZ5d-}Ec zZA1nB(NNbB51l23xheBJpbR*o`aXi=1NOG|tI>7vIaxAtdxgXZ zV60G*{@#1|NnpHuj_7YfSdpZT))+cR&yh7|2ls$)meLLS!8fz$CWYVlqHqWWhArnq zq1Z3o$Rh?_{iZT88w;ond!xC_p6Tv)AIR>Ia{>~T<%{w@2fmkyMKhJoz<5N>pv#O% zb0kQY_$gZvc>{2zu{h{2&B!DTy>E&z4J;K>&6E|NHplj=#BLVsy}*OL5tyypa0e$eDM9h@$N z+r0|%V*axuvf=Q-6I=K3oNeZxa0lH3Xmnp?mCf~gkMQw!j{K10STx=>z#^$l&qAU*LoOBS zKlDWc4#9%pGs|Jt_fS02!4)lbc!J(j1pbghVgfDHEbOVfR1ZniTHr2XR3>CVqnW;# zOOw`7Vn1^d?zGdjoamFSfWgTYQo&Y{_O&X~8!35q#Cwe3v4Y0E;>4q_6^VC5$`d?P z0*3&Wdvfq(EMW=L);*}O9gH+%wu`O(GQwtl80jth0f76OB57W3>`yS4beI|4vL;XW zr4t>ySDq`v$vFa_E!F~MeDeqf0s_euQq8CoDwRpOF!J&l#5$Q|%5fVTPw>yg{>?|e zW?RqVxTnQ?0bc6>TQ|XvzhV614<uDui}zZ?ClV~hPATx^MKuyx4+>(Zgwur4JQ ze5~p-2^#L_ESWHLQW@W%QhuwkKp%-Ti|}Xmt2)CS-u|A5k*|-0Z4pEnfS8lKn`Ezo z4rlibTFZZIJU9~hNc0E$EaG_ZjA|_Sk^?n5BC{?;W+6^SRQ8D&0XL`myK}5OJ$S}q zs9#bJ81~<>ISS@2Du`kqn?|4PqkV7HOv+!4v_)yegiDB)qHC`!oG*sZ<>s=&NPZon zeu^+M5R~5ve;e?QbMBz%rv{>EATmn@sSmeA9vYF$9lLq$UaA6tHcv%v_BMQ;e+5?S%(yBx{*S${vlWq#pyo%S$^Awe1z{K}IB;^J z0;WCB9D*}%1i>cHR^T(lx7a+B<6E5h9>=%vyTUW2oL=lSrH^2*Rok}F)j5+BhTzA6 zZCj%Xrx%RvFG?vO2TqZiA1SQOQg+S%-K(*GXKdx^fG9|kq?$efA3+-WcK~~j>)$wM)O!BK zSuJ*$7o^Ai<$*z4HJ54%ffvbOTK?B`OEi+8CN&q;r+XTE_AC_i=) zV$a-6yhVO>YT~O?^c8ld2^AudO{kerk4X0m%j7gJu<%KO6io)iN=Zq&Qbb>$#~aWn z1q_Dds8wq+wIdGRlg7ToRm0i9NFfMK$~S#tCfDY;Qywn0XC%}FHe5N4b-nUruo``$ zXbd84jilWw7tC{ECm_0Sh}6crdwb-0Ao+^4fj`n?9A+Ufx{wQf7JG=ShX75$4L66+ zMK;S*12XDLnDlIzNH_Bj+ zBnlrO2_Pt0Lt)A#{1GQ_@=d>_j__4A&q)_~(_v()_ax4HN(kBRWTKIxPTmEU9Ztk> zyu-mA;W4Cce2yJ1*8P4OKWv`~pPX<?O3| zT8EIA(&9)2MsD&_gIk%=yY?d9LiR;pv!zt57$YqCg!ORQH)H)~{M6_UBJh%02TR4~ z1<%p9-VdnIPAiLK+p+7vtC&cfrPNwHh5o3v;wePZV%ZlP5Sz^?R!V()_`{U)X~MsQ zJJqIC4zW{yr}re)wnNtT-?BDU)@GwW)vVD*HNKbK%{5YW`LepJjpDIVv9SbqNH5*2 z-0>UOj(bQN{`fUu`zL?Eao(A%Fft({{l%V=l3HO&k|mo~&;sp}W8JRjx(Iy>6X&D_ z;4u(gvNxFs3>?LwcI+Ud z0n{FDCV8DjPqQ8GCLhkKR<^w)Nz*KkT&FIY*HS8vhE&aOzk(M+PSvl=eu5B*ID^-Of5;a< z^9mq+6}H;U^?`&v@>?ogj8p0)#!$}071Ccm5s6%Y=ZSj~;4vW^XugyP5l7CK z>z`TvLAozI0YAy2^1qK93O`C30g3)Iu>nLDzEp3Iz{J{L$u+xUJ18aVoU{YFj+$3@#OM-J8yAuurZ+b}uCQlIE1hpy$cUVnY@3YNfG9?f+Uz z%HzSa*&X5#Z$aSr3*-;Kt5`-z%Hk9K36$R8W`oLrNE+AhtJ_;96E2XW}7f07Q1=?QqDMHa%@Jip$&@m&6InI-ClDZvxP2q?fzP300DQ$1j z3dJ1>bd=wXqprcz785==hweVCM8!Fd$QEP$&4dy}l1P4*YI@BecB1#49@zb)%3cOn z&GI~k+H%)oR6vj`t7MF~Eoghz}CETZeh33jj{1)IJm z`a-Os_Ev`PXu!sq{|@N|U2Gz4hKaO{$p8&}J7C%k&rF71Sq{18z5XP-EFqdw;wpp3 zhqnH%RQ|3^(pD@3Gek+QnJgUhVhXQYq%Aui;y97`Rjwk)-Hp;|ke616R(+RhcBF;G z39Jc%I~G9XBO^Um#QQ8H{izE6SyE9UHe(aKRa={)vd3yOfh*_aWr87rA}4$>JQIUB zNz(Y!gwAVU`W5dM&H{x{+KCd(i)b$*atlsSbQT2V;s7vCflqe6wAx68hv?NzL>0G(RZ?TL%gHD^3wt*F&3KxO}B#PhcI zI2z92h$<%SMLy_9O#)2RSHv$Py==WUdG#g78~xl>V8NohSjPaP$UYh}1MbMm1;=0vnKCOQ+v!lN^<3bRCei!r~Ui_n~J6G%10zKU5SnR zsJfox)K=-|czX=NBu}YNMbfvM3hDXSljnGuNeMInA8Z2dnIqwEV0)kMXCBPfZjb;FuCH+``~AN%Q=`h)+TMx^`J^!Rjd=1a&`|Tk!mp)FE|wA7 zK{{`@%!%;1bXL;hm@$ML6tT?nvdh`y391=8G<@m)9*ZvsKV0TYgRMs#uRJp{PMXJT z-v^557lXkMEaI);d#bVgR%xE}1sx=KKFA?{=@+(2{F;G%9~|*SVGyh}4T3jC)aU4N z`t$HKIp#fw6K)g!@|RlW*glGrYIYc0#R0P?1?Wi_{dhb0zG{@+=9uI^hrTQ}`?A>V z%VM)Hi$_SZu6DEy{-7WaGFy(gsA+7!lP4quzY7gA|` zc@BAhi`W(fKJ~NiPsx0zt2h7ye?p9LBTzyya?hWa$8D*70hQNMW@Pn;sqKu<*d%UmE$)Hm?+bWz{S7t=-kOZ>`3 zpYTP7sNcjFnWBC>Uu22;EqsxyKbR8=^pB2@zeGeP6zDatM12(+Y18)3`kJV#!S%We z4tPOF9iTc7+$;jKs5x!toUc>`PS6!QEgR}xTGM9sJvM%`9fs1nJbEYUqN9k?-Ywo1 zwP*SAyok8ZiS}g%>PAU-y9ivsv4{Y^Fsh0P4@3ZI*M;#e>M-EkD}oKOu)AI7$J(|4 zYS6ZIxFb4uR0$e^xcm(NBf)p3A&s!n4(J%DyfV0bDm)1~+=oe|uE!&Hq_S;0F|CRG zM#m~!Ak~RNyGRqoYGo`XWjb?7f(|V|5_L|ZEeCUt%EDLi9;ZT0!8$W zLDjez5Nu(=%N-WA>wzwTjc79f0*Dnvl-&=3Q=OW$}zmiJ^dsD4}m-)jg zF3dQrHi(w4cyP!P>~&nxSTmr3zDn=Yq2GIO_uSWR6E#`NDm$(q)JGaGK zfqGtpK(3TV^hKK=(i-EF(t!j;t4s$n?07-j)iE%vHFZv;OpOFjTL&WAu5;p`%vXy1 zqhk)`ZP5~pYr!)PBmGOp3Z-jI+jfqVbGQeUlcN7vJkTKmThLzhFrsS9jtAal@R4A< zDNaN1)Nzme2mpv2Rv3&pF0{<~*TAWZKDGrUwEQGOf zdzYmWolv3E@Y0(gGx~6qKT9zZHauU$B+SY$e|q5Yugz;u5=HHE@ClS)jwQ~ zgao^pjt)-OiaJFts?h7Va7Tg7!Lh0G#3&-440MhA;#Sd!dP=pQ)$#HO$h#HCQnczL z8iI5(2{eF8>%8UP}7v{GGPzfapYCP(X4j0o$*IBVCHm2LB#Ujz(Lx9Yk_9(Oudokyr~wHROb8CwzY^c;7;73v?U^H2eu@! zS>J>uC?YcqAg?cG@f<9_L+1h_r0wdbDw6vryr#7#XCb=ZhMYLaYO6C95Y8%`2ROJU zQrVb5GtK`;$S1s3C4m9td79Q?`7`82%M7MBM&YRE!0P#8Fi8&y_&a`ut*F0g0{mQ5 zuSCV9AR3EDJ;zt=(YZ5XjAp~rDe3`GZP!3~k?fR_{=AX5E;&9QZkt>s<-Ovld_9ASwm7!T0j>-pvgBr>8 zL8_DSu4_=+X7ZD_Ic}rUF4n!IOzgMm*1x$g_m5CVj>s7`HdUbQ2K3;6UPUAG{yJvf zl5?+vC$#kD)|+@a#ivPPCW)MGe_gL*uAj44Pk&w1Zv{z}(_9DMqV0V6D^+#3qqzWd zL*I}?3*3@~Rd8&IUI)f5>Y+{;yYe)i`&}a-u8b)9^LQrevry+Q42<44+3_r}35+02 z0th1+2)W^3r{V~HpoB5)tSo-B=H~U}4(oLbc-cR}_eQ?Yrn&1SJ=nr8a#Oz22}@a= z$}k_*+$P<~O}VO<-q3TCbUZPLhpBi-Z9L%6>t8ktUf;qMx3rOC6>Lx5HmGk|KHMyCr(f9$gT=rUTC6TmicKfegBx*P7S|(qES{` zpQ!eZ%c>nnR!QIYb9L3<0c4MaSpH(m$+$x_W->B7%C*(mc)=0vqctb$3z;CXbfURy z<3kf{2wXg0ei9wxw_*PIV>|!6!ZT8z!QY<~^-Iylb_j%`9xcVTfVR|Oj(t4Ss{T~m zdYk@(5#p6*7G#6#-&T(w&VwwFWSZElwylmkw#~qg@`+x5!YmR|NJKN750C)BX?CSx zrPqNlFEh~axBY**QcwSrr~}5ik5vXA|6e!!qc}d$O+2>>{d+BCC9U_uW3Q)>U{*PV z|60*8g9TE$#mo6!qH6{VhQYyUP{<(G*>D#e{+{5CO>@qEG6nV$u zK4lXBJ=>XdHZmQzzo>8GHOG|GSAh}=TweI8;E!p&1A2Syx04cL-k`rfni%*d{e|gm1aA*d)!Zg( zepjnqDJ%G{_QxuwzHWX(=u^c%hOGCAK%Cjm6R+6#{?llw_8xJMC(mf5S~p`HJv>1V zPZJ(CaG57h_S5~><^9+``a?OY_@^znKCi84xmrFf zKWdOh$5m?q?BVHk%zbNfb4|3E0_e`L<{flrqNv&t}=pDbvwGjT)9s2 z_=jvVS#W|K0z?eOF)F^I^J*WLR#}KTNKdIuIj>sqnl8N-mqyVvi!e?|NQLO{ZK{K( z<%quNB_Y7w-*Flzw4?|L<<$KU@-mW*k#BBbQ7}Ogx12#KLsu7AcG~wwQmQ&m54#{$eZp@J z(wni=l;<%qKkv2C%~j^jX}WnlNJ8&yU$f2A>0KL)S&j5VFz4RR%xv{aJ+PI9#YT)Q z7{8uVhe?fKB0pO~6t0!Bh?Iz>D6Dj{MK>6qI*}i3%~ca7U2Sfs&Z=Uma4u$H8SQ%i zV9H+%D-r}Cfax}|fqx5Cu|eevdSc@XyVzjo3y0X?;0veN;N*)mu_28w(#3{!zL+jH zOy>)i*x=%e46z}DFEYi3Ouonx8?yK!M{LO9i(IiG7Z*+Xm@?i*5**LKb_%_FfZ&yh|Qk*O8q zNY8ksRi>*1jVzDJbrkp}ib=VU0~1{-Nak;@@UTu!}?6?M{VpgZ`Y7EY6A*Pq8oN$^!#4E0XKdB2RjsYcTt zVUZ1ihd8)2pMvn6b$oVo3AKB>RJ&t-SlgLBOysQm!8r~TY_7l8ptirx`6jzKiRH23 zV?&-}TJ;%f#S_-5M`1=ie(a1?C1cyqC6%NG*0%^2U%7xg=9K z28o>|jaM?m6C2Gq_tsW9;d2>|WI)P^D-Sjaj&>JHaoVqHZ}~q=~vXyx$*87j;p-m@eub<5wFS7Is92gXJEBQ7@zb97&G6;Q_b#kDwx^%>7Hj43f1AuAV>3%RUsC(K8@bMvA^MEG-}iDRegUM5rwLLuT2!v ztz8t_^?Mzn(4pV!6opRx-ZW8|rr(<`3e)v_r;Ebr`n@hu=+f`a5QQ1~y_upgQ@=M$ z6lUr7=7_=^{oY(rn5*BLCkpfQ+l;E0sF_qi+&w5()aXx@h!tnWN%vu2a4Cnp_XRKx zMGnVf08XUkGAI}zjewH2^X{)`dl%hY6~K5V?AJTke%-)6as#5i6WCD?l+vp?Xlk2p zFhiSnu7bf}5%oD^Fx+(@U0eWD9cV6h>ipCfwGySD0A4_$zt<)LxFbS9t-`3Q)i!H8 zn?;+>n5x%c5h=$FwRVze_P5uV_>v|9Z}R!cYZI2;3i(PJf{}$?J{PqOR4)1xeyaz7 z_69;vCIVROacS|T> zGp|E|?W6L_@UgkRb}{fQP%+95Ge$0z+oWv^ihv)_+qG@2B7jwI_;_z1FoJQd!?i31 zv%Rz;_lv*^-VDGZ6@fi`jdkj4wur#X3{AjPX{Lq(WlY-8Eoo53r)*GO&i!$5@t1i* z=xfG+0I&7S?aOjT1MhNk0N`Z%^1mRvMPQ}*UEp1=6%Yso03&J4lYLYKzcA;QduCD& z4h%-@5^wVwc(5=>W2t?iIeK82Gz`v8{kCa`Jsvz`9XKw!v|Y#9a(hzSc8*9(T%+cL zryVpb$l{&KC(6NuHTswcOKUXNw^scN1Wo2)Hcd7}b6@0WhhUgX~`t3}XUXdtt4u)tZLz71)hDbkv(BIS!0W^Z#DZ>t>Rp+7uebNveO4v|? zQaFX78tp3w30s0}hijI@f51 zqVd$Bwek|tuQhRJL?iyw_(w8R z^?V6)(a4={e3RKQ+n*S)yZSkQS^&f#GJUHqJui7J-tzd4c?VR_ODyEe>-+O}Pi}hL-J;o-DeZWY6)=>Y=oY2p=aZ6Yn zhFv&xseQW+UwPXl=uK*+vr$N*wdViUwVdOOr&Fu0gj^tHia51R(3Qw-RI-%DYjPQv z1}`=IihMks5uT-lD-))2lAL9~q~vVe$1?Mu3E`|FZ2UHbWV)HnVE#O^(wfgPc7NWu z?jJD+bwpAKWARI7Jk-Dad)MUeH!Y>n%;YpX+>J7vI-=nfg^}=fI`v88{iGB%P~z7_4|0`f?bHlEzApd zFk@kslCxIgm0r(sDsJsup5<3{t>B$~QVCcr4LG^&6U^Rck-*?% z-iY6o4w%}_H!IU%2?yzDMDHx#n{{UPc!#AK_h|0GrcpMyS1|>~@Da&z_zD1UVNC0Y%MiRLO4)^uz-aiwI>9A4rAe&O=O~a*NG! z=%mNUUpXcMXSp7`tfx>?YZ2HymXJ{b&Bh!}uLq#D7X$CG43a`O%i4e;Rw;Q0Ht{?( zUf@tk)aoq$9s-$_1sV}(AH_HMjLWcBn=lHo=)p2cpo&#f34aNZYjrz%4mrH89h&{t7#m{acsZ85;EMeO0q~}3^i|F5Ri_(JUfvVA@Ss13v7426` z%Vv4d$iK#-G@HUuBhV-mc*E>nV7&Zcy%9J=QY25*-Er$wbn2P7kk2_=n)pQ9*2P-^ z7ZW)%b)GA2TUguHuXE)j?!X8bJY#Wp3T{FI7^v_duRF}Vkr$AA8SUJkm$~gVW1_K{ zuXue)Mj?>~l2+p$`L3VeHOLz~bCexiOCDcKl}IX0Od1>6k)`STqH#MH%jCn<>-LGj z*0F^4w2U{uuu0+L)$1^}^9Ds+>{3~iYG+LqNJBGLlzV|Dis`4ef~nU{{Pf+4r(v+F zUb{U$3L3)AC+cv35o|EZ&RdO!;tu`xq@ zH1|!-{fgOAU84O*bN5X=O*H#NK(62?UU^Kef|z#-D~AO z6!H_Gk?enKSwEYI{4iW%y+^jb!WU7sj%IazQ@dJ%PUU5sxht@ST4aRRbfv}y>& zdM#F}7&e+-=CCB!Z3WvqFX|-Qe3`8c8Jxgg*YsypK`3v78^uX(y-qR{rrVHmPiniu z=-=)*D|#z5$;dY#VgkYZrHdqm71A=cz$v>`_K zutjb*U^yL7&U>VyANUslnt!>Q=&q{@$Vp`0g` zg!GVR);Gei;L`wxWo?G@e!UJ_qLEW81EjnNlYq{PND9%30h}ViL2m@TWJn)Tj_YiL zA?`|=IT|{^f}kmHDs84n!jo_1#qaJ9#;q(HyF+3~&VX?krgtID43TGKh>HT0TLL2EiiM2gKh%n7y~ zr;p?A1H|o^L>wh4nXk4HVl`uX0#FDJO;f`9?_e_t#-|xMY05EAi{zu4@ozws#j045 zE^K0Dnq)D0trQvo0K)L#88y(MRY5nfnp8E;iu+WZNsu@=gC||w?G*EE;^d5aHpcwO zE}n5%Yu`;WXjytfE+Ii}QKm1HC4w^y`F+2slTn3S#rzB6M{y6cB}whHz<1ahjY$oR z8+VgL7N9+g3#%f;E?L3x%@ShIMUfbPJQEw z;BM%Js#d*s3Ql|GWM5=;*p&Zn^c-BRiil`qH`**u$a{9#NtUo!pq09xIOS_zsvObN zEA^ap(wHR;TT*k#K5DFQsNuA~EYEPSNPh3TqDH@=GKktdC(Ev?#L1PZm9sD%Q<1%+ zIm`3sH9bBqu9)8zs`|=wty+^_TKYa*T_ z+WIL;cIAs|9yViW)ySP&(Fj?AxcWA0LNV8Cc>9-Se4QGySt?VU(r`ed)Dso@<>?2(J@u)%ZHwY@hUVL{VAt-RppB` z@v4n4(#5NGzL+jvb?}8tyz1nO4Do6jUu23`)A=GxygHpPa>T1HzQ`4?Vi#0@I8Xe0 zrZj{Hmh#|POQ>g^pQU>O0VKLXypH_GQ=JnPUed?-c3@Kf#X=m zY^<2?RN6&rf+Sr=k7AGGbBRyhY`qcYKHXq@YF(WB4P&4?E-9=@ZJDUX;KJk^B#QGg zJFVfH$&|WSl4)^x;3P4mri%mJOtHIT_Y9fwgH0A!-#$qZSrNQiU+|q?t*H?5MmvcZ z2d#B)Bqd_DEOnV~bofF!M#WE1@oPU*z))|DEghJgAGlVUiI@Z#n57v^6!k`6E{UyF z*~aS`mwnSMoJJ8rr{h;J6RXPioYR_yv|WbpFAc-y-U3saUY`jHDvEbouRc_lxzC!%Ko9tu;aSIg!30rcc@*4)GG*%vT^V z7<5b>O3trdU%@S7rbKO+=a^@=U)1yLo-^mSMX!f7IJP@ZOO-WiUX|QssUVT67iE^$ zxDUv7N^}1axJF|?V5Bz|aS66lS!?rIAYh>!;ZqmL_^Y{g(rIg@l%L01*3)pXGC8c_ z{GkG8GOmwGzlZn;K%8gE{F{?5e@oub$oyNkW1Ca8bR4OC6AiV5DMZ*BmQ)dHX3 z46p3tNh!$077tF|3vSh_S3tdPJ@$^M+@1c7KRAd4c!OU@2sVxu0PX z=dpMF0ty(6E05lrgiIoY5|JPrdoMlcsK702PJ@p2t+MfwHpSoV_Nk?Sog`uAd6_~tdekl=7Y$a@$+H>s4UwC)44+&!HvBa!$0SB;A zsHX?)sfleHy#cl)lA)vsE0IesP-b;C>C?<(koq)FghY{p($q&_kvz~7_r522?y@+{ z3zB6DULmP3u@z4WrLLsXyDTWvP)+6iKHfj8y8CO14UR3zZ6wlwNAyN?9Gd`B0wmg` z;ha;_>4f!=rWVr>seWH?1kZAZmIc|+!3-vQTw>lg0c$ODz5# z)}6sbk4^Nk#nB0e1NK0ug!@BlUnddcqHteFVq-RuNaxNoiLnW3Nb5Ipp3`l{eWM;m zG|{EaV|I^So;Jak-c!Y%eyIH^5Gf2wM z5fS#aDwWI>J;%H;xe~LxjScwJ(^@vAQel;a3I!($ zU>BThqJ0osd>jBexin*yV~K5EkjD@|oDhrn^z7lGgv=Ey{mD5GR%VPCLae#2-U~tINH69$cinx(?qgc3I z6zni^ZV{_bDCdQ@mlXen?;80j3Fm>?=Tf9!fs>hE&`^uT+^t9ir_bHU2`X?-a_$y* zq5Shd&^YF9)eClTdRp~K&KGx>Ua`|r!OOnp3|_I{`v;Jza-AA4<5bz^d&C1FZJNv6 z$YmbUb`_45b`AQ5l~PeSR-SI;KQdxGGN$cvly(i3x@ANtQEc(Fdi(U%FYDf|+OE47 zmx}p|OWji>2_W`Ru<14Io;JWoLHEAw9t;(1QF+_fs?BfL3pVLxdvlM)g_k|RsRXqEPP2XY7Fa3ww7uiclgf44K*94Gonhy zg8LnxXiwlsu?POjz1C9g(`ISBe4^d^uD)b*srJ*3+n4OXv-6V;OP2&+25jd_@6{CN&C*h8t=9F zg31d6gVaVlDw+9h?7GnmY{FJyr?PN+CH(AC);yv$9@~op^}h4BxZf7%%DybTTL&WI zv=@x?_w>f)7(JlRA{ozG2aTxuzLIM3Xr+Gtt64$6C#8NG?)KS zYwUbI(YIq-pc~|{%hx;6jc)43JKT+C?nWzjV;I&^(gMSVI*SC{Wk=h$5lDteVXHtg z#a}%HUV=qKfbr7}!PD>qc;neevpd9EWZpsIm3(8zjQr}w!MIA@R$6?6sy9j#rZ)h| z%?yxgg~GIGA@z-z)EcPt`+&B`sn*v(S%O1T=>^9>>e8AHisz6hNZWOcR85G3+(~;b_;O*7Ql`8O%my3(v#Qvbj6n;+WtE zV65Ia4NtXQ#|cYBL>wbbk&ZFS!Pp^?-H2!#h#YGZkz?(!6Su`QdfGJ{g8^JpU82Cq z(ovA5ueR$8(*n&QgkWByRX2iFHLcE|`LGtGBWfX@CjteT`8aKT*G||23hcTJpTESZ z&rRomIjg4=5qU_}zlGh+*%r8!d$gT-Q|b1r_b8>>&(G225Rr}j;S~DUZlU7$B8fUu zxuoNMquy<$Cww1V4 zJVr#u8B3_D@{m>-p4PKmM*r7bK7fE= zZ8|lDz&eS~B4}=yn!D|@jp#qm(pR}$liHvLnBt&~GE_3r2)Cf$TV&)>O_9W&NV~w7 zM*7wI&kJ$Fb{jQM@3^)ySEh23H{!s$|-G^JcdhMYDYCWS0{Rctx)cX zGIo2J%dt95pKC`XvkV>hk*#}eRa5i=hsgC6q^HV|O@xu2f^?1D;f#N}@Hz1w5b8Gz zagop?#E)5|$UKSjWgP7sB0U>}fo5%Evn{9vJPL;n%3>cR!~So5U!9159f_nCKaU8b z$GdtWBnESW$+EOa>H>NiLCm`oh;b*M6P?SaYMd^tBx#{K&pdjpiUTx0$d9W2C3rhFduiw^L@w*lETPvqhQ%&7vi8x_l9UY7~#_d5Cdu zCwdl9H_V`C;RA+!0|t%Y!B4qj4=9QKk%*$ijDH=MAmPx$&)yF9H554NeaF+U87&G& zWNZl^lyCo_Fif(HLPgIcC=43Q8&gE6Z>CvaK?(;9G;90$bk2==MNdQ=G4uT?J@Cpp zEZV)tM8C|i)CRw0?^kuFD`J`4lYda8dE&)j_R%U5CnM2tjH9OxMp73)&lx9!i7@6* zTOz%l&hlFs6Vb5)Mv;-C?OaZuVW$Jr&gE}VB1lIhKB!vYQnj7*&wteW7;>T~LX>*? z*#q9cf^&)i0XzfRdX+6@b0PVx;&60X)L&qM`mBgYnj@!UcK7(? z^9s~8y-22bB2ambgg@w}QsKe!3dEi#UTZ9_m*9 z)Pgj7j95LuJ-J-^-m2j?vyaBQ%rqz>6x+<5iT!Sj{i*V0^pZPf8a zBIxTFshXzk>~IqYw^ZGwa3=L+{waK~?X>wjRxTtxzISQf(z49vnALaMS}|xXdlKll z`<6i8eQ-M+_#ik)%3rGJbho#f-}6h+Ik4Pi^PRSpA;*l3^h44T-7TWC75%eOeO!zx zom9OE zJm@Q4m&Sl=#(AouiNc^dIA*?)&e=a#(N^x{N$!Hsw5EwFV~codDC?@0yF=|2RKe zruaU#80r==+GOG3S~hv2YpRq%-zutD^S^;o+gd$QZNmz51(Wh27AXziSoO%5=x1d! zd46ZtG!8giIOYj?&+}5Gg0a^V`6BUz7?|Icoc#HUI6%b5={xg%ZRe2xC{2Bu7}oq2 zHuHtHN{c%L^S36hg>9mTJ@Wyo%9gPdGx`g*CWDH$^Um?gkMX&z1CcRj^))H++d+J* z-PqhgvIUxF8_lV2l;iKwgxqhzD)RMN4YdXfXm)3z0_M7y zW0XA(%0AL|KFL_=e08sE`?|i}3HpePPVpB8M#TrDMpC_jSz6rd~>-zEK>IMPL)VIbqpBj#+x{$m7t)1VL_U|Ll*PDc3 zk%J4NN7GH}VKZaNr*Z6gnK3lWOvsgKMp(@-V`aKP_d2iDc_0x58U6tH1X{h zd}r+gi0rk%FIbanPgT z)8V321iZ9ch%u)tR+6($d6A#bMl2htod6SGP$db-<=v5pi6tcsqlxJ|>sz9&@+_j8 zin;6cl`2kvF05b{qA#(B3bsmLzLi+gxh9(sjgP|8(Mc+(yPakv0OHX#E>>3|`i(U> zZt;Xg;4$7$UXGQ%;rDjv$X53OL;WF<>v>_{>h+3PQlnKPp1EF70Y1W)^K$h^W(fnP z6n*acgoy;CroayXZ#pzOu0OzOi^r*aWg|*{fHX}0C03~C^7_ZqU>7VIPgn>So7+I1 zB{kv|?DcEwk02!Jk{Uh0W1Cx}^b2`eP~{0~b4jI|W1D2N+t34>ovfdeP&DSV_xk{~ zw5*^eYK7^h3T?bFP;TFi1hq&)U(}*6tPpc(y34*UJByO~oGdZNsrkQ)Zq7-s_#S}Z zP?|+Sl`+SW-7NxTa9;beUa&=fz()EQEr%sFM&2T=+K+`YCzH_E$=SpTwlIDYbT}n4 zDb+F`G7W7SOMnrvnjA06uhY~e8bm&OJ#X(j8nrpdX2lT z@mh_$zOJucZ#WUCJ;l?at^fX%#O%d*#-5)hqmS~InVjphZ;XzMRUB*^=W}F`iQ*Zon8Zzcu@)Fcz5D zNTP+^l^}iD9nlr=!y(*SpUXP|C1&$|x6%@g6-zZ>(&D`~SxR;{G1ErD|3@FR6;!3P z@zg2V-Jw8X@?=;(kKZr9NsM2j%cL<&bm1AAle7DJ`kgr!TitQ%GgEbEV1%?MU6Mfa z+-N$n4`o2)xuQ1&i9F3=Q8?}I`SdO)=&;~%fgh`F9s895a^jU@1{;x?OW^kP2;dgz zHu8zeaNo3@auo+2Ws;LKWr8`Or5j~S(ap`H2qc;|S@boV;*-iN#}g-&3+0uKD6h3( zz3inL-MV#j+$h+JDx0*4&KFR%W)CJnP9RpQ7-Bj@Yf?Y>BL{t?Ga0WOj53mqStOaM zW2A4@=7;q;d4c9~&C_0Xr9LMgz`+R{B80}mhCnkPA=B~;Do6@(B!*=y*-k)r>T_*K z*lcnLv`HKydiD>a;}d8t*;3iJh_q|l=8<4p{`a&p^;+P7VXq>@J|z<%S;AM@H<3a- zYs9pRKL^tn|;ak874zgwVX9A_=N&3g{U^8&x>E2vU$UtQtt z(z!mp5f$l6D)fameV$#!VI}UhC@oewzW|HD*iw>&jOdE47yykd;ls*hPwT5|JaY9u zV%O){v<(V-Z#>5M7(^~$LH$~7BZ-w)Y$BiQugpNPpl9f~&1`vrY|pMYmdQS-q6-3+ z>?~l|4}ZZq8SreWr8U-P?FrXF2+*qEW`h6bZ3SD!oCP|sN--x-XIvL^@^zj@`tF{B z8W3GxG_f1t7Nsw7h&o2)CH_fE{tL)|Y{tlBe=){ixePB|g1>(&h0pH-UPcKCYt3a> zRq&)D_{UwGU{)aIf^i|X@lz&=a$=0-vI`>JMUv&b;H0_v#RZ&H0HVhJb5v5%J(BcB z5APlwk9|BeseNMaqbQjkH|Kv&&K+P(ntvWOf5$w|SNTV@JSV;PR_f&zx~4S~s~BMZ z`57WE33%+gL#DlpL7apHOOKC9nUM&tE?4=1=+{4?Z?Z<%d&-n3Ek$GFv5ScKLEQRL zgvti9D|1#bJxKAI^gZyM(Fw35W*;P-hI{I@&%=So5;#!6cy39AALog;M~7Y+9XIKF zteblDR~Wy^^1p~MT#L}V1vO)U$9)$WgtKm;=l^s;&We3PEM3?#O6y6qdcX9U7GmCE zS$vWd`;8VJn9AoNL*~P4T1Rn=p5$7hU4pBDoGXS+;Wm~@Q|CLIX&~e`mH*YfW8>LJ z1I^yA=}7q2Z1weyn7N{}@cw_lJc?9j=_GG27M;F*nWjBShD}zURe*KUP8ZLqxEk`(VNb2P+J~J=pHB^T40+r@}!4mtH?Yq9=D0dv&2(&@l+ZO zPdx50N9b!_EUZSJ)tJ4Q^*{dHTSm%0^4kIvqR!nsA^W|LOvbJ0Cih;ui-=A?YqX)+ z@XJgg4Yr?MYo@we#Qn3P8?PT57qc@7Y9cyWUTy@j?wiOjN(CCR03Q}n7UL84(Gph{ z!45e`aw~@!e&47ZX87Gq_a^*?C*WtR90vTnHVM0@zZ|$&_Jh!rarV0N?MoMNsU`rN z97UPN-QNoXkO-CC$|En4Ubf%m(|2VSF@;v!evG!a7yV4nrcTCD3BBzPO&P>MUXl5n z-;kM=FQjm!bmRipcewgfq~UN~u9fpTM5VoHV{jnElr8Wq{rEeyz`x4G!SBlV{^K$u zYgF1-tA;Gex7yAWKQWy+u)f82W@*LHQq6yW?wX^iG(CAN6z*(Johy@=&(dex&1}-| zn)%}nMQP~!%?H^X7gO+ZM9!|mL@#91F41a(Wf}tm$~u=7FIFf z{+NmdX3MO=dEy6I;_+G7cU>xGBh$_-F&ml4Y^+%==U$v;#_>yHqzxi0oGx0$h0&jU zgf6X26;Bn4{>puCB(iDfkK5%YKr}dd3!^dN@CV|p7Y0U26@5z!{9^L-l-X#;{7b;7 z_i#IXQ;rA5Z$1i^mIPU(99V z>xEp-VoIKJLv!kV=2XTw?8(!ZXoiZx&jvNcyiPs2n6TlxT9xz+&g1y0k&V7;Eo5!6W|tIFSrK>1{vJ`m&YdC z52f7^yIkh}qTj_t|Cfazcpq{`GZBy6UmSV3DE5sprjrZ?q^rX^hz(p~kJM)uPO?Q_ z!Wl`pc1$uH7MV>Y32(Z~Tn5uk6k~Qd%wdrkr3Jf{B4pN6g|UAhF;&F6A_gu(9cgHp5K!OLWzD)g|%yeP@v~shHG} zg{IB5h|pj`B5(ZV{WR^dg-{U9tXKJ_QFRuY9A(>V-o$<#&sX2sx_t+4F@F0{~aCAZuaeSAT#U4 z{vVfz6-&QAM<7pmkMYYK;|E#U&7s*@Jeb+6GbktF=R4>NW+0Z*ALK5*6ry)-Cw`XW zidNq>HcqhBe_-P1>@3qfo81hmaDX@)d!$XmC$oK(+qa|r*h7=!Gnnr|J@oq*@cZw6 z&U@_7RQ}Q=g3ohd7Z<)tCh^KJYTyL~e_krF4hKlZ>^~whG4=$!RAyr1mGlVmK=d`>ze>!=A1+Guc5R z(O-gl*-hHcI~CF9d;j}=A>Q%nYo1s?XtwW-%huDLr+3_3>-lOov$)NJ%1h_-6<}!MoD(?I2R+gx{;A zNw%nE0EAD3${&;jp8zvI`(ocW;W0CP6Fg=(gCdElxuk^abDgC^eUgg$+7v47=`a7P zUhr-tH3McM`}RnxYm=GPcW+O)uf1tR+FDJyZ9-opk%knNh3`s@)xo;zA{m|RpuX^3 zPm4CcGiIlcnLmLPZYp!K{R*u38ou8C{ zat8#gvXVR4Wczi|Zlw40%LDwr_N7;Z384?D*dDYz3=HM(MLS!?XENFr27~WdgT1M~ z_KMGxPN)%g{wY~&>Sqd-a{lhKYPZ5U#7kVxNNV#7Wj_gwBU=u^y8BKw+|g|bALzbI z4Ik{j%XadO^84W6h1}=aM}zNMA}PKWMd|0{;ns-iqMygrNNf1u$zbFrku z%y8;HsyXto8o5C^=lQd@P5L1vvTJi5o_#8l;>r78*!^lX_vd_P?Jo=lzG*#?N%S@|`$IEvw@&SEle>;;GG469 zGqn0fkQF5w?6pKxE5QrdoTe!41ci^0+Gn{dbIscreljNxQJ3GM%fa_7+07A)^1-FO z3)_8>^tEuE3VKiXL}E`T?SBj}Y-Fkf*Z_k+NqXh*Dbj0!u~R4)gW+DqNqC*Mqq3Ptr`ZGEfg^fCB5y&9oa`m7>?;ydUC_OZ<-Z;#AR5kJ>v?}%O7$y;lrDEz z+otlJ=y8u~+cFJY6Oi14O7w++!PqmC_@DFyP+d^z`nH$W$Q&%HYdVdx?#D>Ar&Qr47F z%uFzD_4I4=+dM6+ufIbf%-D^84{P&VO#hpQvzsp&Kj&YAk%cA!&KIl9HA%|;ba5V8 z*$C1J`#zBiC5i($P)hUGivH4?Na`Czm0%OSLW#gC8uFecq|OL*tjxDAXNOOxVgK(5 z`>8D7!sTTyb5DymCQTBs_kpE%|Kj5WKGFin81wcrc`D~zSh7#lMPvCou#QZ2Wtr$= zx?x{C$=`txiMb0@c7_Tcyvp6yvgt= zOTeRK4pJ3o8ky)wrjcVpW1WP-aS<=SwubkEl=I1vf9z|uc>26268>3}>~Z(yoA$V` z{}p@OS514|Mn_r!Fc~Jfzs(+(M}uF0RIkVtTRCppdIBF5~~g6{+C7C>Slb7t?r4#Y^%$}C?20JZFRTFx-vtvRUXab zE5skRew!{{=S0vetiJb0NaV__Y~$CrDJP?|EMw!w{Wjs-k&-wl$nPM(P|aU8YmPw@ z^wlH`fcDUACk7arorV#GX0vOAnC;NHp3v;+PX2Li`7wSnq{fta1^<}(Q?C5UlRrfX z&5+Gz&_hXe`qMAjU`X?&1^!v#r0|zp4x|6@49(!|O!-RvFJ!FQHp;@}^?38A8o8oM zm>yMz8m`2-W0;Vb<@h$4SK{@T`2J=34_Yi{FBkn{VBmw`_${J8GyQt2 zWEfIHvsaq5JLIcMzC|)y$cpZvkZ%i~a^(mKP=zDazk$0Bdvu0;4d2FiD!xDPK|{(c zI9mC(e;Zyns(VP>Uq);Q5Q@fAAidc_zUa3TU4MhFVAG+w`*vb>LcSB}*xyMxF zUwaW^?s8hY>B85{Hqt!N&I_irhOS>@1x50en%7`F?d$nGeH+O0KQWy`(RZ0^DJ^iZ ztZc%3>R>V4w0~zL(SI7De+JOs+nI3I${dll)r|*D+v@k&ooix$feT{Fe=;)3&bk?P z)|Ce_R!8=@Y>nwBl{hPHtuZge2CV1T*^{b_iKLSq-1-qrRh$pTrv`^^ad*Po12(P7 zQOuy!rw!wovxvQ6)7D|akM&lL8uW6_BcAx0@@1VhutuIQo3pGFOp~ti9`AgVe!l&9 z=eOwRR-`(&3(vVSmtlX}bD-=iMmndt@*FR_Ryj!}>`I5{0K4XqtYngO_;-A$qYK+U zlF~z;^ze%IiG=+x-uJB3@eXo%OkWNxF4#ecq;|f*8KR3`h?Ka}2F85x?D90&ZMx|% ze8-#gHaxQ(VmiI{jajwobPFtENWpA4KkcP@BRHyfrq@_-&Gg7e$p08oe8K6FJKv22 z!v{##?|=h)r2BGwZhkl3Gg;vm;L8+ke%BpAPe8{3IKdin2Gzct|`ADQh4U-d4^up>HjPv5>A zQoQLdQNHYL8a~pA4@EwLkG{6fMbqAMoSv4rGO1^om%jbx7rup$_UFGTp8SS}{KmB+)1d(l%g}U{Rye z%B51cE!7#tYY2=p6LNYu0WS%@0IjuN+iGdAf;>cK5+DyiA^4a>NB~8h!vF?UNJ7GV z>%aCn^8ng=@25W|=dsUztiASn?X}mMV$(DCQAhVR)4=-(h==rp1gvKK>plU+q5B}! z7O(HIdk6w-$>(X;+pVg*w-apJ!^Z)8eTsu7p!fg)Fg~mMpar-rt@$ys zlS0oNl~e&KVaC6k=MSc?=lJoT^0v(!@ZkAbYFUnVeh&M|cw9Hx7S)Z?w0 z16nC!kh+Jd6Wj`ya;*Z_(E^A#C{N}aWhDXvS++L#%!p&xpsa^~I~eoTdYI|-lGl!cdCs>-x92oT>96hgpIX+r%ADFis=%y*0-BP4Z zp8M>n`5bgFr1UEFFS7nMKTGu2j8k9Rz@`R;LvVR!Xto8U;B%>F&Gr*3=0>_!F_grp ziA|*2C{0Q@6uC+Y@9vd$k;=vTp{rG%+=|nupJdS6)1)&oa7ahnOMt#Tq%40?IL2|D z0{r^=(&hSx)2$eb>Z%txCGef00~xMnd2toT3uyQC7s&PBhq!6mnI6nzx_l z+CTjgV2N703s|xoB9)j*(}(igN-qnJq{H^|R&-`w)3~WwH5athqAbd&wP;0MIXVd` z{%#_L=w4MO!ow%NkeA#v5->r(cw33f2@L;g(7Ng|uZ$`5c}uM&QJ%dV(*DVEpp`=_ z!rwDs;kb$2{%;S8aAASbgI1Ic!WnsW3uA4FbDwzou7z=uWV{E4p}+48;*?l4M_2{u z>cZ6g<7z#`PIVPVxDKXu7=v(d*w1@^ykLC2Gvp=5*j#Rqzv4Zc*_Qj++ zl4Zt7RuCXQ7~<MkX1t*sx|AYMnXXCcGr!OK^LbNt4kX-;eR0X4mk*e_-A8!2q}pKSa^Q z9*$c3{*@dno!3VAN-I3J#@vQ;{FD`n1V7B8ss$rg%f_&(UY0LB=STCw4yNG5Ud^x> zqfGZFQISonwKSen%VDU=dX8ilj{nCzg{ifknN3gC`-rmNKQ~a>lX)2Oe|(QdybcR3v3Fa&oFUXAa0uA1>Y85OtK{t2;*3CekWh=h4PZ$V|j7W$*Dz~ zIX@TZAr$`r)Aa_hW}y^px|1UT2k_@j+e{_!)5LvvL=HjrO$R-#z%8p9`s*m659qI> zi2gd>bgshq#1o?g)T%d2UeYkc>eB70T|#O|oujStHi^#h!#mr$bmLbs#xKyFK&MD% zB-cMclTj>oJwI?DMvzH78iFEix(M|y7zPOnn>gqxOH_A_ZkbTZ`?@_M0!# zcja@oC*~$Rr%WB-3Bo*4P@TvD1L+rpU$c|fql^O-@9Z*ZzZQ6#L*4WaTB4jtiPL&H zurP9i;2Yq!=Ft4$eY9iB#qsE(&q+p;{8IrwR)_d=;vu`NeL*M7U_APKGNO7vnK zYzZP}ZJ<+a2rccRo?YUqJpg+DcBr8XUjEx{$Rg-!!BkA1%vCD< zt1`7LhEZa{&sDD1+s|}p2~NKh!%DH>Ci9VB!z0)$7F_G?=eqn$@Ca6m1v%b+EDBGS z>F*owxR50T*9V>wISmBe4vV$UyNr(vsrQd&pDaG(gIEB6rAquAa^R0xe5WrL+;ZLJ zEZsD3n2$AIQZ9AWNlxSb}&K2~>^Ge>X=$~gUH8F-q34Nv@s7xpnv={>5 z7Sm5Rp)PEEa4;qoU9rIQY{ub}kVZ|}NA{m)c;O8ng^9y(onY($Bs;|hre0tP;Om7G z&nd$cwrnhH34qAj4Jj4M{tqu;R{89yO+xe%NdZPN9gMk0D$I3M<}?a(9l;HiKZ)#4 z;QbsEi6j+EjQ14#-hr$Xa~Epa3&i3V(SGIC%x1OR63vD^UITIu-)KtP;gzP(ACCh-vfg#*|5g0i_|e&7}1!Oju4s+@%Yuj3)WkwaS5 zl$yF;?LssAQH}_T#gh9xGtC4~g(AK%SH!}!xI{Ezp6ovi&-4lNq_{MOeUi_H@j@*C zu@D+02@$LroKff7eFvmE)+M7qWSespj;0lw81Uy`@g&zF`lN50l)I^xC*}}GIrB}c?r8I`)BKy_LZpb(#yLVN zG4!+vB~~$l;Hz++oH=eHLq_4!KXz%{yH0`taRpX&bN~ zAhb`pbhoRCX@OiW&`SIYoNZF$dZ@}^O5XlBR~!BH#}E)utuf*RDgi(k)a)jdyFe(_ z2IAS<^7L_&vSbb6K$FNx;2#LUoO`pNL18KzW*U^iG)VS;hsd>bI$jTKt{GfeUU*zt zmRzQBpZJ%vHYTc0o_;y-K&Hh;nCsN1pSI*aZduGUoAp!Zpes%Xm)N5Y7A|&@q>D2u z*}j6HuP+^*8G7y8RF7U8yaC_tfiN)1L0W-s^XkFyC;ImA4!teBQ)efbeIXdL^=5v) zQ{NGOx4VhW*=>66lknx(rWi(Pu z6%K_soW75T0#LE+MkpP6=IG~OSd(j?yf7_oe?o=2HlDChk0|#$b*>1Z8Ah;tK1whxe=IXe$Os(IwAL4kn*vK^|Iw=~ zuRi~jQPB8$ATwXY%HLPm{@`Rt;DP(Tg+M_1vCt>!vwJJ0kC?C$$0qIf+n(cn zg2VEnh3T+&bcLgignY&p&hC&aB>NBA*!;lu6cqxIkWc^g zHEA{wM#V!W3`>^cA@8o%#+gg~@93fN=+_C_z6-gq&TFHRm%NF0Y?Dzq?VGSON$0UM zu|gj`VftGNHg`uVn|tkO+`l{_%O%7(;xwH7u1{5~P^wVvdJO?di_$fz}nuUX*aOjSzFJdxf?sVOy9 zQE@$DSq&QRWD%5;(SIu%TTSVhF<9_ z!3UKlapE{dmO>y~A-6azSxU(N;Yf~-iNcM}d{kk>n6a9$YN@ZO()E!X_<(S``*?~$ zXv1Aor%sLAe>$1UcDIEy$6~VD&NEpxPL*$!zEir?X5zcbYnf1fuUx+8x(YthFUhq} zK?3Vn?s5uEUlpTl5i4Jr?3h)S1^AKXCFYqK!h}(R)+-b2vVX5^$zGtQ>p+G#u+Kti zlQJP!@R)+mKu{>eaznFk6DWa4icp><0CO8S^Rmb^;Wq%&mJlaxrCkVHg3G5`} znIoq3`kGu-vs+M#k2bGeY2=Lnr=>_!?k^%G0C@~*N>rwtT8$k9>iY=xOajDIgn$in)cm@jQxOXE zeqr$u-q!Z#lD)0lpXF`8nTqWRUyDSa%d1;meKn)y)j}rX?L6U@CiR9LUOm(CoauS} z-^4557CN*Rm?N;yItG$$k{6~xS%L!DO!JzcJ_F{lx<6kf{E%uL);5E+d2kuzm73`) z*9~!@cm!|*dM5O*AB?~%Y?&}oildC|-+-90Vn@wFe8FuZ7k$JI=*(aQLt+i3F}RI8 zmc%%blv-%te+tLb7i&x3AO^G!p(uR)h(cj&EP>BzX(IlFnIo@=%}ARJsG4<9DPfhQ87WR1o@tmlqg-0?k2eY`X8Wtk^YE#&MhO zp9#%TG^t<9*#kgs*aF%=6an{kNl2Nj`Rk!*s2bxR{P{sDxmsXj#(yEG@U@N5W3*b7 zlvjU*0~7ak{)UMEqPx{>;MH z!CJZ=ZfC-AQJ&O7%x+;RQo@yhqJ#n+%7iM}-*%y82ooj*_hfk!XCG14T_P3uA#BbDFLLc#@C{*%!#X}Q(o#M|r&JcU5+YzhEFs!iv7)w*z_n1;ue%UI zjFNgF&SQU${dHmGYfgwRuc@@0m{47mW?2LSL60M`l-y&)UKFtQyWcz8+__`2wd_O) zq(3=QmtB5}FU2Yg4#Asl@VOq_B>*IAj(L2C4F`S$;ZDOpGvBGZTegX>5beV1W!cYw2 zS{OlUl(jycLqC`pACN#sr0B=m7QdaK!d z=-`uoWT|#}D-e457XA$~azi;bZa5Ch2Po;R?G+Q!(#C0Q3tzl-NV3VEcZuEC%sM^B zLq!<>*P}o4FDq!9)p_8r^l&AAw<6>XLF#a|%H<)KX;5WzfAW5VF*0K5OB2|a)wuPf z8J_{zQ)@ZYCYad!mz_y9o3lm8pO&4h+!!Z6H9cdDMasI9GAUNm@6GofQG0K0p zd@vrDBJnhfo~m)ke&Zb6?)h1q6$qZf(TtaaaJqKMenq%y)O$?~O|AS5`dACNMdjHh z7H6f$L!h3EghMSPw4Z~7C%+y@SqvjnmdZqG%3AQ87R{tDyzcD>5UC7Y!d!TQi^={Q zF0|}IV&(omD{p+P{x*CPd(0Lg*>9L_>IOEd7jP?xhn3(G3+F*cDH9%^If1+{So4;=D%U53hF zC}<*0dN~}(Wd9@tWvjLJ7)3?ueOWDRXXb${hecFf4y8m{6~_AQaF0}1vRblIs3pfC zI5BMHO+mFW+oG!W1EG&I2*6q|2cXMt5w02t)KMa(C4AV_RSk*lg-)$>s3Zs8I33Rf z3@(aLL&AmP)k>i+Yp3~qv6^geSMN9NTh9oflPagI3#*pJ=uln92)NwsFR{?{-L zhJDyH*bd2SQd*Tg@T{W3J+oR7>r*2EHZ3c!UFuL8k&Rc`tE}WKi@TLZPG-HbTBKn* zJtWk3slw^l6hZU$n0D9A?WDi5q;OhB#<^xmWS!o9T3)?dUajFe)Ssm^Lfz1^b4Vo` z#i{gG^H&+1*4fJcjVCiR8ijSOS)_lHH+LYdeA+^qXp{=|%JAUgY+yDHg=!`mXczCL zCV6wS(m019gpFM8Zo8QrW(RlY5;Hl>9+d$o?a~@~*nzp`j){=F?IJ;$#s*9rurFsP zIZZ?5k$o+QIh!sAs-v*wI%wlUd_QhiUj`hO6c^%ytclI$yWj7dgIZM_PZ5)8D zq=>bv33C(fk9JdVtU~N{rzvMPMV4=)ir)Sjq7kV}LUvQpgqW?wEDitCGRvLisP>m` ztmU{FErS*!qrP$1$3z(oSXGQykHHQ{3NqV3g@O!S)qjGsvvDTY#!;Mza4V)Ouf_L8 zHVL6MC34_ECU5x4t0y?0J;iosSFb#|m);qdA5Vk=D(s2HW|w7MU(hBTsLUA$mQN^@ z8$yC9k21HA8ncAKqh%Y^M`eOj_TM(l5>71?6+)`1zwwmeYPv{n&N$QzGb!?kQ65u)M3n{hHT%{-#i6xrWwX%C|2 zxN*zjM2X04&oV>gxPNoy)lH-h&JYHVf==a>x`s_2*PUzf_^6J)c=|OCeQdj*sJ(g{ zK6iy{t`vrj@ZuNHUXrO}LsvMu#Yztxmy2?wYyP-~aNZ~B> z@m{ggk}3**ddpzUxb;hG$5xX4A06kdLI3u&v$QRBQ(QWLrZ*_!;av?W?D`WTbeJQq zYNhT9#{Pp^`4Y=D?#?c>$Gp?2E(UAoLu)Is_8-blK!kcN?&y<{JJ@g4kt{W-LE*wV zRLIeH^~aL*zvxnUQIQIH9A?W}9JW$xsuYnAtvGgbjl&y@-HaWW;l~jZo4c?PXP#6I zNWWj`m*Hpi%p#ikQ@ok5uT77T%6W@z-^V0)baG?e?KrF?zVALL*T0Tfv3C>+nI`l} zx^|0AEAO+Jr_aN!ddXLMNiWfigJS=vo9YZ7qJ>V>qI*j>j(vZuPy32i__SVE$kWoL zyXI%rln5+PO+VKxt+d6aRqnUxB?DBiSz&ktDh%`4j(8PKo=%aRgCrG^432?eWW%*v zxI(iYV7&HqEZOW7xR`TFTdI8KNvm}@)7GE-nkCkSMHy7mKw!JQ(IE?q($hS4J_nZW zIUzt|?D*R9*C9#+OZljNLAV7`DCl(UQhJq-KDx{aRdDz{eLKl-~2pDNmP)?(0JyUppn8I>#M=D zX=JA-q3rTPgk`N?%rLTRnTf%f2$OyWt5;r&;NGql`SxCRX7|O?J zEC;?*#|eFnKOVsX%r`gIIeU1my$qC`*69TbU)Nmh>;0x&KLmCxesBTznG=t{cN|-l zcP0lFP1IU!a|l^F<~b{5=|6n%2dZQiHCOlm=z&U} z)I)>T$x2N^btsUEICphEAX&_$Q zto`YYsq4@*Y)5w9m}asg{Ih<=5oi*(4?q|tUqB#ynKxUqo)ZV99-RhBv=({?9M5$&sKp)HSudMpCaQE0 z!Hheh7xAunhO9+}E^RG@DS0TQK%Ur1dGB z_~mG-1VQ4%i6rD@=;P4rzeHl~wlk>!t#WhVYg>G|nE2g{c{*lH`vf9QgV1Q^9uwIm z;+9hBw5W`28l0c;$|cm(iBzsjPm(_xNypom*q_ak>*u};H;hQedCqp2M>7h9eb78? zQ9pJk%%x+6x%Aqdruoqrzw^8vfl}Nf(8%l{eG{i(HfN+9iXkls4U6V8gbQc%sijst zrz&`H-{%lBCXA9RETd#u7RQ5a7G5x`I4csg#3rTe5JK*zXr|+;InsHDR_!dTxuhVt z-!?uxq7)b-a(6X(j;dvcX!gdKYbXE3v5))`PMl{1*J3A5MV#auO>KjjWSKkA=3#$v z)Fas&A@cIJgkPpO&wQNW$%8B5&#|5P2Bt#Qv}an_~S=@ zsZN=zxTz-yN}7u$mz|Rq_bD5#RywCsk8;oHd?od((pd!oJ~Dh8BGi@e@pZF5TNbJ- zoJ&P8K{kW^Q0gvT3qJyXG0A;3vy0pKW-j~oT!1*hZ}WrOhXOT*5waI=;Ro+zqQp-i zv25Z8JLy3&_@o?ocnCM}jOkkNT_~!{uo5{ATUWa9-!A?$fA?q9yo8g_yvz@>x$Vhl z>JR+jPu%t|(9}!(;M-Jx@gKRVp+h*}C^vOd_HTq&tICGMHqYhi#)M%*#9ZWvi+O!2 z2T&*$?(i@1k^Y!x#4Mhk1K;D=FUk<=DsrSI{Go6dRrY_1f%8$d;{I#!;AI}bh3M~S zE-{`S6kmhO04H-7kWhR_Zb+DQ@fxusF1vxK>~;y~L5u&!gZlu9FY0B0{vZ6Hf#HGE zk;SX|!5gUm?>u~aqJ9q3v2iFGTE|{Ah8uNk=TD}CDKEYNAbgh}T!lK`<2tt3Qf6!3 z(6ho1QZIj3$f54OgIslSf3o6rTvHiW-GJ_`=LeM#T{m*ovuqp#^%>!1?7tGK#T$t^ z5n*>kfWZ^WMcyaoi1Uvd5i@m)Te-+UA};IvUh!YK`b=)`0krp;z!!|_b#AXOBV{PB z139LQ;mC-;URwML7ymc@{V=@ik=s zf5yXP`zzpfErN{{1bs&qV$Bs_a4q*2JspnAz&)wmyAa)5h9gGY!`9-h+@CkGkXpGv zdCC2mhae99!{kLDawhf_Ng)Ya&3S6Op6BTaWJy|D{3ciTDW+o$SGPAkrTuy!&$4IC z_6;2ID`uafz&}JBUT*ivwB8uU6~4&ZkLeLH=L46)uBb-DO1scCWsdng25vWHj;m?l02rEP9aN=UedpiwW<$3mD$R&+dOAH&xC} z9kcR*S^ck&eEt6>FUsP6FBVWjBCh!OZu<)3lj4Z?8Ft~uS5vW<29x(6676jPo=nN- z_G->*kH_~%u3`+meqivd@o5#kzWywE(L#>V@~Y^MYcK_LGf@AsrPh30F?iN^OZoje z>;370r1#wZcQOzki>V;kPF_<%P?=I>bPSw-JYNC-mNmrZf8eb4-pBhX)%+5PxJ3L- zj@P+h;H>d65Zz83wz&#I#4}irUqMEzN08-NQ|2C6X2UMx?M(`^1%xR@oyp@*Ppq}8 zOv-)V4~TMKG>{a}R4f?mLyo5Mt|@cHl308MyGB#yQN150?;nY6m|~3I?`{&0~Yu&Ltb>T#6eswnEuTi z)v)jLdfM2ZG~d*zBuY2hg#P~P2>Q$VQ_dqN;oBSh;WJ`3=cKf+Ed@gO`qSg5{@-0w3W6N# z^QZQVpW3{A5V02gK)pV2vZhd8dyiw`>6qK*(ra?HTSsVB*E?KccdooPizMZR{W14( zS9|r*g5bUP$K$iHZj^L?*A$NVgCK$=#8`Xu9-{si`qyDex6hsQD-zM9<=XBwodgR z=1iv{*W$e9zJq#ZDV5%T_|aeJZF;ryLiw#y=Y`?mn>IZ-6WR;k>vNnAeSm&1gi~y( z^8&iOpc5N!^ax?*bX#CL=}|<8=xPQw5D#|RoZZpxc5LNDhcf*|)S>L=#Ck-6U=cDp z5#LYQ<~x#41cr}ejr#|0ATV2KiCp&&`5Itoea2n4$j0UeIC2!i&nQoErNQX%$+nKz>Q__O!c^X2W~>jFsqdjOd|8q>jnARY>6;cG0vn4^Z`j}Apl&|o8wEbe zYqN*c(UKIgS&t)HU)yP6?pY;q_`?cG_Wwv?jsO4_oYXb~Sdk}JCjhxM;$F>$!5nqG zJrJy!R~74($l zuPHXj0-IrBIT;a?_IV`{ufju&pC3Sq4?a_{R2G}#uJM0cQZzHJ!@ zaQWE)tt=uK^6D@1_TGzQcK#tKnxNlP3pz+jOoXda8Zw=HrqAZ<9rf^WMA{(1Xj{ub zOxY$ZQ0s!w?N)noy=QQxtYNSB$=l4+FJ*SM0uILYGogo0%XfV#`|ll!tQAqa65eyb z)#DzDgy`+G7}$qNWw-f^vQff?hE4VBQcU%&vrHrXDC^{5Zt8Kt|4J$#d}`A8Sa(1~ zyte9D$i;l~oi2w|4B3B+xEd<^|01sJo(U~{uvK`G z!0enJkG&(Tabx0d3**nm#-A0L&$Pf1^lxfJ3H%+J!nDdnz9<++LWq(T_I&Bu<@rRt zǓ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 Znkm>8)2%tce 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 0d47eb844a..e7c56eb749 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ keywords = ['discord', 'modmail'] [tool.poetry.dependencies] python = "^3.7" -"discord.py" = "=1.5.1" +"discord.py" = "./discord.py-1.5.2.tar.gz" uvloop = {version = ">=0.12.0", markers = "sys_platform != 'win32'"} python-dotenv = ">=0.10.3" parsedatetime = "^2.6" diff --git a/requirements.min.txt b/requirements.min.txt index 9fd4adf1a3..cf1b4bdd26 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -6,7 +6,7 @@ aiohttp==3.6.2 async-timeout==3.0.1 attrs==19.3.0 chardet==3.0.4 -discord.py==1.5.1 +./discord.py-1.5.2.tar.gz dnspython==1.16.0 emoji==0.5.4 future==0.18.2 From 247955842c3b64a20243a869a3e763c5779f9ab9 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Nov 2020 01:15:46 +0800 Subject: [PATCH 0651/1221] Bump version --- CHANGELOG.md | 3 ++- bot.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3317ecd03a..a62d66c2e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.7.0-dev6 +# v3.7.0-dev7 ### Added @@ -23,6 +23,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added a way to block roles. ([GH #2753](https://github.com/kyb3r/modmail/issues/2753)) - Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/kyb3r/modmail/issues/2865)) - Added `?selfcontact` to allow users to open a thread. ([GH #2762](https://github.com/kyb3r/modmail/issues/2762)) +- Support stickers and reject non-messages (i.e. pin_add) ### Fixed diff --git a/bot.py b/bot.py index daf1f593c1..a5dce84191 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev6" +__version__ = "3.7.0-dev7" import asyncio From edd7ef26426f3d1f7ac19787474a6b3795a354fd Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Nov 2020 01:24:54 +0800 Subject: [PATCH 0652/1221] Clearer error messages for plain messages edit/del --- cogs/modmail.py | 4 ++-- core/thread.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index d51d210f8a..7afcabcd76 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -898,7 +898,7 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): return await ctx.send( embed=discord.Embed( title="Failed", - description="Cannot find a message to edit.", + description="Cannot find a message to edit. Plain messages are not supported.", color=self.bot.error_color, ) ) @@ -1307,7 +1307,7 @@ async def delete(self, ctx, message_id: int = None): return await ctx.send( embed=discord.Embed( title="Failed", - description="Cannot find a message to delete.", + description="Cannot find a message to delete. Plain messages are not supported.", color=self.bot.error_color, ) ) diff --git a/core/thread.py b/core/thread.py index 426b76489d..5fbcfadea8 100644 --- a/core/thread.py +++ b/core/thread.py @@ -553,7 +553,7 @@ async def find_linked_messages( return message1, msg except ValueError: continue - raise ValueError("DM message not found.") + raise ValueError("DM message not found. Plain messages are not supported.") async def edit_message(self, message_id: typing.Optional[int], message: str) -> None: try: From d294d08d5930df64fb98cbc345e1f5828a56cda5 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 6 Nov 2020 18:57:02 +0100 Subject: [PATCH 0653/1221] Update modmail.py --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7afcabcd76..312cfd9c35 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -855,7 +855,7 @@ async def preply(self, ctx, *, msg: str = ""): @checks.thread_only() async def pareply(self, ctx, *, msg: str = ""): """ - Reply to a Modmail thread with a plain message and anonmymously. + Reply to a Modmail thread with a plain message and anonymously. Supports attachments and images as well as automatically embedding image URLs. From 305196b9c96142f1ca82144c8ac9bbe193a36303 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Nov 2020 15:21:51 +0800 Subject: [PATCH 0654/1221] Add support for thread titles, resolve #2838 --- CHANGELOG.md | 3 ++- bot.py | 5 +++-- cogs/modmail.py | 9 +++++++++ core/thread.py | 12 ++++++++++-- core/utils.py | 23 +++++++++++++++++++++-- 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a62d66c2e6..d83da82e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.7.0-dev7 +# v3.7.0-dev8 ### Added @@ -24,6 +24,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/kyb3r/modmail/issues/2865)) - Added `?selfcontact` to allow users to open a thread. ([GH #2762](https://github.com/kyb3r/modmail/issues/2762)) - Support stickers and reject non-messages (i.e. pin_add) +- Add support for thread titles, `?title` ([GH #2838](https://github.com/kyb3r/modmail/issues/2838)) ### Fixed diff --git a/bot.py b/bot.py index a5dce84191..32ff4e76e8 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev7" +__version__ = "3.7.0-dev8" import asyncio @@ -32,7 +32,7 @@ from core import checks from core.clients import ApiClient, PluginDatabaseClient, MongoDBClient from core.config import ConfigManager -from core.utils import human_join, normalize_alias +from core.utils import human_join, match_title, normalize_alias from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging from core.thread import ThreadManager from core.time import human_timedelta @@ -458,6 +458,7 @@ async def on_ready(self): log["channel_id"], { "open": False, + "title": match_title(thread.channel.topic), "closed_at": str(datetime.utcnow()), "close_message": "Channel has been deleted, no closer found.", "closer": { diff --git a/cogs/modmail.py b/cogs/modmail.py index 7afcabcd76..7c8352bb64 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -646,6 +646,15 @@ def format_log_embeds(self, logs, avatar_url): embeds.append(embed) return embeds + + @commands.command() + @checks.thread_only() + async def title(self, ctx, *, name: str): + await ctx.thread.set_title(name) + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + + @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs(self, ctx, *, user: User = None): diff --git a/core/thread.py b/core/thread.py index 5fbcfadea8..a70f25a58f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -12,7 +12,7 @@ from core.models import getLogger from core.time import human_timedelta -from core.utils import is_image_url, days, match_user_id, truncate, format_channel_name +from core.utils import is_image_url, days, match_title, match_user_id, truncate, format_channel_name logger = getLogger(__name__) @@ -334,6 +334,7 @@ async def _close( self.channel.id, { "open": False, + "title": match_title(self.channel.topic), "closed_at": str(datetime.utcnow()), "nsfw": self.channel.nsfw, "close_message": message if not silent else None, @@ -346,6 +347,8 @@ async def _close( }, }, ) + else: + log_data = None if isinstance(log_data, dict): prefix = self.bot.config["log_url_prefix"].strip("/") @@ -353,7 +356,9 @@ async def _close( prefix = "" log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{log_data['key']}" - if log_data["messages"]: + if log_data["title"]: + sneak_peak = log_data["title"] + elif log_data["messages"]: content = str(log_data["messages"][0]["content"]) sneak_peak = content.replace("\n", "") else: @@ -931,6 +936,9 @@ def get_notifications(self) -> str: return " ".join(mentions) + async def set_title(self, title) -> None: + user_id = match_user_id(self.channel.topic) + await self.channel.edit(topic=f'Title: {title}\nUser ID: {user_id}') class ThreadManager: """Class that handles storing, finding and creating Modmail threads.""" diff --git a/core/utils.py b/core/utils.py index 72c96c00f6..0a7b452f18 100644 --- a/core/utils.py +++ b/core/utils.py @@ -216,9 +216,28 @@ 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) +TOPIC_TITLE_REGEX = re.compile(r"\bTitle: (.*)\n(?:User ID: )\b", flags=re.IGNORECASE | re.DOTALL) +TOPIC_UID_REGEX = re.compile(r"\bUser ID:\s*(\d{17,21})\b", flags=re.IGNORECASE) +def match_title(text: str) -> int: + """ + Matches a title in the foramt of "Title: XXXX" + + Parameters + ---------- + text : str + The text of the user ID. + + Returns + ------- + Optional[str] + The title if found + """ + match = TOPIC_TITLE_REGEX.search(text) + if match is not None: + return match.group(1) + def match_user_id(text: str) -> int: """ Matches a user ID in the format of "User ID: 12345". @@ -233,7 +252,7 @@ def match_user_id(text: str) -> int: int The user ID if found. Otherwise, -1. """ - match = TOPIC_REGEX.search(text) + match = TOPIC_UID_REGEX.search(text) if match is not None: return int(match.group(1)) return -1 From 435021312524670a2645fac036014de4da9b9d95 Mon Sep 17 00:00:00 2001 From: Cyrus Yip Date: Sat, 7 Nov 2020 17:24:09 -0800 Subject: [PATCH 0655/1221] im scared to lose my work :| --- cogs/modmail.py | 15 ++++++++++++++- core/clients.py | 19 +++++++++++++++++++ core/thread.py | 14 +++++++++++--- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index a383a78553..b2656c03c9 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -873,7 +873,7 @@ async def pareply(self, ctx, *, msg: str = ""): async with ctx.typing(): await ctx.thread.reply(ctx.message, anonymous=True, plain=True) - @commands.command() + @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def note(self, ctx, *, msg: str = ""): @@ -887,6 +887,19 @@ async def note(self, ctx, *, msg: str = ""): msg = await ctx.thread.note(ctx.message) await msg.pin() + @note.command(name="persistent", aliases=["persist"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def note_persistent(self, ctx, *, msg: str = ""): + """ + Take a persistent note about the current user. + """ + ctx.message.content = msg + async with ctx.typing(): + msg = await ctx.thread.note(ctx.message, persistent=True) + await msg.pin() + await self.bot.api.create_note(recipient=ctx.thread.recipient, message=ctx.message) + @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() diff --git a/core/clients.py b/core/clients.py index 03921d5a17..6d74290105 100644 --- a/core/clients.py +++ b/core/clients.py @@ -401,6 +401,25 @@ async def search_by_text(self, text: str, limit: Optional[int]): {"messages": {"$slice": 5}}, ).to_list(limit) + async def create_note(self, recipient: Member, message: Message): + await self.db.notes.insert_one( + { + "recipient": str(recipient.id), + "author": { + "id": str(message.author.id), + "name": message.author.name, + "discriminator": message.author.discriminator, + "avatar_url": str(message.author.avatar_url), + }, + } + ) + + async def delete_note(self): + pass + + async def find_notes(self, recipient: Member): + return await self.db.notes.find({"recipient": str(recipient.id)}).to_list(None) + def get_plugin_partition(self, cog): cls_name = cog.__class__.__name__ return self.db.plugins[cls_name] diff --git a/core/thread.py b/core/thread.py index a70f25a58f..7786e89ace 100644 --- a/core/thread.py +++ b/core/thread.py @@ -190,6 +190,13 @@ async def send_recipient_genesis_message(): close_emoji = await self.bot.convert_emoji(close_emoji) await self.bot.add_reaction(msg, close_emoji) + async def send_persistent_notes(): + notes = await self.bot.api.find_notes() + for note in notes: + message = discord.Message() + await self.note(note.message) + pass + await asyncio.gather(send_genesis_message(), send_recipient_genesis_message()) self.bot.dispatch("thread_ready", self) @@ -629,11 +636,11 @@ async def edit_dm_message(self, message: discord.Message, content: str) -> None: self.bot.api.edit_message(message.id, content), linked_message.edit(embed=embed) ) - async def note(self, message: discord.Message) -> None: + async def note(self, message: discord.Message, persistent=False) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) - msg = await self.send(message, self.channel, note=True) + msg = await self.send(message, self.channel, note=True, persistent_note=persistent) self.bot.loop.create_task( self.bot.api.append_log( @@ -719,6 +726,7 @@ async def send( note: bool = False, anonymous: bool = False, plain: bool = False, + persistent_note: bool = False, ) -> None: self.bot.loop.create_task( @@ -780,7 +788,7 @@ async def send( else: # Special note messages embed.set_author( - name=f"Note ({author.name})", + name=f"{'Persistent' if persistent_note else ''} Note ({author.name})", icon_url=system_avatar_url, url=f"https://discordapp.com/users/{author.id}#{message.id}", ) From c7f6d537aeb19d55a148796fd3e2179dc81314bd Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Mon, 9 Nov 2020 00:13:49 +0800 Subject: [PATCH 0656/1221] Privacy policy/data_collection config-dev9 --- CHANGELOG.md | 7 +-- PRIVACY.md | 122 ++++++++++++++++++++++++++++++++++++++++++ README.md | 6 +++ bot.py | 25 ++++----- core/config.py | 3 ++ core/config_help.json | 9 ++++ 6 files changed, 157 insertions(+), 15 deletions(-) create mode 100644 PRIVACY.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d83da82e29..e6dca02925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.7.0-dev8 +# v3.7.0-dev9 ### Added @@ -19,12 +19,13 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/kyb3r/modmail/issues/2773)) - Support Gyazo image links in message embeds. ([GH #282](https://github.com/kyb3r/modmail/issues/282)) - Added `silent` argument to `?contact` to restore old behaviour. -- If `?help` is sent, bot does checks on every command, `?help all` restores old behaviour. ([GH #2847](https://github.com/kyb3r/modmail/issues/2847)) +- Added new functionality: If `?help` is sent, bot does checks on every command, `?help all` restores old behaviour. ([GH #2847](https://github.com/kyb3r/modmail/issues/2847)) - Added a way to block roles. ([GH #2753](https://github.com/kyb3r/modmail/issues/2753)) - Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/kyb3r/modmail/issues/2865)) - Added `?selfcontact` to allow users to open a thread. ([GH #2762](https://github.com/kyb3r/modmail/issues/2762)) - Support stickers and reject non-messages (i.e. pin_add) -- Add support for thread titles, `?title` ([GH #2838](https://github.com/kyb3r/modmail/issues/2838)) +- Added support for thread titles, `?title` ([GH #2838](https://github.com/kyb3r/modmail/issues/2838)) +- Added `data_collection` to specify if bot metadata should be collected by Modmail developers ### Fixed diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000000..8ff361b702 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,122 @@ +# Privacy Statement + +Hey, we are the lead developers of Modmail bot. This is a look into the data we collect, the data you collect, the data other parties collect, and what can be done about any of this data. +> **Disclaimer**: None of us are lawyers. We are just trying to be more transparent + +### TL;DR + +Yes, we collect some data to keep us happy. You collect some data to keep the bot functioning. External services also collect some data that is out of our control. + +## Interpretation + +- Modmail: This application that has been made open-source. +- Modmail Team: Lead developers, namely kyb3r, fourjr and taku. +- Bot: Your instance of the Modmail bot. +- Bot owner: The person managing the bot. +- Guild: A [server](https://discord.com/developers/docs/resources/guild#guild-resource), an isolated collection of users and channels, within Discord +- User: The end user, or server members, that interface with the bot. +- Database: A location where data is stored, hosted by the bot owner. The following types of database are currently supported: [MongoDB](#MongoDB). +- Logviewer: A webserver hosted by the bot owner. + +## The Data We Collect + +No data is being collected unless someone decides to host the bot and the bot is kept online. + +The Modmail Team collect some metadata to keep us updated on the number of instances that are making use of the bot and know what features we should focus on. The following is a list of data that we collect: +- Bot ID +- Bot username and discriminator +- Bot avatar URL +- Main guild ID +- Main guild name +- Main guild member count +- Bot uptime +- Bot latency +- Bot version +- Whether the bot is seflhosted + +No tokens/passwords/private data is ever being collected or sent to our servers. + +This metadata is sent to our centralised servers every hour that the bot is up and can be viewed in the bot logs when the `log_level` is set to `DEBUG`. + +As our bot is completely open-source, the part that details this behaviour is located in `bot.py > ModmailBot > post_metadata`. + +We assure you that the data is not being sold to anybody. + +### Opting out + +The bot owner can opt out of this data collection by setting `data_collection` to `off` within the configuration variables or the `.env` file. + +### Data deletion + +Data can be deleted with a request in a DM to our [support server](https://discord.gg/etJNHCQ)'s Modmail bot. + +## The Data You Collect + +When using the bot, the bot can collect various bits of user data to ensure that the bot can run smoothly. +This data is stored in a database instance that is hosted by the bot owner (more details below). + +When a thread is created, the bot saves the following data: +- Timestamp +- Log Key +- Channel ID +- Guild ID +- Bot ID +- Recipient ID +- Recipient Username and Discriminator +- Recipient Avatar URL +- Whether the recipient is a moderator + +When a message is sent in a thread, the bot saves the following data: +- Timestamp +- Message ID +- Message author ID +- Message author username and discriminator +- Message author avatar URL +- Whether the message author is a moderator +- Message content +- All attachment urls in the message + +This data is essential to have live logs for the web logviewer to function. +The Modmail team does not track any data by users. + +### Opting out + +There is no way for users or moderators to opt out frmo this data collection. + +### Data deletion + +Logs can be deleted using the `?logs delete ` command. This will remove all data from that specific log entry from the database permenantly. + +## The Data Other Parties Collect + +Plugins form a large part of the Modmail experience. Although we do not have any control over the data plugins collect, including plugins within our registry, all plugins are open-sourced by design. Some plugin devs may collect data beyond our control, and it is the bot owner's responsibility to check with the various plugin developers involved. + +We recommend 4 external services to be used when setting up the Modmail bot. +We have no control over the data external parties collect and it is up to the bot owner's choice as to which external service they choose to employ when using Modmail. +If you wish to opt out of any of this data collection, please view their own privacy policies and data collection information. We will not provide support for such a procedure. + +### Discord + +- [Discord Privacy Policy](https://discord.com/privacy) + +### Heroku + +- [Heroku Security](https://www.heroku.com/policy/security) +- [Salesforce Privacy Policy](https://www.salesforce.com/company/privacy/). + +### MongoDB + +- [MongoDB Privacy Policy](https://www.mongodb.com/legal/privacy-policy). + +### Github + +- [Github Privacy Statement](https://docs.github.com/en/free-pro-team@latest/github/site-policy/github-privacy-statement) + +## Maximum Privacy Setup + +For a maximum privacy setup, we recommend the following hosting procedure. We have included links to various help articles for each relevant step. We will not provide support for such a procedure. +- [Creating a local mongodb instance](https://zellwk.com/blog/local-mongodb/) +- [Hosting Modmail on your personal computer](https://taaku18.github.io/modmail/local-hosting/) +- Ensuring `data_collection` is set to `no` in the `.env` file. +- [Opt out of discord data collection](https://support.discord.com/hc/en-us/articles/360004109911-Data-Privacy-Controls) +- Do not use any plugins, setting `enable_plugins` to `no`. diff --git a/README.md b/README.md index 6173484788..700b3bc10b 100644 --- a/README.md +++ b/README.md @@ -184,3 +184,9 @@ Plugins requests and support is available in our [Modmail Plugins Server](https: Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! + +## Beta Testing + +Our [development](https://github.com/kyb3r/modmail/tree/development) branch is where most of our features are tested before public release. Be warned that there could be bugs in various commands so keep it away from any large servers you manage. + +If you wish to test the new features and play around with them, feel free to join our [Public Test Server](https://discord.gg/v5hTjKC). Bugs can be raised within that server or in our Github issues (state that you are using the development branch though). diff --git a/bot.py b/bot.py index 32ff4e76e8..81dc347f95 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev8" +__version__ = "3.7.0-dev9" import asyncio @@ -477,17 +477,18 @@ async def on_ready(self): "Failed to close thread with channel %s, skipping.", log["channel_id"] ) - self.metadata_loop = tasks.Loop( - self.post_metadata, - seconds=0, - minutes=0, - hours=1, - count=None, - reconnect=True, - loop=None, - ) - self.metadata_loop.before_loop(self.before_post_metadata) - self.metadata_loop.start() + if self.config["data_collection"]: + self.metadata_loop = tasks.Loop( + self.post_metadata, + seconds=0, + minutes=0, + hours=1, + count=None, + reconnect=True, + loop=None, + ) + self.metadata_loop.before_loop(self.before_post_metadata) + self.metadata_loop.start() other_guilds = [ guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild} diff --git a/core/config.py b/core/config.py index 91c50d66f5..5a9ecc7787 100644 --- a/core/config.py +++ b/core/config.py @@ -137,6 +137,8 @@ class ConfigManager: "github_token": None, # Logging "log_level": "INFO", + # data collection + "data_collection": True, } colors = {"mod_color", "recipient_color", "main_color", "error_color"} @@ -158,6 +160,7 @@ class ConfigManager: "alert_on_mention", "confirm_thread_creation", "enable_plugins", + "data_collection", "enable_eval", } diff --git a/core/config_help.json b/core/config_help.json index cc941048c5..a0584e36fa 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -758,5 +758,14 @@ "notes": [ "This configuration can only to be set through `.env` file or environment (config) variables." ] + }, + "data_collection": { + "default": "Yes", + "description": "Controls if bot metadata should be sent to the development team.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] } } From 4af8dde4e5b811a9e5379c3546dc3945560220ae Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Mon, 9 Nov 2020 00:19:06 +0800 Subject: [PATCH 0657/1221] add check for title --- cogs/modmail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/modmail.py b/cogs/modmail.py index a383a78553..f59b1a9139 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -648,6 +648,7 @@ def format_log_embeds(self, logs, avatar_url): @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def title(self, ctx, *, name: str): await ctx.thread.set_title(name) From 4bd61ad34c7d4857a2f29fe13b4036803cd9802f Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Mon, 9 Nov 2020 00:22:44 +0800 Subject: [PATCH 0658/1221] pin ?title message --- cogs/modmail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/modmail.py b/cogs/modmail.py index f59b1a9139..ce03a969c3 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -653,6 +653,7 @@ def format_log_embeds(self, logs, avatar_url): async def title(self, ctx, *, name: str): await ctx.thread.set_title(name) sent_emoji, _ = await self.bot.retrieve_emoji() + await ctx.message.pin() await self.bot.add_reaction(ctx.message, sent_emoji) From 2da6fddf644b58b5b7c2075083ceae5a76112316 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Mon, 9 Nov 2020 00:23:12 +0800 Subject: [PATCH 0659/1221] Set docstring for title --- cogs/modmail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/modmail.py b/cogs/modmail.py index ce03a969c3..f9dc2eb025 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -651,6 +651,7 @@ def format_log_embeds(self, logs, avatar_url): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() async def title(self, ctx, *, name: str): + """Sets title for a thread""" await ctx.thread.set_title(name) sent_emoji, _ = await self.bot.retrieve_emoji() await ctx.message.pin() From d647edb34b1d9bc25837838e4322a4779154af65 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 15:24:31 +0800 Subject: [PATCH 0660/1221] `?move` now does not require exact category names, accepts case-insensitive and startswith names --- CHANGELOG.md | 3 ++- bot.py | 4 ++-- cogs/modmail.py | 8 +++----- core/models.py | 23 +++++++++++++++++++++++ core/thread.py | 14 +++++++++++--- core/utils.py | 1 + 6 files changed, 42 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6dca02925..68fe124f9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.7.0-dev9 +# v3.7.0-dev10 ### Added @@ -34,6 +34,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Improved - Plugins installations have clearer error messages +- `?move` now does not require exact category names, accepts case-insensitive and startswith names # v3.6.2 diff --git a/bot.py b/bot.py index 81dc347f95..15deef95a0 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev9" +__version__ = "3.7.0-dev10" import asyncio @@ -748,7 +748,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: if blocked: return sent_emoji, blocked_emoji = await self.retrieve_emoji() - + if message.type != discord.MessageType.default: return diff --git a/cogs/modmail.py b/cogs/modmail.py index f9dc2eb025..01bdb4d341 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -14,7 +14,7 @@ from natural.date import duration from core import checks -from core.models import PermissionLevel, getLogger +from core.models import PermissionLevel, SimilarCategoryConverter, getLogger from core.paginator import EmbedPaginatorSession from core.thread import Thread from core.time import UserFriendlyTime, human_timedelta @@ -305,7 +305,7 @@ async def move(self, ctx, *, arguments): else: fmt = " ".join(split_args[:-i]) - category = await commands.CategoryChannelConverter().convert(ctx, fmt) + category = await SimilarCategoryConverter().convert(ctx, fmt) except commands.BadArgument: if i == len(split_args) - 1: # last one @@ -646,7 +646,6 @@ def format_log_embeds(self, logs, avatar_url): embeds.append(embed) return embeds - @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @@ -657,7 +656,6 @@ async def title(self, ctx, *, name: str): await ctx.message.pin() await self.bot.add_reaction(ctx.message, sent_emoji) - @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs(self, ctx, *, user: User = None): @@ -930,7 +928,7 @@ async def contact( ctx, user: Union[discord.Member, discord.User], *, - category: Union[discord.CategoryChannel, str] = None, + category: Union[SimilarCategoryConverter, str] = None, manual_trigger=True, ): """ diff --git a/core/models.py b/core/models.py index 12365f24da..30565632dd 100644 --- a/core/models.py +++ b/core/models.py @@ -185,3 +185,26 @@ def get_value(self, key, args, kwds): return "{" + key + "}" else: return Formatter.get_value(key, args, kwds) + + +class SimilarCategoryConverter(commands.CategoryChannelConverter): + async def convert(self, ctx, argument): + bot = ctx.bot + guild = ctx.guild + result = None + + try: + return await super().convert(ctx, argument) + except commands.ChannelNotFound: + def check(c): + return isinstance(c, discord.CategoryChannel) and c.name.lower().startswith(argument.lower()) + + if guild: + result = discord.utils.find(check, guild.categories) + else: + result = discord.utils.find(check, bot.get_all_channels()) + + if not isinstance(result, discord.CategoryChannel): + raise commands.ChannelNotFound(argument) + + return result diff --git a/core/thread.py b/core/thread.py index a70f25a58f..3eb6e93a52 100644 --- a/core/thread.py +++ b/core/thread.py @@ -12,7 +12,14 @@ from core.models import getLogger from core.time import human_timedelta -from core.utils import is_image_url, days, match_title, match_user_id, truncate, format_channel_name +from core.utils import ( + is_image_url, + days, + match_title, + match_user_id, + truncate, + format_channel_name, +) logger = getLogger(__name__) @@ -806,7 +813,7 @@ async def send( if is_image_url(url, convert_size=False) ] images.extend(image_urls) - images.extend((str(i.image_url), f'{i.name} Sticker', True) for i in message.stickers) + images.extend((str(i.image_url), f"{i.name} Sticker", True) for i in message.stickers) embedded_image = False @@ -938,7 +945,8 @@ def get_notifications(self) -> str: async def set_title(self, title) -> None: user_id = match_user_id(self.channel.topic) - await self.channel.edit(topic=f'Title: {title}\nUser ID: {user_id}') + await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}") + class ThreadManager: """Class that handles storing, finding and creating Modmail threads.""" diff --git a/core/utils.py b/core/utils.py index 0a7b452f18..239522139a 100644 --- a/core/utils.py +++ b/core/utils.py @@ -238,6 +238,7 @@ def match_title(text: str) -> int: if match is not None: return match.group(1) + def match_user_id(text: str) -> int: """ Matches a user ID in the format of "User ID: 12345". From 69d60abb57a82c834d9e82c92d53beebd2b24d6f Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 15:30:57 +0800 Subject: [PATCH 0661/1221] Fix issue with updateperms not casting to str, resolve #2856 --- CHANGELOG.md | 1 + bot.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68fe124f9a..8f7b91c612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed - `?contact` now sends members a DM. +- Fixed issue where `level_permissions` and `command_permissions` would sometimes be reset. ([GH #2856](https://github.com/kyb3r/modmail/issues/2856)) ### Improved diff --git a/bot.py b/bot.py index 15deef95a0..670ea066f6 100644 --- a/bot.py +++ b/bot.py @@ -882,7 +882,7 @@ async def get_context(self, message, *, cls=commands.Context): async def update_perms( self, name: typing.Union[PermissionLevel, str], value: int, add: bool = True ) -> None: - value = int(value) + value = str(value) if isinstance(name, PermissionLevel): permissions = self.config["level_permissions"] name = name.name From 27afe7d837c18ab1b4e8e9ceabd75d58010c2b49 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 17:25:47 +0800 Subject: [PATCH 0662/1221] Added `?autotrigger` to specify keywords to trigger commands, resolve #130, resolve #649 --- CHANGELOG.md | 7 +-- bot.py | 50 ++++++++++++++++++++ cogs/modmail.py | 5 +- cogs/utility.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++- core/config.py | 1 + core/models.py | 5 +- core/thread.py | 26 +++++++++-- 7 files changed, 205 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f7b91c612..1dda1042e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,10 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added a way to block roles. ([GH #2753](https://github.com/kyb3r/modmail/issues/2753)) - Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/kyb3r/modmail/issues/2865)) - Added `?selfcontact` to allow users to open a thread. ([GH #2762](https://github.com/kyb3r/modmail/issues/2762)) -- Support stickers and reject non-messages (i.e. pin_add) -- Added support for thread titles, `?title` ([GH #2838](https://github.com/kyb3r/modmail/issues/2838)) -- Added `data_collection` to specify if bot metadata should be collected by Modmail developers +- Support stickers and reject non-messages. (i.e. pin_add) +- Added support for thread titles, `?title`. ([GH #2838](https://github.com/kyb3r/modmail/issues/2838)) +- Added `data_collection` to specify if bot metadata should be collected by Modmail developers. +- Added `?autotrigger` to specify keywords to trigger commands. ([GH #130](https://github.com/kyb3r/modmail/issues/130), [GH #649](https://github.com/kyb3r/modmail/issues/649)) ### Fixed diff --git a/bot.py b/bot.py index 670ea066f6..b68e0121aa 100644 --- a/bot.py +++ b/bot.py @@ -2,6 +2,7 @@ import asyncio +import copy import logging import os import re @@ -244,6 +245,10 @@ def snippets(self) -> typing.Dict[str, str]: def aliases(self) -> typing.Dict[str, str]: return self.config["aliases"] + @property + def auto_triggers(self) -> typing.Dict[str, str]: + return self.config["auto_triggers"] + @property def token(self) -> str: token = self.config["token"] @@ -852,6 +857,51 @@ async def get_contexts(self, message, *, cls=commands.Context): ctx.command = self.all_commands.get(invoker) return [ctx] + async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context): + message.author = self.modmail_guild.me + message.channel = channel + + view = StringView(message.content) + ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) + thread = await self.threads.find(channel=ctx.channel) + + invoked_prefix = self.prefix + invoker = view.get_word().lower() + + # Check if there is any aliases being called. + alias = self.auto_triggers[ + next(filter(lambda x: x in message.content, self.auto_triggers.keys())) + ] + if alias is None: + ctx.thread = thread + ctx.invoked_with = invoker + ctx.command = self.all_commands.get(invoker) + ctxs = [ctx] + else: + ctxs = [] + aliases = normalize_alias(alias, message.content[len(f"{invoked_prefix}{invoker}") :]) + if not aliases: + logger.warning("Alias %s is invalid as called in automove.", invoker) + + for alias in aliases: + 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, await self.get_prefix()) + ctx_.invoked_with = view.get_word().lower() + ctx_.command = self.all_commands.get(ctx_.invoked_with) + ctxs += [ctx_] + + for ctx in ctxs: + if ctx.command: + old_checks = copy.copy(ctx.command.checks) + ctx.command.checks = [checks.has_permissions(PermissionLevel.INVALID)] + + await self.invoke(ctx) + + ctx.command.checks = old_checks + continue + async def get_context(self, message, *, cls=commands.Context): """ Returns the invocation context from the message. diff --git a/cogs/modmail.py b/cogs/modmail.py index 01bdb4d341..8c0323ae09 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -338,7 +338,10 @@ async def move(self, ctx, *, arguments): await thread.channel.send(f"{mention}, thread has been moved.") sent_emoji, _ = await self.bot.retrieve_emoji() - await self.bot.add_reaction(ctx.message, sent_emoji) + try: + await self.bot.add_reaction(ctx.message, sent_emoji) + except discord.NotFound: + pass async def send_scheduled_close_message(self, ctx, after, silent=False): human_delta = human_timedelta(after.dt) diff --git a/cogs/utility.py b/cogs/utility.py index 0e9aaac753..55f237ab3f 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -22,7 +22,13 @@ from core import checks, utils from core.changelog import Changelog -from core.models import InvalidConfigError, PermissionLevel, UnseenFormatter, getLogger +from core.models import ( + InvalidConfigError, + PermissionLevel, + SimilarCategoryConverter, + UnseenFormatter, + getLogger, +) from core.paginator import EmbedPaginatorSession, MessagePaginatorSession @@ -1698,6 +1704,119 @@ async def oauth_show(self, ctx): await ctx.send(embed=embed) + @commands.group(invoke_without_command=True) + @checks.has_permissions(PermissionLevel.OWNER) + async def autotrigger(self, ctx): + """Automatically trigger alias-like commands based on a certain keyword""" + await ctx.send_help(ctx.command) + + @autotrigger.command(name="add") + @checks.has_permissions(PermissionLevel.OWNER) + async def autotrigger_add(self, ctx, keyword, *, command): + """Adds a trigger to automatically trigger an alias-like command""" + if keyword in self.bot.auto_triggers: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description=f"Another autotrigger with the same name already exists: `{name}`.", + ) + else: + self.bot.auto_triggers[keyword] = command + await self.bot.config.update() + + embed = discord.Embed( + title="Success", + color=self.bot.main_color, + description=f"Keyword `{keyword}` has been linked to `{command}`.", + ) + + await ctx.send(embed=embed) + + @autotrigger.command(name="edit") + @checks.has_permissions(PermissionLevel.OWNER) + async def autotrigger_edit(self, ctx, keyword, *, command): + """Edits a pre-existing trigger to automatically trigger an alias-like command""" + if keyword not in self.bot.auto_triggers: + embed = utils.create_not_found_embed( + keyword, self.bot.auto_triggers.keys(), "Autotrigger" + ) + else: + self.bot.auto_triggers[keyword] = command + await self.bot.config.update() + + embed = discord.Embed( + title="Success", + color=self.bot.main_color, + description=f"Keyword `{keyword}` has been linked to `{command}`.", + ) + + await ctx.send(embed=embed) + + @autotrigger.command(name="remove") + @checks.has_permissions(PermissionLevel.OWNER) + async def autotrigger_remove(self, ctx, keyword): + """Removes a trigger to automatically trigger an alias-like command""" + try: + del self.bot.auto_triggers[keyword] + except KeyError: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description=f"Keyword `{keyword}` could not be found.", + ) + await ctx.send(embed=embed) + else: + await self.bot.config.update() + + embed = discord.Embed( + title="Success", + color=self.bot.main_color, + description=f"Keyword `{keyword}` has been removed.", + ) + await ctx.send(embed=embed) + + @autotrigger.command(name="test") + @checks.has_permissions(PermissionLevel.OWNER) + async def autotrigger_test(self, ctx, *, text): + """Tests a string against the current autotrigger setup""" + for keyword in list(self.bot.auto_triggers): + if keyword in text: + alias = self.bot.auto_triggers[keyword] + embed = discord.Embed( + title="Keyword Found", + color=self.bot.main_color, + description=f"autotrigger keyword `{keyword}` found. Command executed: `{alias}`", + ) + return await ctx.send(embed=embed) + + embed = discord.Embed( + title="Keyword Not Found", + color=self.bot.error_color, + description=f"No autotrigger keyword found. Thread will stay in {self.bot.main_category}.", + ) + return await ctx.send(embed=embed) + + @autotrigger.command(name="list") + @checks.has_permissions(PermissionLevel.OWNER) + async def autotrigger_list(self, ctx): + """Lists all autotriggers set up""" + embeds = [] + for keyword in list(self.bot.auto_triggers): + command = self.bot.auto_triggers[keyword] + embed = discord.Embed(title=keyword, color=self.bot.main_color, description=command,) + embeds.append(embed) + + if not embeds: + embeds.append( + discord.Embed( + title="No autotrigger set", + color=self.bot.error_color, + description=f"Use `{self.bot.prefix}autotrigger add` to add new autotriggers.", + ) + ) + + await EmbedPaginatorSession(ctx, *embeds).run() + @commands.command(hidden=True, name="eval") @checks.has_permissions(PermissionLevel.OWNER) async def eval_(self, ctx, *, body: str): diff --git a/core/config.py b/core/config.py index 5a9ecc7787..66bb7a69ba 100644 --- a/core/config.py +++ b/core/config.py @@ -117,6 +117,7 @@ class ConfigManager: # misc "plugins": [], "aliases": {}, + "auto_triggers": {}, } protected_keys = { diff --git a/core/models.py b/core/models.py index 30565632dd..1cd3ec4a6a 100644 --- a/core/models.py +++ b/core/models.py @@ -196,8 +196,11 @@ async def convert(self, ctx, argument): try: return await super().convert(ctx, argument) except commands.ChannelNotFound: + def check(c): - return isinstance(c, discord.CategoryChannel) and c.name.lower().startswith(argument.lower()) + return isinstance(c, discord.CategoryChannel) and c.name.lower().startswith( + argument.lower() + ) if guild: result = discord.utils.find(check, guild.categories) diff --git a/core/thread.py b/core/thread.py index 3eb6e93a52..944ab81387 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1,4 +1,5 @@ import asyncio +import copy import io import re import typing @@ -10,7 +11,8 @@ import discord from discord.ext.commands import MissingRequiredArgument, CommandError -from core.models import getLogger +from core import checks +from core.models import PermissionLevel, getLogger from core.time import human_timedelta from core.utils import ( is_image_url, @@ -101,7 +103,7 @@ def cancelled(self, flag: bool): for i in self.wait_tasks: i.cancel() - async def setup(self, *, creator=None, category=None): + async def setup(self, *, creator=None, category=None, initial_message=None): """Create the thread channel and other io related initialisation tasks""" self.bot.dispatch("thread_initiate", self) recipient = self.recipient @@ -197,7 +199,19 @@ async def send_recipient_genesis_message(): close_emoji = await self.bot.convert_emoji(close_emoji) await self.bot.add_reaction(msg, close_emoji) - await asyncio.gather(send_genesis_message(), send_recipient_genesis_message()) + async def activate_auto_triggers(): + message = copy.copy(initial_message) + if message: + for keyword in list(self.bot.auto_triggers): + if keyword in message.content: + try: + return await self.bot.trigger_auto_triggers(message, channel) + except StopIteration: + pass + + await asyncio.gather( + send_genesis_message(), send_recipient_genesis_message(), activate_auto_triggers(), + ) self.bot.dispatch("thread_ready", self) def _format_info_embed(self, user, log_url, log_count, color): @@ -880,6 +894,8 @@ async def send( if delete_message and destination == self.channel: try: await message.delete() + except discord.NotFound: + pass except Exception as e: logger.warning("Cannot delete message: %s.", e) @@ -1138,7 +1154,9 @@ async def create( del self.cache[recipient.id] return thread - self.bot.loop.create_task(thread.setup(creator=creator, category=category)) + self.bot.loop.create_task( + thread.setup(creator=creator, category=category, initial_message=message) + ) return thread async def find_or_create(self, recipient) -> Thread: From a7c23876f7099e69077a03dad1d86c32689719c0 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 19:44:06 +0800 Subject: [PATCH 0663/1221] More elegant solution to dummymessage --- CHANGELOG.md | 2 +- bot.py | 2 +- cogs/modmail.py | 5 +---- cogs/utility.py | 2 +- core/models.py | 43 +++++++++++++++++++++++++++++++++++++++++++ core/thread.py | 7 ++----- 6 files changed, 49 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dda1042e6..812d0ae0d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. -# v3.7.0-dev10 +# v3.7.0-dev11 ### Added diff --git a/bot.py b/bot.py index b68e0121aa..00e478531b 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev10" +__version__ = "3.7.0-dev11" import asyncio diff --git a/cogs/modmail.py b/cogs/modmail.py index 8c0323ae09..01bdb4d341 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -338,10 +338,7 @@ async def move(self, ctx, *, arguments): await thread.channel.send(f"{mention}, thread has been moved.") sent_emoji, _ = await self.bot.retrieve_emoji() - try: - await self.bot.add_reaction(ctx.message, sent_emoji) - except discord.NotFound: - 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) diff --git a/cogs/utility.py b/cogs/utility.py index 55f237ab3f..9216eb7f12 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1707,7 +1707,7 @@ async def oauth_show(self, ctx): @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.OWNER) async def autotrigger(self, ctx): - """Automatically trigger alias-like commands based on a certain keyword""" + """Automatically trigger alias-like commands based on a certain keyword in the user's inital message""" await ctx.send_help(ctx.command) @autotrigger.command(name="add") diff --git a/core/models.py b/core/models.py index 1cd3ec4a6a..a834b033ae 100644 --- a/core/models.py +++ b/core/models.py @@ -211,3 +211,46 @@ def check(c): raise commands.ChannelNotFound(argument) return result + + +class DummyMessage: + """ + A class mimicking the original :class:discord.Message + where all functions that require an actual message to exist + is replaced with a dummy function + """ + def __init__(self, message): + self._message = message + + def __getattr__(self, name: str): + return getattr(self._message, name) + + async def delete(self, *, delay=None): + return + + async def edit(self, **fields): + return + + async def add_reaction(self, emoji): + return + + async def remove_reaction(self, emoji): + return + + async def clear_reaction(self, emoji): + return + + async def clear_reactions(self): + return + + async def pin(self, *, reason=None): + return + + async def unpin(self, *, reason=None): + return + + async def publish(self): + return + + async def ack(self): + return diff --git a/core/thread.py b/core/thread.py index 944ab81387..eca0e19220 100644 --- a/core/thread.py +++ b/core/thread.py @@ -11,8 +11,7 @@ import discord from discord.ext.commands import MissingRequiredArgument, CommandError -from core import checks -from core.models import PermissionLevel, getLogger +from core.models import DummyMessage, getLogger from core.time import human_timedelta from core.utils import ( is_image_url, @@ -200,7 +199,7 @@ async def send_recipient_genesis_message(): await self.bot.add_reaction(msg, close_emoji) async def activate_auto_triggers(): - message = copy.copy(initial_message) + message = DummyMessage(copy.copy(initial_message)) if message: for keyword in list(self.bot.auto_triggers): if keyword in message.content: @@ -894,8 +893,6 @@ async def send( if delete_message and destination == self.channel: try: await message.delete() - except discord.NotFound: - pass except Exception as e: logger.warning("Cannot delete message: %s.", e) From 56bc2125a133dbdbc34664ef68eac92f8382c638 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 19:52:36 +0800 Subject: [PATCH 0664/1221] Fix bug with data collection --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 00e478531b..3afc3cf453 100644 --- a/bot.py +++ b/bot.py @@ -482,7 +482,7 @@ async def on_ready(self): "Failed to close thread with channel %s, skipping.", log["channel_id"] ) - if self.config["data_collection"]: + if self.config.get("data_collection"): self.metadata_loop = tasks.Loop( self.post_metadata, seconds=0, From 5c8ee8d07081777dafb1542d93b071ef3c91e1ae Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 20:03:28 +0800 Subject: [PATCH 0665/1221] Specify exact versions pipfile --- Pipfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index fb20f42c21..348c7846ad 100644 --- a/Pipfile +++ b/Pipfile @@ -5,9 +5,9 @@ verify_ssl = true [dev-packages] black = "==19.10b0" -pylint = "*" +pylint = "==2.6.0" bandit = "==1.6.2" -flake8 = "*" +flake8 = "==3.8.5" [packages] aiohttp = "==3.6.2" From 038fc61e1990cc68eba5257811d4f406b17912d7 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 21:18:55 +0800 Subject: [PATCH 0666/1221] Fix bugs with alias and autotrigger, resolve #2870 --- CHANGELOG.md | 7 ++++--- bot.py | 1 + core/models.py | 3 +++ core/utils.py | 11 ++++++++--- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 812d0ae0d0..6c3c48cd06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); -however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugins developer, note the "BREAKING" section. +however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev11 +# v3.7.0-dev12 ### Added @@ -31,7 +31,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed - `?contact` now sends members a DM. -- Fixed issue where `level_permissions` and `command_permissions` would sometimes be reset. ([GH #2856](https://github.com/kyb3r/modmail/issues/2856)) +- `level_permissions` and `command_permissions` would sometimes be reset. ([GH #2856](https://github.com/kyb3r/modmail/issues/2856)) +- Command truncated after && in alias ([GH #2870](https://github.com/kyb3r/modmail/issues/2870)) ### Improved diff --git a/bot.py b/bot.py index 3afc3cf453..901999f2e9 100644 --- a/bot.py +++ b/bot.py @@ -849,6 +849,7 @@ async def get_contexts(self, message, *, cls=commands.Context): discord.utils.find(view.skip_string, prefixes) ctx_.invoked_with = view.get_word().lower() ctx_.command = self.all_commands.get(ctx_.invoked_with) + print(ctx_.invoked_with, ctx_.args, ctx_.kwargs) ctxs += [ctx_] return ctxs diff --git a/core/models.py b/core/models.py index a834b033ae..4bd62e5d90 100644 --- a/core/models.py +++ b/core/models.py @@ -225,6 +225,9 @@ def __init__(self, message): def __getattr__(self, name: str): return getattr(self._message, name) + def __bool__(self): + return bool(self._message) + async def delete(self, *, delay=None): return diff --git a/core/utils.py b/core/utils.py index 239522139a..a236adbc0f 100644 --- a/core/utils.py +++ b/core/utils.py @@ -270,7 +270,7 @@ def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discor return embed -def parse_alias(alias): +def parse_alias(alias, *, split=True): def encode_alias(m): return "\x1AU" + base64.b64encode(m.group(1).encode()).decode() + "\x1AU" @@ -288,7 +288,12 @@ def decode_alias(m): if not alias: return aliases - for a in re.split(r"\s*&&\s*", alias): + if split: + iterate = re.split(r"\s*&&\s*", alias) + else: + iterate = [alias] + + for a in iterate: a = re.sub("\x1AU(.+?)\x1AU", decode_alias, a) if a[0] == a[-1] == '"': a = a[1:-1] @@ -299,7 +304,7 @@ def decode_alias(m): def normalize_alias(alias, message): aliases = parse_alias(alias) - contents = parse_alias(message) + contents = parse_alias(message, split=False) final_aliases = [] for a, content in zip_longest(aliases, contents): From 1b5d993ff5f8d1816264e3e12dceb68d00dc4f72 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 21:31:56 +0800 Subject: [PATCH 0667/1221] Use enums in config, resolve #2821 --- bot.py | 6 +++--- cogs/modmail.py | 20 ++++++++++---------- core/config.py | 43 ++++++++++++++++++------------------------- core/models.py | 6 ++++++ core/thread.py | 2 +- 5 files changed, 38 insertions(+), 39 deletions(-) diff --git a/bot.py b/bot.py index 901999f2e9..08782084f1 100644 --- a/bot.py +++ b/bot.py @@ -34,7 +34,7 @@ from core.clients import ApiClient, PluginDatabaseClient, MongoDBClient from core.config import ConfigManager from core.utils import human_join, match_title, normalize_alias -from core.models import PermissionLevel, SafeFormatter, getLogger, configure_logging +from core.models import DMDisabled, PermissionLevel, SafeFormatter, getLogger, configure_logging from core.thread import ThreadManager from core.time import human_timedelta @@ -770,7 +770,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: ) return - if self.config["dm_disabled"] >= 1: + if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): embed = discord.Embed( title=self.config["disabled_new_thread_title"], color=self.error_color, @@ -787,7 +787,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: thread = await self.threads.create(message.author, message=message) else: - if self.config["dm_disabled"] == 2: + if self.config["dm_disabled"] == DMDisabled.ALL_THREADS: embed = discord.Embed( title=self.config["disabled_current_thread_title"], color=self.error_color, diff --git a/cogs/modmail.py b/cogs/modmail.py index 01bdb4d341..638ae2e28d 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -14,7 +14,7 @@ from natural.date import duration from core import checks -from core.models import PermissionLevel, SimilarCategoryConverter, getLogger +from core.models import DMDisabled, PermissionLevel, SimilarCategoryConverter, getLogger from core.paginator import EmbedPaginatorSession from core.thread import Thread from core.time import UserFriendlyTime, human_timedelta @@ -964,7 +964,7 @@ async def contact( else: thread = await self.bot.threads.create(user, creator=ctx.author, category=category) - if self.bot.config["dm_disabled"] >= 1: + if self.bot.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): logger.info("Contacting user %s when Modmail DM is disabled.", user) if not silent: @@ -1450,8 +1450,8 @@ async def enable(self, ctx): color=self.bot.main_color, ) - if self.bot.config["dm_disabled"] != 0: - self.bot.config["dm_disabled"] = 0 + if self.bot.config["dm_disabled"] != DMDisabled.NONE: + self.bot.config["dm_disabled"] = DMDisabled.NONE await self.bot.config.update() return await ctx.send(embed=embed) @@ -1481,8 +1481,8 @@ async def disable_new(self, ctx): description="Modmail will not create any new threads.", color=self.bot.main_color, ) - if self.bot.config["dm_disabled"] < 1: - self.bot.config["dm_disabled"] = 1 + if self.bot.config["dm_disabled"] < DMDisabled.NEW_THREADS: + self.bot.config["dm_disabled"] = DMDisabled.NEW_THREADS await self.bot.config.update() return await ctx.send(embed=embed) @@ -1501,8 +1501,8 @@ async def disable_all(self, ctx): color=self.bot.main_color, ) - if self.bot.config["dm_disabled"] != 2: - self.bot.config["dm_disabled"] = 2 + if self.bot.config["dm_disabled"] != DMDisabled.ALL_THREADS: + self.bot.config["dm_disabled"] = DMDisabled.ALL_THREADS await self.bot.config.update() return await ctx.send(embed=embed) @@ -1514,13 +1514,13 @@ async def isenable(self, ctx): Check if the DM functionalities of Modmail is enabled. """ - if self.bot.config["dm_disabled"] == 1: + if self.bot.config["dm_disabled"] == DMDisabled.NEW_THREADS: embed = discord.Embed( title="New Threads Disabled", description="Modmail is not creating new threads.", color=self.bot.error_color, ) - elif self.bot.config["dm_disabled"] == 2: + elif self.bot.config["dm_disabled"] == DMDisabled.ALL_THREADS: embed = discord.Embed( title="All DM Disabled", description="Modmail is not accepting any DM messages for new and existing threads.", diff --git a/core/config.py b/core/config.py index 66bb7a69ba..be53807f3f 100644 --- a/core/config.py +++ b/core/config.py @@ -12,7 +12,7 @@ from discord.ext.commands import BadArgument from core._color_data import ALL_COLORS -from core.models import InvalidConfigError, Default, getLogger +from core.models import DMDisabled, InvalidConfigError, Default, getLogger from core.time import UserFriendlyTimeSync from core.utils import strtobool @@ -98,9 +98,7 @@ class ConfigManager: "activity_message": "", "activity_type": None, "status": None, - # dm_disabled 0 = none, 1 = new threads, 2 = all threads - # TODO: use enum - "dm_disabled": 0, + "dm_disabled": DMDisabled.NONE, "oauth_whitelist": [], # moderation "blocked": {}, @@ -165,7 +163,11 @@ class ConfigManager: "enable_eval", } - special_types = {"status", "activity_type"} + enums = { + "dm_disabled": DMDisabled, + "status": discord.Status, + "activity_type": discord.ActivityType + } defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) @@ -277,26 +279,15 @@ def get(self, key: str, convert=True) -> typing.Any: value = strtobool(value) except ValueError: value = self.remove(key) - - elif key in self.special_types: + + elif key in self.enums: if value is None: return None - - if key == "status": - try: - # noinspection PyArgumentList - value = discord.Status(value) - except ValueError: - logger.warning("Invalid status %s.", value) - value = self.remove(key) - - elif key == "activity_type": - try: - # noinspection PyArgumentList - value = discord.ActivityType(value) - except ValueError: - logger.warning("Invalid activity %s.", value) - value = self.remove(key) + try: + value = self.enums[key](value) + except ValueError: + logger.warning("Invalid %s %s.", key, value) + value = self.remove(key) return value @@ -355,8 +346,10 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: except ValueError: raise InvalidConfigError("Must be a yes/no value.") - # elif key in self.special_types: - # if key == "status": + elif key in self.enums: + if isinstance(item, self.enums[key]): + # value is an enum type + item = item.value return self.__setitem__(key, item) diff --git a/core/models.py b/core/models.py index 4bd62e5d90..8d1be4130a 100644 --- a/core/models.py +++ b/core/models.py @@ -257,3 +257,9 @@ async def publish(self): async def ack(self): return + + +class DMDisabled(IntEnum): + NONE = 0 + NEW_THREADS = 1 + ALL_THREADS = 2 diff --git a/core/thread.py b/core/thread.py index eca0e19220..4beacf31bc 100644 --- a/core/thread.py +++ b/core/thread.py @@ -896,7 +896,7 @@ async def send( except Exception as e: logger.warning("Cannot delete message: %s.", e) - if from_mod and self.bot.config["dm_disabled"] == 2 and destination != self.channel: + if from_mod and self.bot.config["dm_disabled"] == DMDisabled.ALL_THREADS and destination != self.channel: logger.info("Sending a message to %s when DM disabled is set.", self.recipient) try: From 3428eb3f681e09b4f5d40201e2a98fecbde21339 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 21:46:52 +0800 Subject: [PATCH 0668/1221] use regex autotrigger config --- CHANGELOG.md | 11 +++++++---- bot.py | 12 +++++++++--- core/config.py | 3 +++ core/config_help.json | 12 ++++++++++++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3c48cd06..9123ab168f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,18 +26,21 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Support stickers and reject non-messages. (i.e. pin_add) - Added support for thread titles, `?title`. ([GH #2838](https://github.com/kyb3r/modmail/issues/2838)) - Added `data_collection` to specify if bot metadata should be collected by Modmail developers. -- Added `?autotrigger` to specify keywords to trigger commands. ([GH #130](https://github.com/kyb3r/modmail/issues/130), [GH #649](https://github.com/kyb3r/modmail/issues/649)) +- Added `?autotrigger`, `use_regex_autotrigger` config to specify keywords to trigger commands. ([GH #130](https://github.com/kyb3r/modmail/issues/130), [GH #649](https://github.com/kyb3r/modmail/issues/649)) ### Fixed - `?contact` now sends members a DM. - `level_permissions` and `command_permissions` would sometimes be reset. ([GH #2856](https://github.com/kyb3r/modmail/issues/2856)) -- Command truncated after && in alias ([GH #2870](https://github.com/kyb3r/modmail/issues/2870)) +- Command truncated after && in alias. ([GH #2870](https://github.com/kyb3r/modmail/issues/2870)) ### Improved -- Plugins installations have clearer error messages -- `?move` now does not require exact category names, accepts case-insensitive and startswith names +- Plugins installations have clearer error messages. +- `?move` now does not require exact category names, accepts case-insensitive and startswith names. + +### Internal +- Use enums in config. ([GH #2821](https://github.com/kyb3r/modmail/issues/2821)) # v3.6.2 diff --git a/bot.py b/bot.py index 08782084f1..206f4259d7 100644 --- a/bot.py +++ b/bot.py @@ -870,9 +870,15 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) invoker = view.get_word().lower() # Check if there is any aliases being called. - alias = self.auto_triggers[ - next(filter(lambda x: x in message.content, self.auto_triggers.keys())) - ] + if self.config.get("use_regex_autotrigger"): + alias = self.auto_triggers[ + next(filter(lambda x: re.match(x, message.content), self.auto_triggers.keys())) + ] + else: + alias = self.auto_triggers[ + next(filter(lambda x: x.lower() in message.content.lower(), self.auto_triggers.keys())) + ] + if alias is None: ctx.thread = thread ctx.invoked_with = invoker diff --git a/core/config.py b/core/config.py index be53807f3f..102f3cb102 100644 --- a/core/config.py +++ b/core/config.py @@ -91,6 +91,8 @@ class ConfigManager: "confirm_thread_response": "React to confirm thread creation which will directly contact the moderators", "confirm_thread_creation_accept": "\u2705", "confirm_thread_creation_deny": "\U0001F6AB", + # regex + "use_regex_autotrigger": False } private_keys = { @@ -158,6 +160,7 @@ class ConfigManager: "close_on_leave", "alert_on_mention", "confirm_thread_creation", + "use_regex_autotrigger" "enable_plugins", "data_collection", "enable_eval", diff --git a/core/config_help.json b/core/config_help.json index a0584e36fa..970ebc6229 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -678,6 +678,18 @@ "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, confirm_thread_creation_accept`" ] }, + "use_regex_autotrigger": { + "default": "No", + "description": "Whether to use regex to compare in autotriggers.", + "examples":[ + "`{prefix}config set use_regex_autotrigger yes`" + ], + "notes": [ + "This is meant for advanced user that understand regular expressions.", + "You can test it out with https://regexr.com on `PCRE (Server)` mode", + "See command: `autotrigger`" + ] + }, "modmail_guild_id": { "default": "Fallback on `GUILD_ID`", "description": "The ID of the discord server where the threads channels should be created (receiving server).", From 71c27a2cec7f90a70fbe1b57922ca0df4890a20a Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 21:56:24 +0800 Subject: [PATCH 0669/1221] bugfixing --- bot.py | 6 +++++- cogs/utility.py | 10 ++++++++-- core/config.py | 9 ++++----- core/models.py | 1 + core/thread.py | 18 ++++++++++-------- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/bot.py b/bot.py index 206f4259d7..b85a61856b 100644 --- a/bot.py +++ b/bot.py @@ -876,7 +876,11 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) ] else: alias = self.auto_triggers[ - next(filter(lambda x: x.lower() in message.content.lower(), self.auto_triggers.keys())) + next( + filter( + lambda x: x.lower() in message.content.lower(), self.auto_triggers.keys() + ) + ) ] if alias is None: diff --git a/cogs/utility.py b/cogs/utility.py index 9216eb7f12..950baaaf14 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -2,6 +2,7 @@ import inspect import os import random +import re import traceback from contextlib import redirect_stdout from datetime import datetime @@ -1718,7 +1719,7 @@ async def autotrigger_add(self, ctx, keyword, *, command): embed = discord.Embed( title="Error", color=self.bot.error_color, - description=f"Another autotrigger with the same name already exists: `{name}`.", + description=f"Another autotrigger with the same name already exists: `{keyword}`.", ) else: self.bot.auto_triggers[keyword] = command @@ -1780,7 +1781,12 @@ async def autotrigger_remove(self, ctx, keyword): async def autotrigger_test(self, ctx, *, text): """Tests a string against the current autotrigger setup""" for keyword in list(self.bot.auto_triggers): - if keyword in text: + if self.bot.config.get("use_regex_autotrigger"): + check = re.match(keyword, text) + else: + check = keyword in text + + if check: alias = self.bot.auto_triggers[keyword] embed = discord.Embed( title="Keyword Found", diff --git a/core/config.py b/core/config.py index 102f3cb102..85d0797488 100644 --- a/core/config.py +++ b/core/config.py @@ -92,7 +92,7 @@ class ConfigManager: "confirm_thread_creation_accept": "\u2705", "confirm_thread_creation_deny": "\U0001F6AB", # regex - "use_regex_autotrigger": False + "use_regex_autotrigger": False, } private_keys = { @@ -160,8 +160,7 @@ class ConfigManager: "close_on_leave", "alert_on_mention", "confirm_thread_creation", - "use_regex_autotrigger" - "enable_plugins", + "use_regex_autotrigger" "enable_plugins", "data_collection", "enable_eval", } @@ -169,7 +168,7 @@ class ConfigManager: enums = { "dm_disabled": DMDisabled, "status": discord.Status, - "activity_type": discord.ActivityType + "activity_type": discord.ActivityType, } defaults = {**public_keys, **private_keys, **protected_keys} @@ -282,7 +281,7 @@ def get(self, key: str, convert=True) -> typing.Any: value = strtobool(value) except ValueError: value = self.remove(key) - + elif key in self.enums: if value is None: return None diff --git a/core/models.py b/core/models.py index 8d1be4130a..0ad40bad1f 100644 --- a/core/models.py +++ b/core/models.py @@ -219,6 +219,7 @@ class DummyMessage: where all functions that require an actual message to exist is replaced with a dummy function """ + def __init__(self, message): self._message = message diff --git a/core/thread.py b/core/thread.py index 4beacf31bc..e16c73934e 100644 --- a/core/thread.py +++ b/core/thread.py @@ -11,7 +11,7 @@ import discord from discord.ext.commands import MissingRequiredArgument, CommandError -from core.models import DummyMessage, getLogger +from core.models import DMDisabled, DummyMessage, getLogger from core.time import human_timedelta from core.utils import ( is_image_url, @@ -201,12 +201,10 @@ async def send_recipient_genesis_message(): async def activate_auto_triggers(): message = DummyMessage(copy.copy(initial_message)) if message: - for keyword in list(self.bot.auto_triggers): - if keyword in message.content: - try: - return await self.bot.trigger_auto_triggers(message, channel) - except StopIteration: - pass + try: + return await self.bot.trigger_auto_triggers(message, channel) + except RuntimeError: + pass await asyncio.gather( send_genesis_message(), send_recipient_genesis_message(), activate_auto_triggers(), @@ -896,7 +894,11 @@ async def send( except Exception as e: logger.warning("Cannot delete message: %s.", e) - if from_mod and self.bot.config["dm_disabled"] == DMDisabled.ALL_THREADS and destination != self.channel: + if ( + from_mod + and self.bot.config["dm_disabled"] == DMDisabled.ALL_THREADS + and destination != self.channel + ): logger.info("Sending a message to %s when DM disabled is set.", self.recipient) try: From 579dbd41c900fc64aeed43525cc88c53b294de8e Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 22:28:07 +0800 Subject: [PATCH 0670/1221] Fix requirements again --- Pipfile | 4 ++-- Pipfile.lock | 16 ++++++++-------- discord.py-1.5.1.tar.gz | Bin 0 -> 649410 bytes discord.py-1.5.2.tar.gz | Bin 649387 -> 0 bytes pyproject.toml | 2 +- requirements.min.txt | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 discord.py-1.5.1.tar.gz delete mode 100644 discord.py-1.5.2.tar.gz diff --git a/Pipfile b/Pipfile index 348c7846ad..e266f4d102 100644 --- a/Pipfile +++ b/Pipfile @@ -7,14 +7,14 @@ verify_ssl = true black = "==19.10b0" pylint = "==2.6.0" bandit = "==1.6.2" -flake8 = "==3.8.5" +flake8 = "==3.8.4" [packages] aiohttp = "==3.6.2" async-timeout = "==3.0.1" attrs = "==19.3.0" chardet = "==3.0.4" -"discord.py" = {path = "discord.py-1.5.2.tar.gz"} +"discord.py" = {path = "discord.py-1.5.1.tar.gz"} dnspython = "==1.16.0" emoji = "==0.5.4" future = "==0.18.2" diff --git a/Pipfile.lock b/Pipfile.lock index d88503c8d8..92c9665c26 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2be2574267d4b75a621fe0a7202531936a02f5bc0cd0a79addf757344a3acb4d" + "sha256": "085d6c018d43b9b4c4b5bc5e1ff99543b00c735dfe744abbc5a813048b2e1734" }, "pipfile-spec": 6, "requires": {}, @@ -58,13 +58,13 @@ }, "discord-py": { "hashes": [ - "sha256:5aac5a89eb8d70ba7bb5721ad9b495b437cc46328bab6a9170a4e0f8a568a9e8" + "sha256:f52ab61650d5fe2726fb645e4f23eecdfcf3ded74645f3978a010259e1bc28ca" ], - "path": "./discord.py-1.5.2.tar.gz", - "version": "==1.5.2" + "path": "./discord.py-1.5.1.tar.gz", + "version": "==1.5.1" }, "discord.py": { - "path": "discord.py-1.5.2.tar.gz" + "path": "discord.py-1.5.1.tar.gz" }, "dnspython": { "hashes": [ @@ -413,10 +413,10 @@ }, "pathspec": { "hashes": [ - "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", - "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", + "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" ], - "version": "==0.8.0" + "version": "==0.8.1" }, "pbr": { "hashes": [ diff --git a/discord.py-1.5.1.tar.gz b/discord.py-1.5.1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..31eab92a92e5c718020fdba80f5a9ac7cf7016bd GIT binary patch literal 649410 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 z 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 z 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

    bI~J_^wPCSsSR<~v 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

    N=8dWI3u3r`*&K&0Pn$*=QumXnZ9ycN!`x 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 J=(qsj9{}75sI341 literal 0 HcmV?d00001 diff --git a/discord.py-1.5.2.tar.gz b/discord.py-1.5.2.tar.gz deleted file mode 100644 index 79619c9a8e36cfde75e22f715d15cf4a4bdcbdc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 649387 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

      9I&RraX-kD2 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

      -O5Ah2?Whk$Eqb;`t%vP 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

      Sz2z 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

      2z7U%M!UkoWC&SG?D1= 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

      luAa|*bTAU63X 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

      W5Nq=aQxt0yqxKV@=NwMvYtGa8hLK}uD&=T_GE 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

      )A^}}-s0o=K~Vg}@yok9 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 zǓ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 Znkm>8)2%tce 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 diff --git a/pyproject.toml b/pyproject.toml index e7c56eb749..e618107561 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ keywords = ['discord', 'modmail'] [tool.poetry.dependencies] python = "^3.7" -"discord.py" = "./discord.py-1.5.2.tar.gz" +"discord.py" = "./discord.py-1.5.1.tar.gz" uvloop = {version = ">=0.12.0", markers = "sys_platform != 'win32'"} python-dotenv = ">=0.10.3" parsedatetime = "^2.6" diff --git a/requirements.min.txt b/requirements.min.txt index cf1b4bdd26..6d434f974d 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -6,7 +6,7 @@ aiohttp==3.6.2 async-timeout==3.0.1 attrs==19.3.0 chardet==3.0.4 -./discord.py-1.5.2.tar.gz +./discord.py-1.5.1.tar.gz dnspython==1.16.0 emoji==0.5.4 future==0.18.2 From 22689a7348676c79327d4e56da7ce5760923959b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 22:55:04 +0800 Subject: [PATCH 0671/1221] Test deps --- Pipfile | 3 ++- Pipfile.lock | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Pipfile b/Pipfile index e266f4d102..1d1bf9c936 100644 --- a/Pipfile +++ b/Pipfile @@ -14,7 +14,6 @@ aiohttp = "==3.6.2" async-timeout = "==3.0.1" attrs = "==19.3.0" chardet = "==3.0.4" -"discord.py" = {path = "discord.py-1.5.1.tar.gz"} dnspython = "==1.16.0" emoji = "==0.5.4" future = "==0.18.2" @@ -30,6 +29,8 @@ python-dotenv = "==0.14.0" six = "==1.15.0" websockets = "==8.1" yarl = "==1.4.2" +uvloop = {version = ">=0.12.0",sys_platform = "!= 'win32'"} +"discord.py" = {file = "https://github.com/kyb3r/modmail/raw/development/discord.py-1.5.1.tar.gz"} [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 92c9665c26..30dcc051aa 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "085d6c018d43b9b4c4b5bc5e1ff99543b00c735dfe744abbc5a813048b2e1734" + "sha256": "c910ca82e90b6af36846c730e166d2d7a4b82ca96271a6863c482ac6053c32cc" }, "pipfile-spec": 6, "requires": {}, @@ -57,14 +57,14 @@ "version": "==3.0.4" }, "discord-py": { + "file": "https://github.com/kyb3r/modmail/raw/development/discord.py-1.5.1.tar.gz", "hashes": [ "sha256:f52ab61650d5fe2726fb645e4f23eecdfcf3ded74645f3978a010259e1bc28ca" ], - "path": "./discord.py-1.5.1.tar.gz", "version": "==1.5.1" }, "discord.py": { - "path": "discord.py-1.5.1.tar.gz" + "file": "https://github.com/kyb3r/modmail/raw/development/discord.py-1.5.1.tar.gz" }, "dnspython": { "hashes": [ @@ -237,6 +237,21 @@ "index": "pypi", "version": "==1.15.0" }, + "uvloop": { + "hashes": [ + "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", + "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", + "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", + "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", + "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", + "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", + "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", + "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", + "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" + ], + "markers": "sys_platform != 'win32'", + "version": "==0.14.0" + }, "websockets": { "hashes": [ "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", From 86f51b277ed3e72aaf8e7819d5ad51bce9b6af5b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 23:01:20 +0800 Subject: [PATCH 0672/1221] Test deps --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 1d1bf9c936..b19980db70 100644 --- a/Pipfile +++ b/Pipfile @@ -30,7 +30,7 @@ six = "==1.15.0" websockets = "==8.1" yarl = "==1.4.2" uvloop = {version = ">=0.12.0",sys_platform = "!= 'win32'"} -"discord.py" = {file = "https://github.com/kyb3r/modmail/raw/development/discord.py-1.5.1.tar.gz"} +"discord.py" = {file = "https://raw.githubusercontent.com/kyb3r/modmail/development/discord.py-1.5.1.tar.gz"} [scripts] bot = "python bot.py" From 84924a663f42a9bfd313547845092af24051b5f1 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 23:08:32 +0800 Subject: [PATCH 0673/1221] TEST DEPS --- Pipfile | 37 +++--- Pipfile.lock | 327 ++++++++++++++++++++++++++++----------------------- 2 files changed, 194 insertions(+), 170 deletions(-) diff --git a/Pipfile b/Pipfile index b19980db70..4ee505ee4c 100644 --- a/Pipfile +++ b/Pipfile @@ -3,33 +3,30 @@ name = "pypi" url = "https://pypi.org/simple" verify_ssl = true +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + [dev-packages] black = "==19.10b0" -pylint = "==2.6.0" +pylint = "*" bandit = "==1.6.2" -flake8 = "==3.8.4" +flake8 = "*" [packages] -aiohttp = "==3.6.2" -async-timeout = "==3.0.1" -attrs = "==19.3.0" -chardet = "==3.0.4" -dnspython = "==1.16.0" -emoji = "==0.5.4" -future = "==0.18.2" -idna = "==2.9" -isodate = "==0.6.0" -motor = "==2.1.0" -multidict = "==4.7.6" +colorama = ">=0.4.0" +python-dateutil = ">=2.7.0" +emoji = ">=0.2" +uvloop = {version = ">=0.12.0",sys_platform = "!= 'win32'"} +motor = ">=2.0.0" natural = "==0.2.0" +isodate = ">=0.6.0" +dnspython = "~=1.16.0" parsedatetime = "==2.6" -pymongo = "==3.10.1" -python-dateutil = "==2.8.1" -python-dotenv = "==0.14.0" -six = "==1.15.0" -websockets = "==8.1" -yarl = "==1.4.2" -uvloop = {version = ">=0.12.0",sys_platform = "!= 'win32'"} +aiohttp = ">=3.6.0,<3.7.0" +python-dotenv = ">=0.10.3" +pipenv = "*" "discord.py" = {file = "https://raw.githubusercontent.com/kyb3r/modmail/development/discord.py-1.5.1.tar.gz"} [scripts] diff --git a/Pipfile.lock b/Pipfile.lock index 30dcc051aa..214fcdf45a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,16 @@ { "_meta": { "hash": { - "sha256": "c910ca82e90b6af36846c730e166d2d7a4b82ca96271a6863c482ac6053c32cc" + "sha256": "2dd04df5c296e35b94cc2f364e3ca331d06be49a704240dfa8fdd6f99e33b3bb" }, "pipfile-spec": 6, "requires": {}, "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + }, { "name": "pypi", "url": "https://pypi.org/simple", @@ -16,55 +21,84 @@ "default": { "aiohttp": { "hashes": [ - "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", - "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", - "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", - "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", - "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", - "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", - "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", - "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", - "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", - "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", - "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", - "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965" + "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe", + "sha256:206c0ccfcea46e1bddc91162449c20c72f308aebdcef4977420ef329c8fcc599", + "sha256:2ad493de47a8f926386fa6d256832de3095ba285f325db917c7deae0b54a9fc8", + "sha256:319b490a5e2beaf06891f6711856ea10591cfe84fe9f3e71a721aa8f20a0872a", + "sha256:470e4c90da36b601676fe50c49a60d34eb8c6593780930b1aa4eea6f508dfa37", + "sha256:60f4caa3b7f7a477f66ccdd158e06901e1d235d572283906276e3803f6b098f5", + "sha256:66d64486172b032db19ea8522328b19cfb78a3e1e5b62ab6a0567f93f073dea0", + "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c", + "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645", + "sha256:797456399ffeef73172945708810f3277f794965eb6ec9bd3a0c007c0476be98", + "sha256:a885432d3cabc1287bcf88ea94e1826d3aec57fd5da4a586afae4591b061d40d", + "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81", + "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5" ], "index": "pypi", - "version": "==3.6.2" + "version": "==3.6.3" + }, + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" }, "async-timeout": { "hashes": [ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "index": "pypi", + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "index": "pypi", - "version": "==19.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" + }, + "certifi": { + "hashes": [ + "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd", + "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4" + ], + "version": "==2020.11.8" }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" ], - "index": "pypi", "version": "==3.0.4" }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "index": "pypi", + "version": "==0.4.4" + }, "discord-py": { - "file": "https://github.com/kyb3r/modmail/raw/development/discord.py-1.5.1.tar.gz", + "file": "https://raw.githubusercontent.com/kyb3r/modmail/development/discord.py-1.5.1.tar.gz", "hashes": [ "sha256:f52ab61650d5fe2726fb645e4f23eecdfcf3ded74645f3978a010259e1bc28ca" ], "version": "==1.5.1" }, "discord.py": { - "file": "https://github.com/kyb3r/modmail/raw/development/discord.py-1.5.1.tar.gz" + "file": "https://raw.githubusercontent.com/kyb3r/modmail/development/discord.py-1.5.1.tar.gz" + }, + "distlib": { + "hashes": [ + "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", + "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" + ], + "version": "==0.3.1" }, "dnspython": { "hashes": [ @@ -76,25 +110,25 @@ }, "emoji": { "hashes": [ - "sha256:60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174" + "sha256:e42da4f8d648f8ef10691bc246f682a1ec6b18373abfd9be10ec0b398823bd11" ], "index": "pypi", - "version": "==0.5.4" + "version": "==0.6.0" }, - "future": { + "filelock": { "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" ], - "index": "pypi", - "version": "==0.18.2" + "version": "==3.0.12" }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "index": "pypi", - "version": "==2.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "isodate": { "hashes": [ @@ -106,12 +140,11 @@ }, "motor": { "hashes": [ - "sha256:599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909", - "sha256:756c587985d166166e644ccd36fb8b586fb987eb42fc0fc60cce9a3d76d809b4", - "sha256:97b4fc0a00a84df30f866d18693c503eef46c7642f75218a2c44d74d835be38a" + "sha256:428d94750123d19fcd0a89b8671ff9b4656f205217bad9f44161748c64c5fc80", + "sha256:f1692b760d834707e3477996ce8d407af8cd61c1a2abedbf81c22ef14675e61a" ], "index": "pypi", - "version": "==2.1.0" + "version": "==2.3.0" }, "multidict": { "hashes": [ @@ -133,7 +166,7 @@ "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], - "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==4.7.6" }, "natural": { @@ -151,67 +184,73 @@ "index": "pypi", "version": "==2.6" }, - "pymongo": { + "pipenv": { "hashes": [ - "sha256:01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", - "sha256:0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", - "sha256:1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", - "sha256:18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", - "sha256:19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", - "sha256:20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103", - "sha256:26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", - "sha256:26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", - "sha256:2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", - "sha256:2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", - "sha256:316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", - "sha256:31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", - "sha256:334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", - "sha256:358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", - "sha256:3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", - "sha256:444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", - "sha256:47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", - "sha256:4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", - "sha256:4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", - "sha256:53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", - "sha256:568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", - "sha256:56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", - "sha256:5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", - "sha256:61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", - "sha256:619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", - "sha256:6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", - "sha256:63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113", - "sha256:6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", - "sha256:7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", - "sha256:7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae", - "sha256:80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", - "sha256:95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", - "sha256:993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", - "sha256:9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", - "sha256:a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", - "sha256:a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", - "sha256:a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", - "sha256:a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", - "sha256:a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", - "sha256:ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012", - "sha256:ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", - "sha256:b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", - "sha256:b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", - "sha256:bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", - "sha256:bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a", - "sha256:c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", - "sha256:c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", - "sha256:c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", - "sha256:c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", - "sha256:da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", - "sha256:dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", - "sha256:e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", - "sha256:e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", - "sha256:e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", - "sha256:f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a", - "sha256:f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606" + "sha256:d6ac39d1721517b23aca12cdb4c726dc318ec4d7bdede5c1220bbb81775005c3", + "sha256:dce1fb1a6941f98764c62b00010f52143aed19e2fcd8f100aff4fb3bb1bbbbe3" ], "index": "pypi", - "version": "==3.10.1" + "version": "==2020.11.4" + }, + "pymongo": { + "hashes": [ + "sha256:03dc64a9aa7a5d405aea5c56db95835f6a2fa31b3502c5af1760e0e99210be30", + "sha256:05fcc6f9c60e6efe5219fbb5a30258adb3d3e5cbd317068f3d73c09727f2abb6", + "sha256:076a7f2f7c251635cf6116ac8e45eefac77758ee5a77ab7bd2f63999e957613b", + "sha256:137e6fa718c7eff270dbd2fc4b90d94b1a69c9e9eb3f3de9e850a7fd33c822dc", + "sha256:1f865b1d1c191d785106f54df9abdc7d2f45a946b45fd1ea0a641b4f982a2a77", + "sha256:213c445fe7e654621c6309e874627c35354b46ef3ee807f5a1927dc4b30e1a67", + "sha256:25e617daf47d8dfd4e152c880cd0741cbdb48e51f54b8de9ddbfe74ecd87dd16", + "sha256:3d9bb1ba935a90ec4809a8031efd988bdb13cdba05d9e9a3e9bf151bf759ecde", + "sha256:40696a9a53faa7d85aaa6fd7bef1cae08f7882640bad08c350fb59dee7ad069b", + "sha256:421aa1b92c291c429668bd8d8d8ec2bd00f183483a756928e3afbf2b6f941f00", + "sha256:4437300eb3a5e9cc1a73b07d22c77302f872f339caca97e9bf8cf45eca8fa0d2", + "sha256:455f4deb00158d5ec8b1d3092df6abb681b225774ab8a59b3510293b4c8530e3", + "sha256:475a34a0745c456ceffaec4ce86b7e0983478f1b6140890dff7b161e7bcd895b", + "sha256:4797c0080f41eba90404335e5ded3aa66731d303293a675ff097ce4ea3025bb9", + "sha256:4ae23fbbe9eadf61279a26eba866bbf161a6f7e2ffad14a42cf20e9cb8e94166", + "sha256:4b32744901ee9990aa8cd488ec85634f443526def1e5190a407dc107148249d7", + "sha256:50127b13b38e8e586d5e97d342689405edbd74ad0bd891d97ee126a8c7b6e45f", + "sha256:50531caa7b4be1c4ed5e2d5793a4e51cc9bd62a919a6fd3299ef7c902e206eab", + "sha256:63a5387e496a98170ffe638b435c0832c0f2011a6f4ff7a2880f17669fff8c03", + "sha256:68220b81850de8e966d4667d5c325a96c6ac0d6adb3d18935d6e3d325d441f48", + "sha256:689142dc0c150e9cb7c012d84cac2c346d40beb891323afb6caf18ec4caafae0", + "sha256:6a15e2bee5c4188369a87ed6f02de804651152634a46cca91966a11c8abd2550", + "sha256:7122ffe597b531fb065d3314e704a6fe152b81820ca5f38543e70ffcc95ecfd4", + "sha256:7307024b18266b302f4265da84bb1effb5d18999ef35b30d17592959568d5c0a", + "sha256:7a4a6f5b818988a3917ec4baa91d1143242bdfece8d38305020463955961266a", + "sha256:83c5a3ecd96a9f3f11cfe6dfcbcec7323265340eb24cc996acaecea129865a3a", + "sha256:890b0f1e18dbd898aeb0ab9eae1ab159c6bcbe87f0abb065b0044581d8614062", + "sha256:8deda1f7b4c03242f2a8037706d9584e703f3d8c74d6d9cac5833db36fe16c42", + "sha256:8ea13d0348b4c96b437d944d7068d59ed4a6c98aaa6c40d8537a2981313f1c66", + "sha256:91e96bf85b7c07c827d339a386e8a3cf2e90ef098c42595227f729922d0851df", + "sha256:96782ebb3c9e91e174c333208b272ea144ed2a684413afb1038e3b3342230d72", + "sha256:9755c726aa6788f076114dfdc03b92b03ff8860316cca00902cce88bcdb5fedd", + "sha256:9dbab90c348c512e03f146e93a5e2610acec76df391043ecd46b6b775d5397e6", + "sha256:9ee0eef254e340cc11c379f797af3977992a7f2c176f1a658740c94bf677e13c", + "sha256:9fc17fdac8f1973850d42e51e8ba6149d93b1993ed6768a24f352f926dd3d587", + "sha256:a2787319dc69854acdfd6452e6a8ba8f929aeb20843c7f090e04159fc18e6245", + "sha256:b7c522292407fa04d8195032493aac937e253ad9ae524aab43b9d9d242571f03", + "sha256:bd312794f51e37dcf77f013d40650fe4fbb211dd55ef2863839c37480bd44369", + "sha256:c0d660a186e36c526366edf8a64391874fe53cf8b7039224137aee0163c046df", + "sha256:c4869141e20769b65d2d72686e7a7eb141ce9f3168106bed3e7dcced54eb2422", + "sha256:cc4057f692ac35bbe82a0a908d42ce3a281c9e913290fac37d7fa3bd01307dfb", + "sha256:cccf1e7806f12300e3a3b48f219e111000c2538483e85c869c35c1ae591e6ce9", + "sha256:ce208f80f398522e49d9db789065c8ad2cd37b21bd6b23d30053474b7416af11", + "sha256:d0565481dc196986c484a7fb13214fc6402201f7fb55c65fd215b3324962fe6c", + "sha256:d1b3366329c45a474b3bbc9b9c95d4c686e03f35da7fd12bc144626d1f2a7c04", + "sha256:d226e0d4b9192d95079a9a29c04dd81816b1ce8903b8c174a39224fe978547cb", + "sha256:d38b35f6eef4237b1d0d8e845fc1546dad85c55eba447e28c211da8c7ef9697c", + "sha256:d64c98277ea80e4484f1332ab107e8dfd173a7dcf1bdbf10a9cccc97aaab145f", + "sha256:d9de8427a5601799784eb0e7fa1b031aa64086ce04de29df775a8ca37eedac41", + "sha256:e6a15cf8f887d9f578dd49c6fb3a99d53e1d922fdd67a245a67488d77bf56eb2", + "sha256:e8c446882cbb3774cd78c738c9f58220606b702b7c1655f1423357dc51674054", + "sha256:e8d188ee39bd0ffe76603da887706e4e7b471f613625899ddf1e27867dc6a0d3", + "sha256:ef76535776c0708a85258f6dc51d36a2df12633c735f6d197ed7dfcaa7449b99", + "sha256:f6efca006a81e1197b925a7d7b16b8f61980697bb6746587aad8842865233218" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==3.11.0" }, "python-dateutil": { "hashes": [ @@ -223,18 +262,18 @@ }, "python-dotenv": { "hashes": [ - "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", - "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" + "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e", + "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0" ], "index": "pypi", - "version": "==0.14.0" + "version": "==0.15.0" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.15.0" }, "uvloop": { @@ -252,56 +291,44 @@ "markers": "sys_platform != 'win32'", "version": "==0.14.0" }, - "websockets": { - "hashes": [ - "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", - "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", - "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", - "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", - "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", - "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", - "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", - "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", - "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", - "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", - "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", - "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", - "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", - "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", - "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", - "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", - "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", - "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", - "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", - "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", - "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", - "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + "virtualenv": { + "hashes": [ + "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", + "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" ], - "index": "pypi", - "version": "==8.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.1.0" + }, + "virtualenv-clone": { + "hashes": [ + "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", + "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.5.4" }, "yarl": { "hashes": [ - "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" + "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", + "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", + "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", + "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", + "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", + "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", + "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", + "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", + "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", + "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", + "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", + "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", + "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", + "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", + "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", + "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", + "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" ], - "index": "pypi", - "version": "==1.4.2" + "markers": "python_version >= '3.5'", + "version": "==1.5.1" } }, "develop": { @@ -322,11 +349,11 @@ }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "index": "pypi", - "version": "==19.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" }, "bandit": { "hashes": [ @@ -357,7 +384,7 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "markers": "platform_system == 'Windows' and sys_platform == 'win32'", + "index": "pypi", "version": "==0.4.4" }, "flake8": { @@ -534,7 +561,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.15.0" }, "smmap": { From e17c2c9bc15539d49c185280a70c156348afa749 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 23:11:29 +0800 Subject: [PATCH 0674/1221] TEST DEPS --- requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..de59d776a5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pipenv==2020.11.4 \ No newline at end of file From 2dfe72a3463f579ebed3bca8e477e4a5592242ed Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 23:18:18 +0800 Subject: [PATCH 0675/1221] TEST DEPS --- Pipfile | 2 +- Pipfile.lock | 35 +++-------------------------------- requirements.txt | 1 - 3 files changed, 4 insertions(+), 34 deletions(-) delete mode 100644 requirements.txt diff --git a/Pipfile b/Pipfile index 4ee505ee4c..755f74716b 100644 --- a/Pipfile +++ b/Pipfile @@ -27,7 +27,7 @@ parsedatetime = "==2.6" aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = {file = "https://raw.githubusercontent.com/kyb3r/modmail/development/discord.py-1.5.1.tar.gz"} +"discord.py" = {file = "./discord.py-1.5.1.tar.gz"} [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 214fcdf45a..9942ffab90 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2dd04df5c296e35b94cc2f364e3ca331d06be49a704240dfa8fdd6f99e33b3bb" + "sha256": "878a40a1f33596a78d6ddccfec3c116687a6dd2a98ba77d7f63d2a86d07d2718" }, "pipfile-spec": 6, "requires": {}, @@ -50,7 +50,6 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -58,7 +57,6 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "certifi": { @@ -83,15 +81,8 @@ "index": "pypi", "version": "==0.4.4" }, - "discord-py": { - "file": "https://raw.githubusercontent.com/kyb3r/modmail/development/discord.py-1.5.1.tar.gz", - "hashes": [ - "sha256:f52ab61650d5fe2726fb645e4f23eecdfcf3ded74645f3978a010259e1bc28ca" - ], - "version": "==1.5.1" - }, "discord.py": { - "file": "https://raw.githubusercontent.com/kyb3r/modmail/development/discord.py-1.5.1.tar.gz" + "file": "./discord.py-1.5.1.tar.gz" }, "distlib": { "hashes": [ @@ -127,7 +118,6 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "isodate": { @@ -166,7 +156,6 @@ "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], - "markers": "python_version >= '3.5'", "version": "==4.7.6" }, "natural": { @@ -249,7 +238,6 @@ "sha256:ef76535776c0708a85258f6dc51d36a2df12633c735f6d197ed7dfcaa7449b99", "sha256:f6efca006a81e1197b925a7d7b16b8f61980697bb6746587aad8842865233218" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.11.0" }, "python-dateutil": { @@ -273,7 +261,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.15.0" }, "uvloop": { @@ -288,6 +275,7 @@ "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" ], + "index": "pypi", "markers": "sys_platform != 'win32'", "version": "==0.14.0" }, @@ -296,7 +284,6 @@ "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.1.0" }, "virtualenv-clone": { @@ -304,7 +291,6 @@ "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.5.4" }, "yarl": { @@ -327,7 +313,6 @@ "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" ], - "markers": "python_version >= '3.5'", "version": "==1.5.1" } }, @@ -344,7 +329,6 @@ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], - "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "attrs": { @@ -352,7 +336,6 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "bandit": { @@ -376,7 +359,6 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "colorama": { @@ -400,7 +382,6 @@ "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], - "markers": "python_version >= '3.4'", "version": "==4.0.5" }, "gitpython": { @@ -408,7 +389,6 @@ "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b", "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8" ], - "markers": "python_version >= '3.4'", "version": "==3.1.11" }, "isort": { @@ -416,7 +396,6 @@ "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7", "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==5.6.4" }, "lazy-object-proxy": { @@ -443,7 +422,6 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -465,7 +443,6 @@ "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" ], - "markers": "python_version >= '2.6'", "version": "==5.5.1" }, "pycodestyle": { @@ -473,7 +450,6 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pyflakes": { @@ -481,7 +457,6 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pylint": { @@ -561,7 +536,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.15.0" }, "smmap": { @@ -569,7 +543,6 @@ "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "stevedore": { @@ -577,7 +550,6 @@ "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62", "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0" ], - "markers": "python_version >= '3.6'", "version": "==3.2.2" }, "toml": { @@ -585,7 +557,6 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==0.10.2" }, "typed-ast": { diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index de59d776a5..0000000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pipenv==2020.11.4 \ No newline at end of file From 1a3b39d5e17a1007c859de137f1da96011dfda80 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Nov 2020 23:24:51 +0800 Subject: [PATCH 0676/1221] Final fix for deps? --- Pipfile | 2 +- Pipfile.lock | 4 ++-- discord.py-1.5.1.tar.gz | Bin 649410 -> 0 bytes discord.py-1.5.2.tar.gz | Bin 0 -> 649426 bytes 4 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 discord.py-1.5.1.tar.gz create mode 100644 discord.py-1.5.2.tar.gz diff --git a/Pipfile b/Pipfile index 755f74716b..b878d053e9 100644 --- a/Pipfile +++ b/Pipfile @@ -27,7 +27,7 @@ parsedatetime = "==2.6" aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = {file = "./discord.py-1.5.1.tar.gz"} +"discord.py" = {file = "./discord.py-1.5.2.tar.gz"} [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 9942ffab90..0eacd81d2c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "878a40a1f33596a78d6ddccfec3c116687a6dd2a98ba77d7f63d2a86d07d2718" + "sha256": "1f660c7237deeaa50a7098002c0f3c05cce254d6c0ba8bc02993c5400e335a59" }, "pipfile-spec": 6, "requires": {}, @@ -82,7 +82,7 @@ "version": "==0.4.4" }, "discord.py": { - "file": "./discord.py-1.5.1.tar.gz" + "file": "./discord.py-1.5.2.tar.gz" }, "distlib": { "hashes": [ diff --git a/discord.py-1.5.1.tar.gz b/discord.py-1.5.1.tar.gz deleted file mode 100644 index 31eab92a92e5c718020fdba80f5a9ac7cf7016bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 649410 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 z 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 z zlYmLSsHAX+ZDnP)6;!x1nMQaT2V)`@EKz9^YL!2sDJd+H!khp|K)1i+JM`fnux_w1 z!X8A0dAcH2c$kVJmh^#itzo(CN9Gn7!pe&G)YQXoK*oye*Zz0Dfw3?Smk`Ecf5eK{ zHLLBT3@}doT2Z@dQ6SXIJ6`)gKSN^moHVug2g*Eet^ zwyz-Q>r01chF<$N)uY!2Z@{;EAPh`$kXE4Eym~PFiM~C&LvIW3)Y%DUUkJu*y_uiy z)OUp6?QUXocAK92B)mGB7qoPBcIo;mu5=YDB}z%+#!uA=b49u=(H9QoMk=FY8I2TE zg+n0@r|;vT08}iy5lV-iIr=#m*5ukJFHB3@ACZ4weIXDfiHS3I`5(xp$M@4@g?VZZ z@l@THrIlV`ov^;{MV=yYLRZ}cykO47@}jzuQI5UOQi(`h`mm3I*`)9CYPXS_ZZMTI~l zfH@Wj_ZchPk1emA+pVH@qqV$!WJdRrYinrS-EGF>gK_xNy935Ek;Hj`GhzjjG-kSp z)3V~|1CUsSIVD@L+LCivtwqg0zfVYfttUBQ6P~^!GHQ+DYZf>;QwuMKG zqQB=Y*NE%9c6s$4AuEOYFI3n#EA389g!UABVwAR9>*&6VV7EgtdiNr{2ySnu1-%L` zz?xrR2;&OrcSM6nt;2Y)<%tgU+FUHHSXN=d^w*hsP&X%E!#WzyMjkn^19CUx=8TpJ zV}a4hUkZEP4>yed4(UpBFN?=QkS8^P%o9-^$16h?DdE{>ZNO%iIG<6^GE}Rop;!7! z@Ij?XoH$OAr4Yzg$Sn>_mJ;%RIFh4dqHv=#A63{eW~?TxTIy@6bbTZTJ|NugKAvI_ z+Hlv@sZ-#L8DDJ7$$-0e+-;iFsy*FkzIS^~wah?B6R}vKOf7I*{QF?6XkX zq)f;aJf@&C5EKfr+|caX1WF@mMO%3WUI>1h2~L7})p5RI*`LQXkG7QE3BoOA0z1ig z=7=f1z9v`I>=u;bqs?nq8hImz-+B9Ph6Tdqt>L4g>zFG{$mU*7o{;P9`}q|ZRqlez zm^W-N+hJ_J9iv#tX(`f_`-?~kKpumd5|t^ZR$~W&`aXg^lK?RlAz%X?HNP(CRD=S( zUs!yEx3&GbWN+*CXL;Lireb@-*CNs9^6FMsU(INFwUCK;J5RW!Nxfl*SI=}jXL?@$ zH}T51g$}I+<_PSwj)7#Gh5>{(2}Hs>b*Se}0fkt`^vs@m~lkd~GB27_Am1 z<<%eIz{EWyQ(t>u*0p8^jQvO&g(V z+2Z8By2LyS5kD7_KeO<4 zu$Hce+nI1&lqatf=iHa4nSe>n=nP zqof{)^Vpwbe_dGlniHbSYbq@#CR7)tSr)-S(BnufCHEMy7X_^S?)T0%ckY;MEjtkc z=}(T-WtX4gORX7(5_wZL34NT7-fA`< zI{4%tS*l&$3WQ$1g@1#L+)$2<8;-;B0ZKY+d&Pvbv~e2S!WVBHl5Dc)U1Ik&vrdok zP!Y!e_2|$1%L>|NbsqRDJzUA(tq6HTkUCtga(T#Q8dTZbpS<5-jEq?N(ggNpHEum= z#%BQb)LIU;2`2XbWoJ^&=4=u2r)4KAH^#|NP0twP(TJ!|oPTRP2o5^Kgwf`6jPlH?N}M)KU2Kt`4w|VM%`ip|Q$o%rxkm`f=dPRaUk|Xf!0fZG2^+FWjnxsg z5%D%zXb_O_^+*9GKyu(ehqI8*(hPhr)a5MD!m`SHj7=nlhuYYAL2aDD1BX0bm!UEk z3YtihUJeH`**^(E*=nslMp2P^Usem-nRy_~VG&iALn%>Kg|U7++#}VMtd^`4YRPd3 zP7GUlQ&4Tpwy3K8Kqlp_+*X+QoaR zN#5M7G|piNVI!Bj+ioU@*}>hp#7qveM`Zv?yR=3gc3`f#VlhY?916n zPSa3%WM2zn&Zf(O>L_fv4y=xJc7LA3zBO6CppC_xMpWW!!ajjbV=_(HELw~_O_dnq zqKLO7ujK`yM}&=!H=59Q5?5Yj(~Xl%4(e14x7HmyuwN8)=&mL)z(4qXLZD5Eu?8$wv$jJzv)I~=eX*FoW4*9%UPm{^oqfzv*$qZr&XV;+8wX%3 zDPrwv!rX-Wqumr7s}OtLY08;Rk>%T{qPM?>XhiCgklj=?A!aKvOT)jk%yMTrs{N%K zYdLO4%b-N zO+si*i5xhP$s4}%>Iu$gPqE$E)hkc#rFX{V#}lD|3VUL)*=1SR7qkfnDs#qx;#As@9I0l?}eMXx9U60+8OA2FnmbQ zZPK~h;ZLw#Koy`Myy|zF&)@6jd%dd3#ybe|lHU=McJ1rhYwjYniG2jomcEJwsa5E` z@rTbjf74(R-~rbVXFLR`g?!2TGuKXbt1BO3Q6h5Nv&;}V?%!N_brY$BGlapTpi?=eu3?kMb?2HqKB}WHo_>u(AKUIHYOmgg z&t2h~D}|vWy!ZvQmt^YL&=rnuvC;#_<)R!ZITTJMZ&GLfGdoA6%{-ZB_7ZvE2Qv6W>1N5^?<(7!$HENx5O6qgR5=?#i_cvnLTyZ(d-9p;Fu zTB*B&vHze}zQl5kyR!@JG4FJ$i^1CY(Ar9@{fDv>5TRa+JNhK#4)$AhBuh{`l{jntdFS-<7RHQ;4huN|ghpp6_Dn;Z&D~{b<>WiyrU`wL zuH9nO%KL2Q>GN=_Uh7AW20egJWPA*>LR^ zuF$Lp7_WUDOExSbjZS@^fZs1&w-_T zP6&_~JHEF3b%@fyQa-9*5N<&f3OZf8lwRc{xuKcX9ZxdE@Vg2{M$u8G*;>$VJu)-| z_6qwr5%Chk4pC)71sh3a|Cbr2x0md{+h%US2@kR|mLp7Q;>{>01_PxN0kn59JtNII zuT@YiC9X=PYC?a$X)yNB^?dcLx2dYlzu)w)tN^#)^slS{1vjOdCVmg`e`=Ok=GQik zIge__f`G94?*c^gS}iG?gameCG+kwc3l9Ra+#4B4u{C6@s z)giZSN+p*@*vL@#{1^XPkG;Qm(A1TT?h}a<7IL6C-CFx+nGH>&VT+kY1FjiE^I8#~ z{VbE=Bi{2$Snp#8ZV@)KScaU_nK&ksIv34F{~t=?H$P8O5)~veG@dyoXryq*`fBiO z8rkVdD7(B6VOi@JGmPw7W@2zA!la+U>Xp|bxVNiCzP*>7*?qBexx9BB&dz&d_@x&l z%YpCIaYA3?k4LZo^UaNQ&K_QCF9Ri~b$Wrq*EJXWdcP^x4}l$vA6&qF=EURg9miJX zoykE3lwG@f^I9d69h}D0&xFW4nzw>jx*3uG972|kdCm%1`VSxcfhw6r%@sZXdZ3ag z_0XU-`NH$q@*hta4TWrGg!e=Gj$ns>1n<@#p-bcP10eeG?P>jUYsdE53C_528i*G+ zYkzuU>N+$H+mW3&rkbve5{QABy=YM81_#jKtNX16A($HveQuFO(kN19x9d^jfIlA) zQ2)&r|Eyne1e(O{0}w{Z7Z3MLaD>&_cbjS%_fxM7L2_SX?+SO zemR;dL6G=xA_;jJ`ZzTEFOgWg?Mx~_tK1y;+7@3fCVn?#o{kyQK7mNnAT*k}$3%9C zxTRD&Eh=N12IptIatZZxB9-gXljM&^((yJX_Gh!?`nm7I4I`3qp0ge1(ToCNA2bhJ z)Q{Z>bLm)NF1>cAX?`@u?>w(ZpcMBAG%`C#-^3}H%^4|&Vn_=@!=m{N;ldexYN-{^ zsR~})_c_Fj38Ul+%P3iv#qnU9g%`{!&WZ#ru}LXAgpj)_n(4S|j&$CkRXYo7E-47^ zw~Y^vC?6N~6XzMhwb+SM5hpoEQ`=xBS>_J3dDveZ z^+@(ch`hWl;g>1SGaqMo^59B1^g^|Bn(B5|nxP3WL_TD11swi%6E{D`6v6Ws{`iq! zs#E4FZt4kwlICK`W#^>Deac3wmCotZqug^kUrGI{bXGxtj||_22z4cVeBJENmWApH z=TZ?&kj-E}l)8)8!jHgTOmbh%?BX`QnajRC7a$Js+x+16p+JpcgzUvz_`y4wDDe|W zESvbjPI^!bJ}Czt9>NVgW4ac67mDgKtVE8()|D>&w~PPG-~AaiFX7}fFY|+JZhJDC z`U5}s6Sw^fH1!fc_%_vF{6}tT=nxJ#%1xb={Tt!cs%v*@kU}! zMA#h>VDN-;k@tx?;{4-A#7v#yRxUD-h|4;^SNvD5K9k#f0PVdd@CBoKo!jfnNEyoO zK#nP6I5Og|mlnUm#s7`J|1DtiU;Mz2?_cHb*C)Ogd*r@;+oQt;o&+8kV!7{ed=1(E zpYbr+{tCEVi(n%KLEn*uSaZb}T+97MPlw|&a8GLYE=2d1;fN9Uu(fzA_vcM4q*m@v zUUGltA&5i&FnQ63oQZu!Qb@v9bDr9+=XrVpS(26(zsc2ois@Lx)$L7BX}=!Gv+NnO zeFI1QirJ?q@DCA(m)m_ZtvAMTg)j2W;deyP=RHn;4zTHY@5;U(UG1-x0d!g~ML#b?CN4tc+UxIUiUOo0F3 zw24YzK&4aWagBE;Uh%r|p_1|C^ycQX`-`+Yiyq|n`4+tYV#53G0*3eSv-@AjO_g&~ z$EjqoeadsVk!u>lh;%bRHhUe9Rued&sV^|WexH9A2_SM_wjy8HNQk6E)jo| z<8>|=IBR?iM7I-%ZLWe4@eJ1ESCG-_5oCGRl(`3%*|3Xvdy~R!0bxo}XY%;d6Km}% zlXBnp1ESm)4J5@g6$=LYkfW)*Ysy@)Bo-gRuF;fvRPV>h`$r<|jrs(K-i}Sf1JldK zK1WVi2&Mga|GhwBXA`aO|NdZ%qernY@n(`RaY;WS4b4qN>^WaBo}>Ug+-P!4AIqkf zzn*P+B5n_L{_4{yIDtqX>N4JF#xx8~eS19zg`LB%j`UmcfCWCxkQZGnaS&GvrhhX> zHSGJmo;LO;%{O%_iPDWWp}+q+g8s7pl=H|*`1S^W_>7p%IVtUHOMwu+{`C0zs^!9U zqVUcA=d6F}v0tTxi>2nLU&kFC*?&I#b&pk-y=vj^7k%f@KX;B5lC}fKU(6%^dk3vl zb8q)?e~FO676kly^qO4l))89O^$u6qohz@+B1w5+f6RT{ z)n0wHAb9Wn@%U`48ztS}HHBmTAc!CdxsE_k{h>Uu34(Ud{O_pc&MbLtWd_Fv@{EXH zhW#|Ub+x-4T*T8OqRpPazH+Yba18Yv$7ZqBc^3pC^?@)EP~R2kkqOty4XS zIn!y#wK%W2@1UMpN~QN7e)Jc5n_lg_P=2e_d0{yCrcDpdg!aPs`W&Z2AE4h0;S^iy zynrq*=)}ewJwli{-4>WmdK3{Nx|)Fv#DkqSXLq!_9a}lkp-g`fbtt%``Gag)Jst!)YkuGhW~jfh7j&4h@S?JI8v>Hj ztrVhZl3gDxI7;^!<<4B1!Q-=r9FRDv0u(W+wb{&<=sS{YwYUB6$?a|WJ1oF9O*7le z*XCxZ|Bm)P8;Fj?FjATuVKyfnr^dcST7_0CkY7@~J?(C!+3=n|V`E=62< zdC_fP3tz}DAl2wv`UxDBS2f$iasa}8ASTy+HyvqR!5~k&qVnR)(m5(&j)Hh-mCo#l z>y1Gq;jEAppF?CP%~fXGlqc*;hWmmRUzQ|J<8x?p`liK)z{Vof8}@fNsGAS@MuAWA z+Uy~9v?N7r*5ioQ*LGT%dsayt{;)!l{XdeJBLIK}C$)_LR^-Xm2|#X*xL31bFh?D4 z4+LxG__k*FJ{?dZ%AnE`y+9iDZIfJ&*lX@Yr{YOh@~wbq+03)wk3O&GRxso*^zbFbSS5pYjeMX1Rt$QSuk;L1wCc? zYl;oBz-CxjPDaNpjco51XMV?9hH_K6kKk|}UR*;RyokSt`vNXzTh9OqD$C-mZM6IxQ{Qm>O@A9hHIM?HKTkv2#$+SW1< zQ?>~U)Vd&ayVagt?-^VvYuM|3@;3AIOPO7*fP-=UOz5H0@?Brb{(FZaYem$qg!de9 z^|*&3A$mJ42KHf6*=;_fY?N@JVN?CO6jOccEYnCo$~t+Nn|fUEzmf_FpPDp2)*TQL zudR9(axve0r%e=no89%Ff`VA)^hGlHWWmpdAk>yKL-yYyu7=9~zlbZlXF>}fY!zN4 zFgvHmWA6xS+?e>=!uYeX@n=QmGc9lg{hJz50)L05Fs(9?FABzy5Tay-Jzu(Zc|K9^ zb1H#1aYC(Au749MfM=8dnZ~CtGc*h%;e!$SEd5#Kl(Gif!|ED<>lAxsDs1FbPDuSq zBl}OWgN_mivWL?0S-~_&UF<5PPPI!t+8V^z@HJ(b=YPxWO5+xOne7XfD2?o$F*QeF z`=oGmf@?D$AyxTQr#KEM?AIu5$wNO&`e4Vhiy$`%>2V}NP`x-oSUe(AAG#No(^&x^y6 zNEL&_k=r=1R*?OG*2A4&+5hEVSpku(Gmi>8ma3cHW=M^(cKU@K)2@~{#qQFz;zSLH zt{G*f$bwgn6_HHrKe7qh$|1Dk-v>>HTVxTa5SUGpX&|VCf}{f&|&5Kdm6#S zIpg0h&+)FXi;wx#T&gw-BlnWCGMu8bStb>BTq072Sjj(f)T9pciP)~E2M7wftyZ`B z%n*RNKHej`#AA55|7pBUhAS6ijvP45u;~$4WaZ+H(ZR7U~BBrU#F#Y zWlnn@r&a&X=csh{lhFBaQg}qg^HzQnkVMT zO0T4^_^52sSkHVJAFIw&lw9}0FH`DXW2|Aq{C{N_Mt9%2pJkjC@^Fk*_#b`NSzeyB z)xG~eO$r``a>g&>nsjo$@rrd1IY}hF`Ip4UI}1+cZBZJJbh20MJ_4W3=356rNK#s4 z|M21TDaSnpo?+KQ32A|xee9v3Xj%Kjl8Ws449g;SpTNieitluC0r->i`%&imtG;*k z`(>Yj8RVGNsf%(XYd4DtRpG0Ac&kaS|L$-gHWzHnMt5(wJ$q^suC&TwF=;nUk7e!S zCS@sFYbTCU7GryZi)ywHlBA4Veqs3vnDQ4%JX9#6|1lwf1rAXakrsNQUZw;hov7nK zHQSS^^2({{EQoAopb6)LeQ4VM$uI~9y$8jhH=P5NRq*^;1ahrDV5fnoYfzSCRVDBh z=3}K}A5vcj70pf;c{&cJtqjwfypR&;;ojW65xa$o_CCr72JqWs!o$~ly*w$b*`5y_ z$RNJniMF8Iumy)iFYe-YK!>l>Me^!U9+#OFLZGv8d@V0tPuy=-XxbHtCm#`5 zXSG3bAIA$v*07%g>Ew3t*Y>JulDv{S;(EX?*Jq3UV7r+L&W2q|90Q;-ePa2t4vtrA z&2JCVa{>>+X33%W{s)ZZR|t3KPN=Xg3|~Y`m^jl2njYJQq5^e91>zI-#T{Y=g0;TW zZiPr>eVII`8BDZ%?+C`gE1!fbh|-hcad535J=+yVjJ(sn(~?QULN!CQ(klk@LSklp zQR!JhNyXGE?Rk4Q9=npaj8sNJoZRoj6L~VpdXlSG_TQM!q5k>?#Yt^i8F|g5av)@s z@GHotp@@Lfqs4mXAd?J6PLo$=XW2bq+h>|x*bO0D&}Jd4XIrd?xqy(`EjoSjPa-|# zDKpDl{5zW#*kP7=IG5x5HOT=)9uxFNonQ;Jk!a)@TF~LzFE=!q#T%`P@5=>m!W}$# zxaJ-El0>>nWMOjcS64EYkz^B*4qND^Sb)o7+!>Oj+BxDdv;dWiE@&-7FcX0&Ej$_AmOz=n z&Or`7fNnK9nI}K5*?j6sFig$igEHbxh>Ir@6_~S8ndAxp|Df(zEIB@gfRxJQTFHLF zSb)GujZFJHWY!SH%CsmFu||`|Ab?v6bCyu`aw|EaC98_QtBr!GaIbj;TCUd=4aBe< z-hv>@*ARX&rm80^%|S(tG4Xs(n2I~jB1&~#CQ(A?Ip9=6A5vP`CGXT>Fe`Bo!+4-K zv7d(eCY@E^SgJ4jH*-H_4Ksz4p2rK?*nepgW=m|QyeK7!0`BWIM~PUn>Y7#_W=R5A zN;9%@op$xe<=b3kS@PtvETt?LjDI|mtc?mLS8#Xh4JH}UW*7&ATbG#jn{VFCQT!XfF2-t+Emwkm-b7P5` zzl)mxk`jNKI>)Dfto{iWW=;OgrIFbY9(`}50~gzj0|%TJ=DtpAJ=x-fXOxM9*AWXaHhi75U&>Wzxj#vrj*y7R6rTy>cW11P zvGIdv<)5^vw1$Y9tqQqz$t!~PkjED2czj4f5aTswN6o-$DfvMvsqrVk=~ng^bVRcg?el&%$A5C501N;z;0B*98&Ncj=brkho4>6?A>1Y@?1V$IY&!6Yfr&5-Pb3{{(i9kzH~Wop-8Ih*vL6v zhp^U$T)b&Bm2aruEI3IhQ-OR*^cT%J=W6Nh^ntX$j+(1J^ zqLQ`5Ef(D2?{H5FeS%2__T?&F>hpk~@=bLmAgEkb&?&F=U)f66b+&InxYIqye0`a` zF9M)_v*t#`e|v`2l5a+@^A7TKPURXeEa+?*$_qunyDrzP;-vab{@M+Wg~7hi^sNsK zWH-V-r!`hi0iThEu^KYs`?CLmIVj+t@C7`UlK%_&nC3Vusp@G|*F#!atQa7FEXU<` zR#7{oK_JC~2k9lM0>Q_x2z0uKv}9PxXo{>{-r~G9+;OR#tEW1Q@5wc zzlm9P)BJ*1nAu%=$ag9C!)0^r2|!YK)GDl)=%O zsXusfPhz}xq>Q)aL36wx6{qxa!FI7xnEsmResb{#97JqHC5VYy^-aSMe6#3-r`G z$Zli)A#f9Ek?XOFyo16??F0#CHIux9!kcgkrwe!j3CstD1L=M^*epTBW$&QyWg8S( zzosd*IlKW$*qj=V-aE)ngZ`&cS^XDC=P@W;O9Sk(_Zl}ZJWPLKW9ox24*reMbJ9h| z1(H!~eu;czzLt!FqrT6vbDRT5StYFOiKZ*PuHV=>ky=q!4QOh%_IL)Bl3I`*GJep5 z4fXcO_V>lEOwFK=>qG4cTNA)T~~>=F(a1P<~w@c~WRr2RkJ-$&;Iu+EENI77M7G)8I*o@wP}j z=HJ+8tQa!Us?P@kM=@f#9*9e0#>n@Og7$7SW2q7$NsI@dkHHg_*M5y1Q)#O;cyt_^ zqBZ_KV%*XY*8{c%LFxjsVkP`tXdqC@$D#Af*~kT-mXED`$fn#(*pbeOVKC5Um||Np zlyJdOrO$V8K-r$i&%l$T73G+H`wuZOai=F*?Kkcb6B2?oTTDeewK!LZJ>EftsE`9L z5PO0X)89bbybB6~uE{o{tEa+Xc7YD4A|h=zvcr_Ysr9!R>Z92D|8yY9UQq4KroN&V zr?F?TTlQa$`T20cr&2U}bg6d`ZcFZa zvj~encn9H->i&jY-^AYwZ`5abB|skZysO@2V<2~kIjtRJ;1zTzVPN(i%x=a9sCPM!6Zq9)&coMgAx#6_@@w|iVqwGi4i0ZB0L3lH}H+cuyEj4fqCkC#FS)v^x z|9c1DLXqd>#TneL@Gm}vXg-7Mg?}zSdd!rc)gf9wZ=)(bkrzocM@qh=-Rj6!_mRZ> zecrO}iG0zOvXzapkv&wvcrfk{=^Yf#uQ=CZc)3+}bq|ckgSq^mox3!HuZ3r|ftTL` z&U*)i|M~=5yD!CG%SwM)<#Yyu?jMoJ`F&1bOxOzPPb=cB zdXeSxsicI)y_}_ZkMJ*|uX@>cs2XlWmUmK?mJON}lmwlW+Fi}2JJMPCiy<#ovA|YI ztoJu0j?^y`Ra8KREZ{wG!Cp(HpvhoMwET(DYnI}0^7QAE4kwdZANg^4T3QClrZbhY zG`M28&bZ6;%!ja%FZ6-{{u&g|H1#HUqtKAmv1v4k_R;pmM!FL0j`PKyqGrz z2}oW9=SbJP5ck@Y59C!%%16p!rPtmh2S7y$=yhPVo7oZQl-;n7r0;V_o?;2f^CaE1Y<2VPPuWk(Th>IjX&Z!9~W-OLl>}W*#ePxx#bj z7D4QkUgZmAYjpTh=@?Zy?g{(+?_yHot~;iLcHum<;@)e`!_1}1U?O>`YqqWCW@W!3 z@VPW96TC9q^meN>?zgu2anBykj!1fM@G-7!U-a)u`DC}`TgtWNUt_xDiD|dsJx91# zQ9G3>@^txG@^rZ=Pgf^|&;0Yr)8(c-U2YBakLEt)#=A{na{^11ZZiug%Y?}SrVuQT zvSWlhUCE$|Ui8t`+vvM7V#HT!gPty>H5UmJpM*W!$3=vGuw>6{S zr0Go+{f238fs8TK@}>F`{W3>o0Ooc8aq%XY*~kF_Cg9Geh=! zBhjYjGk^@H=n8=CG&Nm8z{`zZoX`)5(>`FU*+fu#z|Q%C&xRU6$-Z7D#5O#^5Zh+7 zF@HJ3qGn)@#fXziE`1`25nJ@@37a*If!KnJja#6enzMHU1i5~iJ+0^(vk&taNFT-J zt!$FFMEmW=PvRyRJ|1D}6TJVbG;3uGy+@FIU1^+wu6eR7Z)x5oBvC;_mm|5bSWa4) z8ZtNo;brc0^~jTZG~3tMh*Z|NskFHr`A{iuY;FXa6HYnf+naMyWFPk1S_dz0(@}3f?Vv{b$4q*5#^GsaTuC@fGT&=85*y6m(1Y~1EK@mOLvm}R~*RLtu_Ku1RLLRso-RthE{z|8NQDPh}bo9@9KAfW6@gY zTS6EO(AMriALDW7QdXBHLY2bCs4~W2 zE4ZAKY4K)8B|G?Ji@u6f8FORS*2bKexk+C`GmWqnZl1h4iQ%8e?}@6_kBqSgs~1Byqz=TUI=F!8pn1`5v8UYP*&xbebCMAxDgOFD ztN)?@xqd0Bewqhbq&+p$)lGn^5wP^BME%nLwg1O=om2mU7u5`CWX2%l_bJumSXe#I z5%c7$nhAKD@QKQ@+}YBsX3KfkCibD4!D7dPZK-%=+3)$`;5ld~=FFJOq3u;(SSl~9 zd7rxYRkgFA!AuiLMTL-Ob5CQU*I96jn!?>EyBPHym<$MU11AvWs?U7$5+f7ZO zBg?dxX5VYvaN30LdB1=0uc_aso%8+I{pR<^8+WA0^WdfLv0SLU2Rf;INZegsn=br< z{Rp9Yea5Q;BHxmgOZ+w#tCX#BJ>FAM&6X~&_Q9X87(T5F9@qR+T9b0k#5)x~pPa3g zE(2<05yAX{Y3r)sHS-_$pz@kbX}0Y=_LsU0&}{53pd+j9CLG{)jp@Q3whj~|+RA^} zzL`%in^W!aEhVgB34dhvyhovkw}Q{T3^iZ-w&aq$X!t6`LEvP2KB9kDCiFPttpvZ} z_a(No>{+mHmqIr|s?J)1kgHwRYJ{Do#5K_-Fa8}uy}nt|${W}Ayxj~M5v;c$W{Pf= zP=~Ru*vS1YNf^2Pie#NUMj6E1cFP5wGriT5&zw$^UuY(ljt@2usrk;#aTC(zKr8sv zT{bO$a&&~RDSf8v)-!V84G_-{JYOIWM|DQJi$nu?@u$KZRzlq`m)E9g_oUbTRPwcE z)TNttmB8+FwsBQrx9V|Jyy01mVYPxMea1LAK zD|o1Eo^VELb{+HVRjXM>r54&v5{UXw^d{F`CQ7Xv#4fZJLCE(V*&J%|9>@ACmt%P-T3uGE4cX1bbX|6H7yE~Z{>MsS#1>YP@2aXK*) zsY&{19Jk`t&rPbHEuyO92y zD3y>#bn4^K;h}H}u7<8!bg>dP^{Jq=tpuT1JYQ>-m&9;{0&t=+)MW*Z%7F(#TAxTV zTU0;5vt51NKb>h`t5lb6Tu6gq#%sXEb2F3Zzd(lyN0(#N7RkyRlmoZp<)wqXwNh@p zpM1WR=;jARH@D~QAt7s<_ed42iOn2DOklQ^hAT3%Tu6t?UMuLD5vl2CLfY(cAuV=N zfU|gbASiIj4yRlLLHT8I2xfBFy9G@uVN<^qx_wEKc$aN{nq`{b9c+FV=J-O>96ttC z-f>!(;!Sf)e7}HzI95$c78b8lgmEb3ZKw)cvf&D+FVd~*QtS_)n-&cXbktmoqjn2M z7;7x~p&%&2Q#8lV$9a!b0#{c!XGk1HVY)a8HPe)|xk+6)5r^|NeSploLJOiPU9S4) z5WSP;!n*}+-Ftt6--!LO&r|ltyhmOWCndL}amRwD9}+eh=_zQqOT3i*PvLjTXMp6} zP8d6A9qkuiAM~&k-N@> z2npf^M7&wYfxKpC}oiCf`iBCj+d`c)J8w6m@8Z3z;+YB&e&A5Y#ylPFpSqSD`^i=S1jAmq~fQj%0! zM3ZTWIl7rAN4rDFig~alZ`miAa7~d{Mkl>kDy|doE(k_j1k8ixd0O7b{xTH3O8Rp5 zX<8`rwcO|QW4@4$*Oz|FfUn8s3)%HF5zOwu9}qROyGd`;bM#w!^%9$ins*4#+w}JE z7QH##q))TyC3by_ejt3Xdj}$F(zEvNPY?pn9&YV!?hb`VMCcR!o&nb$&$G0IOn>@g zxJ<}TRy>^|^*#soF28~uWBw?F1XQ*uAIbis<^v@A$d@|TA-~6|s9Kg4I(=2%UMZ;T zQJVOnGWF1@!)n*4B->nEhCjlhfMXnt&r0~mgQ=0K8kK8>d6*9 zX`C&i%G5&GO6G`l_~z%FQ70Xe{X02@bdH{RgvLhLVMhw89$a_UT!!H$)XB2i8^t_(yqp8w9W z+;A}lW8BE`H;775LFG7#;)ss%Z!vM&2k=nK4q#IJx^9RchF<0j|g*+F0{AY?C>b=SXWx%Y|R&zV>j_bc&)5(Zpk;D z;{DFBC})q{h;Yk`Mj;34BDAkIa2?$tsma^_9`!|Ua3b`I{sPL#OE@sV((_n)5{@ ztyq#xBhv1WXtL+|j0b)FGvrCTl|JQZJTK^o4#RqA9JPYF>3<5dK})0RgEYE9^?YOV zHA%YvS-ZGj(S<-h;l z^PoQ*jOl)Uj1zJ{gYx&9^BI9+bKhZB9>t!4(LvYxb|Skw^vu$i5O*}ZrMpefoM>L_ z+ruNu>AIV)y4&@(?*9FUIRG!#TBd`3vCjqSTtU}Sd)Gds(IYBWy2cSUbpM(i8GH!7 z$~i`F3U8-nIa&`=1EFw8Kc;7vyo7W4UHW6c(%U)@%e6<*ot^ptlq2pSy0jzQLQRV< z(KYb~(WMsZ(!1SXBM9C(9cm`d!d)W1L*0&EWYV*)F76#Y-1o6BYNvjc;D|o`Euw#; zsogO+D-;;R3$u_2OJR5pJqse+8I>4I{|b2&RO$gyN|2v9{PkGc8By8ZyvaS zhTa-()lo@n*uQ2mp&yOB3E&#b{jGe7xnN{p)4mgW<}}QFIku=)yr?oc>Q=zf1y z2J4`7#S`4tY3Ly!aOdI2?3_EW4Gf=N;|m(r)u7@?Z~53?fX3(6>iLV15kx1;(Mf5k z*-6z4<{1cjMrq#ZS0JCY~4?(~RBJha8mI*^uLr9T{`XD`zY zU9=HS(?3G;TkXDx-PdH-7vfl{eydGih)j3-c2|hXv{RWTF2|+2L!NiTnOQH1Gn2O# zpwG?~LTM{qk651TAVX6-9Q5h~?Y~P1e&G?*>5Efcu2!OOqm>Wn8yJ`fX)6iN#Qc$7 zk^Yv@zasj_D^%8y;2ijMOldl`m!zSFkGO}&bX$v_u9vph(An+hc5+{cfX%D#n0Y+) zusz&HNSq1rGyJk2t6Fb^Jk}Z}O4i-%c}>rp@sc=`{2pO-CsEwpgPdjHB^(H~185=5 z7S#OX*O7=peVnK>7)ZtqVmSuVXbBw;gSLaZ1oeLX*nV1o7g3K1y@AA7v=1=_nohq0 zY$l+>r99@bUD?jAD(AF_@dFyU-&rWd;hTY_A`0sbV<3i6`HHSm=QK>j`X9s#(r18e zQh5P=eZ?3)Th3#lg;yZ}VL+b0Ml*2;MiWR=0}kG2-> zf)ica%)WLvbvJq5!=jiA9Lt>d5~rPvgY#(#`Cn6k*WIUQE__KiU?4;1X_N^^ZJf?G zbCjSnCD4*+3jMYt_>y=AHR7gOYzx;{WQ%3XsB+qF!gpWj(c$#=^5!uF$LZa z<9mr!Obn0kWAZa0uZG*fa+ryTDw-wV>5Fc?PLW1wEZc z1?t$%mqQA-wA}$H${?226}>jB%$m#&*T;Tr4o@A#+7@-@44ceUJO5nuWvy5aYq> zLSN&<+(EUr892jEa2Bg~1BfUN(WR}k^NM$7W4&b7G5G0rI&MSJOITlTQ=i;|bL+nk z(l4p*d966(&tM->I+9yFw>fGIXdXtZXvp-WTEREr*Rm0Yj$ZK~%ymaEaqBy5^YssS zUew=qK8Lw~%g=ucI%bnTQN$sI-gIOTOPopYU!GLrO&gvdE?IOKm~8_y7=HsQC4?tN zKrQVyA?R;FM=*H6qq76T++tqOY@c|I>(K}30#(x=wA3jA&YBNtW^q@H9?7>i*ZD7c z6-q76rC%%u{zJGnlooVaW-6@?D5kv%gd*K&jGpPF1`FB>PQw3!7+`XI1uZ)ROnhtD zc>(yD=cvo=9PPFXFhVjQSA&xsHX4p8<Dbs~_Fo zyIc3y9U+M+yaT#QGcPWF0D%m+pzqvoJUU*9rs&5Ja11=mBg{=NNQbal&%V5;e+ zg^Nwe&Lc!E1dDHzZ$rkL3aC-yk#FrQ=c;*St-+5{CtaR*U+XN+kW`cY~PB?yYbV)Gb&YjuSX zufl+50TEf9KM<3H%?$56yD+DW&t%*f%$EJ%rc13X=Ig!aF;Z~-)e=XAEQ>(CfnPsiuGeu=|!|GyTV!#)wl^IsT2Mu zA#sQOPeS|_RSUs#AI(0}hL8lagGgvV`T~hg z-6xpq62k|aXIByN6YE$y4On$=-`7MF`utJ>u^ax*(0vooT@t=DiK;%$cO-;_N327c z=j+%g1?d9lP2E?vh0a_RI(DVh<2x`kugBXje-GQt&H+TK;)3h&M4sGK(1GHMSWbN2 zj~Y%BfyG>e{mWM5VwT@#;=imYm!DPhv!D>+GPj_2iO%2)6+^8?aQ&EjT>m(9NHNb1YWW?y!RzEmyGGQVz5 z{Z@=a&wO9#^kl$q#=UQ*%<**Kg{m0psoPb<<(2J~sU z`Yi(pM$_);CaldYdP&#_iBk9$!Y|o>71~Y6(CgFBBSRn5SLBX{BHL{9opyS*@DeX& z%hYRBo-q(oOZe)2z`|zw9Kmw+%mbLmzXBcTQ8_<4#E+JWM-?z0T^(T!#97JjkKnzp zPQ0JSk9zRvRV;5SHd2et`Oqu;5%lZGsiC~lQ3~@Iy{e>F+TWd`ye%^80#++w|ufP~)rMWi39~1tdY>M(fWn zraR-l!$dpx^7?%Soi%=YnoX=*u;DUNL4Iv=}>?Ro7? zg{kvG{kbi|O$v4+SWRbJmtGJg($vn}AFPIZY+TmQyrZ4_AX*JZ8wfM^c7->g*617O zTIU7h7Pe^Ep%X@E<3VIlJ?8_M^5dnA??zZvAhEfNMf$E3k-jI_6zL!85+d?67Vp9E zwi|Ev(5LNe!E0wNsGV(+khw?W<2S62$AWNnm72Cd0-tdqB>gI=d-w!}Ypud#sNh7S z=VaVJV2bZZ4vwQnmQ5dwDH#MDr?1(ej2{sB3+#sq6G4q5KhK97P|m$c`*FS_@RJ{P zc2Qr@!Fglzh2KzeyiIl`#g-S2a*ZNx>{R~&d%5~-4)F0*-hS=28G)GmlNR-HyExux zW&yC}^^dD`j-H?G>X!ZAiFnyMlTN<=DT{XUyq zkNEW3luYfBKlu6|lyCn~dt@bcPHx{eyQhdf-*TP0zkt_0VLU@=G1t=4y8Te0DGT#b0g3UMQ9agSxF?Z8@o(?v9pgut@G$nK0T^qw-88T&UKI^dr4?h*JcS>uRN~@ZbUURlTwD- zl#lXS@^&f0JXWMmo#@&R+Z1nfi!)JUO~=AQ(>-X6=^n%>1eCBmxkWl74(d8186U@D z8CA}bK<9$L!wu@%Dn6<2RGvW}_{@*nS=wT`IJo(pn}9(e^KGf(L-b$33pMpW0pDDb zX~w9xIn)QT)Zf|}Xq*LCuAhWWWIkp!R=r9qe3i&_L8oeS29EBx<alo~Wl!Bh{i2PTW4;SjW(|pb8F5MG?`z##&(u5Bjq;Q+&SZAzf)>rsS z!rBN@2XnD1R@$-shP-H_#JNl}$D&-d^HTY(O6R5F_O-T|9FqWd#GhYbyG+|!2cC}9 z=Qy)DcJ-z7Z)9hc(}8O~;Gga6v^m2gis*yxD5*ASe}q0C#-7tbQgwIgJHzeWM)wZv zn02>>+qy$hQa2Lu`Ep=PXmyVkZR(JwGsnEb*6u|=5DH>gkFZ=h;A-`Ri9IFzKPQj> z^>fCL%qAFrh&?$_B|L%thtY?*C;zvRA3k^FJMM|RD9HF&#|u?C z679mTejxgRi1&@*ALI3mi|A(k0Nxr1C2L;(AeQ3K2Y)4}@JJdfJd%p=XI_>%&7tMr z9Eh=NPc>Y7iqqu!*D`pw9YsT*r%OS0IGRJtX&A?Z5^vO>z#rW~cO}5*+-Eh%O$Fk7 z=p6I>9^vziHBPlbl>fDju25DQ++Rl8kWJbe{^@{g525rI0G^hAqjCb@C%@f|w*uj@ zt!-kVg%5}2+N(0+6HHvFYp-0pT5M|36hD=O;w>2|ll*=8tvSx`huf9Ie6iLhYR3HL z1Id-cVzRHHsm5&=(BVfnjs3zk|EEY3rs}Kh$E^+f6s%hb}Dbh*QNLL(y=_exPdjOCdu^?6^rPldk0Zrcv_&+_|s#T9SYF3%U< zEzY=?okbra#-|p)H}31Py%sU(vwsHY3X- z=*Nst26%toyt|s|TX=+%Dl!hANyINQY1|m=yt{cnXE601{)fzui5ZXn!)tpujY0&} z0Vi@uTJT(Ix(UZFDL?N1rEkk2d+!|MKOTVwLnoZps0w7sDOeAP@r2f4FW1+939837)Ei-j$-QCV8*PN zFk%K7^Oyr??drlb-#ov2&b|Mf`+Uu_sIKa&z1LpreRrxI@;68qBDELl*K7te%U9gK z?(5LwAO~^fNMG%cdbXb08WxtgL=Il)s4GqqW)ZDnK~{We!0Lw1Oo1x1ALt^4&#mud+=t@`*Wy%jS7B!4_HM-0E}Um01C7)<&*1`<_#zs>F(?cE0Va zpT;kX?MM4n#%O|%)yjrB*OjK#R?EfnDo*t`Y8m=CKDg4 zm_9)_$@EB3UN^m2gY+;}rP@^0jM%omkI7tfmHjhip6+I}<`kZY>CEl+JincI{JYxP zT&or9_1U2eueNs7o>yD{^<1=w9*afPzpnU;QAKY3?`Ak@@!*v8PFHD#W;Pe!gdiXe zniB1NwLo#)qMisE?bhEF9tP4I6YYIJjE5KJ?`J7YQ9U+c#7SvS330L`VDNoqxOktbT+D-?WmBaNG)ZZ;q@hlfxQx+l@jtR*nSAJ^fG4Y;?yKSo-C4fH zM!DTCRvBvN3mKtv^DOf)h)LnIrm_Fr<%$Pb9&{dlF;~2rdrfsUZ}_=yAZgAD)b`Ax zv(oNsJt{83I3>?K=Yp~x)*Vd6@sI&V;(p@Zq6aRc^$|Ip?O7!bOs zy`tvbE-L$GeRPy(Gv{V~G?HiMk+0sXyA!>4(iQ_TTg8+p(T;-6vtBEy#HQQjS#Q9z zz3gA*AU2_^aIqM&|GC_&W^9$Y$Sg4`BKNzR`_83QclgDM^=#7;|8tSK;;mUnYqX!z zmnCfYsmkqN*Q=H9mgk<#JjtetWdMe8(&apNTwLEU90Qr|P*t4Q?V`&$T|@C&U$$_x zL)B){Xq9Es?gp5Y`wWQ0wq|}Io5MK0lx2?3`r z`Q{S-n0vJP3bMnLP1I&{&f7UvpUWF&lyhBm{cC7O+RXNAn{hJce{J4i4w#=^Di)OH zm?z~v|874kmUo$I^LY{UeQdRoeBSEH}-=RU`1 zFcLwW=rHs9^elU!6jbSb*ZSr(H52=i+_&g)eGNp6w z!MrX;`235X)6V9g*8LVkB|Tvk623mi_KJqI6% zb7jRsT`Yi9btS7QBIXN8@f*vG8R*bmK>ZjtA(RdK`<3`uoMG!Z^&kvBw) z{E0Q2Nt1h3_3W5_hLm|6)6cH?R+EPN+A2WRim&{sQNc!gx3-k4iJk4|R`aNyHu+Uo zD%h|iaqSOQe&~98WqJg3BSfi>MNj11$jrpLCLC6>u!beAkEa)z>ERzN{a$tE=lyrY zg(9&|qC3D#UKnQ}FYIEK^JbQ>PF*hRBwr%sK%9wRUA9D75sucVGjlQjgP~m4K4#vF2_R;z}zwU7THrJ&y=I?LuqGH;Z z%fwCKm0iS`*R}TL6I~i7Cb~54=R~u0e7=?EZ7zI` zQK_4I?vOUhNNtqQY7%R}a;$UAUSIDX$Xa=-9@_gmXz%wF@BcwPd10_tvFqrL3z2i* zcsFxY8s?NJ*+?#uBj@u6#F6l^|V2=al`wxD|3q%`;U|6BLRN@ERggA*CvE(G+W6;Y z)Sv}(igxnCM1-lIRZjJ+K1OwwPHoIQ5mB082-ha>*|U;Z_{BT#9K%HP@%hi@5lOG{{RKS zM!+m=q{mrjR6kjtbbpUs$e-!#IDsq zC$FS>Z?&($SSFi=KasasY_h(=+|M}|a!ZrU-d=r+rF_-}^7dM8DVzS5r{|=q<@s!K zuiVFN-n@OATU=%2SvA?A&6_qQxyQv`g_npN!?#z(XKG3jx#m^Y8wXW;&#E}XHYJ_C zoDweV6&pM!*?N}MDibj`YIEW3tK4I4F635Vz(+AAG5igC*bd^kFLFP)JTJYMTU`1$ z_l4|Q>7(4L++uO2hLuxkRql(@2ka-=rQB=yIGYbwntSB!BSeT}o|hB;{K)fIyq;&N z(jvDKD}dU`)KWI(UyWDzn&$9aQ5V)&nERj4v654Ul~LBQiH0=II&6OEr}n7VO*Si_ zsO-Vg+vSl>NViA@TW(O?4E3PA@VI#W=G8Zcb(HaunIDL^JiCI8SAS2$MU!T;uA~qr z=ZePWtiP!bR$-2jhEhT6i)S|jo$5-3Z5bAUWl!;LbM|`lCY!8eW-h+scC3=syuT^S zI_&k``xG%spn>LZSw{N7s1@%Ud~X>}bl{AF5uc-&+I zlMeSegx}pN4B5zW{UJZtCh#A&asFx>H&L$_wY0DQSukJAN72@WwM31b|2?#Csry5^ zmxaRk8s(%{R(-p?fMB_<;trKnvGMq2uWPQnzFAgQEpe0m{ipP5_cN3iop((tBNesA1x}yJR({#){MV=_AJc*7Rrph5oDms!YE{#PH4V zRA|cZG+M+{qko2=`=9ZYZ~6^S-|_z+{`J;Q1AF2)b>p6>Wy$Opi^#^+-{=*(W z$2ZU9TYO*bfrZf!Y4pcfM4~qH%Pp4j$D3EtM64teq5fQa(v;e$S!p@?ML?8RHaVy_w!vbda$g@9*V=P!0HAOWW?{d`WezUo@`RG!_HndPg|y0aI!QVuB3_NqCP zOz6v6`UcWmRsCfw4c06Tc&@)eThvY2_K8j6+P1T1{)>FdDt0B+sXNoTyLOvsUsTuk zI-AH5<>?scXlh6XR|d4B`{ft)0L|9*pTlULk|7PbmBRyRNen1-uyrMd%E8LTYvkC)5&gC z{ebEwUo&-fW>fZ5Y!=?Gk}oZicRQF}B+psHUc&$DBP#X?r+s8e%_EOL|BsLS9Ls}G z-`fX)m?N-wMOrcYo&Mo>c@Fb$rv;|}F z^-8f|JkJWosjN2twFW7UN4l1Em(x}_BE+SrSnhRQ1TE`3D!Y?paO?eBR)UqWw-!w| zaAigE>=`0U&sS*izJa@XT&Z&d&sBemIMP8|e6xPNtb3f&x~%-${B1TjPdnt~u-`DT zzF))f99{91`QfichX_74_x)R;k;_aJs9orN&rY#3kdtaX*R^dLbIF266>YRnx@jZ& z)}D~!Q#$2bPjY;7h*R02QnK&b8)nDHq=Uh(=##a5pAM?Ja;&_uvwrWF}ev!6_ddGTUt|v}K9hI3Zn>)pV zI{d@_IO5yr=x^iQSg(KL4>@PdPxK$pa?YJK_F$t!*&R6-RM&1^t@N+{X#|$oUSUt~ z?&kO~0tbYH3)eyDqq57})M z7x}C-yLnX{VyjJ`IERPLfB7us;`g?`o4V-RM6gBl!-iksO*^-6Z>kGJGx`Go)jxbWD_vO6fRB zIz~yyvC?t0bPSb_A<{8WItEBbKk4W#9lfNZhjerkkKfvxUioWFFraf-TMYfCx49URw-MtrdtD^$idViJ6Y9}7W{(p8mE7a_ye3xYzvqHq>!TRjbhQt5pK4gc%FVgP+HUr0X@%YYe*VAvY(v{h%c+VR>Nm9@9)~Ur&>B^gA=yfQs4ht?*AtrT~oQ6yB8E4I(7E>t54O0<2W5%UELZX z-Y2@*^{E~(C3t7}>ItE$pC9LHf{~6o(ltZrTu=JVK)S9k-6NNNH zFO!}%lb$n{!br29i{()UywYor$Yhf(GdkmCj|<8bZ0Zns=K0}EmsTCv5QQ$^&L1)q z3n=*9F7_OcqCoK)(9^yXA>Q6Ur%x3#A!cGijAKMx+{8E%H75GoH)@jJ~}Ka z3hz)x#KnX~Nnwtf7#F9Sq>LEt7_W?r2#Y3h5wWU>c%@^^M5W`n=t)r#ESQMVUqg;p zhAAURbVT&TxFkmmaN?*i<+zD4j*(&Gq9R8Bq<_zb%JBXu;VAJaQ&8rk{DHE!A8Ojx>gr`NLESsMyE->% z=up3&txX+ktJ;XZ*pFaBEef0kAqxU!__j7WBt11Ddaz4vnYeo`Viva!Qh z0nE*d=Nwcim~L3V!t)|(e5LULclYYzZ@P-MU<(m=M@B}ndYEQ}%SpvV%dAdpC&S>; zomXC)@eJJE-2vlMoIYg=T22*-*l-s=rQj06YH)vp-<@QuEj=n(n0U_@6laviT_bGo zGJcXpF_u|HCyK$Q@VtkXe?{KLSJrspcL8-%jC47P_Z6|{y_o&pBEOF~X26#zEh2hY zzs2j`s7D{vqYn)6Y^PYV*p|nb@wgZqa$3M5SwIIuj(0`-vqAI(uj8i-{tdqW$=0D= zRt|IHWq7EwGQ55ZW%xx`W%zVAWw?jC zGW>pfWq6;C%5WnOWq2yeaTMaI40lBFK=Jj|)_J)9mQerw{9pdz{wx2L|BFRes&g7j z^-&Y4KFaE_7AUn*tWj)GY*ARVn5{79fWkW28lyBpVI52^Z0TndSCrPQsm;nzl=dhc z*+SMRte{kA3m>tvfrS}+|3+BB-xH-bia$y}l>Tf?9vc_S_Etq1$_AF93`ZG>5{AMi zYDS=pK^ey;I-*3QOhk!AiD&g(wm}w35=siIU!$nm`xDk(!}_&Qrm=oHHU@_^_fgnz zJ2s7pZ3f4NarmFC6rivzso31NttjkO&Q288;m1bV zvN?fRnJ26xkPSFwg*aPS<_PO*Kq)~viNY2NX8UNIM=3>N9ZXlm^?nFpU9LA-r!>kP zlzS*_FK=SX}~z4XL$9aX?|aMl?ogg3=6yZL-w@g{{=nnr-2U!bTOhN9oA6 zgh1)cUa+D73R`!Vy~kl2QuaV$gAdqp-fW&`KNR)`b|A_il))%N*<>4(;V2_HElzDE z+$kiS#7chKCD}s(Mp!g$U%23#%5}#3iMe?-4#@=}p6)I5&=TU;M8$rf{LfS5(%Wjf+_B;Tw? z_!|kxJ+(^p&wx|md(-n!Cgf+?% zB&R)+Q-!s?^GMJ(Xv|-+7Wf%SI?jhJ^@wTp#WW*PeUPZderStcXcHvsP$X+*U$j*K z+H3&Y4hh>I9q~E{Z8`*P8;mwa(r!l5Mj&Y$B5Chn?Qb16=At9fH)GL9LYQ@khr!;To)v+0*N~ai5rW=oreI} zg~Yvy#FZ_=IF?{s%P`Ir822jF0ZAKyq}`09eUGH=xe0aMf;w+Q-I1`{5j@s=Fira~ zZP>CHiex>FWbJks(|#0ffJ8llMD;&|wmFA3LXv()lKy&??b(PnMBu5)(WXew5+r9* z1=<>k`2dNT|6H5*WlEe)Vq8EB`XKhEA?|DuXVna2h^I%0qxXm%HR2=_v0;cfutmR9 z^fQM(eS|(tLVr=rM2q|2^JsHhw68h}&lB-?w9k9AL-lIBAJb)s`rBT{_$t}npZL2a zN0N}VfJF2O=EyW9M{MVEqp_bn_)^&Sht z^|U00H!O*5Tx|j-RwQ%16?xR%nj}?P6PQzn_;}cmY4>f2?Si_by1Om$`D{xb?XE}O z$J-HgyZR*aU43F$WKV4K8W1|vfp9Gy$s?{INxI&U&^?VvF%ofFx)ZS-(}bu8HYLSf zn~_Hyoyq&wE~L6ebG*L=F?4H5Y&~2F__iXMfvw4-v2G+Ovkie2ZHW(c$N1Y3+gcq+ zb?1)6C&q(3TI)&P-|j@z4qha4l!6#;;)(5hAheec;qtqXM<2VAq+#6%rQJ!fvmd6v zC$W|FChCknr1+CRd6d?Vyw~kds%H-%oI@Zn{Bsbo?K_yjlOZH?UNCvoDFo9$oWSyt z#3v{W?LUgxUL8%US49$^iDSv5F5}62yC|am5H04_1&C7_=7;DW;)D5`4D+l4%!`UK zZ{aYXa6!Jykh7V{xlAnBWLW69wBrbu%8}x;94TcNfB~??H+~{Yk;3ff(jc;*aQ>S3#MjfA^5}i!LN*$NOGF{m(Ep)krzPeme zoUUxeZ@MmjX>>EkJ<#>3UrVpR*h5d&d6Zu9&N+HMDH=W5qQCSC%mCO7`nP>L7W}bO=poQ%ASqqo!zLwna zvzEm>2i7hKxK>-XVU(56p=Vabf23Q>CYaYLFkV;3XMvYZro|N-mzm1CvRc-*T>c(g zuHVRdvPLpH7pL8JnIj|X``og%FW7j_UUqgygW^Gc4n93)j

      bI~J_^wPCSsSR<~v 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

      N=8dWI3u3r`*&K&0Pn$*=QumXnZ9ycN!`x z8yT9DrSIP0;*^Fc9d&+uy0z6(Lc6IuEfntlnmEUSzK_P+3`Lq>M2h}Jc+O_46Ir3I z9ZbC;kJ@CBYA#;ZJp*$RJm3F*%{4;}Hh$5FPH3d*jhk(GyQ$+FjnakoFBzRH-Vu12 zFe?cBOi-xi66^1AYM|mby=H!?BjH$U`VrrThA?fXF-_k?o%cClV10+D33KShch(wy z={iC;bH1i*ae6G4_l_0J*yakfX;5QJUa3XNR-(f7c;klru_cd;Pw++{43V4QH6^fy zY2A#WHE*~p=uN`i7IK_E*%m*Ei4po6R)E^_C9!^%icL>V&>qFqv!WhWy}K&G^-L10)ZR|op&tenjfu# z}ZseNbZu|8$*KIi+r`_PP(MA-n z*^9kEP0FeqgKJddqa4JMYbQicH|5U@p2R8?K~F@Du_l~^M$70gF%K~a;$UW^*WSzs z#TrAzrk?!Ksnw>ib(6~!Dw?u-;<{=mc6(a@$5cGqfA;PEEA+j&%ApqYWF4#aooY5x zJzKtzzH(pkzr^IDrerS-VeqCEO0Mw5Ai1|0*JC_DxRfptcS=5^Nmp^Y)4ZkMv3#tm)MMoL?6 z*t`OzHChDSwT{{YugKf3t_`?4duu^~ZK!WUtCq53)%(WA?OvkbE8yT-@Ui<#Te_}d zSZdf%8=oApv&uT2)_Z<#V9mTb?Me^h#2bVDvUfUT!u>ouebb!^eC@vuUVqA+dXk@N zD9TE2SpHdqS?GMSF=(Mn5W3yvYCb{O@VNqU8t>XNaBd(hVUWGP?F?19a~{13Ig8xU zz%9;D`c9uhVpqCi3-6rtKe*;xn7{fl``Ko0l zCV;d?Z5G4L3UI4p0jZ8v-9tGFaX<;vh`=HcIz++U`0z$f!cF6(UCW1K*gH|+b`64> z`lqUfe6Q+5_&od_8wvGi!2I!o&=qWysWOhJ7TOfEYJGVLmlMP^3ZDwH7cE8$(C5)mKF!BK-5Z z(eC6jkQ!dxG~NjO&+UY_WReLd3z+m^K;I=*nWnAmxDFR}&XphxKBj4XUM`E%OUCJ& zal|uAUxe?Q%`W@uC{T!AI5g#mm)3(5Fe1==wH$(^fg31T$<`PCM$e9eQ&G#>6sh4L z`U&(v9eYa{Vsc*Ey4Lu_-d2j;(?r-n0iQ=8IIaL8)D(6eVHFW35s61%H=M@`C&MDp z(`1%kpf_81p8pp1E>7c>MQmTX5IKPAtZ{_nR<9}AUqMlX_cB$nabSoc>a-DNqVw(C z0fpdNtX**Bxb9t}QHydVce-{n-{v~?c>nwT8yH*jMy-|kRfwp;;%uN8bq|m(2S?0F zj=iT-#+CK`YPcbKi!gEyLx5o?lskEb@amRnFEyNX0naxP-G6qYh@P zIl}630w)QX_iiFrhp9k;fRa;TK)M~)9)Sd?&e*_*-o;8ZYn9W=ap#NU`9>#B>qVOx z=tuI}s-WD)xiM$FDdHap9i)Dc*cRy~9Dn6DP|vdWnfpk)su z>UKhjIt%pbUX~V|6t+|1O_sk%F3QF7oWS|>8JUVpT6IZb{tH?R>>MWn5>taBZ9O1S z>*%0n4K3a!ap}UGDD_H7AWfrKS!2gRGXABEI4Y{8qhOQP1Qo6~!md%)&YYQPG>0zA z$r`u)Po;@`_2T>eYk2u=tcRqLTL=e-9eiRNM({Vr7s5XgbE*EmX8TzBXDr&q|5?kE z&1zsA9DmWUaMzS}u1wL?xMv=eXTi+ZL^kr=x&}TyPqpE`4U9;wa0^ycKlcXn#$By8 zaxD??bp^l{hC=G-&u%N=`)T8^@hH8RC)oPU5ic6!Md z$vxE3`6b9zBS3qo$;oOk;!QQApzicynOW-=93g7X7@NNO#n?u4_e6TuxtdDh;Q5>V zr?2)9Qsec{yNNFH6G|=G$}2eL=eyn?uT|KJH=$m)+Pbk@6Zymbx8J>Z@we-_4o97z z{VC+sleERJMvvEyeq2+5)&DC+xvRKfE5WOicB9>u!F9ai$wZzawMWi9npkJPoUEe} zo1N7hf82bqGXJ9_yHq?aXR+0+IYH~y+HyZ0V%@Mdv%HIcVesr}Iq3YIdIr3FS&qty zbd>Rm#h*q{n>)Q;T}VDF8v25lt>sQ}zp=(3G|&`N*2;5D2kKMZyR}H_7G26c$qpyy zj``8#CImfB2S#o2f>T?Nmc&w#6sMGvk?JK6dQ^n)ykf&DPt%%|53T+3nE3>QT|;QC z-AHR#Vn~?U2hgL3jbl>&{^H=#{_yo1;FH(!6Y_WW8WR=>E9f&JcC5T0y;HW-fyRiA zjULgYH}iBdgd4Bz`S_mXiN?@L{A5kE5frK2MD?6MU%X%E=onra-{qIBB+}-KVx*Sn zX+`XGRL@Jf9{143?8@~4tim=&7SD9%A;L6oj#*Mm_1w~|(DHJQbyIeub-2s}0qdYjS;VL8m)GjDycIYHGKjrrTL%uHsEy+xk;v2W^HIW;Fb; zn|zRrhSF$166jHnld@)rI^2t|Q3T_Y>OWS?5uR4MP}hq0*KuOK^_O&}W;--Q@qb-S z6s6r@-#3^(fCV%~%$5^Z0A!#z3v(a7w~Ej5i?yKqG31dL#6Q%G?o?2vv-`?TZfMRV z*cy&L*1H*KuQ#6^RYl{(H!ADb%zb!z_^aX082OF5>(P9SY#ON)85z%J==14<2|D>z zqZx@?FrK*cniE#o%w%l;MV*U2=H=`RU@gL6D{k&529#7bs)G2~m?{`Au-UaKlC0Zg zwBIm~bu+A?26r~hEQ0MEf*;yramgi)#%U!cubFY>A-&9KXI=p&@tQV>_gl3}L_REL#d0`Q9n2PI75wx0 zy9@lw!75wLfh+Y={e@q)G0jT^4_u{0L#0oU^N3zn!%4Q9A?&`DD29T_3{gs(FVM1(9$5mD3Pg^{793pCbKmVoTN^#-!2UL|{K zL+tL)pDlH>q>_>BsP!1XSIvfMTWz$_Z5;Yd_mhiaGD%J(qoITcVV-zYZ_UOgX=PED zEfRW(wB(XYFZpKR6J3Muu{rCf*#j)UR@p2c>Q!_bHjC$z{O8HnJGFU1H(y5kztbu1 z4<|F|X$Bi)j817f$FWKain`$pj?j}kvbD%Z4(9L-{`qX&X>QBTMGEbPIYlFj(J<(E z7RBITi1C8D+{YA9WCf}PL?ztgBHt|kv+lCa@LTV<9|qsl%z-;H1v4V)eZx;0Qlsi_ z=_r6yFSUBc%dJN=ILxVIXm1r zN+V|-r47|Yg$hqx$YPlqWhvZ@H3IyqULp85`r!VI8+hxdR3bZK))^S$IQR^G~m zs-FGG5DeeBi8G=$;cz`$>8kcWOYe&($7f@cKvpm1`c%)?|4+K|N!r2{jVHx*E`w^( z6*f|;9 z(r1)RnYIAIeI*ypDiw?PV7QMHse;A zjO(%(ZAm&@88~&Z97G9T3Ql;0aQxp9EEeEg9KvX$pG#?HX zD;W+wv+eEV*=mGA81M2#(pXWD>P61J4jJ45stQv@jC0s1<%l%%_V=)_W(?AIk}uI; z1dg`+!+cVV3XEbR!#M7}r;qHfo|nsT}biT=tsm9 zQ{B${B2xnZIYL*Pt)M#D|A+ z%szYvGoCMgo;G&)zq)^ScJ4nkf8+Ce_a1!p<$p^)e2NKJp(g}1f765fjr@22enM9G z!M%q&4<3H~VCUiebmw3&+nkb0`kFbvRasyP z%@{<u>hH zJ^23M%|Bp`o*cY+4z(svpw_+Q<=(3|2an!K{kN4r> zxBJkyy>GwY=eA&6kG|hKc-BuI?>*alx=&xdfNEa-@ef#zJDB|N-9A3U7Wd%)DDfJ2GD)f??XM5ZxS1d|BqBCq-Q;$%W9#gK7Tkk?(+0d;ABzP`#iRUK^YOsDl!r=hxl zq-pXVwjoe4T$=Oo?rs8Nll2lU^T5RN1-vO?$FOgheS?BRYzpaf7+maay(kohF0f^ zS@w?pPnEz4*iCk6{dSLG!j4VrZ%l)5r#B8v#n^mN%$D94#W znt3*QH{7rT(0TlfOP!XBX|^=qTO{q(!zDG~m8q*Rl{7~cxCg0yEOiuo4)xOL>; z{H;Y)1+8a6d0AAaWqB4hFUw-3ICHoXf6av`$4^)Tt%U9vamS!bOAcB)gnv_%G3H&i z!@zGxfp`lG!U1~gRxhpQlLDUfZ}ox!F7o$9elcWDY{j?Ua^Bo>y`P2)V4AX|5s$K4 zfB9)A9S(_rhC}!Z*J3#Qh`x~UrWk)@N5~HRz&}2AzPTl{*A>hKyD=DLeOr3r)i-~g zFOUN-FMZFr@laK7)dHld2tDvxKi+jXve__HA7SD^DV+4TZyu$Z=&?mzG*TXmG;?XENL*g!JH+%YGptDaL98sMMzQ5k)KrmA}nN!)(0 z^WPtSk`;3rd6_L>_bfrW30BQrIUGmb>81}A9IKph%*+H`X_8$!t-i{NDi2yTzYk{b zvq>@DTb$8N&uvLw!sxI)8jpJQ$w(0|IcnbP$Wm#|&3c5Z(axt?9#aPe=}$;b^9ngV z8Aw;i8vn;tUvKrlTm7%m|33VW|Nh|Xt^Rkb{~h05=GpWnl)r2BzxQ^&eE7iI|3HcN zxBA~-vHo|x26(Fj{*CH@e+63LtseMyuLo9oTGH!kB`-RG$%}8ZDt}8pT|Eos&tQd6{DsAaTN2uQn?mV7)kJOYSgv-y6 zg)tC*{-MA$AzZ4wm{FMUx{ktKn2MSX3&NSFUo(p`4Zp9;` zMh;tR1k`jS)xt7i`Sdy_MYlR)AQTjaSHJ}by0dIqHkmw$skT1K zS{+ZiH^^G>i?sDQXg-}4|6b+hm~n>Fz6b6su#y#1-xTlj83^4uFNx|WB}N1KJOx2O zAC1|2;AD$-hb)(+MfDd4AEwD8^6@?4yy?;aNEl{7CuKRYM)Z1~kBZYG*A`ipt|}2s zvJpX*N=4Y3sR{RNY;JTeID-I$+{!)95dejFmRp}ElzG*gZw2?yIp~vao}PioID22< zRS$)$5e9+eD%K@>sw;MrnulM&QBO?H9OJsX_{`jDnvZv^qxq1h`5K615<47qyS9BW zD!n?#q3Ld5EVMeWk9m7QM-4~{;>>Rv^rJ0)wiEbIi;qbs`DYNE)N%G7mUbTCXa1C6 zA^#ba3y%1$)^Dr--|GLi`v2{Jjr#xTd_tM~E^lIMH0b~De|7Jk@BjVPgRTGfR{y`% z|8MpGTmAp-sA5idhRQpfZ$glD<+MoPh zuo(FLQfDN2kWKT0Ks zN1twE{j~F^#m7{u4ybBJt<7K8sItY>muM-vNsIcRg%*_`AhJNS29ukkf})lK+NtW4 zr{9$q$rQydLdzDtqf~(kO#)6?A^1}}9j+auVh#OzQ59ZASl2O;| z8kKM$@_8CH9oBtqN_g3H$nINf@wG|IxB2ob7#gtEf?+^;(&f>Q(476fvta(mjP;HY zHWpT_qOPb^V?hPUY=^UIHxKpDmf$?OK?gLnjZ;%Q)%P2l0i!+||3zIjk~jXwe**=0 zRR68$ifZ9Mu_AjQ_{#nrQ+5aVwu}zQAcefOu@`-Wz=FLYK(=Uq4sdLNLHuvbMDoAw z*?mOy;hCq6;LLAe$dn-2<-g{`Z;cU(966?#Cwh6F&60aN3P@0yP7%fDf*B$AUkYpx zxL_q>XgI`T2bs+yd9lx&LoBs!9mla)fiu!bNMs1-YZ;Z(>1w8YhB$jm>VEq;Kh0JX zRxa$K_~Et$lr+4^tEC-mz%D)79*DQ5NQZV|il)2A^UGZ-affh}N>U9RCkrLTsI|j7 z)8usy-{Ca_I$WflE0C;SBG1C0a{4+9Cfiheq=8Mv))8`G*m-`rVs1 zFZT)hEu3Zso7GWR3|E*2Rd_KZ$MGN%^3vc~6^YXzlW&?llaQU4&!3_$pYb^yA>I_Ge3dum+0dW!ALx-@` z1oKs{bZpck`qdN}rOXsB7bUX7u9@6!N1ma-E6*i422WSZRW=!l;ziJyQB9~+G;!)L_aH7k~jveDY*v|!&)RjpD0Geu!#j%w~at0+sm=(I|^k@xUL>EAV_ zWxfpZMEp5qU?Mq7$iJUm%s?|_^gIH*faRtdM5CHdPfaoM_2XX zdINW4YW0^*Q(IkD%Y3@|#-Y!s;-PZrNArR?7t*(a^v@hm73vQCa3eS3Pe=GsOrPUh zjt@8-1OGZH5tP2e9l^hbB+!khrxiVrYzQ}JJ&Pp`2ve@FLBi5#TDN`Y`H!6jbglf~ zy4Qr%7pd7wM@vQEOx2T3A|gGNraio(rSYemr7E;F^OL8Y9BbxT1d`B!u{>Am(#3U@Xqr4Fp)7O6TE z3eqWbsF;FCMp3r^sw5yZLgzoZ8goOgyJot)tVl?zyK1)lcS3Kw_6+!CTms&W%&z|h z64|bvG3)KeCb7t(@vQvy^}!5uo}uYDxG)JQc@V^ZgMaiqj}d0 zzeJ}hoN=i~<~B-#SWnmHK{#CNH{K~0%L=Ek+sTHOGlxrd4!EH^TGV8XhVra~Gu)}i zKm1LTrncoO+(q=!0iscZ?)+<>pYgx+&wO^)L4{QZD6T`wH&Yuu!#2wq=I|k-F#&$K zi&~`y0WpU5XewL>``8+hQ6UDei#PV>1XZs~G>-C)EoX3xSvo6PdcFwfNEz2pA7=mW{Io_6}UO?fgO!9&tp?P9aO=uG)l*PQ0$VAE+D>@ycH+S(QCuOl( z0;mLtD|H=+bd~L9=twXrpLp{?N`dJV z?zNwZjm4ql?W^x);A%+Dq|^hdxa-`xi@xULyRupD{`4_LR&QU2Q@5+EHf$UlD`%U= z!wOp%&|acGXBQrgBrKIceP~V*->U`iy&N5YN22hxovhUI%JOHEi|n$}6^gvY4XYs? z`==t?itwDB6qy2VFBl7Cth~;!@x}3)4dyn~T{m4fF9M+xuZ?lSJ2={KW>tWxZL)V>?NjZAAyPM1}m*?f| zOCSDb6mQQR;6^f+)MyTIe{7NSOTHm_0&#Yk$OhStQfr`-PV$)E3?0h-f`A)89Nh##Z-U*^tP4gEpL9@Y%U5r zNPn&zD5UT3K&rXg%|$*oyXYA|h-@B}l1FbI=pyW&&LVeucSYY^4@8N@=GaX>HW_9L zoON-QUzL%MhdG*zVqUYfhHn#HHwEBkYP%WaHsLAPhHR61)?u}+4J2F_$S3V=guY&t+;MW4$)pD{>_;K(n%)eppk|k#3Q0*qT79abfv_ zu|vS{BG29}^3&n)s$K!@0Q!7ut=kots-He?7QblD_XfDWwa9$&ct&7=SQ!5Ks96O* zP=9~4_ZMOLnV(H}(KXh5HIuFvg?fFPul8{6WFvHdLu290>FRCTFLhP0{ddoC@Lfgp3z;v7?gWA-tqu5l33@~unCV!y^!Ke<6Sr$IC;(fML zCne7kV0RdKT^_DTCaw_>5Sa%~az1BH*iv8#0M*rQNivpo1_NTJ2alZ_(nYTBoc0{C z3<*;(-Bd*%oEy&EMN?*3%@(JYNxntmEXPaC;c-M9YK<0tWP@~ zhYQXhfmdX@A2&@6MXjd9dZ2aRnLFiCGmw>x(-fTIw40Heh< z$PXQ-)mnIgefWl%^k}tMpyGGV?(51T$^a<7pMi7TVlf^Ce!NO#WZAqxxtNc1wB!pn zvqV2~H@qn5XlLcyzgNYg^}>|NYCb8mF&PY$KSOiqy>b!|!)xJd0C~OiHP4uX+4jxt zVcht^sn$r2QF2+HFUr-~Ikb-x=T^8>7iyYQ?Hw`C@tX3!$bdEDk8YIDaYBx zNvC&w?7BuM|BS|Q>dU<9m(V}1=<)ICJYD43xZCs0#_Aj)O1BNwm4a-_*$so|0cQjn z`mdM!PwA@l^7+%Now1XK%Q5a)OUsTwPE2m$cBAbYKy_}Lz${lvZ(jY#|uJ5JVaMpQ2RW%&P48ukd9&{R^ zC5#|%yeP7IqXOKVc4IzK9OnjCD?Vyte&jigBl%l?N!WPNYLbENb=Zj2QJlI>*F%gr z)yIKG?3#M+*O8{33jJ)k&8SV?aDFHb3ab>)bc8LGi zcY)6v(;gQ;3P!}`;ieUxOG0}Lj5C9HjZ>1yqe36feIj(c%-({Ns z->8aY4BTq8)Mk05QA$jm7K4>+h~yew*=6+ABci3UE@h`|dJu1`ntS{4uR>Y(Pit0d z!AeO6n<0B5@kAFx-Oe!%*ZB^`DVm?c6Ff);qUC5IU9}Uho{%JEA?}gqSY*Z93P*eF zMD5h6Xo!f0mI3dpdfw8=g6Y5kftjGO6Ww3U;o~WGpx#vrC2sDca=5CM!z5B)e7h9l zxK+J(XrKeFwega_B=_5i3;zJYpSBCp0=g?G%sp#A1(WQ_4y=C~BxvI3uF#o^Z_jv! znQD&O;;d0Hip5kgtc=@;;Hg=2(w#Dpnq}0AEJ);ND(jO;xx@ehnbUhOtHQvGnAQ=A6}XE5RqeVp*e z8q@eZv$wv4Twn`J1nAqi5jzY;2XN@xBm{+e;4vIBW$)hUC3n6_{@?%S|Ng(M%^*JF z43{xfs6h>W0d4v=E-KO#JbqT`pdh^E2Rb&DS`rQjVo(g)fkppSlIxorXEQ|DL%iT> zG>_|blPWdft;F~c!xSjPgQO&F!fQ%~hu5QTC&iCHW(#rZ!2Y6gkrrL~DrkMK8N&N~ zg6j9-)ImRQ)p0gknmQ1h$);YJ@!=V4Nw|S#CmYtUR*YOB&lycg6w^;Lf>h5=Y|R;U zXSiHeLqvduDcN)((5X9FqP)bXuiD<|s+y1QR_@&8A5i-Qj1oQW;1w!xf%CZH$RmFD zgkD9CK>mhK2UTAp^--GE$wYT1wNFD7#T4u_*_=(YoGyYxb^Rfi#8R^{t1U5JQtGlQ zKPyg$NRvp|m)D|Ya`A=g#1up5Sy3*;z}NG#5}+(MX*D{1zg9lVzV6ZFu9l4oEdZ(K z3kPBNu4F19o}@eDOHt&fl%L|+kKv#oOsh)X9@t30vq>?_hae_rqjQujhf_E(v(2fS z=EqcHJh?Bad&=m5L-Hm}xSa3}r?9gH(0GaG>^gaAK9!GTOZI4{KJa?k-M8 zyT@PdJbZX;+`q&rtS}2C{uZpk)tY_vfm^M7Qp?X|uMCxgIxvEHHd<~tIoJ=PQvO;u zrzXwga~vGQi?J#5=Wi4K)=*|8`#-W*q%{yfN3vP2(xCp_w5bt}BMir!!E!2JYl~8o ztYHWKh8+W;I4|Dw;cR8SCE*D;|4>W;bd+3?(uo(a|G%`3gO-YgSs8yp71p}4=Jz%I zj+v(>d@bM)6n-@Ie1=^hbEiF>=oDh7@Wr2zda0OC^A0OQG4@Be)(y+kQ;@IMIj^YV z2^SYyzp-nrvKr8VEz*8yHF#1kP70v4S=~e$mtHpM0B>Rqm~G*?;*E3+L|nUq+RIv! zgjhG0sDXc;U6L8vb_Eon+7bxhHQ$`lc{%TPYQzTRcIDh+J;}oDx3n%gUm#NsNtlmj`fYkB}$v$el$`j`1eadmEiZUlMYA8`e)6h7Vf9@Bt4E zuhhB```z00D{#jc90lGegZI&Kh6jr8ixIXG8*5N*-Dtz^?S_@OBNn#q4wD~7$0p~- znvO^XyHY&1DVhJf?b5SovwWzmX_Y_U<-)Nxy)Q)P(>^DFt2+ApbEz*>RB@g`75n5w zJ@p+3I*C@p%Uy3xMzC!x)HW9CHyjIvd-^vW2jwa5*T$7}ooJ5!hWVhb8%bdYO{`7r zM7g`f{t$F&bnY@lF$0dL-CiWNyeY7HTH6N9Yd~g?pDE+YksD^M3?&wZyVwp3fh5LE zHNbE_z|AZIBvlvy2ZKI@cP3OHY17B!XS$)2ON~b6pa=;jfu4}#1nmVYI33`sznak< zw3TBzA2GBF^F{FHzCzl3V)ec+_%(CNYY*xc0TV`6lSqsDO;f^PpcL7X%!n1%7 z{8{Dsi7v8PHP7U-$%&jQ+dU>P=Uybl21$=hj~f6H@LC!sR}7~86u8G&xn&rFbY*=i zQ}2Q#OGm|gQ7U&`KI#Mu-~6Rk-Oh)~tL@VZeVL8NLu)rV_WCs;46U}F8 zxB-p-V&8c6Y2WBi>!oh*G9vY8$WMHGxfie4^o3T!r}>io_4#8R+n`cW1l#R~Ero0K z95rf+=WfP4n58u%XrVWG%HpzLk=C4S!V)6!5%C}5)R z-Mfc$4+!Hi`9r?<@!tf}z>OYxc#>S81&m}IhJ!4!+A8u%wxF8TxML1pmuvIC*+3HB z)3q@;-n`X?-L##!PaIwI$tO+57xF)P(T?qXi$%>9>o>sRXmFcyG#aK-1#|S1uLkxU zd}`DQc#OwRp+5Pl4eHh?bzK(CQmtkiEY@2t{F_=X8Q`W1CPQtpWNQ{|kx$F_d0WXr zO4oDja8X{-Cl<`KYsLPxuMdydk^|#L_=csT32+w5s^U{DT1855)`g=-eO@iwE!}Gp zYfQIQpQV!D!0O@9gMO==U+s6>Yr)*5*g&_gE3IE_MU>)dU|{M!l}H47G+b=u(%oLD zx73I7@o!*ZPxIyIT=lnVw~9~j8?}rROzi*Id_Fx!}nEPO!;R<>zdxA~v9`CqsBpSS=0jQO82h33s9fNswJ{NVnVJAVG> zuXetEu+9Ix&Hud3|Gdrryv_go`^x_;fsxr_kzJZ!c$qIs{oBe9&AFfD`D#`m%s)G3 zQ5XP2MyPPhlZOxT8J19Q;pRQk$&vZj5aTCIhG_w~mk%hS+sz+M7bGl(2og)5Q!S(RGtWMVD9#63VJ%X#JKC*y*d($(=E@8Lo0mOlm5RF!`VUu`q4Jc0nFl z>9pEjGU)*PgbOjciz@Vjeqj}l4_-fd@#^t#|K*EE-@)zm-h=x)J74cSpaboXe|Wa{ zKZkp-|MC3M!HePReISj;uc6SF559Uxua%qJC>sIW87lkrZY}z$@~sGRZ>kpvm>_@p zA?`z9_vEt!Gldh@RRsxsm0tSDt9a8_O)D^jeo|h{+^m%7bRut?Z8LVtMjOV!8A4ap z<)B}3mrW;}PcRu~HqD2_eiDKjNZT4DK~ULg%3n{)<;%6R&RVwGxtG+Mk*u1(%~c7( z7^L7?d=&`sBml_KK7=tKhSm$;u=uu5Qp?aIl`*C49&ep9WD$2zWs37_4swF-a6i}+4v6WP3QOt9{ZWD#U%ZR zkSvxj@^P3oc_?pS^5w69xQb;yH3T;PP@8*N)pZDkj@)V{`K;c@52N_q8axUY2j9x{ z0ux$Ck}9JoW8rtlLyv}2p}s4HO1@s(mjz~b32Qd>nMyD&aE2t{ovGYYfqWq$g z;e`@)+MAj|s)zM^`>$FaNiP=Jyy{w;+2ah*OvAF_e@ebcE07gHYGOrphtp@lcpgiK zVcY+}MKOCbjjKF8mB8Tur6w^}GOpOfmMv&O)$WG@Fm;;p|-PfaXE<0O{>WjWP#e* zMepWibZV7DBlNEu?kJzC6%9Dl=#!VC%raXo&namiN1+YTyTQslB?)E86d->ua)yb4 zuL_5ZF(p`Xo=-3uU&|CudN;fczSx5n@9VUG#q7O_+tZkBgxtXP2W`#r;WYwxe?5XG z|9c6xJ9td3le^W4&L?UO+|==ZlqSunJu0tKJHeM^ED}p726duWGrFoOY*YbAV2Jq> zyC^D3Y_D8htMrd%B+@g=O6K*6gU(U0u*mNKAEk={>qc(3l52R*+fwbFys?yJ3sHFeVM5vrUNCn=q$>Iwq!>H7G_+C=yu}rMuR-BggaO$LzbhRAe0%`f9+lARkzWFA(f4>KF?Og7A$=&20 z+}i)Y_P)HmZR^KW}7v?P<}BZyAI=KKzWofC;7F@38 z+%y+jZrjSKzA*^^!a$~BoL?K}7uF$`L$NOCRi5EFP)i~kaxzBpjJ>SSa(CZxSAp$@uXmAuyX<)}O&P0@9_jpGJ%)ns77Uw(FB$*qtoB;+W$#@h76GG#r zaOculGW=T;C4EvMn@5U#O8YQKdY2Mnzv%afXimJ)Q0eeDTid2u>v5_IGb(;tDbs>c zl57{qBcS|Ja%N&}g}ex0?*`rnx{qoL@T_oLjVuWO+fN5k=ytKrZdd!yVi4yHY!5Ra zhXWZdnpel)iyNeCA5>!qKR*SPzH%-mOdJ^vwXPwOFa%H7r;|m-t{VdU6kHe8aaqd; z2S>j1M$9{NuN)^9@oofN#ZaN#TUoEsQ3*mq;3;-gpOaVut4_j{PGVUAuM~2hEy7Q+s4Wi5yM`C+0f$RsReiFj(!K~kx=+*fg|mD_I$3xxU% zx9oRli}Z^uYYa8wLmc&#o2k{7Ivl+q;Jw);iZ8@AZoDAGFm^Eq($oS1rS~rQF`{fW9vHzh=JT9;` zAF5*wLz;p{$^`9akWOsksZQz1=EC2y78kEjpyiO7-yogDF@XV5?BaR;Sgh!%=p@Xh zm+L8(3|Zm1QIqP%oWry7%`5nHjRo6Kb zEt*s1g?qI7>Xlo=>A;SC+gw}cp1*7Vc<5G^d=*|y-ST-xr0bl$g6F=S=-5sidImfn zQGej-6;K*K#GHQC|HGr^M_4PL{)dv*Kf<$5+$eYh@kr*bCGRC8wi3B}3_icp_-a2%G0b#{S zXDG_gF+2X$IFbc7jv#qs8eU zwj#BC&|)?^WSjX*zHqF(gKS=c4>ovst4B3Ifh}TpAq^&B2MUL+?+s=mS(sv17?NE! zh_+geNnukqpZ6hUhf)|CV1}|Rm#WwgR9fN1*_1V>vV*cb_~CJlswFM{Tj$<%02Hbv zvuy9}b#@Nk9PYJG+AkGn&3q%*Qv(FB)dDG&MVwIicy4=n?O31L6=Uc|VL0s|&b$Xr zsk8DTe*R$V@cS3}O2rI1TXh!)!`&3J(2a$cBqOhwg0$3oAs$eo@x(Oz5ugi?#5ff0%^;$% zNbYC9M3X^WVv>c0wJ4Ef)G2YRl(I8@nn%{>BtRHrwxMje0$@1)T$;H>wHxubRw zH(bRKMfH0;aMn>)6-(rFZL98VtbVt3UbsubU}&N==f52+74A@oy~AGsVF?!>9+&XE zW>|qq$pA22JhbWc@n5rL6I8X0$g49I3=1%Flpi4a5uS}Or{EUC`yQWsnTgQoISC$#^E?lZb&C2pC5RW8c9Y-Gz4$;A&O_|=%!|* z01cf3=_U~xudCZTW{@$&>?B1_RcGT&tTZ6;r5|1^J)o?7l7@-4r>sDy^K!Oa5VNe~ z^+4%EQkgiPA|b0`8*J{)Tz7hke5(xif89EFWuVKUYYqBNJg(IWK|0wqt3*p20fVe9 zu5$@^X=gtJc?@bQZ0w?dkc8W$V_Q#%1Ix(w0>Ytcf@ zfXX|&$AYNr?Cv5D4ps_N@C8oXznltrb+vJJ{d{o#&3_q^5jdPohMlXZFC;hmfeS^I z)KcF{?sh4I3(>qqeoZ$}V+ewh%qBiHU`=yzz+ozAS9nCm{6BBn+bFQl^xhb z^Ps{)ziA0d37~OcJOycMc&wH;3KzthMMAkY8g)UqCHxgbdXT1%ZO=6S?ips%rurdplww0^2{}CDcAdbtN0sCr?nr>VDFu+@XVgAc>Xsv*SJEU{Yjf5xvq2dHWqS)fHL+eh}N2| zJ)jcuD-0s4jo^|FIyPs8IWi#*f8{zm;ZzkXt&C;fSlsRoqxV!SLZjw6%g}1gy%wQ0 z4&}>GCEezI9DY9zlK1`iYVzXi^w+P`n(KV+c$LE<&fM}AA*b)Z zdAoPAySKaFR%dsZ_JV2Hk(GE^cugw0Anib4H_7A)>N1=Nwz-1=aY*A=cE4_$8c^v9 zBw3vCvZBQg7HCm(?q<8DAY53Ns=$n%qWW^HiIFY`4Krj}ir)vR*rBrMqTjsH4P+1* zA#9Huf2zLFEE6jgaaVsR{60 zVT}zg59u9aj(s`tu-s4 zyFEf=ANaXv%yp*2;Hs6H%8OmyQMp_&u@H?%1c{#$0$Hc2w~A$D+eyMAX1qMw}g|e}OMAepLs`udwvSH9S^M|8kaofBwzsU%vP2&Hp)jeC7&5n4X)* z2QeF{zQG$AJV)~SSUe{n{D^1;@%HLTJ|MLaEXguhrQpEh$tWK!n#54?qWT`0-j|+U z?%5ewP^)TAT{#hUd~bX76h%MnoSpssSFs__C1tOm-nNUPxuf#UqI%$HH;xN6fptnz zD{;v3)7gcm5~;bUJ9Hwf7S4|nz5>@|OBDvABeID<_yyh;Yc4+sRRL#fI;VYNfgx!; zN%6$G-Rnc1L}=ZF8gPJ$5UqORFk6N8I@#!tMg_yB}-Ji$xvl5O#`B&CIGi$fwu|i?G2Y~5EV@M+a1fYD|{?J z9OaV*6Le!g&u-@yxchrgE`eWR1^jyYyZ=7_;dHg}-TBA0=k@iqPc`80et$MwUtPVh zmfsS_XNmu{#Q%91{I73Um-t^x{IC8B85`r6Df+PSmFhgJMyIELr>>Orh|`Gn!<4!cXu^LzL zooD1C0Mkw?f1Rgg;-qQPtK)7*${40(CPMezmtl}xghAGcz)0i+MwZlV4V zBtvH!U~fTo<*qdeG7X&vDLgkoHS8YZBlt=nY!heC9!?qxofMOOhyv^lww(L1Mhzm< zs33DGAQV~ZU4^<#67^(&``SjZ8Qc>+mYL zkqMucgU{_C>!@()>&^~=d5iBmqc{ak31x$*=AnYW+ZG3Ie29l8T2;i|snu^JixMwB(?IVscM>4HHj-#u$cA zKf@M0OyVr=#UoS4#db~P%&1~fn$gi|_f9!)!DAJi6U8YBI%3xR;5HjY7fF!ZNJBoj z)D_1z^c-(0$w(y7Rz$N2IE6@E4K|l%q-)nm?Zq$yh)M!$Qk5qvk?lkV4N{Ry%>?dE z(5#D1LPEhH;gv?p_N&;Dxv1z^B7dEY@-6NgPSLGOyN&&HQ>ml!ce43|4yRj%i`I%$EL{QX5+J!LDFL3~zYQ#} z5i7J%v(`?>sjjqB6&(!?&G+MMS8o2Wj}&^|@*o!FH(MWU-w8odc^?uxg|+*+dxolo zVKQ-FZe`%eI5aW~t`KuD@3urTA<~II%n8U9g5@=+yhhnGQOYE148SBmtzJWA&3b@E z#52mPMtgI>#SFi@&oU|}FN$GLtEswm%RZq4U8X`aeN2dgG_T4!VXN?=2{o(YiS>&O zOgQ2Ix?M@j!C{{;IwqV|Mn0BDCO8V@o?);carBxr9i%rBWUwsGfy;$6sa<9Fk}kwh ze?WtWY9BoP47tV_g#qvzzh0pgqaM7&KR*tKhcLVX9F)5sa4 zUWf%{G7c;ee*7uA6Ht4m{uYxH80`tdWw98|M|ODFE71;Maz5P1Mb-jlvNu=Uyfpw=v4J93M&V6=J;*dYQKH`!EEUpaiuZ1EV>!ghQwcS>l7D zYPiINF!xFjR2Vntw;VI18odB$8K_7R>nSrkD5pIoMHx%?Xwc?@oj&H5Tz%Docpu)N za;GD~swJ)yIy{>fJ{^(NtCceOe#h7r9gfOq=JlwWysS)uk!Q)xmdk;Xm4lq5T(S-S zES5k&MjH=;mLqyvCjs_m3Sc^7{D%ZaHpbaxyBM(BnE4>zT@FwkXBF9pjWTP({Okol41TXF9JRyBOTl(g2S!uCG4BQegzN#oUg)wq1K zis2=TL@P(tNVVNEAV}0JR<4)6!?A5~6iN7z$EH{DEp7F}6FTQ4n>b_aE|?%g^U0dq ze3^QEw6t#5$3@`T1BJ=Op!Czr;5~Vh=VFOdl4+cgig$F=(ac+QdXwdq$1ZrP^SV?` zDa}Q^^t^1jjBs60KKvc?NK_tSdgd4{vE-HZ#V)?ZP!)vs&PhofLud%d!|1)ih?A zrQH}2s{H3@czF-2D@X>Ea`hwkynsb#Ce)8?Yi~ZQisX^3FXj-$+}Jz;RS7L+6gkYY zGQMN{EAU)!xZ#OLdO5XnDvL83;8+xyfl32sV|8t#u4<7LD4^5GI&Y}-K$}2=SqGZV zNVI>FA`NM+V1{oDaCz(r2+it0cFg=HE1QA|^Q z0U__TsRZ>6F$*~f#)H<$Yq#x{s?*VDptjFaX-9*3C#eta`cO-*QKNY11LB6~?t)_i z%!}ddRH8Qa-O}Jgg>zZ*OgefsK5=V>Ei7#u2!NS3rv#(NP|j*hC2wt(Dj&MF+$L73 zc|EHAOD!v3r7#>Ebi;GEVcp?~NFxcKuX{l48VOm{;S|2(I%n0siSF(|Y0}~z%Km-S z$3}3eC@KIBSlUg&Hus_fFB>alcoO4P>yv4aWN5?X0Ss7i>8O+jl$(2%f}-E8 z6{y=Efp!6cVF2IHC91m zD##dXehkfQtpK27>6(TWN_t6aTW0FY3Uk_EvrbL*w16!~JJfgJYLNS=0N+stW6sy9 z=&xV6c+k8=eU8BHd#;vt4eGOj$}wv z9NuFfB+Al5k1zH!!S^TfMvS^&18hi4FhW$T>L(M|k0 zH~XsNPFqqWLZ$vXl}(f6rFIW#`c8v9fA%UaFFR_>7kYh_D#$q2pJMOdg%`&}#y|-w zb+nkdQoT@YGNEP(YC^_VpdYR{WvK;M06IJxb;5FmfUe1au{GgT(gm(>)d5<-<+B&T zPRUKa?-!UAc{aCXX(naFxI!8UYqM6y{}uObEq}?)2OJog*pX2#!U4D(UWNIKN;^Np z2HJ>&3#=;y53JR1$&*{SrJ2r)T{>1R&RyMB9!vJAQ5dCS&`zD7aK&C@e`>)q-0xC_D3=p|GHNnzdk``4Cq-n@*MjEhuTrg;-ecOZ)HT4KeGb zLsBbg4#tU+7sBN{G3>ig8hv^$+;$OH!`aL|2wtYi4e-Byfdih0!M7&PjL3QFtoy5T za~W%`TDUYW*#wQ4g~hiO;%DiMD@l!2iRDBxr)^XkWuM4FCkxW|r0YT|N?o{0J#DB` zazrT2fmU3UGY+M6YGJ*w%?=coEhqvUjPu$RcySxlf*Bnu1BHbpVaBcK@ncL9=dSAF_DNQ@0(n-9-iLSgRPw&JAToeq5-u_>bU zR&2x^h;N1d7v#uE?`EC~n5<-)&K@NdYcIPB(JJe8d;93*$M*IK>CN=-&i2kv1fY)y zqeZiVp2P+~*Or9nU|?H=6N{?2G}%-_i!{`Pps$+*g1XMoZ4UIx%7m5jfR9y>5}ZE^ zh_VvC1v4910p?X|IDiw+OP~V0`mx}(9@nHWEHWTSo?rvHKXWE05bX$rY;=c7+NLZp z5aT$_&<#WI^6RU{Wa&Yvn4&K=0=fQ`5pI*l6MtK3k|;eiS|!dIMQ$DTM00V*O6m*B z`SyEkX;_RROF0{1K&FW_Mdy%O#ew?-uV)O`MM#aHWaJ+pggN#rBgOMBUA4-GWDWwp zsD4%X92RZ@uw)}FAFOo>u&~99pqCdJ?JIrY^eEyvZ3N3FbPlI|@7cR$25(`Zi~M&f!Jv2%^JR}awY*eVW2&Cq0D>7ZJNljhXc#OKB7=vo2%RRWLHVljzP@Mhqkb?o_Wgubdw*UbJNK)LTT4 zI8U~aBJ)*xD{n$~83om37$eev<%$t z1xNAXwOi*5b!WY9+tAhK=G--MZq#!oc8h2IR#uuOJ!=3&3zC~@#^YEpg>pMG0iZo9 zqru{hf?bX$>*a`E#?;A#13HG5+~6zV)&nG9(1+IF@|Pa*WSQp(_oVSF`-?9pPm&yis?L9 zN+Vi=Ji`o!hi*<67t+}u{Q@)8?P8f-!%dNDA$S)oK+Emn^<^+iEtNCl9{`i8Onz(^ zr-4pz8BC|)BxRip20X@NL!w?NLwxe>RPuzS{ObiX0AXVH%|Z!iKmxqj6r9#6>@s%= z`iHnIW^ppkG5~II(Yjt~nl5-KtN~EA2o78D+DS-qQ~!Q^2MunsbjpfM2X&JtgiL4H z=G}x4N8SQM2s%5WvtUAestk}7{7Cn_4l%LLi?yhgk#`6;)IvK~*QQ31Fl?Z??3 zdPL2u#By;$7$A7SFUg@Q&!UzabIlis(61#ohMv1`4v*$93kHm`0v_K*i!Ys)V(Ex| ztQD-1ni_*%UZ0vo&*fc*D_N{XBP*8vanPZb$!vn)H=NqTwFLr&Ojf(N@fC|6K> z#;WBFP1Ipr(6&{^gHkOs*G;~8;6vJ9uyt0O#CZ*oDb+hH=m8o^4YJyaT7!}%9*gJc zypd9u>r{UE4Xm8KxFF4!y73tN2Q;;;U5D@@&7HhJZ|~bUs>vD?2=+HFt^Ek!JY@A; z*F1+A+ARzD_?V=eJ$`@slJV!8^T;u~`rL&YOB{s->9DT|F*T?3^f7jD ziG>O^;b+LsZGfXq?aj#zxabdaM~^kEpe^a}*w_w3IB4NS$Q{s|qKX9hej@i)I2%bO z5Nr^Z2<-;a41i?^TpUU`QS>4(8~11FjD2CdpJAtRPRqA5wN*epDD;%@vQ4lzZg!_P zO-#`kMejq0z?8bg^uf4-Be~#)fY)KQ6?el>hhP}3Igk++q3=R$+CYdpMw|j>jW9+H z)bxOChdn(zF`eh<3`Wp^^RM~&=-zVc2`B88g&)^xsR z`%KgfHci8NIcp{A!+Ddf(us0RmasT8IbFsQ^?e!tVTu1<5&vQ1`7-{)GX6t<R-VHy8n8UJA!|6v*b;omF%Lj`nw9_jwCDeN%` zWG^@r^#h>)(9mg?j7HIgKMj&pMQi@s48o@h38XViBK%>Z=|?eu+ZNIAEgXCs#(sAK zkl_98WH_U!i?2^k4h>ZMS8;L?_4^RtVL#4Zfj4Pg+T9tzr~kMuwnEEs%K(&vX|(d;xog=K zupbs{zPPrcn>W887!ApmMg6K|5}ZS(#NP#=E1E$L#_E1t^`Y^fB-fX&d|dmq@^Rg` z(J|YIF7Q_Q`AZZzbgCJ)t{W?t8q+YjsKZ{aOVnz-?H9-AY?MU+Yf*Olzm3>6i1^96 z@iJWHV}-O&sy`jr0A})jci7P0DwIIEa)3r)LJ4LkF?MZ8e-7b$TWJt884!#^zW(Pz z&>(onuE~wjDmC^^r=ajhEb`n%`nH4d ze>s{nMuV0mK;OqI+zEC9xT#`O<=CM41afdd9yDUZY)ZGc^@@e#kAb{4QwGeNX#R!nIHrM}SE!4&`T zSRAqD@R%b~GCzv}{n}Sdg)75CfSBMm85JUn@wX>QiJk9Lj`KThO4&H$=}S)-Zn}@%P+oJnjXL zfAx$OmsP&5{6H`({&o5T?%1#UF!o-_DTgPLah3!(7aBDEZEvEj7PVeH>zf4%*Nod;>9E7 z?Gh>9vwzmi;C4Bg+ev4U1W*GhheAG*lEsX_1}3e|X5JVhKzm+nXapGYPznwhG5cm< z4hDYp7l_Mdfyx^WPTVRoUpS`MIeA|~%CpQS*>Rq$bfHLTTt*d|EiNGZR*okwmo-=b zpbvfrUX<}7_Y$JX-B6wUmApS0_XCys_{nDR3N(g}IVPRJY@ME4Y=e}A?5)KWoB}u( z2vH5te7SJYSmz0zeWe7r>NvFOrruRm{ig<4Q)P|4c-T0?l-K~JIP@sHnA2bow%iq` zU}vkPz(5xHVW? z;;+t53bDM7%*Jd3ui$hJZX5Cz%pX;;7s%>Ba1z*T;rkYrS@c20orx6C-0`1$|(s8{3sDK6mS~=+z=zbP$T5H zi|y}Ix>^~Y^G+OsnIT{|-XH|*WXR!X@Z`aB8^-VQW+-SXggZ{a0xvD|sAqXt@R20% zBtCcokzJE0x8>r^qg%sSF7i%tKl_z*ql|D=VRq@(Kr)P^0?=CYv{nWQ@Pu&N$vm1g zNWe(bb)a;G0(1t@l@_exsM0Yj##*`?gmV0+#*rusixPB*la+avBSllih#s%Hge1(f z==`n)hE7#`G8`F{vxB!LokZ5$p(c4rt=d1nbO9=BuW1rCV68+SAP|!to{(iwXf)s+ zNGu+?Cb3<20DqI|OULKg>ZJmuTVclPOc^}wP zFpLhl=d{#Vow~+zvHeyMHOF6F09(OcJ>dRk45|iKkSH1X)ne3phc$D1u3OMY)WibH z&L)8DY_6`?nsbH&bXSITKd$lU|bpfkuD8k#iwajyv}o6)2nUU!|#Xh_OrK|t62wP*e+ z(t8zs=N=-by7W;k@>$+=z6W^#ZieGt>Q8U7%XrdIiQ;6q5>8eC>mkkX!uUzPfEV4) zxh=EG6nWuwfc8WbiCvXKp23+j6lXNrKN*Zl81yccT7u#cQe?frnS@tL0tW(szA}NT z5diz4KtoMy>gMI7J$)*p#yx#Xaz0$)2{|8{dK1rl?V*uVxn_oJenSyd27JsRajm>A zP*(*pC8QV(7&wbUNdo^lg8+{du~Hn_z<(N?j7c99zlQCR=K?&^VbeqBQcGqp8dA=>EBkGSE$VQ6659K zRfCrY^7Wudt9#VR3!Jg1Ur- z^#~Q?{4cIQ_-xI=t*U`Ca`pw(1G>?luOjHY5|bsy=;pOxK9oH-4*%^&S+vxV9lr%aDXLpz^E_orwoC> z?Jmh))>Id6b=`VxjtCdndJqPK471*)g0j_SbW+Afm@YRZV}e`##O0kc+q2tItl0tF z9-ld-K5;JNE3oZW&=*#sI?`G%h02@Sw|zU+BlJ+vp4KnUAZ#OAKHVEV=0T;YnW=L;rUaPC;Iq zWMV*7L<>nfWcxWk1px@i=3E+ZFf~ghjEtZ)c)qH#*tw7GSH;4h_|Kr{nSqWbW3fTi zc7nz_P*rvnqouB($2ZasP&LM9fVm{i2&ONIXG7Y*V#V`e1hFdwbBx>@?(FCy36dK? zhQEaC;3g27X4l7%gpy{aU06y~fJ|^AggXvnsFP6>ptj12v=zPRZ481+4?Qc$P=2C_ z%LPWto2g)?oL`U|ba&7;Iy6EMHb6GRu)OEi{@v$6LYRtfz4)BW)rNiQ61g5kdzIC;*o4%7G`dz1Z_J2idSnVM z#n5{~cm=Gxiq1@F6LpDr$isz!1)&E4cx$F$StsUVD0bq60?lSg%+nWR%!v3(D}@7& zPhF?n&^`u~9!J-Fm4VVBd_l3BIuS;hqDiDOXGMVXz4$`9^A`x*mshB(2@o8kI70%k zs;-v}I-Xqt`nCjWSyk87G|b7Mp$&}4+H5+cMSo@m>w}`%`AV`dJk0eni?@P8)bJCu z%{wssWE>alatz~-QbRMZMv-~whM>sPYImvpit$|WPXow}t9XJ|nbn>&Jgdb0zcTil z=L-5iik)|HGU|iTryzG|1?0YpW34|tNUCRU0&*HTC4;|Neh&4DxXa8{Dx~L5Y59ec zS#EKBdAoUQ>(4eex4?|~q96=E^Hvm0|os>%cy1zdTT%r)G>2Q=#IYa<~MmAih5hVH(kH((7{ za2N&cVlF47lP>LfCdKZnjyk3G0Pdv{w=wVw%w7E1oC)v3T)lAnl(9Qemkfxz*0tKi zx~c04-XYFR(7iDP&CS`rNc5H*)8$q5wsVp$I@UK z!xvH*;5Vkn?mTH(x$ALasf5Vi7!vL-+%$=Wg?fuq=*M{>7%=SOmU7VdjQw6WvSR1Y zYeigj*?+B62XQz^JE;t^!H{z1Smle6Lwb85YVh+#;4&e!InpKLB;-@^^qc8;N($q` z&gD8G%~%0M7l$7~=`oIdQmPv9q>>|_TZ$Y?IZ=_ow|wZWvj=yy*-%`DHX0y7EL2Bm zWK?rfw{Fjx(@@ZDHfks!!FK$g3DkzIWojByUuseGuRzm)FZk7SJT(%w(Q zAn9FtiOa{Z_d_$7PHR6Dk7MtL!|$X1i__J{cjq71p4ZpcKGpCcR`lX@yYYWP;}`Mv z%s*?Kb?A&qW>2&5h^s19Oj&$*C29kqOY)A13i^3P>wn?yY3l zMXI$2BPYO+6O^7{(U7DlVT5^m(gm56h1v4QA3r%yK7M*Ec^oS)o{kh~sKLUpUqxf7 zgW7b?J+Gj->-py>6Ev|=`B#Q?(o_Ndiqu@mA371m_A4euHoRGicpx=qfOa6h`Xvp* zbg)#DXdF#~EKX!%>tR_gY#k;oG|Ci0ISu`)gje`vLsLsbS=srHzVh=sa63ZUU?D%} zoqvtR2a~&yj<;p#dT%}GIH00lm-xVziItu2Jz8yVnMGQy3^Pji}_8 zZj}g2cOW2EMPsxretWW0D$|mdMrmg{kX{O|^BPRVrgMv!jR=oMBhAov_)4M7u|CJt zS6&8baiaBWbzRTsXju{vBCMExDy@!J!H76HFxj=Ns78zolV0t$P(-*A%v~f7b_AY^ zG3FQEAJNUI9uslEb`>U?aiDXh8ryD>y=WC{n?KmE%;?PGY-ZI>8vPQs)>hXy3PF1Q z<>w^io+{A~?8}9);r?|R!nK(A3xB|Fe>xeuehxcB42&T(PJtVO-0R|Zb_IsOYg@Rp zVesy!Mxo~2uGr3X2QKWh3`~&oFlus2;vCn_^T7FyyAzE(SXjMN3}~c|R52R2>3nd+ zSeT2(U~LCT%@lNT0a<22QRc^>&!uyBDE1>cDbb@YUgWc&Bc4LTy7yL+(6TRfmu#0{ zgYUfDr$sv_M!Bh$E*BM^NQWyttLP-U7|oI?TnSD=Ck#QMN)05jiJoyZgG)*{z6kp( zRAsud>QBkuzMx|v8joYuiNtE^z;Jo+Pi-M#=JB0&%44euJcTX!MefC1Gx;rQgeH!M zN!0$O?Yv-HD=30hAT$EY^zFtno|PKXkE`Mo`~-jK$tVBg+NU3opca8<=?Br=T5a1cys z)pSpN61$ZxOfM-t9#}YfWX+^`M&CKU1Pi3u)S){~44Q>V-hTepzwDNK<4y`1%aYnI z{H1Q49{)nHC~09{5~#Is!iLw81wyIJ-HYL^`msKfH3&YM{?VGV$82GZQ(3d*!A}-8 zI@SR$uIN_!N%`)iX}MQB+_L!4_B~w=dv-olY$ZhO`>P-sri<<0wkYUddG{`65>T{P z0cUN~NWfGOS6K#7(7FPo^W2zqiL}biZNyv?s80Vus1mG=e_Eo{R*n0OXwrWQ{a_bk zcIuwz#K2lqq6(*^#yl^0F#HtWl=&_IA3XQ^9r!)QAD;t9$*q;OqY^Fg;KyR#OvU_p z-`rxqmAQuOIjb%owC|evUZnT31=>^vNd_Q;g5&Pi28J&5DA^rH#wa^KHl=5cg*I-T zG$zb=J{YM2a7Mpj4MSkUCo7XqdaD%%{2`BZK1R$Tt1&E`F;}1Pm!2*;v$?(uYYe?#}DdJ-hau8lYc0L@-hO*nmnY3!4kjEb) zjA;-VV%C8AEX!_VPVET6{s?-65(sX95zX%85Nh&YK z1LbJRSL34M5JOw#)eZpkpVA5@%zGqS+E5t7bxp{LgbbilvMP{zCks9XPk;jKKONec z1wLaN!x2L{e=KFXd?*SCM?e)ZhLBEaEaE~)kBOy;R9U1-)F3YyY(CGacF-c??L>@= z#U<8r%BB*okxwr(a!O@ajI@h)hBMvX6>%mk944fQ^z#I>^gbTYU>sAqgGPLyHXLDigih6WjmPsPOv+L04Kb6 z=e5KlPeh{fq}q&+4Fsr}qemq|g(jGC1|=DhbfdaS`Q{WUX1+oBv7%;G4i@E^Gg@zg z>u5Y1L)x`ftEGi#IQB@EodV{z%kdX_Ah+qw8-9@+(jD?bOL_fL8b-|!`w3!@;I)Ij z{T@m@TTDJXX0mN=vHQqp;1>W(VgTuGR~bp;%s7dDF>NzT2uCy#Tv0;&!$LwIobrMY z^rAroP6{#ZTRFd=K{nPVS2qeLLy;^VR4!uZpS%m8Ob{ItJ6$fo_7U2ohL{5>jX!mk#JeLKZT~sKepF z?9s6VQOb;h=xGR`Ze(Ldx+#pXf(g#cdNij)<>kY7T}Qr5d0H({DkoLC2`L_x?2c1c za4VQF%WS9=gCVP^MyF6U=|@Gk3*<&Cic)r9s7~M?R&$f^Lln3Q7yo^2WAoYnbFP>1-+%MZ`nod)Gqu** z*6POA*0zP7&g zJh%Th*Vo1VUtRA1-~98$X*_K>1V#%%1_zCA;Rzg^c))?E^=9|P*^7E1;G%ls2Og_C z@$@E%hL@S+^=i(#n6t+E>iVh>(x=nx7f5latRsMZ06FsF27qfOi2XT8LR5eDF2(r> zTKE`3+=593T=&KoGLCTY)uW!MN@+^dc#vHo$P;+EXbGzy_mIaYA)6#04@BO_$DHFa z1d>EEjbKE|h|rPml;?gD!kt2oV!I28d^AI?fqacx}ytm7)$_bt4*5(J0ZC6~t=?)b)4@NRF({mhG!E)C!rL zWIRk*@zo`Pzdbp{p6$wX92mVSK*LW^~xtB`m?_Kgw~2w;*T$@tQo2N&^&5c@-u z3}KLaA>)^Unp96Nze+Cy!Zx5$Lxqp%n$aZy?!N&4M6@ag3>OK@Z*~>61}CrE&hf#k zlXu%kZD;q`IXpV}dG}@erSo|ESp0rmci!!uygqn);)oPS+xsVfb`D-S+xvfZe%jrC zS$EohIy`D0AAj}e;KgY$KAc%lRt}jdbNA9FG_V@iBh+n!|kJ!-JQ34+egmf+oQvSB!kRIQ;X`?jK&C zIIj=(Ube-p4oxSbdH+ARb_M7cLv@zF#DCX#^N05#h=)8N~hG$UY zw)p?f$?m~E^knB?|Kv#gtc#8wov6I;c8}Y2XZvXPSTt7j?&v^N0Anh$9AGIT<9?fp zf$?^%X%R`__uJ#P(VCa-?LAT2G2}K=`%F*1dPE45cuOpid~A#Px>LY8KuAo$@9IR8 z4^bAXQqX~)3X!h%@?9V{@dp61=J(_k(2)Y=m*uvW5f!rD@+IjyAF>^ zcD<;;t$WPQ6ptM`Se2{LZ;T5heK-{ET|iMLos?{YJ_jPGv}_O)7bt%&S`ZeHD?N`H zR4fuy4)gJcibYP$R?YzpS_Cslm%97X!gVRXbF;G;rdrcDMPj^IT{5)+)=~x4LI5m| zE3=U9trAnEOg7f|DfEt_0#pN!n5jykM`T-aY3(NJ2Bz5@a0^xO9T73i?F+aVWT}i( z0^i~9?~+eyq~V$TE}p-zOqb`m`LGV;HYg2p-BP9vK_;R9By=AYLsvpfMM@DizJKy{ zx*)>G!?;Z8Rk^Us)Es9ileoSCeQ4rI8ehAW%8oIEa+b$JM7zkZ?6<~ZBMMJVcueI!1rbe`3k9`xa6@|jSo>QmLq3&HU)B( zK0vW_y)Fw>H83Eppyf=BORD-g76O}c6*j8`y9n(sY*j4F)&6o7%!Xt$Xk8-XAyydt z)i`gq3x6pn9Pun-Q(#V>bj-`sgmrND;X96Vd0B-)nF}WOls(j?$9y4o5ms)7VA{Se zr~uW)h~zetB*w)?XM=&tr&7Uo6dIirMRZ0U>jDy;94o69cqj%8EmbiPlTnVy)Un>@ z3dBLSN`oi!O0(YQyT;RJ6==+<%rws$dZ;1pPTk?1Y{1FOP|Ar#pto{JHX}n$ru2qn z_nn6W%&#BFnSVBiIb)~L4s5L3nMd;oHqR(1g2arl2hUxkZxUD;}S&MQ-VoLsae6Vk!MSUg2kSxw+%mA$ju9M-WI3f{%C_-i^8UY+}!G;u>O^pKkIGDtf zn{o6@*ndnWLePBC!rhlCs#>jMGG~*Zqlz&rpzX(;z36>-6{T$4E{+VAg$e^B135+E z2eMJ=!5C=?sp5#&=&W*z5vi4QY4DW}Z>~=19u_}z3pxiIP&EINm3Z!U2Q{^D=V643RoIOt9Xoa^3%f0^OE3LMc#a|S_(5DX1G=Fnc zenddf!wXWVUw2abJ&bNmv2qWgTB~TOoQcIWl#+q5m29O=d0YkdiyVQ%hTsyQ8IKno zT|Frafn=c)gWAuB2J}E$V>9p`89dS%Ud=75yQq9%_yI)e;2#AKeq^VIKuakodF>nEj z|G`xbpJrhIA7ca-^TUeeU9y9@ES`s%IBKwrlOy=5wCFL?1jA4{bU^o#_&OSc;6|H@ z+C(Pv)oDq<>4ls+7kM}e#X7z(W)4qk(a4>rSH*b~^SokBa;e`oPifwWl{O0U8TM_e zXXdJv#)YO3GA5cGq6EUp%VntI?UinPua$!F@JJ(8A#kt|FbJAdaT2i_Wofwe?_~e^ z>{-$Nb8Tt=`9aElC)ZZX z&wpRr+z`o~&E@`I+W#--|BKoGBmLjnSla(D>Ay|?4}bbYV|V}6!Ea*!FE;x|p8jub zKHpf{|9^?kn=lJNrfU35uJ=u6-Cz6a(LUZC>9z3HBlRAa$ybk#XX9~@+%&-u8UuMc zR|%*TH716ul7%EOz(U5OTfYN0!~aC*bp(=X9fj)ETc z}=1G)gA?nMh zYQL8GMf~9c?4aa^qUn#1{Atrr*K*Jo^ZAIqRH%fWgPSlj&rWw6tLN8 zjGbvTRSj>vEv0w)V&6rRetebYi*y=yFYo}=!*IV1djoE8tRcb?V)UeclJR-0`*VtI zJ%9Y*1H}cGm0P*q`r(Ig6i?ysloY8B#uC<$-FvA1%MmB1(;ESx$YO`Sox9g}s$W(Q zw!hFw$eSVOP@MLHtQh0{_rj-i(+i@vAMA`A56G>x0&8XI4@ZKUqahK4)(ComOG&mJ zRwoq$3mDP%%(>D_c*-qyK12be6$C%{ApXEr0l#y^8W(>O#|!LHvSlw?7%c zmoV$qB%+nzylcde^jteq17(@-RdK^#mz1!6f!V!QCO`KHldro?jX5rXW z{6FP+qrBWUo=SfUo^~l@h9an2rGxpuGq6r;Uk7kqP;p>2=C9joTEz}uy!NV6ks;ih z8@f#M?NxPROn6ps=;7b``2(q$IwH z#DNMy_}XdLznr zr((gna|)*mE0vS2R1liO6&)M8wd#3|uo6ABwXy`h*fk~5kM*}71`qvp(@6 z60Qp6T7>9N()*EjbXA#xJwmHgO67~jd;9|~J_pft%Y9KjX1@pD1x}wIF1;3E&b(fB zUAj4NzQoIg<_N00ZTuMn)fq&G#h_y2r<*jD&%9)Jh*gk zI&b%X+CO-=|J9=%Abfy1C#3V21_MD>>5f>so5b4pXXr=2B3_^|q?u9T35e7O$Ggrp zT(pA;VzX(&4cK{hVf%w)`Sz?I!Bsq@<`$s!fth$8gc&htd%4q8buk_bVL$W+MtY0=S2(U zE=vc~*rCIs0kMsnh?}}{-AK|5RX%|5nl{?kSrRmf=PEviE&A_oXt2EhS>FFF?|+u} zKXdPYjt|}*?X-{m>^gh+`=8ZktDAZJr{~X}uP*O@zQkv5cc;C7+IP7QRAez|Ef{UK@Q+n~5&!$sY zZq_4=6KFnw=sulM3_ub3=|#|c9}foRBM2vIrI9kA`G_Pr^GQ4s;;#K<0?b2)6)IMd z1lVdniT@f|4*){OdH{Zi))T<0vVM;OyLUq&A%M}X`3OC!j0b3rY5a~vtB34ZXFo@C zOY=zv{BN#JWEg8dnG_0>;n8fzPq?bWr4LVuES`Q+)Iy4_;VKBCT3z;*Q z4I_@5zcGZ4@c>!WPI0+Ppl$P!#O-@D?;3r?gz8dbghcFTGT{Jqp(l+q+S0R-PjD_?_pq3T^P9?p4( zv|`?Sh+Sh(b}mIc>c>};bWVyStbAD{F4pVIa5Sxa`5_9g@-MR>eQyotl3`lPe@ppq zDgP}$|4H&+*}?yC@*mg$+WUWfZF6IDDgS+m&vL6T&;RB5zdZk!=l}BjuRQ-vdH&(` zzpHCox&6Psy0*H$JpaGMCx~KD9e)4Jf9|i=;U;xU?EgonaQk`wD4kwLlj~M%&HvWl zfh^ z{Lg}ao%8@pe6{8I|0U1=CH~t#?)=Z=zkR#ruRh=WZgX=<1O6k=e=_jx4TcZ9|L30n zo9ns#|7>~x|AjuM;c(aiCC~X+kFsFcxd6jrr{&PEK4I>!0W$I&^H&`-l4 z>0Nq>`C=|@@AS-leD0n81^zs(;V-wYS_E;%{Wov-PImWp_uDmpn8dTG zw}y?21{gz%3*>r}08proW)q;H7-G(I6)atf8N^V!vH*VNhm(E^-Sa#*z$l0CPttR1 z5*p))L*ZF8#vK3=pi{ha5u^kN1FjnYngQtgl+BKgC+Bi@F->9!&81&WA(E1M?jPbX zpSr^cqb(-khsG#M@q43z<-^z4?d_NDdA%BRg@Vjv{dktO%+hxc+YsGJFc?YxxdGiF za@8G?L^N%RU?Q_g)DsM-2`D1QP$bLYhsZJdrUftkeh3IEo;%A1jc;AC%CaPynuXln zd2~;s-lvyh4F5%X?|w6Moo{X*1dfW>#y@&77o>*Q1@j|#uN&rCwn%1}Pq)%8sXCQmqa+gva`p)t zjik?`a7y}g)I|}RLYJ|5mY?Nk z`B{FJpXF!yS$>wEewLr*XZcxvmY?Nk`B{FJpXF!yS$>wEe*Q6^{}24_ J=(qsj9{}75sI341 diff --git a/discord.py-1.5.2.tar.gz b/discord.py-1.5.2.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..df0732cccac17a0d0b0f878dde24c204773b8d80 GIT binary patch literal 649426 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/Q@d9Q`4-0o)w7tj34MYxDAsHT3*DWV&mBT3N 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

      1!ba;}Hg5IgWP`v9i@Qr%<+n_2AY)yvA5<(J*e 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 mVFvCv_D6N36TPC87JPG-A6Q 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

      gdXl9 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ⅇ1p+pdQ)u;Sc{UHl6K@KMEPq0RP}} zcW9eGC%`PR0pcnn@u!~6DYRbuflN_FQfwZ)8C>JMyWju~!_1>*x|qHtU$FUZH-7>h57?Mb+3(=d+f z^xM;q&T7FcCqDlcTxs8=q#ai6iOzWSEVwC+;TcaqfmuT!V)tlGGbZeNhngeaaI*k(59_e0?Pb^Gs3(3;5rf7WWDDS6GrBjYKHCj1N!E2LVqkaxRR)(E( zOG1_hKE`h(T@nJgv3dxS(J$N9AJ}Hjo_nhu%bRebId+;1yf^#rI$VGJ4j!&8+3$6@ z#CYI*b&7*^!wj%<;X&(2Ny3TTw`}_|`3HMB;Kv$Yf%FfC6eht)Z%=Ru09^iBMeSq@}&pqr_ zqX_j-yi|?7o~c|ks*iq1svKi*4Sb!mh6hyJE6{$U2v%1&(Xleuc4nCeTAU>=L%l|C zZ^$v+N71d4y`_v1%VN7!Wdo%f# z7Pb%XOZunI1^-i5%K!8o^h)%IIzK~yN_gLd`RlI**XiNScYa7LVC@p}9HxAUqL%H1 z`XYYrE^JRMmp!8sVnR?C@Xr(i4;=69v}ixR_`NKqh_^*kN&imKUu~V>SftM*^qKfT zlAodXHT;2Gd@&(=7thN113CCCp5{+X?;7%PZgsb^(ri(oYck~@OW(TDM_0T8Mv7cU z|A_u2|2u!tD{hhgqCb)TqEExXyA02xq=-&)3zaaiPwJ!53+66RC@oaMLDa>*?M`}s zk{!KNmK8DC0!9-#`J%gd;9RHs%v39CvyS=4(>sx8a0*J&h49#v^Ue{wwWfj!DJw92}DUG z)QobyluNJlDXkgBnhD^^D7%LfxUIH$FMizCdtcjn8~VzZie@GW`6A#XK{$tm5HZyZ zgM3;tiIKc(t$k)PQ2V^U_j&*M=|g7EoPGAkT5GSp*52zw+{)#d3o80sV325!m9Oy5 z=ohE1=0CZB^|HbmXDJ7Tvea3MMQxd+$~1ANeF>E|3_5Mw$0IPzQnkng$+PAWfg}7u zx~I}v%9gfUY%`(r#VAFiR{l*)nGNrzvqs!aP-o<0&_=NeZy9G-CzN~zq%oPs(95>` zYnqwgR^3beI5e}LOn0AZYxIQKuUh!KV<+y2x;hKxZ)vvqOqnYY^_D6zNp%pERL3_x zk)NJ(8PtjU&}zQcnaP^}auPIby%XrcJ`{FE_)jL4wa!vWIVaONYnLa7qJ2-ISD8i` z%FR}7n%TH&^8jj}-ba=t>?!nLpq5;bL|ZUVd&j~_9K8Q`D#q%D>1%c0h~oi@w)i&5 zJ%GRBd|Jl_?l4@PK!LnhIVJT*fvtF3*j|9rm@XZ6bxUFQad+ox0D_%0A7{-Gn1+2d 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�JhjeX~FdYcGA@_0J}{@M@@mFG(j4>V>;w} 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≪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 z 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�ggG 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 ie1wA4?^Yn9m_%khyPj;a-~ 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�|h-|zj*;OI+c?*30G=$_$! zJXbP0LO-iW{9at5fX0SfBlzhGlE>Oym`y6B5MTF2by^%K5-*zW# zswTdpwW^-#k9Vw94MDT2(uX@owf4JaN)DJpi|MU~TqkgM5Q5fO`2;FO9;0tNs_Pw~sYbp*BLFyE2i&V*lRGpin$0?(_j~Bg+7EnSd$r4B zb;t6pG{f#Swm)#)ZR3FjHH}5A=<8b0eo$AWw&rDTYS{`j++F87zAm06ZsW(Il^1e= z2fxW%d}9t8$~6y*!~XYy?U5P3AL(26`?472B0yCexOl%G)%Z%Z5QF%NSvhCKvebc| zhf0viXQ+@Jp03b-%Ni=R`o22Y(j|ln1Va5{-IO)4s>R(VSqgu2RmonBhXkG8H9;`K`oN%eN%DUxu zNdG}?JGa064;kA3;i4H_dd~zWUugX){OsNmZ09`LdwFmxD$hy?LNH(+n5vBfn1MS^ za%&E-$zJ;#v$d>JL&WLt+pNm>sB`tEBcUMI~32=Nn}UU#F>IJ))vr-R_`=4+?^U@AgP%X&nL-cTG2O-RZukAJhuGsPc}^T9pbRxaCdr25%aS z1t?~&yvS9iPOr_6pqxd*n%cLRk6Zl5E&k)L5C0LB*W0bR#eZZ#-=81<@!ppYA3S^* z;6HwSZ;Su9#edx5KW_0KxA>30Z~R9xhZrvo4MI3Z4-@Fm0_4(s{J21?n7l%=)IU_d zy*5aa@_U+LoRIvu$g_8g{M7trHy!hJF(Y3${rhZW4Wq0Ku%cYS_{CGsuFP-DU5;Pc zT0>QaqrLe3+X~V1*cBN6)+mkB@?V99sKg)YjpUNhv$uzPJ0Cxj$Ew&oTj1s!Iu*U- zhhBYEm7Qs9&1d{myDZhgG;HJ|2!xD4WsZyb(jYhN^ zQ35GNl$xuqGtHqnT|2VXMJ@$~JEh(+2_&VPF*qbleTwI~QuUzo2p5;Zg)kfna7%rv zluek!N}i?vDvKHAKW*r_o|1HE=3iQk0ZwH_7zxcby%><8So?Z$3Q$Q0xl|*JiR13X z<*oJ$3QCQ6{iv1}Ii-TqExB&($|(7Bk4wcxNdHYI2!}Cf9@bL2p!Af|Kp165p7B&R zj_GKIgAOo5bTyE)%(9t!txpwIU*w*Tq9PL5%)6;bfk7I-t2}>)wAc%Nz~^+s>FmtNeGh!LM0}EsU~=94ZD@h zr3{Ge<>vk{l=B<8qg1VObene_!cg3aFg&LQANw;b5y>$d>j*X$I3-wMxHTxaJYl$f z=no%sq(VQtIJT^sdz>cuGst=lYl5%w-$ule7<4VgH#K0}p64K0oxn~Xif^dD8bJYA z5)bLfI#Hy#cvX=iP}%)7w!E?G{!)`B`0r>T+Gni8*momN<9mW>8P75_Uw1-BI{pxE zf1^dAL4G6scZxv&JtWPHrP`sEcD+jc_Z{C-7E}-ewzb^T{PKNYu#P*9qwpzn7PJ_K z^mf~Eu80m4Bx>80dd~RX*wmltvK)>J8_dKpz&caFPoonc zJ{n;Ndpm1$JBxF~HrmQs%-0%!H1aq)9Q8?Kc)qu^g}Ob-HLBI3s)oWH!nIH%pDi%q zJo*o)M%DnMnGs80Qk|&WV-7S1DHFA#qWB={0f{cF&i@2DH-)F*(Gnr2n7leYnso`zPW3vKn2$5J7TKaU~AL2KM z4|k8!)tq2wFlq=}4mytBvkMpcgC9kKP)Ye37xGs!YS9y?Kfu4VKOeK1C|g{H9=@bs ztpP{@$u0gybD{8+SF>cmF>F>_(7`Q0E{zS**p;=12%JHdO8g4Qxm;W_ z6k9Q!=3}%xP7uMJ{5HvVbO{$0K|Vvp#MJhNy3K*JD&7b0^yrscz~z_agDI*R?7s5g zttXbx8diJDzsoQCItJKyzd22+ z82xbLO9@LKZ4}ZpW>rAc#sltcRAI^mZJlo@o7hK~46{assp@0pS6vxSMK9Vpn2Mes zOFglSowN|3MT_Id95af#e zI{DPNoNP=B8X6-H$`^&hI@C~VLhrw!iaZHE!-KSJ!8E{WW2 zwXm9_ksBI1h2t6+VWpFl`Fj4AL0s=gaDo=Iq0V1sQS%axH#)1O5okKIo4L&nAd~{J zO1|9L`7Alj5YU=&G>%bfK;wYivTc+htd-4=K!;td_z+R5kJ^}QEMe?W>S?|4Me=w{ zgTg!bzzl6b;|q=`{Dgwp76rv0!rcA|?y~6G)v3*b;r0$%ba%eJ`{{*iJpg$?hQG4r zyn*tm4Wf9D)_LJb@U+PY(ppgn^BAI`kyqAyjN7l zAium(gsj$;75ruoO*Jc?+Y4bnjA84FaTKpq2KUgfu9tj+`T}o7hUk(jjH;F-kcZ$c zuR8$=KoEOE3<;yV(>3;!$Il*x6VgtJK%7fgg}4I6oD-A9_?X)RsjH#==C;+`JFP}m zK{+Cgryd#jr?GO88<-x!4R)X}4(rcVQ3gf`Pmx?e-I30!GpRN4)>A~+kaEQ$gy4PE)Q_g49ZjGSW zZlC&cvw6Jc8sqp2TH>jxN{O9NQLnufPjL_Svp~dbck_qmrRvGrL)lc$pWs{?$HHhk zb({wM3E2-8t}O%WlfFxXHlziv@FKXd|9h1$E(e`XpJOowJLGRg`oRFb;-ZR!jvEOd zHN28xpg5bA3qs_qmWw=_`d(%i=>5l-q13mk>vxL&SM`hjS9Og3H=+`wFFa>e+17St zT2QIPy*JoOv#qJzowTT{45r(Sv5I`)xL&65pbw5Y%qxHgjF#P^&sH z%gglQ^tlPKFMuLcUy_4u&wvts8O0=wr7>^vdBAdG$}NKRt6ViaVg_a9x%zCuGA9l* z`D|nThmmU@Ck=^Brlyz*=$E-)KRgB1{ zq~`j$0s^nA3b!ZUo>3lmhljwVRRG)3jOyf{7&SC!J415VD1pft;;VBytTC{(C(boF zY}fco`AZxZegd&4_Mdj~wlT?kRMbALWb~R8Cn{xPQlE<>c*D1AMrwy4IB>W?Z`7^b z_4yebuU+N0scY&eIkCB-{2FaxID*wo#6ewgo4)P$PBXfD74okkd#>A1c?hn0J%V;P zua!QS8m#5QE~}+n!@S%E_fj3P(aF0GiCl{`@+;KcND8;b{>r$txcqI9?2L8RVrWq$ zJDn-2ZXwa$2)$65`c5$015}@*^IyfnQ`l!xfHLQP{g3C5lI#?fq!)Py$QZ5CiDFDq zKdI)U%A~9oX$xt`Y71%SGm+QO15E3T1SGrHh{4G2`Q1arU1Q84ZHwaXkFJJ$5Puq76CFMT?mqobv3R!N6|Tm zX;Dlift|#SUF8%5D%)Pg(GTn?R%A1dRc9grI? zl>JRGNAsyWGrs;UIytA=Xj!6AkC}+I!d;$r=hMRuI!*>NQm?*LpHA}uYeK2x^Qoir z>eWURbvxo|)#)_`?$jWQQbGHs5#o!+_BUehj%#*!z+9+sCsL0n4u>2k*sPM`3HPqZ zdJ;Wlv5M;+sE}@@zZvM-^geFVEt*kCjh()#I}&gd4JM0jW>rv$zR^0BQi*bki1v3l zq$s`N5Sw_De^`n_Ij+-6@q(mhnYOR|AoWSza`5S0vmA@;0(V_@Av61!3KO2IzY3Im z$XPU{n1)JH)2*IUJI5i9hh`0_o_hMiD0`>put@N*?E=yKa#z83FokgvVn!V|Q zR*#yfpi*R@A2iYf&Rz?maBFN{@_3bQ4ZTQ2dqud3jV;v^3w+%`D(o=Ta-Y7X!cht_ zJ8grVYL>rY+mw@WzOZ&OLYk!bKM{^syZxO?n&nuF73AY$8LYWA+Eh(n#51a9w@Z~y ze`4oBo#nqpN9&dv4oGstM+u^4!IjSv%aZiViIXz^P;B?3WzMQ$H7hSpC)vB49Xt9- zhjrL~y*7SIids-7F=sIzs1^{#shDEG6F-t*N_hrDWz|qW!7zC)a@fJY9id6n?<*Pl z9{sN&U~yW3{=V905TN1Sh9zXB+$V}7+=$TKMtU(gZ3Bl}Bum(j|a%Y;KEwXVwbdY

      1!ba;}Hg5IgWP`v9i@Qr%<+n_2AY)yvA5<(J*e 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 mVFvCv_D6N36TPC87JPG-A6Q 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

      gdXl9 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ⅇ1p+pdQ)u;Sc{UHl6K@KMEPq0RP}} zcW9eGC%`PR0pcnn@u!~6DYRbuflN_FQfwZ)8C>JMyWju~!_1>*x|qHtU$FUZH-7>h57?Mb+3(=d+f z^xM;q&T7FcCqDlcTxs8=q#ai6iOzWSEVwC+;TcaqfmuT!V)tlGGbZeNhngeaaI*k(59_e0?Pb^Gs3(3;5rf7WWDDS6GrBjYKHCj1N!E2LVqkaxRR)(E( zOG1_hKE`h(T@nJgv3dxS(J$N9AJ}Hjo_nhu%bRebId+;1yf^#rI$VGJ4j!&8+3$6@ z#CYI*b&7*^!wj%<;X&(2Ny3TTw`}_|`3HMB;Kv$Yf%FfC6eht)Z%=Ru09^iBMeSq@}&pqr_ zqX_j-yi|?7o~c|ks*iq1svKi*4Sb!mh6hyJE6{$U2v%1&(Xleuc4nCeTAU>=L%l|C zZ^$v+N71d4y`_v1%VN7!Wdo%f# z7Pb%XOZunI1^-i5%K!8o^h)%IIzK~yN_gLd`RlI**XiNScYa7LVC@p}9HxAUqL%H1 z`XYYrE^JRMmp!8sVnR?C@Xr(i4;=69v}ixR_`NKqh_^*kN&imKUu~V>SftM*^qKfT zlAodXHT;2Gd@&(=7thN113CCCp5{+X?;7%PZgsb^(ri(oYck~@OW(TDM_0T8Mv7cU z|A_u2|2u!tD{hhgqCb)TqEExXyA02xq=-&)3zaaiPwJ!53+66RC@oaMLDa>*?M`}s zk{!KNmK8DC0!9-#`J%gd;9RHs%v39CvyS=4(>sx8a0*J&h49#v^Ue{wwWfj!DJw92}DUG z)QobyluNJlDXkgBnhD^^D7%LfxUIH$FMizCdtcjn8~VzZie@GW`6A#XK{$tm5HZyZ zgM3;tiIKc(t$k)PQ2V^U_j&*M=|g7EoPGAkT5GSp*52zw+{)#d3o80sV325!m9Oy5 z=ohE1=0CZB^|HbmXDJ7Tvea3MMQxd+$~1ANeF>E|3_5Mw$0IPzQnkng$+PAWfg}7u zx~I}v%9gfUY%`(r#VAFiR{l*)nGNrzvqs!aP-o<0&_=NeZy9G-CzN~zq%oPs(95>` zYnqwgR^3beI5e}LOn0AZYxIQKuUh!KV<+y2x;hKxZ)vvqOqnYY^_D6zNp%pERL3_x zk)NJ(8PtjU&}zQcnaP^}auPIby%XrcJ`{FE_)jL4wa!vWIVaONYnLa7qJ2-ISD8i` z%FR}7n%TH&^8jj}-ba=t>?!nLpq5;bL|ZUVd&j~_9K8Q`D#q%D>1%c0h~oi@w)i&5 zJ%GRBd|Jl_?l4@PK!LnhIVJT*fvtF3*j|9rm@XZ6bxUFQad+ox0D_%0A7{-Gn1+2d zox#uJ%VoUs#b3}BMDGNY4~1KSKjG0yYW0zl<*^v}pv+OQ8wcQE&D|$4KKPPJf)Dl`-HYw1w(aQa5x9oM42~^x@7iO`ybmh`Km(f28vQ&$@&=EduQ%+m8 zQp@oDn9CvJu|2!dQ~S^Z>=ziF$`xr<%d$3&r=@L7V^42NlCxELtEDlkF+`8b2F8WmROSHT;2< zug{rvrT5C%QCNU`E=d9%5KTs`qS8UMye%~@!FbG`K-4sTRYlZ<4|E``i$1^s--E-9~G&e`G`|QV>!is@v60 zS2t_wcU5EuFZ&rF(A5j5xc`A@9%Ue6O$WtS%*}`Um5OQ_Hn2WRtI~OI@GMFTqTVH)-og=SM+G zWP2(<58GI;=NORuG1pNJmDXjAK2JWqDfp%KYG^%Vs>|$ZiOqBIk*}&x$cg>;dsff* zBe!6rs83yo`1h zu=SrB>%0GsNK$^QTK-lxfl)bwbI8S=TYG#8P^mtU&E7f&I=ky#}oXq95=MI2^mg5Au8`pv=|k>Aj`Tb>NQEu72%daY()v#N62E zcZ3LHNB71sZkGM;k7mFSXSI4x{`6j@&0zwB#^>%Ml7c=CaDD7A{E%>@v|$eisD}=w)5zX2*5z zfmzkL${MHSKg#wvoV+4OX_E!I&G+-Phb3v8ugTnOyWM>`_Umiz+erjKk=G7?ZGUTJ z-*MH-hQAv-ipdqGqc~gqC{D4FH_)O^%D>C+bN3;^+0{BCx-9j3S-f-Xa&XGOYCg~) zNM5kwzGpJQ$L>~3vSW9mK7&Xs$yS$Tg@>(5iNpGL$hJUK2F|u=f|%E`tjBKTE#cEQ zZ<;`e`MZ=7rxZJc%V&E1yRNXN^R5fh_p*WdGF{`WsS|pOZJQn+8HtD?;u^zYF3aj4 z=12R_!XW=?eyJE_3y(mLchM-ni=+H|q8}LQE&rc}dedhN6^4nUb;YRzKt-msEStW8 zh#vbTwvPj|67ws`;w}BBk&(F~!LHg2sU(|6XgpB)u|+tfT-YDSR|F|3b9@&WiJr92y^T0DhtWoac$G zNACc=&@_2LdF?jw#uqGDf3pRD4G-U}efMT0p|QvpOS1b<&58iFW4n~ENT1^8MrL$@JatEz&G;I4MGVh2C4$uVsD+6 zT9m)c<585D`_H0tz&o?XJja;wFid*v=pBcHAJyIqL(Pocj6#yGoU@KrOiWE}`T!dX zPSxMRFHY0nAk={xNQH?C8%`oa(H2Z8!|^d)E0yAO@3kx^*!TF))DmYfJFt*J1eb3A`XvhG*in*tY5YR(&{=un3#5bjb55(4z&n-|tyn@9+zoZ3XVobtOcM zr>%tmLtc{W?}CwMDId-RP5CPc*J(k9u$SqhPvd!8qVEk2YDq4P=9th&mcja!T@lNT z^GmF$*<c|fRAj}IFycVE%$3hl>L|M;rBDb!+3U4Np52AS)kE6mkwiJ3rMPVorJNj zg5TCb54HGsv2zUoX+J{FfG;(g?OpS-8I)8^VZIoYINQD9QyF*zF5W1E$<)ANTm|bD zn_UMJs^uN7F<=q3&c#w#YL!}ekrQJhe*x9%waz+tuwFWVhmCv;7F+=x0fVTF`g*Pt z#djraRa&hmJcg`>H_ga$8eW{GY%GWyGmHh3#zGnDS&dfxpwXgt4@KdjbLij@#(Jr7 z<`^Ydt;DQmPpQ^3)Ma#2wY#y(-B^X6RobneS*OOCYZS+gnMUy>J~I}{RNb%9(EbN zG$X9H>affVpA4@!jF5g9KC|@0n5M(%HY%O@L*4YtLHOQ9Rbj!<0J`9Bw!YYz2gB-u zPpA1k7rwXZr;HVGm_yiDF=-rxU#jCn?-|vT*w)}d>1ed<l7f_C>|93Od=|cY90R!f&xni{#OAq zh$kAG+K`V5Ma6^*J+FhctTqrdFsC_d_$R^0a*%-qLz?4$*65>jtb(s;x$l~%tt9dH z51Z^)D>`6WPC0moCKs#U$qRo-){b?izaionh*+}`8w6&-KmZw0P2WdX0{g6U zf+ob{tP$gVXv<~k9SVSh<};OKk41inm87}Yh;7KoTht;avX>b~>FhXgfvb3lHOWhC zMrkH;fYMvzu>7H~)QVk0J;VoK?dMRXJIzWCqgA?s@`6<4{EXUwF+|GU`0=4}7?+(< z5r+ce@7_2p7%OmvQm0KnVD#v?%nl)wgp$p8M}N<#fgyDPRn*)XC(*LjDeKT?HU0{X zejnRyGit23)=n8Uv*So~Momr}r*vl=7Mq;~(b51GTebvX!H6x{Ql-=BLF5Mi6O9lW zC+Wh9XdvZ0etPQynoxIKwRIg=*X3z4#o6q~t<%nXL4yhoV9;Nq^m-W>Icxkn3iPl6 z0p3k2Q9@*khZF!u{I$z1e0prO8pOD6-IF5x6t=NCWetC3u)Cq2FXEm4qci8r#~@lS{>78A=nHjy}4 z*P0XhUmcwQqLiMgGFT44J?E>-a{|`_3wip)qh7_lnW&61X04O<*#AQR`r`oC|&&* z8A!p?&WjH6wFhPYhblfV!PS|qIYv!?*EAf+FiaI)8%Vz@a*qBNUf9pXAQVcgtjQ_o z1BM0U5~zQF+@t$1Ds8sEI=^v;#0?B4rCjUGQ_(iG)>))7P{P;pgW=dbuDc98e;U)@ zOe+S2;-Qmm-|&XCG}k56gInpaF+9pZT>p|XE^W9zHZQUMJVyPEQb=i)v`TwADE?H- zABCF#pp_9FfpC3CrxfAcAa+6jfTnDkX70^0jkh*7G)~&uK86^B|7H%e`@U z9j}NwwbEJAe5B0?zewhut`_D!Djl2;1OFOuMLsX~LNeZyxNlnP%ukr=z>Et}2m9~S zQ&XN%_9Wh?y@4})9cS!1jmMt7;nNHWc|sERZJO-{t!7S|M`us|7knkXA!fs0b8qn+ zcD}0_)bC~*aES-1tgR&Eq9zTQX>DB%$%9*w3a#5b8 z`fO_H&#mfDGlN~+8v;?E`#aV2`kUQ-f_?#jy>U;kmj9IhV|c{d!}0XqE4+hyN3?}g z+`o@|_nYx;xOaHk*|ja6E^hLz+}mSv?5o0s{1z&x<(Z;R?>I&~=rC^twJmPoPT$rX z7R{!rJ_j$4sO1hRjDay0TP9UsrdDEsaRBRT<=!^dxE!NCo!7F2UJ`f(^j{G$Mw4&l zzE@RW5BDEY{Sofpsrq${Wa8bK$X_R^fnie>7`399zA&ytLF)WwTn!uniM-gVqg-=; zT>ma^ev1pNg;hE*@%tzJt*Wn0^}lNI_h2N7z$==A2H>iSz&=%;2_x5P)Je5`WPW)Y z_vxNqUVfzh9}`MXEBC*`126NcS6v7B1@7Ca`XXvzCxP4ONvX5e0P){1Oi zK8D6NEx_shqr7*1`Kzk`6^vs7jrerf%v|LW>9EwP`bIqctnpq9w$hI=PXcY41IFio zcE=1}rK?pDj4x4s1oIPF)bb-RZQxR%&nq2PV^Lg*S&h44+Q9M`TjOf^FiiIWoO~HU zV79f;i&EYLlm4(8fI)^-AdG;nR_9`Xh%wFflTU40AwSKU9VLdmNi82V z3{7D#If5yUW>O4e5tT&7VZ!Ko&rO?SUUgrh zoIw$CC9sED5G+;!A6uh7mKhAGH={jpeU^J*di?|E?Va!d{r(Ep8$9Jl!xQOh>C;wN zbZFRD=giUWLpvH|`s-LzrE@*m0v6}?UI|iuVlC@mfgcrVHM`U5e}bAYN?*W^)`UN> z;AQdH5FX6&E8!2*wfuF`#MnvxDVZU|CIa#eri^FJ>#u;!l8vcfOYfXCBn#Sn-iAS~zt~;L{{Nmc1GR@P{$n{dbDI#L4Eg8e)D7&Qb0|7=z!)9)==i%QPN3J} z;o7Cpkh~e=7e5-&=d7x&eMm=L^bm-OJs4g2g#PIM6N4uVOcO{!tKc90`q(d^C%_Z7j2_B z_R^L~y%dNjV@)k7%w4(+l6Nwr^a;SIvfqgwp6MWnr-QJ58*8XUS8vL%2x2n2eQIl+ zHK=uxe|NB{YHG%Q~W-3bri#m*W~{AvtX0jPKmz(0M?I(Q6_ z5RSh!!8G#x&F~-BK=Z&xu@h<#d7Ro0P>qYdz{bl45_@<$~MURMs_S10_#^Sj)s2p(}6zVmpNW%xeE z^Do1PDtaz(l0sP7{{L3dqstLQ67WAL9_l+G7pkbKvmk2n(&?vm;#LB{>eh1X#cR%R+QNP+U98#XZF&K zw)e=i?-ocxSnFq0D|lH$0DC*@Kd<_%==@>wGr84BMg!+QD=_SN+#jJjs^7K^Nd3boOZ#o^DdoMHyu2sXpJAAfcgW-{CWTyK-uaw6;)+n#w-7y# zBr@otHNEUqy**P7LLxHaM3$sCK`+59%Yh^Q|JJ<2FcuYr;wqaEKaPlDohP&AVNff- zyCcxG)fl&QMTVP&NQeRSwfXus4o+#75O? zhp!f|9lm=(pv_b>%?ioTM7*7QcZvvLVIF~x-f_@AK|;6oPVn*_AVW(hJ?%_;f${-o z@;t5NCX>ht&yXTgh3=QP(V{HEqdJFL^(qi(LUpMGN|*ozNcB2#t=objTt(!YqU-4)DN$LwPd3sh_=~(9 zH%gK93Z)0we!h=}y2B*os=l3=myaAT4R6HoZ3BLnu@ZUQRgoDAEEE_9H3U#(UNO9F zntU7gJ);MaF~vl9S+jlJ-6x$NyWsJ@ z!duzaFH6GMwc(3+9V73~hEAYD2xRu1SWPX*h!KWwmsW+L?)bPXY{ru`eESeJfGqM1 z9T%vw1FFb-Mn>;Tz!Y#QT%#e~t50tdMiuEFZPP8r&+u+RG(0jF z&(w#;0WPrn?n&fd!;ol1nZCgUzemb_27n2gZ|zKg)2ql-fT;rnK?WAwB{UV$yrU@m z>adA=M@SYiyn}{!m=5K_z#XpU3||1D;C-XZ9m3OH*hIU`oaPbR{KrOJBd4YyXz>QFw#wiP%P64a0{O7 zb_s<=hF$F>kjQl@^rCm_C?RINgw%E2$>?K6p2f~GXj4|9^JH^+ne1r=A^#lcE73=j z@-j4b1N^&w+q8V0j?!4JiJ!#&z8Lv%l`V0OvhW7(?=(4T^aI$}eRy)S*l7OOvCQY9O3>8O2A=Zjn_*e+HtSipnlLDreA1ET4tkMXCE4OHg<>!1_h z{U$w$kXx7K=>oJ;4tw z2$|h^kAlE}CK~eYVo^kU1TXH~xbvr|{h*JvBS{Qb%vlv^cR6a$c zy;AmK=nK%M`|{>e=NHL}d_SF4c&_Lx(6*3n&nE&IizI;Q=F~^HG#! zcFU__K=;va$9s3%@LLU9g1`>;9x50SsHTUFwa)vDN%;E!{HqzLaaLnl#m|f$sDbuD z12Igt1wh;UuOynCI35Fe>%{TI`O|*qD|F4VG)uWK&$843KWryIpllc3!aL@p<=NEd zH+*d)sKaTsxysbd8?FxyP*@qZ@WecT{3yjvw&6>vjz)Jv(&jJ`Wy0Z0}1eDx_Tdov8PsHnnJwjPsQe9 zXSH+?U4};l2q;SfFs8e&i1%S;#R5(Ql|8t2@-g=#`Y|9m?nw)i4Wbn`uvOQ9XU_Ye zNe-s?W7OVYjJ{5wA0|QX5~X7ztfYx`DG*|n#5atOyad|CP7wuT`ZYbw$E=AzyONNP z66?>r3{Rv~EiFYGR&iv7R2yBD@W;t$ODyuUxW1#eP0~&JD<(aVGj8HMybroM@yTcK z>rH0{=`5fK+ zHc7#C-eT_6l|avIBSXiRt0MI`4|wzSIm5(F$dlX`iDjUEsjD+l!S$Dc(1j|f_a$|a zjE9+n$S}MwtBb71yFeLkD$m7OLW^u1w&g{33YflAU6iTab3^zvi5-iw(dNVZs=CMt zt>e+cq8yA=wQ+e?VUE+suE^h9|Ri-GKh3Sv*)K!mOz`^YR9DX|@>^aFLZ$Y!5D+>X$kb z@z`vf;w-33b3{}w44_=TQC*sAMh0Bu;2H%pDep0Z4KB)*x`ltdtCK%lD*^?QE=2r^ zdBf9@lDid@QnK2&6@@ zfp6!4cF->uWvig>F9P{%7hjZvVSX0ns$R?ymJ4W0Mma?r3U8i>$wQI+(FjL`)pua{ ze~<<;hHL3u)h?o`5aj7tE0Y36*yyA!H2P2+Rk~tOG+{{x6{6id^#^T+9K_8P77u7f zD7j@OkEc+6_3Gk`vOs)LSgq)~Pp9=Ectn;azb92R7tbl$VdV^XBc{Il)U8CuxpW$k z&vsDiIB!Apad@H7t8aLa1%FBhEjkc@dtmhOPWTcyg_$#aN7SlZULHC5qe%GFEM661 zd#g_A%Tpuxint{QHw4bc#0@0QuQ7h(`a9 zGiZk@boNvKZxI~ z5!pf^2zqSGVR3|0sT6IWx#1+5hoVsIacuYZq3q(m%um3i%>9xz9YzyHDQd`@ar#M7 z<17G+*u}3}(e`f70D0zN%2aIo^*K;yzU>t=FOGSOR7^r%4Vkhn#xnGy{~aQjcLKKo za7d#=T?9A_bP8ZuB|w(~xUK(!3pAFP*H((X^r?cDp|v+4Xo$Ien8=5Atv^IU-$A8h z9YC|rKzp(}y85bxa?!&F?|ituGB}_$NG6}oWx+iXog^nU9LKbV1FI4ZY5}xsP<&(sQooHRd3=)Vbh)q%K*E=>x)*Q~EvXDRTnGQ0p~+E&vr zS1Z~{R*OG%qrq({`Ol#j7>p;sjch%N5$A$c%vA5QqPFV_v!;GarLz=`3LU>9tF`_Q z0Gickje;y=0M#7gYsfQ(C}Wmj5PG_CYuyeU46=p}WDI4DHo62IU&{~b=nA*`DxLsu ztz=tYOXUc_4}N9UP*!Kel!!rp(BZoy<@*u% zE=Nq|08LSSBcmU`Bkrn;w8c&vIt+lg>>X6ScF>-f_S-aZ!0opwqZ#v2AOIPmSP5Ck zh!1lJY5D6^ugyF(*5pFfXP@u0N$=osTuhq2FJR3xSo3}9Y)^Tyt3{LV1)-pP5c&(p zWltwt99GMRxdg+23T>>Zg*AE6ptpY*UkJx%^q)ebRm*(uAhgP7D(&ftcUXp%qkJ;u z|E&lInMgWS464pfx`_#47C$WPkV2?1c|#U&n;WF)Ug6rbR}VW#~hO+<3{?^4(2^Yg6fA^=eJ+4u#3jTGlT z06_%QVfDUjRGq#1wEUmO)+P1jK&I+=kp>%p!M@lm2HO$r6TdhTzZ_Eo4ka+0rrKWA zimYsDht#bG^kA1-9^&$gZ0SK)i#S>+!whhMC=L%C;z-zDyCQ`S)WA+xi;8%z0<_Dp zZd(Xqow5 zfV#^sv2tIJ@}qqcrXut8_@7LyuM0CM`6KraW5%)_sy|aJn?eiI2oJ}y-Vo&Xf*QyJ z3VZ>;3uB5&g}z8_o*EEil3xVq)5RDTa^>U15O504HL67tZ~_zj}D zq8C|Nw_3FeSG$&9pn6B7!>YX9)56MIJneOU(4+8V2oI*+Q%XReTYtSf0h$a*T$3#)XdWoMue8;B68d>H=F5}uMhSL$bZd=*TyV2odHX+o$?UV_3dX9*z% zyIQqhlfUWm4hQ?xz;Ny7ppP+~?P(%ly)al;F zkf>f~VzhKT4w_miHdNU>H5GdR@0$nRebJNC`F6egRsYM9zY}`-57l25yv%}Mqjq#y zD)YUzlr|!7Vk~Ey>d)pgp!>Q_s{bW;6T3`HGr5TG2vB5s=X@ZWb=k;;nzL>PRL|g@ zK*O$2Rs#%ppmXe!3b1d|xp%~6z*>RbgK0U~ZD1c+#pEd+>CzP*IHK8}<6}TyY_D$^ zmI@DqwESQ3Zq|H|pXXP2M{EYL3it;6fwvM%9g)&8yUH>D2f!m#zpj?IslfZ1tAV^WKU6}RFmcT2x2eTli$0k#tE7(AlQ@sHXZI#S!R@V3d z^6Id}n#Lav$M{6=gcL!S0{M_Qxa>^uTg@>`_4Zi2Bd}(8H|*V>2-61OpQlqF{5>CcU*acu7(z*Y5u*a{Byz~dr;hFDT~KD> z=lkQYTS;36-nKL>wb1_Oc#&V654Z^wz}whxi>n3OH;@^UqB7x$)xhf1M50lo=)2 z-KP{imBwCZ;SMsi^V5j7r6*Av zvIfU22~dsD(Y^@Z!&8>0R&-uf4Ng0QIWJG%yiY z(EWbH)vc9on}m&gJ({~4X;4x|Wuw$8li{{_wFbU*DHBu4*Q;g!5n=uln{$;t5Qe-G z-$6MmFz^N_#SrVUyb}iAm4IG?2E=zc0Xd**lRh~hZ^z4IN)2+b?gE;o6x`o>qK1-X zVK7WeAByVA4ee;jqhjFuN;*@^J^?yX?7yy#FH*IfzjGgN=Mi_`vat)w8QI;%4`40= z0djZ9J>c%UF6&q5S@Z6O%kIA50DHE@lJ5jbIxNkKc;4A?A{oMn;zwcpB;M-2tehz# z<21Co1w9d!IWB@?RRDW<)`7bq#^A8ylc90FQ=gNAR-GBS_!n)m(b*l8aGZ=ECPPX& zM$-E-0tC@H)P>Jhz%xz$$UIKZ^mNqC@UHOuy@w@-{L$1gt~M$MaMUWDS`mH|6?g9Ry3`D;C{H-d;y&l=pL2p6dD0vfbn2F zAEiUG)Ht&}pVr=sCmg}hdc2`@{*5=5!yCcNwcX^}4^_C%FF*@sl}xOHsp>_G{y!$o zk5OM0v2*PFLb6G!FGKz4a*Q|j8S!>%Xe5~_amupEaVGyZ&(VM!obt~~wr}P!KvR8+ zo9%W9v?j(HZ?~}cr-ncm(d%6Cn0*m-vL4O2bkAa z_z!@#?d}4(0S*3)F81rN@gBD+YPoJ?J(-MSPeJTEHuUm19|u{wH;$%s+BuShbeZg( z!y7Ie8F`P1yEwEC-6Q2W+qR0}c}^x4WnG zi~?u`kBPmf@b{svT{4tM(B>cWlf3~PpvU;ph%ssW=mXM19W}Dtio@*!UJB6 z1<*CrM!^U0*eoOrmOTN-hGxt;&dcopK5c;ocav|3F~UG_Pph(9Zo;?uO+zh)Se9te zA4Oq}&_FL4XEz)IUXDg~zosOC=CzLDi(p1`=rRM>j9XlN(dno+qPOaulr3F;Qg1V4 z1^#sDoklD07xZDG%N^q&T|RcPMe%{Y6UPOZddQ%$OPAl!WHMM%yt7FrpT~j$at4dZCv=cEZvws9cO*7DA>TUQR^>Oxba%uezX{+%u#9hrCN@VS zBOe+$8D&o`#PHk13ZvC2_ubR>JRpz-?3(=i?b8PI4>pTKi5#ug=@52x8_60J_Q7v^ zTI+v`iQ_{o*dY>gjFnaS;5P6ZkX8P3+ivje1S@eztx3n>|2#KCx{Ox%#AVkZL-$|G{$x!eG%~T$W*tI zjyp?5d=M7=g}7v^pnGixC7#D6QA7irIwp8!B6kr0bTCZ8ji4WR2xBC-CHvI|_b*Fi za4+1aa$(cPCo%T)!@`QZi=>#4ct-xR-69lAB`xX`x(N7;DHzcK-8cx(GpZ31JvzFa zWz=B#kltz>(&wy%GREl`0_ke#5^&#)ifM77d*IB8K_hg$Z|Hz=5XXjWYbdJES%pvQ zoOj|~-<`;~F}wzr^^6>ot(Q8#WMHfyy!g8l{(Wfx_E{{W%rx!Yp|s^;a5UqfF`OJ` zx863C7$%gBZgZGz7>r`*A^-z+n0nOY@* zzi8pL(lg4>?vQfE&VJ**42x&3(q_kir_=Y1!j|GUyq|Y&dJl^BP4B~B`R~%wtZAE0 zp^sVZ!Qepc=Yt_~yQqEJ9Rfb`YzP}_SK2Mq8VPH$-%iKRMv*30sdMIA(BTzrzl?P4 zXEyXD#R%*FBOMJ#@m`jX^V3GLtUWRb!g4VMu~I&mME}>rWJ2ns7*BHShUEQD51K`h z9elddVpqa;H2hs~-jJOjvSUyVJAeYCOi@S}nOs)UL9fDU2;wt?#lT=bHWqx0U10q# z`qgfy$;eCaSXS0U4g4#QID!4?jYE>dA*!6|aQIB^{Q z87MRw6lqEhpH9bkg}vcZ>1bpAeMveby`y~><7?;F=NsNMJRd(G8;d4!nCOkFrx}^( z(5n~ApyVKo^kOUe4|nM>vTBgKyB{CF0d zJsUG|hSZ%|6<-rhkEtETu1nFcDB@s3K0f<8(j7(gaQ9I-A;5j~jzm2C`<5AJNfyS) zHh4RWfpX3U`|7jYA#oqHiP$kLKpQQ~7FdR->DH1W;24>x%T3F}6n43*J|_{U)i|r! zHm4dnqsiF0OVjs_XInobpQA*pmUn^FJg&Sq zsf1-_mlc*X%IuCOSx%g_k5)1JswZtM=QwMRr?K|I@35Rx%$4h8D~_=hSx#nO3s0+$ zvi3i}fbX@B{+_jmrdYMX?2DhY!C2XfKYKb^&PBGOlU0Arst@}AJ$x=5q&+h2Zm+-h z685RBmo%G*C*K}QWA>saJy+O@4px1>-i?%cos7Qn@W|87>~*tcde&M0Ie1oiot3rs z!8@h1t!zaLyaF?2)fd>@yrgv!@kg3yY|FHLLkfJ*+Fs}!AStN6Otn0VH8u$$2;Kt) zo#pi64z3JO!lbwe3~h6Vm_6%Bn7~tPMJub0KWm03)aqQVeKtdm#7$KH-zC8Sdcg3m-aCJj$dchf5D7z1H!fKDcxe+MkR3oiUL4Mmw$<#e+4P8?tu z1}F|hZ1ve_4r#MsMvd2q-tn{(O~%V6J>5vENpzU@9fXfpQt<%_B~WPqBiCK7WukrYjQ`DE%lKh)assKct+NS^EHx%t)GYYLc~ol&0lNusFne1JZcPim=SN z0AOCis=JA#&p*f7C!S*~4zcPnM3cQ}6I%g<2?TYR=GM*T$_fG@;@_|RAb$S?=EbQ7 z+!IDM^^-{f*?W{TWvZn9#Hv0nbMLUaz{dSY@H7&XwerF6d+EYHQe1-0d(Bi`8J>`Y z&!(f3&j;zFGpB!Vh2LZ8=!*M(`slg;g}*CN8yQHJ4{Md=-^|HhPab`x4J`?EOr|ch z;HnZrg-Q8F0O{Tl z;taq=8vlU7F=g`Jz$?UYm|7pds0^Qn!hlw4u>+4npBG*AwJ|$1Z(q%R9E1Ittq8+L z&Ft0ZFR}YWZ0-Vh(*l)kSV}2 z;8rN>)rgEPGN3i828}LUKZ8)h)dFQd6fQ$E-V1u!LM67v6*CS}tkA&0!T)oO zNDW&|2XtGey`VDxXtKK-VN2xR{p2NuiP>56_6BTq<90h*W}FYl8QXi1<1rBi37fF@ z##z(R@VFg~$$F(uwcN^E;AfTN0Qo!NDf?YhQ}ck5KbMYI6&ORZsUv(U!xh1!tk>|z zw<2$%r4r^H=x4!lD-?$}@LpuR%CMxwrCUP1Q`D)rOvn);Fe1(EocG3`%?bV}>(AxN+Yu?KuQ3_w9?S z=^~#ERNPl;rzoevTypel17f}l+S=5Lb+!Bw)_fytRw~Ew3Mt(c;k|5cBpkQ@uy63& zuwH){8vHiEKB&(Otk}f?Ro!4L?2m5 za+?ut7+(yw6@abgAg?gfBv+H?qOC+#8~M+G+J!t7#E3S4E#ACnrHV|qFHp(m9K8W* zK)(yI^>UlK9Z{p@|4Ch7$9qpbG4@+;00@WhFAhE;wR#Tw2k_{KUYNFbfmSvW-3rT| z?L=(@EckDD-Bg9YC2q|@-N-;-Co*_F{#JV0U&V(qldWvU8)~31wpV9OLr7GB+XSKi;iFI-Du{Z~;~gdh(w|t$qhCj)?qhHN>fv=eKyaS>wbQE}1h3cCM zh#%|{IfvWCK^NxxlQl1eZ+~bUt(W$zGr5R5hY7Z1O{NmyOS4kR1|=Jn>`fOnNZ3U31Oj>MWt*gWmCxsB`1|~pp-+UTqxyIDGy3{RLX}^K9vffR6wO7C>32z zZy_|n+Qyn;_}=~S(TxUK&E6jF-31?qxpyagM7Z}=_~_%_SK#9a_r44tN4fVU_|Um` z2Yehycb#TDzi)t#A@1D>AH%{y1xlxc#|e~1RR0#WYO`8y=Wmfu0XYM}gAbIm#nMGi znRI#87U6Y*0SMxs1A@e1KEs-YyFojw6W~!U;GK3sK+)Gz^LgC`|Ke=Rf2QN|mibYs zoo)UJdg^czlTH1+^cnqRQ9nCAqn}LbXZtn%X!19u^GYCO zXTkr81p0(S^*1Ey9EAp^p#g7i{ESiBaFmoq`ZEAx#eq_`=g%0cjAPwRRQOlJEyU3h zC4Sa`VIQ8FsV*yKjX%c}L`V0X$E;KB_?Pjm^wTULJqna ziK@b;cKo}(8_1NefXD7BB0zc;U6E2>rIn+4H(SWfMz!;s)e4y}m(>a@Uv5<^Y<#&* zt+4aucD2I6mpjyoOujr*t;pibv($=gzC2s4aPs9&wIYWv&rvIK`SM(~B9AZ6Q!DcM z@_e&e(Da%u?Zpdcet_+H zW_C!eeQ0x~^DD8L$^pr9qV~(|ZEspbsP+PE-RLrG6=BM)Xrh_#ggWF5aNBcJQ=Sw4 zetrVu8w2+#Tnk5ly2dw!UxBaTX!@l; zbSP!BL<$Z}Brc_mx<_FZD=1>%5WaQ^zQO0f26TdOxI_0p*qS_lO!(V^SelLUX6e`M zd^bAMnHs^2-Ful3ph6!aDc0N$017lwocCKc;I}n6)>O9BZC~cthIV31#CC zDV5QOPrLhG^U1&d2l=1T!`s-~zd8&uFnhbPZ#H(6E*v*e1Qv`0w39s>55cp|-u;pI z#<$;rqJ_O3(&W3@3&9eRWV(5O+Tx$rK2-kzKOg>3c37Yys?WR30z6|k@r)AS8M}#RlmO4zO+2Fnc*btx8706ob`#Gi0iLm& zct#2EjNPVih3BT^51FE)u*O1i8nBs(nw8K>PCl4q2)sk}HE_K*4$Brqaf{c6GS04P zISrS9ce@lKRYfq;t$)IUF`HVt-Hs89Uk-mVBN*b(ucRFFzeiZLym>v@^UT8-YV56$ za(*6JvbVWf(b8Y_zT7{|!x%0tgnQCF+~4+gJvT-=L(^g0b;s?}c=$cbn8Eh;!bZ%G z@$Q7gbX1z)9l&+MAL7njLHb0u=J3Gg5b2Kx2T+h~{2JN*TAOh#sc9Y%92?#c7L^Sj z0=k~#$J`yRW9+RdWo(|dbV}*S>K~pTXakJ}eMN>~WgG-8gf`_Nsa5rc#$r5z7LsAM zH%vNmJ8QbY$7w5|0mGKNt6@4&}u7c?L_L@x?(;Z57mNE zYg%R293c&;n>3)RwqDY#0R|YBeAy&uo$r!)?#nzgp`+E#QAo2*XAO@6C2kDthfH-)nE`;?vKw!KlkL_)th5*b>Rr`^GA|UZSo||lEAH1_K*-j3$13&D7 zXHXZjQ8syZiKd*%rgp+>JCjXiC!2Z|-h4ILRMxeEW{0{UQ#vM24WRZb(9$d7#2~H& zwu4aoN2Jon9m)1KLVFvN?b#FU3Hi5)=zs)RkTTSJY>D9ss%RoXI5|jbqET}9hc@sd z*|eN&dYx!`o!NAUDov00*|p*wc#b7%IIvlW$CWQmvc_%{hK~vPxI@Ut@6b=PfOHO# ze7y2%`M4`pK0YSo3G za$K?*?S-VMm(kMtlD-bBugIxlH z+;0?;SK1XVYlc=kV^WiE;J*Fp^AypdT|~~`zl}A|#jFf5nMVY>=)lzgEME2;_MZ~b z59R~t=rwJz0t0@DV)1>N2%Ny0Gg-~+agVk`H-Ycw#7kyj{Jb#OVgUH)8xq^HTc1q}jfX>L}yIAl6 zQ*80QB+)z)2SwY>=n&q`(Ql1UO>LUOEuA&ILryp$%*3>%6>aE0QAy@iLg|PS?|y{M z-je_*6Ph)zO}a1RhPw~FZ)5+5Jy$OD@Avmce~7(2kHR3jL5Xl){3#L(cbC#^o~KCG z3uP#g&ae=x*{EcLlATI+C^@L)fD+}OT$l-^EGlI|DVs{!P;yeq38fq=8LQn2Kh?10~QPv912G1j)Ml zUQe@&c)9+Dj$%3CNQ>#dE3B(|nM})47RQ2Iek~gQ#I*PDw6d)mpb1YuESU1vzZ%Ey zE(@CRgII(MP~B$o3J1mp3ZX^2E25QMaUDc6O4YXuE#`nn!tyUBNKCK&GF|{BbC9<4O{siB_ zhE)~e;R?e{k%>xO6eLG*E<6)a@K>Dj>;IXac!i!yA#KZbr2;rsY~Ciq20aJ$FvRpV zJ)Rz!nj+o|KWB-WPjAFQ;J%|a5CL#SrftB%^QI%%3Hw@_GbpUL^Zz6#p5b97WFM{i zC5)*mi*!eah#*A6$DFQ!QUMQ)7oA0U%0>@Z64NFhfwvV@Hj(W^@z88@D#66u@kNJo zblXgH-^xl%29v%9$XhI%93c|bubnWbtR2~j0QsMRgvZvQrb``nD8ox?ho)ljS{V^vyCsnO~A5NpZZ2&1VvQ>LV?&&G&UKO0VTswp{%LCQ|8^2>BHHG1(kNY11pfnr|+gWC8e zVj^7tk~RFhxOz*Ch->zM5c@5?PPDs=eoWNUWD`CTO2!Q+yD~k8pmB?(mz1^H< zp6!^S)mCp4BW)*8B_<+$M47kNXTt<@1$^bvLTE1P6clGy6`Me;Eip3a5dhv};*K>d z-Lu7b3*SwwHTL!|aTvi+{X#YO+3Lq@A5yo7{y+^SG`#)vF1&|yY((3g&%q0h8R1hi zSM$mRE6wZ-XD}{%c($j`>GVgS!`|6RII!l1w9=o~KE$8eoH9c^DZ4j5oyno-h=}ol zznMgY7{rOfU~h+~(I2JyyCIR_*rJkH2Z?w?Yn^M=8s|zp(F=a$zY!W(qwZH$&7zCk zhRY%gC7sbBerj-B`OeX=Y^3dDJosq!X*T%u%F^>?P$eQCX@`x~gcxuK9McSs(p zzu~C*%U4s{*6J{J3NZ@SJUbF+Z-a;xw%lz`glh9)BeZ1Ukyx0tn8l)`2)S78>;H!Q z9-pjM9#2oJ|DpS%m`lXRiCb9%-fLjk)dSMzhWXf-sLPrcq74G9%IAv#v! zsj7xMN90!4FdWv@+v(eC zA9~XQf4Jxi!;_`t@67FJ*uTwW>`8=(ZHa0FU)%Eph0MuQ;&ZwDhJWE(B%Q_iIetI^ zMO^3mU!{n&-u6TTfWB+m_-Sezzpw4!=Mcqpm|gTSk%PX#ghlwn4B(4-mi^>u`@cE+ z9`LBDYu}v70R|l1BaRyFRMNibEmLS8(eyS`LyeMw6cfOj09S)@)#gg&_EODMTFF1p z49V`!38{dF7L@kU`@HXQZ}KT25lAMJU?u_7gdhe`h)OjhXag#l{2_U3t$og9g4Xt3 zzfW@Joc({Vz4lsbul-vfD&QXsd5Gs@QUuZ3l93~8?P~w;Hh>_1rf|NP8fYo$)9+Pz zFCH*pjBzK)?48K z#>WzS{MB^TviO&<<6yc_-xMX32r1~2D>DC>zX|O8DX7C5LUfUkQpNjjNLcs&>?GBxzUl$d5{#s(DZn83z0$ zEz`a#Y0B7G4bre0og;GE@MjF^d)^G`ixh(7aISWee1`b?^0LkbR~8Gt&Vv6Jm!ht6 zuA#-ChSm0aUPkWXvw8O0N$gk-Rjdj1Cr*S`q8m}!1dIpbcS@ zim``jf3}lsiu*xA8I)jrv@!oTWM#}h9oN?WEYJvafqFqoOWau%N%oJ(^Q?A~M`wgd zzNG`Yp)jc#QvOXa-$*xvTv9}k{f$k8GCx<8>YOObbnhM{yuO}G^ycz1Y0<;?6TqB6j&X5 zdc=4NToPGo;T+h4zP+X!t40V6uT|f2BkE;S@E@tEPr;9#R)pKPh69;Kc2C9XZy->3 zr?lW{Ly9QHJUoCGpqal2-|xU@^eKB;Vp3P(5n9S6(1ps(g}As> zWVB9KSsM*2%I!ZyNSK&C|&4%Sx{iT*Z;MYB6V;S3FY|8%wDXhp2YEwv&cYFYFKt&T3JmaGy{EC@Duzz*26GG zO)D|`)_hz^feW~j>TxNjv;;1p0f5Ry>S<1plo;hrbMgBrK8QZOs&-MiiUt;jmHF{> zOj2G-r~72!Xt4{361wt3=!!O?(6uX{T|9?8C8ZOnb4v&&LFBm*y`AHgt2_#CK=zld zx!5#}4iD362&dP63}j}GmbMXVQh-d$+D$4tQGBiN@4!w)^3D5Pf|`sACxAt2KeG6y z(+K}yngpJD7Wp2(Q8*THdK5)AR=daw%ZLRx;eH^BlDDnt#C>&eh);vhVwNa!Y9kik z^>L%CmXVtR46UG9@zkfsrnTrn^o!N$1x=j3%))8lSZ%j*RKLqCBv?uS2)P8Ys~5Ur zs#5VYmIPygQ8Bz8d#S6-c>rh6bC`FqC2eTOuQdEL@|-D`coVi{3T^yv87qQDLCPmo z1`ni5>X8<#w96)rF{UOQ*1~MG9!w>^IjRn(w0^`rikyL~ve`-POPBxK89NLz0luR>=errjP z*i~`6cxR{u8+uXQ@zT;r)@j6!ZL%>L?)K1lZLJw-UkJ9ACC}pxm&p-#`=&e54emEKZG* zzkfo%A0fhCX3*oUidm;vX%z_(Wf;#-MCG$M!u!k-J`_1L98cfWo;L^Oa5SQ*pcKwT zX5FDRBbg~I&>+8xYGed@9sV0GjmdqbrnEO@HjD_ji)Uw}u2MZ)$`WPaxi?EOIjUFS z{RVeC%F2JjmI6~v14##VwcNp2JP0Wb;ED+ip}dL&^Oi(Y6w9n4E%GgR{e=m-{`?ga zR9Wu4nR@JhaLm|8O)*o>ip0CFrfxqXJK8)ZJ6dGbtIuB#Z7IipB^+M(=>v)KnL0Ia zz3kMm{+pPtfcc)=P5E{k`NDUl$FkJLvMiSB0^O|HeUsEq+DmE8hD*;Un*%#=lN2f< z{}6{yB1PM@@he$c48KiZE!T!QG9w?!k@45ifPf%bhMY)WHaVGk9wD;e%Nk-BxLv`@ z_bdWobq&t{RuW5KR6xE#d^#f5Nb+Euq^6jXTIpw~SxJ3rV2P z5wW289S=my`FPXH$-b{hQa#G!*z& z#Y108K8pNmJnnpi^fOU`mEwQ_|IJW&dEXK#IIJ*e;A*^KcvL*-gdKBBD+_kQM=2BY z70dY2EU`2_&`GISFt`UBTeL^yC8MzH+Y>m-pUf@H5Wbx-yH6$wUk!iKRXk4$KH>5- zqlw}2OyjB05ia+8%(vB^gcy1#$mY$}+i;dwbuS0aMgR>PdHxj-HBzQ}6ZwhfNO3Hh^p zD4!d?2s#=I5&XFkGj8Y6DU~;1wq3lJk4%j3{zq@XSwD+Qs;J97EQ((Ayv3Hh#cEr5 z-Vr&7K#BxeY17hRxBO3ZvGg6o@U+GOIqs81yYcwTw^5#vqiGL1q5Bu~UuWvSyKoUX z-@wdA5?{t@73i!+HPcVL&R2BIUE~ngiX3}bL#mRPkZ*a~b=y5W$IdFxA#;5hmnK5n z?VyE-i^BijH0db*v-Jk4*!K10JY*LcPVrNHQ{{IC7$UXd3h z@Ml|TQ4pk&G?9^tfW>~i%&j|r%zO9!OF= zX&4zqc;oqx41?Q7=U)uzC`0u`u^*f6AD1CqXn0w(|GLB2W9|GBiM@^oxwhLk;}kLf zw-Ti}kG37W415zh(s>UmLc^sI`M#;&Xhn^QJg0*N+$4e@L>BZS9(6dUM-q{-NSv^K z$5zQ(Iz?WWAWjcMm}&cHa?0t&B_-%GJUIJ{^Mph(lCM%bj^Ly{SA#?T^;{DpjY zBA*!g49;M$@9(5Yk&_JjSm0sk%p48LYBGriR>6nIIvQ7)j z?~}i5x|Vvo>3dD&jy(TvT)~YVD)=~vl|VmY0|ypKL#TG2s;=ywfD(dv&8 z^Lbe!MoH)j4O?KT5~8X9!BI3KjiOUv{O*DH*j=FdwiGX3(?WxL20=S|k?k}fCF$jB zXW|0V9aD?1Va@ZDPOU|me!V8 zXKDFkS{7_w(^6=IOi>W-ul5jaGQE`U5j*6@AkCp?jV&U^7y=)T7}_Kzigb|ZOzj_@ zhUSwlnEwBoMyhqyvMGkWqc1jnbg{efw+6<5jVMRKV6KpA?(MNuQ%u6uB+I$d|u6MY6V)YJ`1`_JDT3T$PdUn)WE0wq8{VuMzvLcSz@zS4ll`rd*N- zE0Kej1&RS)5ovu6`Zs68JbcJP3w z+xH3Ndx6#o?3drshLpl@35Slheyv}LT>{LU)cBiZrDzJKm4!QCZ^<(tLLoOH2A|Ri zOh9@?vMLO9#oxeyC?~L$#|SmOgul*ntortQ_K|cF481p%uR4-)s;o6V36Xia_%L*& z2Mt~`rCnkpohMm*w!tuA-jV$L(Q%v@C}RY_e`C3{!0p0zw*wU-v^VK7OW1lHBF zzt?=7OD=Lw`c98r;&mgG9aYE9N%6jJTF5`yuy-?329d&YG0H7q0cT8ytDv%vC5bA=-4L^txsR*+%W;*7OWY^!N-G3%L^cE_xqhW~o3%>B;( z<0^mmqp*JT(%;_9w{OP?Y-4z01muBX!Pt8tTR|VO<6h#^@6(L(D(yiG zu7MruAK#vb`h7{_#tAqI-xBIr!+Rb_o!*tvCcB2wK5~)9rp99HpimqC_o3hkae^TQ#lX@A(8@v}Pi=<@2uz zcltapAV>J)Ez}##Xse1{>KFl;SBJJZvZkp_1|eqlgLb^rb$@k1_@99mi2iM#QSp){ zv_p2@^Y@adt#psDZIAySeTiN)hlIg&?YMMvUn!XxaqI6;t>wF6r=sS_qWPGsC<{EjT=Fs3Gl|IByoL`8zP=er`nBQzUQz^KN#YWR-% zw+sF`7snOz+LR-~`P4l_{o4RZ_}Fl>o9y8 z0EYCwMzZ4!Z;6#<(ud41?oV>E0T6aJBWj)&J~iSNm{r46#!oEf;s*Zdf`(55NID|%3Z+vo=F6j=hzssO%I zeXP&?FsRWTn!k-0>~GRJtHy7*Z!07uZAjRG{&TYV&3xb@TFuQ|YzG#AKm4b# zq2f4X(Qb3Mro$TbzqWx@kjw0^kiI~Dk60ExBbviff0sbEc!5vcZ6&I?n=6zJ={MKB zNkhrc4XJ(9qCVCvuAe&MiOlvm=|f)L+={C4PJ_u;gXp}2tkDhk;8u5wQnNtOco~pU ze<$&FnND#r95OLpX&hG5`)?QJuiWzqWV%5+5Zv?^_JIwQE_DwE_EdLkm2^7B7doi^41DJT* zA@^aJNQFDNsA6pO*R{b9uQ$Qj_tiQcsIQ_@JamjbOz$^=;~|~KKJDvkA|Ho$6)m~l zO!WB&y8&J3qB19?760zd4`>l`sks=`Z_q=3W?#7-1{r!0c)jRGmKKDwB9hkT6)VEb z2Oa3RciQue-Pwxg0vj{S6FHiZKCouS3ASTK6J&`0%lw+h<&5Tds@a^FR3mx(PrmcF z#W_}gecaED_in#4Ao1sq3XuRLQ466#2Zlr=QfPxX0%A%4gPDr ziwcAIPCxw#hs%(ck^*ocN!JN%r|^7(UgLh)1NXROtd=JU>QzwWB&`2(@ta}m8mQ9x z3Ok3j*Uh=WFJb%hdOmf-cpVJ7Ux8u@DMo0QX&im#n8>!p^yogfPxY&`>KjeR2i0W@ z?U3=aghBoqoKM-7=bLOK`&r89^rlcaP(ayxwD~FRi$SH;+XMP!CmS-Vbtjb2K0=X5 zZv-W{#k(tRF6eNbp23Ox_-BBgQZ|=gbwy0Mct%(nF#P{0OrT{f5F-OA}Cre5v$fOt{4g)9#*;9_(U%s6&o1YV7xj(b(|_T@O&Z1J;4^9YUrr&CTR$DS$97u$T{eci7~d2AH( znIAfpY_v6cZQw{Yy`^}j2mp|Gr@5m`n#D$el6&=4U%UkUF#pG$uz=!X9iy?VoN~Wf z@7-lJj|Uh=_RDM+Hk8*QnpqDf&O|!LY5+MXgMyWkw_ym(i&m?;*v0)bfPQ-~ebG5! z3+*9v{xMFe3r0^$P*h%1&mMiaP`Hy`Lz==yF#~;CDPy@|Q$s{aMyR-}G_b~_1PqXP z_m4&}{@C-RB$f!j6^#E?uLN&8VF&~RYxTQwtYd5b5#KuiTmS{T-R3CD7UeMOk2PPe~Nq?gX<3gu7$Tky*3iSq6pS)$hNH0gdDs zVnX&K!pk@8e-`G}=zr$`w!+Ccfle~~xyP0F_%ZdJ5m*nJi@ZW`MxJB--3P$2!81^q zeiKPSPj3WRCKW6doy+p>MAou@(`hxpMX-Q%_QHhcgtM@bJO_yx0J185_2I4Lz7m0z z8a8b+rZ88}D8kW7vaLv>dKnc_!q?rQws=_cBThLj4QGJtBtriT!a4SYm`DmOEP2i0 zQovR9n8;1Yn>6Wj6(;?Lc!y{QV_UMVXm46P>uiVd=%R<0r8fufpjYS0ap9um1#sR5 zCwcX)b|^!OBFJRXGfeUju|Y9?+vD#*1Vq{sAJ{bJ{WKVYpA&f0@Ya5dY5h7v|Cgc@ zw%~QE;YW3b$_q225%g405jI&>+=C4* z;Z|HPYemhqsbxQO;faeLK~eWi#;Rl4<1;sD)eA!B^D}<{Sx-!rKVg`B005>gcW601 ztSaxa?Lsm(^vBqW-pZ-;Lfx4Sh(*D*Bds?Qx(oSMT&ebw%$~7Cw1ors@W_1Pa+uk| ztz`7|tJ&>m{C`KHXo^-0S{Q00O4FPnFXr$uq6>}NZ4iFA_PGVqKY8%GLAhFZcgq z`gNy-%iM)q@?flvX#h-ffTFV>cU1rK!MUC1r#dD;)+o|zu<)B7DS4sncFme7K(5F( ztFD#p@w>$LkKg%L_gG-YbofH4&<+5ri2KfoP5QVC79DrE;q|}FrmqR$whG}zlJFkk zWCw4M`xV#BKbLRN+b2ZJ*Xo}{6aV4^C~5uHd??YPt-;D#kn582UW zSsv(yYTXri*{F%=gA%Udt9md_9Wb&IAdqlL0hwVd{?PUk&{{kW&;IP z;?&YWeHD|cZCb|P7LZ-c7bR-`s#%`agg+_1H<{pS0&YK(n-;QldF|0J-{KmCa4`JP zHJ2{b(&b1A1!0$3j^Avx2dr+7u&&8{VU?p5BYuA3Pi`;R=)T*t*zhPkK6o1&iF#bL z>Ain>kY}KSX)OGmAcRmt=SQT_!UVG?2u(c0b&?;k#`_|%XS?7h_yV(IKW!7A362s7GtIU+3zEjrnr{eEjZF(dSa$QiKLCTmLD;p@!{nc6JM$Q)?d$A1*mTcgZr9?9EcB`#o|s^h|8mV>?u-X;w1y}S zsr_+WrEHpo6*O-oBB^{f>dyQLq<`C)9)>CVd{7%bP1)+3HrHYw`gSgW{p!+g?Sf$^ znO-%qTP5rCEf*?$Md|f7Ku?M%jHEvsXoyzlh09)>Hi>YDk@&v#AFcyK6C5My=HjbK zLCu=NQ(C@r{_ImQC6hg4f-E~w(AVJ-bHPkt*Jt+2-^F)zE8@xyvy{U1{~!UmTCpYx z35-Njj^RAPQB?Xh2V3}z6ZCD)CHzCgCc{x+5A_4vq_9iKx)Mp0-! z7~OP`U-bHFGb-?pUquP8T=Dr?4CftunKg?V?Lb^WnBVlo`ow1+5^ zYl|W2K_2qidh1BoK)FmT#7~_KksP|CS+mgDKg6+jPyutx_grtqZ+zYL-NA~Ug` zgYKSv>p#C)60WE{l7sOK@kP2V#8SyG_r>16ja11=#(%|W2O42)HCCA2t+3sGK1AcG zo)zi-js3vSwCv=sf9!m{-GGIEK5-rrxJ=c1qx{XQe3^t==*T16MnD(ZG6^@)QAD?l zgDrJr5$$52itYZlMW$XFJ)RDKVt$(lA*3AsPG0`oDbPuSDIY^a0x&jzHduP)$Zk4j zZ>@X?WO@q^)i0J(04elJ;--o7du&t%G$u*pR4G*@)p5o^tp0qkTy4m_IjjAXG)#8^zsLWBy z8q(_SiRb~Ga0b6e{9P8*ddS;dlk`HH%u_tIceB^u9Tu2icm2{l zc!H|9Cj_k86mh(4JXl$6f!sX2HLMnJ(4FO4!(=4lY8I#W{!V6|F8ZN0dm5O|>~a)J za-R`TIZ*dV2#hz|S}_n+G?fo+wZJsDL@qw@3(R2417Q&0l9)3$_VMtK5kn|6g~#-3 zpVK7^;uu%|hHz>!VyCW=TRW1!M{#j6SR=)p zIX?WN?(*-(&nKCp>T zY4y;MSre~cY8`3AOj{bvMTAz4N{B9O^m`V8Y_ZXhO-F1Ktu9kph27nC=>Q`9peGKm>9T_?pV zXkK$rWD%=E2}~Fn?rtJ^pA4_$b(MSOsO8lajZygY@_H6okG^t#aq<9Y_A?kR;EU1p zym10FTObC!8WxUlfJ9}y|9TW3Xf9Hy=dpo?6itLZ{~17$rB4@6%$CYNMTEh`3Js2-)-T_MepWWW+Cdc0!pFocy&)8%?|Qn z6vFCEd+PGU6*u6UjCmQfWNTocOi^ z?cN57#NI7>(HVZ?>>q50XgRmNOqVMTG$u5P@I>zh=0OY(wOqlQTJVGRj!(?Dbok49 zo4HAY8F_nhw_MIby13Wy)_)A?!DvmO&8JsS z6PmeP%$oyZ0sLyJCRrsSedPHeNi&E%54orF{h8yhtr}NHFE>Y95w#~ghEdVaq;Y^9 z7!87hLdDhq!nwgLKc|B@wVEUZc%o?jP{73IecE8IGpP~JL4cpTyS1`54hy3cNqkCl zI?r3rPIWVTgEb{mm7#t{K`0g;epq-uv{@Gs*U!Nc=3mEmhAK|9Blmm~AzhO|oo4JE z%U!tsu8m%u9f%-qAUa5=FpLCYNX~2sfpL@})6$|RRjOELb>iZAeju=VakZ)6`P2|_pC zd_AyZHeK9WN5UOU8T?Ehd%;Lj9NAE9a_OWr!M94t?TV_ghUXkdzNRvxRn!%53#$Mn z03sNHWlHb{B&0bBrpX@?1YC)lxduHhEN=#U9}nmKGjZ;wLiwK6Gq2WfxWH8A4x=gruGi zwa2FTXH1kV4@0EhHPAS?F2E;Lz7ZQ8{+{hZDwWckP$<91=(Ij93~Dg@PCrxnO^MTK z5f?YSfb>F9@w@m^&Uhx=p({i5lB@r*W;C5m4AfFfM=7sgcRZO<=6}FLc_T;ZLceV^ z0uzl=^m2-W@L6jOhe|RzbUu1wojT^XbgW%#Pma6xruTm`ehqNH^`Hr+ZNqAz zqYiOP(KxC%J-b@kQwO0nt789SF@}BkiY0cgwsNrd&r~fgeR@ykvQ{mh31+IDA(np%cv&ZD z?`wfN1gAncvb)4kzCdbvI7=F|iAzw{(}7H_553Py`3_MR*!|Ppr5@w>~0= zl?M=X9zgDpbxzVhTGVXyWU^GoAo~ifU(_gf2n+7Sj~LfH7Qw|!J4`SdA1QC2K@&0I z31wmI!BDSE@*9Wl`(u_-w`HHF-As5B<@?T%-L5<~+$8w$)fzE?1xo%-2+56y2cTRz zT`lg$)D`Kc7yVDgAVzQxli(83{9xxTv6dlGQZ-Wd*%=3mnn<2 zD9D=(*vwzzd(aOA%g}nVCpk1mqA5MC<7i5^YkpuoNLfW|$o~h)RkwT&EvXuFP7*b` z68?z(>D4qbn02ZCgrNYRB#LDeyg_{lV{L#sBCphW@GFio|BK&EhAxIuQS|L=mAYip ze9^Ge8sm-YuHF8HXmehgP9U-5?S~Au+;8qtXx5vzMv-`Ik+zUo%`dXfoc!k=z|b)} znlrmpSO?*Ci^=!5`KjFhUH;@n!m%`Gc5+X}OQpcK>9M^1WC?b6nWmu5XdpJLrz+;O z++-S+d7mzE=nvmQFymXpoFp(%)qdfZ_%nfJk^u9?7!Y-sXqR^|gtc%rYL*+Fw~YEpz6yr-;YDp3(9-67Zglkm1WG*By&`r@}Vn_Kk`EHvTEji#n(Q6%*#QT42N*sv_ z@>^6a*k9oJaIwJf+5@*HFtNd~ZV6o>JN2fkEOL9^QfT*ZM8C5{5rIC!rdTZaNY&HLm-mhDhNhnRU2^ zjaaVZWKa$2V=lvDpUKo6nrhpxrmpnPP~{{+C!=WKiQLBQjLnh@v((|UA{_BB(J{e= z|4CgeW+zT;Xa&m7$vJ0Be&|9IFM-_uGB^N&F%mU_T0TciD)BNE+6S~-8~0J99_*>_ zKh2lkEUMV(mzS=Z*tLBWGgN4m-OgH5G?JGh4VKEbk`v{49b?|{w3o^Agt$fhyk)gW z4~eVVvDlbm@fe9uP!Tmn(Wrx5GqV7^V!4UJvqW!F{<&0(0_ z(yGWrHUGv9`i-kPQY=sT@-!1_)oeauOT(#qx~bdji_WpU5o?AP2Es<8_MT#L+nQXK zX1)aPK_qqybwR}pk6|FPn^eqv~bMw2>K6+2UmgmKmfDS>Likd(K zM;xNaw|8S*T2ly-Bg@5$&5P#PAS=*RRUiM^bEM(qeu^4zV=A*J!TW3*@lTfs@p|F% zVx5z6G`*aQi%TROygwo~rM1s)M0*=)DRuD%w5YTz8a=}{S63vhxpqegi?Fm10jPQ6 zu!Qes5p|TdmYez9XgNU-3E@?57`>yf`@ajW=tKOqcmCaq6e2A0;)AJ6E3^vw&|%Jz zICo+Dl|ctP4>;za~4k zCBCtej%1KNj5xIwTb;_#HnZ*>0OKW8Vj$tlcp%C`Vs;`m z@X-hHyorAZSoSGZS{tHNbfp}{C7>MXfm{^DJ|_f;7bq7ETYX0mc21F(ODH*mz0R>( z&$O!c5|7qR%X^Y+(fH`Pio*vUqY4uY4e3*7iPvcFovlJ@kQ7UXn&B&UwMpAcgmY?s zms(C8=tEY#Ew#kk>&YGQOlS)@Tt(-hmWJ(EUaw=?LSAvR7^{k|Y9goaU=;nbv^# z)fSG7l92V_Ld7L(t;d=5ZNSqo=DmNHIfIPc#inSTcL?e*c0U*))?-7RIJIt|=M z9^TgpiKWc<$d+ym?tmFTUfNd%5=aA2HsIL;a-8gUWtQY-+bdLn4;mr=FJ<6IHR(;1 zcj^G!Wp8nkW8poY3gss1zvo2HDd4w&cn4X&`lwgo%Yu04j-so7HU~q}s4s5o#j)wT zqXxwv1&Z%7AkT&;41VGW>_5t>_}5=$O5Tq584WduQKUb~(g^ZD$!Yk>?=tM(jt?2v z&x|>J$AuA{UuC4;nPUddiz3tkQPW=vr4Jdp0LK9C-yK_49FNBSOD7Zrc@j48k2sz= zUZU1~Yv#;ED{Jt4^<%PwpfJ`UC2s|;>|MhmkXF9(#@2(a_v zJ}LooRg@P?T$d1CSziayChmz}QIzaC;`QHkFLDSbcfF>>s+9S2Y23J!Q4%@aBpeCy z=c%oUSdCP=%Or-YHk~4_q*jzSwbr7CL4wE+nsw(%=82SdoDd6u6*?{fuiW*!GHF2O zGOfH!7gdxy&-8TrW+lf}+^(WaIIR6Hb@sfX^K*Ek<(_W1SjHYyf=lz>BW z_~6ZRC=9-V_^Mp&8@BSMmC*d9!IC^q@L#AqP`LbsJh@Ix+A0yQR7qPBCmO4|cY4v$ zLPe3eQXORc66Hlv6z7z&%_YOXC%fmUEaQg1YO5awqz`6NNjTz)pe!AfbpM6S48L-l z%FsG`W>2&Ntf(kOPZ3ba4PrZyr;mphba84vB)0FiymvF4tVznw=K0geHKHZ=C@&Y5 zzvA3?MX`Br`orFe_;m91hO=}@&=pF(gyPI+*hQzm{{+9?mR&;EgPnMkV$bOmTJ z%zU^aa{B27J7tqcg|v}zYrr_LDuABc8bSUCW)5>Ak#f;BQ3b1nnWjlXpajR4gt8cN zB&wcs!mvB5yO~CFg`t$&{M%$BAri8WVd14PWZ<2?Ac2iCUCgh%n{yV>M0qjp2}Ji| z4nJZytFXpa3X|6XfB0r=(S!MA_Zcr9Gs6 z$pq6mDX*>Zh$J^{xV_ItWEVu1nCxKXxs)lTWFZ<3+|SE6wqINM_vJ9e3nX5U(-e`m3BUi25^feL{I-}6j-EF^2$g9h3Yvia-St$*S9!3Qcn8J|7D7K= zcpBE~Sf0`*$72ymG7pu70@iv5nM%_-j4zL|J-u-Cx^~U}&ywdQpI{hIvAt1lZW1zN zgW^3aAA#hzf?y3@Y6rx{lKc!aA^_cgVQIG7T#WLbJgbEQt2xiC>tpM;+=y#&!$H&u zOni0fzpnmMN!cdG5EjYO)ZJk?R$7bB!T7)Ox=Yd4AEo`WY;Cl1*Lh8ZeYRr@wLk#5 zltcnpxC5uj*$Y&7*yeWO^MbWq{7=qQOzBBYx$fx?1JPPQs>*xf)V(xEo z*hi3tW%(*zmn}}2lx2)bGbE(x+o#Vm6nD#WnU6;2Pq7P!9&?I~BUkZLEN9Xqu+7%H5~&@t68?$emkr3c_OOdap!!FGCJuTQN9(|T8ofrpfz2Y~hU z?J~5T?epYJSGC?ae^{E9=LOLcOz5qLiFMeNLW4FhpL?bYRuMZ#MH_n-)DJY5?!D$Ov*JLe{+8dQ=D;^kf;njZy{f|cZ$ zq6V1{gBRI0>^K}&K|=>$Ip^Zu@AY{j1|p{G{fT@|cd(-6#XR(hEjsb~bNZDgwh(25 z4#x{gb`)e@oWY?>idp*o{f$htNXkHZqCsD#?}!dm+K3B`EvNNlrzd zT(K;gu=~7i?(;Vp=y8~(Y12)W*mfK5-YwpH!*?A7+xzM#Zru?7gL6uAl_i1%8Q#JHFkGfT&@jFnuG+Z@+@J zz*wE<;uM~xZ={j$I>Ngk`0?QE`LjV2k(Q!psxSwcT_`Vnat_Jq525Z9F>EK;#`GX}exuW5S zKUVS3&vqUWMVW#Xy+v5iH9F#3LvodU5HbP6KE`mCce~bF8uUnFWnYcC{;J~yG-RC( zgh;4w>5uwlAb{+HdS1BoOSFg?&+QaBh?xc`U8m4seMgqz=s8jOviB3v<75L{^hQg; zW3}-oEOj4bE3CO)=pfC%Nvv0z27D-vjUs4%Hf%AnwmSO5>pn$eQB)ig;S;dI1Wi>w51gCExyu z3)pvhMjOcx5wjAkvE?9vFG+Glr=^1faTByI9Ue@iX%FQY#0p6a`fbf%i2X09Mjjb=q*=m<9Vyb4VPcy#IN@yb0IRzs?~%F-`B3=AZ&oi$Je zowEdgV~rl|JzJBZG)Jtiw&K)yx@yE`M4o_*jA_=SM&g1g7?zy+|2JUE0ic(Rn~32s z#1`cF`*WNi%;8=VWbUqk^(EfSPj-3WR!L~Zb)y=BCBK%Xl^7L!34b5ku^~psWF`jz z3`dwv;RFS{v()2Wq7vTaM#N7RrAhoL--We!zj^_vqd6(zyHX|Md&Db^0A-^)dP*ro+VP}@D6{W8Nz zt>v+_R*9l{=Uuq&nRFf(+6Ja5_0SYo{coPMZe3^tVX^~CU()U)U-OIMXgK0L5YqfQ zn9$(391i`C9fhe-nc`7_zKFLYz5UKbR`7P%I|kCoob|Md_+c*BRmP~c9m#DxL%dzd9R*4pC2Q0}ia%3;n%6)Y1p_m9zrX3XQgJsh3&4h7d39{el% zXWdzhNL$z)cosV$EUh|s)?31fl(Z+iFXV4j$)Uap=ct2?hI^YMF0_sc#q1D@25&fi!W4&{rWjwPXkm^(Mbx~ zjNofji82Z?Tt$7xb=p}Mm&BcYu-xNvTfg|hvL?lW#I0QwX>~5Mq24#^(StegGk~~( zG4(NuKIxK0Ym6Rz+JZ4~kx?OyHzyAQJ2=j${1n3;urdFDi`UWr@NZ4c!31|GnOJUt zs^-P_lbRKUN&_-)nqKd+g_#nUiioU&N=Ej~d4}1tPJaaa;8(ky>D#W z+EH1C$?}tnWE!N5!t?q>%G*Ug0y>DlQ~6n(jA{8xW%FRG`o1+(7#gxC_Ef0eulsf| zq0(NHW$yr&hd1UA*|N7Em&Z()_qu3WRBfeSlj-yK6X}YSXFnTFmS=nRTux~}mc(TW zAD88%IdT`N6KDQzj~nKXdlSc1iQ8^wHmk~viJCDV0q|BJ1SX9^eahFo&Ff%I>s|&s z(X1?;BuGH!YomX4`8cx#ER1At`pc379=w0-`~ul}H}x zcG@Mfq6K&e3=I?Q1&caRv`zg)zoa8zyc}4O!$X_h3zpns)p^N;6e?w6F#v^(vC;P$ z-~Js1IpYD1BpQPl4_o8BbS@A5q^E?$rEhA7`XJZ=xWv}Wxpan8=5YRW8@f~LGor~C z@*3}x7_Uf<+tBR1@#MkL@UsaD`;!XP$2IuQhNR{-{I#hGT45vc7 zKVj^**hNmRf;cy~>gcc-_dXWJKtoUgKBhlm)}ghD-&Ht*xRoD7yE?ISlMDrcsYt|m zjIB?7Kk?Sc)7FB2!@WE&&8GpY=X?2q_YF)gg4PP##&s0MkK@GC1imayfj?~$w}pKL zvigZgUC^koI1W&R*vL3nti){d8gXTKTs~a*eQ<9kh8XbwG*2*|Ar@Z>EM!2p?+06* zh>q5f%zZqKe*QjNy~v$U5Ec*=CBDHBi1{OlUZhJ1{oQ!gJR=Da`MOmqq@L>>tUA0s zb|Fv|DT-B|6YqIVO}`)P1R&~jD*Q-|2sr7=J+jacuZ5}&eq+R}&i*PMdQ_3E74gb( z(p<9B{ns5-)106N^_cY~GC&(hDzvOLK+{Ga+xyWIaoFPEK8n5;eJL^Txfl`P+u2Ei znb8BFy4vHJl#vZgS{*J0G`bj4oYxSrcXDbEX6DU!1n@te)QS|!&*Sfe;5G(Noe=`( zvv=$Oqu>cmnAts;m$)iLd)Y-?5+3&*KL_bxu_eV~~ztr9?wAwXz zE!Ynpk{gBT`CGhB_+*Xw3;bs{WlTjr^V%}ZZ@0&IUyRupC`dEtn2o;O!VUKVL}9f}TBOR*rp5S>w>u5MVu5TlH^7D8{1K|WbxSM7nt#t$w3S)_5z#?%s-m<9P@=kW@ar#)8U zvYu-=^4O#%lDJXtki+_T$lNuEwzYsP-{w=o!WuHqa*6`fPUJQH6!=e9OQAUg=UoD)nA72UqZ{?0%yj9lUOGu$}er>2# zX!RGH*oXrg+AsJlx#%0^=_-L1(oEeCVlciJj=mNb-B}n7LcAs1TAxi_?*xGb5Q5=W zrhMGuL#eeY8Blk}{kkC(?w9y(Y#nBA^M{BnIK1TwPIopx z$u_&A*-P(lbLfN0pMk8TDLcO|dER7wS| z`k@sov_3a#-O&wpKLDkOFCx2U{DaR5?p+B!enGV zamBFoUMO7o68_(q@SU)UruiFD4$v8UW@KF3Aw6{7Zi+jM_>?LjFi>O7tg>13uer?V(^%1xncTh64Z59khUmjrQZ zsTA^)iV`wnUlC!xf=LfQdQZbCvUvKiiv6(iYfH?Y(Y0nH0?1f8?+KUQW2uGh5Xxa) z={qOf$kL#P)YJq&>`(NY1(Sd1S($z`>}sHa|IP543o0;?3^IbbA>djGDuiloh(H#G zL0jtQ`0@_IM@(vw{dN`wye!C_*mJ4dym|#^gQKGcROJwkj+zI6ZJN82TyqtsTgb zXlVdZ�JhjeX~FdYcGA@_0J}{@M@@mFG(j4>V>;w} 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≪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 z zwX4ITG1Tz4;feZ+X8VV7==rR+55eQNMW;9>*}N2yyB%_YaiqcD-MXa&gKk?M`Jg&bCs4vNcXuGft+?P+l<+sVt@kq0X=BTr?3Raz|nZ9>-{M>}Mti z0^@S_AFB~cGy!>)e92q5F6^8?E&(6Q#xSl^~2P0j{7AqWmn)p z!cwHq8sK6)QBsNZv>YZnBZqwb_-gy|%2;r)ZFuaDj_&28F-xvibKxbfJX&X)#KS^2n0UkngPx~tq6=}uQpls`ISwnK(Q>)5Xn%U7Q(Dkpl;?+mL zOlWn_i{bXP4%B;W1Dy5{&QFO@7$l$}p}QB5nfxu9$jt;GsM z(v>eS&C$LlXQoIs%U7`TB&^Ns{}RY>TbpB}A_tA17_2!JF>?^BDQX_5@t%afppi_) z_VNDvl{n|sy@=J*PC_KdvTMYmtbS+OZjRa5TXtY$8I3^VNy9otce+X}<}}>0n^R21kuvoBi8t zvnbQ&->YBv>{)LsA{96?TkjpPh-M~~A;_r82)x?s8tHeSOZ}fLu^)&yoxEP$V}i39 zv|PJVzH{R-4Qa+7Nw_jgt1MM0FDq=E^_l|@Ko*ilP z$Srh_c6OI(9J+=xOf*?^?K5&Sm2#xBL;070?A=uG>K8v$7Hgr;-EQmLSH}=)Cdz(}!Upd*zsPj`((*rrJ_d7+2E4*;>SkuKALjFyjM&3L-HA41B z!Y^Amp+h8Ed0&b+y>rbcR|ruMamT5ik5%boRr}P1v~Tu!__qOY%*2cEsw*oVj8= zC_WqW%8xIT#hbUO9h8ZN`rlGROgimQT8YoVb4Z9Z&`Ea;grMn~80de{8&C5h zoWWmT?>Wy-CCmIbsVX24>vcdpX>Z%)Y-jh~{im)+jfZ}!7g$nq%zhlBJ)r2g-Bd6Zl?}Y3;H9)T zK4`kP#VaOoaM3W+&>MMt&F3csnwYF0OrRc#!l06nqz8vG!_Y9$PDT)Mcz7xQr+eF1 z?|{|P`Ju0%`=7A%VqsyF#;+NT@2#7(tF7(+hYZ-IW224@FR8W`COY-Gr6Ck(V={tZ zBz{sUCC*fD;{Wd7UN+IUWs`X+*rSq;VxMCwC!$fFa9vMlx7RaA{@cIbf1JXH?7Em* zMKZ!uzRbzV)J);hKC-p4M5BGfu@PCir+rs53w=y+g^DtcrW!ahfTtTlP)TQ${ygmDvkYJ$G&8`%$wy$t|GW?7TXW-Rk zP0C@=)5})jFDIgX{?+o^EG2P5=v6?y`ww@pq*21?OQq?!u{_u4`ca*QQ*!v+Vv{4Z zW^s0P$@waQ3vIjQTJ$E)f3yW@VyT@TA98Y*lr7B(TBAug#G+zFmKFqbU1_pL>|b4C zso8&nmJ=V0A428e;FDDI(~HNI zaGSNR6p1F+9v^9d#R&#s3dXn{Is;MG&xWq<;AGMXt-Pnoq&U1+skVxYPN9T@+PBo5 z*jWiEs4L%P@l$5~rdGkA15??39k}s%P^qosY?t!+2Eq@eypnddw6yEty@+Yu_(IV7B zAo3fA$F~}X%@5lg)qE%TD_ghZ8PW(0GUz<*t>caCfW!&?9fBidvST&(8tGuC*{6gb zkqE}OvD-RtzI3^p_*?5a%TvgC40r0My9dWkAl0&9eT&xhw)WpTu-_iIivB)<)7#eR z4`#O=7u~L>A=|jA!fW3`m4NAzx}FJ`IaRW zKdUgR-(tF1+30gq1N)Exug!&IjI-VZ!j>?QqV|RxgOwM9)!K1`*;?**^IL^^CuD|s zi{9ovuQu9kY{!J~eaed84Vt?oojh7T?y;Ooe(*p-4Xv|XBE1vsy@&64A`(1|4|@3urk7Ic2NPxnbct$zrr_7uq_$E}PNvdNLw@md;{rpKB|2He zo`-w}eG1*$5wM;px;YF#iho!3O{{fu>T$;U+DeAld`xZ@|Ev72d}6zr-~_Xk%|<~d zmRoV{Ekd4cmCLKKQ&xtLiWnbQ?`~qe_*^X?nGig%*a5$-pSrgGWRKcm0-J$7MlN!M z0P3OZ$YFG5394~rn;k7xgC?POJ7Y_-;KI(wIo{G$Kt!8Bk7xNhB?nQ=jF1BV;g6e_ zihKm~PQ!LLEi5{Qg@mTyzp{4=*wXrLv_Q85yEjSjaEwZ#e%xm*N6(6=9{NQ`Qs=9&L1OBr<=S%f z^V_s%vuAyz_H)&B1acX32qE8wQ;mf~+(a-DLmUg(Z~0 z-%eGPZ!;_${y#8e%M|SYpt9KZ%sM*@@GeC@B94RgZaZ&jvyXVjXC?wtz@JAZ{PX}{ z8}l7wFTm?qP8_^vTDGVZuzk+96EjcqFrsgqfz}opdM#FSUYNXdWo5_vk6Fh5Rt}nP z^_G{XTmOvlhWqlkqH9bjK?=IBfN9IR4N~q^k=dQ&4&wcjbH!zNwtLpy zPP{;Mf883W*Sw_Odx7c;;dqH5AX)VBrsrRdsE5Nulj}9#n?0WV**>y{uM2lJYJJy~F`U?XgGI zO-EEUV-Grc>xplfx%V5&q0I!zAs&Wg2eqVv`uK|~?dt0aB3>G4u0CF^)22p2_q~3C z!`7^gq^9wKaeO6u8i8P5yG~&Ab*+{XG4YYaJ!j@45G0rXh<3HOn;!%|XppDk+o zS>tL3#cWn}Zbu%IdHXe~+jcB2EQYk8ln~F6eJvK@lO?Ll$QzhTBarN3YqCK7{{Oes ztA(iinlT`EV9*CYW-y|asWw_yXsIpJx-!(W{2nCo;2S2X_6lJF_VhNF>l*b<01Y-# zN$a+Ja+pG!*it2I_}J|PS&lUGStj3eToZO~=dd_~WdqsXPi9$y;MuI%i2!ks9l>|{ z>?2s3U$!t-!&-t=TL@2onnG9tZFc*pcwUTkJl#;adxsc^4DON4#4l&T={ZjkIHCc8 z&5u_%U;Wdy!&8_ZM3)e1s24C2GaUm-gCsZg$*39kLAB$`P%StSWJ|gbdNqmt3MdYx zngxChe^hFvz)e@r*SLpJC3Rm0CjCKK;J~h3u~4wpa+IKdGw=N-<6k#Jsj261Z4L_U>x;LcA4`mtmM24|B@H}h9@vj@AJs}0V)M2AEW z@3hpyTeb3>zuA$d(=br}IsVkY4G>Rl;6%73MUI`0_D5c;v)AdTNEP5l*}jVo4dkH` zr1_2RY7GeDyrxcVhyhT{<09 z#IC_;riDWhULrVm`+F881HqSz8#^0aIxsDxxX-9~&0gd`b_#*f5a;a$1LH5J)$?1Y z)*dAkZK51VfvZXXjP0XdF%-*ElH3p$=EXSe!Vr$w(!2p;wvYX20^KsD0TMbJpg_z2 zsCvt|HoE71I6#XQcXueIKyi16Qrw-=;_j}+-95NVad+21u@;I;iaR6_;GggPd-FW6 z^4ZO1XHU+YYv!7rnKN7X>eg`-2ry)19n~}BGs~$)JYAbps6A?D&x#5%{O*AJGTOykJ)rkQz2u= zjt}nb`t14a`t0~@+w2|fLbsv2{@ecWh@Z%A&yL7W&$h^CU58Oe;-4s{`U6Bg~Zz&A4~Z9yij~mHvL$_b;cq+S#>(ITE~@ctullY#Jo2 zv!n2_%Xajg7c)~KY@>od zkM*!b{P~XeF+EpBG*eZXFKP0& zucYAo0NYIx#D)19<6;7aPTw8nqF!7#4W}DwtGQT zs=LieY9aZ%#!geBg{xS}btA*n9>E#5pY2KP`R(BW+akdnkgxnnua5AOT%gU-j(b68 zCw|Go^XcWq$3_apn2nAEuQlxe(%m)tjS@|RZlhRGJ+#NC3wc9K8Qg_=x_8TUEY~G` zz~6>cGQ`t9mU5!xMVH91-c&V%ttGj~x`u4qJp~l&XC_S3w8c7NUi;i(b$`?@UeH+G z|9Stc#l^{cP=`m1-ca+kb}#o!FLxAwe_;IqS+AGhtFF(IIgrSCHk%>fANZTQ*M@`! zn)ED-9lI@uc>xcldb)+^Sw{1BjE2j}xty8r51G5=&86F2xpyB@kTfAWmh7u5TEI$l z0)p&)fWp&;U;)KimxQoMrs->&Pi1+;?gWmlsm`nIuNmjn4|mfaf`XQI&O}r7<|}0D zCWAc2zc1Qd5I3|=e@3>ca0T?n^!|n^oyf^C1hgGdx+3L&Z~G;*(^{bj@6ns-2xQ&7 z_&#S9S6=U|5n@j5xz)X}qq`}S+AP$(Vt%$^!u1PrEpROLcx!f&NcJp6@PWJ} zms&Fjyr%hJsHNq{U%*q=TAuCO`?%aD-0*8GoNLtG>Z^-KT$Pe)(X747vQt?)H?#gN zgVW)Ul~=hxbGZ+VC=GdgN*}6zuUg;4OfxP_97-!9)j@KS-Q$wgfa?Nz71r;{Pb*C8 z-Ad~gh*G$7mlG0Mbps7X_3j-HD%|3nRc209**4z0oCQ>|-l`hz-%;_Eu#lV}^~%rI zfpI3*C=cr`W%aFGnm;B4`B$8k>CgWDb82vKWZ_hx2$fz8!994ar(5ny@rl{73N)tv zGqq%I?K5~2pglvO-)gkl{2k>7w6Vz|GCMUvpOrgb^=yRv>Avo)?YO#&#DKUCvsXrS zLGId-AEtD4f6e356LDRC*xb&x@8IB%tdqJsfl=$V_M+&!u>G!SvcbgDT-2`CY{TYM zG3e5D)K2wq`DxE?TN*HQUKYa5%X)aeEm-RTg5D>{Ss#uw=5BMU>fzv$ZiM=A9DzaeY+3kAYqFX5Du{ko!xK%*yUnCNh0gf@R6U-y?o4dZ+RV+G%< zo%XTRyuamvY`r#zwNI@?YqqncIyTd|1K-WI18zv4xyzMI%g)AbYp*O}a+ITmu1wUuqAr9GqXX}=NLnB(6mgEL*u^=SsB;2C&a z1#QieRr;f(v(SP3tx#TO1K+T8gOjzi!@D{^{pr>Q7II#bTHZL-x|jGLv&m_QI6ABH zhQEa(y*dm`&#POTHm@^YnC=!Xv9)W%>KaZ=mW7V}xVo-4^-Q?Wo{d&XN`4b{RlXPulY%EO>PGh|Z2RSj=mx-wh! zYrF4A_FkSk^M;s&R?+jyeFI&}AT2r8VoOZ?TD8G~eT+N%5X2NUX^^8ZZ5&V#kK>N8h2&5i9jYs9s>yZb^J zi5h@815_ zuldzKU;C?|!qm*3_Kx96olcP9_;O9fxl^II^(m3IpxypVij=hx{pzh&6`BGx??mx#qGw`R>-O$9db`D_Xv~uUgxX zy*&U$gzKQ4q#oi&m1l5K{@v@8@S4KjL_@?NJHYFP97dM<0Ol2#2>|rvke@*V1);QSqP_qR@{(X$hie6GH>YK~mX z=`#22(**IYdTB1(s~(zUvgF?bMJrLN zDeXbX!GiDf%eKNswx)@h=n?)mm)jJ7GEi~t?_Y_v#b4=5<1ISviEI_UHVKOy+YTiG zJEtY++1P)&z5|6K3Y=Wh)4#-6@gC)z<{Iw0CszW!ZLM0}mlEbr&3d*si+vWr|ESxw zGz6AQjQDxP5c-=_Cp~#n2YyDRTZ%uvEAO+hiRE=--J^Z-Sk_MotJigKpEf&YEUXHe zxGKH>DpFRvytTBy&5DzY{dV(Xfn%8oY@yZ1Y+B_J>-wj!{F;VCW(L&~xg&nn&$3LD zYEyNm1=pG?$^)V73O{59ODnVTqhVu6(N`6{fNwSU_;yvRA!7?3S8ScPo~GY9Se5PlRi)WNRp?1(pLrtB7p8B}wl*YG`9dEZ^1e}Bu8#c3*@mHy>SvY|q& zHI7*Y;ph>N!YG(0lE)SDn08g^QGV~$#3&eiTFQd5>JdB1Ov1kOfEmV1=wYQ&uW7pa zbd>rJm)D1v*pBbW!+?}A^cR{tGw7F>MV^^^`S}Th-3D6aY~Ufb0yHP4x;-tYy@rIA zi2hjaU$wxEp{rro`|)Zd8!)*6`TKp^+OL>DW&Tdm%sv?}I7gFhHEFNTr%V~h znAD!3UAqqs5LtfewB*=usPS7utnT2HeeK%8tq5NF+Y;%TwxhNc?-}yca@_e{D~zKx~0pI$zc2MkNm2LJ4`gq91507BikCupwe()2%XJBaS;fOe)iWAR#Y6G4YqSb#e8On~8EFAgZW^kQk=#gmX<6hLwqoMvY>Oxo z(jC0?wB22*9XvipHm+qxo>|7*2Ca9%f|gMyAiOe@# z(17}THPwYH#v{u|%2iyhP|wMOVFPAbLF!kE+3YMu(F@5A>L`i-L|Z_?gY=?hEl4c_~H?#uJSjV7Cf-t{!LSt z;0+~J>V?s~?VylqF@7Jv@gB?dMtOnI97Nh(f(!jz(yTg)PWRv_VsT}*7frFc!laa!6U})iPX`o=Cr(kO>TwGLaa3<4)U;e$w|)oy=} zSksHrii)uj;nGRt_f_>@9GTuc^BBF1>Pj$^uP>?-iVR71-^!Q0=yDLD<#=3T@wqPD3a7@F~@T^6`^rIH94 z1Ec(So_oDGejPixIDU!9OMAh$joQKOT)=jc+Esjh<&SL2d2H{-npOW7xT)JR8#VSS z^=$t4ZB!rWj=+#gI6tb;8%In+cd1KBl9*3u=6pkP;b*AKxb<{B((_9si{Do@LMWy9 z-v~-^gwuqpqvGQ-(U#Kg)67Wgh5jUh*S&lR4};eiWa6IFiAZ~EDnGvx0)s+LlCP3& zl0`Kw7A=OQ_rjvRzP1*6Sr}MO1*HtiYDicbsPa70<1dD4qzut(Y7UlN*aXlIv^G=@ zTg?B9l8_mop+63uUuxMLW3L>$2&*V9mt}evs}gmqe1=a>9o;Q~d(QolPo$n1kH-e! z&-5UhFUH41b$9e?S;PNL!qR%o%hEZ?!n>82AR7+PDZ%}@oD4+Sh(u~(^otS9bfg5h z)T9f-tRh^biJ`9J0jp|Zd-U|FgF44QC(^=y%kVU>&8 z7O3?H=%Nm|9j`i3T&b2wOC>R-rjca-7;_WpQjY(utSak6wpEDmlO#NxsZ=hK1Tol< z=8ZajY7*IJt_eVzD&nQ&dFdyBPO{W)>d;;=wqUrz)kjh^KBO~(x62r=2$#Wl>2X>i zE$=Vm@Y3-}(FP2aw3w;|_nQjGm}M~_11UKY;g0euQJ+y&<$Bu2jnNcIlLx50Xyp(t zOX$q;sH1JWn9r5osVX41rOp2=P2)+DoVA{w_{qX`7liisKqXZ90B|ajL>LM33%7n1 zPD;wbEl63(nC>5cZ5#dwZP;qty|@YcpgH_fm-b4&CMr6EK>PB=Ad6DrwSD%r!Pd`A zk9RuxVhVd#{<7lo1`-Q>fRc%+n6WtHLQu!hzN6}(&Y{+$Q=xu%V_B3wZ}MC6J+8%2 z-%#1m@{rPwos+7%mrH$z48{BP%1L&VNH24Zkhf&-XmJg7$*3x-h5dDZNgNZuFKSyJEk>!*tqp@^$vL6qFWf+!Sr*-;$NPtWH>_|-O23RpS8~~LnPr($8{>`gI}FBPL0CHs1j88e8~S`9xY_U5 z9YYnp7K`D(2@y&Dtx!^?&ZYY!vADh1gjSbbeT8+l5q*aAak%jl!R|@Yj`-oI*VWJc zhh63posYW>g#NhRXwTuCLo&3JxjVBr_cv6hl*jpJDtLD|@i?QgVljCm`J)zBGFNn0 z&Wb4`rb3b#4BfOhnO>R@*0J~BS--rW`#{CI$vS7&zvFYtx$;aENFS85-BPId$w)va zYuC+CDIGIib`opSaWdjVocIUNPq#Jau+<@|6z4x0`aw{i88p?s@+lpe+>pTi|Ayrh%lCVGy8%4Rtx*ehz_EQM=Ysf@>38e#ri1l)Fs%cH_5NT zD@%bn1|J+dJ)$~#;*SOWdw;~#$)?X}m-L4vhQK36N4Dd-<+`ycSE*8?daSBg%S!9} zZ}mKE?vx^xX2{^s)h-F=mQ^W;07w(vKVw+%5hUrrYY2_J2DD*gpIE%hoit_t&HRrk zd?-*V=i$qUX`gkgHPh5AM1v zvZE60Amo|^stlEfqROI^;X`9QV&Y@Cv_NNR=SuMXV4M26l=Lf&D&F)lNaydzZpI45 z{IFoAE>aaZzUzlVl;?J9+}{^F*f`mA@Mw1#bt(Gl_-Y!^FDnaAj5PH%;WmBzL$S2kcl`#c1!aO#L4nswoz0;Q zBn?@mdf~qFZy6d52<*$+n<}^rvdjLK!;GPyq3Sl(u4-?qBlh-sb1Wen0Z!&sAa$U) zfBmC-IwE&d5qrhN&Hh`3Vv%H&0riML8NN86(F?^3R$mzaRd6V_*sd6xuAn3Jw^Eb3 zuhA8iUE+Q*XJ7k(e&wAf0j6|72bv=0wld#7?_$LaxH!=uBO)M_Cd&RFJtIs>%=8oT zWQB3)`lf1cZZ9p_o;Q$&Jn*SMr$7Hy_!aM!w?7AjBY@uLZP$AydtNLu890frh!5|` z){^Mnf7`kw#gv4ZtRnYZPNhha^qDX2Tg1H4zmKOZe_6?Pnspxy%J@(JI(|%SWia%V zcrjY2{G{!vTb+iKPCK<;y;|+dAi4fE|I^QKdp*-{Z?aFIw;9} zz$2NX{G$kB%Kqn1Y7A$L?09`LAN1qBk2rvA;w<8kk7xJ{=@^8gG0f<_p(QO^Laa#X z=1Td$G?%59H9Zg%e~nkSe-N6Q#ki}>)Dz_!p&`}WAqcZ)K=QZy;pk6mI`sf<*xNcx z#F@eSh*L{sPhc;favHjwvzr(5YfBXw-E>Vz{{Scma%g~Sd$@bl+1XFe~Qa?g+Er($p0~I6S zED$EJKE)&@J4HK1<@))WNVdopfAe)}GU95Uj;r1}WIF6j19t@*C_EeYPy(&dN`3bX z@&+=_;1cb+Knaj8)I-ZcTSM1}95)Sktj1#&N39rmD{s3S?S4}-umMRuyNgKHeA!F$Hn098}&{T=H&^P&g_x1{6cdK&8<4 zMhRJQjE1Gp(Dkz`f?^gp<-?@^k;M^*hH!S`j47W2pCl1&E}LPz9EeJvG)0}-a&f=n zpnad45QPB!K;c`cYoA8tqwJlWc2$#9FE6%Xuq~8bE{wt&^M^CZOvD4`4a4Y9F?s$M zlD-TlMr)3Lv?O#B(gB{m5{wifQ{IxlmkqajN1l?}Gv7$%IhYu+l}c0;ltKeCP% zC#{lHD^)W<=u2+w5S}MJr8Px{6uJ>8?WB*QRTbjo2#bc5zHYWbo5Fp~Y4B;wY5i$$ z+y}2r>~!iR>h#Pamx@#`9e3T&+B4c8wNKSx^)%+9jYoR8L=lH}=-xjd=VTKh#wdW)*J6G2GCKfgc#ZuI)~UfpgS**^O59P{I?%5M4eA`vD^B~o;A<55GZ>)_FxGK2_XrTlzTX_ zXD8m$wh2S`d$~k^n=P@^XFXya7=q} zVe{gqZRDrV#)hW+^ZaJm6cpZ|0cuRx*#ULmE_Fj;`@}Z!ep}fqt?06+$}{OV)|pVnlWJH0*0cQszc1B^kKcv7-)sHDbnn)`XUB^~vv&bxbWg1T94CI-Te1PU zk84klgO5R&#RF1X!25uri<1D*eg1Ng`fK@1{fi=r@x_^{y`BmPu>?s=Cso)cpEkIIw!`fis~)dr?#sP|TCfa;y- zVtaXz@oU73+Y{(Q_mTge`3XO@X0pQvC5{o>gZjh-#VxF{V6s5w8*g+x_ASL}XD=A* z@_nS=`B`TJ_@#jUQw>B4MC?Zu+HoqN0QCmpi!%G-_15^U-qxRN09?*;`aA;s9v%Np z-)*c0iOZk@kAn(d2VW>N|1e^b(W$ih1rW&C{ye-=eWN;~lWg zWj8O7DB>#c*ryOI8S>C@FM?Ro!8(}wskr2|S)gM;;664udn-e{8yp_kDmc= z#=4n>RNck?KnY}iS?-9tFp<))lz~)DgB@B!#2UUdC^jfN3H0*s1^ouug3muPHdeF1 zgujHni!}N2Ojg7rn$iCwBCLf9agdbk9bT>0)C#e~PC>o43bE7z1^%) zz@>MGFK>@k22V6s(H_J#^2&oR+9fD23h`=feQnG{nLKp0VsM^ee8*O;x*7)Pc75Sn ztZg3^b%j%qHG?=d9|`G40|fMqC^x2p1`T}D7t3FR7bCex`JNG`JTg#ykk81A2 z@2FE~J_UTC$e^iW^7Y8Z2&r=19UyBwjAqKgb%rkdT$+~2`yi1}z_D`kJ($9$4AqDK z`sM@fHD_=s2M#SZT_mw%V z!PM?u_w83>hQmB+S%jW)^|6jj-8bn%wp+yH6_QvodP zWvEd+Wq8FD7B1M~@+AC?4>c@wvS00370^VxxBTG%r|dVjl3+!Vfwq9SiuJUezjqL} z4D#2ON8SN-u7&5@r{L6XrKK_M%af=#KAbqDe_5p1O=L9?LdS%*SB-0AP(NScc_}C5 zU!G8*HCYj6yQ1UR5waag1rlFW!U`3hhXH>s&4H0Kmn(vXKMH;=O{}=i^s&+r2^6W69VLxKn7-}&|1{N1(&<*VjV+-Cb`ht zTBe;G04c&mV-Uf~AB?0y4j*3CG6}`k=CYm52)Exnc$J)}bNDx$IsI>)f0-uS^Ev6O z$g}E_Um#EH>&iI5a3!#W1rqrBW?Dnq`ud92oCWMP=2RF=cH5;Zw^sy&M|58L7uF!? z+Z5hqaFvLGyf>9;`IREHl$$1jMIDBYHLCE~5Zt_Dw;)=N#^bwhm-2w5@Mw;EtrV=q zpn#{Wm_!wvmqa7{vp`;Ef<2fbiH2M67}x2wSfs?r^k&1LHKh@VwsaA{uroWcvs&We z&Ad(V81bI?J_R3dza;8L1^1eC3Dhn#u~nkOI6)7X3@{A$r=}Taoaisn>8F=kk&f%! z0m@9g1!TjR8w#q7-am~slaqdJ2i&1p-2O!qm?~N5btQ35fGMQeAu>KUI$8RsP2Y{R z#;3$SEDnDWH^!g1!5r9;=4=8L8Hp!kJRdeYTSfn;{VzVLH`-UoE!&16k5{NHT1ZZ= z|E$HYQ=M5T3WA1w&J}r@ytXh!#7Rq_*a&mcKpDFi5P_f@@_IPJu8Hl25nqrq?_IlD1kD z_A#D#`1u_TyoLLVRqCfS_1VK1KnwJn2p=;MA@AOn4E{bk)sIVgVCOAo+f~i`QPq<1 z2AvTfjoTV{8S#@UF4xCzzCl0>;aUkx zB-rE7`l&~A;M4NK6UA7iHs2+c;O&Q@`uu43s4T6F|4>;{(b&6glfOA2c=tT8QvhE) z+yjE{CYq&nFQUg*?d`h1H1uvp!92bzhiXnj;z`3!2$7>Cia}9lcWc(dFw*UPc3V8x zw|2BI!t9bM07cV}Qpf<;7srL;dgO`5_~O<|fI2%aEnzT)BN1&op!64=l;gNYuFQle zK(J}j&O?hmt@n(gi)g;1&6&X=2i`heK4ch))8Wddr_0Vw)4o*RCA9ZSX-vWNP2hRV z4wUt`P;SW?*w32HP-Y!MYM`7Wokuc|d{SY>qr}dPz1>25ZslEueIaYc`7k;W$iA)h zYr$k!V?c0;Y3=x%UbS)fXGEE!O9PHN(kj#C`9F^oYC)f*G%EN#oCEPNTf077x_y+p zY0n>qMxV2=gTB`E`qU97Cf_?3V1$UgQeDFID1A5Q{cLaju`h3yFrt9iNa81(q#Nf! zu2^8^p7CeG?S0<>+$j3dt&j`B8kZZT(`il(k#_>)?Xt>O^eva>4FoL&E3gnIuXO)| zGH;?vuQ7$j@08wc$q+FGm!Ac_(Q=0xfrOAhxWhMP!EVJ^1*!rf{ayhj-|;O~@FAVF z5n%JB8!bxTf?W|)SGf`$!pTRLOU>vVeZxo>Z^{?JX`jIRI62vANb?&QMcfXQ=DMdF ztH8^x;MGmLm)Eof*61D^+s3j@KE@7erf_bs`zg3aulJiJb>kL)fBfWKA-Hht5?rxV9zwY}oF1;+1bn+q2 zMQgMeWfl>9Q?Jhl{4w)AD85U&_Z9q8`irKs9_cu7e4-5L%On4uL)@;JC~{Iyi@C_w zJD3Rm@QoAF+|E`4I2(RGGe27arnCpcZoA&#d_i$e=x1CD6vGq<^NPB^@v`>tf>`9X zJ9l=0tS#+tJPP|b18p?AZnEA2ay7xT$A;QiJjR2&>5%5)>{Gjew;d}9|H5$YH>SMK;nFp{*abh z<`3hQ^nUx=0YM+)ZjF)J`^*%OsTAoS3%% zEsktx4+;%RJw9zvoB-bbO%#2)F9{7ZLmoarkX_QG1))|q)aXsp?HyeyF-^=HQf3sN zUOS44CN?~yU**$tW?N^ef#?G}^qdan#i!NpS91Ye9lGH?*o^5k*ns`u(s&L82Cpzw zC|X7ejTI6`WSIsz0@rKLv2&6pyrtmxd8%#m?aOEq%7hXlw_+oo@O{|{2f(CaQa3ib$4airis8LvUkI;qS;3o$!uy`JqQhF^TC`s~5t-%_6wOuuXClY66>|iQ&MRoz z$`ibD)szcZ8RD{vlWXOFKTQ+vNzD-kW4!#YZz;3Wnux z3{4`siAEt{xM6%`#=VvYsDIeedAMpOOLuZvQTi~apSUeZ?<_!C65lV=NmWB18W1;q z*Ps%gSMrR1sTs8cmH#ay9`&E*UD#Onv0q)wSa3#w-2n1Xu+X9P>x|D_-a;j%5#)w! z8=OE=UIshUh<^XNicz?R$gJSla8ZNFrdE!;u6ejuJSRxZ`uz zIe%_dfa^s52Ia)(fi%7)UKc^UjuQ?P>;*OP|BW$dMXi4U8}@GOQY1#eZqKJ&Ko10D zBI^tb;J-|b-|<1_=Lh10xJL(lT``SI8W6;9I7O%dc@%qswFO>?a}mVAok{%z3KnO6 zi#@J&YNX@#F@-C@HifTj&mjvI>8$B5g^P#P3DtL?aX^Kuq6ml}vNo9C-4cdy>E`qf z^co>z>MOUOXMM&xV31;BS5)@%@S(x6lpyne&PJ&MePl!Ist8>Axl`q#d@wN5!n**F zSK@IApL@YAwF0*R(3RIX=y9>mDlg^P{wWy9{MEhVl`s#OrMI*LHN}fJA`jrcY(5m_ ztoamNV*h`Tf`qwz!yE#{u<(AN*xoLm_vVy1=Kz#IJp2pEaecP?+-C!r(vt&1Zb_r6fvVGl760S(4$Zkt?e*ERj<%{~R(f58yAngMn7?(GxiH?H^AJ9twcAY^=|BO_PBSayKxZ?Y#Nj|66$ce{aMcEQ%+_2B0TdoUdUb#jUTe<;8Al9QLogimUB4=yg-4_+Yr z4`LMydn%FtExG-Pl>SIN*lm~6Lq_Ie+x5Aps7~`uVB6SbPq4^qp!>;REP>Pu5orOH zOSqBApaBA+T-#Bh-< zA-B}{T-t)S_+e7-Kpb!Lklo+;$d)i2~D6= zyx}YH0nn|$&@@@0r?|+$WyU6UP@>@CZ9y|!*EsKEIaU`q`eqJ%A;oe9L@>Hb?$;N{ z^4U~SUe~h2H_4efC{`w8AEL+>Sqb=}C*p=etlt~`%z5y^10eqk$%^kW)&}b?sd>`M z1T_PP{s<1PSMHR(Pd4nUdzhlB>2MMeztf52oFGoVA*5S;s!|i4ZG0~ULoPzq@4)Hr zWRcoLPMZ2t)AJA@I{oeSFqW)UV|d-&mDYFcN^<)$gyh-(opS;g%g`+>8{_H;vnD*Z z{uO}+^fAhhn`&^@1sp9Pjx@3DQ#`gJ%{;A$^OhzuNH*LZ!SdQ4H)XIMYK!X{7K+=0 z%0j;{1ut8nL1CD9LU8>YDuS8>=j!IcrG&WGn!yz5G|{U=uf#s<`Nv6lynfnr3#;>< zAi=X2oH22+9$lnJY}uA>p$a&7&9D;7w)^273;HJiF3*HJYmKOXu#%+7Wfc%yh-B9* z5%xzI>E?0~Twxlx*R;VD)+tFc4qwifw_L7-6#t4_S zlc@}m@+O~)Pl+V3l#}=^M4x)s3VBm0Jf!JC-64;uoGF3-EGRNdp0-T^bF>N4eN8;Z ztc(d9QN7!_@Fbcg1G*o-2DIn4gOMV>`%ljDHnO1_{~y*9S1R0XOO%eim%u(0+qQqMWe+3=c__!`;alZ=i1^5)_j-0Q>Wt zK4oc`14NwE8@UW++Zi%DGJoIZ#Y_ASgoO~6%2llhTHTeX|UbT zufArup!}!s&FZo_2&=CAwtBwc$o~gYK;A2YLdF`C!mXR<)4qIY-j)PFNpzp zL-% zs8Cu7CdP>%a~8BF_O~qz6HLMTf!ot!f(C50=((~hCCh$a4Sbl@`f25A`~X(N*!17g zo71ZukJ`fa#v;Y{HEMmowBa(3zH`>A8rC6<1gc=}MJ|#(?M&Uq3j9%~c^b?jGJ2CY z8Aas2s~rh`DVMv384iFp;7pUy8B3H3PUq0FOX_Gq7bt<}aK=L)?AC!*fDNQ85XXd3 z#Bb}vlR=t9a|Qm72AOWE6};kkzt|FhR#dcIJ*%(^5(P#2>nEQ)q6tqf{F!0-3r~hr zh@Di4DXdO-2GP``)3Pp*&b_gz^j>b?_TyoiRc_Ab-EQkhj|q>ZX0Wqf#$CyaPK}PS>^BLBt{zZ4(=$0f$!=0vbV|s{3=@EDuhj zg&ybdjq01FFD_LZlH%FMZu3opjmS&(tFtg5PY~7JgJOXZ<^iqy9=HSZl)tM#7tCA8 z7A@nHX7oYnI|Cm~zQ>3Fd`qgyIMed3_*)P+k&HV*3Qxc`| zqLI3q>n=3LD7hj@b54vRNsd`quBZ=V)5U>_iH``>ck=#A{d*-Z6HS#{XC;9(iI2Jg zOZ~?sf&ThS7uV{hx9Z}DB`;oL$B7)VkF?Yt>+;XE)ORIf$IKbB&sfxVF^P}F0ZZa% zB`-_xJDiu8+^WBZCO$rk9Y+H>A|EBG?|lBdocdPXwE5#PivJRZ*fI8FW&gT-z%cw5 z$0Y?f>OlkY&q;}o(COm7eit{t8~5s}?6JNJ@&Ui( zpT(%}$OD!Zr;EidO2iIJ0vi&29e-33`!9JxDyi?)xAsalyFVTaQs3cH-}(73G3!P> zVu~Gyi5+`ARVG3zvDsrEsi^Pj;K$I)ho{O5J)*nXMBj!)_yF&8sP87fsZ-ymt3Lg2 zJbf!gVx}ufB=KDW&G-IwJ=m4--Hrd=+(3-Z-4eK2BG16CfjM(3khzQS$>qDA>%TsK zcYHiZzO0_9Gzy45T=O|xZ~2X^+toDmQYni7&(bSPdfOh?PJ9kJZ_FsqmJQ_n96Z+S z0=E4xx$>}IM11RlY-4|SU&$j)Bm-mZES2F7gybe5iTV6GpnqUt$4dI9n7LwjxkxbS z>ldujW)|z-r@Dcn=Ru*B(t?DCzz1j#@a!hg&wukG3iQ;wNqzP)_OUe4w<7V8QtTKk zyfjHAI}a*~q9pk8`T7>Vq@OHYiK*Qnl6l0?YX-=Tev3AWl?TlnF#`C$(o;N|vwpP# zT&fB}@%IgUe23zS*CG_q!uM#saa?i-X|gU0Xd$4klclbU%#f{FmOnwHW?Pp(ahonq zzg-a8lCRk! zpk)POec<0xo*{c;^#1^XKz_d;t<^qSH~46EFN@X&AFYIs)*c_NOP57!osZTt-kLkJ zK3eORMQe?Z)>A%OlRjF{G%bhLY9Fm9eYD2>ncHR2O897<@X^}gqjhXqw4PbzrS*i5 z)`*YRgUh1zl#kZqK3ao5TK6uC)|0CuV(hRuy!#G^mO5M2btm`5sO$XE{9rDe0wrWr^iiImFU!mTjh`Dr9qp z^0dg$_qXs&1;NQ{MWDqpW|TV8=9wI4_v02m%citwse}vBW=z~vaqKCv)U#W7W`N*W zcD0-@RUUatgcS8M%g=KuO6Gq3gjCOq(tP#AIcW=!!|WSR6q2M1?uq~YZw|Q(Ao&|b zoL>XTJs%Vy20#u<^&CjMUsYqL*nBmA5+FIL=D%S$XE>0r0pxxb=`qb?%_Uw6PXQRJ zmf$<0nZ^KzM@wy8uZ#^{7Q{Nw208^KTSO z{Q= zC5YYbmEx`K-ZuRJKn_TyJ`RwaMC)$=vgcY6@-3u(qu4q{HJ>T5da<_s6| z+77z!L$JdRsv8%w8zAROH5(lXk0sm!kX}iOw*ut$s>t(wQazOCn-hu)A#K{0$VzSc z0F*i)wa!Bw?ibT2fSo6~WSuBf?bGs9XPf64_10-jPkW9C?=4%P0GUGz#yS;jT zJwTq2dgXfoQgJ;J*mGW`r}|ut-48fL$y)vvAcqYR@;!juA!+_!yS?qcz6WefvW?~* zuV?E3$e4sP1&~$=G7FGTNw)ENfb=?J0qvEVub&nY7f5pb4wNcLJl_qF!_o-yhXA?X zp{1(BQ(1LqcAYft{3+lJN*w+z)+6=zM*%V^Y5rM&JRnK&A{0UDUl#!6uw)yT^}448 zS-bao{g?reqY^*2_L56q1X{QE(nN?W`}+Wrl}dfSH{r2_|BKWmoPP!g&nQyJnSEXz zo(qr@QqOJcb5HcJ@f0BEOZ;5l=e6)MK#oh)Z};K6zt3F@U^fB*q$oj-vJF0NsjmQJ zQo{LeANBJtGzIJC4k&3>F`YedEAml*nan` zY83M42$3x6ANt+AI`7LjCss=>_)WmMKfJFV}mqE-X(b@!%9g?Iy zmwBVFQGo1`aI%+q+iM;mBT~(8^5MK6AhuM`#{i8bSVSz&$g|qFi68CjfGTq_L*}vO$8p`f~V3 zvW=e0Y0lTJXX0{i%{hQ5Qa!)La9qfHFLzI)P{>CBa=awc`m4+FOK>9Ok;}c~;E#ON z&$`R*%3zd_pK3U~faA%Ev9 z_1!DHa;+YO{w#Tt*9^L6lfgEoi@IJJ?3C;20h~@riitt@lre=|2arLj@7ROhetHm- z5>k}=PJlcv$@QlIxmDuu%K%ZNn*S9bIfox-UlaRTbsBQg5LMO71r{6A8T+$?xc|GV zt9g~g(KN2gjAycgZS7CSY z)t&BZr>`WHvX^b?c1c@Qj~t}I%!;xiHVtS{D^-ff$1Lc^tP&Y#)LEm5ZJ9UA7N}1G zCe%4F7%SD)0|6+pJT(t8L3>te7G}ouY8C4tzwj9M1hBBwy>VCb7GPBnbOI*jL zLf*Dmo9gAf3C2Q-TE@*f1A`9rQe|EPE07S?1;d_K^*pk-pQ)#-r>iXie}ncxv-!d- zZv*xKjFi})t|`tN+{#kj9cU5mD|;T`^JcL?UbQpX@-@pgOAfzzyXcXC^i;WXcto(4 z%3zT0GuQ`;R5UGHg7~z-5FOdW#FKO&a?R^?x#kcb*G$iqTG7G|Fg!YzRVS~xTHU2i zP3%r^Rsvp+3}t1nqUTjx2ipXuP?uW@yAim8idzj)i2K%ZOG2}ISpf}z-B-bZnKe6wJ_4N;d*N_I^(6f1aD-?Q z3#1%pfxWey2f_k>sAzV6o)9s#qKs$FLxT9-{{i1O6cCv<&pHhMt${yQ7)JZ1Pf)dY^cL69Sm&?gtiAl!5a`M>;1EcRVNoFJiaDrlF6Pu2r#2RmUX_C{&xl5du64v^CTVQsno^ zp#dFvD|uG!tU; zRD;G=zV2=5ML<-Eory20CGOL!WgV9>R1^=o6X+G2vEUk&2nwyh!Zz?$L4e*$D9{)A zAUMRc$=U(L_7F_5kYyD2sl($}s#ADvg!}AAVxxs+%#|$$VnZ4Q`3jIgjE&d+x2!BE zM-P4J58TDKU^nm@RAYsrfX^}VdiZ-54urghSP`O<*?{*~0(^wwOp}41fHNR512f2o zZ!B^v7%c}@z`zVbpF&$BR6sc#^1UVjGw(VAq@ep@nUSEj1-)0yB94N-mt&LaeywU? zkPD-xZO+d6amL4{w+D>W^kvz^j%&?p6`k@38sZ8WpT64D!5lF7QxY5nyq+8zmMk2e z6Y7qksh#RjHmhFU)|H?rL*pX|86Dp>lpR%fT@(6>y>Tj^E7q93258%w*U88rHY|k& zbPvBevy-GHROsAk@>jr%wy+oNVK34_wFFh*g>B1Zr`4;w{1xyQZDB8bRT;x0+x_(r zXj>q(J;aiRx3#I`+ti)g)T?{F@ZEFswt97+7fk9SWyCiy291Q&)UHmCj*Sjy{e|#d zaCP{GNYAS55?_wMBxFe4nHlGA!J@*&+Z`y|wn*`I`(@i5A=vIXN%gLrP8|n@D2*EbL6@P0)`Bk+BlEkTF23UjOqLJq9wnW9s=QU$hKij-K`s- zx`6HqBduLEix3<#7@Lx#O$^o@B^zZ3 zJ;U2zdVDG*vf%F2Pg}QZ6`n`J=s>P9wA@fTO6!MfWsSsOXbJnkFZmE=4A3G@^=y(o zD(G!tFWQ65U}gcLTre^S1v=WJUPtx>9?_7=Re;8vz!IY3xiU;zth`nXuql{Vwd2(! z{ug)`0E{F7!~HPosJMiwPnMAjL^8^lWYg)KiTLzdDC;NwuD5Gqw;Kg46|&{SP$Sf9E*GgS|6CNotpYOT_)?zlSUwYpH%DwP-`Aeq3vJZl6932j%X4G<<6#fy^2}Wc#X;zk4?kx%}8e_gz4U<{&)Zjh2rw&BN7Pc(CGbnl!GY5X0 zz!`@#lVJGK(+Ob`5WoXTK34KDc$m1(Q8X$EH`RuTA&LNzyl&I=H8P?C$X)RkQ7R-k zEUcx7aGAN1Y1;GZtXAMvNNG29czSnsct@(YTYBp*^3lX?d&pZ4mH}< zNXMgX!PD{7Ph!+G3teP>X$Gbc2@0Sq$UW(FkBhF~R5SLI*DegYbgf*_%K0UAX30i= zG3+bBAj`<|N3xhl*!Qk%(`N1rdFufS&Fri=vu36vG?rnxc0o`YbBL|Px&XI_l36y@ zQq3~*ZhHy~-V9;CY2@_~1V$sO4u%p{%PcMq3(u^NwDopKe9JBJ5Amak>+|-K&@O?3UdFX)TPs_}9Os3Oyy?i=q_@4vMEN-}CG#!S*cq3aQz#X*@GN<5{E$)B80n7R;2iIenXExj~Wt6~MNG zznC0lYV|&ETN-Cur70^op`~xOo7JI00lb41^mZ8H4clAU=pyF25o>Z(n`Pd} znes4%D`4dW!>wB{*GlN>(MXEMOjWBcsb$|PI{&Omcwa0S`wNDgKOZR;N6U6~iS!4u ziClh(yUab{I+_8uZWrnP2XK-nTexV>1s8S{Qq>HyTMD{CmF!yDW$Bn0ogb|Vwu5V2 z0C&*b0&GUo?b%cecAiuCD1|2YPiDcZHSn zIf}t zxXl(Vkp3E6YH>x#IHU0}xDx|fin$&{8PSWn9rQv4Au1LbG?+}hK6=GQ5KrQsgzO}4 zf2r72OTD^BTsR!vlXVOQn`BEfwV&&WLYeVEkhuN0HKB?^g(~mYo|IFiqjK3>ck}33_m?OG&Q_K zY){OP&Fo`)3N5*xv2+QpHmT0q6V;2?fGKkkz0hZ*)Xkx%DcV*6Iuqu=^19qMBxfc! zknCw3d8u*?bB6!TZcq_6y74BRv4kE#-qy+nvfNpRYZ@INk(zm4*D6Bulj^3NX+ekD zsSZz!Zl6v#E-z@_b3s7av; zW*gWPV3?*QTBNug$lV`AQj~Zrj9Wx zJBd*qh)B7)x(z*(<7FAv)pV01T|=6n4D)4f*8sa61N69Mat?qGTbwr{Exja3Rfoj^ zjN94_LJ$V^C;)NSqR3|RNz3jY=v8TJ^E1DPa z&Z0f^iVjtz*eOwDXI=Cn^iMI`VF8sKU(#1j03Bouq0tnrd=$+)T;cS3vk>zc-{uaN zPMq)BQ+6JY@KJLb*)=rgj|1hgPmFMA&$7iRMw~CYp^qwz(s-+xJBCz9o)Y?anBF^_ z0hLt1E@g9F=Kg%X2#Qg0xtPq0dGDMiq_ajzAF?2X*6}aY*>q;?D!S-9nx%`rf;)35 zx4V>mL!rlOL+>l$Q0TD0P0Kg1ira}x+9LO!Y~+t%Ixuty7}K`VF-;1#teVMVt;o2W znJM1X$Nr9)js)8q3dp)Q9pj}QZfcnKraOZjtB9QQ$o|Ro&cy!a_gVHncYycC3TbCA zC3aLXC={J94bf;+6d0}t*4^%u@ry~d$8J$zkM0aw4uzM>ymh<-i`X>-r%~ZEC118O z+-JwezTOSpJmT{v991Xz<%~PPccve(^d%(M9XmX6JQfXC|wR8F^f5Rg&<~f^Ub-$`aY^qa^b!N;|aK=Js@ZTA6|w zq^x_&K?Dd<{JG0FOSCCmOJj zd58^K738#4A^U6bw@xIh;0*!Re5+ExwR&TwW&_YdAWqK0a zzg8kHkg1?jZKftdcwycwA@2IMjxeC6*;=&>M0HZAV1n8R%^M-sKnT`7Yr=$N0j0x> z3rHYt^<%?fn`<-Xex1le8@X&ktU{GenxW!0E_-FogU3*2bfz_+Y=Dw9L8K~j0Nt&O zk-UQIY)H~8YKpM1V^o=**q+@zG&QPZrWKgfU6UCZ9Z@bCnuh0#T9w_I?2d_DSp_hr zhQ_nkDihn4q48^#t1{ywt;*=0$*Ixl>Gf+TrWBkcWkvy*86O_oHIf;>QrQLt$0xw5 zGhoh8Yj#3Gima~8=rq>0b98EW2RsgK%Zz2R*FrmO&t%7;R%JWXI;2bvO=UB~yT*p5 zl*wIFlM~aUz`zL9JDwTeJ_Te(caDx{Q$QAA%IG!lpiJ)=8XH4e>(>tL0s*Fg(v&hh zF?sD&=E@yeWyi$W$S44}jRJ3Y*#V^mat)6SWp=hIBSSlft{f$$CZL+B^=lE2GN|m{ zF^VvxI0XL%wwb>;mu~E;xY8$m=kfMtg^%Fe`>G z3C@lfHLnFzYFL15jMa)j4tgQ2t6#J2YJZL~mIcHVGLUnK4cW|VSoC?b`$GXR+kmM8 zDibXDYv>al6`@Sz zJ$ZpoL|OBiMY%}PYRRo&d7}bd-Rg(2!?GP#a1B|cj?M=6H=rB98J9(?MmOfvVxyBH z9vf7M1Q{8o-F}5v&XkywtLKd%j27t6-Rd|-%CQQr`+`2J(Cm|{Cc!_?wsPE7Wz#0S zpl8l%>37K?N{Z7P;JBXmp7sJfV)7}vUmoE}T61bnx0B5t^yZcpPko}cp)#?o_^P>u zOEp-jn&80gC0EEiy)XtS5iv!XKpkAaGp1S0^(z^?>H+i!ePA}o2#5lC@hZ%utL0$H zLH{s%DC1^XkK&DP&Ga!=6*#*Rd+snr%-zsW+MONrWHWg!8CBm(?p9~*Mdoo8ml7|xd74KWlJMJ+YXMufIlr|;l!+ih~3bPHgi+-6VYZ;$IOV#Xr*<47r zD4RXaMz)L=RLyoP(u-C|y@8xW)X0YxYJSjOfddPwLCfQ+vN*y-jx1Up*POJn!HrdO z+C><~atdCi=%^ZZO=8N5YP-(S19|BWtX%e@(x@2Nm0a0aTi43ke$6Oi&e5ene2821 z2nxVGiBt)yL8yV%iHEJpTqSZ;844RySigZ`dqzjKRcoj_Q^7==*cqu!V5H%4E`%Q* z-OSaDVu9h4T4X6Gsd>{veK|cLdYB0vT}Y*}mq$>ucT^Bm=Rp_V_)4)OCuA8H2a{WF z6Wzxw#_;$GM`N){u~c)ba{b;GPyeEo(_02b8cFhRi%3L7O*(3jRK;G#Xo|JRfD^6uG))g#LlA{ z1)y$}?IgavzL~+A_X@hYSizG(#PUg{e8{~*a`UvjbDjsP2yya$OH5VFN|I4-B}G~y z-MPPp>se{kvO75-G7aEAt(@lp+G6w}GGsCVBX0*sOab1hAG{K;g$LGTG*+-jhx#cg z?)63~Ak6hX!}QstGsX%E!KIsItwjvM_6C}6fYpXoSJI{W{6lOCPd1keh$!}WHu%D? z6^;T~xI-N~ivs=b*(irce`3!S`;b?3+JCSSJ%#`*K;XBZg?`ddLM$*xT2E7~3N#m< z%2p(+35p9P1QsB&en*`%ixNp1s7!KM+|=B=b(&@|W-WQT-amXtLLV9UR>`Tx`F zf2Y^~POtx+UjOs2|FLm@1y%qf*ZO19NWnW(Xe~vUtA8rCY>#x3eb{GZs6|s-Cg+k_C2HCYPxURp*Gu;#Dl@GxMgo zkJquAKB7)ka_OH6Q}QI8nlxwiMVkrN+{$2NBIE^*?DRs0!ClUo)g>={+6E52KygCt z0G+!s=xK!|d_l$p(u_34gco2H@qj+=^Tf^5NOdX6^2biD4(GllbjJLJFnQJ%H1Kq# z6s;rK;%3_g>63z?q*&q_o3T~2pwyL^pJgRlou+OHSp{Y}d6w11v#C6J;7nFiDRi1R zM!__=U%^}jy@5`gJOWZdxANffb--Cv1_qQR@jd@?0<-vHu4qOtn&mkhW?3zUP=!&pL&#=EQhaTN_B9pD`7#$+!Cvno3oRg2#j<}s zj#{ye@tR8`FD#GPFxKV+XrYew26EeM(LyOFC4?12&nuX^Rc;28S%kQSFYKFi$9|L+ zI1Z1oIO~|ePPq$Q3br{|+OkK6A%)x@ixub6 zYza|kxo4;mP{DViO%;U1*ftZ}2f{y^s3r+Cwsqz8!Z4gFX=Q8#?s#&b24QJAya4Ar zr_3x>(8+T>`B%$SynNTT=z!RaeZ09oO=n&Immd!0u>wT>>OW8`@kbfVEX3wsWzi?lGS3;qtk&=)L+8%xE4YJhp_>Q<9UbV zo#rQ+-|JgmcEofxCXp+dG)uM0=E7`I+eeqpu%l?WNX1NBs)lqwMv1OS(hlj{yTn~N zXUb`8FJsuAQiZIsw`)a9m)0UjOybROB}#{BXx`MTc{(0}hbDgZ3JAzyEY~$kpU0xi`UogkySzU0n4U7wlamGSj$U;DMB$X7#o ztg=_M5gbjxBh|5eQkbF%;W2K7nBKlSd?^}h_Y%b6>p2jFARKClRF&w^2BfZnMdhv% z(7K|V3gUFTZs+IO;aW@G&19t>Dj2)_|q$8QH#m42+KKZjr9g(2Is0&hC+2@&23IllO<^KjfV zlz6Jq_+4=?o%*&gNvRkKVcB!lE)q&b(V+2+AM{>6N<6JINInD=vi+TnjHH zRD|O}=(bu?i>cr(my*-h=qQ6-3c8UP`Yz$OlUd+L9kVm0orM764Y9)pXR%IDDAOVG3ds}xpE$SNkp|CSth~m8|XYA1QY!Jo?e5qE- zoGZh8M`J5JHU%;;hrn}Kp7E8sId5>DBT1-$ml%%ZP!Nc|aRCJ!1Wh)|)$&tY?&fG{ zWfK1JAd=?w(F#U)PveMF=Hy7En_~AITcWC{>yOX7azmu}q?vIHp#gwsiqrEZ1ZK60 z!ZwuS2LNrQXwC^lU1#jJ5`If5=QRxJC21aV^@);XVT7hsO;7}hSlApz8a2dIA?~O% z!O4@3LO!^}mzIMDvMIbx)aRXr&5hnwF_C&^R!A#b3fMJit+J9vuII>!j4FlLQs$J8 zh^>fc%*wSQsxVMG`V8N8Xu1Ck~nf1_6h%TbhNQgu$EGluL75Xb6F_6cLx!m9e2SOIw2ysrp zY+9s9>LAyrCvTA@VdDwcCes;LY z@r|M#KHyd z>Y2ejf`uW^~|)Ma0<<>;W=+Y$Ylg$X085?3g==oCFQIbY@V}br!Q>kXSV|WzN`-gUu3+B*~XdrDfS% zj)(v=SnRc$`^=+Ca3cE+=U}&R!j|sXVSU%FkncmqprmS$%Q(fuzAHc0cuh%1foTx1 zL|6o822OUNtR!n?5|#siywJ!5Rxlds-dgB9G>9iLx32P>rpTBfj0e!RCRyL3TU?zZ zM%)Fm^78p9_O%0?gOcf%4w0$3ge{KW6w}FxdSTZE-L+KRR6@6!(c%Kw)UTE-ThOcNp*g`L&CR=EI(Bq7 z>h&b)y-o)2alj-L69^3zYs1JrPtsFjG7Od2PwurL@tHr--YRGD0z`362x5$kQjkii z{W@ErfQ4c~;L4MF!c9@=acl?&g^LQDOA4?**tm7SsJn^Wlz3YO`gTG6yUOr8B;0IO zB5t+u2sn+~9e{c+M@g`3j=X9jFha(ahVnru#Iiw#w8rv5NW?KB4yOUVBgm#)5@O*v zLg*ZdKabg*6g(5P%ErnKC$QuxwfZ@d23`mkYGNlcm^@vjw6M3-e$*MC+cW zv8zh~F;2;vb0QYrG^w(_XeT|f3Cp4hr#S*@yBLq-ljMRJ98^rNJK&Z?TH*2mr9Gf7 zTA!P#Xm{(}e7WcYgNxcG(-MdW?SK7dLacvpFgBak@v|CE;TAu-UUWDQm`uBzGwNDs zQqNDH@Y%LkI`$C~l#&Z0WVU=afk;|LUVT$)m)ixsc|Td3$>YH}4Pqm3*v&W9^qSsW zr-HHi=dNB)?+mYv9h^#8cw>9AEl^q9TP4BFTS8>Vp~afRdH+_)1B9ScbpC|C}NYc4#js;Ba;2 z-FFZgXW*7yXJGQB+xRIwsSQ(xXUr$t9T{6GY$kT$Q-Pf~7!c}hA-s``Op)~7fJH&F z(#lE7tAnk$rfzH1=vBf%!x|>lV!+D}K?W#k#qBA0OOmH64HZ~=4nlh>Qs~yCle$Xh zB=;mSY21@?j#DB!51O1U;V!MczP$kW8J+%IWvQPaY{u`@vb!5|{>vL(vIV*^htW%U zImB%hi@g|vb7X-SmI}io?j`T;B;SHd zQlyrXaBqHBLlzUr0zMusrH6&M)1kloi~1f{7~~1bfeODf^m{*gV}k4TaN5OE@KOFw zGzY8z{T$!D!5097T6zM#i?~7CVktnzh<}GA9^~_9cm@CA+zLzcjKUHG@L-$!a2=6M z<`?+sbE%>8Z10HX9^AlFqEbDE9pYrkrcJ)JbQw8XLEEmk6pn%inK{!ku1PGA942vh zAR^TuiCb326S=o(h`V}x`?v%l=&mzxt>WG;nMRQEI#aAqCGD)WxwZT0M)(0b3h)H4z};>yuIYNt6p~A=Knq?6u$+4dm<0Hz%WKFz0?y3^ z;)Mj#^#k(N1uua61%8Py4{%$CAhJh4p4e>cs&xnQOL3C`9)+WK+0vaH>OOrb>gYE+ z9gE{TwPTbeq(*upaz!($b=Jt5_W3$qE}kGE68sY1xlsRI3fzr~R}^sb_kBmeF>VQ6 z=mjTt^}x!U(|C6WBK_yw@L{RB|FBry}02PA-wRF>h*;?S%^F7RV>J z>E6UjQOi4f9+_Errn3v~9&REKIqL5J1j z29|zy)DR6H9$VRC9Y&AFN05Wf{I*kdL5I}8E$7&XPR?*aWIBVl)|z;T1`QBiH{1N% z5bQi$Nvtko)4i*BD$2+k=uOCl9MtESf-F;clZ6GdYij8lc z)8k~bG=5%$K=EiG1$pXt;oK^6$_}mZF|uD8M!g5qgrTwDJBB_#XVFgIQ}BFWB;a4T ziy?PDR_SZ{kdeud2P=%HO}h^%8j3FGMQb0`M)Yt>?B7$ehRE%;FdH_ z3~5{qf8+1hR&R6kj4=N9ZRGwWf(5^CyWCkiCG>uYvw-#9lX{XV!&8yebH<0byxu8G z33HBC@{#kOplO<=CzRcOuK+LsS;oqx`UwrbqNO2iTXjEYxs#c~4Z$!)W zT#C!4RTjAjkKa&X&dwU-E+;+)?#i^X>v+fth2_n4W5m5cFZL5}sSZo~)2|}qBn?l{ zCG#P>+~}~B^lXxIZa5S-$hEpR2=H}#&*xcjeJxM-f_oMf94YW*kQ`k(iwN?fdkWQ+ z1SdS;N^!D_Sg0tq?zR`VuX>tYz85^Q?Ss}Fqjln2PYhF#X9IA#AT^oXSYgU!=V0^w zN;2=XBeX2;=vdwwVcq%_Hwbza?6LJ1s)j`oR$j%vqdlVivOtW+eGrbh1D`+eJel}x z2yc|A+WDHzZ^CvDiqJbY1DMH4TZ)4^kawzH*K(C zMwhuTF_To=f~ws=TOH8`Xv;{0k=O#E%`5ev=e>O;Zh{+F>cK12!cv{5rio>}uUH$? zjIH?TzT`^ncE%eCPVaU;z1#V@?{+?^6m8fZ=NI;-X2%uOm9~A3pCps=y8jb0xuX9% zoyKj{hp|ps8rS~~rROt+OUAKNwO33UH2=qc&FPeX_ULpn{Fg8peu_TLa{9X}r*}gA zZ`}!1*IxG1AK-YQe}IF|=0|qMe%rp@nS8;h;x2T4kL&5l{EAQJHGGd2;ar_Vjnx}IPHkhxC>r(>H8*;u$;qmN{ym==%G==1J-Tux&!8wgQRmE0V8kMqu$r@>7rPPm zs=`T5EQiZFLDO&O2R4n0>CMgTl2_@(-{!zs5#CL&+P39h0^KiuSO{M$+3d9NjU&87 zU}R@1u43=q(o(hT;!2`aE2!>eT&;vw5<28aqk(flyr@A;4N#@KdzLWCI#&kY=~>`} z(NEcj?dLO=+?iGZ)GcZpL*n zxE=$zR;UC#O?mZlN2IT7#e`$}7EDOfipwBGkuBltH*B#4)hu;! z{~hsiq)e`<5icbZrU*59g>H>sp({@p_WHiZc$L01TCJK@55Wppp65zAS3?85E&O*q zkQW3~aj*oM{@c-|0#x&}M!Aq=`-y7(N{yYm=k!~>+Gy}RhJO)&^v=MIgwE@LkEwZi zjre+8^R6;h;@)f{zH8-|?3A<$SI!~~U@5u`;3adC>!0FyAHM$i0-Q#XE*-%Lc0sjP zEk-djWI+I)>kk>ANw3xrgL_-hMT_jku-tx!$^6Ttq+{dNO^f(-5$x7&X@13)&}=YG z(n}S($YLVL^y@d!D*Qrss8HvtGbtr2tfe8g=Ca3E%m^c`WVYlkb!*f|pdri~75NLB zZdWqtV9K*JQ$i6k0M1r{P$9RxUUtJKan2Xf+lD}Bwkqd*dV|gYnTo=j;kjH84UJ7N zC--QS6N)`ebFq@07@6o-$PyUureIUhjVmVkoTjpSntW2!mTmYZ9=?j#5~KAJx~h=0 z7VlMa*)+=HbUNt!l0d_~*x11phgrAV(6#F4*mI^xB~s^cKbqoiw^M9HY&kxPqvv8t zM0l?)d5E22YhsQ1ycm?YuV>S&p%)7$vyJCD7)yOA4sllpnJ?Qk4L7F?85w6su9LRo z-v|xf=HcD06OzMOk*}5@<5>Sma>$CTjLC~T1?sbJf8K#ipNPOg8G~DP#qlGn=#oOWDryiytlDUSWSqL^y)QE{vooH!rR(9yE+oe;;B$T z4Nv|6`IQ#(m*l6tT`6IfaiFa`-PzsU+t=BXN}uk9|7(8u?t;qF=C)KR1=4v!D$MQr2B!g13Fz^LVTa^-Ma~6N=M6yb4rf696MwykGu+uHHWGu*Ll+qP}nvt!$~ect5cB#KoPIsk~?oO)y^($F< zIogeqz3-+28L)i2hALE}zwmIDo%jlA7ga82j`vR=Sxc`C{HrDW{vk#(0CdH5T@ z(R`cqdf8`fi_)v~ORmKV>rm~`r1AW6Ycsc%)!8JLb}OKRkp~(UmcPd+>%m5;-_+^9 zxjSo^F;H3T?%xDSXZRw<#*$WF>-l;_eQ1gq${~~#wb>K8?WC2LX;`?}5W6i(yEYaZ zt5pB%DynLwfs_*i2G^X(<4rv7-k}_N^~t z6M0n6khMi6ix&GFy*+qKnJn|zJhQKH(n}&M)uUPn;Lf#;OKEYlRqJ_bhY>57yLEl* zFub?$Bx0?S_H57SeS{lMYRA2F7Z2Wi9YX?z!swIlM-@^mkC6TEPO_!z0m=en*tN-@ zQ1Ih8ydpXOq`ORH_Q)++*5VmtyD?F4H98BKp8MCM*{?RmDShh&I`6Q4?YAv5yl_mE zUZo+}d1&wWV<0t73$JhVIO-ey#|X><_Lns%6Tk*Gdc+{n2?KKelEjx?DCd=zjo;BF zr58`Hst+%vt=Z-=+8E)ov2v5(RYNU3e~qWzcp@Q+WH2x0`c?$5v1GwMS^77@+4!V!2l^TP+98!La?yk3F!bna)VuAAc)IM8m%Z&|PFliCUzI;E3$ZWTc)Q zC_h$jmiW~M+H|Jv=+B7iToup-mZl{O<+t4?0RSyfaTu0~H2k#(RKtPQxOZVy*o zo>*1Q7E^q7j=^Bu5NRU~IE8Ie2n3OC4?Et!_6y&~d_ixc!+Z+3F&1r$T&0RMpK-?y z75Yq(u`o)9${h`Hxm|K^uqr<1OOcB)YeauJ_Svi#Lx#N$s8cSBr*B_`&Qm64Dqn+e z%+G@9uqa&nuR_$L9brHMFNCJITkh1xg0VRwqCo7i{G@$PYOuC^^ z?aUTh+0gF`i3`(!e3=cB*nFN@HPEYCvP`bYePO@dtf54_JPen{auOsb2Q7xN;u5w) z^Y@mx@*{E9gWpvp1$f3-Y()5*y0l91#s!s$N+rp8LKRqDCw5i^RmlZNm(1@99eOV0 zm;O-7zhGtaj(VLRl93QzvvE6LBos$yq4qkT28d*MJY%3QAcQ#JBN;m5Mw%>=Q033v z-|#86FOjEJRA@hA8p@}4j z78WcA+XrNLer6}$Z=s@ph2$A;xzoB3ceW(<_2!6p1|PBueheA^wTwZZJQ#mKn+^59DU zCm5C0=g|^X>ZqmAH>(>6eLMyj>c_SS%Y;9HTY@U=n5p|Ogt8ss|Q>d`B>yhPI(>yb}I8is&%@ardL3WQHvQ?&FtOzhpf2JmTMD1lJ85Miu9dHNT zG%`7L`ij}2Ag%2_T5s{s&!IYDroUU%vAIBmjB2q| zS~r|WXYm*ue;)gZsb@XL4%`%_RrKY*Zf)^AC>R>kB<%~r>E^9en9=DKAuC&k=<0pf zqxP2Rc7ej|_Fy>e?ISF8(XVY}y2txorD(rRvZ;dEow~Ktmaaa#U{JLzM6&HT;k!G` zptyo_k}1PAt1T?Z%2OwgPOOf+X{0Ac_}!&C*yzSv(`Y9ujA+Aicw%B1r_UU^CF6pq7RmZS zi%d9TKh}1cw_-NMs6G#*^5%nnDI%2h`3D*>y1ZR0)iA`K7slly`m(} zuLi9*sR0NSme=zt+5h2G^~_sC@a$2yU)lmvzRFulBqPmH81t7*2b=YUPr={*TDgm8 zsy*y)9;}#CM>(c}CKtA|8)zXBmM#G9M5y#Eqst;5Hc6>Is3(Am@Y7Qf!wPJ=V`~Su zprl2>%gqEhMm$GmY1Y3}pL?&T!K*#JVGB{)LUIb<+PqC*Rwg>} zh=}-Cl!YWZbhz6 zdWik8DJS7(g0st=4x@OTPut-=-PKmog&s+?`M~PQ19rEM`O6U=D1~`i&wg5wx|0Yg z&wqZIai~XJgH`B5zH3dyMu;2i<`!(f zK{FL57({xy6%;?v$uJ2<4!l}4kqhAh>^p*gZeXzZQ`Cq#(aMjC5c)n+cxG&TMZC){ zut)n+oau)@(^7|G&4#Yvt)3GWE7u2>MEwz|v3{v6Gx|HaByST?%HW#}zyQmmAxCGg z{uA1FYbMjetS7e7ef-llNRI}yOPKX!M^ItBb8l-Xe9OSdWn^0W#Ow55+3g#$!6ASU z(Zn-KYe?(Xl;hiVeQ{hxLTBnpCyv zPyz>aI0@nE#s%@A*I}<_;c|KCM3)bO3zG6h+!|D}an2|c9ugE{EoEqdBQ`nAsHg=e za5FN5zMN@uB8QlAaZCW)ENt1{2Ow8=K^>dV^9c_~|F2tdW`xy~bm~*=Y(fe1tj>g3 zS?_>89y__@eSn&UAeyvBT$f9_b-IGJ6*HwGl4|Q5ObPTFZ2pw!Dv(Pz0bus7Ki7JO zbjg(QI~^Hk*F|c>+kaV-?vSt47DuF{)|J!`RB3&0g=)_-S(k`zWa_J}re||@=GBxygv}Pe%6)*F&d)B!Mzy|-Aaz6GZ~);#yr$il9i%>gyOaS1DgO9gly!Z1vyHlsdd14C53GAn8r%zuPpRdm9Gja}$!<}j=)3VYNf)#E~E)CTBEWskX7OXYb}-5wAlZT+IN&1H;w z!kS9H{Ui&uuivNp1-5DmrC;XZ0lLdgM=UU3P zD*`p;&?r#Jn8r2^h|Z7XwuY{UTaHIOqu z@HrU8M~^1oTQxS3WLO&6<4K^wNM%wXN|eBmn0{9!nU@BemGH2mb1$eEB`56%sMF**_ zr>(B1o%{sS23B@ST;$R;DeZYYIF!HaP!F8iGMaQ04s~qE1cThl2XU$n?*UQTb%)ib zv1(Q5IvRVHjFO|fD`C<|YIy+@|7FyyucAYL2jH?@h}%3f++>^K&~@WiZ3bue=$|45MY7Yx!#t(w_;K4eM1>0*09XC&Rv#Rs~q5 zn!T1go(cV)6{$Kq^nX0R(FYxu3WVLW=YaFOu=T{r1O9wcJiFS03OG0NTr$B5A|;daHrhc`;kM8|IwE!&!FK-lW*c+^VK(%u? zc+zNf7}5r=hLzS4+TcOvNZH+252&C8+&`Cs?RX-?U`g`SRUtu8JzkyRH89e-K?bmF z1nasB!UHuWYSCS-sLA(%h1O#c@Lv*`7?zE z{1!9zo(;TuFt>bcftRZ$6m$N}YSB|O^P#skV{MhqItg(&TSG}Ln;|~N%~9qz>O55y z>ZJ&ua&RcKm+mRG(m{y(G|h%uyR%Ll+j!1oY1N5cIG1gpN4yL@MSx#6)@HruG)|Q$ z^Lr^KrQU<@2d5_GT_>j@Y_e+|$;_AWBPP}CKROrXtb(sC455C$>MelwZGX0aR#uno z7Qka_%Tu`(gI0TVeozML8e5EaoRVd?dy1#u)o%sHOYD|qki_i#!dx;=-nL#$T7ZTS zznHF3Z5`=e+lA;2q};W4a}C=iX1_4j+YT)Wg-l)QAN(^d+%HC0`mskFI_LUaz77OU zMe2Bm=D>=>YvUo?f#LJ~GS;weI_JQMR%D#6C!i)j`1%ihuKZDe)?fllMG;^4Rc(B( zlB$UF%tFuT^#}FiI(-^$C(1QuNzw^Z+$wvM67Ht+lKd@=W;?Y`Oj8x01?{V72SHca zN={&4GbtgL?R)>ib&NYWn@FKR8o_}c-xQjUG+17^Z^i;Yr1z~rG!Ke&NHOwi!Vi)B zQp)DW*qMnK)X0bVBmIs| zlu}9|HUsyKlZaE-J!Md{YZFfcLYEJFM;yrrwXvp zC|BBgF!FDk5U-CU9%V>S2yPrh#p?WOSp7qj(N1i}EVVR4Fu85_M zMaag>0bit4-(4gJZbY)V$wj(zbL%czeOBBxi*Kk9j9}}a*EsPQE-}Vk(o9yMfy7dB zK{+ldUSC?_+2n6FkML4sZd3X8fT&fE_^Qe3nWfT^@hp!-Dho#}Lgr?vfRi$t{QsCu zUDCr=H>TQIh5sI{nlMwkd0mH|acDu@lV+~7qOXe{HY)EC&KgTC-OYV}zPGpgzY7C9 zUl4n+AG?<_3%f+}ncdm`1DS~R972QUMA)Mqk|af~=J1P}*5A}TYqPLQgeQroXqX&z zPKN(xf@yv^kDNQmr8$uYZWR{$(hP}HY1Z+5rB z#=lfrH;|STL^pE;H6ozD%2BU96&=EJ74GqEzMLA$uY3!o@0s5TJQQ58?c~&g-TaU z7OU5lqi{M`JD+PvT4-^8T1~=S3qZ0r*D4uyJIv1|<6FOObC+#nL8~gu_NLn*N0>(2 z=%Vh&q03ys)}hy%Z$2r54q=lIAC{i)-KD4-6xXb0@X14@#)|JONd3^&O=Il8cq-y-P|ItIRHa5nk>CHrPcEDg zb!wz>+yDYxqV9wGWQhxYXE=iVGit#rjBQ+Tt&T*5TX(>YaVUz_LlaZeyx}jkahLN>s*fTdj5}`x+U9xuWT|Tb_Rea~g~*1p7lAXc`C1aH z0*j&x^>!G#mD1`EDUEHqez%yhwDK@XolDq)$oV4G!}$0(`tcp+#INBFYiMR(uR4kS zZi7hMz3806Rzm|3R;R219Pi$XftE4k!BN+qCbm~bG|oCd)(I-rcJA-q&~Sw&6zH;V znOHqiP!$|rMaF+wmYo)pOu_04i$i#25S0T2tf4@!b0H148$+B2L_O!;mWY(7b=K=3 z$pJ{&hU2oY1(^oB#aAYtdp$Yq*y6>cdrn6=W{6rMip!81%QQj?A)0k&iV0c9qrs0G-er_P zq40zmiI9BOK|f*h&CbQX({``M%7I=>VIjvtXQJkhEw5uD`VYmJW=uA?d9-aE89Sw0 zzw&>wK&UN;-A|E5+cEk#Gcpv&estoyG(Du|_-!6KXzcdlYVsI{ODOszCuX>6RBPf~ zC6|E)+;EhD`PF=cII0Z%oI@ihP}aSPly>szGdawP9!bhsFk!AeCZ~xgWXlZI7r$z% zk_O0&E%?i5cRRZu{lSq#_%MPn`y9FXdOwD)U8r_uj_P1PgveK0K?S`91M5TRjr+n8 z@0i&TtX-l4CD~Jq{EXXGwjKh`Dpg>(mguIh!3bpOQ8dHKX8FUl1A2(FshzllifU^+ z+%=I=L4MoMlv2?U$PIW?K)1j-?ph9FM8Mwun5YFnLlEw7yT%3@3 z_KEIUT_UI)MCt#?G)<9Hb3)D$HlIg#@~+CE`yrjWA!<))2KJGOR!y*N2WHShepmPf81Tkc(Gh;-EehB<*dms<2_C9|Oj!>r89 zP8Y{M(U77h*Wi5!o5%u``ATebj$HDhh2f3}!P1geSCe;*8gUNKV5mQ84qef%mozIT zlN6*5g{MrWGY9m&v{xQ@ml=k)bT7?ErIkA+bCF*g0?b7+cI&&^^Y8UxiA&rzH+HJTQSgw4WFwUdI6}i?> z;G!B7Dg6MR=u7#aoW4MrhAHBS3MCD%@0s)#dwH32LFLIy_FmQiKRC+>Yes=zol<@} zPOU^447yZL7rwC~|2$1l#WtLlma>3uu?zfW_i?}f8gJCl!zZ2co00+lb$Y2iS9yRb zU|S(&aj(?_P|`=2LngvlU?GU5U3DE!S(l4k34WPsDWW;greDH-Y9Sp~`fCPdnU84H za>?0+n*BS!(VYN4JURYCPLX>o>^5&^W&@Zopt!LA9bFmT_Hbw!?Aq!gSIdJ-4d)Ko z@kG?+O_}uG7!QGMa}LX0OWyhWO1C(%bTEUB-LUEAc7I%xJoCC{@QHX7eyS($!qXIN z>>PwLE!rR7&`7<104RFc=IdU9h`C9XPRtLkJU_gq?SNm{A8(Sxd?XqXayr=mq{qn#iev!e4 zY%~(MEBV|-vO_o&?t}+9r1wXpDL>Vb3<;%DAN%C*6F4bvilq`O%ed*<-$tra@C*>u zr}q}xRTi`Zqw?vp^_oP5;zpUPNAIqw4?zfoW=JC{uknuJ3u11Nn&9*aV%wKd2M2iM z;L!bc^uMQWfiQigv(N(SR5_C4rE4Z?Wkey>M3}8HDo>$*PKP~}iZf?-1wg-EpDKd4 zB_+1KT=>OBbY|lXA`^JKuaR7TOc}3V!V%uT7-3L}1`?M=fdT%TLulw`PBTKI>W5%x z!*2QD#`%x@`;86keUI#P`&kztpS|ewH^zcUA4(x}d*~FUUzbQ(`d6 zWC!c#oC0g^aVm&PhWCa@F#r$)r}h?|NIo3T9X32AeHhI~Vg8%)IlBMKO&b>&J3fw? z){C&R)e6zu43@;FP^X*pXZ8#*;r$1w&@hBKfuhoGI{%Te+<2A~|7$Eu8eYdc7L#ON z8&;g99ytv18#h(D0Pp(%V`5aKoMV{uL$h&<(-9}lI+}ZdYvr)~GnaRXfU=@R^57%w zP;LPy%~sfNjcdnoB~oIx#jbisyD%HL z{Rs3m0@_bLL`h2L=WqR zh!?tW{U;zZJa(m%BS3Epv9-USSBd--HH;z6xtVhlzLsedY>GB!A6Bp*OZ$=Hb zbDZ0wEB*p1y3}gtI$AvE8CJ3*PGF|}?%&JSImr|gM_!hF5-p8=d`Pf#crGQmIIj|6 z|3bWj_ik%FU;<=)@avx^ZXKK$hUU?-F9q~CoRDy1kS=Np)VtI>{7#XaOfYmUF&zWt zkTP(LGIEMp=>Br9X1URFG;uik(Kas5@kuu&bgIkk{2CHnNN%SrpQI^T5r<@Hl zuP?PC|Lv#^Ej+$l(ZPMBUkR4I9=G?`A|MMb{%n`TM%3mg?Yr zh#8=mOLc!qY`69W74jW8qRZpZ{OwEYw>pj%-KMkhdAeNlch z=E#+G3vII_xkP~QME&rsWF^#BaJ4<9&gqEN<1DA0Flk5X( zqOX=GO`@d}Ex*MoRZ^h>yv*XL)*I#R63uPdR7iz<2Qikm_NfT6yKwcv!MQ$Ws+c-5XJQ?U4*n5Za;!4-=ECZZn5DwFznGW*mQgEcRuo1KQg9s_;IBMZeV_9i z=i-<37+LQr1-?FINAy>KtUwLNCl4dbamGM4!3|@TE;d5Ys1FUZM(YeT9tAplwe!^? zBG)JuM11v}OzsR!bk*M?uYD8wP{MaBdOFGtLqghuwE64CaoBT&FLHRJdYo{x8j1vl zOV%ofpPp{l4U^SNXqlFe<8m;*fVT=Mg{kz4Pjq*jRd7k=$JLAOfAH#aK1>bHf^SN> z#8nhq+5sb|Gt|XGS?klJjH+Vjrz7%IC8o>?(@C!S=z?yr$#Zj?jr^4tCjeJ+wya+= zzzc`60LoT~J0acr=;0hVNa|fCvvD!5f5&PAtA+|}o6pIGp)RlC!z^?)1Pt26MEnrd z_E8lgUOy=}T_^SKE^#igwMp}u_7EWh!(Xd;MvuL^GG!k4x7qsO0^E=LSkCo?edW*Xj%hay3>3!{( zZDrF*Sg^V|mU^d6%DP=211i3-yAM;TzpFKN(sm7L3H3>M>jIGOPIbN?bJjZ)hgti8fr|Lyen zc_>Z^wKkoZ&q4`sQNyJ?iEg}OYc7G#ytHPt)@~Ricen=*5v=sWY`uNrp{v(0(%5*5 zPEeS?TqeY)Ig3tUM}vze7NnKNIYcO7NM9qD20avk3h5+iO`nAEa3M{xZg@e6VxD3} zHaT~rq_K(arX!Io%(?oiTi(D~Z*c`7lV@09!HR1;tncZ@Q5FnW07OOt{$Pe5;Rfm`0*6x5{D?O-0Ie_GbeLTD;=57vhm zp&juw8qhtqBxdcX4yxKuE=;tQim~DvTDq-$a+FHusxuBJGKj;n5#L%fM(%26dKV$` z|5T-os`US5##iTyYPk_j1+9kyL(01Nijo(D~`@%tU zXFJq&vM_bJwPYc*T<}f*$@i8E(lCM}RMO0Qz$%Ugp*VhGHh1kCW!u{E zgU$TJoaIk^cfmSp|MAnKCwq68Lc8`3@^?u^h|5K=DgK^w)K=L1*<#P)L3;A!xF`20 z_W7UPS8>V=@v9hk*{M#N8azAkuCWRR$}SHa^vo@2NhYCA9S#3#&nCO6znCVaLqUCq zmxRe~h>Ph`-T&Ik*Zjk|+>_ID9}D~f_hd9tHQQawkMzQbFWe$ymJq*XvH3|Gi&PCv z#Ebk4(+2l;_N>Fe)~RR=D+~U39cu@|f1{Q>Jp+U&3f}#mspJ7cK_Bl+#x$p4mVyX` zzEQw1^j+0)7Igj?@qzxaz~H9XpCi()`t<8;kfh3!$1F$wRp>>QixHbISyoG;= zMNIJ3kT6sLP}osOKir4k2hPT#?-SyL0OLxZgZ%ZUW7~1-sUHzin2XxB4saz7)yjB5 z;S?6ojE;4Ew;@TOBUX@4o5mqlF2VT!1X+c~4o)!P70T=r?G^%y^+*&w$C2{?C>j|smFpG)50QBAhpi;>t3xPKVw|u( zKAnxXLt=>`R2~)ywVj4_T~zF0wi)HUlcsNZW^z>VE|Db!BKn>_*IuZCzGfb7TK5fj z8yRynV9m8H)nzsb5-1{8mKWG}!C6Hij{_HI#nO_@@2DAm;-LW7zRJz12%ignm6%E?NJ;;DF9E4^M6N|Bbx?;wTSxk6t3+svZ(bC*+D&tVPxlFtAx^s38AUq|LVncs9?B_POMpdpS0s#c%o??X#tqc`me| z6l-F%WlHi-0Rhpl^XEH`kFD3*bfH8I!U(?M(zsLvE&nyvSw>K;(!&SV$x4(((*z_My4MG)IrpNm9rIIXYD1Py>K76usw!(zk@0XlXAB_oW1@~bvSwVH`(jy$Xs$niD@0}OsJlg=$u zt%(a5y%Liz-J_I-a|?$L?Jr^MVG2ruXX7d+h4A%Y)hB1Fb;a9bCx|~ zdeu68K$#*83MnrGdj}V9QJX4OD#a<%t}M}>-$=o6(d5^e-DfA;eQEsMpSN;v(##U9 z_8;D4o7Y=zVR@(-bgtBldUubPYj9snKnqn>+kJl;q5fr?)}}3 zO;u~{48ug;E*{;d%9>M?8qg9_6s(2aSf9+V9%Ltd3cqHk$?}o;2NRbe+g40FXZ0tS zhrhgf)va6?_o7K{hD&2FyedFbg_1wB#9J)Um0YZoZl9tiW|*PWZ0CL5{xGjikwp=8 zIkRVG;>b4Zi}$kF2u}5BsjWk%fgOt;PKY>NiLrmj(z85TZLP7^@G(wI8o> zO17A}A%F>f|=Hbo8oY#P%$6CgSgCt`8TXt^B%S1F)ep-*R z3*aaRwOPcbuJ7OE>|0CG2sisSVmPv=j1_Gp z4^hGRKz{7*N>8U=-DRqC*mgS``%S_H79*4^tOmMRDHSzZvtKOW@ zczWZwf>$(F0^GKb;}+?dR-1O-+IBEUhXwaF)1KHn^ViL`+{y+2OvLAmF~*y9YDLJN zVZXong-Qazep0PJiPirgd7CVe(Y%TCF4;}}qeu+D1K#}yLyELD5zxL(`Y?|Z`lASz z6W%leu*c0*5A>JY`poy?S+ZbR5-}HcwsgjU?Bb4#{1O#^apB;H<8lv@5QS)W+oBS_ zFT>|J5e`yyvOc?|o7sVWNZDbS?(#jpY26#o45VeuSJ^*ioXlQ4$qnL_I@<)~o$b|8_SdVvmQ@jbYcz z$By358{=D{DsSY|)z_qL4L5!#57VvR*sP@Q)TkIGoym1Q~&K~Klfs;b&;>DL$}D}glP1-v3R$QE z6nZ7>M}m*C@)uDD%l9i@`vG7=pU^7DqG}V)2}7lYS;=PTOf#^GPNQnseP6o?;PT-y z94*+?Tz*(eIh1yn-Z~r>$CX&xz4-r0_A78Y2!moBpHN4KVqym_NR z9K7-MK+gVr{X83I1_0cjU(YTmDQ9|Odx8;jzraox$q#dOKJxTVVdm$3|L}uQUrgZh z@$o;OKA*APT^_F%jBQ~@o?g%9#$&fm$LC@tb_f?V1ccZbzi&P1M)9M3zW1rRFPEUu&Wlrg$u7?$7fBmEOx<_3CCFJKl z2HN+Knrr1jeSg0n)h}gIj!G!xx1_!{>+xXt+=uR%v+BM<&7>CSqUs`rFU}eB{5{MO zDI48~jn=?9`Y%|zz?RS`)pY*I?C?88_TYy4SBoA|sZf*D>Xe8q;(or0<=GELy>py!2y#`f;7BIK6xuG2Dmtqeo2e)Er0jf&;h?e=y4paT!IFZX@6FugzKXPtTv;4R~FAt+*{S;(@)Ws zCn$1!&^+k(q~LLx^t+Yl%ypQw)@k;7kX!tw=@=H{o%_MO>URB$^#1io=n>-Z2n)Yx zF^zSVZGSj2b={IUG6N#zVR^&Km5DmR6KnKxtcN$~}g*Ce)^V!}|B zzgkt|F_=Vl5U5V?`yY1g3;q9uuC82=y}z8Be)3Sin1KqnC$+bu)gEr@IgW@zP@%mu zj0=N=`rqWMgX2iA6)0v#7UhhQmFqCk6w9ctk+MxU{<#F3y*v3Tw|x5YyL#q>v)~8> z66zqFs=p!Xi^~tG6T}7xNi9QBrI=GmfBgoPTdW@14)N)KWBX#u_UO%plZX|U^G+nm zEMcJDeiAShcym0)O?*7cc)xz6=r?`k!iD<2Dq zA%Xo7)$8)SnbSBhG{$Tn<8XX~{&25*+3hd%oVJn|<9Kwer}0DeDUj#K+4g1gnq8Ip zZxs`t`5gYu-jChNW1D~f%|SDk-D7zR_ekeoS0LQ9xg^J2GvgPq(MY`e@4rFB?VJ$f zxSRIk`4-g!Y!Pm4@Y-CGV6H{j_;mQhAP~en&yV;x#KV70!HQyksusQN8hY5z%4VpV zWR8Cp@yYdcxVqZarnCI#ZqC5hxytm>pl4y{^eEYXn=lCIaT4l(u)9lV@Y_;~dROEFRz11CE3^dnNRBfm_zM1>Zrjw8 zU<`iZ*w7;nkfPO5aA9w!b{vXA z*R)T$twvjMUx^`1h*5>V#0f0&#fU0^FiBlM8|gJ6H0#{qhabo&nYY?VX$ zTtuP}yLle8V}d<0p@hP*7Dn$%)O&7H6gMuOaXs34*b}%%kz;;bjs%K72 zbF8#zzu<1gV|wh)iaM0F;o`-N)CD%ZrW?;|fPGpKu`dD68VfbEUSS0Y)?39aH)(m0 zmcKmbz%XDDb{kq1W3YR(@OwAs6r4}4E2p=$3=t!i zpfKC-qWpE9KJI}2tn0_hypPEGE!i17oilJxU0~FBfptO4yVme}_+~%0QcX16j;|<)rt(zMO6?OVX!(4^EDtI>lLpRuHuD_7OY3G$)sX1e<$#;%=bIf_i-{v$AxN(hg2B z?HkksbSaK~?OMS~T*{(>%-fJBc18xE?i+6e()imUUwW3q_U?ZaNqq9 zBCh=_ZXx9!z|D$`Z>Y$&@bpJ;PaZ7KH%KW%o{?Lj39%Zl-+4nzsJJ6*BCLQ7PbM=8 zg?|9HqNua{zJUhLF5gLW^}B@V&$_8w5y;i9K+yK@pXaB9IXe{8Xn~yLS&mOEL3isX z>TEP$-7!W0egJD);zhp9UThTKIau=LuGj`Sc!g3b@Q7chp zIA>J?c;RKIG*02>9nBT!EHzRPS$E_fWp1fTIB4M((^UdG;ZAO`pJEo9s=x&6sj zZo51^zFYdr=kMaYIcS5O>}Q&Zt+Zycu(0U#W4Cs)r_U}2KOTe<&b?KlJyTJyOsmi^A}UEVxqZr7#DI zRsDXLcGFl@3TgD#opaNn@Ed?W?I~A~5u;e2@knC@Q1VdG7WA|*8bYO4{{dC4kUz4p z@N!YIYau(EA;s3V3uZLvyJkYl>~L4})4369XuaD!tISbi`#B=Y>aI;o5QgbSX-qTjn>}g#ulV0Jg0i5B#XW}D&Hltw zpzuO{Jbj-eEI2ykf+2WQZ#-r(pxY|kKl5wS;r*$nB89_wa*=(%-ou-ld9FJaVD<+T zy4vStK8|fn>C5{C93oY(IM++%7-UV$LIQwmM`MHj9NSrNniQib@j zWZk)3Kkt<>C3D@QpBD4mW_g?czGW=vf_@RcQPJn%n+8Jtbc6#I zO%vvrGGC>CiC%O1wX-aaDy&ISK zfS;{s$TCPN7|8dizE56KS5KiRRNXF3;Yq;7c>$D1>4`BSmHSnA1>L=18YS<=qhOeD zJh^e?z}GB^$v|f^-mRNLfp=YEeaAvacW!EY!)fsesx43+=2-I+lRavM+y zv7VQ4&%PeYb-8E2&t(nzQJP?*ajBk9vq&8c$rK0sy}R9NOTGZ8L>aO#t@lj=c{&_ShkM_2%tb|@R%t+KPlj|FKv`GZC!7X3f&|7}D%if?Qxud@{N>5~0xhyyV74qezB5;t4 z*vX3Fjr{|d66188{xbkKq8<~J7S*Yri4~oGy@Sv=XZ)T8H_G--%Z$I}uQ1^T*X@OI zEHcqMGw4=Uew7~6wi3)8T>?{sc1niOh+0b|@P7c3KyAN{gZ#)VN>Bd{ZcR!(YqO%j zLMr9$=UGeDT6j5e2U7x2YdOg%H6tHOH0{$#f}S%)3Ju}?TTI=u!2bplZ>hsK%ss2~ zKF`dHu5ugY02loaQ4VN>L+Hr0CYa7!q!&7MFRLMtS^Q7Z5a5_M9h+E2UGl~AY^P(_ zK5r}EUqaC!(|komgI3Rb=OL@l`7p#Et<&&B`0U?8b)Z!9w${K`80@Z)P_ zD1!Ctxy|EgniM5dGbXN=*>`f*|0>d^!D6~s!yu}{ml3=!Z;#nk8e)T3JtKdbmCFz} zf7`&$^=`p@!UM*@YS8_Xt!@){#~4~F`R~Ns-&ovz%s$W?t&VSP`qrcJ8ND_FO|yA= zh{4c4D%p69GtHAHVIR!X*>U7I!qRDc)+%@@LBbVi=(SJ{Jf)@z3@GsG`1vfwToM*_ zmS@UCbDWh6^u!-aMfIGVzpS!w{tlQp|IppQ;r;h)edYqb(eJ}H?iWmqF`~}R$86K? z?8a&AdSHNW=%=?^uydd@;6D6iU-E4rk&n~&yU_j#Rb$A1n4Y5ROJb!!r8=E`@#`N1 z2bbD0G?Ka`IEvzVxIpn2K-q2Sc7KmA{b@(V*I zqr+}nU?XrG=mk9Fe^TEMUupHGCu1>R9^hzfd%0!0rov2n?g z1F;wEIux~p_-qP{0%ZU4g1ARo$yJYD?7f~JMD#OO2~uE6!jWcgYsDg$_EyJVDCd!O z_jBg%EIVCl4?#-0lPnl$XPUiBtymuv(&Ep+n#@jyUh(S1mk^D4uy3f^s<(67C8%z6 zg5~JvPJ_IxP4ZVfnSR9h;4IxmGE*}&!=+i|m>`V&pqc6xMS44Z4=aQOq1foZqhNOh zTHv7S-}nKwF&(?bfi%gwEgsD^GvbN7fQyXskFP%i1Fng3DsHu*v>TGWm%1L~4ky4f z<=ZXgkx2x=e3U@_Tb;^zt(*i!U*PFETqjH^;~&CONRD|d#XoL57L|``NY$Xi7R(VS zJ6c@h$!|XPX8$JM@sx))<}0ME@btk_TKgW{u=-78vpGY|LywEZxR13|9kdP;NLdj zzwJLbxO?E^zwO=IyT*UJ#(%rUf4jziyT*U}ZQ;K$t{d#!bg5w7j_{X87`R1JAkp;c zR8l4S$N1Z;WTHyJ9@J2AugkQsF~6W>TYu20f&Uu8X#f@R*{7~OPg1mj;y%flX5h z8e?#R*?g^HHwS}gkz_?Fs7j>pE`vxyf+jQ+2%^MPjXouCf}m044l3vFq)XCoaTU7~JtYe!~kc9eZEa#@umD1Lpi(w+e40r9`^Z$97Oqadryo~tAjx?Oivl;HV z+9s9ioxn(_>QaA%XAw}XRK+Qd#!a2KdswOu(B_szuRt~!ouRTeofI~{)nt`T$NlE{ zq%ts@*0K*%n5PLI?P{dwNj622+c;EH3652LM)U*ItU%C!G4`ULNVk+swbvH%OCJu5 z_$fIk3wyJu#XxY5N0YBM$LMJ(uA|%>MX2St8C~S*A7%QE?_yd z1bso4pawJ>*Y?B!GuWk0&?uqtI#GncVl{?=HQ5E*A-o#dWa0cIPHqrA5#G1uGW4lZ zmg{rI*OxQ??bk`~mpvlY5BNhi{_ssJ7V`sDS?SD;uPa~N@V?jqfA{NS=J(@6u8Yp~ zLuufP;)6c&@R|DggF3&o7=NaKM7yr^wVEqUQoVmT3x9{!neVV4ehqa67^8A64>|F@!HyAk zrytPMoP-GJ@2P#7#jsi0_!E-X+lEP|Dw-&a#5|xdBh^|mvnA7ph~8Rl&Jbl<)3T7W z$+NP*D)Fdp>JbUaJx4em0{cLbwP9gHKDl>m7Er*-U7;epQi3@j`jS^7{29t2mf;_J z%f-9VY^?B#ep;FFWWH{z3N#JiENi;ylLL|6WFVy$c_CWB62Rrke11p= zzPl*@3%A$&f9;8WLf?uXT1fArP&mowjnpPP-4B4+EJVwk*86f^YdosJoe6K%Z>vsY zT%ND(%1zUvw&>*6@(;->>TkJXP$Q5^-8Oz2EC--Gm z>=ot??VLKnvqp}QHzEi)vID(*yK!4%KeyY|v`yJlnzyN(TmELhw#{Gr|6JSuKePYO zqy76|UHkuB`~QsZr1RCR{2cy2ckk{!+^_imJh*r5|8wpCbM60g?f-M_|MQ#7|A+w- z%Dra_7ceyGvk+oZWq^Q48VN8v|7gVx3v%yi>XF1&rTjenEL^2zW6T02@qSn`6 zmhqQmFZ#0hhO%CAE&3)dKiYAKTpgJpE45K&p?W&X_l9)2qiLxJot1qRI+PdfeE4@Y zU@5D@_uvhBJCL%y-|8T5_Ft9tuSAE}Z`mnKVfubdmq{BpcBkjs^&~fsijW>+{xG%< zI~m7#t!5xW;Hl&SXwQhAnqOk=E(U<9l~=rmDqN%ZyL8=ErfGKaBFl5Bl9c*!tj?rk zBkn5rVW+jRm8d;(K5fkT;=a2Xy_D>EBg|!KCEdc~o+A1&h!9TNB416;D!r-ZFtDoV z?9dUat9cA66^&kmt~KSIFw`h5z#Ay`cKfb`flHAZmA8UJaBt|howM*%>r-TPXhwBd zH?~j@kTa=Dt6$i(uJCy{4%aQ~ZWx-n`ijl;x@9cAAC+kJ@m|>1z3x+}pfiY?PfU0C z8@GGSAJJHy&*+Ku(P+6!rn1M&6$q#8PP~cn1UN+8mzt<`cz{gVR9-csS+4@_D433{ z>VeRvhWtr+gFSpp{D%&mY#vq0@jO%fww z8oyWPsibb|sB)>ccxXZ$169wD=#_z<2bue&3XM46~6$zZA+$DGlzzjZ47 zMu90PX45svoIAgM|Rw) zbY^aA<5gy=nq~;~O_EW0wpyZPDE3_`$0$IXsw+9ax2Ut~oyn&jCD+#ON9w7Y4%Ip% z-*=F z&G!-Lz$>%wmtkxrr_6yG@pKx6|Cm|fS!aI2nE9s6J+}-&x%y>f=79f})$T@mE|q}A zEGJOrp;}wIv$Eq*AJt4&n$535WtJJRuzg+6quMeDW)Ami3*9w^_TAC5mdzO>6gn_V zZYd?4C0~{Oi?m$oZ(6JJHa=+S*J+w8YTW|S+yiH~$$XMfKrrzoY zQsA^m=I>5dg<=9N{Xhx~i{zi#)T*B!$lJ4gK6wZK>$mEGyf0Q|nI+cO{6OCRJy|4k zYrR|aKn)C+AvrCwQ6d8~59}iScQ93Uk=yh@ie&S#X&;Nw1HCvX6h-#h;RAU)UJaAX z{*)fb%kyl}H?(z#X;2rpUD}c&LLH`vMIBhgERhtkmuNR8skuIH8AoU5Y?^sKPkp|q zxxB>(OG4*6L(xFjux^Kz9ARdl4AJ9SFR(=ya4k^^+FxCKks>X8Q0hOQMvT@)hecq%PY z;K!;>BlFNPmdwZ2E4A3-m!(#VyFn^krvp>65g`Z19~7HOig(0a;NO`n{+RgKNZ)Yg2g9^`$v4E53Gwkn41yKH`*F)%psP5I@j zT**=TJL4NpSH)s#tcfU#urQ9c9q+{b7!JS23V;S`(X?i-7)Dkcf1C(gDoQ;E@seKh=L9H_Q$>I>UX zoJv_8Yz8G>vU(`1#hBmUt*h3lj<=G06y(QBER=?w5_@iuN;OsCQo0*?q9x4>Y~2eu z&QiZAD00^?;xz?D?)gQ$rl5$3@b9wGyPC_wfnrFkFjy3;xj!ujikd8W$uFYTVqQ}S zirjS?sxAN&x#tui7k8%|6uIvd@$N@l6id36|BGS+W+w(%?eSgm_5+LvU z`)-i{dEb|YI4&VDcW`aovx@{bNMP=u$bnjEVmYC{JoJhTS5vpoP~@n#?) z8|*!|$ZGBtf+F|T0_YfXcj$1(-K!Sy$os4PY7u8tP^2*;PBBpbXqm!Vtrk^4QkPN2 zHgoKS%P1ZlbE$4)eJ(5QHP**vwyV$S;Ya8n&vA2oT&B4`pQHD#I9{Lq_pUfz|E%a; zalCl2^nB(?F&&Mc*@sPU`_JHPsWi65XLKm_F55|WGhAWrtEXD;W7LO7U~l(fnPaiH z-Q4q0+1qgvt>d#7ZD1(dYzTMuuD&SN!Mw4)Z8n5+iubNQUgyaF2t??}2feG0*A*tN z&fs0i174CXh+}*w;*t_EH_Ql5D_N(=w>L5Tu;~SJEo8r!w=pDZ7qrtu+cb==zXV0Mp zfkn`->h^vOXh$-gwrn_c`rvE0fY+2K$u;!FLERT3`-Q%_TlWp2zyvH154@T2t%T>Zw6{O`dIXyK>7(^4C z6S|I4g`+8)mpRJ&c!dMVzr%At@k(TL!YYGIyoncal5}o1+kLa#nb$?hS zs7}SZ<-O{=Y&y*-2@9%lzN)-Wrte$f7&(tMQb za+%KP%Kzwr0wXN@h*SYBtYDwbm$u^htNlj@{#^8@sF&*>&=lPL%4qi~QBoiA6<~Px z@85k;eFIzrzQqkrI;j1>gTqs=HsutJE-dJXqCHqF^v@%c7l{Jc5y6h4YBVOL@&m?j zZgfQ(44@hduJhy+quEcBY+B+Ec6KH8DUf0^w&ChzmpZO98{B5zD(hRV!%S1Q9pgM> z+hMfdyfg0$d-;-uy@jUva+wKDUWZOz$A4ePe_!W+xc>7SjsM0o_Lb&I}YS}!lXZeqLk)BYv>(*mJw&M+oq52qj8H<@go+TKp1gx?U#Vy6w z#x?!<9$5TEvVN8?zeC5}ramErTBTcRj_mGnwri?4I$I6J-7PPIlYfy8OLTWzsvxUV z?1$-Y1?t_teTz;Foef`Ejly~;=ON_LJS0jcVj8eCq8!%f%>)O9W2~dJEp6#A_*0CS zO6PqN=QwNam?FFmTP>}%!Ks(9X&nRIjn0u0$13RA8V?3!VE))c$|C#6s1;+x{5uJ1 zW?0~NUgAkRzPL*XZT$eBaI`LW9D8?;L80_ZYv&kUCa(1T5`$#}QvzL*L3MPHx7S8E zYbjSOQMhfXqZ_qgPX3-}^Y(iNcvt#uc++Bt)%Q5LG9CvVK9t-w+{n2J;}iW3jfF~E zf%y*H;8i#jRun16O|z3-zSNMHqPN0*t+OPVmXs1E(UnQ-Wo+uvDn1~&rX(yB zq^1zKkrv|Uu{yCRZwZ2CqR>bQ*Ts^tT$N2JMYsft^m?wGNm9D*aoSXi&KKi*6lOP6BMs+Jb&2;X zK7){b9ruem+uGw7ifuU`2>+^rLGX(JgVFT_xR!8_>PKhkbaA>uAokYZ=E8|u zK-vUNLWm+uUPMB8=8Jxq?dh2!N#v>JtpY87zi~7ta|-2ug6Vp^ zyGGMMC=h;Z#9X<{MClyPFf`+q%B-qlOVIE~paGaxhHpVnbjGpG9Xu)~qlm1mf zsvt#>w#tcsTI&AM>5S5I8(3-_T@)CjDarH-`IQietecW^4}?9KaauKkfu1MRe3Gw9 z&JSn z^GjD6PRGSH#8a$w;LS5zF<}AVZM4o;cs$VPvKf2kV7$yYCH)QSb12y%rB|52sA5x% zCl>^+&V2Pjuc2vbefGAicFecCVVvaY( z?epwy5~QdDjx?t4k~)PRBc0caiGpnoYjB>8Wi(5v+e|cuvvR(<3T8TX%ggOXr8)B) zOveNhWAIdAwx`8nnvDp8!&}3EWXEG61b)?nE{(SWxqp-QqPd>KFN92`l1Eyv_$T|@ zB8BBW%_b{J99Sy0>k*DhcV}bh+u{N*MIrr;Y1DmuXg4MoglL?n<-CP>=jlu?>?OID zET?O=9S?czKsNc>>2{$lA z-lqj#Y9(7zin^RN?p+oTk@7qy`$^*v@~3~q%~6QtVCZC zhg3HagHltTr^1U<-1^wp3>Sb`O_N0{`kQ~TXj!C^7t3S{claeY3p6pq6sp86CcBeO zs5K%bzkk%Xz}RqGLeJqKEh4Mxzrp4~vW|6PQPC-e8eHTh1t(K_sks(kB%EAjh6psS z+>TleTm={GOS9P5PCeKDKfj9q&)vIK|DXM9|DVs||MSrJ|J>g@*nfEMQGD;g!GnXZ zu9?AqxBt)k<(-eX{~tblSik@MpZD+W-+%bK=>GNo|1aVH_p1E=;F|w`7XJU0$NwK3 z#CN}X^awWK^#SnT&j0ZS1vj4Z&egZZZv6lL!v}lU{Qtj(|6lw6|9brY+W-HW|8L~~ zN@H}naDZn2e>fWs?z{W{0G{2y_W%DJe>VC5EBXL(h!r{f;rIUv{lRa@`*NAi$uWGE zk5`=E_cR%0Q*@k98F7z%K~FgyyHrXF{+@O?@707A^u%kvhfxV=-bkaTCy$Sxy*wtn z5Bifv<8&!LX`YIEz&M)R=;LXH(c76U|@Bi!b|N76z zJO4Rh$|ZvVH=O?m_wIS;|Ng@V_YSVl|9^+`|2qEfI{xoE{_i^e@8iY)-PBPUus_o2 zI!dBt!cLpS1kUqp9F4MKw3;Sb1O`%Dr6|$gjQ)xl+?3k{d<&vALAjUtbVYj~U5i#z z5Nd!gK=qjfrlkZGu8YOW8LE#_hL8EA6L*(>zntdFk`vDUw%sDG<^OB>|62aPmj9dO z|6#to0uo50{J+0<|G|CV{_}AE;9CE8E&pH3|JU;Wwfz4ZlmA7cCuoQ)*T0W|8aZ~S z)X%axh957@@AS_!8^$2QPxa$+Z9Z8R$tWErqj&lV0!*lvsKv7SAZML15!kx>?=cnV zkJwoQgrrT;K_F80ri+I0wF`d5Ad@z*i}iTtY-n0|Koh3@E%;F zMSF;bjafNyATKe&r}(WW)Xix+BcJg^Sz{2__B4ARVNAq%GL^8rVT#N`S02D3nWc#; zO5tAYkIw8y!Xe^sb8SiLb29_V}li71V<97)7PH%CK(hrCLP%KLn-Q;`d z#f|ue6$sZC_WB}S6um%F`WnFJ*4L7c`8M+H+p=Qc5FkKbrD{HOYHwSu!uCU*Q`?*{ zonCDymb*{8gt^d{hli>$kS$Z4@tEQ+yV0#%IYU7?L0o*Nlp>f zys?Nq9-6AJVX2Wq3!LDBSqW94khtYa7EJ|Y)hg5~rNr!yl&{aGm zRJTX!RpGPU+1x>|68l%xwVOKBo?cU|w&8#vmM;*jUpOFf70NUZOf4xUCGxg+7x+_% zwdt>$zOzPg&Ut712xwzIu@GfYuE6K0GpH~-+GXi^7uZkDg2PJT5ys`YQ#htjp9FJV zOCl7%TRDVS&{0lSsK{7>%+s!L)9nD(=(E*)nkQrPZCn4;2|}&uVDLbN3g*G?F5OkU zj^cr^Ll-3-0DJ|a>*VU)Vu?)=4bOC72{0Lt9DHbeH8z#a++Q~wKB$^3s^ z5g9$xST<0*gYk$Mexfk=4nqn(rwff;!?$Z)0qbDk=E=&Z9>TWt25=Dup=S)W4x+RC z0<$-x`AdPHn3^zEUqKi{jEH%TJaL?zo~9%w>HAM2mNHM!Cqvq4R>1WK7z|41? zqlK+WlgBAI5Nn1hz#4JvOjY${sBEy7zYM@+eRK&UpZN?hDe*?tmNiq7O_z2(*S5JH zq~OEIR55XyfZux+OfR^V$9=p^nAz?l21`Ch3dTCzF?CcNIF77WcR#Q@MFh}polNFA zDj{rS4SkOQiK?g1Gj9qyT~d@D8oIIemDZ%h)J$Jj-lk+shVZRTVEqU?T-VU=hZ<5x z!DN~b;UM4>tw<-RB^4E`m3$Q6%I3p+j3brP6@Hw;mP?idje?jCV-zgh%H{6u@2exX z6e^jsS%%hU!Xi@lCVD&2n4%QXpIYz-IU+_8NRG_>Fex?MiI|~_)jVRcl=g@!$uh2~ zigqM{A*cj8tnQ{JX~$D$cKyAK3Q0Zo?!MK#y57b*^~nga1~a; zCb+LGvw7+?@zS0u)gO1t{wDVNW%4e?H32DaGDVe00SE0ewJwIJ3PwKwg>n&#@{EC$ zu$#nsSj4lyZe1`YQy&8^#Ui{gf|JvGN3r5unlGV3_!ANq4cUFJ=~wOdvOQY9@AT?b z0L%Aki^l(0B~wnJgklM9?1VDujQhqNyM~<-FNJ-;!eX1YP310&PwkCn%A3tb;6A8n zp}W>$MZCrRxBYfg?jQJWp3iTajY~q|DKHH>(-^NxXvjLak-qGA)G2papDWTT11qca zAtap8=-r)>dO3j*k#~j4yTl)1>?*6MBL>H5nCH`hTltA#m#X^9Gdh}ys0pNcifAWv zp68vu<{OEH5baEqvovxRi=EK!Ncm|pYu3O-7ZJ`D${peD801M)m9x=S-q=LS z8WEeq?Afkf5T1U(?RNT^i!5recb?bbtHVKDvAEcUrDK0R~ zbdcvVv6d-fO{Ne*eqv}Y1hJC`8A6NsgJvCoSPE=ZW^4F98rxa5#Nvy?sja zPWp&bN^XFSR7ahaebV{Lu+gTxQM>b)IVwW!Y3lu#>IWvJ9T4;M-*7I@f%Z_wW?>h2 z$n*a30Y^J*1u-Ej_4GiAEG43G@^$kxU84Fi0wT)c{-5C*tnJESM^DbE=@mok3tRaY zANU5euAj|M^MTJ?1kMp(SR+D_S%FrnIs(d@vdonrN*XNta3#ZTwKaFs&;HzXcX)n>qK~) zh(82wB&W2OXt~+s<5s1#;JSF@RRPvlo5z_kc{J6og5g7v7zv?H6>J;3`0rn4*fR+UpddLv)N)#DP|ip;0omIII`bp8%gdIHH#T`_KDFgg$7rdw%|-6tw|z;J6=$e! zwN7fjvM!4BwpchZYpwKzKsN>m~s((9Ps1h{`b~K*~27ptW-K^GBfNjNFzox)x{Fa>cgNQtC2BXORZB zNkNy>b8vFlR)sggv54v4j| zE1b@T$`Gx)a_?g)Sa+QGtSs}TSS?Z{u-YO$?F|fdeyxA}Cctz}Il3D&c_T$L;%Pli zzd-S@+9Qie-bA>bg~lp%D$)$@C&wm$z=ImVqvk zVvMaTCvR=`58j>eGqe}cRv_npNlz2Cvp9{t>)Z(qnlE*K>%@Tzt|o_!eE!J}sc4`a zrSqi7^77w>p*3xk-xNplH_Nsx&2!b-$kA?U6LhUr5pT(=kfWnUTT3N>0Zhp&wpmyV zFqslXFSB#UYQeQ$!78yUaXF06H3{Hg0ub_;XX0(OA~G|noMh*s_2S{s%}03!kr{F@ zz;@$48K?UF!Is*UEBWWb#i2-(@w(mV1tcL;Zno}=5=Cc4>{Z-A#5+26C_%N**-*~3 zz|I6JA6TbWH$#(q7&((hyNhglC7J$NKf+E(J8eNhcw1BRuY_N_4~X`Z0XEDN}kBf)EjmCt2L)LqfGpUS2U z=Y*->su!3XiMOoOXm3}yhS1)vQ8rwSY_}w$2IsE;X@#vm+gN@=)*oI9A*DJZYLa^; z(!r>@O13kyxMRB;;xeg~kE3sslC^Ryxsh$ZB$q0~b>XKO0vb51RD7+8ijC*iVw#MA z%qce(2ybWD52cJ(Ty5ZcrKwJrVr!7zjk#fUI5!FEU!T0CeS58zy)-X-kFX<%WxQf=Wo+OkpR!Rm~XL58X>Eel7V# zUu!2}2bLS~=0?4v7&)@iHJ2heBzq0sF-p|tdE1*WSPsJBB`zA1o_%Ya1TPj7=Sor2 z{j1#~CX@_IoFd5FCYkT-zFE{@8{ORcj^^6&Ri`H>|3!2{HmPpIN^dVt0Q!aDnUjrj z55=DlH-oK`DK`uF(%o~)&vO#PO#`E)+5I{g&hmze;g518zU5DCvP1t*Lv}>iF5P{f zE=y5vOiZp~Nt)y-8^onUh)`z<@4``z$JJoKiH{^SPGtbP6xr=6evBkzph~618J|Lz za&(Dw!b+E_D!HGYX9?yJzo5umhp#G41$wvA{xe{rDj^;|Wr5?==8(JEYbuu|B<{C0 zJ5Nb=X3=n^tmRRWe`=ZPuGzxPED|H{XJLm`*QzGEG7>NLvDSC$Xgbw=BQ?B=9Wy*; zMgt|J0eiQc(a3cT@(UZkE|0?m#hW11tSb0$8v&1m`gXgDN_ zdrXK~guO@@|HE-6BsKc3;Yvc@=w$9i&Q!zC>H~Z5fBQi!?N&Y0E2aF9HsWfj&HMP> zpEw8Ta&0+H!RRaTDKK3>0%M2m@2DEOk+YY2aGIhxdYjB3GyZ_|t`4Pjx!z=f35jM z^I&GiT#ViyS`w0xP~P6)P~gj0;G?ae2%_@fD0T7B2;HpkP_`RYLvQ^*SY?r7qGu2m zrfWqm_AuI=O|!`{b6C~pDYfEdn&y;4)Ua?k9<6z5rA;?lR#gr3oxbmyV7GJ*$1RA} zY1=efGrv^8BDAem&Ht!wDTE=5GKqRwxxwlFW5)|LNMKH3{xVeWIGtKk`G!7qv`l^S zSCX#5)=#F{KcQ0L4%2MdMIptmx~jjx+X~DzoZ%FJL?5+4CMyucyH#~o)d@lC9abB3 zXkq;c_Q987cM6%)6=jqeIp_**MIF*%2s2YT3#s(FlzrM_{Jz`(_tN?PFulg;^zoNS89QWBr5pZ)@g(d2W~11R6IzWt@z+8EgRn6i5B4c z4#UglbZK(E+*72kXpyrfFuI_N7~#6<^xMu@Xy+=kJ$-D&4Nxt1hr}OSU9TzCFmwv8 zii4dDm^)s*OplegkKLyH%}VP$CihNspg`YljG6Go8Fo~E&MU`kk-cz7Kf(Knk6TY)jSjW=Ma{K8*aJly zfyQY>HR1&c78U(a1?25wIuf_v!;zeHt1Dt1gdW!@>`$hDqa8s7Y7GcN(j2x;e zo_ZC%tI{~xx*`%bEtQjKj*uROrjEAx%z$xzBWbccb58-4m-FHI4SV1|wkgL^6WJ?gl{%6VI3h zZNvcJ7^~CMhLPJw(>hexkhxYWbZo9b9y;hfSa&xf>y6z6>Ia%@(UlkEr26gT8O>?XZaI1s_t67v znTIMfF|gbA`eXLkwZ7LXMuT4&Z#RY8o{Owi4Qsb>t(nRKE{Va|_7~~0$kOvvN8TYI zu##U<+eo*50@AQtjI|su&9Kk2LZG{pLx@&hNaqy`*Fe!8o3 z(Cay{k~io`@5WMeqlVCmBbp}MJQ_NlP&E`7hMHA&v5~3u&lWY$r}Y?$8m}3+l9J2n zpqmAi0rArX&^rKxsKd8iRE1fx9-@g`+(6q3pV}p@X6L;EkqTh0wu1rXzL2dCehiy4 zH5&_je-myqEeh5vGY$2h06EN4_UR^0DEUncj$sr%$>*(Qs6M&DqKrV)PBOxU@@^z4 zCc!4jV8*G>3<8B&N@Rh^5!@VwUehx& zcNld48{ux)e{S%RNSKt2E5j~Um?MYqmOtE3kd#o;U@seSv|`{F>Ve|5XPEK$Z{JXp z7KO$vRvoy|M!Irn-#=fN6iwaGXGeWzu3>=LpMIaDJLCH0u zMGDh4kzoqERA9KwxrF4J1E->>H8~noLKHquC#!^#K(R+T$rf-z69|JEx)YeoK)Who zhD>TqpA1Y869%+O$U^cCft8AsK~Um=|E$v=Zk#Su+0uBp^WzcBP=+Ei_toNN0>}in ze}l&KFB^{Ov!8L+*;YeEHi+D`40!%0BVdGU+q+%)b5ht6UNF$*Hnr!`<7x(_*hC{& zqn@Lee1)r~{ou}F`@>X2A`_sET5F|FPv?wYlUM8iTgw}rhL*f6a*M9II&0O*j_J9I zAYXEFlzGHBIppB%xY4re3sz(>FgApGjSHn8I0Be!{Mk(;a`U}hqsSD->yJZbp=hF5l+IO%Ge_p!&u}M3nRvoR83#Jjb{>5D5=Kj ztaYer$)qZ1$f1H05!1l6Wno-_5zVD- zf2O;#dI3+$8u*=hO4?EktcszEKUto9mRI}X)XZW+A~-HquM@}dnKFKyJ|OU+flKx^|^$axNQjEiv?lzbV(qjb<+ zCK-Z#vRqXo%Q&GBoIat{j?@}sgdLexJ`mtZSP?6Q$~%lEy@i34Q5qLDTLY}l)@ zLGh$hoV4VF7SHxJXAs)s%t@;N9tqmf!-%ZewfwaWQ7)@q?ZGBCZd-f^KoN~qAhy|UnXLkGE35@jRay~wcUw0jGK^p%Q*U3(9u{BS1K#y^>Y>cW#2fZRV7ow$uOCuKJZiJ znqwm44ThEEHY7x06NNvk+~hbC0`I42@gAM!c}dqf$sAH!LnP^hT-O4uLo8&1mg99W z6qBd)2*Xu2T?#HT&m!srSf#Mc^QpLYRiZc(7m}-jRAs44d zei`vqg!Lhc7SLwyDDbGg`p)Iy>pWe7@?YapFT27Dx@t_J<6X;O8a8LQSq5%kO1V|N zf>{L}-XBPwq~H%~xq?+!g_YPBQJp%mE$I`x`hM|vn&G9_U%0Up5M6No{3bNv^Dd^=oyLs9tq5 zwKOUwl5HTga;_MLVynqCQ?^V}kImU2;-M1gc!<(ZA1Z`gXGM5r$!;QZW~KP3`xY4G zd9Gw{y!U1vD6vjRrKKT0xMGd=iA}Iujg49^H!j--1y@eQnoUh_wKCMhX;zd=qpD#5(3-8Sz9}i#C*Ul~R(wkZEcZN|hn>w>_FCg2a7#g8s-6Ly9Iy%`wCY!2mz{WIHVvQF>+gDE62pIxG=U zP}GIj9V5QDR}xKJKQIR(jj`QVZwqmf`$~7tP_5{zq$Plje7ZtM4I&ObXagH72#e1A z4YD&p03xOw*OOyRrl8Miph_9+W&6Ek+m~wVzGI%HDXtY`o`bY+w zk#~IsSPqaBCrQ5cUGg}~bl+<8eC9VCLe}hl()r}ePH#9&*c}{x9eQUy7or?Bn1Mr$ zjXI0^c7P%k7Ieqm&2bzsE1y@oK$V~l4+h6MUq1PAIJAr$ob$tT%G{SC(tp$O(7S^u7LuGjetfT`aHG{hjKq}T|xRI#oMlQ0u1nNmd zJw9q7CN9uZAxKo8f!5Jdk=f#s?Omn0QT(7Yk^?w(v9uZ4C^?A4u7{(Zp=0^2z1Nhn zz6$Z2T!c)v;5t9v<8ynw@)i-P=A9HN)`Y09ec-E7JI(4wG8|HRpuX#}j9N1bP-e!Y z?P4du3!r{=&4-nep3lG>bf#G@aS5qyw}AC~~XWvY6(`R0V8= z(*$xi10Mq{35~Pz{G*#bp5`;#CGxtKb%Bv4XEZm((~Y$4I2dV&8k1choE}AA{;Xj7 zg2Ttea8=RDK+cfq)j|{F(CHe9tE-spq9E*_ASNc&`8bBsNE=OULHE|0sG3+d3pvJ0 z4AQ2TcE=kZa1|S|X0jsq$IsZ|o2JA0sY#ImsU_Jdmghsn>DNz8yZL}WKwwf_A`?=j zvsmUpCMYs7wTr8c>iW=ytRNn(C)9`@dYmUE9!+Le=&CgonLg_Bj$k@7nRUcmg*Cn- zU7q&TRJ=UT7V7Kh1yZ~Z?vk-()C@vZG}Jmr&R&J-=OWpN{M$j$f)Kg098cke&=><`JiKl}oJRv;p&=C6B@zmq@r z?j=(=eRl6Y+`j+t;a~oG|Ig|6M}N5a^S|Ve=123(t-I-i^xmiZ;8P_Ue5yr*PXR@0 ze)&FIVA?ZUG)p5y?Sh@Zgy2weY^=yJNqIXz^y0G~;|>MEh50ulBE)PpFzszY2gW&EhB@^pET zEppb!R;9YY!cjXeM=L9X1AuQu*N9s$xx}qr|KpD!<{NTB@HJn*GYNDSIb3czZoHU{ zCabZ&KEvyYk^+|OqKFYmFas3}LEySOzr+Bs;sA9(ioX>8&2eP&)rq=woTIqG*ULVr zawjyUs#9txpDH;?-C@9Oa}FuK8In-Mi>~KK_RM84+CqSoxnY0WENQoYpS~}lY+L$E-t^S&NI5v)2!hQ zR1HlGX#qU|?-00$C7^Ghd@JryQPw;D_8@ur{OjZI_l^#d!-m~K$lILG1d(VDI{&x86aX&fuuNOxLFJFHA(etC^@ac;u zhX?B6;j{fG-#tEj_D%9g6@2zwQT3rBXI1O*^8{Mtx(*LsLTyhEj`qJ*zwSLcd~$gF zgIcGr509UzT9dC;t$WFfy`$s9{qLUa9VIWmJ9_c_<$>zJV^#07!)IR~sb&tI9y~i% zSD$Cd+r1}GpskNT+WSro;7GM}l?pTO_m zy*v=Td3><WsIbj0rfg!O$JQ8hpi zzCp5^bV)+)?3ZUx^JUiQ+uz}5VK?G0x=^tY?)Ly~G^0=8rh@o8bY+GkPv1J8mCM{( zcv#CcUz8`=$%H_=Ru!wRb=D#boyItVd0A=A@_e-bg*3M0uq*WfJ_9pPsg-rp3}e5^ zJ=XS8`O`@sjovF{EBYl4o1n%W|5d z#jI;4VE0OuKo_tcQ)54o{p;Hq-nBz$sz0g^|K3x=S}kP>=|(LH(;c)j#06@&T&)Xw zwK<8CnUqn5iMpj?NybeEUZ#Ld1Gvm5h6}nZL%`qU9vLtZ?OXx#pIUi(xsPC^ayBtB zL+9xw!FI1n>#)|I*@TRsv7O#0I+H$b&aR6c9rx47)fo3 z4H7eJjN2VlHJ4V&Y^_u5UP3^>+@!AH(NVpphI7wMX*M z(@`P+Ls0EY*+t>!VgQK^k+P`8+=5mABImR8aRF17O=}>>XX;BOle%8|au#V9CbD~8 zjK}$m9O7aW_m#l3r3l`L51jRgI4#S#ccf3n5TJ}rCtF_x$&TwA=k8Ko=BAFEWQ!|w z1nU+0DS>}EnB^CPL3acWLU$D7fPe11xqzQ!^#q{+>Ps-x!aRfH$>Pj2!c?z15?H?j zG4fOy;)ZtMaVG#Fa7tF>iCb}LXKlUOoOi0)pMLgo02u1g#OdE@jCuF{ps${y^fQbM@_q$ba3EZDWSaE zOP!?%=Uc3ctEN36JCsGw<*EUpcTtA$yog3T$)|nmA`Vrk&tc~b1~ov{8ZlUvL?C~j zp9bhv{mCmR^SZG%Gs6wlnQwG`ks7N3V2Bi>0k6Ss)UzGFpbvUAk)3WeHJzgA+tqRE z18~dm0dK>w7Vd?Vj(hda`7OH=1q9eP$BiXPpu|rWKU+dR$(;#FjqB+Ns@h?yd%fE3 z!D|_Cg84cIU_po7(CKXwLSXk_`!2X77KafqzPfyMiRNGnkQ>%}WDBA*8Nn=wEiW@O zHT{8NYBeKigKq1N9E^mqv99snzJ`^%uGMq6vA=`?tK^g|#_l=LQRU_|)!^wpb@-w1 zp`8^_lYtvyA93}O(Yjzr+>1d*tLZ_lpNdn!;a7El?Xti?gIt>QVIEPydo`YT>^t63 zZSpoUHi4-55A=QG+CEusspl%Xqt?Uqj$jbB3-#Y4Q}p z*FDDwBtmw|pv*J+B8!)z&?^Fl0#Ugl>>lsqJTMWWseN@xBciWU)j1$Ss7))fpP=@I z9f(yI^M6tzUWzyup?Fu6v2WuI2|XN>`}ZF9AuHsJ!|h?8lW(oosoN`CiR$4>R;0v@ zQ4Zxvr)~By@q}au8)C?tT1J=O24|LWiL>IgPjT2eYuvGUH6b>U6Z?RtE@)+3x|K0; zQ!Clqono&5E^gcbIl>YamNNyWQ$V0P_*`H9QHIBfE0$?2Bba-pP|&Q}eALbWGjo`Z z_9#ON<+2LTB!{6*%JK~&+k0qrkXXcJ9y{3Bd(s9>41OIy6jwX|8!GJqV(``MYK~DvaVJ*f_uu zT$AO1DK(-~B8GxOUGP)eiKs6h=wu_D1qV(kjkw*>!J)r^^I<5%HB6Bo^*OZ2{k#bT zuJ3Ej{8g#-Nj4!FA`v2*x-c-q##2NCb9fS8jqJXUGY$v_Of8)PjlOO`rt^e{2ri%0 zaPBx*Gpc1pYgXSz(iIHMO{SMNXQ>ksNlhn(59?KWD2n$1Rl{V=%?;AyxEzsJvfJ12 z#ufISs~D&%BsXkxk*cc&Pa*2quDLF%{i0sRVBw62hehNbyT~SQ>Ngo!usF^@RN zP^2SEk&S$Mual=Z^$v|1d|LG7hABycWPEW2jq$f#pKOfk#UbjxxGUfD8LyF#V??2M zP!_UF&Ud8s6+L?!ooB_uvQEJ*O3bENj=}E4?X)p40u9UNI%pl{xz^pgU*@C zx5D;)qG!>pyao6T$s)klFg$ie+VOaktm)JW zLeo|p4`i(}ys;5b;hF|s`ykyHIfW}@(gftPLzupy)UXbz{-K&@j6{vpLHZF58b^U) zLrFnH6G~wea(*@79$1XtfQelRRVS;Z`+2gOovs#>%3(K2Ajp7PcSRr3kf#_(KeKp^ zq(;;9t80piwC-Nvhi0FdQzdfxI7x;Fl*1z`04H2dpPAYTh9NkNLK=>ZGf1}1m*%t{ zhUCs23QxIn$J-fBLJSi}EyPcoh`~O5X`}WTDU48KDc%RqkxR;Qp+`ca1zvoTE8;53 zRXyCV@OWvSMzpFxa4|ys)M7Nn=Tk$jRCGqm1J~9m&Psf^Zia*DO$_2VMBu}TR6@|N z$*N>)8xB>^F(whV9iu%sek~SivROWqz7;Je@S5lD-+bEe@^uZJ!3>`$Dd4tMXgb&#lq(-j*-(}6G}(E zhv4$&1aoR9L>pU3xL zn_cGXTmeK$xW)x2tL6Kwfb7oidjU{CL&i&7{kZ9? z#;2+=Yn7cpoLI2gsmdv;D*h$j=gI+oUZ<;mwS$xSy1|JGhT)n0xYjzepTOR9Nt>WH z)$A$~SQw?Eb_-&Sl?z%rsZt{sYlRP}Ys^Eh=VsE4L})+t0mq%n2L$2~X#wAC&Kge#|{3+4gSZ!CH{v#co6Twdxi<4WE2Vk(TgG)U0deGT)4>ZPNu&ZtI-6gc*Slz zP9nb;^g3ge6Lc{88-YnZ8E5J*04s+W6o`w*3V^)FlsHjsq1DpHmc+BfNgR5Fn?=Bl zF`%?lPzd*xy$LKNAb931Fn~p(uQJ+)hff~Gz1Ik#E9ND&_C;k!nxdIULMd#9?&nJt-NH(JbEDDj|wy$90%=>(tN;OLc@* zpa{h!AcPE^ED=<*KGK3#07St)vk@jdQRmmn#NV-jEMgyf>%=rHjiML3Hqyu|iUCu$ z8Lbwg5TORt35ywVJp+6hV4gw;hCec7wl{P-iv?RfQ&bL|{&xhP4+YiCM`nr(|gS(kkoxj1^0JD+V!f&naVfgYG6oHi%dqaLW|WY1`PYQeV9Vn^&gNPrW>0Gj*`=jwqTHTa?v5 zA;VteF3V=CA+j36P3xl)Fk38xG^hIShmoXuBDazj+Jx4l{Cd{UEF>qVYS@>9@@oF?5O%~NM5us0lCtF_~5FAbTk&QV0! z?idu$Z(uXl{&zqzs^#q_FSF(cn)BfeuFjPZa5Q!uLDRltncK+UE{mOxYdlAYwcE}@ z?qhP@Bh97kRmbyELYF*^h(ox2OEL+lOCR}gjh!4}?3rg0h;f#2evCiscYaE8b3X{~ z4B?l~WuWGo5WB5f$v=K2@chFxwFhx$NX@$lkFqGs<$zsx2nfaf+KwN3X^BWw*7`0O za6Qh3O)cm|)H>@VUWQ^12Bkwjv{^n0!CFL1{7u75D0#1G9so)=MDdWOTE694E@u%b zQWFKQNZMR0^LGFS7e*QT!;i678(Uv9UMt0#F*|lBC3QlS%OISHvsE#Pl0=wF?i=`Y z_}ZNoEWh!@pvUQ3C5(Vsrk-N7;sP7lbYyjeBA5AC^>3fP8}0}y7drWo|g;A z4md`&wN5_DmLpA>>*A)I(ficga{QD!J2re5^u$=pKvtH>Cr|U~iF)$7W@0!^`Sr{T zRW{sUR;a|LlM+W4@=#!S;9@E$y%|oBoQ}{$M4Lo)(%gZHC8h%AA-F+wwpP^4sH+Kx zK~A#icgv~`t(bIG#BvkK%BauqB9z5{0yno1q-u8N_D444)z-e zZCPIKey(Qh3rB7Zuh5FVFRC!;D$lwrOm_4K1?5(ifXO|noWc4+KV?ViAWb;9&E~kPVC&EQ#7^)l=S=l0h07cwXu7DMsTQmKI5E`{T<4HQCA;dj7QHn=4NA6#3!;<% zmN}efVWJe<^EvDX>JdoQ5QV@`{p1f6X^3|XXW9V1#KRFzXH58ZP!8(a1B0S*BVkcw3#*9r`OTF zDA0fO`rMMWv}Y?fVHh{@UpMhzzm52>zii*cf8E4?h4EkH&~v2-u;%!$2lpTTdE1Zw z`s~3?{^y(cubcR-LQFjfqn#MD%G>as%fp=I(ylW`(^ z4rYYd*Jw)!UkJ-r`0{Y}wpd0A9hH;F({HkAxcO`e1`c82B?smE$t8$qaR!s{(~QTWMyb)4_TilQtL9a47hNZlu>WjrblVs#IJjMem$sS^ANpKD3>| zxKxv_J?)IV5KSl}O<+t^s)_|D*Tonu?SObuzgD!NG$Vtv#;%)?B28_Q# z*FSI^)scWfi^g6cl>f#KqQU1z88kK=@~DTygwbwIoBKL%In+sBgd%4agBdw#fTfcl z3!dD!p^h&k#5+9Oie<%J$Ma}Xo)sew+TrgkWN%aIfiMt7qVi$$5>wNs(^?PFV+-gH z*?i1EvGYO4J%f@4-^4>5<2?zRiaUhHrI_QT*b3-jmT9+62U{|9>J+Y*0zwasoRdrZ zAv+$V`@9Et!-pmg%Htl{!lrunX zDn?=z$`M8hGJdsPY-g}((8{B1yvHx2F8jQjM35FgtE<3wW#nUaT71lO zq>-*ozBj{MV6(FaBXK%$gmleMdNTzfU_s>9pcoGp`5C&PDX9@+Kh>DJi%$2|_SRot z|8)03|K8o7d;j6YQ~Bu`oxCb|EU8+jfNRS;J+{f66hgWeDU}lEiXNvYb#%qJMJh#p zM%gmMaBTLguo2iwuBKdL6{c%>TV#anhJc^s(g5rOf@hM)PxXfB>wv(VOLLrOg^r>c zknU_!o*-u*o#*1@&}G2z0`dWwxGTO$RG70`^_7N2h3=-w6Dp&sK77oCbx%Hp*`ml8 zc=efl1ty8xFawKXO0Vzxuj%mv=W#K5L$By%nkLL)!5io-AMfm_emxM|1NX4rLNwEg z$iGJ8wKS6JZ5rKdjg6&uyPdvspv|~f=hKKUnCC_8Ikj9>%nUV2acmNoE%nTYZs>U*soMIeMe!wmQ!iWA*JW z&4K>4cRd7pQKdyS0B(qrsP;oIF!Ua3LobOD@NvRl>r@)wzhuUIiaHbw*epftir({n z)h|nkk5H_;vou*qaKZoQ;kG)TK(A97H!7T2_(H8(QJuGOIG{=J$OyR<&rescMAZ2} z6AN_TfamVCb|cUZBd`RKQoLROJj$J+f8vBo@7(wavoe&xK3JV4#ZjO~nT}&Jl!XU1 zra)0#7w6Vi-Ddyst~113?Y`olUf1nI_WtD>Ht=-j66lI?#hVTJ{HWKi%MVID28-1Z zpe*B71gIo5db;R*{?#X6l8pAJ-WUJT5zPt5MZDZodx7s_pbpS1U$q@PS93;vj60m% zW`7GUg`{aff7pJMYPE_1R~N?aoQ#oSHCo!awzzVn<5wS_w@Br7Vv3nMYG5u>bZY3{ z?e*S&u!aM`DqA&37gkRD8}sCX$KiOlhGV?%taR+D5QYhHXjx?a^^hBIed*TYiiNJ6 z?^UR0ygAFT85*$&+(41TFLvy0Hg$FN6P5asTVy;ih^S_4v)KuhW^_Grl&@F29HATd z+N%-YYB1{a_i?qk()|S{WAodKbJySaaQHx{Kn<(c03E<$TwD&H;n!@x4OpV-0-t=y z`JntY@V&^!Rg)lYDE?l&og5(sHj;Das}34Zl?(!IIhV<|b*Y_BOeCp~M5c zPJPvhCPnAAy{aSbqaL14ML2|Aa+`uiVYe_-w0m96_b zufuQHpA$aVQI$n9q%U%UIfCd=y63ERl9jpV;J?tFRq9`$JGYX{*Ec+!{^P9UDR3 zX2~t^%}}4->P62aT@$%S=-(x(OfhrNGr1?d$T6l%e|PlcipLdO7|n^*Pli0#_7GG1 zX=uMYQs)^#Qf_>>UGaFU(=`9T?w32=FJFAl6QsEN7mn_EtM?_m=1B@(JF4kPF?$nh z#oGv-|98Gz&Z{qXKK&FLpD$1TTis5x#bxiuwD%=bt9p~o=e;j~gjzo3-c_G={JNk9 zr$_(Y{R;9#1Rs9dG|Eq##`x)%{~>DYg(5?p{u~zF&Pk|U_wU5;KFT#bYKqliJ0B4V zZrQiDB>T0~p$a(#?3-I*BSMq_Q~4|>T&MxvWbl)&ZP6X5Mpdm*aNx_JgP!c7LwjME zwxhisYFg}&FbojNnFv~M!uff0%Dp`;+i40faNO0ojL|kV%#5dP(#>9#8ocVhE*B@5 zS$)k=HCyUhcB#*wn8V1`ZaOmP`hTA-=$ek0U+7ZgY0Td5wsiZ7ccq9T?W(%5&2>eg zrGtnOs82H@ZS~kpTSg!SkO*khYZi4y$jzvqMD|gSfLR06#b$ClcBXIQ%fJ^+56}_X zBja%nl55t{X>K3o!0)Md<2pP2#Hl?vDZ~m;wee+Wb&_l%^+(;}uEus{RX2S1_r-T_ zxGr;D#(MZQ*{Y(V7+zYwzHVPBo0F7Qj7+%(MfLexKL5_A)5FUac?W2x^4%V zR0mH^u9lBALeMhdb!ovHpiBQ;%d~Ng91$!xyBeIwge-S*V&SeP6+#s^qF`I*0<-l? z%Yh9pe7v7y=eZ6sc|m}Vtgd&t&y~DQ{&Srqsinf58XLD2E*5GwyAgqZAKLgkR>>lD)DyLJ?9AvWcqjD%$+~r>^|c=YIs|3;SWSZ?N@H4ztd=RbN2x zqu}5?kU@qpk{Hzn6Vc_2CaQp4c{gtPYG(-6QGbXa1!(7aagvY+6ag_B9A?7)%$CkU z0#SCA#r~wk9pqYph^w9Impxq6kq1s&K);8Ntxs4E^Ki{44$kfG!3{C{a_3dH^{>6H z|ErFN9|y1hv~xw3Lyg+`{WKBB6tSVx2jEKSl}Yh!K$z+EDJ-zLs_7tVa~Q2@3Rp{B zLk-6%7HQBO)XXGoD}diMGg*v3z=Rd!UERMAIbC(lv=a}UExM6*A&MPwmBUw7RC5Ho zYztjjXT=mDe;_KWZ(T5BEbU6XD;ypa{aP{e)gu3;d70!@-R#SrVowgc>eceAwW2N2 zj+xF~Oz_#Pg`*lp*K55!KMS{~C-cb#Na2=CUf$h^<5b^}!>&6VW+t}7{kY0> z&7Zn0Mq8+nQev^-4n)eD9pfMol=^0vRHMb}q4%?*sX0U_P;+@uw4z%ZcZ6+$2la_89tVgewe3eIO{bxA z#*WG8`hI`u(*PSGR;${HvBzFXr%hGF`Q7{0>k9xQyHBp{aO?U?2A+*(K{GwFpMH*| zhKWuCT0OKd=5a(y5LlIyx4Ctc9OFWPIJs-eNh3o{k%p`r<3R$;gWz}F&gZxPSbfn+ z{;2qjy6{67dh#b#n2a*$kx8DIo!jFaGMkULwDlq2q28pVtN9psrav6rX}sC!O;;HA z(R7hr1asDlO|DBRZD*B(2J0u4V|Zi1$Ze(+qGJA(Od%R$KFKZzdLpMhk6-C|glL3L zqlDR_+cF^Ny}P`wt1dQobg|tw6nTBc(0^}8=8b>UpQ1vUU-}dha*y7Xy7$Si z(?8f*Er=BreFMGdVDI#Z6+VdS(P7=uJ~~*x@(Z)_3xDOI{!Yt{9<~A#{GU9*P?Iyg zI5vJ7iz^O6bLKk%Uv{0?F6~kt?g&z z8-1xpwZ6qNpPEWBxwVr+C^VkaH~nO}nuoiMUcxx|vt9ro!)+v=>?U{DdX=W> z>!h54pq#zQF^`Xu9Vb>Gmj*pKY-&s3EJ*Vu;P+F6fRXzX1a`CD8u~@wA_d|%I_-k^ zpfebhi{h-9LC%HVCIidgfjtmLwNxKak|Ari)1C3MjTWb3pa9#qTu@>P@eHc#B54G# zTmoFz%|BMJp-?g7yr+tRSqSO@9_oXE-#u+ht(haXr*c0bcQ@t%B1l`?@nYsQ1sO~R zgSOpX)OS0kS0O#+0Jo^x5_WsJrKol|Qs}4dHKt6s!T-O(|99~J@BjG*|NjR6KfwRz zRP!GI|Nq{-`}hAG;{V^f!T-O(|G&Zizrp{%!T~_o|_fs>CwvnRzm_}2pm!_a+!|X*bmSep6N8)TQ>1K zdmU$$P-aivl)yi(-zM}{RIP8n^cJy_|qb&evyxg{4Gi> z$>XQGe$`jZJ*j`cI4{84XQn1ao#M!eydJ>}jLWr4>!m~$Z~9)1JyI$QDqEIw(p)1M z$e!(Ezjq*&PI;4D1{m3i0OJ+s~14Xs|&{wTx$wgm-Ri z^m-Qdw!oLJ;O046P%?~iwJ`J@o%Jo0?PN_=p7TJ?0FPDTRF7Ajj|FtwxB%qmMz_Y| z)TyRq!Xh+lnmxE!mIe74LD%%qfN<0)FpSH2)pW#@;-_*Z9^AzLnJS9K#($h2g z`R6Y>DH$%Qx$soxyTnfIkxG`1Y!c3>Mz{wr8SjlA!LBLZW=b)*%js6vtA~;)GbagIX{8gy7`%sC@5h%Z3WVF%A$sj=u4K7cK z>LF&?AP9qGe3COim$hIVotbB%>NHWkO_@cJ7@g?6^}5Om)OmtC>l)PM;o&wp5f$_u zA@b{^M70O+Ia*IurKUtD4pR~myGvZ-b67$z9_%`1YXebZ0gLW`1C!DwA|d#ROM`V9i&Pu^Ux#o`G?2vUa87ogHtmWZ}jx?TQJ2V zLYl5_y_~jo(cEUmqWdL#HN8l~rloAgt{*S+X*O5)zFg)UgcL;h8sdtCD>1k_=O3?F$EwvFHF_uI@;n(r02_MF4uQE)HS`c2 zXN7z7%#7y$tnwA8zJa+QAG3Ujbm=H(DIRqJ`ueRcR1aVZKB@}QL!j^Z>1i>7I4|%X z8QCGCx*~%}LP&~uQ9|~R75h-)a-0-QuaGP7O`%!=hH{~^(PdRJ!did_ub|Q}XOnw* z$C-nR`J^o8bZu92pccLHjy_xYu9!%uI%uC>doQ5@h#cg(1ftH>L9!gci|`J@-2l%i z(BO7A6u>NP6LIV+feL^4IpLajf4b|5R(*5|a+-IK>j|WHX_hf~3Q!|!3l`M?!c^Yo zb!Eeb)hXgsagn{r2djBd0p)yj%6dHPbhQNEv0eA0bY9LU`RTG7M^vCfDFoVdV}6YM zzQTJqDB!vgOaeQ=(BC^7Va`(PLT2|~L_dMyrmzddB!3=WFVrQbI0II}cT0W4qQ)?F zaO8l)MkPZRb6VS!JF7s$sk*`eyQMer^G8p<5{>iOxJ#e*+9X_}h+8TUOQ}@9MkCNW zO}cS>cri#ta5%+c`Kjn6lDx_pmZr^K4Q@mrGf;zD#!4X;16Z4CDh*Y?)$`PYeQ#>LyU8USQVW}-JdgmMqLT@Jhks+&{u-kj%g;5VRIS;&q7k*5*>R*wA-vbs7H3{cXf$UJ6F*DP z9tObQ^2-=>PFNDF!vG3%$jcRU`orTIi)=^JR1Y>-TaTV~;W%D+%rNXylAiy_d`q?# zo$uSpaXvbqu{?*ivJPB+RmlweU%Lh3L+edVESwCRc$@h+aY{FUe8*Rsx z%8b5m;_iEwL0y}!#Kt>jmHnmCpAA5NVOktKQn#0VI$v$K&TNOy;@-zx#CQ~=ccJ^+ z)8buWU6Ds#fitJTc~+q-p!2})?aFzs__O;lde^wW!AIuFNv`g%U@d%GzOkjycgufx zQZ5HUXY|8ZhxEI!Q@%5{r*tyU$I}70j@p?4ms0nUx+toeKuc&J%&OB1XIiLSQy;@G z*zp@x{)=orfcyVR2GH*1qSTktugsoE;iHOP`Z@-9HP6T0I}obcPww1#a{)g!Lp+iX z2je@cT7%NTU7iUqcAsbMuU9~ufcCX^Ia<`e?QZ}S$vrwmLLvpD`c#5N#ym^J?>uvQ zAFsS@#=qGO-Q=OTDDmn%5 zx%9)t-Px6ro33r4WZOMfOlTSQckl`-UifZ*`Dg_LywE`X z+M^I0m@+tyxV`gyGH>eXq*NDO_=sj+<2t*YK3Np1ugP;x@$pJl_u8$mItMv@U4<*m z*IU!AJ)J-VUBZzxm_=1uKcR#FOgmgJCRqVMM8q(8^`xklZfDh158!g#hu#n-sua3> zQDx5baV(&RC% z1u9*JLv$(|a)0L;#?^#`qucdnp1yazV^OW5pPb}Cuh3Mg+5&zE$!Jc=LMRMg=tZaA z>P=UkMLTikcL1)M8@3uYhH&OP2@>BvV;cd&P~W8tNp^dN=;rDKX1c(Rpnqu*Qprd= z`D#`m9C#r54>NSABip9*N%twc$B@GhlM13*5t%6mUpb&GMVz@7I^}c<*FrM1F3%Sj zkX34*qf-KRWmIXahAo0*loTT^R0|Q2E zM)4k$txt_=v?#EyCO)d6Io0}|7Vl7m1>%7EGzWOJS-Cm<&lBLZHAcUiI^AHOUr{G;HiU52Y$i1W!ScFgrWS z$#uczjKF!~$)ijn=&UbgDQahm_;(QJfUvgU?laG@QXCXa8%(^0rc=|G%5{?6bLYXE zvI}ruoujAm%+|~zgE{ZS9;DC$*)aFj=fJ#Ryv}?$464-$(@T4HY|F4qVP*~+O7k$g zG}MF>K6slg2J^BisQxyD&2*&-7MWsvi7oAt3IGZ#K8Q$-*jD9CH3k0zJHijuL@yJ# zS0ZD!H@VuWvXxm-^h|9|1BuSFc{T&h9U`x{+_qGtBDD5dUapAl;XHxmC%%-~^ZXPe zPYSvAJYtf7K)h89JLzIr$#D4PX$j>|l98Is3u3+?I3SoEknM1oNxFnahQpKLu$Ly! zfygfq;=PV3;`fwH0Uz*=TU0VrTFx>j9X-4kkIV9@q7c0Yt6`(4pw*Ln!B@vpg{HSw zb8G@;6c>VX3WlwL^07WkOI&f^@pm1a;{c?3I(4}p?&gMOr1}KnJ!#Jv*<9Bu=c>xV zzSO*Xh37im-Ddd(AmTwNR)gN`Hjq&?G+`-n2=v|fgNxh*3lQL@7azs3OPU9nz_>)) zA$3pi_#_-xk24hIwti*my86y&2xS40LnbYr;47MtA>`v5VU_5YDPIK5bXsay{;*%uP1-??nr5pKebLZ&bIW{fdBA4o8?8hl4+Pg3` z$6{PI&uN1=QPPT4!{dwWl2`G91Ee7=FZKP12YP7+v@~c7q95zxC!t`2|1$8u_-4}X zkgV@TTN+)*wGN>RKdeVF;G4eyFfcWFprYiUN7*F!A08ZNk2>&(G8;m}2KckdBN;P2 zz`?V&o6lHvVUHx+UplyAoz-d$K%ZDi7feD4rdzBx41U__qZZVu6XeywxEy&U;Bm+G zR5f4LE1^YYI_D$iR#1<+`dxEUok$I;KTsZ5KG2kliPuHdmbg?D9*ZOpwd)^K&{d7| zZU;S2BJ+&`z5PCTj~W|q_V8J!7i2b|i~{mYd=`8ut_0F+;|OHYYvxDeefBJXz8tR>cxdHMp1Hj;!{c*>(2^Z z0DUUQhhid|3}SpQ-qox?`=$-(h2#qMbG4Kp<2qqFY;!WI&!{;|HzvDF5A_l@Tak!Y z=vwH9HRH39_`wLZ$T-+=6t+rd9NIwMZWJ7pT18_1MYMm@(0nsmI707>O$Qn_Ajj+5 z|62|_tkioFY?%1vFkm6EkLtLIyoY%e$yfGh@c`Llca0iya6hW+5GW3J9Y_U|YfjWU zLjEDG20zVEw#=3!P8E0!XD&A0r6dnnPvzT<7f zcv$`rLb7$dS1i#wk2j@M-cer7ZX&^G!~-04vQ*dkZOsOh9yfL%^6Aya;nmx27_v}T z{?coJ{V>iL&B!18l|}pD2N1!!)G>8IeSKpji$SM2@}8?N#!u7@Vl~Oddo{K63?@~4 z4Eke^Ce??jdDb;O2ubPYPiiVX(^<@n(695KlQ?B)NNIy?h`U}q9qWAS>)RCHMUCp zMXrPYg)PnK6Ts26T)asp8E84=A|TdAe%aUX05dsp8n|PpNpvN|0XvB3{lQxi9ipa# z;i(Rt_TARC3zHscK@bb2fK?JqENvy)0dmaQMi>BU4fNf0PD%&GPFFKS6ve;Pq;kdp z7yph2?)L@la8?}@&C?LQKz$c&lu}HvWe!M?ygV%Vy3w`ScuW$U*okuL!LU9a!}Xk? zyjV}-8_s3gNO(UhXIs_`ZVca;_l`AySl@b;=x6c+L-Il^7}(4yC|lkQwVv4q@!_4H zw+bRlWM+t|Gx+cVN`kEkEyrt_45S9##JVXx!msSCn!K5{^J=;db3D$cq*TG(jpLXh zCK*sjjcM(aV-RgM4E!RR?NnNB|8-WSeF}lO_lhD4yy)4K1)>zXwz_^fy;WCBBe2Lq$`%YD^YVl6_XF% z#%OA0rxj}sz?%IomooNU>q8ZXQcQU@QVptGaYe|2xlR6RPI_;tJaw8-g&9>2Oe*+# zb?a1aV%8%uz5|pIY?`EcZMEvqe}g9MzvZOwqdwk2z%eq1o;GupKUbo9usPH|%6E4B zqk|HEUg|Sq0+}@d6|kt3II*=QVq1tk;Z%vOx^M_IfshSs9>hYOVwSOVGVbGQtPpB_eXQ2+DW@dnm<6rje8wpX_oS z?DgS}UB1%wG7t2%)CQ@H)e0#h`kC--JvjL?+L%yFxb@XG=wIl~ko-6C_CkF?wAzSF z9v%`@rCMh)HF8z@_sy6Om^FkoAxlkUGf!FcwUd>gpe%Gnb+zrYgYhp`AZ71~5Tbwr+_4XQipNivs z%>YQJH@|E#9d9mr4Jd#0gNMIbu{mavlGN>y2+3C=;Wv@9O!kWp9MIzRzWtoX^1XN&ng$(i6P?7k!dIvA)pXnK zI3wwJD-uY6ls$kWE87ekdb31m#5CV z?q*!Ct-y&dYQcagzYOq#*!=esy?ipJDcqcw!8fuZ9QVJQym}qJ8~|H6wi#mTFilT6 zhjFWvz>Y^E=V;I>jd`{Zn$EF~9Uz5B{H%HlLlrE0ve1fg;93bvu->%3f4ku-1CH%LOsZ*^W`O(ft@e> zUE0h=^dD^k5}KcgL}>O|xW(dCM>t}f$AyHitpmWLYbvil$Hg?_$YPViR1R*p2<*cY zeWA$~^^L9tug;*Pcp&a#!w7Ls`jH*S0(ccmozrseYj~V<-G?iVJ*q^Sdgt2e=X@6i zwR)jmP+!R00$K$LsJ?{X3&_VFs|B|Lk~-n1%J=PVmtHGjA`JaLJa8Zqpn{W19yY;b zvP;a^%w>GT#%G__++8$(TY>RFE%-pY?_ul&UTOtb+SR}P?cj&+pC3IQJb(7&2LT)D zItJRxfiJ-c$SlfzkydYt`QW1Rjr3r5IwK>kLm?Fg4x@e^l3e8TN}VJKrmwg(L`&cX zLz{|r=SKu7u`%f=sLWXahV}8Kv8m4>Sk+1sgj}Tqi(Pvi(2Kp>cy{7J)KF|F(4A7t z20Xz#wF0i(&4z>qG+*E})wOhF!eqw%P@v93{Ygy+*PcXedd4nYPVd_EDZ1+f zixnIe9niDHAyI58V(ZsX>2Pnb@2b5yoqA|>U-xt+-DOpXEOkAuiT3Np#0RdfR<1D;=Pu2zn!)^? ze`LkDrSDUw1C6*I;4)aG+eUi_R9kY|L3Y)0s>FD7|YW5Ui4f4*enOQmz}mE zfY9niIgfviMfs0h>e}OhGn%@06bdte1E+@cZzCheFedWZB5(-g`QC*pzR>PR{u$LG zLqYp>YY>n(U7Uzu+?A4XKhc}w1@WK*-_YQU80W&FB>ywy39T44t)mvXajY>9f1SXV z<%t@m65qXn{-+X~x`|@-i4;=pX0*D7X+%6uH=~kkq>!sqM+?e`9w4Z1K`~n&CGIg& zZ$xwMS23?wh_x7y!(q&L`y7G+^D&?mIY1v4fI%xxjf>Rq^^RlFT@?9ShW@ZZp{Um& zpYm-Eu}%o9R66-MhT`uCcK#H7gDM@KYdrvK4ZX||rT=D~HNV+zrL&RRA%Sd-fpL+o zJ1^(pCfbE$Zo6$_QMmd~DYDgVh3BY!Lv;$xwaGkKDln^Jy3r9d895_e_7NdF5CsS1 zI*;nh80krJ+wlmAM9F`uV+kYKeY6v(GW0vcaDc4JozLvm5Q23H07S{aY4WwAt^8dE zm!-ag9WWez2|E$8YAw$d6Qq!4-36@kLL+bthy1AtLk?(C6ViIC^rxuojjV1b>7@TqSH>=fK8?FvL9fZbo*Ira83$X)04VH) z%mW>|njA#r-QfRwZpP6R}NKmiGa+75?(r-$I6 zHXOc!Kt*+qyl#PtaQF8{95K4!FB@oJQI<>I_R+(p7EHsZ1*k~xxa*?_4M=o#3XuIO zD#D9M>%9n#+K17dqDQ!UZF4g6&^TUg-F@9K>4r;Q3#V;nv4t(D>y}cskL?4!Gd5Atw}; zahEnCu7kQH9~fs?E2aAmQV7bRxQzPwwJ!l#{q8I4xC$S-RWP!Hn~#hi=_W*KpbPPe zevzwEdryG0cjyzoi$8WFkvL((o)*%aM&;#>!h;rTQ{IkLqg=XgFxmW z2u+RFs+Hkb$yr@pNBmm)a#_v~@r$@=@^lL1Y(Wx0N`~ZMY79S1u)8Tsceh~3w*-7y zr%p?x-DvhFkaN_?kzLiIlVi+NMWaGkPcY0;N3E-wV^>|=Box=pO^bQicurIg8>Zwv zrURiotVtcXoF3BkYP!u;-AP&oY3Pl~L>Nn=Y8Z3si@MtCyQKm-s(i*xPY6pi6XP7> z3@SDEq#i^ZuUnWbSlxz4RlRalBCy3(pqturTIwO=1&E!!f>nNfeM+e{M%DU8TdsFh zA9YndxX`TIbgMPcW;IIdjrG#4w43b=?(e&IHrqoUACtvq%=PzGO$=o%YnWuzWgD2b@OF_?*?NCl^fEo``MuDuHJ+Wi=2mzRk=tU`=UlxM7ghPi&&vzA z>Gx&-ZspbPBQ1EdMcruO;}0J;jSRprC>God&7jc=#3<^wc3H0r_*&nwnofC~ zK&Byx-%r+UM%j8I0?&IXGW%H?q!GzpBY z#33EMZ%EN~Psn5VyMWl;FY7>c))s8n zp%ziI>o##bC|2j<_M7pdrCq_(X9bXrYi33mrz!|(r7+}+^PEvjYf=uB7U(v;5}br; zdM8Ak4*@2CAC3rXMceAw?GE8wkV-Y~w$Wf4@HWLdmaFQjk+>e5Foogj{0y9$0Rdfu zYzyx6tW}aX@?AGkkA1fh2Ncx&eti78csP%Wc`kH%sm?vvddzI$O1|x)vs9?qSVO@_ z@R`oX!DN7_F*SyiUx;F{_inFbUXWTOhDOr3Dhb%KDfX-DCoHnCiQ+4}q&PmD5R?#oD*i2KyzM7u0FkoY9o^vxdusgL==jwE?C0k~FHp#k) ziA_$J_0gVb4w#0>LaJf%1SA*gW!1JRDoVy6GOHobTqMtha~AByzJTqcPQ3aI8kkLp zQlz8SSNDZ~l6+Zty|ttVguvZw^m4OSRgDC zB=pE7m6p)|0s#rWi-eZJL%RiBE%CK%1u#qqfHQ9!cyDdi!DOtivm>}h8p%+~CW5Re zNg;XQOtUwTkO@w%6+6ia@IjESS93^go`EN%6TddgS%-nX3k2;+@+p?lev+Znk3WJX zb|LeUqV?62K)`e=dzq_!3aUl*!J~5NS{R08uPE`vJ}rME8?b3)C;2wQSmg-s{*NB=zStY(ez6-`aE&VyQLT&>zKh?L)a9ztL^jw7&mV z`_NzGwj0rdWn8-tW1%ADS*-_YJe}=MB!TZ)T|Q8g2G57)Ly!pw6xJ3%N_>5$QI={v z{6Y-=$e1w2E@XXK?-u)w)|cJFLbnoC3hycw?mVhFdvs~xF=YCt9WX%d3^9|;(1FW! zbQF+$Pc5|C`HBOK5yaHEtuy|)QCyI>;snT*=3D4hs5J<~6^&`Ly$;S^r{JJeIBOwK z?YChSm~E$B4e)7N%_qgO+xZcaxZLf%+I}5|BXk^NUL;PI&X9PVYy;wqEpTPXnL4es zgERwE(SSBnSBH*AmAxbw3vdW$2pa=EQ@;gFf`B%j;S@KKfbZ9TZA}WrOl;DDl?}>r zlB#mD)F?$!%A|JmfZxJM9OrG$U4R@w1hWPFmmbHfwHXCu{BaWzs(CHv%kfOaa7H807a2MkQUnACohC{m^d(58vI1l9uq@8sq%V%KKn=4o2F@P z)xBCj*BY#)2G`MWR5+v{{_DM6La*Qj>UQn(KUQi|z7V~8uVF$WiJH1_oM*L}D#97# z#j307y+9nA7ii-Ma+t<1qAMIm_*Der6MAlut3;i#D}q8#HgiAsIvKch=5@$mYK+yw zv6J(-OC25N0~LZ4fq_d&impZK*erFnHDRz1qv<%I2;@<$F|q-;{aJ_&FGg465uoc$ zFpcN(gIeg+cUIcwNj44%CplW;Nx`|_`%Gkqx zGw8FX-9~>;dD4Jjz9ED6q z-Cb!;XzTnGMV+! zwC#it8=@)if@LJ&JttWd<#av`nTrhjUsa?Khl7HwA$_AnKq;LJ3jdSZDYBWqr0PBP z;Tds0RJ^Ktg0=h*)`q9TTqnBU_f`f)-(&}1Z_PX$Az@M|@c}yu@Muc#{hQ^BVr0s= z@xQ|>8bz?-oi!P5j)iCny5RAqV*Hw6M%?6Vg8;V#_$IK8(N|+a$e(r_gS0*_PET`* z$CIwLwRr7##ul5SP0f8kv1>*0*hbwCH$|<{#p{(dxyWvDg&p5%$xFLl+p>q+5)D_7 zn?B04Y7qA0OI?ri6V&@ke)`%1#<=X1$--cy=oIrI>oi}k8$pXvU|oHx5&{BFC+bYy z^>S6mM}=ja;e=T(9af4n70YuO<)kLXqr*v^9ESBQb%fFmDleP_o)_7r$Rf!zd^IyH zjK;DXQ1OSX#x>p5 z#d>3xdueusf_XV-ZLDKf(dzKLVnw&J&UWW&_s}MwAD4acPH)~(x7n2yXNWQ)^A;lz ze_;o(q(cdjt7T0h4QE5hM(D!H4WTEyZfTMObnrM=|Jv%Ac&6!|3dlN*NKVK9tE!&M&|La=w6CR*)^>G z5{45VELRq`sXpwwrZJZeBGDXcc>Oo|@)u|HXZbS91(l%)YZ+?vs7`w&S|Nt z&K8~Fut&<)Gnw57`YrsRheP$LVwlYHi2&?NRhPP10}IaF6uOqdA(TM5nh`&avtR;I z;#82_3dXSP86@^v4 zWNUr5L)W?v#pLv2(7pB=5~%6?^zK$N63QN9s*TW?i^nTom~l8~A*|*#Q`(u(UH?Yu ztVSBT0+nG+zfl^a#r>5iOju63b}a*)2vQb(>swMd9uMvwYVw!msGLY46D2RMM?LJPSg-XL5_R7o^2S}mhvR&$`b(Na{~c_#*#jYfS>i4d7V z6rr<;lFuvQ7(mXqIszciBnLtwUTU~6N2_cp+R*N$_#5Q<%t_Q8e@pQA3I6aOY=kE^wYQRDBC%icXguIivdrCTMm z$5mRB&Yp+sr#C=uIbC}lSCu%-Ulrl1dJ(w)#xh8rL9PW`+fW_iv0Sud%u%G7~qbDTg?Ib1(ow{415-Sa4Nbc-}H^$D#aM9!Qd z0xkF6q5my*`G?uRO}GP_Vh>y;%)SwW;D&RoTfUmiU{``*sD)wJ0E!_3$58J&)`ng- zfoAwsxtZDcgjQ|$yZ!O^#pc=#=;M!WE2&nynZWN-4QB#fR0WfYfAXZ*Sd6m4fUX2O zu2vh=IMS61O-zb5aT#5uN?_12@}~_w#K|5~_@5X5x5!7i66Vz{d9^I3>fSd(KOSw< zV41iaaj+q4(@ozs<{fxZt4h>G$>jO~__>nV?$G23!!K2RY(~KJ;ih)Hik)045nEry z;2sGM(K)0wAL}cza61IeziJ9I*e*}E{*vS~^(h3?nJe~WVs2gllf_?4SP*ZY6dUM)gl29DsEd_5J7;~Hw|vcbk(oL{0Fv%h&N@W=m=3+2VZ4`Y5P7+fUQ?OpA7w6au-zwB#908I?rLqBysy^l3SHYN zylwCNWU^&MJKc%c-X|3|feey7CH(t!OfhKH&VsXE8$b2fqIOvnKGqH|(8@d8*2zk9 zP$Unk#>@P-l)ljHs(oU%fN4DL7I4?yQ{0Yd0-{g=4*x_W5lEU5d&5x(t4ca-FY7sT z?;K;{D&a{7Ow0Py>qS_^Rv{x_QVN66vl-|@$$607+ga6`SI9vtqZ%{T>}&+MzAXijWVHH*)x`C)Ad&78dj1?u4l%vv;Co+PMVLTLZj3$AcRd)SI(a@A; z)anp9;7RCIGvk6O!e)jnpfca!XemmZF|($UW+W>F$1-yQCia$(U56r0$_q{`Rl?6i z8p}mVDNI?MLcBthP|zi0OlN>$A0P;AolMHn8^t>2mulW-pUIm7>|V9L>LyVx26F&M ztbpBA-8*;Sx2hM#JD@-^eRVs}s%X*FjMtN}jh;a5mRyPKV+vlX3mk(JPF*+7R>h>w zDJ~wqRP)wUmeR-z3{1wG;Yv-=A}!E%Kob}SV`C`o>^O=9h$PU@f0u`goYpOe-jC{UK zs$Zj0oWeR!vZ@%_gP_Xu0QMR0UU+6I0Xf}}uCeApaXKh4-_taYVzpbE(6FeM3KUuV z<-up$e|_-R&px|<|1WnR+}(B%bHBo{M?N0~b$sQ~M9>q7uW5Dz7|;evKB?)cy?9p` zmwlMLfkR$R%qP~7{8-C4l!+H3$@r*4K?f{Gz|Rf0*_2t(MEZSWbbS#{x6`=&+D`TQ z?Pi6JRtv-3ubuvOQ~!KMS2i*6YcFG_qdV56Ksbnz=FzCeS2-}T(%pfe0PDVbC z^*FeqKw41gd-sV!h?QNqy(2hhRoJ&lkWpQ8JB* zL(e;jS=^|8VZK6WiV-Co&^f`+-kv@b?$9BPW2`~i&Hs}OeC}EnAMRcFe7jN?g zy|}{Ri-PL_OTYIiY$R#}gQu+pYsBLZa4@^2k+YabkZ6Sl+z+OpS(3t!sy0XZx4F7w zK}Of5UYt*~&Ptq}s~Ov*1>hHKV*Rwx(pDpA988U2wd2?|sTEwDuYvoc#W~jfIl1(f z#nL-K`tWBx;%%O#H9WS>=;h%V&$AS#LA-re5wJAan=pB%g;o|m(w}IM45%Y=OXI1O z92@-VVmC&9ys{f){St-p@rBMsD^B*b|=ZoSkNL>28NswX^i{N&>Fkth% zuF_#ndZv=95s%NLWK7SC(x|tLzD)^IEW_w*xvJAcy{6z@$>Od}Z=Lacs&ymt@;GowO*haimN%lOIjRse!0%DhmM;os)mZf?%sRw*9Y5wzIXq#`*;8HFnWD~ z6k=`eEZ_wJPABj^fD-2U*txU(mJSNVoGB%&Rd3C+#Zqn0ZotL+iOZVsEzdb(HamQ}QJ{B@4BmNND&F#hP-Hf!lg*o$roP{}#{;My ziX}J;YAKvMAtltS@=nFSVzT2&xeQpR6~=!LoYNoJ^>WTlB)HMJM(U&;*SKZRuXm?T z5hw1PbwJbn`h>k{emz2-t>>!3ox9SF^4$h|w?@LN-@&>-t6j`m7OO6&(+*BaadCeC zP3_A>v3>nJ6Nw^&yXxM?sbwRD(ijs`#6n*MK=d>%L`kBJIwps+2BCN zu;5c`6c9}%vmBh+lniOe{aJ|)SJ_ftvH*h7rY~7#pR$H%A%N8W7XXPa zK9c@%PJe2yV3W%Po!B_E9S}i_93AR(9%gpJ((ZAZpkCOzo0M2t$gTX_o2*ICC9MIo z675gc$V}Fo&T#3Q?4DNn%%wECgXW(

      Hd?KmtaefmHWMSQy=3Iq_AL*e|x&=g%ASo8kxeibD521lVPH?|M;mkZ1v$LK#F0U zr);KiSNx3_$m8ogVE(Q48&jUyZmq<9C(Bu|uUL^55Y+#^v*|tdR8W+6mhpiS7iALk zE8c0bO>;xZSE6Kf0sY-l@wFTbSt4Gw{tPI9Z1p}H)uP7`!+ z_Ea^MlSt&kL@n6>SwExcW&)T?2+)J~xYWfq*xik-=us!RC>@8L@4lD8n0?$n)LPS^ zw9g5AlqQeBn-L;v5i|4`Ui=y*+y8btynxH+Sl+DJSgqJw?a8*mvbFFdjHVwmQ!6gu zH7iP8C4~lukAv02msktg2ru~T<=5ZqCs$n&RRt@Jk2nv!x2t%C zK7hhK)^=MTiKUMjoOd6sMsM;ZNOvatYePu94#wu%k~(M+ z9W84<|b$m9rROy_GCz%Nf z$qUT*h4Im9JxUW$n9uu+yh3_JMgusY^{tkyQ zvBM#R9xM9MN^tcipRK06qtq96-s~C-U58dPeZa8^&Bpjoxv|pEXZ!r^(D_)++nU&B z0~m>E0VSqpzDVUt+KiRBa2UX0g57mbH_(zVFt$xP_IN&yD>Q!?3dsDdnMgMTg{23~ z`9E!E_+DV!Ih~Z5-x3iVv{Wf_XP{T|5r?>5y9^f;#NMeWYQ)TPYFBk9x~y({xccR( zvyEUnKXmNf-~@X_Fdx+PQ%b$p-LK&fy=Iep5G}{mU~pY+oi%ZDP4Q#X>>cFaAqCUu zvP;B)l}1?I_ak+qjXOtU_o;wX8G)FzjVX2|XCn3EPD;2?20n#{gmy z;O0V9H&jdhoJ?nkcgrzrlJCrn-<$y=0yGksTZBngTdri}+&5}R5P_-9j_qs23n*cS zJkLuQ0(5DynnmvdR}Nj%Kt|YD6LZn)ZM!8`KqH|#$rhOFs6Hcpg1<^JW>e1sc%p=n zRuCRYm2i3`Af0id4MF61e8Ks`$vqTp37)WIWC!Gqt2u$@(lAvSVx_8+tcd5RNwu7t zB+S%d!-hw~yML~7$}$6oomxHz1DPRgOf-TM<~Ng~o+d5DktrtAVf#Qa*;xj--tyJL z%S@;3zS+BdJFiE1rX(Y^gE_904&d{c(51C-o;JIoHb zD6%&AtM?vX=VT7 zG3w_f^*gzpJYOy6E5)2X|4yA_Ul3PQ?}7b(eyo_;7tJmRg%a){^0r*M$rA~3=zF!2 zHf(giJnQZFcmNv?!i!A}C&eFWK9eZcwF6dcr)I>lQKLjbWQpnGR27s!`=_5x0(5C* zl`Gq;pMf^RS>$!&c~-*KIBU^;by0gv*XGXq_2%;Z`*MJPZ0;f%If zP+96~>-mS@`JeRD^wY0iWbeLJ(qq2(Fl}tppSpjyx9>f)Kg098cke&=><`JiUt$7Q zio_PG`Rg9!@8r+Dd&v~;G`n{nZr^|S@GpP8|L1i3cTd$ff7<@h>h<~M*4^|$dhb(y z@M->T`Kdk!K4qYW`Q??j#>0mXYxe)cd)xQ#di(#+48}+ zS&!)SP(DTR3)2_R!L6SS%DR93gl#_PH?!nvB0c-L#)3D4L4d2pOf^byTS|DV zN^BD{v}jCKi*gD&`vNt0czr{a{t2X^9+^c}!k_9c+d7I$_zLM))plTR z{o(Pq&%ZlP)E7s4&yIgco`0R}J^LZ~+u^gv{p8@kUK|~~eEIQ5&ySMBr!Sry9;kH%2YMs75 zJbtEXO})RlUy+pM8C#nmKrS@a#BM&8la~ z!Qa&{$;)r|o;-oJKK^L$J2ikK)zVS2|NO-dM~B~hdz^gx{K?}3_3+Vw>f7FAY%)N@tM(Z?Ubchte;`)?268ML^k{@Xu3eEtkZvj6>VAxRGn3$J9@4ffH_q~o?|Vl;Ijj&2If0)mPLI6zkm1gK=kJE z!QK;9+e;`dzfPH-eEbor$n~+q07bafg0jK5rM{cx@8+2O>;J6ErAMgj?|PtU?-XB;Un0h)6pWgquCoR>JyuXcacv}{$UZ_^*)P0X=={vx9$0%*;b(u8^7#( z27*BpP}R@~WHP`AKPgDNLWl^Lgq@MCK%RhFO`szhij89x0Od<3He(AcmjGGAju~`Sr4x>Sok@Ui&KdnR^xtpo3iO_t>(#Bpf0=AFpgiLi5GT=>m`kz((0_zMu0^= zpJb!F+cCw#0SNFEJPt2p9_zo%--{7x)I$%G2=2VZbQ4JQ)y6KRs!zH?se1*=^ zP?5%XC51Z|#}pY@9edHu%Gs8zX+71t84cE_=hfIu+`P5T8o}e8F}Z-@g1_^O%%(gk zVhJfQ)rp@7^L>YVXg88uhrxyy`xq(%Aa|OqG|_RM=xukFvqk%Dk-1>5!{K0n%JjiN zqT03~d^3=~=}(Z()ed&oe(!xFvJvPXEv>j>M;)m&@imqPO^)&j*kACOR==KQ3%rk^ zQ$_)eZB9GRWC0HfxU**No07Zm4_2#_Pv=Xze_3>JssiFzW~KQYgSTnah)Y-KB*?xi zF|Z_V5d-x(jzpx-?t@ntI2-%}yylFPG7tms)+sp8I8GV$UA@+ct&zpMF3qf+Vv`cv z3TuNcKRsgLfgsXz^jcWO`>R% zgPWG0FxORcyxWFE=1GZ;0inL0}t)G6r zWt+Fsp=qY)UJb9jta#V^_gGXM%5QK3^R8)$$MA}gvZI5zTETwZRvX(Mm4p1<%$=GE9}BL{}kjn{C5 z#kJUnTE^ch6XveASvnlYFSvBnGh8{!MuYQMvvj;@X3bP1!8+O{mKN{p*`Xm)`EO^9 zDWTDCZ*#&bY`QaDpM;IquE64go)U~|ofoZi+8Zoi^Bgu_Upt3Qos<_;#}ngv=s1Te zf2)Lyo%=0PGA_hMq}=E(v++{W#F*QFks(#c|fAVT_kVjMgoPvo!On_r)>zBhcz7x z)K6ft9t?hl=Qb8M?}aXQ*3b1CDYcx1NIloQ=DPkJga6{BV8$V0l4Y|tm0Gj~rn>I` zfA-#OIc_6c6zpe5_#e>JXjhe7f=ODI-3*scQxt7+S|W8w%03QRQ3Y0kBw9tF8bFDv zR%^$e%fsB~b#CWO#LVM-!Je7(az16w2kc*%b;(TR4JZ^vNw%Agy<03Gk-2i^%9ShE z6)Rt3o(hf^P??kpm1EUg9y%PW+(&wOW)bn2FE~!}syTo}T7ZS1KkAWlP_*(~V#xYB zBp%--k#A+7KCoJ7Uqd79*D~`T`Yh_J9vn;PNvX0GZl1m^6d~$B>grOTgY|WhB^6@h zE-5XCCl$~2JJ`KTrWgfSDdvzDGSlVNlE9$RH&On*+kUG$9nZgj#sJxyN2s}6p}X@J=g>{PYJ?6@6~ovFiV-aDLJZMx`aDSSct zCVtzwU5H#Y*dOL^WN^O?HpJJW0lwF2LtmggA$qwTsLUx0Vy|hl97}H+2x7XZWKMv^*h5bX9Q{t5WbUkNB=qqV`x#OqHOOSg4^x%x8*nn?Z@i zQc_ga0aql)qLCg8iy(`d+A3>sMeisJ9Z?v0d2?;W)a=8~kw1-7mD(Q8_)RNZLZiGt!uD5o(6R_MQhv-*fe0wkS4_$aVKrGC2q}9 z;SkLu3<4()yT^Y-G{(&kq;bruoD%^IXDiinyJqB2TRE3VAYAE3FT`?95|C8~k~pr^ zqrD5;ZOvJ!!cDd0sMH_T$j=6>lsmaP23HU{Xhcu=s=@ZCk;e8!NzK~gJyp2r5JOD^ zJph$5-Qv?C&S87Nt1>C!6z!>sKRR=yk*>}haf#mxcf^hz_UO4A*z(Dnw`QTH<|}AB z=2!*ix-!Z;Z4!^1a*K+Z}^3~H9J3y3xuPLL)q@#CJkL zFFw$MgDG-Y`gve(ERZ)Rtk4&MieXbgnr>fMDIkq-iz8HpuOt+0V`Q}e`1+u+y0}>* zHonk<GyX;Q~}aqffEM4@P%83D#s-^cn}# zzS($dMLzcf%QE51j8o5`wl(Xv8zl=}C~ibSy*XZ`DR87yw9R;6@gTj@e9jm1@j2C_ zlvS;i$i8hkN)*@3z~tgVP9 z6%~!z_P=1)!9_%|eH0%VQKAugJ_(o{mgCL`)+z>38m`HCNP|=PXvnA+0vjjj1AG~) zqxKG4B=jQIf6jkqIh@-DqHmFSm3-- z0(j0rM1xj2HzHA|MU+_1qjAYPEMK~f>!b~(AIO)4AqbEZJ?O=u)tp;X1a|1t1>~#< zX!L#9RihEk8P7V7IjHu8a%PTWs%uBI($Vw)h7&|>g0FWV2wrSsEftzL%S!p778hm} z4_og33SbFsyRi3a40l);a>2PNb-SF{%yC;}MsQZy zv(=1sOe)9(Q08KCmjkh|4w_Uh!*@lI^t%4#VCGyvq%T+-+&IY`-xW2cCO!&AqXKSH zv(X6TPIO_M;Yd^*qMZxy^`mahi#pUo0i2x*w6#@8i!)xnEAoUwqReOsovboarn$%_ zbFLKX#qJf5+VZ%*!bw0fn06HWs+_ZNz75I$yovp+>$OH~yuF5r@+8b{> z4O9%>>-4EXxts}d;MhCTYErBsC)QA$zhd(j)x4iX;%+w@$qDLf2upt%Ew~PH_>st1 zbb}yR-nQEe+=$)**eJ`LMB|s2Q9dCx?+mU3C&@G^7b_E^n-9mLNPNGP8pleLa}uj# zqdnEt&{|zq->#LayshFwOXSlbNXgDDZm6PC7Ty0v9!M!8V2RQcql=6yY+L({6rC-g zN39C$ElaYxj0EMBX-c(J7)_H1%ipG_(`4MPmS)wV{EXZWiHkyK@`zE11+mC67p0v{ zS`+D=z6F|D5w}^Q=Qu6nbBxw99%u|6wj3t4?@$AB+P1!B$8G79*yxOXjtAlp3bgX{cB16 zuZ=Ihy8p$xUH@zIt9v)~zi#S(-PHfOssD9T|LeC^|4Y>RO3FAd;c{1~CrE7S2k9=2 z*TN%8{mx_cr#!^wWil;Pjj?b{)#$m&_jxv-sS?7ng;jn`^HK8G8G9O`PNLKb1wDe3 z(XQF`ki&F+ubu!?yNIUND`Kd(0z74dyWhf2!m`atdYa+hz${0UJ#qFq{8<9nxCzl~ zs?aMxs1D@KPIWYiOIF6p9>)?1))(JY-62~}(r>8~yxF251vvr(`pqOR#(6?Ygm1+o z`C*Zq0fy#+rSj!6E@Z1|2J8k{wfMc0oo(U2f%^VifZ`k$gWEK{IUq&A@*eiHURaMF)OrQq%<}j&dHt<{8&r6aUkoGBjO)2&>TgT87Xg5`8}TmGb1Mll)2o8> zEB1;_CX~`~#_gDePvYQYnxV%p4kE0iRbFUK*MljpVRhqtYpY6+ZMdsJBVOW@7}}tF z*K4Pc{2{rOark7JmB7)p2!CncE>zwdIi>cxO-5joYKdDQXCQY+JlM!UiKE3~5Qk?W z432^Z>MG%sdMO>1IG(+!ZxNIa(oCGHZD;$WAB+liR@;g-JQMykv??GeSiXDl7;;M12zV6}+g`P$V1}&e-JsT>JKg zw>_=SfHs5HwikRBYy`I?DSj5D@#Qqp_0_(zTV4nnhh4XjNnaJ>Gk*8SS&qB8TxgWR zU=1bEI+lf7s52i6kEC;4ZyJNiq16~7jf5IQkd!$Fb{b74Lqww>Y5a50>5w`W-H{ir z78ZHrWP*->*jUQAGS7(EI#u&M7CNads_QAr2@{8KVG>EgBYc<|8HqHCQ}#Q(XFE*H z%iHij9J`JKN#-EuQmL$oq!0zQI&zv`s>+oBGGQa&SxKL9mvHW(xkMTuj0qf4kv1E6 z?LAOrauEwL$qO->HnDooNpdCv#;T>TF&r9esQIKo16(%ARZK2(S8fAE;ObFDkML;I zaa2YC(MQ?n-`b$Gtj0vUoyR(mv=!p`2D4BMSMzBOm@pyne3tC)F?DT+At3SH4V>c@HCGvNT!@F zf)X#DDVDqt2cq{75R7G&LqNit7OptlRoR`j>CaG23f zJedH-6qqodA>OD|?N_56-PN(Uo622BV^kNbp?2}!yO>R73O3ojN19Gj)R`%F&O3L| z=fMgqw9T*ljm&K@#B88=GjKbx`HSZCzsw})rO633>Xfw|1cDX&05 zqAPpo2c2fp=-3-b?zVAu*1(c0hm;Cbfhq9-20R<8=$Xir5o_42uUGk}j-QA!C`tm8 zON)!@F8ly%K$O3bqGE=sSfZZ-ZUs-Gm$4p2Q=TX3OJr@644qPhw- zg|lpylrbF6Q6YU?G(mww4VCARS6u!fn$D%LEMnPZHMNRLIz4twGMCx2Lj?0?=#1lw zF*!tX!*x-KLeESx|1ru_GHRUz&spf3O4|ZR1x){>Si`7CsZbRV6hIL=#ec}U0X#?&b|A)>r6agVA5yZk{4qO&42gG(=S zlu9QdgC;9f@4mksb5 zuWf)ANjw!*=xd?oHVCi|mohGd^VtNh5<>(FOXB6o)j34Kj|5N8ck`kmQKnfh>dQo%pjG(P-n-M_PHBuCXA&MUH)|R23 z!l+g{yC@1U0v=H)6F5VHhTcgW&)B>=5h`g^SeQQJDsN7SlM+QaC~+{FlW~<&$_bBA ze##K6MGMCQ@YeG2ZNm>4Q9Ak~3r z;v1eP6A*E06@o`1CmksRxox1fM5PJDu2N~Qfdh;~&1EOTs9A6YZ4wYhQpN+ygOhG3 zYXr9e!yaGe^8yZ0R~wq=tw~paIT|^^2nrKERpDF9b2G$#3q3p6ShvM&2NU-Dh;s5a z;fe@_R$9AKWy13^pO5uO7JWhOq^M-YDJTOv^v3Z4Rzh!>e}akwDLh2ua!&L^tyL7W zBp-{D*LurhEX+ZU-E2pb=W)DA08iW z?;m}4aQNg1Sm&E+1TC6y%Z(=N>kTP>aLWuQ)EeZSZ~plEfAxI*?jK&f+4!ozx%u{6 zYCVakAYe}JQmO>2{Xf(<$NY9eFY(r%%+u}TRX8Ce|OJsCY(+2>Cz=F3$lTHaSPal}GRHlw6AF zu#p+uWCBJ#DoNd)N%^ zr+QS}MW;7$Nr)F6IQ9SVhwu-7xE+kKN=`zXpYr|~IOI$&*sm^l$20P!8<4L}D1Y1P zAtg-@nMz5m_QsU>UT&%Gy00vu8)@TZKX`?x<{m=4S{Ax+{#jP|p!7wc`lmLBzjCM@A#) z=*?TL8+sVR-WCM`x*d$xzz}TQXPw5D)B;)_k#A7RVjKC0bX-8CqR+goOI18Wh)0no zCM#71E}VUn$WH#O)MqkMuzSZ(0Jws6^w=}HbYO7-bchE&GiV15&Sf?{$x1;!@Rts` zU0e-Jtdx#pi1%IbZ*K`!mD)iAHIFZ{SMd-pzV-0pc%q>k(j6eQ@&RFwI1hq=V~e2ux%+6$@2vHI2&eSeA9y*ObgX&oDCWn4~?_5dkM0 zUI4{|JmfPBAI2am#jivih!ZNxNR9Pu<*2LL8WpfyfBP`<4=8C;jg$#6+D;C5C&BOHA8x%zeln(6L!*z+_K!FW0qvFPvjj z^t;?dLDpg)zI7Y?7He1V3qbUICUh&p>>LJ37>6ob4Jo`vqd(%AH5%#3u|}hB{+LX@ z9a&3)7eRYXC{b(3rIp+lY-zU_F~p9HOAzv#lRS=Ik}5;9{tH;;29p#eRBxD&YR|~h zlTpSukx-kjXuzHBfV7{-ljLN&xPy{BsS7YN9Iq^4u-Cy#dF0V`mf06RWvl=w&pMM7NG{=8>eRC~@TT27+3EkWfwJ1K~J5qJoo(09?iZag(X;6URk93%cO~SqcUrJ0nIxOadukZhHvsxf&z#m?H@zkIyje z;&j2DhqRu;5@pryYnnin8if7iFB1$;(hs|&^aH^ca#6jjc3uW3DR!1?&NPY8UYoK1 zenpvn@LRL=oRH1ll5VO2z{1%X`Sq0NLi5e44I&QuO)(^#|HVy$(E;c zIDGl3wPM0QOg0p;Gw2+)ZhJxSD|FYA>PY)5Z;IUKw~N2sE?$tUs{DWxUHCu43WNly zN7xdz0(p9$VMcWLUwG(&6xtatJaVIKr$Z%ueZ&}aI$PuE&2_8`;_uupZWq!wS4AO6 z`9#cG(R!$sx97RRNI^vG50PKk-X45;Q9_H7)P+V)x4E4F!*?$-B772)?I@i&S9T@M zIQ}W}`&W`tDvGnoC#{ISHO({R6walFWSlgRJtL0K^z7u%aZQird%b6StD6J zmF8V}nMi(7d;I+cyQ z;Q4|KlKpg{t6p5?7n_llMkJb-*#(@YW76c$Q-m4C&J@!xnS`Ce(~~5fROs;C>ieol zFm{!cY|G=6Q>*fEk8SsC^ul+1oT-|Ist9Q<`s>67!{gQv7c@7EKjI=FUC8T%PFo%i zCT1pNOF%{-Nuz?2sJ_o39*Y&!c={lFXjw|*OP%morjrg$p()WcdQ19-w*9qmo~EX_ z^4FvsY)uUIZx_+x1Z((af79o&EofPaud-}ZC2Oe+Z&IbxWUyta9S&-wKpKm_5-d%F9PRDr>+pA|}1eIC(nzmubU52$cZ`Z`PD$|w4_>mT?2xx~K6_2_` zx|}2vJ;SILe~tB3;-ZY-C%MK6UJVVuqq{)E9U*H2&it1Wz@*4Ds>Pp&osqS!cg01Q z6H*wh<)lW^(`7WOTtvfb0Ee!Cu=#kc-qynAhk*Li=B84K<9&^L9*wwb>tK4nHyo-D zr;`i`mL8%Bubq2Tqx@phM z1%lqt^6Og+$z_A94WZ=#tgMr^OZ;gv1?sL8)dp`YNXjp=TzHW8(E-6p?}^;fVU#i! zD-t7UkyvUMQoceV)}f3B;}DTvxR)r*mszmiMKtC>HBDz`Sm{U9uXv-`|3{`3on5Kk zZL=3!!5%cnYL0ecBwIAyPANkQHZ^dds-m5V`-Mc8zU2WijnTm>i)Kb`MF1Ry0aOiP z=`;_?9YDDk?mmFJc;P}X7Yh3^OjtAF`zc%I*!9^t z^a>mf*@yJBWKB1P>V#fS&E(mYC}O4t|KC&CC#>LFsz%l&i{^x`TOw9O_FG`})JXL`6VtbJFjn|5 zSs^b-0F=10`eS)xAiy-I=_n_uu^%Am<-p%LTgE+=Lwwwoi|d@GT5O9{oy%BP>lAcV z=E^cM#bkG|=SjN>Qo~t!E>-4L<#0&@NKx;6;2fcJ19Ho9W>kolCPR|sDYYST1~~7l z>ekI9ICdhO%VbhvMad?$EHEmDjL4!&)TNY=dSb9pl!P170Nt_e{iMi~OWoO_Gd+(n z{B^e)y)Vy?!Ftaolu3+Pm#etJ1`3fl`bWKZ^b_{b!vlCEOpdPcPH5$n=YBPW%uc1Q zhQjTSZl<`xz(2E#MC2Y<&ge!T4;Jct)NPW|A0Al;dB;%g=PT-;ow>S1qG7T%B9?0G zq=8u#$F`+jGJJ9*sBo4PTiyxz^a{6+*nI5s+*j2IC75ja48J*+RsXL30O{gmhJ%fT*L zg2=Vv+V1Gyy`yhJVf?naT+fbz)?ShyLDvd)HpBJp4d@S=Rsrs9H{!9iseoh2jfXoF zWII&+7{rvvr&}Xf(<*$I`GEMsTG-`0TKz5oMft02j`1wq0!adP<7R@FP28WN1l{sRXmUB zb5v@Pekm34thj?kIqIU2E|`k$-mP57GOa}{U z<5;!Gg=jT~&V8URuZEo?8NW=kODP3pG=85i@2nG#)B$827ou_(hU(C)kYyIvezJYY zGlpyQV3KEDXqJ>FoljA3M0PN#GeX1cB-1nvMi>zV&;zU_2C(KdBWjatVkrYh*gB`< z;A+f6z_}GX5$>ygBW}M&yW-qvJ%57p%b!It|?kNT80`hH-i(o>a|lFEjH*;%NxWF!pf&O-C8cneL)!(A9H z?}fi8g_bU*_`KM!!ed1Cg%&rt8*cD-!VO@oXmXLIl%!*8E5Mj~T#HAB_pGMjCQ3P- zPaT#**u?9ZOzNOzZ$WC4p1R8XFFGQ0Mm0XboOhS<`oMCJ&*j0T3ja7bx?4pR@wBaA zA8(tJJc&=2HKIZr!$~$?)=(=g;x?^_(urCb)#KF7)>|0qanQT#G>)fSlM0ofs7{S? z@)qTg83U#CJ*Bk+tRsiT;{eNui5e90bYKGnuNg7oC@rrlv|Y_v>>3%Y$USPPVRZ$p zc+}3@XFI_BEYtW^JXPD9?x&mrWnL&{G>+Nm=6*`mR5(ki;+>&sBhhuRRkyms%!N!* z23rqLLB$^B2!=Bm4u$R~vphpyVrOTPJ|&VNQ-CPO8i-n4Ng3KoX{pGyjvif?+m6dg ze^4Q}-r;lLjw{=$OuZ-BWDy2Ol-S21^nlahPmlIIHt4e(CmbeLBp@M};T zt4-o)jVRx@+nW^NwMg%_{{DVFvYoa~-ye5Y+#&=ZAN78Dq)F&Vq(p)5xqgjqlhBV< z_QvH{YR*h%hM87Y3h6;ZDlg+alF6Kq@u4*;D#z!RvEr^jh`z3Y)HT&MsIHO?eN@t* zuaPa=c$&C&RNr78pYd9t3?s6ymDkecYv^OhqZ)G65)pg(xX!_Qc2WZjAf;ReRvd^C z#%j6KROt?sbx_a4;LjebL1|iKq%FpDs@fsH6mpd66s+X4lZj&VK1&N{o{T$7qgiU; zQi@PLl7(lzIKg~7d4}1m4BwOL75q+PCkBV+fM7&I{X$XIVqP{Yx)mHzLa@u2WC~Rn zNmY6ib?r#2frp!szUdb8JF1lKx_9?=&os=GD)+sa0iik#`94SW;sU-uzKRRek5rHA zEN0WB>~;b@+Y~y44cxpG80v>#KVN@gt=dH$-3d2v={;6qy=!D3QTtS67_cKe<*4oG z)N%|lqe@glH1y6)2^-HhwqCSy*yDJm0r~3rFyr^tQwdOOQ}{F^$;Z$72tz%xY(%Y& ztQPt5Vc|spS5cR!|HIAh-DX%3J&kX%Rz29@LIoPM<4u~1)pVGwPqFT@H%*Hoy(d{1 zb}L(UwZD5+xx(XGHHwsUz2b`qfChOI#cPTYl`@<*PIUI-M)y(FQw4{vN^95ULTe zMnkMI35cCyE?z}%y_PweolZPUj+AbtzOEs0y(}Z;i;c$P`Ne#SS7>o#mH{e=II+4` zZYCfcbB3d8NvpVTV^$mM>vGos0b*W?nr-?hy9B_-=lBt~G&c!{f;8*FZD{}`HZkL0QP`m=fT6tatv!bmEVwGLjE&;2^P$`Z0iN`V6 za0u4C$X7}p%iif$wpM!B=>to*lVl3ov)c1h_+Zm+Z=3~Ac3S;qO?FwamF!Q&ezG?tH;byE!Zmaf7ORq$A% z(UDUw5kpsL09k;Nr5}G97HIc;OWOu_gD+lSDxNS5-P&%P49g6J;PW0O?|6A~Kp7Tc zd+f5zfPGX27cSuXiAhN6!{CG2JfCHSUDVH)@XJh>Wld|1iiE#efBRMu{X7H(H@oM9`)q)J)?RTR#1~X zX>A>6RS|^_o5`e7#0tVydu!BIZJxLRoPnG;NLX4R1dqV)MgVOzjsXQT1;(hVQ#%sZ z3abkyI71?3IKC`2$k$aR$DusW{F5tIo$I}z-5f(;)>qX z1}4#&@a_cpaiL}lHgd3tqf+LzTo29C!Ca7XnTV_1=(vopNFX4i-Oi$!sz<}9C@cps zxDm`#Mdp37s}#D5xhf^iwQ%h8uVOyF^cQZhZ(8}17fWj}nI}`IYHEuG>A48ginntP zE1|npfsWqTUTh)7qK1e&D+sj81X430!I8}jZ+ZhcrdBe!t6=F8`-4Y#z|r5fmMJYs zs^}2k-TLMYefIWk@Mg2hv7pOvcQEB#Kj3tEV|$198Jq$AZ?8#22#wg07P`zG_ ze@kt3+if}{(We5?o|Fsem?3X!N;AMJ6`QYmVynV;2>7AGzQR4mnmWzEThY;p52Cub zPCj`Rpe9d^Mn9izg%CXh0e?(0hMWK0>SUN^Ry(5e zeA(yH-UD%;&^U-ncd4zqn=rJdn1PSt#~Z`*vOxZ)+5X^qPw9Hxkun(VSz(RE&)>mds9)YAo} zvz6&^%sHsrg{y{IVa%cd!rIu~mX0gVMZdy7ir2Xt(P&lWZM3LfG2*Og4V#R!XiBT+ z47Y-Bw3pw?+`aACht+lMJ~prJ9j@i|W?jG1zuoFs#lQ9Bp~Pw`rIIjC-s$T-546AS zy(g<&O8>pHn$*c>cFJYA&u(=*#$itLp^a#m_+-|ifo@N+vn6rzetFZPL`jG^PAQ^K z*e{#8@|YY}ukf1qKXL z(>9@C+Nu$M2Qtk{g&u^@&RNr|veFoevMK|Mr?9FbXa5Db8c_(wgK84$0xbWqXuLF* zMb~chY`Hydc|Nz49kIurr&PEyFg(1oC78=2Mw&q6 z#;b4BGs72OY*d%(dLAfT@~SqRBrmU`)E0u|XZEG29m&;`-3p}lUdgX1mXLYxm1Ntq zR)?jV5q?HvDr$WG*f8OOOtJFU&Co&w9Q{vXo{ z|0}gJDubN}ZfXNQ=lO2{9)=)gUnv{Q%GX+bTN}VS6ZqOTcYx;58;6G*hlgLa!^4dw z|9UO??`&#+7hCt*FU)Rlad5jE>ov|qvDV+1(Y~0Uo+hu!;@KB<-3HD$EsXg$x_e7m zna$#~$@pJm?iZiEgT23{)mUaUbc3pZ^97?fObc({==Sxu!E^ER#i+>%pe7U*ZU6Je z&c*hq>0P=5TxYo-lhNwPU!i1Lvj+v^lieNK;jfJo#oXw%9pbL!N3mknw3el){KXBH z)uw9URC6fYaXh*2-qn-lUBei!l~(oHw2yF=mPJ&?s{^~vyjbZ7Z6-buIVAGbrPd#~BEq|CQr2pF7Vj_SGBmC1zoRcw2f+&4kH2cD$0=Ym7hZNUgzwPb^ zM^CqRc7yJ-&oM~ZXx=lwUZ_aAN_Jz_r@-CVDx!qyn^NoToJ01UDG7_L7T<*+@6 zOy#Y`p;1}vvWAH_(P_1kqLYt6uHt*^QZ^#quCs81C>e5%l}pCyp9c#AYeg!q za~->(*WZFS?C(C%f8poh=YM<}y?zAr6X)+|jCKC6?%(zG&HL5g_!1AY=D)v_|75)GY;kuZ{4(79 zTs-(3#Nf~6>GJujB))_u@31xQ-@jk8|2OWfe`)XkufEvW{9Ukqv;W`wf473WckaSr z$Hvfkd3yJ2e1hjB>3dNr{ABMqc$|#m6mFPZ_(7-WPBvTQR9U$@?gg9c8(-etT;E&| z4x`zu{42J28s`^DQ3$&Qnhs7DAP7e26W`}t+rrE84C`33^WiMc3pBNyU>0=B;P&Y! zV;G9PRnc2AJ1sBK1c5S5R211bA#27Y8_z342}n9|5p>a5vUViCSnKsa{RA^G$Cy)> z?bY&~D$|h1=s!8;yG@dgr*mb&$Jf*3A}K{)d#b#N7$`*602CT#7&HXfJjH)wn%QiA z0{8OsKE}oiC-gp=JEx3N01Bq^IfvU%v5Tbgc$V;qnok+x=K{+j6Pjq?%k%7leNEM5 zsK!a2!k}z(nxL~fjrr$ztUQmaEO~J4olp+@f?S`-P5UH!71MlpkwL%$aD_b+`kz&c zD_#}n5myUktf9pSkQ8ZCqO97O*sx4s1w_=LGgUHj96#C(jt;&%{&D+oH`qH0o*o|j zu=jBHVX(G+1i#n%!H;{#j}D$42hiehd;j>S;NZJpd;h24PkZ|h`@!zNd3v~eboA*b z2ZzDllc$gOcH!aP{?6lP5BK)J4<5jW`v*YPdqB?6>+wN=BZ|KEc8{>PC%cC`kKpI_ zgT2Rl$3MY3eYbbK54{H8L9g4v)9u6Ky`5)|w-1A-&kmm+9PI)C524@vz5VYFVVK<~ zyZgr>3=7YK-5=mbaP(;V@nan8(@(aa!2}Lrq{Cq6;OS3?d*44g4jvsmez*${AM65b z+YcV^@>nphoyXgIPx`^b?I+vc@6uNX(9PkepI|!%F!=G&E6!ANlA%{?mVl zkN)F7{rB3V|A8O<{GZ{Yf6@B-mw)~*_|bn^J^FuXMEplR`uG0_ zJ^J_mr+V}s{x5v;AO0T=_J3yB|8EV1|As;MZ?#AN6+il~@zGmACT6Nt2rc5WD}IHT z`?lNb1%ET1QV~0RUPTe{GXDTV$5yRh_sRV9G|rvt83tXUQDEJA&do8)F#Tb@jVaA8 z+GRwy_BAe#F$a3V9gB*!SW-34%S8p^c%J8Q1Ua@Q&FxHSZ)0G4q_fq)dtOc9MZ+-g zY26SS4!#|zx#@PXc{<6QuMN~0Gd5C_y-a4qOV^2cmK7$(FVErlC5^tEYAZXXK%WG` z!AbBL4)wyD8JHXu`1=sfB~0X0Yi>NrpI(~=&+t>syTRZ z4x_^0{>4aT+5n1`Nml{SKz{n- zUC1kK0VXCDw=w8$)FnUzq#GKf%XW|IO?Sz?+7vh1O>uY$x7%MN3;WpYYZcV5M7rjT zm-9XUx{otlJFFEdm+F#W3xd8OKX*I`MZf(as$v=zO)??l37!l{dxBWi>i2`>Ealp54Nie{Tj{j{ z(PhY$hfNA3Fpq5v3Rh78VSkgtWr%mvfw|F9d#8R>B1$cXP#tYG@#RJhk3n_*1<-bVk#W7G>}9mLDkJv^ zVS;Uyjm+p4GCJh#D4ocfE?0x0j;hTN_dK5ELKWf*HT)r-zciaE#-p2hK|F0YWuN(ib8~?8x|F7Sc|Cez2q6my6 zQ@tDJooTn zc6uru(RN?YvNR?FeZX(M9J#eo zJuKdzlqrOa48ayAm)II%|06wRW{qPNmdeI(IAjW#?+R%#h^3Kak{LAnRVLk*r8*?U!uR6mHdQ@>W2R1dK<(W-Z3lFIhwdzTdhEdQ5lOIwDgt z*VYmzn5K0#w}fCJ;Uu0$i*9c?_= zXAe|duInR{p5rbnvAZKBDxc?OgY@MRg0-MxoZV)s^GI!O&&!Sl3iRppvHPA+gn;LI zo(IaNxwYC5m5AEuwZ@T$mn^$Pw3JhqpwRJO9}=^b*RSm4&-6bi7Fjis;Ayk_D)wNw zucG2?KzGtg!cuoTks8Ce=?$)Sy7>gKG0=cg2CpfcWY-P&dAw3Au48 zgSp#p&AW?ppRt=87a99*V>c$MRqbMqDytTig-Kc}g;yorvKp}Gc;(>J<-tRh)Mh1% z)N7V(ak56~tEOmBsM4^D(3kkyrSJxTO#1eLp(-?v4^sao}h4n@infI>i?Uk57L#Yy$B8BToC8&%$Rhw@s41B~9ter3AO={@7lEY%Akx4TqT_KKPt3f>3OLqs zV=03o$)l4gX4FckIA0!Lp!JHF9>GYUka56}-5D@kCd@1vyhf6#|Y_rud)^@iyiKQ&FYw{+53W8E44cEC5R|9(_X!BI82UH)KzxGKI zeADq9UVU7LF@ zN>PiW40ySC$bq|N*7`Gl7$Skvg%b4_U1tQ}U6N^}PWO6cg2be9SAeFTnz2KZdr87p zPhFzM@&}b7idY`5%}K0S`DC6823}q2*hFcN*Mo&BU_dQnX>QQ60=sQN$ALN^5U zD&rVgy+Sc49*{ExdXM+@KoXIN3fWSY~IPm zZjLyXOjJ+(0*H79YXovsa*{{+f{sEBr+iRiecJ3Yr81A0^FiVN$~&^^Vv8&=>iYE{ zDq^M6acU=|YRjoA3f0O`54_&I7%NKQ$`a%ZT8%NoAxLf%=QJJ|5f4rAMP!VklD^Q) zsOf9Yte#b%p&SOpE02aHf81AP_QEl`sG!=ai{a7?U2oQZpTAY+LIHkd&cbebHGyWB zTT_nJ4~#jjI<&wOW69{r0i}g4`;PaG$t{e7+$SFSYmXtPB`MZrE#w1{tKHx3jHIoK zstTb*IH4e>DV8lwxt1xVI7#E*Hx&f&AfcSYy~mab=$u%Uk;+Ni6Q;EyIcimg`U~d{ zE8LZ>a@zQxEU9hKND*9Otr@xj6b0X3csV9@m0q&xWH^n}Ghohxs>Xu7(!I8n@S2#( zYgGu-X$af;wcsRyu}>^9x*im?GH6iWrKUAo4zIlG^14h9XJ{l|Yz5!NCEW9Ux*?dN zEIeFQ_(=+kYDH~>SLSZK7`s;mAH=GRx+cx?&QPBxv)mB4QcV0X^r@k?2HT%xSXJK)nR_`to_3@&j{OkUyK<9gQ3lyX-Y zwhYblfJ7|)Dy7d&-)i{o`CUC@%nTS}+3h?_C5zNHTv{!Y?N?XlT~axg^vNlBP%H|N z;wJ#1Vfh#SfgufFZBh14sdj8X&`wA7Ck(B6K>fcvP_^iqg+g<oA7RMqUWSwlSI(4tgUQ;4ysyYExYT=5YGux? z0s4ATWRS;U=lCUe?3S&dL()FFMbD~dcF9%dyDRLZ`i;bCN!0qW-V&bschdVH+h*an z8V`Rk%%}{P!lXD9xg0mDN?CoT)TCA(t1Kun?jXXeEZnL2NW`sA<$8(GDy<*i(g4$R z8d0V4*mic}9t0C`kLnQuGa8{*ax@Yhe)LW2U-O%NyL{goX7yIvRu>Mg>Fm?RqBteD z18SebUC@yuT5aGAYhaVTP2P%3O%~yJvijCGRAU%NgYpJlthOdm8EuU!v0)UPXP2A2Er zCwjd+n=;GV?xvpwhHvF0o|aM9a4+JXP34i}H&54u$}efXay?p?eo5=L!H7E1ZnUq^ zMwu}Wr!fNpYXzMW?)Ydnn^LTrUe26s({j$f6IBh&Q&UQ4fXO**QD;blOgMNDz1y%> z9377p7b6|BX2;vc`dQGGNVpT+ zUk}%N_=thO6MS)h9Uqw{C+gvw7SS%pSqVGEUCK&*I*p5h!(aXy=b10^WvBqgf326Y zLp2@iJx^o6Yhk^UvmQtyz@d?6u&>4v79oPu2B^-`y$-qfBl#NQtDYX8XUQ4`y2H$; zWKPn3r!;kgh{dBF${oov@Qw;sl=7A%Fek;XCB;4#F?rmNy=C3)x^$~vWt%SDqF33T zOSa}}+fuoeZUv626Wt+S)OCz&UT_3--_UcSb4gX3T{8|QDa!t3EdAv81e6~0b!u#- zNtF6=VPq8XoRw1mYfCHl5sGj}^|6Y)AyT`G0Xfn1EX#q&E`lPtNTyLPd;>HQ5G02= znbguH=29^eoYa=%x3(?fPBVu!g-5v#9mTox+)$hPn zZ2_I_nxyaJw@Z1gM*wUo{239lY72MqOI;FS2Y{+@g#51~wq9pTMbcUg#5RHke$VO- zZ3=kJ&e+yV+70lMJ*=&lie8~X#z-sJzg z$^Ued|LNwx-$ec=v|(Mni$>O{=awstBsrdPdE9WZt_3fPVNVW3FCbm>J!jJWkiC*$x*Toln+jQUurr7bS8 zdH9%rEaD9K4!nRTR4`3;yzPi%G)%pPyqfQ-0e2}jKp+xp zBeMSBBuUYQ8F*JLv=-5Xerf6kG@4@(*%^)S$7sZHMTUZe)*wUg@J@cx(NsRTQchISaYh43RG>W$SvnP9 z$>EcTRr@2t@&XoV?_gA^E+k?+Ix1WvOGl;9i0nBjX47bart>tI&!$;K)CzAN3hAu+ zR*QU68W$B%r7K)UqtjW4Be0P@Ii)A4{nC`Kk*w1qQHPBLWYClXw3N9r(~HpPT9#s# z!8iGG?HHabWf_gnFGMA#NPeqpUHH%8u$X3LF&v_Qa_4l`=?5JN8vZ@Z;9$aZ0mE5V z@Po@V&gn0zg^_o9W|X~=-pWzp%HBR_$BUtQC$L_;BMn5^^V}W7N7W&sA>5|_eYmqt38ZAlQHb6mU2ilBEr6Q`!IS%=8Wp!L;SD!vuLT6_O(W}}Zd{i`Bu!)M-kf}x zFEbIbB|Nfux%7{S_fdTnB&0qve-Lo?l!qKz_VaAY8&)!^LhnI*xEG``i(o}LKOwDi zMjJJY$;FnM1@jb5F8b$t?306)f-)z`9TB&TZG{D)2M($7WAucME>0%VmM{C4VnY7N zTbHlw@$TWU(^~>Z)e?(o>MyN8#JJRr;_*weVClWzqBCTK z??#$*gm44Bku&Hxnwvz;F$QJ6xGRIL*~EfgUFc+@WEE5g8PrIVCHV=r%X%MuJfqa| zFbV!LPx6=UfG`>QRx+pwA(gEmCo$~o)QHQyCH*HO`1UXJxL`{=F=*a~yx($)fiY?l z-olpa;-+*yM-T2PZbEUkk+Tii916qYgLUN67^4jE&_|StfvB8NHJSiN{}90ntP=zm@P?=(6q zu15jftpEM$-u*AWwClgD-@A9C|Gm-w-spdC^uIUy-`|%0mo5=uoX#%_nRWjeNTuK5 zd4unVRCje)rC9GAEs8R}K#$)vo>IGx@Mh@xse>?$)1RuU5&lS_N8=!5ix9_qkh~RIXbz zO~h>YyKeQT90Aq5(zO(his=yfJAG21muZs1g%}PMQx9l*f{mwUp7CiB{s5%^aQNNh zZ6JTgBaBtU1O?5&e2Z2J&SlP1O;YL+NCVEI;V^`&`bFWXqOR#@=)=B7wLQbH%c2>c zphYVe(d*#}<|`TCpIvKCA@vGS^g?JPg4S)A@SBIb-)%p8d_072pY1|lcQ?>|c{8|k zM~%>|!0~mmu1`V6Gfay*U$jp@xfP`0Nl5r8JUKHsxf&d2C)LJsR?z1#2vPL zdTByQr{AP$_T`YGJ;5t2uJgup&3q;=t6ViK;VOs?3hU_MCDERe94GI&Non z0}T4So=xK0ZUxYaEcSN6*c;p~zQr}}n$y)1+tv<&szlEa9=7K6ZF0D+7A($Y@IIAv zL(%s}p6Tgok;KzU(B*k(`5f?YtN3}(#E5aA!qR&XuPs>c572_h$mqm4L!lGAC7qp3 z?Yz+Z5cjR8#(*}+->82h&29B59d-5J{=*>n$E=ZQJFJB4RU9MC`2=nkTG}ME5fVqK zAvQwiAML|goy?>Wmps}P5W9+POI%@Mo*(mQRs{1I3@;a%!Ff94K%`H`dk*~hDw)XqW|x>yVIj1{KV@@Dm5CT(107rl<5##bCJB5r zNB@63fH^Y2>NqMypsUD|Mj3MQ_-u~gAwG+cL)k>mP>fOmsN+e(>UJ?#J`v-nNT!Rb zq7+%R;C?zEBWTj#z7PPDUEIywH;M7`Cm=m#30}mx#VMH~VcFr@OU@3O!v*e6xZlV5 z)>f6vsjyrFCUG%_>v9ZjUgO?a*n62$qA*SX%RDW_dHff^V_4tU?^oyxd2@4r#Cu=& zJT(9~FOtGdr=YgA9U%{G>WGalWV6X~c0}lE%(XW{Q&5syM39tDcjT_p7rD|RwBq!u z#Yeg=D3Tc6L(t+1ql@}euIbTJvcPT=X)S0vag%`+Q=5em28{{QNe-dlg0lLp+ zJt(;?F&n9Z-U=8o1Vpu(w}S6vMwJ}4URIGs!7fS$A7a{DliY4i_7Uf2Szj^%OiPH2 zM0$UqFH#Y#74sS5qgV^NRM?J3`u^FTJUnD)N5Y)O-XtPu{w|<*o|NdiLt-*adi&`f z>6OG)OUQgVo73f9TU>4Mnn?t&C2G+bZ| z=grx*(Filp>P0fY7@}$rIN|I)g1w)Vd4`BAh*d0yoC2ynQiu{fboEnfkwbjsl{Mm6 zF-(^lMmI<3Z|VrGscvy|zFy&c_41Z+o)yImwtK5|fOQ<@LhRRsy+Ne&hTgkua|QmF z6vOKUlM*&*powFglnRn9)^odBIHg!IMBYCs-W^cb%iI;`uA#aZa*&$rkN27(%;xzx z9*XXVlbFg_yvtES9V*0fl8;2gZdK4j4CaOU>qe;O`5Ek`>w_}kv`5sdB1@eiZrg9k zojOHX5}utb)Hwyi6nsrRicaHl5jLle+_>L-R$5k>CC4SsOag-6h|dL|dC%vd1?n}O zh`wI0d^TRs!FZjg;yWIRdRO}ym9fV3(5UK*D_p_Za+x+Bp!aSL(1rta`{ZcFOcNa(_Tg3jeQtghA1~wJy{`*6em$3V<6~w39=Mo6T$Sq(z$MoN@%O}?Y zHC-l_qy^N__G!hh@*VE7^3i*<<~^? zX|Ri(21l~fC16`1v;ew{ql*ApK&HPTj(08CK8D^1oF#CoQCmRBs+em5PiS@MK{pBE zL{^7tq1bkIdYX(AR5kXN9WK<}N}O;wjbFvn;pR1S#NaV@8*H}aj1r6%umc}TKsteq zkd@a0^$()M;6YX{L9jmdilD;94Q7ba&2{cW9roa%9ASl7RA5>2v8~ZITe?QwI%c&P3qTaJN^i!V670r zL_|VU(=k8|mwJWU(>MZB&}#&RWt{r<)`v8`_+ z9&VO@FXbUQjmAKb3svxwD&JCFVLhpZH@q(oNy7(NY}A!EnsDFwQ2ULCYua$4MdOYW zZ3I~c-831$WSKC}rm=ni=wIQ*@*|M4m!c%w_Ov9D4#(&7^kx0)Q!L`RycZmf8;v~M zi`#fR;RYNO7ZOq%-Xr=OUMsII^fC0cBUK}%nW}pUOtXw~SZOgk?cs2qPsNGoeK(AX zQLnCF3~i$>(b+T^Culd6_XVY)p>W!0iZ`0ecsjKVst@2cfIU#6 zmyPvxaYVDjrc6(Begt+c5V4EnFA)vm zC2e>P^_az7qK>qz^(_5B|;~`b}U3~@UV^JRX zLoI^_@JG9({(vwD*Zd}7+GwX^xB;j zC)%19y$`;^VlgUE8di*9Y`RQEmqXQP#ruX@jfNYCZ1eYd<@maVDPFibuf7?ObS8k5 zapf+(%C=zVm2*JUcwWj_FPI~UlBf?lmxRL7k%ZFF_D(LU=hwaOs9V@^1d3Q79Qxst zI)#e#uE(I_D}5-A##KJW?Q>0^uMGAqPhR0k<7iwz^yNfEjg9J6ru4N&M0!7!NA-kb z4n_V}+}ljhD3PBhGm#f%>1>ve7GQ~#oWp1`S#7c8he8>6rEka6m^4StpZXXIo6`Q! z3;rhl+}P|bw{;A!%wAUHDHtC}YvbV!#5-YzLFFxEzC&!hF9t&CwQ&%}c01;=+DB^V zxBJY?G*6*cv zy~q28g4e!qR4=ZVY5W?*LYp&O9cKOGtH7;)-wgW)SB`L4u|_`{U;-|6=WZo(Z>(!F z_ZMws?yGOMtL`w5^sQmQ6~C>Q*MA28Qe~wH8RzJA-7K(*a7V#_R( zP_ME@-tiRj%c|B+aD>&5wo^Wmn9+8Fta`Yt`e6;2zodQ9{vZ)&M+-TvT+9IK>k!ho zr}rqL@c_1bCZ1mJ9+5+}lQBNDDN~3B8=9DKuZ@`SvDVrnEJy^ql7jx``k9|v*rg}n zN3l9;1>R!hpdE&a0fsEa{;NX*EE6 zqC<6-VJ;u;osOSinx$tb!|2jktu(sa-}PP4tndaLhdOgb{JOat9-#Hp=U77at-Zqh zH<#uJZmC?*UZ|@P4fLnD2AJ8XG>>S%J5x0PTL+fzgF37bSpH#i%4hR>i5XkEYH=eE;&Q zS>%$tP-HL506KIp6EJCzux!?hGnUGz9)b|dY|4;5D$d{>CsF|F^el8~Gq&MrScDcJ zai0!EdZ&J1v~|;=@sncUz;aqr77eiyvH zssH`n|J&RQF3RL09&FrSzjy!s*MInWJzW3fcQ^n2o%}ap^>H{`+}#Mj3^zX)4?Y*E z_u$2~?Em}s@7L`AukPRf@+)iqe|c}?tKS9dH~as+|9306d*^O2&ale;RxmG5?|zL> z@bn>>8fEY&d&j|JE><6O;RhX?JE}B%ciam$Va@Jtu5YdfhtX_S{uNtPnJy`5PMpUl z3$((3bTa9~X&qBu#_@TSpT&J?pW?#yL3VZ zgol8x@Opw6p!?9gAO-Sl(GM;Fo2U41Of#b#q{Vq3RZT)&+z$#$Oo}K4pkOMWV-2`? zI>jz9U#GY(OZfUUU6g$gsii;@4SaddV$Y|asL24DPC=xAK}A|kxUOY1CfP?x4-lF*~7j4?}G>M;r;X&(`ScI4~}*LfQQiU z{@(s~hcL|UlimH}5Qc?k!R`<6BRG1r{rEAC_30Rzf@2DI!#XW*(BL{SKGyuPR=I~iueMa=C?vjREmUP^|HZDH(0m{8j`kodqnCXeCIIPV(8 zc88RuiVQ=1Iq3-7@{jS!Q8s=Vmz}PZzDB!*Jt>}*$xt$)y zW1frgia=wNy4#?;OX(6}@|q99QF4|>Q&v(%r!m|UbNL}Ln#=g4psy>HzsLRs>@1%Q z6Ts)4K53yqMy8^}cB)j_5bXfsPpbug4e$96>uol}`!sN?XHRxJcs&=*v4mvM0j38G z>ARmg6&lp+alwK5y-3(jMJh?;(4|a;JQGE%Vp2{DRW_3@IgO+p*cODoV9cFYI6_N0 z0zpMbFwb|4UKImvpLRROyG}po%<_1Gq1-Whwg`1zz}vQ?t?l6jFtN7o;5j)8);U&Onr{W|G&h%nI^n42I3oitZ3?IW!$Cj$m z-F}uD$=Q=)m@7oOx_-et;X`D5A^wl0A(bt@HD>Uc0@~J5u?C|)U8s(uL6y~zy!2ws zGe3q#|T%R9NgXlBlm%zvo!?siDV)564&tf+EhU z+fmGr8~|`j1bYv$(mc6As6EgRE~A7Lmbb%A#0`qP1#l>1EA@HIo;zlQpR4^?OyPv< z%C;2~vk$(mk|e@o`m0yZ)b600gWv7q5^eax4`PwBqD&@LNsgWA_7% zf9M~`$Lz4p6ob3G?aA6TGOykOSBJma?XXqN_>G4vsr}$YNfZzD$zJv*`8RQiK{Qzh) zvIvpyFZqS>onLD80+by|y+V~lPzQ$GFCiye>hc^4n{VM5t#AUnk$J|951h%L5!W+50VWZAOo0YfBJS8{Sq7l>0O zGu5FnC%AI%6Dq^}$}9!!|x9k$H~J zq6KV)iQNg#Y7(E!*?%5+oO|j)PtQA(&I{{PYpHQBSNo=mUs)f#<9aV&>08V+1K1eJ z6f4#eag$d)dN=7(d*$d^(uuL|zT)-7qoQVI>dDO@xsCN+eXA3g_)Gt5L6@#RBRpny zK$gk|2zYLv4Ja0{wo&6GXd>t;f(yL><|sfJ(!-+Ld+s}@U(|OfCiSOyu?+gkoNS($ z4SmA}V@Zd=%J_hiDw~jVs}1QzmuA@XA@pJkZ2veMkRgXdU0*gNbBS#5+j4Dp# z?7gj@)7u3)jdsoXz&*G_X6-J)&}$f8XY0P5N(=6A#uogomdDhfwB-?Q-s2<(s@Ge)izzXiMp{PVNm*e)TQzh%NVIki=%mmk#0SeQ9bNufDHqKe*bp zrJA zffSxHRy})1Xu~X`YOo3c!rT0)s;|Q#%(qnTVBY)H> zRMEdz_uI$WJls2ax_!L!NcTe>$GBb5;8o{8VkBG6BY%*hY6x$>`|mO+=yb6$|&C-Ax}2+ISJ47ZO4?pLn}FBy$Qc z{p+MCg~40EsJ<7b&N>1*rZuKDrTvINX=bHNOkQ3GWv~DG+3wNt@X79z2fK$yUhuYY z^_5j-4zi?-j}#GSk%7CteG1}%l^7oF9v$r+?5_fGM&cn}MKO6gS_z|)-Q@+PXnZ%F zC_)ROGm!q(c6j{wp#7ld`hh1kB=*i4Ztwi5uFIV)JxwqenfDtmA7MrH8e*I-Myk?P zu;g@QdX!vb?y5q?#c_Ss>o`^r(&VaU_QyYE;(+a+Pz|Y^2P{d zr0L5zH9wE$fGxP#a_VDlL8Kv_w47)@$@a8kS6y@!a`kr`CI>OT+cdd^@$L(U5mfK) z*}Gc%?n`}p8W)TK=A}2bp8oDD?*`&`U)Svfc=rd5MgwcSjdc&zsh1lzN$6j0HUvtt z1V%?LsWBY;FrRS)(a^Ew{euDtALs&QDKmTr(Hbt)kX@p|JP99imeQyuyFN2?n|0Iap*T ze|81U6BwL~w#Kh8YKou2 zNrU>ag0Xf~*cGa=v@4w^d2;+fRGWT5PvC=}(OHI`5>#Ls4~&|)GI=Tt)L-a+K*BYn zAtX5;X$if*B8@JRG06teWTGbu z!EF6{jZ(DG(b+UGz_Cs=G_+{vu4=12G#iUk4lbj4!HmZJA`~y3FD2I#pU5XhJ7*z` zw>b47)z0ycT)j637xc^gDmh({agrVx?Ha9**pIQvmbK=GbPe`Ht2Hn`)H$Rh^l2(K zdTndlYK>=pAZl%FWL$9DixMDkI|ts%#Y-;D^fr|-qr-c8qHShf$_j3yjl9|xI;qbx zkh1yU!;O6KGMQP3 z|Hf%g1*xF<5TF-A2*KD7l)F`5cv=x_w3y+K5oQG-A1Eb2b|&&(%n5PgvJgopM2D-y zmTJ|EvCk^7v?y~=h$jz>Ze-}gf!g%>j1tyF1>OA!GXSo}UlmwpZ`-Q4s>s?Cy(Fej zma;0x4n{QN9g~xPyoysRJ4lsjBV<=H^YesJ)0uQA`GoK!|4)xV02R8BP+vS;hBjc* zEew%h4Tl%8_;oxcR!o7ny7Ebe2MfnK>DoSHV2pn=?^pM}^)DNqZBTqG43%j&x1DomxF8%XAeKF*K|LH9k zbEWQ%hIcpPslb6P=%_vKiLb|-C1YP*zh;r?hsu7Iq>F_q*Q;G+2R@bS8`qfAp`%%T zOh5XbRu$d*yP-z^NrrFHQaA%S9Bpg;x|Sz-b_s_yH1AYlMxEN`6Ab5!+qR}NBIn+h zwSp_faFU_uN3Gq)Z++;H@;`ioCSqTcDaNjlRkS$IrilH`FV}qyIddF_KaBH)lrV){ z4*dZ1H1qZL3cmfqeE!y>Qn$l>^^rh|Kgmk#=n|wyt^m(FNYwF!3pW;>7vkdQT9}lq zA`-~Ev1=xPoPJ-!cNwt-ssh8lQ)0gnd^2qg3hZsm^c4#|e(Vix3b`gq% zBerZqt5ONk-Ofo2+#^MhTZRv!yZlf}@m>;Aw}Gm)e9~`qjf1qZ&*PQ>P2lzOyZ5$Uphx#f=jYe!>-_(xr}+P!8uu8@&t_=px4Jrf zy{6CMf9bnP-0e`PXs6fU(Y8cK*21Sp`n|Pi?z+e^$Vg@((n;ZbwGJ}38?aJ|l7&#<;QZdsSbpvv5Y zJr3j_nxcJ~`}ZwUKm(4`E?L~rFRvdsZsjg*u=-IuPD>|YmS{p@y4w$b>Ua-Cau1e{$6_0Ehx`hldH$l; zd={f@2gmsfZ!2wGnMG*QyY&MiV^m42Kf;!y(`8^!0Gj z?-$h#MZBOBaiY&l7?hPAta< zHUUrK*L|U)#%k#)EIZoqx^-%0PiPo@9rC0=VrG+xFYGEUlhjnK7`k-s=hUN|he3ov z07U0;Je{K1-g}3liAvQs&92uT5`UZtC)JkVDFwYRddLoAKu1h?=IK;SrvQ59*KZbWvi9ppLn@}j{FML0G$WfZ0m&CyoyB#tnrL?OQcIitLcsVGF{twwpBEGI2W z?#?l>hT^P}L0tjgwb^KaqPS#+dwjKCm0zKN((wI)9W-RNKCJw$jjRRi?&Ej>aegP$ zA&1^La8h%T(!RSvp4<1<*YDx~ZqVNw8}z@M^uJ%w|K9Uxy)@&sXKkKl=&5O1fdy;D z72r9t=8}BH;A$fMC&*xD7!OgJb8LszZ#zegYXf$S__+NqVgIWeJ4Z>(Q+eYDQ3f+$ z)j8d`%!u_t`gMd?Lg@Co!bM>q!`(by4sMPD6dF4%)AIlFdCtN-Qa?;^Z4Kh zb9?#U?CpPF_iDFyUBKJk`z*FDoLA_7n9#~1)lGDKFa^XjTc8Dkk2qRfyJuFuxp+FPgmGp>v3+&j#vGTwyE-=9s#Kvk0o15?C-t&y=uouQnETHAes$pVQVA zKsb~%d6KZR2>fr8n+g5fU*_8~FWPwo6s&YLw~pT(KnP?TcDyftU7v5_RxXxt+qAL7xlB zi7ko&O2`uX7_pEe5aLCe$smeVXKcl1wI)dcAG}te`n>B9TEg6gUBIu=-7OtA`z}!n zQo7P#*7p)Qja|q_)QkLKb;L#d+B#HGYwC{v;qZTMGb)Jhj zWp+iRFa(r-UBGa$t*v+R-0$Q72X^Z{S(!FJubij$=a-5$mG{-@)+Yf(Q9)Mk8N>wnH#n&_)8=LDmch;mn z;ej>@bSn^P0=!x~JyU@&tq&jW9_<|NJw0v+SN}eF#n}f$IQkg%>lj}lDg{j(JRCjU z-TqTu5`q1!Oiq*7h(+fd3sz`$AyRCw5|YkH#=Zy*l_FW$&Z9Bfs;2SeOkvfNqM&R{ zE8Q4MRo3+9C zRaCuamsDZ(lFH3)zpM>q*8}UFp!+Ja-V-5Th1N?;vib54zV*~Y}Z@*{}K<|y7 zr!dy#k@KqsoiB~dX7)2g_A_YED=pF2crGt?N-0W5w}gz}qAUQPnf*VzS-nYLA`68^PTWkF6gHSjA>2uO_lk>a0)Ij;J!!$~S6D z=xDp=B@7txIG@aXu3sHIJQ_khqsc_XKf(-Z+TgsP0Zp%_2~HZCTsTHDYiGY(0FirL zMxs;HZ!_XHm^USzwuq1BlO!7!MLzaLc;fT-HD$WWNTCWD#K5Q@#8p{WCO;v6wmxgb zkRtgb!QnkHsO$rVFTO8yw2S)EgtQ-g!kOx3%jXFoLwKGe#$GH5-?8Iks?Zm>In zemYepQ(8YJZxYgsb{_n}3f@b}+}cY9X52HBR6%fQx}heCaC3^iq)riYW3htD-D0tJVzH3>eO5^xG@ z+cX=a!33F|ie4%ZolXKYBX}Nyb*!gJoj4`auzZlFk<1_w3033c;)-UD!kSA4oi zXL%7#<|89OQabGu1g-ZFZ#nijcA!m`sLhR*D(oSc$O+57l|{B0#Hn&yrQX`inCl9% z6%#z4&=liALC^<+ZIOR2VIx|Pw$|QmoCa|_ObXUCrBiZyK-kiSRCKguH0z^wmPFs> zSJ`|t{4R<|AYG$cQKu|JTh)9%#k$Z4@y6zd98N6p)&|ha=w|c#4O6zq5)`hz7i(V}&bYZ!rU8*rn zv@q|UivZeJaXN8*`t4+FRwT4Go$=YAd#x2oo9^$?8p$Gfv z&ENGP|3&_7ZAD|`k~^DEHy%HI`r}_7|Ips}`l~Ph{+ImIq4w4{+fUkCkHm{doOED% zb4Tm{>C>m-^}n_C`03_`v;LoKJl^^$+W4~m@BQDyX#Kl&+=SqOz8z6eCwzmO7D?jh zocFSK677T7Il(g#JkXZ5n@w+K>E%@sH3w@T%x^wf-`W6~{wSVKi+^B=SLnLOuIMP( z!J+p2Cc2!(6UxVXF-zzy021EpGHFo?jmZt>!Uka~%FYXsw;1G;0W^h03zQb~?4r0v z6CUyt>%Kl6T-R8<52}=T5jt(Rs0nBIJW(QR4xsp`n>!5|3omR{4#bSJ>+)FJ>u8 z8K{^WjOL`eke|sEHD>)7IT|1aC@kwdC&^=aLkGt60{7Q!tcA>{x`hSyMDyqT%ZPwP?5){~wWB2sBZ{*H<9SLD_GN zm;u_T&EG_}64JwfWS(7%bST4=tmU?G;wTrzIXcG=)Fgwouh8lJC0r*9sbhW>+VGl zI>+$*pcTF8oxC`FeG);5qt3y}uhHT2sB`dZ^!MHYh^O8EL^Jv0Z@xY}ih3_!?e|b- z?H%myzurSznrBe);1H;~2jmP;PYxq&QNZeTj}h9-?$PcGcAvh9oV1}?co%hlfrse$MQ48> z+xq6~&TAOJ5wvs^?H<1R^{Dsri<9WZ;r<>bx_H)wzIC4MceyPX*Y19&_p%l3bzXLU z?ozEo0CV)s*I14_7`=JX#dp|Z2mZH9mP9a;-NS>EBY14VK#xvT-8VfH*gHqPW3r-q zesl;8AWosmAwdyWUkC>A9a+-?MezCcao6b0UbnLk(2lXTS-LIw$v0o8V^XytX?^oG zokRveh{9Sk5??VI^styuM`U?JrP}fv8NkSwvuu>e4@Vgm=l3Fsj`;GDo?5Jkr^_%| zPCKQDZgCu9`C1lB#@TPFEYrmY^#vVzRKJD$$sV1nxPV?Ms+8u8hwqcw^(@u>0bHP6 zohZ0VPnAtcPnVVmex{-)D2Qw-EvtQ}piSmu1&k1Llw1F_cs)JJ+!B$Ma* zQwJa9my1z+sXB6elNZTY;P5$q(c>u+*at0w@9B4J`u{ARbVcZEy%?Ge`5Gg#unRmH zIilL8|1*kg`{eazS2Dg}JtpS!v>-P*+A7n3Bn>N-J=JK+8Cui!*hdLFhLJI}Or0S! zRBL~Oa&UiI1{`Vy7J< z$m|5MLE(L_Dj=azRy{yS2sMo0`FTR-h~aqsClaUxNjf8>rF=U&K}vfoD-wr1YeApe zhW5ULX&0!xHc4lI*|mz~Fe4_kZJSatXL9<&B@HO&$tat^0?Z~9x1R-}n0!SBiEfhX z8Ae7a%Pp}rZkl1d8^CDBQtN{*&6glo>4=juA78wa=1h7Ju;QK0ON%F18pvp5 zB0>b&dH#3l-(tWNZ?>aDk;Cn+ZEVJ$&ra=TB=e`i4@DA0w-J>ifE@UtT$bdMM#1Uw zL>JNlPKwtyV(K(NB0zUoLslMEp+Z*Z?TO+*4P<+`uo9Q#s<>lB+nBXx`;3@Zn?Ayr zCcuYdX^lRg#P8#DgiK9?$NZOPAD;)b@630F=Zbcyai z&e0UV;1r2NI#xkH#UaKV4^6f+DwCrSt7E%H45GDtmQ6SseBY>XM!jRkBc|cT|va3V{)!;AIm@rNh zf*->14K|t0wz!Nx$I`Rq)<4&2%PT{b(O@|!Mn#F5i?}~Rp|XrU=;do}gScB?fLM>- z_j5SR&(fS#1q4g4X2tI}XfYch0o1}l{wTHsa!rc1o_8dzqyT<6PA7QO2l#M?7Q`f4 z&>M<|ys6m64Zs$|d~ZnE?sz>3yo95gFvK{;uKi&;0?CWaBT;bgUy$UkTd)NB66g|0 zB!EN=M9ug`JRcSPiE$RlbpzfJp`;PBI_#1K+L%aYasfv*3@5f7VP2RrYU-#=sh2t8W!h5N0M91d>t6>FTGd&qwddeFg`vz+h%E*9F5?ExT9q;BX{a`4&bET ze|@yCt6@CrtN1+%An)S|okjV;1Er#7(!Ok~1fZ9fQa`&MPt!FUcJJVq9)`1hwcj~@ zp}^x#%;2VY`))LrPZJWp^XZ6XO{=5bqu%pgcTabdWGSX7x|T*s-l>Edk%R7$S8E#2 z3g880)m|PRb^G0yhkxtY^$_)ZdY~Tb4CzlcV3zU%16ZIq>3}2OBj`V>Pc*>%3g!{7 zm4OQ$Q!n_`0Y7dW0MEOflh;ml><_YU&KoT#X;=f;?%~UquW`~HoD8z@cuun�ggG z9_@B*G9e8iZRRkPobuHdh!w%kb9X*oKilu^x|2bgM{CQJdeY&WS}&S}4ijRN+f zQ9QYv>+EksS+6h2VZpJwk?T5|4yxs|?>z_Bem@^&Mc(g6J5jS?oJLXeV;m&$QOe2m zTP|;Wz!en?7Tmz2Kb63sR}JGFfRDVQSv7^5AHgMayfCmnYHxF+CrmK(f>LXRtI6{r zmQ&K8Qfv`Cxz&dO__7t%4hu#2iD#o^5JspC9!pM|zrGQ$qJ{1b0eL74(U5s*h~}9| zzA$F4rcDPU_|(wLnkKpQfiQSVYTaUMX{BdK$gz|j|%9aV``yU@$P&PLn|M=vG zry9(s3U}`B*sM|5;ra=dB%i^F`s99;C-2ZVbDaeXC|8EU$_&aM$>$S0W(VM-0RTUh z-IC^D{8ZlixAFv?4l!nrc(R~!wB=<1x2M^(se!kMgzfGch%i#X3Wwd&B;F?a$m&y8 z@KC{%Al;?t!SAn_ski)Bpn)GkSXc%op+Lb6VE`TB1;Le#7dI-q&McXRDxtCl>8a^B zxG~$7`<*l~#NQNaD;XNzxt4oIo{dDf_{O*S&bPBh^lj9{R$8d)#UBC<_;YQIV=%){ z($8RLPJL)H(s#q$Ck1R9sJE38_WTT{jsIYY_S^g?1k#WJ{$-0c!bM_cKdKZjbjTj1NUZ-}C~rJhA758Q|Xx%EVXB@!-l2 z*{ZHegEr)>i7zOlSzt*a(ds2tBa~`6thWNrsjFS&l!;_DI=yM*f0_iv4PJ=vWH#-l zHAQQ66E8euWP&6|raP>n;^#|N!$c&8;@tARMF6LAi2B~0db4A7X~*xHHP-6Nf;sqx zxIu)X^*Hv)@-|^p0fYJkv+`#%mAc^)5(%e3Pz~@+v>wG(>3?LNuwba};eAgJIxD^M}hoMt(h6h)ip z{Eq5WK-ph06Tyu=W0aeXV`Q>*J;B9;HiA<%sJ#|0Z#cp?&|eLX++!=Qz(ew%Q4OCb zgBZ0BX#ppzyqE)jhztFBqaE$TyA)2BAEH0+xVcgvVt-M;`yPZ@zMO+`BH2U5ysqyr(|CZX!Xf+c$ky=mKN8fMzviiXblQ*$ri>#VJsf!;Pr|BPvT#lfPv7O*rY zfN$}?VuPM#qa?IFJ2UMt@`rpVCo}w9k)60ex`;6zjhXTsO>%auAt7&+aBXKd6_9tB z4q;m51k322CU#L7d~Mb}kMk50%HHVQ>1bv^mTO{(Tyt0`7;Au9tf^Dnj_cD!F%ASE z3==at+AZd7eaC$Ii5gJPfmQ0wmKI`-n2z>L#RF6ii>!peH76}~ODQ}F^<5NR%~joT zns9s3FvE36s#u|;iOHBVVZ}+FjT2m_;>70N%W1YGvO({N&*Xs=5?TU$cVQxFQ;<~c zn2+8fK?%?93(0)0UzM;n*inX1P0MD=s4jFLYc2#|>tI3it*Q)|0gZ(RKd50aoAB;j ztR`f2;nd_PD!kT8v?XSHj#Ze^R=KKW!)wS;VcFAU>bBfUsKByIlS<1iCe+eaL7~KQ ziyF(qE^q6=q#K;w8z}=*wYLpN{xz70d#<&d^ZDr^Y8=@^PTw=flm{%ppiH_H%C@(s z9*bg*jrNmGEp)JOzcW*^37zLd%dK*oQN@(1&RO|NgXfVL+X?|ikr~@Ko;FAE_X6nQ+L_;kvm+|9>!<`A3#WL z=>jt~mNo=Om$^|^)7Nc=;RJM`YiPB;v0!4^?9(QRpx!jtMN4byb6;G(E5_b0FRn|knGY)Vvwi7CbbMeMQH-sT)_o{Y7f%a zEUL~0?cwsU%$nk@&5OsJCvM`4JYkL;JTi{?wKIBRnieZr>v&q+^l6J%Tm71i7r8yjh$V_Usyi_s zF}CtimSqJb#@lyfuq72io-(N9IHJL})g~+Ta`L%lqQDIk)=<~7a_~+I#&rr$hpOO2 z$U;O85Qr=g%Bx8Rau_4>s!u@xb|Srp@wevSZ391f*prz?xmaa-c=NtRg7xIiK|m+A zRuzXujw=+4#4-?z&j)Q%ek-HtZNo}TA(X7dq)j<#x0F$Z6{JKQ)+xV-bC)Ak0@dMT zW|v7-{1mcI-mb7pjw7)K1BR*{hOY}lle8bisAHq_YTB;q?2HQcG0tXwm5qjHXOTEG z+r*r=;LM*6uC%eqC_SIWobCd>V}vqLTA~74fhU*cYE)qsxU0*kR;?SZ;5HD@n<^)W zGVQ6H(L|=H7o%SDpjz!y_bXi0&we9*%5*0yUY7nYP3eqhn=fF(DfkCOG$e(sb zJoa@Dc6`hnO?M?lHYKw+QtXz^A+pW4f*|0Jjv(bO1t&@1;7WMqiGi+}mtSq{YV%!k zvm?%ZgKZFEl*SC$6h2Q3!fOoWXO!ov{zgvJu2hXqg(r8!Kb#;zu6uPvrn zj+7igTi6%nl?bGTL~D8^g$tQZ+`>aal5dOBhD~mQ{MM4zaadkw+tq_Bk`G1w)p!D> zgNqTt{QIm$W0?286md7Z%w}n_82h_i^d`JxD>(o1=y%3>w^M*PKfmbmD7 zZc!Qbr9%=rJLtu0*autIjk&Q^VMSxQ5XoW=%3VTv0xYP)l|q3Xva^#gY?lLUDZdI4 zb28yfLNek2pE*oV4CP5>Ww&OWxaqCU1lL5X>;mbrM2~b^(E@4o^mNI#A zMGMM2OVR>f{!1g8u)V?JOZ6()yyoe>Wwn884&(h5E;`<|A`IhN1mhGoo9~Qu{SMbP zV*MQ}J(}yAt!RC74Io&@Mcf5-&_6kemuBXaIPVkJgx)r>c=H`WAv&NQq545gQd%Tq znEX?(wq``|f9I&)Z|PNu_Do!(ME~d3=Lj@q~>!4i!taH%dFt#`tI%F&XOnbjJJ+uS&+wf+CF3P zTV?EN_JNu{(;FDM>IIyqhNYXGDU6vkXPa)7$^f>ayq1jVNMa$McR3mov5_pfnMmAi z=I&-P$xPd?9BEE$9TdiZ8!$G9ASx_HcgYQqhd84Wvh_Q{BP*<{4my5xinA~(1?D7r zlnY*n3Vfi#?v^%^X8Gkw)yS+1ht&zBv4BFz-~*d7ruQYL45r8c@+yU{e|>p!1rn;| z*HMSY+zXj|BeC$^%$?83HJKZd6K!srQDGov5$q*1RA9MG(I6sG2|pzvK(>{z?>J%? znIRl896CLSs3V5^9ByP*WC3|Ye0IM?@VGt^l-`@&8#U0ydY|HMu7o+3`yCBWaQsDm;V= z(b99s8(TxkvE03c^sn%)C8W4)d1(Y3N-R-X=qv9?QWmX!b}?BZ+HXpX?^Y7A#CXWx zCbY%cmMFKTl^Z{EVgh| zXp3e__c}Ner4z6O01zJ7cn*4c1q%^r<0harIteR+==KU;$~aI67b6eAD~>McMD$Tj zETRBXY&AnB!rsEGl$}#|CIJ>jW20l+PRF*t*tV^XI<{@wwr$(CZBEbIyv}c^T2*V^ zbN1QIJ`4n4!q0#@-{FC4J` zri!ptUkN~R(Pj*wl_iAXg`s@1A{RxHrp6h11%3sKTuE3cFNU{wKsV>;wW(3M6luI z!`DrL76QV%n5E|upN6-6N9o40yqNJ*(I{7J&op>YXThcBc%ot;%*;Mvnu(lU3@LBX zxtl-H;<{*s)72xX{GFOQTnS(GpMu3>1O1~4u8E2>l3|G+(ECJQV9ZJRyzl<3U)dg{}sNUmFgYvf;%gW!YOS zh-8cQv5yU-k7v(~E$6=f@C>lXd*@ztx%cm}73Ss2P6c|)&08wHQ<$q+l(GPZ-3GXG zsxj8Mwo})+;F$hk!yPmCyOr&xikLlIQmkGp-H^?el0Tnvw| zX=ZvXRHXQBhOfR%2+~QL1g=kmi#4r3&%=#eh9? z;nKWvE+JxAF89l{xv9PbOFCQZls*!}#AlJd>Eo0e)9Ty(GZOL`#_=71Bm5`iJCOyT z-lq|H4T8GQ33qxSgM?xZGqG+*C8dAgs;pDOFn;gaF{X#$;NWBor{-aH`4CnQ*YCs7 zRlRxB{$I+A?plSi*9x(&xv?F?qm0`{3aT3#JyQ4rHa@9-g|xx)n7 zG@Ud>?>t?ElbHSPjc6U2HbhXKN9Oi)jNC(b`g;yuXZxAgGlx){ChPj*e@NM0dUDzA%iO9M!;w?4%HjiwiM(^u5g{Ps?`nZt|9H+@JdVd3bjg1nmcdWEDBei z^#6{ll^3Z%aPF(PH9_p>4mfM@$jA(9SlxN*8lU99Y9dEGfQ2ZyS`bZgS2q8>V2ZTs z#cxnZvNKG0hSgC|uu}Y#UUY+3r>;A{BSy2FudoHePz_OQc@5&@YFQbzwI;89aj)L& z?R7m)c-E~kZKTOA2Cb>aeB+Oa3Y+m}z??)3L$j5EY)qQ7k* z-C!&vrOAnpbsBb(uZN=FmtY{POJ7S+I-j53H}ldMv667TB7xN+nWc9gg_~RVT*-w| zBWLykGC>-{sr+m2W(3(LzAa2#3X)|ixE6$Zh;P!+*X~+*B}K4N9fE%drU>qzIkNRJ z1uX_WO?Sr#N2H5We}8gz2~Vx14TDOkq?hdrp71DSKY}RZvN}e-Xs~=@E<9MUDU9&$ z;rue3KL>-J#IAymQi?mimJ&EzB47<_61x-w31xa9OG~Iqv;6~hh)13)G-IT~*lRmu zY(aeN!FguhJO(4u;jVK!N#@Ia^j3+$a4D(ZA)291C_Ss+l;lA^ZzB4}6Lg>>&Ql!_ z8t6ig3h-0<`HT^?y)sU1hf%f&r$V)3Y1_k3b0#5EedE}FOu}}45$4H`j^F(~tc4%R z82+1v$d>yQABaTz-^Dh-bz@`hvAA`de5Ye{ zN`3{|a+W{`29B#!7E}D^uvtnFJ$H=!(U$Tc_?R0Pq(KG3r4yC0aav2O7KOpjun&WF zxZJ0DYoknU+S2xTx~ZhUdhw1UcE-4{sgh+#ufa2Y5)e*EB6#)Y9PTF}ojqFFH_cD; z(>0C4@magjS|~AWKKi9?s{&lWG(*w7xbp?k!HG8ns{593h|NWt=#yS17pw}xn)Ea# z(N$glnV#&5bUkEv6MGd0rPHpN^wP zXsx~m97qJ=n`3zf{*|GnbVzr!+6h+Uhwd#L)ws)Jk|K_Jq+~1H-!#vc_UeH!7lgu- zw`;n@w4VZXm;JLE0`2p_JiJgjNlpZEso@?gBJyd>Gi7=Qmq0uyoV3t4cB#OCqPTj~ z_=su@F8(1sLWl-23iUh$VkzJzFW57D$9-{XJYz~*dlOpv*VD-V{x=^{ zi^@o>C+k(mZKZZ2xJ7Yw0x@yVSRy3#<7Xz5Men=e_lC>oW2nQ5oy3Ftk7}w{^&9R8 zbh*sVN)eoJcWsZ!YK~~L&#!NBryeFy7V$@hQQ)dpZI#KHeHX>$0}+V`j_o>$_`;DH*X3SX&5lIX@q$!)h>6+UDE7Q>FsQAudzq9p^9~`V?TXuV?~KEm}Qg zG1K)SSiTSp1r z%cPl#z$$Fkp%Frbyy1a`gyR|jgkD&)X;ZMvdJ%_Jb5btYQ!V6@(FN?n8EZnevFIPR zl!1;Ye7dE?!&^5iJoC zMX>6-c;y=3P^v682{|yVcod(1E(5D&s}E;gT0#}QGiY4qDrSx!U3oI1&C;POjH5Rq zAc=Ch)3{rsipug_Zvi)j8~on?jTRk_BU=zG?72E^J~qcJ9;8&OC7MpS&Y^|`*zjj4 zh#9ixBIC5la?_fO&eSYBuz;zUU#1^=tmvv@+!QU0RjE3}iTSsPRl-ywpuAjJAc?Wc zGQ($aq!-Ex$Ls@0!L2)7XZHH)2leko{dprO-oU+kXWmr8_o_FhtN3kgY|FQi-GrlM zO&;gujLqePGeGJJZ^$DOTk_?5byiH0Ei6;J_M#Uz#bvS>7A zc?dzMGxoKKWT8?$?`Tvkz*{3>BdvZJ&nRLbVL8mqy&^UFh*jlonpHMlhc$FQ;818tZFGe> ztG0teD$P2OA(MAQLGJBtJy&45|L=BB*ZW#)5Wx|a)?mB_v8|WeM+|1JH282d1|hA1 z)pWG^>3puM@;}aw!0wLm!cBXxf|^DahH{9A!IhYAp!+DZr3s}WEVGT;Kgj~;Dy^s( z!!%)u?G&U|B1W9`U38g0=Hc+nNY?D1%4(@T6pe7}@|jOpBoz~bnRv|3X0ueJPmDBL z^tCR%dN>wDFNg$1mzS2RHsa1$0s$gDvSot&wc-@p%5K#*MH$1Z0MDkws?%Ay9hNp- z^bp8(S`>M4{Xm{dB}yMY;&{`t-D4W zjI1oZ$-z$44bksV&m>;0_?eB30~T|^!f3cGk%Xur^y>&JVyN=0ySaVf95jRIeI02G z{)-{0GFGaQSgt@Z;pnX@>6W_&80SbT`gSCUBVUh?n?@mvqm`P93`zCA4cUt9k7is^ zMOoNDy+R<8Mbj@a!!=)|1hO5^&E&#rb|kI%j_jgb`}<_a#9-=1kT63_V%F6!MzJrp zesg$)m6HHsNYALQ6-8jqkFcjJ+ZL3LwV{r%4EEq*>n?3e1`RmdJz3mkTuw|ljCU^u zjdZW0WDXsW>Z2M<+*)}CBfU__$n(Lh79S<=IMjfOaon=+saFNX$$2mvvDuRfmWe!s z&%B22#YWPBjo8B0agMhq((}&qxBv~DKWcL(ARTUzX;}h}wt1!ua=%d%<&Q0V29ZBl z8P?aA70;Rb>YsNXk2m3E^WP4)zyjf_mPPo2jupm3))kGH74xB;xyg;&REy}0_L>NP z7cmp2>N2nBm2HUy`K1_i`XM>#Ba5CgM;uEtBcK6=3eU5KTu&=dgnZSKhWrDz^y<#5 zs-REW*4*&YS7TmxWHjGA`?le6W}$b%N`fjL88; zoTi@c6?YQ6WLH%FT&zZHj0B8ME3s#AYkzL6{}nGAcSphR#dt}S1B6dc|LK?Dd8DfyVhCYC}PT&N6*=BzutR;e1kAm|FnXB)G zY-CNd2Dwpu@H9q`rak`J>fiP==^86mTS#d1lk|LZkB({ykOA4$mfcU|VEbiz&7yuS zVUz3;1)#c5A)df+3C;sJjF{eI_336?gMnI3`mTyfC+KLqx-!f76|K)*t#kzHtTK$KRG}?BhS=IPj=9rhnE`6rY~d z0|?A{P=pS`Vj3rKCv*gsB(3{3@9~!oDPLeRGr{fjx{V>@^W5CS07831Tx!oaPBc2S zCO%?QeDeMm#*1SbHX;Njj*9vVtwYc|?uUs}s0nEgP>lK@aVUP|awv!@>dPdm@Y!tP zYW&ljvg(ThAJ z?{n@;V=4%)WB{xQO(EDXWR3!5)FP2_$fH3`Q_TEV2ef<0=Gw1 z87AL-GPZI#&%R6HA=vu>a)3ND&Wy+9l$g;u++1f?d~k=o9BavJkdQ{?5!ECC+N!(= zJ21df0Fyga1D3Q2}rb81cLLndD*M- zC{geD#3kkO=d60uune7mF^RJpTj;m%5P*+L>qbHUj>6-p1{}4@Ts#)>g@1EHFGP_E+wA;KD@2X;|{n|$F-LhgJ3 z++HTgV4zzYqUhxd4!KISjHi3p+OB=flKu3wy>V@8Q$LViY^mY$32=3-@lH{IE9?p_ z_6AIagHb5fNcoWY@FA-tXR<4zZ#vd47>oN^AE?SihR2wBnrsM>Tw+%J7HUr&Z!z8P5c}m4IVZ~L2 zaGpa>AD#@iItwJxLa_@6uwqbrscn=UDJ86SReMA?Jq{#bSB;D?oHbV7zky?xUJg4j zo0mt+%{}mR%{6qpJ$d~w%dmP`(`SJISWW7YAMJb)z}+M5VHj3G*T>m z4_>gQZ(%==91WROg+@^vQP;X^%Rlr93q5Ua;T0VZ7hmD%^5@0rXiQfu6fqL(ssN=s z%}KUp+`I40Pc)RNBtLcI!iqc=PK(o^w;p7~&)KLPJ}|Lp86_(JQ?HBVC@|g{7?J@Y z4h7(orx}P#`GjnB}nMO4zu8H}s-7n^do&HOZi>aHWlQ#BsCjuJAi zdxr9lf(Baqka6nK_p!r~Ho~zMS9UVez4<%lNd->-WTBV~TlP0o z$C3fnm#W6do&q(@aC4J@m@RPizImcD0E(D;)i{>66aA#!4riY0{*msunM22EDr?g) zZ&%4wu(^{HZ7P^)dvp^e@>UTKAG8s}hfyU8gDhtCIuc6A?mCmCcv!}@gwk-l5FHkR z55;|_{zJ0_BF(s4lg60y(XA*8PoZ}7WC#0qdMPWF|Kr#n7#iEDNUerj?@h2HOyJEf z!rj6!#`WqE3t37eR0@hiSg7fQVd(5+xCa%XkVarCF1RbE^3SSfY!n1jR#8X%Z-=v& z7;(6ntE-{jW6NNaX}*G86+1Su2Fnquj43V51rKEmzF@2^+jl_(jGXS(W@0R}o+3Vn z8B6V41YZ$B)p+wu;=+cX^2FThiTMO^q<^(l*vyvh{9dCluFc!3(U?1Q41lG9ySaoUdOSSNZdn7Ja&k&6e9eMs@NMMns!@V`A>Lmfw)1_=lMd5{8!W!1SCy%;5Lb+^d3 z1VovqmlKYG$Drt!+kvbrU*6+=vT$6hmzTTS;+3l&{&!V~3k~kD_~twrLAWtfqNdR$ z^ktWpF-T8799haWve06dDfa%K5erK_sf$11{jfbPw{GkK50zB_lK9eS>(JbkNE?Yb zck`&*B5%s>y&c`J&HP<%CUvqU!vk=<``Y6wk|UJ|1{_`|>WWVvEGnqNBR}i@Dw3#2Hh$QU}KYm%p+9Xo>7Dw|BgF!{=P1p#8FS;B}lVE6v z3*>%!^e_T}&Mm4x^D^44;5QrRSBNH+>CI4MdaA<24gz-9C&Fjt@#KF(!3ye99r27hTVjJkpHuvUK5xCD+# z6GxbPlmv}`%qr+^%G1X+NFFA~pcpph>&T(N6e~^o2G|v!)bJqCsZf-l+W?W&8EDZi z91G6qY@Q%2)-5IXJmGT?e`J#_cuK&ZP^?0le8)YrI4}uzU4FQgmhn2O%>5(VEK>_rm)yXI>ni4 z`3Ct!cQ%49lHOyy7`i2n{B1K7b@c5ED8yTpdO4cFpai)bGf{d+S-WQa@!~PCWPFQ& z!Od1ym_^2Ek*q3TB?%tf5|q2IGk@dxVjMT~`6Y%V8Ox!@x$^h^!I^1jnuf+Uk<*T1*WT0Zeta!Z#Nbf}DCE?4*)lWoB(DGI7_1`Sv zd^-eFMrP8UbTI=DQ7g5?K)9jU1|mWV)Pk;JkYNV(ToaRt<-jELF%G+Gcj`b3Pv-G` zgVc)LuL+lb5Ew9e-Y`(n;S6|YKYk#KCsLQXA-jlTQ(I&mm8OmIsZXKqc;7+opm(VU z4g5_(Hoi0h7QwQ%97?5RQDJ$cP%_rviiV@(@9c46AlV-iQn?!lIO_g2=qKC9Z_%anc@u7}s5@V^1~KcaGm)(6>}7URs0h6PWneDTot zUrF)bNKx(IS0;Pobm9*%v58OQ~9RU1B;K!GGZJV0!e45s#yd1mvbUu1_q3PwRnbxxb3%v?xr} zvQ~k>T0+u_IVG5=eHfi?%Y)?8xK68x9*HkQM5ALIa=Z`5&EAaU$2BEQX-=r=pLer- z9q$BWXbwe#@vg|Ja;)OGPV0lXNw(o#9?g>vA(;j3_@Fx<8;PO3#{X-D>opD@RXOjUHlGFFTZCSKv0+>Zw_na<3D{DIfOk&*Hq#ZTD zPklb*&%xPe6-e#==FR?VORw4>uL6lx_|TX=k+28W%r2aS7?&cckpZB+RSeaevwGb# zXwERQToMcZ!gdmpi;N;>OCeo4JYFh%BH_K5&a=rj_%>zzIPV&lY+|*}-%kDO5{=4V#?PR50}@ z$W7EQNJJ7DTnHrNzHQboEF}g|NgA#qbl54EaM6wJqVIY?RCjM)>B6JRoK9;1QSjBT zZhz71BrBS`9M~^V#!)rgSh_}uIKTedIwyAN6COVm^zvsJK*2_paL~(>JWwTao zP=AtNLwuPJ(=u?YiszRboEO@@-N*2;W)gBF`YY=^QhJa>%ZB5?P2%NwI5zO`5?(^J z?qJE|!623=1(Twv&k8Gck|ef3tY+?j&+8L4%Af_=TQ{;)oBK8zDD`x9eciH!kVd>d zIkc8oLuv9CQmJM)SywBi%`q;;avk1UUr9r(FjpNsBT2R_2S6z<6YNEMRH4xvAevw3bfg0AM_5^xVBz)t5II8-y{jzRL zT9S#5;&hd}^bg$$MFx2A>P%7GJOcY&dQhU8!H*OFG;U6s_7Q)(3;EeaOpe#l1w5wJ zx7IlaZdt}S)*jguX(ef85i;?^F^a}&fC|zieX>UaM1AM3-h{d4N zq&TNtj9nY4S~@U2wolK_D2tpaL^-hQmV_LZcviX~zJ^(~KW%OCBrB_Qjs0u-5+J5~?eJ97;y90fyj8LA9aVO<7RQ@l86JdE#}(qEw%UCSgd*S*X^36PIP zhSj+E#K;b&A3`n-g&995X?52-tz_BtIV(lcQ*GPAr$8=MDVCtFqs(}bh~NR#UJY>$ zVGNGxK^-7*j;(L8-xa3= zCjz~=MI7ee)sSxt_jXn^*afL|4R6i|cMB!_-$!?dS5Y-R@Ao%8WBl$HtX&#A*8CQB zyvPjr4kEW19hyIKIehRnKT}gx1Z+L7%RdJMY&o82Hl7+kFW)x?jo({bn)|+k9e7g_ z5l;kn@Z+?Narv*`^{ycC--Kzs%`1VB$m1>zXH&FYoz(fq`gH5-tDg1UO?6Gckhy1# zuaL>JE$fukhL<@H8K;gA_fXrB+LQ5MGhmpp5F}i=OTB?YAGue$F1mwM8l6Ksp1i)r z>U$9>G;5*`GIRHgQt}8NZc^Pf1J(lDl*C%v;VTa^sJQsJ5!i@~dcQOdhDoUU$Ly%B zCTRuFa;Amo;_Qgg-fC`{p{su9B#y2?{Ql>U`4wE0>hH-7VXeEn6DTI8vH3vrSSq-B z()prf=_*3y3=@10RcI9pDFy|?;2h|!^&GQci{Agx{mVQ*xTa<`_%WkqNuM%f#2O^( z9@-AdY(H|n5Y_H}GG*@%Tr>h{FVWxB*yT)m&gnTn%Up6S*g3y zzH~8kpAlSM^lqLIyrZ)PhXRCi&;JdK3-EGA)`|jGtniANGjxb|FznVnRJQDJzo8wU z{SPj^#L0Db)xCX#D_WyV(8a}bEd==tbS-$Z`-a4_;I+ofVIkabOMm_if;e{T78K`|FG@YO}%>G^;0(tqc2- zyU*}kwG>WDUE#8V=J4=@Ktq*E9VO&*_XSh>2)jwFG;^W&dibufyVfpTo`KhYFS7w! z!1>>fFHq^mVagyCW_pfViw8lAOY$=v_c?p~9+0sBGiC(}9^1v4gK@9#J5G}p7$*uo ze1J1Sl}_1x*M`G>YxXI7s{wYfS{e56wm=h|w#G+MSwK*`mFNeS^R;_s^l>ZWi`xK? ztf~ED0f{TkJC^$VAJ2k<0|%v)Pt9gB%liE{8Aoqvv-kqioc?+VV=|5Q9Srda@1CK& zNP9Rp_%@QepcLAV)HSQ+M1z$uwTl5}!?v8MchUPi^Jx*lJ!2%p5_t6IYH|KpU`uEg zgbmVs;e^ZBNUCK0GFleJC{p&~>4h6k=(7*E0;o$U68^P5ZA`6jb+ec``mGWU-OoMw zvmlGc_2dsDm!H#e2hTd5$-~|dWwu6hc-uWA$}JLQcfPQ>bK;(R3M|hBkc_Z+oc#Tj zxFWeFV*Oo#TRNY|kMIDC;>C+W1@*gEk}FmLCh$&71kIz-N;H2YjC%C zOYa&sfzOUpuOIrSz=`xy;^XvZqbk;W-?hI@J9o6tzhr7#iS;7TwsF!AXHk6NGM3JS zib}FJ`Lpmz-^Zlla$ijD;Z3^?Zke`zU@L-ufBFOG!)o^NE3KHV-TM%Je4!wZD((8S z#KtYLD<2Z9Flt;m+K7BRiAKX)3^cQd<7+oBfDzkp zo)nC)@Yc|sE!mqS5mx%%H1Bz#MP>i1{fKjsQ)+h^RF; zQ#2V4bQ}lkB!?XCrJlMs-ZD;T5LIwH*!0xWx|%TI68o0fNJxjzO*WyOqK>6GQ}0xS zE6Q(|dUG^3W#h$gvP5dSv{-Av%|GB$8!E){GfS2hS>EU8*6eany!b(G zPF#H`hw`v^8pQweNh#n0@6XT9#r2(}B=0k%fH#A|Ex7a}5}L)srxIER+WCZa@r{VV zm3ORK;bRBEOa#rHVWNqW1p#MYR2{!S`E9inc$cx#Ch|}bJT)Q}mL35=OPH`Ck^@d} zC(=$U55OfJq(5}$TJiJ*8nDO>cS(eABt|L`F@&)ld58jJU zV7$L3vtSbZHKE{wAM=j-M;);?BH*8=dm5!jAg&8w+>#}CqA{0}gZBfx-D2+@OLnj` z%bw1y;ftTCXn(&?6)5C3XE0sf)-Ve$>={;wNJ2Eo**o6z^si~&_@ z4<|_BE*OLrDOL;vUl(Ixcl?HWh-A+ZOo`M!(~ty_j6Eao z0-=;V!1pvIH*wgfn!sRDNLjzC2V^?#82761cd!ik6pAtkt0vhXF&2!|{rz3Jksf5S z4jbS{P?)Hv2BLmFn5P;_0OOim5T}&hU_Zo|MG7QJ5jI=qR+laM+r`t3aBTM6Apjwz z^)J&~($gO`Tm`v7BvyN&CM%}a7I+r%LKs|)*3ozM*0CH;Y`mprFOp9Vmwp3x3MQ-Y z#-pZ#)jQ~x{YlXgr~ATZ3+26i{*|$4H!uDUZGPmig*>Bob;_Vao^A;j>=@z=X8}Vo zCXPxP?~TFl@MbTh{f!dlWSH?Y>E?kp+NAcqtJ9hcapk2n6-kz`B~Js`A#{- z_=SF9rh_oX7-GFSC%mEH!#kqG>;rcHnhf;&Hc~u?ePA+HRtdwg%R|29A6^)@fYt0Co?N zNDQ-gu04DMIBf$gQWyFG?QYz8TC9i+yw#qf^jHVuYkBc{b)za=bO#o2gx6=Wfapc! zvAGsh_iJ(MGwl{mU8LGALSeIr$%CR|Uc-@|V-ID@EZ-ulV#7@Io;RtA27U(37JF;A7XG)R@>^$sTf*R<%wh z{JSeb@GQ@sI;5{c=z|ix$@@AToSV*Ty074#?I2enGzk(i3swaEq&=B!cmKkw6CNzp z8VJtrEUrL?X9hF0?|6`cb=^yZ$5{V+AmV8|xTwD@rZppO$OayaUtwo0SYs z@V;JC>97iu8x=g<$Z0GW>^8Moyvm@8UJI{Uh&nONTL`N(A4&|EwG>{m^;SpwwNS$* z_8_a-pRy%hdDnFi5MGvfD7~yjS`~gwo!!zD%>yQBKzYr<93CJW@aq6O>%3%*H8p)X z`~LIa4*zI5x!G^*Ue9&i>_fJ_+Nu2y*SY0&oyY_os$)jD=Yb*Eh|cfm#=9?nge(iYEQ#oql|3wNjeRKuXNON^|3`$42aE>fPH^7GO7D zs*Ak=^AuM1(!HVsG~3mM!6*UqNvwrQACVZzRM}Y}!+0WAwj3VYJ+Y@lha}%z$ud+W zdMSJ{J2&ttio@%8ozPo&%5KA;=9+Ovkv0lJh3Jz(_8H2oRIY?lv>Z2#K$Il{I10@_ zHpIoBJmK8r=*PRlZ!CpnV^ zUX^u9h1KOoO7S1pfp=N=mZ%;{8}Dqiy$lKWkmFL(E^fBhtL<%rP_mC;Sz^Cx6q-2w zZSG($D=i^hQaAYsGj^D({dJtK`*7=VYI=(`_h^8QF5jEw&CY?#c<7&5jDx`(%x%k& z)R5!=*OmpImV8QCD#LP3$jiSofFd*vazD}q-o8hI82N#mf5TfG7L{-923BOV&9?@> zIv)@4u?Gfq!}H!>aDj@dq&p@}x;5I#Wk}jg3)s;W5YiBm2B#HCMh!Yo1i68eaa)_1 z5N9Qd1#*0gsQ;p3WYr+VVC9r9c?~|ni+&B9o@lV;P>+ndCFaw`+Qw!2e59z4hC*(f zw5A6*hohS6xFrRb*qz>E=Iin*?`)_;?;8i|HART*P9hEis2X%(S|DDQ-uJ zV_#rfnRq=os7HmTIho1becN$oiiUnglH}7;8Bhz7ai457BnwHN7dY$hxF%Jnx^6^& z<(Ug(6DZb`a8J#_M3gzze;9&~SEM+kA(XypqAkx&Po4Eh=JgAtfu-k<77Z}AGav?i^^<16 zDJM!s8!Ta1v^w%zHTOA{mq>|?(q>fUy+Z9`qt%Ok(s1cb3_rI zeEfMR(I*$KcED~hfmjWJ)NJk0IDnXH`2l1)@<6Pq1UMY}%sAC^*F~OMg8bokaINH^ zFNj&Hd`i#M?}a?zi)gN%SZIY)=F7Oo@&ntCkf&5fxFj2ArUlWqP;EPK{`XQMFAp%A zlmZsJh4x4}D-}zGZliD``?+7C zu`}E-x9L~V2IP2UnT6$x#1pt+{tM0TiAl5O94UePhn7Wx+RvVX>$i$T-Qp$S~(Mqh)q_HepgNjek$IJ>4QCdfiO^8p7pMFwqYJ9RBOh3uh z&eezyv}?4Tz;iVB&Uo7*4)bs50iwL8!zkQxCtzm zpw0Q(X6hI=rMz$Uj6MwTRhU+FKWJ~qatxFx3@e# zQmeQon`GM}jxpjn`A-lEBv<)CNeD@1nHI?XsKFf^6i0Ue`)6Okv@{o*8&kyoVZ@mH z&<2p)w@Whwy_u?so19%^oKjr|k1E`ZA&hb^-yeKSV(VklrN8>c8@^HsEUI&O|@Xhfk)#G#RlRB2c zWX-V3KZd~ax)%Sip}6g31iyM|4>r`Y)oK?}5%uM(2xJjcFZDIsPDMEj*a!oED|9d7 zoR+X#)?!dT*MAzqPh`!TASWx?Y5OJ|w%0(^VOFbbg4y|~uX7ACj#Sit?&cmHWPF3U z$PN}d{mh0RhO|@PVgq1hNPk)1>gFTF$sgG)v08cDgpPunO3me~&-3n{(5ug!jHI>_ z_7Wu;O$^T=tAW4xRt;f*nhbcA60e60$qR|5Arl7%`u^7Zf=$524o9A7rXo)v|DbTA ztheCmB{;G0Yngp@ir3RlAz%(gdC}PMovqL83_EQJACSUVTxn>`1$6->a!TS$BqFmH z1x3xedtvW#3QC5FmQwrch;Y1+hi(XG!6i0xpw$37;L#i&r5eZiQs@t>h)WTY_#|-1 z!3m*DS5I%Iz_PM|J{eE-Zlu&8S8XE**;1l$@9AL|?gJ6NoXo1O9bb5E2fp8`k^;Kj zGUvjHNP^aXL){Ex zIQv$9ese@h7f7YmN$@n^e+Pz;FB_BGI2QMHrSy>IbmJ1R!twMb|` zc3Of5+PnlCJ*r!WU@ZEuHDA#S>A$Sud`iLDgLEGQ{+w^J^*|AS?Z^SXO(Pd~$9(x@DT7-^7y=1wcR;<+t6d$fK%OjyLZ{;N*Yg^h<{N>ER{fmOJLN=}K8)Prj>@{1v?VN7-oeAxp^7PcXFeWF!I<;~* zpOkQu1sG~V`;dkRGi)AgY`9s|hbO?@bFB zVQGv8_j&yko*V8EPh0I=3d4fP*wPl0bj&#WO1U5oU*_DdC}_Kk$7=*sm&H9TCNOg< zyb;I8fpLwBCa9<|b(a~slBx1IRR$bmmjHKNUwNV7I@jq7OiNx_Gnc^u#;&doeCUh) zdlOpzo6_Kb^`CP31`)_dC#vQl>5-M5DB?wwhg$H&hf*@N*#flYsVF1gj+PRWc{c&z z_Qk;Mv~{KP(Tmvuoz(kEY2=pLlfA#Q>m;Zp0e|yzMMdfluQtB+oHPXKgC3raXxX|r z{41-X9}WAd^L~mx=~4qQ<#}OtI|kYXxd+LJR2(_M&Lbv3-Tr-dS%Z5ljUhB)RYDoN zp&6v0P2VaEoN#0K)?)_(u@wtfpIk0SnzVw-dZi_v;_61XTqMDG`j7cB(1oObmPu+3 zAALf{UO3T?Z+d;b$Vxi1WZA z&W?`MTppsm+)UrcJ39l&F|=r2c4)pnApDz!z>u&%U84eRsRf2u7CS^B);0FgC{bSX zr9|z7ajdnJ60<4m-5~&U7s59&G9Mi1pEI#iWo|^5C+(m9wTQqc?ombLK36+RQxo{@_2=3;%EC~`ewme9VTQm-Wg=Sif}#^RC4QjlGCn!2 zmkGt6gd;~;{nzZ>QFTeuj2J9MlCe7Nh0WgUeKovinNkcN%q-an&D|gI&nkn5a_hHK zDTml7ruM|n@%c=8ebL zMO|0$jOO!*n~FkNp_cktLpBb?iR!5P1DQ9EF4zYbTu4aoK45^bx*f;8us3A$WD8#G zT+01#1@oNQLG=f1pQ1!=c8_l8?|$g4w)6P?EINAD3D=+Bl0 z>u0H#F*aYvPe7@OJpIrrA%`z!DgLpuZfjp)%vV24$LS<4ve_bWrvx#zgz-Ag!B%I) z>D}(&ssg$hrUC$3RHGxU_Aw#pGC3jSxh=z-5L=H7P&(m8C=Jjhtgh?$M$87R|70@A zhUw&Try-PN?{}WIqJNCib5HYei%cM**%kAEaGH*v8HURF6H3GZ#ExBe>ccT>o^T|F zPybg9_~YiM|MPz5>m z%kMjHkDv?pHh2F5v&R#bU&XUwj&ru$Y|42==2NkA)%A|3+6;niI)NGPr0qunW}QBa z=fyl8iTxnI(_YxS!GgXBQ_d^c-17gP$HV?rHqRwi=y!Rdh=%+Qn}hthq{*;q_fa+( zrIYF?4D8wx>gw_uNWWqKaFZuR0c0y@Cw$SoRFH{B5@cYUU#;@5{iyxuudm_{FDM1@ zSNHO7ga4KN-PqWAs(<7A&CSP8zW*xv@YQGdH^)3D(EMEw@?YfN)>bq|rS>zgv%3yn>Tlak2g*zQL{R!Gj0ie0_42L@#?M(SABeCLnV* z;ej@~-E4X@OE0gAs5w}Rwl+4OtZ!{>ZA3?KYAXJLC0;2_4&}K`X36`EUx_>PiE;K{xu|lAU8os{bWWnEjtp)>Vx&Wa98s!A2!wk?yZT^-F z3VegG*%=#0r}9&yi9OGGe4f2eXgoa0=$trUq|-ELHJ$tff;@EpJP}xfmQz4dYz%3J zJ%HslDN>k$X*T0*$IejO)P)z_==kvY$(znmH|iZnuZ|9X>Fss*q6eL0cz)1|-t>cdxzuxN| z{2V=liU)^4)jc3*fO>KmVT%G*uX~KpUUrXmU%+GMS#Q60@+-{K^WMn;K#iUQ)K2uO zb9B<%eZAi~ie9}w0*SN>9oPf72fc&mN6<|7W%uBu4b8&4sQU{%M8_{W`}^3|H(z&N z!vKz;rK4!~@YSzJy`Nv4L@y5a_qy=%Sr_`&dA8r>wqRVl`<>p)RPqR6BCQ`lL)kFp}~_vvPz-Iht|XQwjm1FJ6j zeRa%v2kZDU_fi#INjlS<_iFGvOmito5WRnqRmRiWYY+AJqoMg)Z{@5E^UvM6k%v6 zS+il%nK#pR(vD7GOC4OT2_OlXffyY*&$5vL=*?9Et# zLQOeH*eWXl3J?MyUU_P0Xg%bN6brz1oDQx$uxRK^r6`q)r)VDru9FiEicVxM8}#kOdpHha<3Ln{dVQ8pRfs5v0E*FTqhPmk4M zh|aK81*8!If-;3U(5N#?zS+E^HiA*vUAQ-kn1PQ^NqTo*4Wkn?D`JbnGxfGl^)w)|le z@x&vxYrrQ|Ed;Q7Y@Oo(<1bq8iqm6+30_we?F}!pLdF9Pu~sO0L^eu9WD1L*{CDBl z6pa7{Q1A_j8+_n^hEYSHvILN`wrHTjbfJ0=lfeTC8QxCDEOCdwx_Rgbk@BN>bjy&n zCbmF|?$tN9@?DLuN8uU&hIcvDqVA0})Px4Y+qWl~HK}kbs@Tk_#9Jn(Q>O^+2&xF* zl{!TP`RP)U9^Hb11a(14lWsvt0+r)2$i%8lTz)5#cJ)jPZQ)K0(_yu+795KvNZD65 z()P&JtM05Qyj2!rYtv!UErl&-#hq3IMVP_1!nMpbYRphz0f%ObRx?!7a(&a7I$rCi zmJbY7CEmP4@_v(+*N(CBM7~S`huDNSWXXSb0wR6$yYEC@$y!bWSBR+JSMsiJT?owf zpMz|c{nPAw7wY7J{Y01eHJeA{IdE>kIm;&7G02MBXaA#>DS4aiR5-50nRyZ~G9$HS zM*F`7{uSD=Ck5<+`kg0xt||)n;=na-PR?OS?8JPxIf|zs7cD6sEGZ($Zg~R2bx{7h zPDi6Cy_{gm9_rrE>T+&&!V}y|Buy=C-krdvp&#L0us@;t#q(@-p28tyV)qDX4TuEI zSBTmyIwJZm5qF0lZ{Q+{^!XSu!Jx6KVR&BzS#eKvH$sE z|MPEU|HGy?uyT_^n%;EqA!rDsF9v{ogGiojJ%3xNZK6qNK`Me9C`Q4N%Hq z|7kVYh>9BUuVC7nAJ*Ikj`)HV0XWbbIn}8EiTQKNAm$|?CuT1f+5{~!@F|7A*r3R- zs?3UP>mFlCEChiym#`>n=_G`o-LfonBW;!obqpq;{El8VxZK5lh<}va@YlWH|Lb&y ztGT!_9NIbVPDt%#Qr9-kD@rV+--!#3iI7J(*&Ia(0fuN>GA~QZqlWeH+x(|S^sSt# zimq``7u4ic_0Bhl*|t`ira^*7orF>-^SwUV4R+GrTbf$W6lCpWC0CcRv-axWk*#y! zk^5lk2IM1^I~hYVc;ydQS%K(EitWktox{ftE#j3?eK0BXXD2$pE%0v6mHXmJ}( zjCt|a9G`KA+JS>qPLwUn8`s4!G41F$QOa{#ZEacbjBd)uct7u3&*Ak>z<$zcJv1P! zDS&X+q}9N1U1`fl$)Z;54>J!2(Uu%i*&(b3)Bvz(XV&I`67gm#Etyy>|h6MulG!Gi5DdOZ(e8X$t{{gb1=q6UN+9 zj6SQJz^He#MF_P@3LLv7ee{~fVS(d|h8UQcH5a@wPgZX#=I=fvB#?LdeIodNAATW# ze*aU26{MUkd?$Z@iW<>>c_eBM1xp)`ONm!^Bg4ogdlwQ+Nk@@1`}5hTs+)8&FXRh(lQ)c#chw1;!19iMw4guwK3z(H~G_V?CJK6=U~yyM|9Db@cz7B<6o^pB~`hZe_HaW(jeGk>OZ5>e=li7<=_7)Mzd4n zU#&tVRpn^J`CTMOI16@3ayWNgwgttV{a4EobHsZ!n$I;i#%R!+^X%VZH2Sb|_HQs6 zl^o;j-(habsO(>JH1QNFo406m`MCKuQ4w z+R<)2!CNCKNuq>Cb4V5padbJ$=F?~hBOaiPljneQ8NeWUpS`OHvd~i~OfMV}4O#?J z5C9}9)!tFhSG}Wb^Q9;~Owe{n%sG89S~cWsc^56&dyNqg5g5?O?kv|JL}q0SacMfH zT`296%)N}#VisX8VW5TgX)K-exR2rr?_D|oyhQUUoEgY~5?Jw|(4hwMDyko&-5ZWR z40lC6idLn zV&wNw76ky>{!I|Dj_Ex{=(b#}@&ZIE4wr3K@TOa^28-6j64ZKF;x#M+>ZL)7K;Vnn zcuIbCgHc|!KzxsEnf`(UNr)KWOu?sb@v8ZBt*#&TL)^=+6QF9cJa4fq%X@dO#H78K zfer}FRt7tl1sSzuiAt}Ufpkj3@mItO%R`>Ns>eziAH;(zR4_^r9L0em!#zBMAm?ZCkvdLTB*l%UN6KhD4CQtoxih#_D9{tFV!ro6^m0M=NHUth&ma_I;J(skm@jRQ59&ebyNic7Y?XUKcs?6!%Vd4=Z*W~3H>Gh!}57fdPOqMgOMWkddItmM|=J5tHa$FExG|W)5nbCn{!m4%wQF# zlUku7=%Cq$gQjYugKze;%kHF@k-;p^*6~UI@Y&x0OO}#r!?0fNcW|sdv7v4U?p59F zp32`H5AT+{I~JJ2Ns`PQnsB-syV_kmNn;J!#F!=^%QAX+39;EB=2jGkOy!KOn^INkFvx>&86jO}#6ljK9u zTyx_I(=C@l{Q*#77<_Bx4O}(itN}gP7IntIvVzLxtuYlCVFhBDq5Wb$83Z(l{g)sb zBtvU4m^DmbuzwdSuWu0B66kX(83tQ9na*&t~{@}(C&6#XNeOcJD`yl z#ZmQ}0GVPZ9QAHYSH6|{ZXZif4SOG`<_~_HUsjKM;g~s0^>L9+joHLd71g&9Vg%4W z4uHCy4lz1Kant;ud_}m>2ZJq#=+WlYvU0%iL!OU(a_3um@fmL-#ZT9{7^-3LX!zsf zolxhZ{2_~8gr(d>BZ*~Xi-xM~7%#cl)M+|H;dpdo+#Gq-DZA%Qm%vzu{CdTG& z7haJoAU3$}%du-+kEyyPH}3lTj;0nb*tJ1O3fm5GEf|*g<$Nb7@Rk0$c0U(9rS#YMCXBqKxot4#kERE$p2ths znHWEr=H#ybe}(xNjT*%<{99rOCVLc0RAgO1%YrhJj~6Y0dWS=2xxM}W=E*z>1=S8N z1|SIy1$sUO*6hln22X^)ZRyV#B37Gx}`W>6AT z-7DeIWMgy{58lCnco0L;=%+zC8_aQS@?j$X4_&D&m8FkACeELt4Id43X(@RLm$PgA z$4&%mgAK#rhbt*c+SY`QaWfcjVvR0t>3Ki>D4ni(p`mc8w zfF5Cc0rp~yb$v%cD=8<3tB-8ThC~%3SnBG&i_Aq@BZDkaV?CVpSxE72gF`EBwd=3$3_wbz(>7(+IkgB3X7=ov*|Xn!(e|q#*-j5K zK&P`7|9Bga#JxbfT*>zpZ>#DzYO5Ld6n@^;UDkY_Dv#|3@0XQW;~r{8sKL)BmO4d?`yD|c)V4lMYu_xhWZt65)b){%T zD%BzCI8jr(uj7LT0}jT)NE6^M=E*3Xq~e(v=)JHUI|g(XC8i)tE|YR$zK{?f(>(@L z7pRCpU8`z7NsDsPL*qRWT?)(opsrq|W~#q#w49>~i3c_?&z0YT(t768u?2@JD5Naw zFBEtxvV-SC%1k)sW9Vnd;T1g#Lqtm|2ueL{2s#EH2&K(U=)P`(jao!_eO8g-E{Z`0 zPRoGDG2c3dh=>BM|FuRF?wfof7;69NPxpZb+l7CK87uRMd<-N|3_D9CN^24gbC{pC zU?|EMh;eKSrS4|rGF@X@5jEM$j?|~Y_h6|8sPU)h`MB@o3wU8(SOGHD(}>SB`HRRs z?43!v;$_RwHmD2Xc@bZwv*pZ*PLUZ|-Ake}!bNQq8iIbJG@w?1NHs>wjy;oUsq@L@ zY&qah;E?iADkNvJaFV5M5(d7bSA$TXC(h@BfjVyjB6?0N$1J{rgSgv6*C4&UO@G3X zQIetr6F%|^cbQ+LbEE`r=qoS9*7S#6J)q2%sYjn^!5X!KkC9_paA`di*`^dGY9>hA zsMq`73PqrU+>4$jlXy8L1DEO1&c7rF)uVhlRwuqhMtU$HW$a*34J@h`= z;hiaIl#HO)m8Fs&Hkz~`=rGh4%f?-HtHpzSwnt8d%{IE02Hn+pdUk$5lkaruh9%(F z7`4i)dV@+wG$t-i!;pldb;jg<-!v$-==cTBY{b_nUsMbd-Xuzzwwbv`ZJ+nCJzPb9 zV{IrSIBja=gLe007k*xBW zRuXtq4(lP%phY=*)EAaMLca^+u|fW~g(IQIVaZ9;!5Xfe1@J#(U^m6}v|H*5#dvPo zBIR<HB#=%8U*L+A^parS|s)~uooG@dpa09ALDFlVh5Eh=`c0@bMN+l7r{ z*RruV>!z@sQsNL)08^4{WWADjZ1)6GT^#)l7lT~7rtv6o)hVf6e~}aOc}8gySY>S^ z=kB)yzUqT~D?wCja^mCREW5vbt4iPMX>L2qP`#L^fN0V#0(Ge_eC1l)!Bs%Vvn#HB+)d_=XJ{3dXg)1a5bIbPJ~v}cVf9LsKydl=)>jdS!=Y}yzI)LZ*giIZm)2x zg85WfUA^X4dlO*4HAW3*;XIngF)}4N?6ACM0>dlj$uTRKisWxg416IObUCjJ4!q$W zZraCHGQ8c!pJy8zaAk9{mewTidCVfjlp zGH@Q3X57w3>!>3>sa_)p?#Hk&m)f5V6DY-S?}Ae9doQTOLt3i>@C;LHMst>R;|G%yR(Tk zbH(3VE}AFsyQ|iy0@3OptBJFy{C;gC`+6(ON>NePg^;eGst)NO*n5b~s4`|Mzv!F0 z@Q9h*Xm7gp>fMS)w@V4i`l(X+iXRQ-((qk>e*5~~e_=JO@53HKJ{P*RpU!KVk({LrF z-9M2V6$n-`q&g8^A+zc#-~(G_ZN_wC4CX&~n}j*nf4eObW_{HT+1cC%Y1CNDSF07) zNYkb_QB8W`I{5h`gN!+XzqrKD(DaRbjvek(NRJ)A*jByy~bO}9@wu$@v-NNVgB^6QrEDpEc=3C$07T~bz21ptjE}7w_q35&bxYt_S{`Q`=(t*`) z?=w2qGG|xys}(3dzgMjQ;5*d@$Ja{DfX)sUPI+h6DeL~zd@X&wQjRRmFkgHgH^UXW zpJx9+fU4J*0u@f?K^3>vmknXH*IG7y71Hjl=^D6>bD3v^b;0qUH|uY1sef6|PwZxJ z)~Um4HC4Apz>Oq6VQ1F!P~yfn5&NY3tK3cs5N%;_wb@w-0vqx=311dBgXP!uI58YY`#;CSB*-q@W}SW=?~>pe z{T94QSaI}%!o)w(uK_6D&brdkuPLT9n&sEcf)a>%Y7#$OD|SWpP^HnTp-I-;N^p|; z=F@VMs5))ydh1i`n>p+A0RNWUa2-2*Ykqh`?)vsx+>Ytlbm9{S9Lpf;%u(n;m4QOa zSF5$p@`&rgXj4gDlBEC#3j!e>sR9G@g zijpQxVw6fT!g3Vf1C>q5kK0;qs@fqO(~h$2Z1ewb^Z#%2|8M{MSI+;>^zYM?|F6vd z|9E$A_ko}Pe{bi(Hvj)N|Nl1s|2F^sHvj)`GXFm ie1wA4?^Yn9m_%khyPj;a-~ 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�|h-|zj*;OI+c?*30G=$_$! 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 Date: Fri, 8 Jan 2021 16:19:09 +0800 Subject: [PATCH 0788/1221] Release v3.8 --- CHANGELOG.md | 2 +- bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbc9a029b9..fe8852c300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.8.0-dev8 +# v3.8.0 ### Added diff --git a/bot.py b/bot.py index eff4000aca..4641e2dfc2 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.8.0-dev8" +__version__ = "3.8.0" import asyncio From bb812252eaa9dc6dbedbcb3049a76d8086288dcd Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Fri, 8 Jan 2021 16:20:47 +0800 Subject: [PATCH 0789/1221] Linting & Changelog --- CHANGELOG.md | 1 + core/thread.py | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe8852c300..5db65bcb35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Sending files in threads (non-images) now work. ([GH #2926](https://github.com/kyb3r/modmail/issues/2926)) - Deleting messages no longer shows a false error. ([GH #2910](https://github.com/kyb3r/modmail/issues/2910), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2910#issuecomment-753557313)) - Display an error on [Lottie](https://airbnb.io/lottie/#/) stickers, instead of failing the send. +- `?perms get` now shows role/user names. ([PR #2927](https://github.com/kyb3r/modmail/pull/2927)) ### Internal diff --git a/core/thread.py b/core/thread.py index aed4275abc..75dbda5739 100644 --- a/core/thread.py +++ b/core/thread.py @@ -898,11 +898,14 @@ async def send( if is_image_url(url, convert_size=False) ] images.extend(image_urls) - images.extend(( - str(i.image_url) if isinstance(i.image_url, discord.Asset) else i.image_url, - f"{i.name} Sticker", - True - ) for i in message.stickers) + images.extend( + ( + str(i.image_url) if isinstance(i.image_url, discord.Asset) else i.image_url, + f"{i.name} Sticker", + True, + ) + for i in message.stickers + ) embedded_image = False @@ -912,13 +915,15 @@ async def send( additional_count = 1 for url, filename, is_sticker in images: - if not prioritize_uploads or ((url is None or is_image_url(url)) and not embedded_image and filename): + if not prioritize_uploads or ( + (url is None or is_image_url(url)) and not embedded_image and filename + ): if url is not None: embed.set_image(url=url) if filename: if is_sticker: if url is None: - description = 'Unable to retrieve sticker image' + description = "Unable to retrieve sticker image" else: description = "\u200b" embed.add_field(name=filename, value=description) From 68fa13aabe1e2b7a821ed1e85f79f596e38c471a Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sun, 10 Jan 2021 22:23:25 +0800 Subject: [PATCH 0790/1221] Fix bug when sending multiple images at once. --- core/thread.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/thread.py b/core/thread.py index 75dbda5739..fcaeb025f1 100644 --- a/core/thread.py +++ b/core/thread.py @@ -915,9 +915,9 @@ async def send( additional_count = 1 for url, filename, is_sticker in images: - if not prioritize_uploads or ( - (url is None or is_image_url(url)) and not embedded_image and filename - ): + if ( + not prioritize_uploads or ((url is None or is_image_url(url)) and filename) + ) and not embedded_image: if url is not None: embed.set_image(url=url) if filename: @@ -930,7 +930,7 @@ async def send( else: embed.add_field(name="Image", value=f"[{filename}]({url})") embedded_image = True - elif filename is not None: + else: if note: color = self.bot.main_color elif from_mod: @@ -940,11 +940,11 @@ async def send( img_embed = discord.Embed(color=color) - if url is None: + if url is not None: img_embed.set_image(url=url) img_embed.url = url - - img_embed.title = filename + if filename is not None: + img_embed.title = filename 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)) From 8d72b79cec09ec93d8f1e8b49358907f69801761 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sun, 10 Jan 2021 22:25:43 +0800 Subject: [PATCH 0791/1221] Fix error when reacting on confirm thread creation message. --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index fcaeb025f1..9baa0c921c 100644 --- a/core/thread.py +++ b/core/thread.py @@ -658,7 +658,7 @@ async def delete_message( await asyncio.gather(*tasks) async def find_linked_message_from_dm(self, message, either_direction=False): - if either_direction and message.embeds: + if either_direction and message.embeds and message.embeds[0].author.url: compare_url = message.embeds[0].author.url compare_id = compare_url.split("#")[-1] else: From d19adcfab8588af2fd325d2122064e4d27885caf Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Mon, 11 Jan 2021 16:52:37 +0800 Subject: [PATCH 0792/1221] Update changelog --- CHANGELOG.md | 8 ++++++++ bot.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db65bcb35..4407575641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.8.1 + +### Fixed + +- Additional image uploads now render properly. ([PR #2933](https://github.com/kyb3r/modmail/pull/2933))) +- `confirm_thread_creation` no longer raises unnecessary errors. ([GH #2931](https://github.com/kyb3r/modmail/issues/2931), [PR #2933](https://github.com/kyb3r/modmail/pull/2933)) + + # v3.8.0 ### Added diff --git a/bot.py b/bot.py index 4641e2dfc2..3b2d228505 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.8.0" +__version__ = "3.8.1" import asyncio From 80010ca5704f22f711060f65f97e2de50eecb62f Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Mon, 11 Jan 2021 16:56:31 +0800 Subject: [PATCH 0793/1221] Autotriggers no longer sends attachments back. resolves #2932 --- CHANGELOG.md | 1 + core/models.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4407575641..cf57a18a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Additional image uploads now render properly. ([PR #2933](https://github.com/kyb3r/modmail/pull/2933))) - `confirm_thread_creation` no longer raises unnecessary errors. ([GH #2931](https://github.com/kyb3r/modmail/issues/2931), [PR #2933](https://github.com/kyb3r/modmail/pull/2933)) +- Autotriggers no longer sends attachments back. ([GH #2932](https://github.com/kyb3r/modmail/issues/2932)) # v3.8.0 diff --git a/core/models.py b/core/models.py index 66965a6d88..2f71f0f202 100644 --- a/core/models.py +++ b/core/models.py @@ -227,6 +227,7 @@ class DummyMessage: """ def __init__(self, message): + message.attachments = [] self._message = message def __getattr__(self, name: str): From 2efd4a50b857ca90af045ef0b22bf9521bd655b4 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Wed, 13 Jan 2021 19:33:55 +0800 Subject: [PATCH 0794/1221] Fix `logs` command without argument in thread channel when the recipient is not cached. --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 29a3c79df4..ee109420af 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -680,7 +680,7 @@ async def logs(self, ctx, *, user: User = None): thread = ctx.thread if not thread: raise commands.MissingRequiredArgument(SimpleNamespace(name="member")) - user = thread.recipient + user = thread.recipient or await self.bot.fetch_user(thread.id) default_avatar = "https://cdn.discordapp.com/embed/avatars/0.png" icon_url = getattr(user, "avatar_url", default_avatar) From 232accba33a700b48cda451980774fc7fe188cc5 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Wed, 13 Jan 2021 19:44:18 +0800 Subject: [PATCH 0795/1221] Fix error raised when recipient is not cached and reacts to reactions in DM channel. --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 3b2d228505..54006ad8fc 100644 --- a/bot.py +++ b/bot.py @@ -1153,7 +1153,7 @@ async def on_typing(self, channel, user, _): async def handle_reaction_events(self, payload): user = self.get_user(payload.user_id) - if user.bot: + if user is None or user.bot: return channel = self.get_channel(payload.channel_id) From cc170010782d7436f2198efddb01c08d5e010e7b Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Thu, 14 Jan 2021 15:20:59 +0800 Subject: [PATCH 0796/1221] Fixed bug with update notifiations --- bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 3b2d228505..0986880004 100644 --- a/bot.py +++ b/bot.py @@ -1519,7 +1519,7 @@ async def autoupdate(self): ) logger.info("Bot has been updated.") channel = self.log_channel - if self.bot.config["update_notifications"]: + if self.config["update_notifications"]: await channel.send(embed=embed) else: try: @@ -1548,7 +1548,7 @@ async def autoupdate(self): embed.set_footer( text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" ) - if self.bot.config["update_notifications"]: + if self.config["update_notifications"]: await channel.send(embed=embed) else: embed = discord.Embed( @@ -1559,7 +1559,7 @@ async def autoupdate(self): embed.set_footer( text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" ) - if self.bot.config["update_notifications"]: + if self.config["update_notifications"]: await channel.send(embed=embed) await self.logout() From d00b562a8f34d4943272cc3e499b1c7c10f57e62 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Thu, 14 Jan 2021 15:26:51 +0800 Subject: [PATCH 0797/1221] Retry with diff name if channel cant be created resolves #2934 --- CHANGELOG.md | 10 ++++++++-- bot.py | 2 +- core/thread.py | 33 +++++++++++++++++++++------------ core/utils.py | 5 ++++- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf57a18a7f..ad59acb814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.8.2 + +### Fixed + +- Retry with `null-discrim` if channel could not be created. ([GH #2934](https://github.com/kyb3r/modmail/issues/2934)) +- Fix update notifications. + # v3.8.1 ### Fixed -- Additional image uploads now render properly. ([PR #2933](https://github.com/kyb3r/modmail/pull/2933))) +- Additional image uploads now render properly. ([PR #2933](https://github.com/kyb3r/modmail/pull/2933)) - `confirm_thread_creation` no longer raises unnecessary errors. ([GH #2931](https://github.com/kyb3r/modmail/issues/2931), [PR #2933](https://github.com/kyb3r/modmail/pull/2933)) - Autotriggers no longer sends attachments back. ([GH #2932](https://github.com/kyb3r/modmail/issues/2932)) - # v3.8.0 ### Added diff --git a/bot.py b/bot.py index 0986880004..bd9f7eff1e 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.8.1" +__version__ = "3.8.2" import asyncio diff --git a/core/thread.py b/core/thread.py index 9baa0c921c..17eaaa34dc 100644 --- a/core/thread.py +++ b/core/thread.py @@ -125,18 +125,27 @@ async def setup(self, *, creator=None, category=None, initial_message=None): overwrites=overwrites, reason="Creating a thread channel.", ) - 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) - - embed = discord.Embed(color=self.bot.error_color) - embed.title = "Error while trying to create a thread." - embed.description = str(e) - embed.add_field(name="Recipient", value=recipient.mention) - - if self.bot.log_channel is not None: - await self.bot.log_channel.send(embed=embed) - return + except discord.HTTPException as e: + # try again but null-discrim (name could be banned) + try: + channel = await self.bot.modmail_guild.create_text_channel( + name=format_channel_name(recipient, self.bot.modmail_guild, force_null=True), + category=category, + overwrites=overwrites, + reason="Creating a thread channel.", + ) + 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) + + embed = discord.Embed(color=self.bot.error_color) + embed.title = "Error while trying to create a thread." + embed.description = str(e) + embed.add_field(name="Recipient", value=recipient.mention) + + if self.bot.log_channel is not None: + await self.bot.log_channel.send(embed=embed) + return self._channel = channel diff --git a/core/utils.py b/core/utils.py index 6803b97d1e..b34d253baf 100644 --- a/core/utils.py +++ b/core/utils.py @@ -339,9 +339,12 @@ def escape_code_block(text): return re.sub(r"```", "`\u200b``", text) -def format_channel_name(author, guild, exclude_channel=None): +def format_channel_name(author, guild, exclude_channel=None, force_null=False): """Sanitises a username for use with text channel names""" name = author.name.lower() + if force_null: + name = "null" + name = new_name = ( "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" ) + f"-{author.discriminator}" From 0ab4eafff7cdea072225a893896714037a8ecd7e Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Thu, 14 Jan 2021 15:31:19 +0800 Subject: [PATCH 0798/1221] Retrieve user from Discord API if user has left the server, #2935 #2936 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad59acb814..bf81e46645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Retry with `null-discrim` if channel could not be created. ([GH #2934](https://github.com/kyb3r/modmail/issues/2934)) - Fix update notifications. +- Retrieve user from Discord API if user has left the server, resolving issues in `?block`. ([GH #2935](https://github.com/kyb3r/modmail/issues/2935), [PR #2936](https://github.com/kyb3r/modmail/pull/2936)) # v3.8.1 From 9a8f61e7b1272472861562634eb19c278e3e13f3 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Thu, 14 Jan 2021 15:32:44 +0800 Subject: [PATCH 0799/1221] IDs in `` commands work now. --- CHANGELOG.md | 1 + core/utils.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf81e46645..e32f44d20f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Retry with `null-discrim` if channel could not be created. ([GH #2934](https://github.com/kyb3r/modmail/issues/2934)) - Fix update notifications. - Retrieve user from Discord API if user has left the server, resolving issues in `?block`. ([GH #2935](https://github.com/kyb3r/modmail/issues/2935), [PR #2936](https://github.com/kyb3r/modmail/pull/2936)) +- IDs in `` commands work now. # v3.8.1 diff --git a/core/utils.py b/core/utils.py index b34d253baf..5f88c21e80 100644 --- a/core/utils.py +++ b/core/utils.py @@ -47,7 +47,7 @@ def strtobool(val): raise -class User(commands.IDConverter): +class User(commands.MemberConverter): """ A custom discord.py `Converter` that supports `Member`, `User`, and string ID's. From a56f9479272bc6ce2a9e202663dfda9f89896ae4 Mon Sep 17 00:00:00 2001 From: Ayam Dobhal Date: Sun, 24 Jan 2021 19:04:12 +0530 Subject: [PATCH 0800/1221] update version in discord version error string --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index fc0a607068..37148cb2bc 100644 --- a/bot.py +++ b/bot.py @@ -1590,7 +1590,7 @@ def main(): # check discord version if discord.__version__ != "1.6.0": logger.error( - "Dependencies are not updated, run pipenv install. discord.py version expected 1.5.2, recieved %s", + "Dependencies are not updated, run pipenv install. discord.py version expected 1.6.0, recieved %s", discord.__version__, ) sys.exit(0) From 8b893bd12fed422811d7b784ee2a9a5b6da811aa Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Mon, 1 Feb 2021 16:09:04 +0800 Subject: [PATCH 0801/1221] Hotfix: Corrupted data no longer saved to thread cache --- CHANGELOG.md | 8 ++++++++ core/thread.py | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e32f44d20f..d1d6209f17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.8.3 + +This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 where users were not properly cached. + +### Fixed + +- Corrupted data is no longer saved to thread cache. + # v3.8.2 ### Fixed diff --git a/core/thread.py b/core/thread.py index 17eaaa34dc..ca6d255c6a 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1127,7 +1127,9 @@ async def find( ) if channel: thread = Thread(self, recipient or recipient_id, channel) - self.cache[recipient_id] = thread + if thread.recipient: + # only save if data is valid + self.cache[recipient_id] = thread thread.ready = True return thread From 308d6694156c1e182332b15fe1ec044f6bb5388e Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Mon, 1 Feb 2021 16:10:35 +0800 Subject: [PATCH 0802/1221] Bump version --- README.md | 2 +- bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5741f1db6e..74e37838dc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 37148cb2bc..447d41c6ec 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.8.2" +__version__ = "3.8.3" import asyncio From d81e67b9c3fe3efdbcfe98134aa4df48eed0711b Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Tue, 2 Feb 2021 20:00:00 +0800 Subject: [PATCH 0803/1221] 3.8.4 - Another fix attempt --- CHANGELOG.md | 2 +- bot.py | 2 +- core/thread.py | 10 +++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1d6209f17..ebf7ecaa3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.8.3 +# v3.8.4 This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 where users were not properly cached. diff --git a/bot.py b/bot.py index 447d41c6ec..aa19d7b3d1 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.8.3" +__version__ = "3.8.4" import asyncio diff --git a/core/thread.py b/core/thread.py index ca6d255c6a..18acc88c7a 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1092,7 +1092,7 @@ async def find( ) -> typing.Optional[Thread]: """Finds a thread from cache or from discord channel topics.""" if recipient is None and channel is not None: - thread = self._find_from_channel(channel) + thread = await 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) @@ -1133,7 +1133,7 @@ async def find( thread.ready = True return thread - def _find_from_channel(self, channel): + async def _find_from_channel(self, channel): """ Tries to find a thread from a channel channel topic, if channel topic doesnt exist for some reason, falls back to @@ -1151,7 +1151,11 @@ def _find_from_channel(self, channel): if user_id in self.cache: return self.cache[user_id] - recipient = self.bot.get_user(user_id) + try: + recipient = self.bot.get_user(user_id) or await self.bot.fetch_user(user_id) + except discord.NotFound: + recipient = None + if recipient is None: self.cache[user_id] = thread = Thread(self, user_id, channel) else: From 7b2e63d1dac132162dc4a9e4c9c62dcc604b839a Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Tue, 2 Feb 2021 20:06:30 +0800 Subject: [PATCH 0804/1221] Improved cache saving methods --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 18acc88c7a..d534ef352e 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1157,7 +1157,7 @@ async def _find_from_channel(self, channel): recipient = None if recipient is None: - self.cache[user_id] = thread = Thread(self, user_id, channel) + thread = Thread(self, user_id, channel) else: self.cache[user_id] = thread = Thread(self, recipient, channel) thread.ready = True From 4054c510f8694372173ff99eb48cde43c359490a Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Tue, 2 Feb 2021 21:24:40 +0800 Subject: [PATCH 0805/1221] Ability to disable mention on thread creation. --- cogs/utility.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 65b3ef00f5..a89442464a 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -674,20 +674,41 @@ async def ping(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) - async def mention(self, ctx, *mention: Union[discord.Role, discord.Member]): + async def mention(self, ctx, *mention: Union[discord.Role, discord.Member, str]): """ Change what the bot mentions at the start of each thread. Type only `{prefix}mention` to retrieve your current "mention" message. + `{prefix}mention disable` to disable mention. + `{prefix}mention reset` to reset it to default value. """ - # TODO: ability to disable mention. current = self.bot.config["mention"] - if not mention: embed = discord.Embed( title="Current mention:", color=self.bot.main_color, description=str(current) ) + elif ( + len(mention) == 1 + and isinstance(mention[0], str) + and mention[0] in ["disable", "reset"] + ): + option = mention[0] + if option == "disable": + embed = discord.Embed( + description=f"Disabled mention on thread creation.", + color=self.bot.main_color, + ) + self.bot.config["mention"] = None + else: + embed = discord.Embed( + description="`mention` had been reset to default.", color=self.bot.main_color, + ) + self.bot.config.remove("mention") + await self.bot.config.update() else: + for m in mention: + if not isinstance(m, (discord.Role, discord.Member)): + raise commands.BadArgument(f'Role or Member "{m}" not found.') mention = " ".join(i.mention for i in mention) embed = discord.Embed( title="Changed mention!", From 15a38bcfc11958291c24f36d3d9cbde44d032e3d Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Tue, 2 Feb 2021 22:08:35 +0800 Subject: [PATCH 0806/1221] Update thread move message in case `mention` was set to disable/None. --- cogs/modmail.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index ee109420af..fee8e0b5fe 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -341,7 +341,11 @@ async def move(self, ctx, *, arguments): if self.bot.config["thread_move_notify_mods"]: mention = self.bot.config["mention"] - await thread.channel.send(f"{mention}, thread has been moved.") + if mention is not None: + msg = f"{mention}, thread has been moved." + else: + msg = "Thread has been moved." + await thread.channel.send(msg) sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) From 0bfb02e01256f3c0a8b5b4ab872aa6a72e1a95b3 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Tue, 2 Feb 2021 22:16:43 +0800 Subject: [PATCH 0807/1221] Formatting... --- cogs/utility.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index a89442464a..0428f079d9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -695,8 +695,7 @@ async def mention(self, ctx, *mention: Union[discord.Role, discord.Member, str]) option = mention[0] if option == "disable": embed = discord.Embed( - description=f"Disabled mention on thread creation.", - color=self.bot.main_color, + description=f"Disabled mention on thread creation.", color=self.bot.main_color, ) self.bot.config["mention"] = None else: From 270483fd65898b46433b7e4052476c276f1c590f Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Wed, 3 Feb 2021 08:26:19 +0800 Subject: [PATCH 0808/1221] Apply suggestions from code review Co-authored-by: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> --- cogs/utility.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 0428f079d9..f4c490b846 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -690,9 +690,9 @@ async def mention(self, ctx, *mention: Union[discord.Role, discord.Member, str]) elif ( len(mention) == 1 and isinstance(mention[0], str) - and mention[0] in ["disable", "reset"] + and mention[0].lower() in ["disable", "reset"] ): - option = mention[0] + option = mention[0].lower() if option == "disable": embed = discord.Embed( description=f"Disabled mention on thread creation.", color=self.bot.main_color, @@ -700,7 +700,7 @@ async def mention(self, ctx, *mention: Union[discord.Role, discord.Member, str]) self.bot.config["mention"] = None else: embed = discord.Embed( - description="`mention` had been reset to default.", color=self.bot.main_color, + description="`mention` is reset to default.", color=self.bot.main_color, ) self.bot.config.remove("mention") await self.bot.config.update() From c5a48f0cf9e28131ea36e8307bb35e7ee3bc2e45 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sun, 28 Feb 2021 21:56:08 +0800 Subject: [PATCH 0809/1221] Fix typo in 'config_help.json' (#2957) --- core/config_help.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config_help.json b/core/config_help.json index e6bf3730ab..271e726346 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -706,7 +706,7 @@ "`{prefix}config set confirm_thread_creation yes`" ], "notes": [ - "See also: `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny``" + "See also: `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny`" ] }, "confirm_thread_creation_title": { From 049508fd944b0fbbbb57b8001044ca1a652a6f04 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sun, 28 Feb 2021 14:59:15 +0100 Subject: [PATCH 0810/1221] Update README.md (#2955) fix a sentence which said to join the old deleted plugin server --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74e37838dc..b5dfc9b4aa 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ You can find a list of third-party plugins using the `?plugins registry` comman To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). -Plugins requests and support is available in our [Modmail Plugins Server](https://discord.gg/4JE4XSW). +Plugins requests and support are available in our [Modmail Support Server](https://discord.gg/j5e9p8w). ## Contributing From 907b10d466b5df16d6438c6735088c4d65cdf098 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 7 Mar 2021 22:53:56 +1000 Subject: [PATCH 0811/1221] Add msglink command to get DM message URLs. --- cogs/modmail.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cogs/modmail.py b/cogs/modmail.py index ee109420af..2392916fea 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -601,6 +601,18 @@ async def sfw(self, ctx): sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) + @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def loglink(self, ctx, message_id: int): + """Retrieves the link to a message in the current thread.""" + message = await ctx.thread.recipient.fetch_message(message_id) + if not message: + embed = discord.Embed(color=self.bot.main_color, description="Message no longer exists.") + else: + embed = discord.Embed(color=self.bot.main_color, description=message.jump_url) + await ctx.send(embed=embed) + @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() From 5482bed00fab2da73c1fae694271acd682967dca Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 7 Mar 2021 22:54:16 +1000 Subject: [PATCH 0812/1221] Add DM channel ID to genesis message footer. --- core/thread.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index d534ef352e..98f67ca897 100644 --- a/core/thread.py +++ b/core/thread.py @@ -300,7 +300,11 @@ def _format_info_embed(self, user, log_url, log_count, color): # embed.add_field(name='Mention', value=user.mention) # embed.add_field(name='Registered', value=created + days(created)) - footer = "User ID: " + str(user.id) + if user.dm_channel: + footer = f"User ID: {user.id} • DM ID: {user.dm_channel}" + else: + footer = f"User ID: {user.id}" + embed.set_author(name=str(user), icon_url=user.avatar_url, url=log_url) # embed.set_thumbnail(url=avi) From 784aa4f950b2b6fe9e275860c03fa961ff1abd84 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Sun, 7 Mar 2021 23:07:54 +1000 Subject: [PATCH 0813/1221] Amend typo and black formatting. --- cogs/modmail.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 2392916fea..d9965243be 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -604,11 +604,13 @@ async def sfw(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def loglink(self, ctx, message_id: int): + async def msglink(self, ctx, message_id: int): """Retrieves the link to a message in the current thread.""" message = await ctx.thread.recipient.fetch_message(message_id) if not message: - embed = discord.Embed(color=self.bot.main_color, description="Message no longer exists.") + embed = discord.Embed( + color=self.bot.main_color, description="Message no longer exists." + ) else: embed = discord.Embed(color=self.bot.main_color, description=message.jump_url) await ctx.send(embed=embed) From e0956c48061ed164af3ae3953589f9cba1e56cb7 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 7 Mar 2021 23:11:20 +0800 Subject: [PATCH 0814/1221] Improve messages --- cogs/modmail.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index d9965243be..f0ee2815c8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -606,13 +606,16 @@ async def sfw(self, ctx): @checks.thread_only() async def msglink(self, ctx, message_id: int): """Retrieves the link to a message in the current thread.""" - message = await ctx.thread.recipient.fetch_message(message_id) - if not message: + try: + message = await ctx.thread.recipient.fetch_message(message_id) + except discord.NotFound: embed = discord.Embed( - color=self.bot.main_color, description="Message no longer exists." + color=self.bot.error_color, description="Message not found or no longer exists." ) else: - embed = discord.Embed(color=self.bot.main_color, description=message.jump_url) + embed = discord.Embed( + color=self.bot.main_color, description=message.jump_url + ) await ctx.send(embed=embed) @commands.command() From 9d9a609b1f8a521161bf483eb5a8e7ab7f5594e4 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 7 Mar 2021 23:14:00 +0800 Subject: [PATCH 0815/1221] Changelog update --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf7ecaa3e..da576ec027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.8.5 + +This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 where users were not properly cached. + +### Added + +- `?msglink `, allows you to obtain channel + message ID for T&S reports. ([GH #2963](https://github.com/kyb3r/modmail/issues/2963), [PR #2964](https://github.com/kyb3r/modmail/pull/2964)) + +### Fixed + +- Non-master/development branch deployments no longer cause erros to be raised. + # v3.8.4 This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 where users were not properly cached. From 41d5593d0370bece803628cb79bfd1a54809277d Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 7 Mar 2021 23:14:49 +0800 Subject: [PATCH 0816/1221] Non-master/development branch deployments no longer cause erros to be raised. --- CHANGELOG.md | 4 +--- core/changelog.py | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da576ec027..83603ac880 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,15 +8,13 @@ however, insignificant breaking changes do not guarantee a major version bump, s # v3.8.5 -This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 where users were not properly cached. - ### Added - `?msglink `, allows you to obtain channel + message ID for T&S reports. ([GH #2963](https://github.com/kyb3r/modmail/issues/2963), [PR #2964](https://github.com/kyb3r/modmail/pull/2964)) ### Fixed -- Non-master/development branch deployments no longer cause erros to be raised. +- Non-master/development branch deployments no longer cause errors to be raised. # v3.8.4 diff --git a/core/changelog.py b/core/changelog.py index ada9fe6984..878a19b26c 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -180,6 +180,9 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": if not branch or err: branch = "master" if not bot.version.is_prerelease else "development" + if branch not in ("master", "development"): + branch = "master" + url = url or f"https://raw.githubusercontent.com/kyb3r/modmail/{branch}/CHANGELOG.md" async with await bot.session.get(url) as resp: From 3c42c22e34ebb5d439a0af36553db027f66b0a24 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 7 Mar 2021 23:16:12 +0800 Subject: [PATCH 0817/1221] Update changelog --- CHANGELOG.md | 1 + bot.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83603ac880..0ceeeeedf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Added - `?msglink `, allows you to obtain channel + message ID for T&S reports. ([GH #2963](https://github.com/kyb3r/modmail/issues/2963), [PR #2964](https://github.com/kyb3r/modmail/pull/2964)) +- `?mention disable/reset`, disables or resets mention on thread creation. ([PR #2951](https://github.com/kyb3r/modmail/pull/2951)) ### Fixed diff --git a/bot.py b/bot.py index aa19d7b3d1..bc68584f58 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.8.4" +__version__ = "3.8.5" import asyncio From baa7e5fb677e35433586efe0cd63e97d95aa55c4 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 7 Mar 2021 23:44:25 +0800 Subject: [PATCH 0818/1221] Fix bug where autotriggers are in dm context, resolve #2961 --- CHANGELOG.md | 1 + bot.py | 3 ++- cogs/modmail.py | 4 +--- cogs/utility.py | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ceeeeedf0..0af5009464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed - Non-master/development branch deployments no longer cause errors to be raised. +- Autotriggers now can search for roles/channels in guild context. ([GH #2961](https://github.com/kyb3r/modmail/issues/2961)) # v3.8.4 diff --git a/bot.py b/bot.py index bc68584f58..1b474e40dd 100644 --- a/bot.py +++ b/bot.py @@ -939,6 +939,7 @@ async def get_contexts(self, message, *, cls=commands.Context): async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context): message.author = self.modmail_guild.me message.channel = channel + message.guild = channel.guild view = StringView(message.content) ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) @@ -967,7 +968,7 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) ctxs = [] aliases = normalize_alias(alias) if not aliases: - logger.warning("Alias %s is invalid as called in automove.", invoker) + logger.warning("Alias %s is invalid as called in autotrigger.", invoker) for alias in aliases: view = StringView(invoked_prefix + alias) diff --git a/cogs/modmail.py b/cogs/modmail.py index 9a659a1934..cbc6e4c2a8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -617,9 +617,7 @@ async def msglink(self, ctx, message_id: int): color=self.bot.error_color, description="Message not found or no longer exists." ) else: - embed = discord.Embed( - color=self.bot.main_color, description=message.jump_url - ) + embed = discord.Embed(color=self.bot.main_color, description=message.jump_url) await ctx.send(embed=embed) @commands.command() diff --git a/cogs/utility.py b/cogs/utility.py index f4c490b846..8663e70e73 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1798,7 +1798,6 @@ async def autotrigger_edit(self, ctx, keyword, *, command): split_cmd = command.split(" ") for n in range(1, len(split_cmd) + 1): if self.bot.get_command(" ".join(split_cmd[0:n])): - print(self.bot.get_command(" ".join(split_cmd[0:n]))) valid = True break From d24501bc2550de890b5389b8debc000bccad13b1 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Mon, 8 Mar 2021 02:28:01 +1000 Subject: [PATCH 0819/1221] Use the ID attribute of DM Channel. Originally used the channel instance by mistake, causing the `__str__` to be used instead of the intended ID. --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 98f67ca897..cd2797ac67 100644 --- a/core/thread.py +++ b/core/thread.py @@ -301,7 +301,7 @@ def _format_info_embed(self, user, log_url, log_count, color): # embed.add_field(name='Registered', value=created + days(created)) if user.dm_channel: - footer = f"User ID: {user.id} • DM ID: {user.dm_channel}" + footer = f"User ID: {user.id} • DM ID: {user.dm_channel.id}" else: footer = f"User ID: {user.id}" From 93b027000b3b775806ffb13c8fe189ea9ffe819e Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Thu, 11 Mar 2021 21:59:45 +0800 Subject: [PATCH 0820/1221] Confirm thread creation when user opens a thread using react to contact. --- cogs/modmail.py | 7 ++++++- core/thread.py | 15 ++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index cbc6e4c2a8..29552b7ca0 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1027,7 +1027,12 @@ async def contact( await ctx.channel.send(embed=embed, delete_after=3) else: - thread = await self.bot.threads.create(user, creator=ctx.author, category=category) + thread = await self.bot.threads.create( + recipient=user, + creator=ctx.author, + category=category, + manual_trigger=manual_trigger, + ) if self.bot.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): logger.info("Contacting user %s when Modmail DM is disabled.", user) diff --git a/core/thread.py b/core/thread.py index 98f67ca897..e08ff3e5a9 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1175,6 +1175,7 @@ async def create( message: discord.Message = None, creator: typing.Union[discord.Member, discord.User] = None, category: discord.CategoryChannel = None, + manual_trigger: bool = True, ) -> Thread: """Creates a Modmail thread""" @@ -1215,8 +1216,12 @@ async def create( self.bot.config.set("fallback_category_id", category.id) await self.bot.config.update() - if message and self.bot.config["confirm_thread_creation"]: - confirm = await message.channel.send( + if (message or not manual_trigger) and self.bot.config["confirm_thread_creation"]: + if not manual_trigger: + destination = recipient + else: + destination = message.channel + confirm = await destination.send( embed=discord.Embed( title=self.bot.config["confirm_thread_creation_title"], description=self.bot.config["confirm_thread_response"], @@ -1231,7 +1236,7 @@ async def create( try: r, _ = await self.bot.wait_for( "reaction_add", - check=lambda r, u: u.id == message.author.id + check=lambda r, u: u.id == recipient.id and r.message.id == confirm.id and r.message.channel.id == confirm.channel.id and str(r.emoji) in (accept_emoji, deny_emoji), @@ -1243,7 +1248,7 @@ async def create( await confirm.remove_reaction(accept_emoji, self.bot.user) await asyncio.sleep(0.2) await confirm.remove_reaction(deny_emoji, self.bot.user) - await message.channel.send( + await destination.send( embed=discord.Embed( title="Cancelled", description="Timed out", color=self.bot.error_color ) @@ -1257,7 +1262,7 @@ async def create( await confirm.remove_reaction(accept_emoji, self.bot.user) await asyncio.sleep(0.2) await confirm.remove_reaction(deny_emoji, self.bot.user) - await message.channel.send( + await destination.send( embed=discord.Embed(title="Cancelled", color=self.bot.error_color) ) del self.cache[recipient.id] From df41d314f6c1ec7200454c47df0b5d3063b8f2a2 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Thu, 11 Mar 2021 22:30:08 +0800 Subject: [PATCH 0821/1221] Block new thread from react to contact when Modmail DM is disabled (new or all). --- bot.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 1b474e40dd..06e5a62483 100644 --- a/bot.py +++ b/bot.py @@ -1238,6 +1238,26 @@ async def on_raw_reaction_add(self, payload): if not member.bot: message = await channel.fetch_message(payload.message_id) await message.remove_reaction(payload.emoji, member) + await message.add_reaction(emoji_fmt) # bot adds as well + + if self.config["dm_disabled"] in ( + DMDisabled.NEW_THREADS, + DMDisabled.ALL_THREADS, + ): + embed = discord.Embed( + title=self.config["disabled_new_thread_title"], + color=self.error_color, + description=self.config["disabled_new_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_new_thread_footer"], + icon_url=self.guild.icon_url, + ) + logger.info( + "A new thread using react to contact was blocked from %s due to disabled Modmail.", + message.author, + ) + return await member.send(embed=embed) ctx = await self.get_context(message) ctx.author = member @@ -1245,8 +1265,6 @@ async def on_raw_reaction_add(self, payload): self.get_command("contact"), user=member, manual_trigger=False ) - await message.add_reaction(emoji_fmt) # bot adds as well - async def on_raw_reaction_remove(self, payload): if self.config["transfer_reactions"]: await self.handle_reaction_events(payload) From fd6dfb7deeec7bf8463434c8c6d71bd984bdb644 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Thu, 11 Mar 2021 22:39:34 +0800 Subject: [PATCH 0822/1221] Bot will not attempt to find for linked message when the recipient reacts to reactions on confirm thread creation message. --- bot.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bot.py b/bot.py index 06e5a62483..9a33922283 100644 --- a/bot.py +++ b/bot.py @@ -1188,6 +1188,14 @@ async def handle_reaction_events(self, payload): # the reacted message is the corresponding thread creation embed # closing thread return await thread.close(closer=user) + if ( + message.author == self.user + and message.embeds + and self.config.get("confirm_thread_creation") + and message.embeds[0].title == self.config["confirm_thread_creation_title"] + and message.embeds[0].description == self.config["confirm_thread_response"] + ): + return if not thread.recipient.dm_channel: await thread.recipient.create_dm() try: From ccc8e6f89ca202e2950d692c18be57cb735cb58b Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Thu, 11 Mar 2021 23:03:48 +0800 Subject: [PATCH 0823/1221] Fix previous commit. --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 9a33922283..dd1008ae54 100644 --- a/bot.py +++ b/bot.py @@ -1263,7 +1263,7 @@ async def on_raw_reaction_add(self, payload): ) logger.info( "A new thread using react to contact was blocked from %s due to disabled Modmail.", - message.author, + member, ) return await member.send(embed=embed) From 69a272e6734fd38e50323bafebe748b823bb5cce Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sat, 13 Mar 2021 11:03:42 +0800 Subject: [PATCH 0824/1221] Fix a few bad refs and typos --- bot.py | 4 ++-- cogs/modmail.py | 1 - cogs/plugins.py | 14 +++++++------- cogs/utility.py | 1 - core/config.py | 2 +- core/models.py | 3 +-- core/thread.py | 2 +- core/utils.py | 3 ++- 8 files changed, 14 insertions(+), 16 deletions(-) diff --git a/bot.py b/bot.py index 1b474e40dd..3112895225 100644 --- a/bot.py +++ b/bot.py @@ -46,7 +46,7 @@ ) from core.thread import ThreadManager from core.time import human_timedelta -from core.utils import human_join, normalize_alias, truncate +from core.utils import normalize_alias, truncate logger = getLogger(__name__) @@ -1591,7 +1591,7 @@ def main(): # check discord version if discord.__version__ != "1.6.0": logger.error( - "Dependencies are not updated, run pipenv install. discord.py version expected 1.6.0, recieved %s", + "Dependencies are not updated, run pipenv install. discord.py version expected 1.6.0, received %s", discord.__version__, ) sys.exit(0) diff --git a/cogs/modmail.py b/cogs/modmail.py index cbc6e4c2a8..64d6a2475c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,5 +1,4 @@ import asyncio -from operator import truediv import re from datetime import datetime from itertools import zip_longest diff --git a/cogs/plugins.py b/cogs/plugins.py index ce21551831..1f7aa869d7 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -178,14 +178,14 @@ async def download_plugin(self, plugin, force=False): if raw == "Not Found": raise InvalidPluginError("Plugin not found") else: - raise InvalidPluginError("Invalid download recieved, non-bytes object") + raise InvalidPluginError("Invalid download received, non-bytes object") - plugin_io = io.BytesIO(raw) - if not plugin.cache_path.parent.exists(): - plugin.cache_path.parent.mkdir(parents=True) + plugin_io = io.BytesIO(raw) + if not plugin.cache_path.parent.exists(): + plugin.cache_path.parent.mkdir(parents=True) - with plugin.cache_path.open("wb") as f: - f.write(raw) + with plugin.cache_path.open("wb") as f: + f.write(raw) with zipfile.ZipFile(plugin_io) as zipf: for info in zipf.infolist(): @@ -253,7 +253,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): description="Plugins are disabled, enable them by setting `ENABLE_PLUGINS=true`", color=self.bot.main_color, ) - await ctx.send(embed=em) + await ctx.send(embed=embed) return if not self._ready_event.is_set(): diff --git a/cogs/utility.py b/cogs/utility.py index 8663e70e73..8ce7af5b69 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -3,7 +3,6 @@ import os import random import re -from sys import stdout import traceback from contextlib import redirect_stdout from datetime import datetime diff --git a/core/config.py b/core/config.py index 2c88b10d14..14197e112e 100644 --- a/core/config.py +++ b/core/config.py @@ -14,7 +14,7 @@ from core._color_data import ALL_COLORS from core.models import DMDisabled, InvalidConfigError, Default, getLogger from core.time import UserFriendlyTimeSync -from core.utils import strtobool, tryint +from core.utils import strtobool logger = getLogger(__name__) load_dotenv() diff --git a/core/models.py b/core/models.py index 2f71f0f202..24236e6279 100644 --- a/core/models.py +++ b/core/models.py @@ -190,14 +190,13 @@ def get_value(self, key, args, kwds): except KeyError: return "{" + key + "}" else: - return Formatter.get_value(key, args, kwds) + return super().get_value(key, args, kwds) class SimilarCategoryConverter(commands.CategoryChannelConverter): async def convert(self, ctx, argument): bot = ctx.bot guild = ctx.guild - result = None try: return await super().convert(ctx, argument) diff --git a/core/thread.py b/core/thread.py index 98f67ca897..069d2eb8e0 100644 --- a/core/thread.py +++ b/core/thread.py @@ -2,9 +2,9 @@ import copy import io import re +import time import typing from datetime import datetime, timedelta -import time from types import SimpleNamespace import isodate diff --git a/core/utils.py b/core/utils.py index 5f88c21e80..8f4b152ff5 100644 --- a/core/utils.py +++ b/core/utils.py @@ -30,6 +30,7 @@ "escape_code_block", "format_channel_name", "tryint", + "match_title", ] @@ -222,7 +223,7 @@ def cleanup_code(content: str) -> str: def match_title(text: str) -> int: """ - Matches a title in the foramt of "Title: XXXX" + Matches a title in the format of "Title: XXXX" Parameters ---------- From 695baa511df5a1b8de6b5860927e228cb9cbb32c Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sat, 13 Mar 2021 11:34:49 +0800 Subject: [PATCH 0825/1221] Add local plugins --- .gitignore | 2 -- cogs/plugins.py | 80 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 463016ca95..37094114b7 100644 --- a/.gitignore +++ b/.gitignore @@ -131,8 +131,6 @@ node_modules/ # Modmail config.json -plugins/ -!plugins/registry.json temp/ test.py stack.yml diff --git a/cogs/plugins.py b/cogs/plugins.py index 1f7aa869d7..48de189bc2 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -31,16 +31,28 @@ class InvalidPluginError(commands.BadArgument): class Plugin: - def __init__(self, user, repo, name, branch=None): - self.user = user - self.repo = repo - self.name = name - self.branch = branch if branch is not None else "master" - self.url = f"https://github.com/{user}/{repo}/archive/{self.branch}.zip" - self.link = f"https://github.com/{user}/{repo}/tree/{self.branch}/{name}" + def __init__(self, user, repo=None, name=None, branch=None): + if repo is None: + self.user = "@local" + self.repo = "@local" + self.name = user + self.local = True + self.branch = "@local" + self.url = f"@local/{user}" + self.link = f"@local/{user}" + else: + self.user = user + self.repo = repo + self.name = name + self.local = False + self.branch = branch if branch is not None else "master" + self.url = f"https://github.com/{user}/{repo}/archive/{self.branch}.zip" + self.link = f"https://github.com/{user}/{repo}/tree/{self.branch}/{name}" @property def path(self): + if self.local: + return PurePath("plugins") / "@local" / self.name return PurePath("plugins") / self.user / self.repo / f"{self.name}-{self.branch}" @property @@ -49,6 +61,8 @@ def abs_path(self): @property def cache_path(self): + if self.local: + raise ValueError("No cache path for local plugins!") return ( Path(__file__).absolute().parent.parent / "temp" @@ -58,9 +72,13 @@ def cache_path(self): @property def ext_string(self): + if self.local: + return f"plugins.@local.{self.name}.{self.name}" return f"plugins.{self.user}.{self.repo}.{self.name}-{self.branch}.{self.name}" def __str__(self): + if self.local: + return f"@local/{self.name}" return f"{self.user}/{self.repo}/{self.name}@{self.branch}" def __lt__(self, other): @@ -68,10 +86,13 @@ def __lt__(self, other): @classmethod def from_string(cls, s, strict=False): - if not strict: - m = match(r"^(.+?)/(.+?)/(.+?)(?:@(.+?))?$", s) - else: - m = match(r"^(.+?)/(.+?)/(.+?)@(.+?)$", s) + m = match(r"^@local/(.+)$", s) + if m is None: + if not strict: + m = match(r"^(.+?)/(.+?)/(.+?)(?:@(.+?))?$", s) + else: + m = match(r"^(.+?)/(.+?)/(.+?)@(.+?)$", s) + if m is not None: return Plugin(*m.groups()) raise InvalidPluginError("Cannot decipher %s.", s) # pylint: disable=raising-format-tuple @@ -155,6 +176,9 @@ async def download_plugin(self, plugin, force=False): if plugin.abs_path.exists() and not force: return + if plugin.local: + raise InvalidPluginError(f"Local plugin {plugin} not found!") + plugin.abs_path.mkdir(parents=True, exist_ok=True) if plugin.cache_path.exists() and not force: @@ -290,7 +314,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): embed = discord.Embed( description="Invalid plugin name, double check the plugin name " "or use one of the following formats: " - "username/repo/plugin, username/repo/plugin@branch.", + "username/repo/plugin, username/repo/plugin@branch, @local/plugin.", color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -314,7 +338,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]`). + or a direct reference to a GitHub hosted plugin (in the format `user/repo/name[@branch]`) + or `@local/name` for local plugins. """ plugin = await self.parse_user_input(ctx, plugin_name, check_version=True) @@ -395,7 +420,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): Remove an installed plugin of the bot. `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference - to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + to a GitHub hosted plugin (in the format `user/repo/name[@branch]`) or `@local/name` for local plugins. """ plugin = await self.parse_user_input(ctx, plugin_name) if plugin is None: @@ -416,17 +441,18 @@ async def plugins_remove(self, ctx, *, plugin_name: str): self.bot.config["plugins"].remove(str(plugin)) await self.bot.config.update() - shutil.rmtree( - plugin.abs_path, - onerror=lambda *args: logger.warning( - "Failed to remove plugin files %s: %s", plugin, str(args[2]) - ), - ) - try: - plugin.abs_path.parent.rmdir() - plugin.abs_path.parent.parent.rmdir() - except OSError: - pass # dir not empty + if not plugin.local: + shutil.rmtree( + plugin.abs_path, + onerror=lambda *args: logger.warning( + "Failed to remove plugin files %s: %s", plugin, str(args[2]) + ), + ) + try: + plugin.abs_path.parent.rmdir() + plugin.abs_path.parent.parent.rmdir() + except OSError: + pass # dir not empty embed = discord.Embed( description="The plugin is successfully uninstalled.", color=self.bot.main_color @@ -477,7 +503,7 @@ async def plugins_update(self, ctx, *, plugin_name: str = None): Update a plugin for the bot. `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference - to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). + to a GitHub hosted plugin (in the format `user/repo/name[@branch]`) or `@local/name` for local plugins. To update all plugins, do `{prefix}plugins update`. """ @@ -514,7 +540,7 @@ async def plugins_reset(self, ctx): shutil.rmtree(cache_path) for entry in os.scandir(Path(__file__).absolute().parent.parent / "plugins"): - if entry.is_dir(): + if entry.is_dir() and entry.name != "@local": shutil.rmtree(entry.path) logger.warning("Removing %s.", entry.name) From 51460971a6be32d4c9b2d7d4015536abe22d754f Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sat, 13 Mar 2021 11:37:19 +0800 Subject: [PATCH 0826/1221] Add local plugin dir --- plugins/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/.gitignore diff --git a/plugins/.gitignore b/plugins/.gitignore new file mode 100644 index 0000000000..4522f5eb88 --- /dev/null +++ b/plugins/.gitignore @@ -0,0 +1,4 @@ +* +!registry.json +!@local/ +!.gitignore \ No newline at end of file From d6ba02e66aee2235a85c2c1f5cbebe5a14a3b1af Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sat, 13 Mar 2021 11:44:24 +0800 Subject: [PATCH 0827/1221] Add local plugins --- .dockerignore | 1 + .gitignore | 3 +++ plugins/.gitignore | 4 ---- plugins/@local/.gitignore | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 plugins/.gitignore create mode 100644 plugins/@local/.gitignore diff --git a/.dockerignore b/.dockerignore index bae84fb2e4..99fd4a241b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -133,6 +133,7 @@ node_modules/ config.json plugins/ !plugins/registry.json +!plugins/@local/ temp/ test.py diff --git a/.gitignore b/.gitignore index 37094114b7..635e3160e8 100644 --- a/.gitignore +++ b/.gitignore @@ -130,6 +130,9 @@ package-lock.json node_modules/ # Modmail +plugins/* +!plugins/registry.json +!plugins/@local config.json temp/ test.py diff --git a/plugins/.gitignore b/plugins/.gitignore deleted file mode 100644 index 4522f5eb88..0000000000 --- a/plugins/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!registry.json -!@local/ -!.gitignore \ No newline at end of file diff --git a/plugins/@local/.gitignore b/plugins/@local/.gitignore new file mode 100644 index 0000000000..c96a04f008 --- /dev/null +++ b/plugins/@local/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file From b6b9e6c7a4c0bb76141cf1d193f4a980a8e259ef Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sat, 13 Mar 2021 12:04:37 +0800 Subject: [PATCH 0828/1221] Updated version, deps, and plugins code --- CHANGELOG.md | 8 ++++++++ bot.py | 2 +- cogs/plugins.py | 26 ++++++++++++++++---------- pyproject.toml | 4 ++-- requirements.min.txt | 14 +++----------- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0af5009464..c065153e2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. + +# v3.8.6 + +### Added + +- Ability to install local plugins without relying on git / external sources + - Simply add your extension to plugins/@local, and use `?plugin add local/plugin-name` + # v3.8.5 ### Added diff --git a/bot.py b/bot.py index 3112895225..5efa4d521a 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.8.5" +__version__ = "3.8.6" import asyncio diff --git a/cogs/plugins.py b/cogs/plugins.py index 48de189bc2..94a3f425c0 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -86,7 +86,7 @@ def __lt__(self, other): @classmethod def from_string(cls, s, strict=False): - m = match(r"^@local/(.+)$", s) + m = match(r"^@?local/(.+)$", s) if m is None: if not strict: m = match(r"^(.+?)/(.+?)/(.+?)(?:@(.+?))?$", s) @@ -173,7 +173,7 @@ async def initial_load_plugins(self): await self.bot.config.update() async def download_plugin(self, plugin, force=False): - if plugin.abs_path.exists() and not force: + if plugin.abs_path.exists() and (not force or plugin.local): return if plugin.local: @@ -314,7 +314,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): embed = discord.Embed( description="Invalid plugin name, double check the plugin name " "or use one of the following formats: " - "username/repo/plugin, username/repo/plugin@branch, @local/plugin.", + "username/repo/plugin-name, username/repo/plugin-name@branch, local/plugin-name.", color=self.bot.error_color, ) await ctx.send(embed=embed) @@ -339,7 +339,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): `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]`) - or `@local/name` for local plugins. + or `local/name` for local plugins. """ plugin = await self.parse_user_input(ctx, plugin_name, check_version=True) @@ -360,10 +360,16 @@ async def plugins_add(self, ctx, *, plugin_name: str): ) return await ctx.send(embed=embed) - embed = discord.Embed( - description=f"Starting to download plugin from {plugin.link}...", - color=self.bot.main_color, - ) + if plugin.local: + embed = discord.Embed( + description=f"Starting to load local plugin from {plugin.link}...", + color=self.bot.main_color, + ) + else: + embed = discord.Embed( + description=f"Starting to download plugin from {plugin.link}...", + color=self.bot.main_color, + ) msg = await ctx.send(embed=embed) try: @@ -420,7 +426,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): Remove an installed plugin of 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]`) or `@local/name` for local plugins. + to a GitHub hosted plugin (in the format `user/repo/name[@branch]`) or `local/name` for local plugins. """ plugin = await self.parse_user_input(ctx, plugin_name) if plugin is None: @@ -503,7 +509,7 @@ async def plugins_update(self, ctx, *, plugin_name: str = None): Update a plugin for the bot. `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference - to a GitHub hosted plugin (in the format `user/repo/name[@branch]`) or `@local/name` for local plugins. + to a GitHub hosted plugin (in the format `user/repo/name[@branch]`) or `local/name` for local plugins. To update all plugins, do `{prefix}plugins update`. """ diff --git a/pyproject.toml b/pyproject.toml index e7c56eb749..60b86fdbc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.6.0' +version = '3.8.6' description = "Modmail is similar to Reddit's 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 = [ @@ -36,7 +36,7 @@ keywords = ['discord', 'modmail'] [tool.poetry.dependencies] python = "^3.7" -"discord.py" = "./discord.py-1.5.2.tar.gz" +"discord.py" = "discord.py==1.6.0" uvloop = {version = ">=0.12.0", markers = "sys_platform != 'win32'"} python-dotenv = ">=0.10.3" parsedatetime = "^2.6" diff --git a/requirements.min.txt b/requirements.min.txt index cf1b4bdd26..d756599661 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -1,24 +1,16 @@ -# Generated as of June, 2020 +# Generated as of March, 2021 # This is the bare minimum requirements.txt for running Modmail. # To install requirements.txt run: pip install -r requirements.min.txt aiohttp==3.6.2 -async-timeout==3.0.1 -attrs==19.3.0 -chardet==3.0.4 -./discord.py-1.5.2.tar.gz +discord.py==1.6.0 dnspython==1.16.0 emoji==0.5.4 -future==0.18.2 -idna==2.9 isodate==0.6.0 motor==2.1.0 -multidict==4.7.6 natural==0.2.0 parsedatetime==2.6 pymongo==3.10.1 python-dateutil==2.8.1 python-dotenv==0.14.0 -six==1.15.0 -websockets==8.1 -yarl==1.4.2 +websockets==8.1 \ No newline at end of file From bf1668a491205eeb0a4ef4a797bfe5bca8b4ea57 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sat, 13 Mar 2021 12:06:29 +0800 Subject: [PATCH 0829/1221] Forgot to finish sentence --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c065153e2e..6be578b8d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Added - Ability to install local plugins without relying on git / external sources - - Simply add your extension to plugins/@local, and use `?plugin add local/plugin-name` + - Simply add your extension to plugins/@local, and use `?plugin add local/plugin-name` to load the plugin as normal +- Updated deps for requirements.min.txt and pyproject.toml # v3.8.5 From dee522888bc97b28f968c365ea07ef719e7afef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9D=A5sora?= <60399731+6days9weeks@users.noreply.github.com> Date: Mon, 15 Mar 2021 17:18:09 +0900 Subject: [PATCH 0830/1221] new plugin A little plugin I was working on --- plugins/registry.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 609345c395..11ee857335 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -204,5 +204,13 @@ "title": "Countdowns", "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" + }, + "action": { + "repository": "6days9weeks/modmail-plugins", + "branch": "master", + "description": "Have fun with others by hugging them or giving them pats~!!", + "title": "Action", + "icon_url": "https://media.discordapp.net/attachments/720733784970100776/820933433579798528/689105042212388965.png", + "thumbnail_url": "https://data.whicdn.com/images/58526601/original.gif" } } From e42546a7c041e3575f7920da955b72e34e05538a Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Fri, 19 Mar 2021 20:18:00 +0800 Subject: [PATCH 0831/1221] Potentially breaking: improved plugin events --- CHANGELOG.md | 9 +++++++++ bot.py | 2 +- core/thread.py | 7 ++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be578b8d2..ca9c0ae183 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.9.0 + +### Breaking + +- `on_thread_initiate` and `on_thread_ready` events now have `thread, creator, category, initial_message` as additional arguments. + +### Internal + +- `thread.reply` now returns (msg_to_user, msg_to_thread). Can be useful in plugins. # v3.8.6 diff --git a/bot.py b/bot.py index 5efa4d521a..ce91a3d326 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.8.6" +__version__ = "3.9.0" import asyncio diff --git a/core/thread.py b/core/thread.py index 069d2eb8e0..a647219e71 100644 --- a/core/thread.py +++ b/core/thread.py @@ -105,7 +105,7 @@ def cancelled(self, flag: bool): async def setup(self, *, creator=None, category=None, initial_message=None): """Create the thread channel and other io related initialisation tasks""" - self.bot.dispatch("thread_initiate", self) + self.bot.dispatch("thread_initiate", self, creator, category, initial_message) recipient = self.recipient # in case it creates a channel outside of category @@ -258,7 +258,7 @@ async def activate_auto_triggers(): activate_auto_triggers(), send_persistent_notes(), ) - self.bot.dispatch("thread_ready", self) + self.bot.dispatch("thread_ready", self, creator, category, initial_message) def _format_info_embed(self, user, log_url, log_count, color): """Get information about a member of a server @@ -753,7 +753,7 @@ async def reply( tasks = [] try: - await self.send( + user_msg = await self.send( message, destination=self.recipient, from_mod=True, @@ -809,6 +809,7 @@ async def reply( await asyncio.gather(*tasks) self.bot.dispatch("thread_reply", self, True, message, anonymous, plain) + return (user_msg, msg) # sent_to_user, sent_to_thread_channel async def send( self, From dd23edfe4872c77f6975cda1c862e52322322b5f Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Fri, 19 Mar 2021 20:30:53 +0800 Subject: [PATCH 0832/1221] Fix bug where cancelled still attempts to start thread --- cogs/modmail.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7ec111cee5..bd89571193 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1032,6 +1032,9 @@ async def contact( category=category, manual_trigger=manual_trigger, ) + if thread.cancelled: + return + if self.bot.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): logger.info("Contacting user %s when Modmail DM is disabled.", user) From 9d4813d25e9d0d6cc2ac70d14dd63ebac043aea6 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Fri, 19 Mar 2021 20:35:58 +0800 Subject: [PATCH 0833/1221] Changelog & Ghost errors are no longer raised when threads are created using non-organic methods. --- CHANGELOG.md | 6 ++++++ core/models.py | 3 ++- core/thread.py | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca9c0ae183..d6d796246e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `on_thread_initiate` and `on_thread_ready` events now have `thread, creator, category, initial_message` as additional arguments. +### Fixed + +- `confirm_thread_creation` now properly works when a user opens a thread using react to contact. ([GH #2930](https://github.com/kyb3r/modmail/issues/2930), [PR #2971](https://github.com/kyb3r/modmail/pull/2971)) +- `?disable all/new` now disables react to contact threads. ([GH #2969](https://github.com/kyb3r/modmail/issues/2969), [PR #2971](https://github.com/kyb3r/modmail/pull/2971)) +- Ghost errors are no longer raised when threads are created using non-organic methods. + ### Internal - `thread.reply` now returns (msg_to_user, msg_to_thread). Can be useful in plugins. diff --git a/core/models.py b/core/models.py index 24236e6279..81247d387d 100644 --- a/core/models.py +++ b/core/models.py @@ -226,7 +226,8 @@ class DummyMessage: """ def __init__(self, message): - message.attachments = [] + if message: + message.attachments = [] self._message = message def __getattr__(self, name: str): diff --git a/core/thread.py b/core/thread.py index 72ce5664d4..9fe23301e4 100644 --- a/core/thread.py +++ b/core/thread.py @@ -245,8 +245,8 @@ class Author: await self.bot.api.update_note_ids(ids) async def activate_auto_triggers(): - message = DummyMessage(copy.copy(initial_message)) - if message: + if initial_message: + message = DummyMessage(copy.copy(initial_message)) try: return await self.bot.trigger_auto_triggers(message, channel) except RuntimeError: From 5efdbbe80e586838c3088f118fbe630ab85544f8 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Fri, 19 Mar 2021 21:12:50 +0800 Subject: [PATCH 0834/1221] Add reaciton menu plugin --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 11ee857335..86d424e228 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -212,5 +212,14 @@ "title": "Action", "icon_url": "https://media.discordapp.net/attachments/720733784970100776/820933433579798528/689105042212388965.png", "thumbnail_url": "https://data.whicdn.com/images/58526601/original.gif" + }, + "menu": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Adds reaction-based menus into thread creates. Check out `?configmenu`", + "title": "Menus", + "bot_version": "3.9.0", + "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", + "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" } } From b008d8a78fe94f2dde09c668cfc3bbb47982a7a4 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 21 Mar 2021 22:30:57 +0800 Subject: [PATCH 0835/1221] better run command, 3.9.1 --- CHANGELOG.md | 7 +++ bot.py | 115 ++++++++++++++++++++++++++++++++++++++----------- pyproject.toml | 2 +- 3 files changed, 98 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d796246e..c83219b367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.9.1 + +### Internal + +- `bot.run` now more gracefully handles closing, similar to how discord.py handles it. + - No longer displays `PrivilegedIntentsRequired` exception when exiting when presence intent is disabled. + # v3.9.0 ### Breaking diff --git a/bot.py b/bot.py index 28db42a453..24e7c717f7 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.9.0" +__version__ = "3.9.1" import asyncio @@ -6,6 +6,7 @@ import logging import os import re +import signal import sys import typing from datetime import datetime @@ -184,40 +185,104 @@ def db(self): async def get_prefix(self, message=None): return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] - def run(self, *args, **kwargs): + def run(self): + loop = self.loop + try: - self.loop.run_until_complete(self.start(self.token)) - except KeyboardInterrupt: + loop.add_signal_handler(signal.SIGINT, lambda: loop.stop()) + loop.add_signal_handler(signal.SIGTERM, lambda: loop.stop()) + except NotImplementedError: pass - except discord.LoginFailure: - logger.critical("Invalid token") - except discord.PrivilegedIntentsRequired: - intents = discord.Intents.default() - intents.members = True - # Try again with members intent - self._connection._intents = intents - logger.warning( - "Attempting to login with only the server members privileged intent. Some plugins might not work correctly." - ) + + async def runner(): try: - self.loop.run_until_complete(self.start(self.token)) + retry_intents = False + try: + await self.start(self.token) + except discord.PrivilegedIntentsRequired: + retry_intents = True + if retry_intents: + await self.http.close() + if self.ws is not None and self.ws.open: + await self.ws.close(code=1000) + self._ready.clear() + intents = discord.Intents.default() + intents.members = True + # Try again with members intent + self._connection._intents = intents + logger.warning( + "Attempting to login with only the server members privileged intent. Some plugins might not work correctly." + ) + await self.start(self.token) except discord.PrivilegedIntentsRequired: logger.critical( "Privileged intents are not explicitly granted in the discord developers dashboard." ) - except Exception: - logger.critical("Fatal exception", exc_info=True) - finally: - self.loop.run_until_complete(self.logout()) - for task in asyncio.all_tasks(self.loop): + except discord.LoginFailure: + logger.critical("Invalid token") + except Exception: + logger.critical("Fatal exception", exc_info=True) + finally: + if not self.is_closed(): + await self.close() + if self._session: + await self._session.close() + + def stop_loop_on_completion(f): + loop.stop() + + def _cancel_tasks(): + if sys.version_info < (3, 8): + task_retriever = asyncio.Task.all_tasks + else: + task_retriever = asyncio.all_tasks + + tasks = {t for t in task_retriever(loop=loop) if not t.done()} + + if not tasks: + return + + logger.info('Cleaning up after %d tasks.', len(tasks)) + for task in tasks: task.cancel() + + loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) + logger.info('All tasks finished cancelling.') + + for task in tasks: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler({ + 'message': 'Unhandled exception during Client.run shutdown.', + 'exception': task.exception(), + 'task': task + }) + + future = asyncio.ensure_future(runner(), loop=loop) + future.add_done_callback(stop_loop_on_completion) + try: + loop.run_forever() + except KeyboardInterrupt: + logger.info('Received signal to terminate bot and event loop.') + finally: + future.remove_done_callback(stop_loop_on_completion) + logger.info('Cleaning up tasks.') + try: - self.loop.run_until_complete(asyncio.gather(*asyncio.all_tasks(self.loop))) - except asyncio.CancelledError: - logger.debug("All pending tasks has been cancelled.") + _cancel_tasks() + if sys.version_info >= (3, 6): + loop.run_until_complete(loop.shutdown_asyncgens()) finally: - self.loop.run_until_complete(self.session.close()) - logger.error(" - Shutting down bot - ") + logger.info('Closing the event loop.') + loop.close() + + if not future.cancelled(): + try: + return future.result() + except KeyboardInterrupt: + # I am unsure why this gets raised here but suppress it anyway + return None @property def bot_owner_ids(self): diff --git a/pyproject.toml b/pyproject.toml index 60b86fdbc4..5171e27b5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.8.6' +version = '3.9.1' description = "Modmail is similar to Reddit's 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 380ea6ac04e4e285ef3b690cfa1db643ffdd1a4d Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Thu, 25 Mar 2021 16:46:51 +0100 Subject: [PATCH 0836/1221] add music plugin added music plugin on taki's request --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 86d424e228..65d91e3cf8 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -17,6 +17,15 @@ "icon_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png", "thumbnail_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png" }, + "Music": { + "repository": "Taaku18/modmail-plugins", + "branch": "master", + "description": "Play wonderfull jams through your modmail!", + "bot_version": "2.20.1", + "title": "Music", + "icon_url": "https://i.imgur.com/JmJPX5W.gif", + "thumbnail_url": "https://i.imgur.com/jrYL7F8.gif" + }, "media-only": { "repository": "lorenzo132/modmail-plugins", "branch": "master", From c434a1e70369ebfdc149779d56327b94757da068 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 26 Mar 2021 19:55:52 +0100 Subject: [PATCH 0837/1221] Update registry.json try to fix install issue --- plugins/registry.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 65d91e3cf8..515871e916 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -17,12 +17,12 @@ "icon_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png", "thumbnail_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png" }, - "Music": { + "music": { "repository": "Taaku18/modmail-plugins", "branch": "master", "description": "Play wonderfull jams through your modmail!", "bot_version": "2.20.1", - "title": "Music", + "title": "music", "icon_url": "https://i.imgur.com/JmJPX5W.gif", "thumbnail_url": "https://i.imgur.com/jrYL7F8.gif" }, From 68d8d887a83ba52f9f3c5a6c9ca1607d100b0563 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sun, 28 Mar 2021 15:44:33 +0200 Subject: [PATCH 0838/1221] Update bot.py add hosting methods: screen and systemd --- bot.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bot.py b/bot.py index 28db42a453..9d2e3f7995 100644 --- a/bot.py +++ b/bot.py @@ -108,6 +108,12 @@ def hosting_method(self) -> HostingMethod: if os.environ.get("pm_id"): return HostingMethod.PM2 + if os.environ.get("INVOCATION_ID"): + return HostingMethod.SYSTEMD + + if os.environ.get("TERM"): + return HostingMethod.SCREEN + return HostingMethod.OTHER def startup(self): From 9fedb5b3a597d3852746e925e627111612a0bca0 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sun, 28 Mar 2021 15:45:24 +0200 Subject: [PATCH 0839/1221] Update models.py add hosting methods: screen and systemd --- core/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/models.py b/core/models.py index 81247d387d..37355a488a 100644 --- a/core/models.py +++ b/core/models.py @@ -277,3 +277,5 @@ class HostingMethod(IntEnum): HEROKU = 0 PM2 = 1 OTHER = 2 + SYSTEMD = 3 + SCREEN = 4 From e3d1ab8817fc3c638e8ea969471dad726b645ef2 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:23:50 +0800 Subject: [PATCH 0840/1221] Force update dev? Weird history --- CHANGELOG.md | 7 +++ bot.py | 119 ++++++++++++++++++++++++++++++++++++++----------- pyproject.toml | 2 +- 3 files changed, 101 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d796246e..c83219b367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.9.1 + +### Internal + +- `bot.run` now more gracefully handles closing, similar to how discord.py handles it. + - No longer displays `PrivilegedIntentsRequired` exception when exiting when presence intent is disabled. + # v3.9.0 ### Breaking diff --git a/bot.py b/bot.py index 9d2e3f7995..d55971cdf8 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.9.0" +__version__ = "3.9.1" import asyncio @@ -6,6 +6,7 @@ import logging import os import re +import signal import sys import typing from datetime import datetime @@ -190,40 +191,106 @@ def db(self): async def get_prefix(self, message=None): return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] - def run(self, *args, **kwargs): + def run(self): + loop = self.loop + try: - self.loop.run_until_complete(self.start(self.token)) - except KeyboardInterrupt: + loop.add_signal_handler(signal.SIGINT, lambda: loop.stop()) + loop.add_signal_handler(signal.SIGTERM, lambda: loop.stop()) + except NotImplementedError: pass - except discord.LoginFailure: - logger.critical("Invalid token") - except discord.PrivilegedIntentsRequired: - intents = discord.Intents.default() - intents.members = True - # Try again with members intent - self._connection._intents = intents - logger.warning( - "Attempting to login with only the server members privileged intent. Some plugins might not work correctly." - ) + + async def runner(): try: - self.loop.run_until_complete(self.start(self.token)) + retry_intents = False + try: + await self.start(self.token) + except discord.PrivilegedIntentsRequired: + retry_intents = True + if retry_intents: + await self.http.close() + if self.ws is not None and self.ws.open: + await self.ws.close(code=1000) + self._ready.clear() + intents = discord.Intents.default() + intents.members = True + # Try again with members intent + self._connection._intents = intents + logger.warning( + "Attempting to login with only the server members privileged intent. Some plugins might not work correctly." + ) + await self.start(self.token) except discord.PrivilegedIntentsRequired: logger.critical( "Privileged intents are not explicitly granted in the discord developers dashboard." ) - except Exception: - logger.critical("Fatal exception", exc_info=True) - finally: - self.loop.run_until_complete(self.logout()) - for task in asyncio.all_tasks(self.loop): + except discord.LoginFailure: + logger.critical("Invalid token") + except Exception: + logger.critical("Fatal exception", exc_info=True) + finally: + if not self.is_closed(): + await self.close() + if self._session: + await self._session.close() + + def stop_loop_on_completion(f): + loop.stop() + + def _cancel_tasks(): + if sys.version_info < (3, 8): + task_retriever = asyncio.Task.all_tasks + else: + task_retriever = asyncio.all_tasks + + tasks = {t for t in task_retriever(loop=loop) if not t.done()} + + if not tasks: + return + + logger.info("Cleaning up after %d tasks.", len(tasks)) + for task in tasks: task.cancel() + + loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) + logger.info("All tasks finished cancelling.") + + for task in tasks: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "Unhandled exception during Client.run shutdown.", + "exception": task.exception(), + "task": task, + } + ) + + future = asyncio.ensure_future(runner(), loop=loop) + future.add_done_callback(stop_loop_on_completion) + try: + loop.run_forever() + except KeyboardInterrupt: + logger.info("Received signal to terminate bot and event loop.") + finally: + future.remove_done_callback(stop_loop_on_completion) + logger.info("Cleaning up tasks.") + try: - self.loop.run_until_complete(asyncio.gather(*asyncio.all_tasks(self.loop))) - except asyncio.CancelledError: - logger.debug("All pending tasks has been cancelled.") + _cancel_tasks() + if sys.version_info >= (3, 6): + loop.run_until_complete(loop.shutdown_asyncgens()) finally: - self.loop.run_until_complete(self.session.close()) - logger.error(" - Shutting down bot - ") + logger.info("Closing the event loop.") + loop.close() + + if not future.cancelled(): + try: + return future.result() + except KeyboardInterrupt: + # I am unsure why this gets raised here but suppress it anyway + return None @property def bot_owner_ids(self): @@ -1576,7 +1643,7 @@ async def autoupdate(self): elif res != "Already up to date.": logger.info("Bot has been updated.") channel = self.update_channel - if self.hosting_method == HostingMethod.PM2: + if self.hosting_method in (HostingMethod.PM2, HostingMethod.SYSTEMD): embed = discord.Embed(title="Bot has been updated", color=self.main_color) embed.set_footer( text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" diff --git a/pyproject.toml b/pyproject.toml index 60b86fdbc4..5171e27b5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.8.6' +version = '3.9.1' description = "Modmail is similar to Reddit's 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 b6e2dada58773bcc0f9c6724bec8fa16c4e1e7f3 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:31:51 +0800 Subject: [PATCH 0841/1221] Add docker hostingmethod --- CHANGELOG.md | 6 ++++++ Dockerfile | 1 + bot.py | 10 ++++++++++ core/models.py | 7 ++++--- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83219b367..86292f94a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.9.2 + +### Improved + +- Additional HostingMethods (i.e. DOCKER, PM2, SCREEN). Autoupdates are now disabled on all docker instances. ([GH #2977](https://github.com/kyb3r/modmail/issues/2977), [PR #2988](https://github.com/kyb3r/modmail/pull/2988)) + # v3.9.1 ### Internal diff --git a/Dockerfile b/Dockerfile index f616ef29f3..8165e06a53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ FROM python:3.7-alpine +ENV USING_DOCKER yes WORKDIR /modmailbot COPY . /modmailbot RUN export PIP_NO_CACHE_DIR=false \ diff --git a/bot.py b/bot.py index d55971cdf8..8fe8b6d8da 100644 --- a/bot.py +++ b/bot.py @@ -112,6 +112,12 @@ def hosting_method(self) -> HostingMethod: if os.environ.get("INVOCATION_ID"): return HostingMethod.SYSTEMD + if os.environ.get("USING_DOCKER"): + return HostingMethod.DOCKER + + if os.environ.get("INVOCATION_ID"): + return HostingMethod.SYSTEMD + if os.environ.get("TERM"): return HostingMethod.SCREEN @@ -1671,6 +1677,10 @@ async def before_autoupdate(self): logger.warning("Autoupdates disabled.") self.autoupdate_loop.cancel() + if self.hosting_method == HostingMethod.DOCKER: + logger.warning("Autoupdates disabled as using Docker.") + self.autoupdate_loop.cancel() + if not self.config.get("github_token") and self.hosting_method == HostingMethod.HEROKU: logger.warning("GitHub access token not found.") logger.warning("Autoupdates disabled.") diff --git a/core/models.py b/core/models.py index 37355a488a..0238e051d4 100644 --- a/core/models.py +++ b/core/models.py @@ -276,6 +276,7 @@ class DMDisabled(IntEnum): class HostingMethod(IntEnum): HEROKU = 0 PM2 = 1 - OTHER = 2 - SYSTEMD = 3 - SCREEN = 4 + SYSTEMD = 2 + SCREEN = 3 + DOCKER = 4 + OTHER = 5 From de36b95b159f6797ceb1b40e7da22380ecdfcff3 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sat, 20 Mar 2021 00:02:32 +0100 Subject: [PATCH 0842/1221] Update config_help.json Change disabled to enabled, as it is enabled on a new bot creation --- core/config_help.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config_help.json b/core/config_help.json index 271e726346..e764c9255d 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -86,7 +86,7 @@ "thumbnail": "https://placehold.it/100/e74c3c?text=+" }, "user_typing": { - "default": "Disabled", + "default": "Enabled", "description": "When this is set to `yes`, whenever the recipient user starts to type in their DM channel, the moderator will see “{bot.user.display_name} is typing…” in the thread channel.", "examples": [ "`{prefix}config set user_typing yes`", From 31052354db1fe61224f5c2093946a9c9509a9cf4 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:33:16 +0800 Subject: [PATCH 0843/1221] Bump versions --- CHANGELOG.md | 4 ++++ bot.py | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86292f94a3..6d2ea15f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Additional HostingMethods (i.e. DOCKER, PM2, SCREEN). Autoupdates are now disabled on all docker instances. ([GH #2977](https://github.com/kyb3r/modmail/issues/2977), [PR #2988](https://github.com/kyb3r/modmail/pull/2988)) +### Fixed + +- `user_typing` default in the config help is now correct. + # v3.9.1 ### Internal diff --git a/bot.py b/bot.py index 8fe8b6d8da..689832b73d 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.9.1" +__version__ = "3.9.2" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 5171e27b5e..3e4865bc8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.9.1' +version = '3.9.2' description = "Modmail is similar to Reddit's 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 edf636498b9d29b706f79ec10930bc3ac2b73566 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:49:46 +0800 Subject: [PATCH 0844/1221] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5dfc9b4aa..10f7baf737 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      From 20877d9ffbc87aacf3fa31a31670c8447333f621 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 31 Mar 2021 21:49:32 +0800 Subject: [PATCH 0845/1221] did i not commit this --- bot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bot.py b/bot.py index 689832b73d..d4ef7f250c 100644 --- a/bot.py +++ b/bot.py @@ -115,9 +115,6 @@ def hosting_method(self) -> HostingMethod: if os.environ.get("USING_DOCKER"): return HostingMethod.DOCKER - if os.environ.get("INVOCATION_ID"): - return HostingMethod.SYSTEMD - if os.environ.get("TERM"): return HostingMethod.SCREEN From fa6cf626fea876630f2dfad5cfa2bb019e2c8bef Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sun, 4 Apr 2021 04:22:33 +0800 Subject: [PATCH 0846/1221] Update `mention` command. --- cogs/utility.py | 45 +++++++++++++++++++++++++++++++------------ core/config_help.json | 3 ++- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 8ce7af5b69..51ed2b476d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -673,41 +673,62 @@ async def ping(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.ADMINISTRATOR) - async def mention(self, ctx, *mention: Union[discord.Role, discord.Member, str]): + async def mention(self, ctx, *user_or_role: Union[discord.Role, discord.Member, str]): """ Change what the bot mentions at the start of each thread. - Type only `{prefix}mention` to retrieve your current "mention" message. - `{prefix}mention disable` to disable mention. - `{prefix}mention reset` to reset it to default value. + `user_or_role` may be a user ID, mention, name, role ID, mention, or name. + You can also set it to mention multiple users or roles, just separate the arguments with space. + + Examples: + - `{prefix}mention @user` + - `{prefix}mention @user @role` + - `{prefix}mention 984301093849028 388218663326449` + - `{prefix}mention everyone` + + Do not ping `@everyone` to set mention to everyone, use "everyone" or "all" instead. + + Notes: + - Type only `{prefix}mention` to retrieve your current "mention" message. + - `{prefix}mention disable` to disable mention. + - `{prefix}mention reset` to reset it to default value, which is "@here". """ current = self.bot.config["mention"] - if not mention: + if not user_or_role: embed = discord.Embed( title="Current mention:", color=self.bot.main_color, description=str(current) ) elif ( - len(mention) == 1 - and isinstance(mention[0], str) - and mention[0].lower() in ["disable", "reset"] + len(user_or_role) == 1 + and isinstance(user_or_role[0], str) + and user_or_role[0].lower() in ("disable", "reset", "all", "everyone") ): - option = mention[0].lower() + option = user_or_role[0].lower() if option == "disable": embed = discord.Embed( description=f"Disabled mention on thread creation.", color=self.bot.main_color, ) self.bot.config["mention"] = None - else: + elif option == "reset": embed = discord.Embed( description="`mention` is reset to default.", color=self.bot.main_color, ) self.bot.config.remove("mention") + else: + embed = discord.Embed( + title="Changed mention!", + description=f'On thread creation the bot now says "@everyone".', + color=self.bot.main_color, + ) + self.bot.config["mention"] = "@everyone" await self.bot.config.update() else: - for m in mention: + for m in user_or_role: if not isinstance(m, (discord.Role, discord.Member)): raise commands.BadArgument(f'Role or Member "{m}" not found.') - mention = " ".join(i.mention for i in mention) + mention = " ".join( + i.mention if i is not ctx.guild.default_role else str(i) for i in user_or_role + ) embed = discord.Embed( title="Changed mention!", description=f'On thread creation the bot now says "{mention}".', diff --git a/core/config_help.json b/core/config_help.json index e764c9255d..d430b9ee4c 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -52,7 +52,8 @@ "`{prefix}mention Yo~ Here's a new thread for ya!`" ], "notes": [ - "Unfortunately, it's not currently possible to disable mention. You do not have to include a mention." + "To disable mention, use command `{prefix}mention disable`.", + "See also: `{prefix}help mention`." ] }, "main_color": { From a4389b43192edf44547e6cc6b8e0a32b15bf9aa3 Mon Sep 17 00:00:00 2001 From: Ralph Date: Mon, 5 Apr 2021 10:39:57 -0400 Subject: [PATCH 0847/1221] Add Coolguy (Prime Servers) to SPONSORS --- SPONSORS.json | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index fcabb119ac..98abe37d81 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -42,5 +42,33 @@ } ] } - } -] \ No newline at end of file + }, + { + "embed": { + "description": "Quality Hosting at Prices You Deserve!", + "color": 50195251 + "footer": { + "icon_url": "http://primeserversinc.com/images/Prime_Logo_P_Sassy.png", + "text": "Prime Servers, Inc." + }, + "thumbnail": { + "url": "http://primeserversinc.com/images/Prime_Logo_P_Sassy.png" + }, + "author": { + "name": "Prime Servers, Inc.", + "url": "https://primeserversinc.com", + "icon_url": "http://primeserversinc.com/images/Prime_Logo_P_Sassy.png" + }, + "fields": [ + { + "name": "Twitter", + "value": "[**Click Here**](https://twitter.com/PrimeServersInc)" + }, + { + "name": "Discord Server", + "value": "[**Click Here**](https://discord.gg/cYM6Urn)" + } + ] + } + }, +] From d77d47dfdecc0e20f9868c4e62ec4baee932e66d Mon Sep 17 00:00:00 2001 From: Ralph Date: Mon, 5 Apr 2021 10:41:27 -0400 Subject: [PATCH 0848/1221] Small correction --- SPONSORS.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index 98abe37d81..a57ac2d60e 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -46,7 +46,7 @@ { "embed": { "description": "Quality Hosting at Prices You Deserve!", - "color": 50195251 + "color": 50195251, "footer": { "icon_url": "http://primeserversinc.com/images/Prime_Logo_P_Sassy.png", "text": "Prime Servers, Inc." @@ -70,5 +70,5 @@ } ] } - }, + } ] From b4ac84f78fd1cff8e1b83ce88660fa1242df1429 Mon Sep 17 00:00:00 2001 From: Ralph Date: Mon, 5 Apr 2021 10:42:25 -0400 Subject: [PATCH 0849/1221] Add Coolguy (Prime Servers) to SPONSORS --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 10f7baf737..19b9eed923 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,10 @@ Special thanks to our sponsors for supporting the project. + + + + Become a sponsor on [Patreon](https://patreon.com/kyber). From 4f52b685d1659d870bff2b0d8ad3bd6cc8c32698 Mon Sep 17 00:00:00 2001 From: Ralph Date: Tue, 6 Apr 2021 16:51:21 -0400 Subject: [PATCH 0850/1221] Update to HTTPS --- SPONSORS.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index a57ac2d60e..53034c57b3 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -48,16 +48,16 @@ "description": "Quality Hosting at Prices You Deserve!", "color": 50195251, "footer": { - "icon_url": "http://primeserversinc.com/images/Prime_Logo_P_Sassy.png", + "icon_url": "https://primeserversinc.com/images/Prime_Logo_P_Sassy.png", "text": "Prime Servers, Inc." }, "thumbnail": { - "url": "http://primeserversinc.com/images/Prime_Logo_P_Sassy.png" + "url": "https://primeserversinc.com/images/Prime_Logo_P_Sassy.png" }, "author": { "name": "Prime Servers, Inc.", "url": "https://primeserversinc.com", - "icon_url": "http://primeserversinc.com/images/Prime_Logo_P_Sassy.png" + "icon_url": "https://primeserversinc.com/images/Prime_Logo_P_Sassy.png" }, "fields": [ { From 4da97a7ecb5381f9568bef0e19c59df745f2b616 Mon Sep 17 00:00:00 2001 From: Ralph Date: Tue, 6 Apr 2021 16:51:45 -0400 Subject: [PATCH 0851/1221] Update to HTTPS --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19b9eed923..a211cf22a0 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Special thanks to our sponsors for supporting the project. - + From 23065593c2b03a4eb300411b90eb04ab1266871f Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Apr 2021 21:51:22 +0800 Subject: [PATCH 0852/1221] v3.9.3 add use_user_id_channel_name --- CHANGELOG.md | 13 +++++++++++++ bot.py | 2 +- cogs/modmail.py | 2 +- core/config.py | 2 ++ core/config_help.json | 11 +++++++++++ core/thread.py | 4 ++-- core/utils.py | 20 ++++++++++++++------ pyproject.toml | 2 +- 8 files changed, 45 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2ea15f89..df4c140165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.9.3 + +## Added + +- New config: ` use_user_id_channel_name`, when set to TRUE, channel names would get created with the recipient's ID instead of their name and discriminator. + - This is now an option to better suit the needs of servers in Server Discovery + +## Internal Change + +- Signature of `format_channel_name` in core/util.py changed to: + - `format_channel_name(bot, author, exclude_channel=None, force_null=False)` + + # v3.9.2 ### Improved diff --git a/bot.py b/bot.py index d4ef7f250c..994e956e11 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.9.2" +__version__ = "3.9.3" import asyncio diff --git a/cogs/modmail.py b/cogs/modmail.py index bd89571193..b5d48d61c2 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1490,7 +1490,7 @@ async def repair(self, ctx): if len(users) == 1: user = users.pop() name = format_channel_name( - user, self.bot.modmail_guild, exclude_channel=ctx.channel + self.bot, user, exclude_channel=ctx.channel ) recipient = self.bot.get_user(user.id) if user.id in self.bot.threads.cache: diff --git a/core/config.py b/core/config.py index 14197e112e..890b3690f1 100644 --- a/core/config.py +++ b/core/config.py @@ -50,6 +50,7 @@ class ConfigManager: "sent_emoji": "✅", "blocked_emoji": "🚫", "close_emoji": "🔒", + "use_user_id_channel_name": False, "recipient_thread_close": False, "thread_auto_close_silently": False, "thread_auto_close": isodate.Duration(), @@ -157,6 +158,7 @@ class ConfigManager: time_deltas = {"account_age", "guild_age", "thread_auto_close", "thread_cooldown"} booleans = { + "use_user_id_channel_name", "user_typing", "mod_typing", "reply_without_command", diff --git a/core/config_help.json b/core/config_help.json index e764c9255d..2aef03ccf6 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -96,6 +96,17 @@ "See also: `mod_typing`." ] }, + "use_user_id_channel_name": { + "default": "No", + "description": "When this is set to `yes`, new thread channels will be named with the recipient's ID instead of the recipient's name.", + "examples": [ + "`{prefix}config set use_user_id_channel_name yes`", + "`{prefix}config set use_user_id_channel_name no`" + ], + "notes": [ + "This config is suitable for servers in Server Discovery to comply with channel name restrictions." + ] + }, "mod_typing": { "default": "Disabled", "description": "When this is set to `yes`, whenever a moderator starts to type in the thread channel, the recipient user will see \"{bot.user.display_name} is typing…\" in their DM channel.", diff --git a/core/thread.py b/core/thread.py index 9fe23301e4..7798454958 100644 --- a/core/thread.py +++ b/core/thread.py @@ -120,7 +120,7 @@ async def setup(self, *, creator=None, category=None, initial_message=None): try: channel = await self.bot.modmail_guild.create_text_channel( - name=format_channel_name(recipient, self.bot.modmail_guild), + name=format_channel_name(self.bot, recipient), category=category, overwrites=overwrites, reason="Creating a thread channel.", @@ -129,7 +129,7 @@ async def setup(self, *, creator=None, category=None, initial_message=None): # try again but null-discrim (name could be banned) try: channel = await self.bot.modmail_guild.create_text_channel( - name=format_channel_name(recipient, self.bot.modmail_guild, force_null=True), + name=format_channel_name(self.bot, recipient, force_null=True), category=category, overwrites=overwrites, reason="Creating a thread channel.", diff --git a/core/utils.py b/core/utils.py index 8f4b152ff5..af47c8e2e6 100644 --- a/core/utils.py +++ b/core/utils.py @@ -340,15 +340,23 @@ def escape_code_block(text): return re.sub(r"```", "`\u200b``", text) -def format_channel_name(author, guild, exclude_channel=None, force_null=False): +def format_channel_name(bot, author, exclude_channel=None, force_null=False): """Sanitises a username for use with text channel names""" - name = author.name.lower() + guild = bot.modmail_guild + if force_null: - name = "null" + name = new_name = "null" + else: + if bot.config["use_user_id_channel_name"]: + name = new_name = str(author.id) + else: + name = author.name.lower() + if force_null: + name = "null" - name = new_name = ( - "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" - ) + f"-{author.discriminator}" + 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) diff --git a/pyproject.toml b/pyproject.toml index 3e4865bc8c..b74dfad111 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.9.2' +version = '3.9.3' description = "Modmail is similar to Reddit's 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 a0e580ccb5db20002bdc8c5d99b506d18d8bf3e2 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Apr 2021 22:25:18 +0800 Subject: [PATCH 0853/1221] black --- cogs/modmail.py | 4 +--- core/utils.py | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index b5d48d61c2..a45da27166 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1489,9 +1489,7 @@ async def repair(self, ctx): ) if len(users) == 1: user = users.pop() - name = format_channel_name( - self.bot, user, exclude_channel=ctx.channel - ) + name = format_channel_name(self.bot, user, 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] diff --git a/core/utils.py b/core/utils.py index af47c8e2e6..2dfb319a8e 100644 --- a/core/utils.py +++ b/core/utils.py @@ -355,7 +355,8 @@ def format_channel_name(bot, author, exclude_channel=None, force_null=False): name = "null" name = 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" ) + f"-{author.discriminator}" counter = 1 From 3e4d9b019ce748eac1fb8db2aefec5f51c33c755 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 12 Apr 2021 14:56:43 +0800 Subject: [PATCH 0854/1221] Possibly fix emoji problem --- core/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/config.py b/core/config.py index 890b3690f1..30f364622d 100644 --- a/core/config.py +++ b/core/config.py @@ -47,9 +47,9 @@ class ConfigManager: # updates "update_notifications": True, # threads - "sent_emoji": "✅", - "blocked_emoji": "🚫", - "close_emoji": "🔒", + "sent_emoji": "\N{WHITE HEAVY CHECK MARK}", + "blocked_emoji": "\N{NO ENTRY SIGN}", + "close_emoji": "\N{LOCK}", "use_user_id_channel_name": False, "recipient_thread_close": False, "thread_auto_close_silently": False, @@ -93,13 +93,13 @@ class ConfigManager: "anon_tag": "Response", # react to contact "react_to_contact_message": None, - "react_to_contact_emoji": "\u2705", + "react_to_contact_emoji": "\N{WHITE HEAVY CHECK MARK}", # confirm thread creation "confirm_thread_creation": False, "confirm_thread_creation_title": "Confirm thread creation", "confirm_thread_response": "React to confirm thread creation which will directly contact the moderators", - "confirm_thread_creation_accept": "\u2705", - "confirm_thread_creation_deny": "\U0001F6AB", + "confirm_thread_creation_accept": "\N{WHITE HEAVY CHECK MARK}", + "confirm_thread_creation_deny": "\N{NO ENTRY SIGN}", # regex "use_regex_autotrigger": False, } From ac2e0c6e1423c485cebd3b50aeb44371102b1e42 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Tue, 13 Apr 2021 06:39:41 +0800 Subject: [PATCH 0855/1221] Cleaner code :D --- cogs/utility.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 51ed2b476d..7b39631dcd 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -701,7 +701,7 @@ async def mention(self, ctx, *user_or_role: Union[discord.Role, discord.Member, elif ( len(user_or_role) == 1 and isinstance(user_or_role[0], str) - and user_or_role[0].lower() in ("disable", "reset", "all", "everyone") + and user_or_role[0].lower() in ("disable", "reset") ): option = user_or_role[0].lower() if option == "disable": @@ -709,26 +709,24 @@ async def mention(self, ctx, *user_or_role: Union[discord.Role, discord.Member, description=f"Disabled mention on thread creation.", color=self.bot.main_color, ) self.bot.config["mention"] = None - elif option == "reset": + else: embed = discord.Embed( description="`mention` is reset to default.", color=self.bot.main_color, ) self.bot.config.remove("mention") - else: - embed = discord.Embed( - title="Changed mention!", - description=f'On thread creation the bot now says "@everyone".', - color=self.bot.main_color, - ) - self.bot.config["mention"] = "@everyone" await self.bot.config.update() else: + mention = [] + everyone = ("all", "everyone") for m in user_or_role: - if not isinstance(m, (discord.Role, discord.Member)): + if not isinstance(m, (discord.Role, discord.Member)) and m not in everyone: raise commands.BadArgument(f'Role or Member "{m}" not found.') - mention = " ".join( - i.mention if i is not ctx.guild.default_role else str(i) for i in user_or_role - ) + elif m == ctx.guild.default_role or m in everyone: + mention.append("@everyone") + continue + mention.append(m.mention) + + mention = " ".join(mention) embed = discord.Embed( title="Changed mention!", description=f'On thread creation the bot now says "{mention}".', From c74e5f4ccbf0695b545e5d362897230b24472002 Mon Sep 17 00:00:00 2001 From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com> Date: Wed, 14 Apr 2021 13:25:19 +0600 Subject: [PATCH 0856/1221] Bump python runtime version Security update --- runtime.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime.txt b/runtime.txt index 67068f10fe..87665291b8 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.9.0 \ No newline at end of file +python-3.9.4 From 404decc10454eb9fe2315ae1fd04beed7dfcd3c6 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Sun, 25 Apr 2021 01:34:10 +0800 Subject: [PATCH 0857/1221] Update sponsors --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 10f7baf737..7a5da956f5 100644 --- a/README.md +++ b/README.md @@ -162,13 +162,19 @@ $ docker run --env-file .env kyb3rr/modmail Special thanks to our sponsors for supporting the project. +Kingdom Gaming Discord: +
      + + + +
      +
      +SirReddit: +
      - - - - +
      Become a sponsor on [Patreon](https://patreon.com/kyber). From 58e8944bdebe61d68cf0916228dd5a2ab0458e65 Mon Sep 17 00:00:00 2001 From: redstonedesigner Date: Sat, 24 Apr 2021 20:46:00 +0100 Subject: [PATCH 0858/1221] Add docker-compose configuration for self hosting Adds a docker-compose configuration file for running both the bot and logviewer with a shared mongo database in a single container stack. Exposes port 8000 and ensure the mongo database can not be accessed remotely. --- docker-compose.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..7b4371b5c8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.7" +services: + bot: + image: kyb3rr/modmail + env_file: + - .env + depends_on: + - mongo + logviewer: + image: kyb3rr/logviewer + depends_on: + - mongo + environment: + - MONGO_URI=mongodb://mongo + ports: + - 8000:8000 + mongo: + image: mongo From b145cf1ad9c0118f6d647372c3880b109f78086a Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 25 Apr 2021 21:22:31 +0800 Subject: [PATCH 0859/1221] Change Max Channel per Category to 49, fix fallback save logic --- core/thread.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/thread.py b/core/thread.py index 7798454958..530e063e54 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1205,16 +1205,16 @@ async def create( # Schedule thread setup for later cat = self.bot.main_category - if category is None and len(cat.channels) == 50: + if category is None and len(cat.channels) >= 49: fallback_id = self.bot.config["fallback_category_id"] if fallback_id: fallback = discord.utils.get(cat.guild.categories, id=int(fallback_id)) - if fallback and len(fallback.channels) != 50: + if fallback and len(fallback.channels) < 49: category = fallback if not category: category = await cat.clone(name="Fallback Modmail") - self.bot.config.set("fallback_category_id", category.id) + self.bot.config.set("fallback_category_id", str(category.id)) await self.bot.config.update() if (message or not manual_trigger) and self.bot.config["confirm_thread_creation"]: From b557a9bf16cfb37a78e1c932a30420cb1c551992 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 25 Apr 2021 21:37:23 +0800 Subject: [PATCH 0860/1221] There is now a proper message when trying to contact a bot. --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index a45da27166..87b9d91e57 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1014,7 +1014,7 @@ async def contact( embed = discord.Embed( color=self.bot.error_color, description="Cannot start a thread with a bot." ) - return await ctx.send(embed=embed, delete_afer=3) + return await ctx.send(embed=embed, delete_after=3) exists = await self.bot.threads.find(recipient=user) if exists: From ea52a8bf4079eee1293ebb2ccbef4dc45fe23416 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 25 Apr 2021 21:40:25 +0800 Subject: [PATCH 0861/1221] Changelog - v3.9.4 --- CHANGELOG.md | 20 ++++++++++++++++++-- bot.py | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df4c140165..ddb87eebc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.9.4 + +## Fixed + +- Certain cases where fallback categories were not working as intended. ([GH #3002](https://github.com/kyb3r/modmail/issues/3002), [PR #3003](https://github.com/kyb3r/modmail/pull/3003)) +- There is now a proper message when trying to contact a bot.+ + +## Improved + +- `?mention` can now be disabled with `?mention disable`. ([PR #2993](https://github.com/kyb3r/modmail/pull/2993/files)) +- `?mention` now allows vague entries such as `everyone` or `all`. ([PR #2993](https://github.com/kyb3r/modmail/pull/2993/files)) + +## Internal + +- Change heroku python version to 3.9.4 [PR #3001](https://github.com/kyb3r/modmail/pull/3001) + # v3.9.3 ## Added -- New config: ` use_user_id_channel_name`, when set to TRUE, channel names would get created with the recipient's ID instead of their name and discriminator. +- New config: `use_user_id_channel_name`, when set to TRUE, channel names would get created with the recipient's ID instead of their name and discriminator. - This is now an option to better suit the needs of servers in Server Discovery -## Internal Change +## Internal - Signature of `format_channel_name` in core/util.py changed to: - `format_channel_name(bot, author, exclude_channel=None, force_null=False)` diff --git a/bot.py b/bot.py index 994e956e11..1b9a9f2c1f 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.9.3" +__version__ = "3.9.4" import asyncio From 5241e5469a8631d2e95e9a89329c8d6758efca14 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 25 Apr 2021 21:54:47 +0800 Subject: [PATCH 0862/1221] Fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb87eebc3..de46a80f38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ## Fixed - Certain cases where fallback categories were not working as intended. ([GH #3002](https://github.com/kyb3r/modmail/issues/3002), [PR #3003](https://github.com/kyb3r/modmail/pull/3003)) -- There is now a proper message when trying to contact a bot.+ +- There is now a proper message when trying to contact a bot. ## Improved From 7ed1fcb8aafb83bc280a314643531c5c39ea72c2 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Sun, 25 Apr 2021 22:14:31 +0800 Subject: [PATCH 0863/1221] add fourjr claim --- CHANGELOG.md | 2 +- plugins/registry.json | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de46a80f38..12f1ddc9ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ## Internal -- Change heroku python version to 3.9.4 [PR #3001](https://github.com/kyb3r/modmail/pull/3001) +- Change heroku python version to 3.9.4 ([PR #3001](https://github.com/kyb3r/modmail/pull/3001)) # v3.9.3 diff --git a/plugins/registry.json b/plugins/registry.json index 515871e916..579c3e3eb0 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -230,5 +230,13 @@ "bot_version": "3.9.0", "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" + }, + "claim": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Allows supporters to claim thread by sending ?claim in the thread channel", + "title": "Claim Thread", + "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", + "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" } } From c1dcf7fb9264d8ebf6a658265a3a73ceb0dc4e77 Mon Sep 17 00:00:00 2001 From: redstonedesigner Date: Sun, 25 Apr 2021 15:46:49 +0100 Subject: [PATCH 0864/1221] Add volume for persistent data storage to Mongo --- docker-compose.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 7b4371b5c8..5d2061df6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,3 +16,8 @@ services: - 8000:8000 mongo: image: mongo + volumes: + - mongodb:/data/db + +volumes: + mongodb: From 702486665202325687ba91eed961d2b066bb1de4 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sun, 25 Apr 2021 20:49:11 +0200 Subject: [PATCH 0865/1221] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b74dfad111..ff08d3ee31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.9.3' +version = '3.9.4' description = "Modmail is similar to Reddit's 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 aec76a045571b65208b6e6203c72a16d5ef6c317 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sun, 25 Apr 2021 20:49:44 +0200 Subject: [PATCH 0866/1221] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 457569b32e..ea07edb304 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      From 3717df646644159c5a0e713aa83bfcdbf768dcdb Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Tue, 27 Apr 2021 07:32:00 +0800 Subject: [PATCH 0867/1221] Update sponsors --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 457569b32e..ad07d59b1e 100644 --- a/README.md +++ b/README.md @@ -165,21 +165,29 @@ Special thanks to our sponsors for supporting the project. Kingdom Gaming Discord:
      - +

      SirReddit:
      - +
      - +
      +Prime Servers Inc: +
      - + + +
      +
      +Real Madrid: +
      + + - Become a sponsor on [Patreon](https://patreon.com/kyber). From b101530b72908098eb67e85bae82d1cff000fa6b Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Wed, 28 Apr 2021 07:58:49 -0400 Subject: [PATCH 0868/1221] Fix typo in config help. --- core/config_help.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config_help.json b/core/config_help.json index ff6be5cba1..ede81617ed 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -421,7 +421,7 @@ "default": "Thread Moved", "description": "The title of the message embed when a thread is moved.", "examples": [ - "`{prefix}config set thread_move_title Thread transferred to another channel!" + "`{prefix}config set thread_move_title Thread transferred to another channel!`" ], "notes": [ "See also: `thread_move_notify`, `thread_move_notify_mods`, `thread_move_response`." From 9bb615bba932887f4ab0474ceb4a5bde240a010b Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Wed, 28 Apr 2021 20:31:23 +0800 Subject: [PATCH 0869/1221] Update 'bot.py' and 'clients.py': - Fix return types, type hints, and unresolved references. --- bot.py | 7 +++++-- core/clients.py | 12 +++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index 1b9a9f2c1f..cce02ceff2 100644 --- a/bot.py +++ b/bot.py @@ -237,6 +237,7 @@ async def runner(): if self._session: await self._session.close() + # noinspection PyUnusedLocal def stop_loop_on_completion(f): loop.stop() @@ -811,7 +812,7 @@ async def is_blocked( *, channel: discord.TextChannel = None, send_message: bool = False, - ) -> typing.Tuple[bool, str]: + ) -> bool: member = self.guild.get_member(author.id) if member is None: @@ -892,7 +893,9 @@ async def get_thread_cooldown(self, author: discord.Member): return @staticmethod - async def add_reaction(msg, reaction: discord.Reaction) -> bool: + async def add_reaction( + msg, reaction: typing.Union[discord.Emoji, discord.Reaction, discord.PartialEmoji, str] + ) -> bool: if reaction != "disable": try: await msg.add_reaction(reaction) diff --git a/core/clients.py b/core/clients.py index db4e9936b4..f3b2248c67 100644 --- a/core/clients.py +++ b/core/clients.py @@ -68,7 +68,7 @@ class GitHub: def __init__(self, bot, access_token: str = "", username: str = "", **kwargs): self.bot = bot self.session = bot.session - self.headers: dict = None + self.headers: Optional[dict] = None self.access_token = access_token self.username = username self.avatar_url: str = kwargs.pop("avatar_url", "") @@ -319,7 +319,7 @@ async def get_config(self) -> dict: async def update_config(self, data: dict): return NotImplemented - async def edit_message(self, message_id: Union[int, str], new_content: str) -> None: + async def edit_message(self, message_id: Union[int, str], new_content: str): return NotImplemented async def append_log( @@ -359,6 +359,12 @@ async def edit_note(self, message_id: Union[int, str], message: str): def get_plugin_partition(self, cog): return NotImplemented + async def update_repository(self) -> dict: + return NotImplemented + + async def get_user_info(self) -> Optional[dict]: + return NotImplemented + class MongoDBClient(ApiClient): def __init__(self, bot): @@ -659,7 +665,7 @@ async def update_repository(self) -> dict: "user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,}, } - async def get_user_info(self) -> dict: + async def get_user_info(self) -> Optional[dict]: try: user = await GitHub.login(self.bot) except InvalidConfigError: From 6b006129aa6fec35e893605b787b5d8afb6ade6f Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Thu, 29 Apr 2021 07:21:08 +0800 Subject: [PATCH 0870/1221] Update database after resetting/purging all plugins. --- cogs/plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/plugins.py b/cogs/plugins.py index 94a3f425c0..bc664aef44 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -539,6 +539,7 @@ async def plugins_reset(self, ctx): except Exception: logger.error("Failed to unload plugin: %s.", ext) self.bot.config["plugins"].clear() + await self.bot.config.update() cache_path = Path(__file__).absolute().parent.parent / "temp" / "plugins-cache" if cache_path.exists(): From fbb45e37829c2f17d81e5036794d197d14d30992 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Fri, 30 Apr 2021 19:40:17 +0800 Subject: [PATCH 0871/1221] `plugin reset`, remove unloaded plugins from `self.loaded_plugins` --- cogs/plugins.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cogs/plugins.py b/cogs/plugins.py index bc664aef44..a01f4bc61c 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -538,6 +538,13 @@ async def plugins_reset(self, ctx): self.bot.unload_extension(ext) except Exception: logger.error("Failed to unload plugin: %s.", ext) + else: + if not self.loaded_plugins: + continue + plugin = next((p for p in self.loaded_plugins if p.ext_string == ext), None) + if plugin: + self.loaded_plugins.remove(plugin) + self.bot.config["plugins"].clear() await self.bot.config.update() From 039b4b91ae7dd9e86b6ff06f582483bab57ec1ea Mon Sep 17 00:00:00 2001 From: Cyrus Yip Date: Tue, 4 May 2021 21:11:34 -0700 Subject: [PATCH 0872/1221] turn mentions into set --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 530e063e54..76516af8b3 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1061,7 +1061,7 @@ def get_notifications(self) -> str: self.bot.config["notification_squad"].pop(key) self.bot.loop.create_task(self.bot.config.update()) - return " ".join(mentions) + return " ".join(set(mentions)) async def set_title(self, title) -> None: user_id = match_user_id(self.channel.topic) From ef4b5b2f1a8fbea7e1e201bf8d4078bb4170395b Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Tue, 11 May 2021 21:05:51 +0800 Subject: [PATCH 0873/1221] Hopefully resolves #3022. --- core/thread.py | 58 ++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/core/thread.py b/core/thread.py index 530e063e54..8eccb04c02 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1115,16 +1115,16 @@ async def find( try: await thread.wait_until_ready() except asyncio.CancelledError: - logger.warning("Thread for %s cancelled, abort creating", recipient) + logger.warning("Thread for %s cancelled.", recipient) return thread else: - if not thread.channel or not self.bot.get_channel(thread.channel.id): + if not thread.cancelled and ( + 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) - ) + await thread.close(closer=self.bot.user, silent=True, delete_channel=False) thread = None else: channel = discord.utils.get( @@ -1186,7 +1186,7 @@ async def create( try: await thread.wait_until_ready() except asyncio.CancelledError: - logger.warning("Thread for %s cancelled, abort creating", recipient) + logger.warning("Thread for %s cancelled, abort creating.", recipient) return thread else: if thread.channel and self.bot.get_channel(thread.channel.id): @@ -1195,9 +1195,7 @@ async def create( 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) - ) + await thread.close(closer=self.bot.user, silent=True, delete_channel=False) thread = Thread(self, recipient) @@ -1231,9 +1229,11 @@ async def create( ) accept_emoji = self.bot.config["confirm_thread_creation_accept"] deny_emoji = self.bot.config["confirm_thread_creation_deny"] - await confirm.add_reaction(accept_emoji) - await asyncio.sleep(0.2) - await confirm.add_reaction(deny_emoji) + emojis = [accept_emoji, deny_emoji] + for emoji in emojis: + await confirm.add_reaction(emoji) + await asyncio.sleep(0.2) + try: r, _ = await self.bot.wait_for( "reaction_add", @@ -1245,29 +1245,31 @@ async def create( ) except asyncio.TimeoutError: thread.cancelled = True - - await confirm.remove_reaction(accept_emoji, self.bot.user) - await asyncio.sleep(0.2) - await confirm.remove_reaction(deny_emoji, self.bot.user) - await destination.send( - embed=discord.Embed( - title="Cancelled", description="Timed out", color=self.bot.error_color + self.bot.loop.create_task( + destination.send( + embed=discord.Embed( + title="Cancelled", description="Timed out", color=self.bot.error_color + ) ) ) - del self.cache[recipient.id] - return thread else: if str(r.emoji) == deny_emoji: thread.cancelled = True + self.bot.loop.create_task( + destination.send( + embed=discord.Embed(title="Cancelled", color=self.bot.error_color) + ) + ) - await confirm.remove_reaction(accept_emoji, self.bot.user) + async def remove_reactions(): + for emoji in emojis: + await confirm.remove_reaction(emoji, self.bot.user) await asyncio.sleep(0.2) - await confirm.remove_reaction(deny_emoji, self.bot.user) - await destination.send( - embed=discord.Embed(title="Cancelled", color=self.bot.error_color) - ) - del self.cache[recipient.id] - return thread + + self.bot.loop.create_task(remove_reactions()) + if thread.cancelled: + del self.cache[recipient.id] + return thread self.bot.loop.create_task( thread.setup(creator=creator, category=category, initial_message=message) From 08bc39b4c00b09a2502fbfdd1822021827668abc Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Tue, 11 May 2021 21:22:45 +0800 Subject: [PATCH 0874/1221] Improve contact and react to contact: - Checks if user is blocked when `?contact` command or react to contact is used. --- bot.py | 83 +++++++++++++++++++++++++------------------------ cogs/modmail.py | 37 +++++++++++++--------- 2 files changed, 65 insertions(+), 55 deletions(-) diff --git a/bot.py b/bot.py index 1b9a9f2c1f..ccececf8d6 100644 --- a/bot.py +++ b/bot.py @@ -1304,50 +1304,51 @@ async def handle_reaction_events(self, payload): 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) - + async def handle_react_to_contact(self, payload): react_message_id = tryint(self.config.get("react_to_contact_message")) react_message_emoji = self.config.get("react_to_contact_emoji") - if all((react_message_id, react_message_emoji)): - if payload.message_id == react_message_id: - if payload.emoji.is_unicode_emoji(): - emoji_fmt = payload.emoji.name - else: - emoji_fmt = f"<:{payload.emoji.name}:{payload.emoji.id}>" - - if emoji_fmt == react_message_emoji: - channel = self.get_channel(payload.channel_id) - member = channel.guild.get_member(payload.user_id) - if not member.bot: - message = await channel.fetch_message(payload.message_id) - await message.remove_reaction(payload.emoji, member) - await message.add_reaction(emoji_fmt) # bot adds as well - - if self.config["dm_disabled"] in ( - DMDisabled.NEW_THREADS, - DMDisabled.ALL_THREADS, - ): - embed = discord.Embed( - title=self.config["disabled_new_thread_title"], - color=self.error_color, - description=self.config["disabled_new_thread_response"], - ) - embed.set_footer( - text=self.config["disabled_new_thread_footer"], - icon_url=self.guild.icon_url, - ) - logger.info( - "A new thread using react to contact was blocked from %s due to disabled Modmail.", - member, - ) - return await member.send(embed=embed) + if ( + not all((react_message_id, react_message_emoji)) + or payload.message_id != react_message_id + ): + return + if payload.emoji.is_unicode_emoji(): + emoji_fmt = payload.emoji.name + else: + emoji_fmt = f"<:{payload.emoji.name}:{payload.emoji.id}>" - ctx = await self.get_context(message) - ctx.author = member - await ctx.invoke( - self.get_command("contact"), user=member, manual_trigger=False - ) + if emoji_fmt != react_message_emoji: + return + channel = self.get_channel(payload.channel_id) + member = channel.guild.get_member(payload.user_id) + if member.bot: + return + message = await channel.fetch_message(payload.message_id) + await message.remove_reaction(payload.emoji, member) + await message.add_reaction(emoji_fmt) # bot adds as well + + if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + embed = discord.Embed( + title=self.config["disabled_new_thread_title"], + color=self.error_color, + description=self.config["disabled_new_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_new_thread_footer"], icon_url=self.guild.icon_url, + ) + logger.info( + "A new thread using react to contact was blocked from %s due to disabled Modmail.", + member, + ) + return await member.send(embed=embed) + + ctx = await self.get_context(message) + await ctx.invoke(self.get_command("contact"), user=member, manual_trigger=False) + + async def on_raw_reaction_add(self, payload): + await asyncio.gather( + self.handle_reaction_events(payload), self.handle_react_to_contact(payload), + ) async def on_raw_reaction_remove(self, payload): if self.config["transfer_reactions"]: diff --git a/cogs/modmail.py b/cogs/modmail.py index 87b9d91e57..08930aa946 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1002,7 +1002,7 @@ async def contact( `category`, if specified, may be a category ID, mention, or name. `user` may be a user ID, mention, or name. - `options` can be `silent` + `options` can be `silent` or `silently`. """ silent = False if isinstance(category, str): @@ -1018,19 +1018,28 @@ async def contact( exists = await self.bot.threads.find(recipient=user) if exists: - embed = discord.Embed( - color=self.bot.error_color, - description="A thread for this user already " - f"exists in {exists.channel.mention}.", - ) + desc = "A thread for this user already exists" + if exists.channel: + desc += f" in {exists.channel.mention}" + desc += "." + embed = discord.Embed(color=self.bot.error_color, description=desc) await ctx.channel.send(embed=embed, delete_after=3) else: + creator = ctx.author if manual_trigger else user + if await self.bot.is_blocked(user): + if not manual_trigger: # react to contact + return + + ref = f"{user.mention} is" if creator != user else "You are" + embed = discord.Embed( + color=self.bot.error_color, + description=f"{ref} currently blocked from contacting {self.bot.user.name}.", + ) + return await ctx.send(embed=embed) + thread = await self.bot.threads.create( - recipient=user, - creator=ctx.author, - category=category, - manual_trigger=manual_trigger, + recipient=user, creator=creator, category=category, manual_trigger=manual_trigger, ) if thread.cancelled: return @@ -1039,22 +1048,22 @@ async def contact( logger.info("Contacting user %s when Modmail DM is disabled.", user) if not silent and not self.bot.config.get("thread_contact_silently"): - if ctx.author.id == user.id: + if creator.id == user.id: description = "You have opened a Modmail thread." else: - description = f"{ctx.author.name} has opened a Modmail thread." + description = f"{creator.name} has opened a Modmail thread." em = discord.Embed( title="New Thread", description=description, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: em.timestamp = datetime.utcnow() - em.set_footer(icon_url=ctx.author.avatar_url) + em.set_footer(text=f"{creator}", icon_url=creator.avatar_url) await user.send(embed=em) embed = discord.Embed( title="Created Thread", - description=f"Thread started by {ctx.author.mention} for {user.mention}.", + description=f"Thread started by {creator.mention} for {user.mention}.", color=self.bot.main_color, ) await thread.wait_until_ready() From a6c24d952ce287bc842ddb864b26fff28bd22d3c Mon Sep 17 00:00:00 2001 From: codeinteger6 <44692189+codeinteger6@users.noreply.github.com> Date: Fri, 14 May 2021 16:12:26 +0600 Subject: [PATCH 0875/1221] Remove publish plugin --- plugins/registry.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 579c3e3eb0..b74ff1f989 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -187,15 +187,6 @@ "icon_url": "https://cdn.discordapp.com/attachments/717029057635549274/717033838966210601/Slow_mode_-_icon.png", "thumbnail_url": "https://cdn.discordapp.com/attachments/717029057635549274/717029110907666482/Slow_mode_plugin_-_thumbnail.png" }, - "publish": { - "repository": "codeinteger6/modmail-plugins", - "branch": "master", - "description": "Publish messages sent in announcement channels.", - "bot_version": "3.5.0", - "title": "Publish", - "icon_url": "https://user-images.githubusercontent.com/44692189/89184422-96de3600-d5ba-11ea-98ea-d096aa385ad5.png", - "thumbnail_url": "https://user-images.githubusercontent.com/44692189/89184422-96de3600-d5ba-11ea-98ea-d096aa385ad5.png" - }, "translate": { "repository": "WebKide/modmail-plugins", "branch": "master", From 7073fcac2aaf1cea4cb493f450e4060d5a90362a Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Mon, 24 May 2021 15:39:30 +0800 Subject: [PATCH 0876/1221] Initial 3.9.5-dev1 --- CHANGELOG.md | 7 +++++++ README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f1ddc9ef..bdb75a3ce6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.9.5-dev1 + +## Fixed + +- Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/kyb3r/modmail/issues/3022), [PR #3028](https://github.com/kyb3r/modmail/pull/3028)) +- Blocked users are now no longer allowed to use `?contact` and react to contact. ([COMMENT #819004157](https://github.com/kyb3r/modmail/issues/2969#issuecomment-819004157), [PR #3027](https://github.com/kyb3r/modmail/pull/3027)) + # v3.9.4 ## Fixed diff --git a/README.md b/README.md index ad07d59b1e..c150bae3c5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index ccececf8d6..eccdfd3868 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.9.4" +__version__ = "3.9.5-dev1" import asyncio diff --git a/pyproject.toml b/pyproject.toml index b74dfad111..a4d4002580 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.9.3' +version = '3.9.5-dev1' description = "Modmail is similar to Reddit's 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 204c5385a8d88c676d2cc7f85ce7255bdf04201d Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Mon, 31 May 2021 10:55:07 +0200 Subject: [PATCH 0877/1221] Update registry.json removal of profanity filter, trying to download will result in a infinite download loop and will stop any plugin from loading/break the usage of removing/adding new plugins --- plugins/registry.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index b74ff1f989..a9f565a013 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1,13 +1,4 @@ { - "profanity-filter": { - "repository": "kyb3r/modmail-plugins", - "branch": "master", - "description": "Checks for profanity in a message using machine learning and deletes it.", - "bot_version": "2.20.1", - "title": "Profanity Filter Plugin", - "icon_url": "https://i.imgur.com/951szZ3.jpg", - "thumbnail_url": "https://i.imgur.com/951szZ3.jpg" - }, "dragory-migrate": { "repository": "kyb3r/modmail-plugins", "branch": "master", From 5648289fa7546a090e24fa7855b3f82431cbedce Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 2 Jun 2021 21:23:05 +0800 Subject: [PATCH 0878/1221] Group conversation ALPHA #143 --- CHANGELOG.md | 6 ++- bot.py | 28 ++++++++--- cogs/modmail.py | 59 ++++++++++++++++++++-- core/thread.py | 128 ++++++++++++++++++++++++++++++++++-------------- core/utils.py | 26 +++++++++- 5 files changed, 197 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdb75a3ce6..c003f1db37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.9.5-dev1 +# v3.10.0-dev2 + +## Added + +- Ability to have group conversations. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) ## Fixed diff --git a/bot.py b/bot.py index eccdfd3868..456e340218 100644 --- a/bot.py +++ b/bot.py @@ -964,6 +964,15 @@ async def process_dm_modmail(self, message: discord.Message) -> None: logger.error("Failed to send message:", exc_info=True) await self.add_reaction(message, blocked_emoji) else: + for user in thread.recipients: + # send to all other recipients + if user != message.author: + try: + await thread.send(message, user) + except Exception: + # silently ignore + logger.error("Failed to send message:", exc_info=True) + await self.add_reaction(message, sent_emoji) self.dispatch("thread_reply", thread, False, message, False, False) @@ -1224,9 +1233,10 @@ async def on_typing(self, channel, user, _): thread = await self.threads.find(channel=channel) if thread is not None and thread.recipient: - if await self.is_blocked(thread.recipient): - return - await thread.recipient.trigger_typing() + for user in thread.recipients: + if await self.is_blocked(user): + continue + await user.trigger_typing() async def handle_reaction_events(self, payload): user = self.get_user(payload.user_id) @@ -1286,20 +1296,22 @@ async def handle_reaction_events(self, payload): if not thread: return try: - _, linked_message = await thread.find_linked_messages( + _, *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 - if self.config["transfer_reactions"] and linked_message is not None: + if self.config["transfer_reactions"] and linked_message is not [None]: if payload.event_type == "REACTION_ADD": - if await self.add_reaction(linked_message, reaction): - await self.add_reaction(message, reaction) + for msg in linked_message: + await self.add_reaction(msg, reaction) + await self.add_reaction(message, reaction) else: try: - await linked_message.remove_reaction(reaction, self.user) + for msg in linked_message: + await msg.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) diff --git a/cogs/modmail.py b/cogs/modmail.py index 08930aa946..ef816a99c3 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -681,6 +681,48 @@ async def title(self, ctx, *, name: str): await ctx.message.pin() await self.bot.add_reaction(ctx.message, sent_emoji) + @commands.command(cooldown_after_parsing=True) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + @commands.cooldown(1, 600, BucketType.channel) + async def adduser(self, ctx, *, user: discord.Member): + """Adds a user to a modmail thread""" + + curr_thread = await self.bot.threads.find(recipient=user) + if curr_thread: + em = discord.Embed( + title="Error", + description=f"User is already in a thread: {curr_thread.channel.mention}.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + else: + em = discord.Embed( + title="New Thread (Group)", + description=f"{ctx.author.name} has added you to a Modmail thread.", + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{ctx.author}", icon_url=ctx.author.avatar_url) + await user.send(embed=em) + + em = discord.Embed( + title="New User", + description=f"{ctx.author.name} has added {user.name} to the Modmail thread.", + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{user}", icon_url=user.avatar_url) + + for i in ctx.thread.recipients: + await i.send(embed=em) + + await ctx.thread.add_user(user) + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) async def logs(self, ctx, *, user: User = None): @@ -1463,15 +1505,19 @@ async def repair(self, ctx): and message.embeds[0].footer.text ): user_id = match_user_id(message.embeds[0].footer.text) + other_recipients = match_other_recipients(ctx.channel.topic) + for n, uid in enumerate(other_recipients): + other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid) + 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 + self.bot.threads, user_id, ctx.channel, other_recipients ) else: self.bot.threads.cache[user_id] = thread = Thread( - self.bot.threads, recipient, ctx.channel + self.bot.threads, recipient, ctx.channel, other_recipients ) thread.ready = True logger.info( @@ -1516,13 +1562,18 @@ async def repair(self, ctx): await thread.channel.send(embed=embed) except discord.HTTPException: pass + + other_recipients = match_other_recipients(ctx.channel.topic) + for n, uid in enumerate(other_recipients): + other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid) + if recipient is None: self.bot.threads.cache[user.id] = thread = Thread( - self.bot.threads, user_id, ctx.channel + self.bot.threads, user_id, ctx.channel, other_recipients ) else: self.bot.threads.cache[user.id] = thread = Thread( - self.bot.threads, recipient, ctx.channel + self.bot.threads, recipient, ctx.channel, other_recipients ) thread.ready = True logger.info("Setting current channel's topic to User ID and created new thread.") diff --git a/core/thread.py b/core/thread.py index 8eccb04c02..5638acbe44 100644 --- a/core/thread.py +++ b/core/thread.py @@ -19,6 +19,7 @@ days, match_title, match_user_id, + match_other_recipients, truncate, format_channel_name, ) @@ -34,6 +35,7 @@ def __init__( manager: "ThreadManager", recipient: typing.Union[discord.Member, discord.User, int], channel: typing.Union[discord.DMChannel, discord.TextChannel] = None, + other_recipients: typing.List[typing.Union[discord.Member, discord.User]] = [], ): self.manager = manager self.bot = manager.bot @@ -45,6 +47,7 @@ def __init__( raise CommandError("Recipient cannot be a bot.") self._id = recipient.id self._recipient = recipient + self._other_recipients = other_recipients self._channel = channel self.genesis_message = None self._ready_event = asyncio.Event() @@ -54,7 +57,7 @@ def __init__( self._cancelled = False def __repr__(self): - return f'Thread(recipient="{self.recipient or self.id}", channel={self.channel.id})' + return f'Thread(recipient="{self.recipient or self.id}", channel={self.channel.id}, other_recipienets={len(self._other_recipients)})' async def wait_until_ready(self) -> None: """Blocks execution until the thread is fully set up.""" @@ -80,6 +83,10 @@ def channel(self) -> typing.Union[discord.TextChannel, discord.DMChannel]: def recipient(self) -> typing.Optional[typing.Union[discord.User, discord.Member]]: return self._recipient + @property + def recipients(self) -> typing.List[typing.Union[discord.User, discord.Member]]: + return [self._recipient] + self._other_recipients + @property def ready(self) -> bool: return self._ready_event.is_set() @@ -103,6 +110,23 @@ def cancelled(self, flag: bool): for i in self.wait_tasks: i.cancel() + @classmethod + async def from_channel( + cls, manager: "ThreadManager", channel: discord.TextChannel + ) -> "Thread": + recipient_id = match_user_id( + channel.topic + ) # there is a chance it grabs from another recipient's main thread + recipient = manager.bot.get_user(recipient_id) or await manager.bot.fetch_user( + recipient_id + ) + + other_recipients = match_other_recipients(channel.topic) + for n, uid in enumerate(other_recipients): + other_recipients[n] = manager.bot.get_user(uid) or await manager.bot.fetch_user(uid) + + return cls(manager, recipient or recipient_id, channel, other_recipients) + async def setup(self, *, creator=None, category=None, initial_message=None): """Create the thread channel and other io related initialisation tasks""" self.bot.dispatch("thread_initiate", self, creator, category, initial_message) @@ -619,23 +643,30 @@ async def find_linked_messages( except ValueError: raise ValueError("Malformed thread message.") - async for msg in self.recipient.history(): - if either_direction: - if msg.id == joint_id: - return message1, msg + messages = [message1] + for user in self.recipients: + async for msg in user.history(): + if either_direction: + if msg.id == joint_id: + return message1, msg - 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: - continue - raise ValueError("DM message not found. Plain messages are not supported.") + if not (msg.embeds and msg.embeds[0].author.url): + continue + try: + if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id: + messages.append(msg) + break + except ValueError: + continue + + if len(messages) > 1: + return messages + + 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) + message1, *message2 = await self.find_linked_messages(message_id) except ValueError: logger.warning("Failed to edit message.", exc_info=True) raise @@ -644,10 +675,11 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> embed1.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)] + if message2 is not [None]: + for m2 in message2: + embed2 = message2.embeds[0] + embed2.description = message + tasks += [m2.edit(embed=embed2)] elif message1.embeds[0].author.name.startswith("Persistent Note"): tasks += [self.bot.api.edit_note(message1.id, message)] @@ -657,14 +689,16 @@ async def delete_message( self, message: typing.Union[int, discord.Message] = None, note: bool = True ) -> None: if isinstance(message, discord.Message): - message1, message2 = await self.find_linked_messages(message1=message, note=note) + message1, *message2 = await self.find_linked_messages(message1=message, note=note) else: - message1, message2 = await self.find_linked_messages(message, note=note) + message1, *message2 = await self.find_linked_messages(message, note=note) + print(message1, message2) tasks = [] if not isinstance(message, discord.Message): tasks += [message1.delete()] - elif message2 is not None: - tasks += [message2.delete()] + elif message2 is not [None]: + for m2 in message2: + tasks += [m2.delete()] elif message1.embeds[0].author.name.startswith("Persistent Note"): tasks += [self.bot.api.delete_note(message1.id)] if tasks: @@ -750,16 +784,18 @@ async def reply( ) ) + user_msg_tasks = [] tasks = [] - try: - user_msg = await self.send( - message, - destination=self.recipient, - from_mod=True, - anonymous=anonymous, - plain=plain, + for user in self.recipients: + user_msg_tasks.append( + self.send( + message, destination=user, from_mod=True, anonymous=anonymous, plain=plain, + ) ) + + try: + user_msg = await asyncio.gather(*user_msg_tasks) except Exception as e: logger.error("Message delivery failed:", exc_info=True) if isinstance(e, discord.Forbidden): @@ -1063,9 +1099,23 @@ def get_notifications(self) -> str: return " ".join(mentions) - async def set_title(self, title) -> None: + async def set_title(self, title: str) -> None: + user_id = match_user_id(self.channel.topic) + ids = ",".join(i.id for i in self._other_recipients) + + await self.channel.edit( + topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}" + ) + + async def add_user(self, user: typing.Union[discord.Member, discord.User]) -> None: + title = match_title(self.channel.topic) user_id = match_user_id(self.channel.topic) - await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}") + self._other_recipients.append(user) + + ids = ",".join(str(i.id) for i in self._other_recipients) + await self.channel.edit( + topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}" + ) class ThreadManager: @@ -1127,11 +1177,13 @@ async def find( await 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}" + channel = discord.utils.find( + lambda x: str(recipient_id) in x.topic if x.topic else False, + self.bot.modmail_guild.text_channels, ) + if channel: - thread = Thread(self, recipient or recipient_id, channel) + thread = await Thread.from_channel(self, channel) if thread.recipient: # only save if data is valid self.cache[recipient_id] = thread @@ -1161,10 +1213,14 @@ async def _find_from_channel(self, channel): except discord.NotFound: recipient = None + other_recipients = match_other_recipients(channel.topic) + for n, uid in enumerate(other_recipients): + other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid) + if recipient is None: - thread = Thread(self, user_id, channel) + thread = Thread(self, user_id, channel, other_recipients) else: - self.cache[user_id] = thread = Thread(self, recipient, channel) + self.cache[user_id] = thread = Thread(self, recipient, channel, other_recipients) thread.ready = True return thread diff --git a/core/utils.py b/core/utils.py index 2dfb319a8e..b95200cdfe 100644 --- a/core/utils.py +++ b/core/utils.py @@ -21,7 +21,9 @@ "human_join", "days", "cleanup_code", + "match_title", "match_user_id", + "match_other_recipients", "create_not_found_embed", "parse_alias", "normalize_alias", @@ -30,7 +32,6 @@ "escape_code_block", "format_channel_name", "tryint", - "match_title", ] @@ -217,6 +218,9 @@ def cleanup_code(content: str) -> str: return content.strip("` \n") +TOPIC_OTHER_RECIPIENTS_REGEX = re.compile( + r"Other Recipients:\s*((?:\d{17,21},*)+)", flags=re.IGNORECASE +) TOPIC_TITLE_REGEX = re.compile(r"\bTitle: (.*)\n(?:User ID: )\b", flags=re.IGNORECASE | re.DOTALL) TOPIC_UID_REGEX = re.compile(r"\bUser ID:\s*(\d{17,21})\b", flags=re.IGNORECASE) @@ -260,6 +264,26 @@ def match_user_id(text: str) -> int: return -1 +def match_other_recipients(text: str) -> int: + """ + Matches a title in the format of "Other Recipients: XXXX,XXXX" + + Parameters + ---------- + text : str + The text of the user ID. + + Returns + ------- + Optional[str] + The title if found + """ + match = TOPIC_OTHER_RECIPIENTS_REGEX.search(text) + if match is not None: + return list(map(int, match.group(1).split(","))) + return [] + + def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discord.Embed: # Single reference of Color.red() embed = discord.Embed( From 82047db09e2c726b61a3360507cf26f5c3a29474 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 2 Jun 2021 21:23:23 +0800 Subject: [PATCH 0879/1221] Remove debug --- cogs/utility.py | 1 - core/thread.py | 1 - 2 files changed, 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 7b39631dcd..4ac02b9073 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1780,7 +1780,6 @@ async def autotrigger_add(self, ctx, keyword, *, command): split_cmd = command.split(" ") for n in range(1, len(split_cmd) + 1): if self.bot.get_command(" ".join(split_cmd[0:n])): - print(self.bot.get_command(" ".join(split_cmd[0:n]))) valid = True break diff --git a/core/thread.py b/core/thread.py index 5638acbe44..5c1e596051 100644 --- a/core/thread.py +++ b/core/thread.py @@ -692,7 +692,6 @@ async def delete_message( message1, *message2 = await self.find_linked_messages(message1=message, note=note) else: message1, *message2 = await self.find_linked_messages(message, note=note) - print(message1, message2) tasks = [] if not isinstance(message, discord.Message): tasks += [message1.delete()] From f6b5bf73a0c603e9ade170c452bea1f1a5304785 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 2 Jun 2021 21:41:04 +0800 Subject: [PATCH 0880/1221] Push ver --- README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c150bae3c5..9e662a7fb3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 456e340218..277050a627 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.9.5-dev1" +__version__ = "v3.10.0-dev2" import asyncio diff --git a/pyproject.toml b/pyproject.toml index a4d4002580..4e6959fe70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.9.5-dev1' +version = '3.10.0-dev2' description = "Modmail is similar to Reddit's 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 975b15805df6fbe60b22855507de919b827208d5 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Thu, 3 Jun 2021 10:24:34 +0800 Subject: [PATCH 0881/1221] Fix closes for group conversations --- core/thread.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/core/thread.py b/core/thread.py index 5c1e596051..04c16661c7 100644 --- a/core/thread.py +++ b/core/thread.py @@ -502,22 +502,24 @@ async def _close( if self.bot.config["show_timestamp"]: embed.timestamp = datetime.utcnow() - if not message: - if self.id == closer.id: - message = self.bot.config["thread_self_close_response"] - else: - 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 ) - embed.description = message footer = self.bot.config["thread_close_footer"] embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) - if not silent and self.recipient is not None: - tasks.append(self.recipient.send(embed=embed)) + if not silent: + for user in self.recipients: + if not message: + if user.id == closer.id: + message = self.bot.config["thread_self_close_response"] + else: + message = self.bot.config["thread_close_response"] + embed.description = message + + if user is not None: + tasks.append(user.send(embed=embed)) if delete_channel: tasks.append(self.channel.delete()) From e79432525fde5efe69550fa7d70bf01eb2be80d3 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Thu, 3 Jun 2021 10:41:27 +0800 Subject: [PATCH 0882/1221] Bump version --- CHANGELOG.md | 2 +- README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c003f1db37..f83d08ca53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.10.0-dev2 +# v3.10.0-dev3 ## Added diff --git a/README.md b/README.md index 9e662a7fb3..9d90568bc8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 277050a627..07c654be3c 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "v3.10.0-dev2" +__version__ = "v3.10.0-dev3" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 4e6959fe70..f41c8484fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.0-dev2' +version = '3.10.0-dev3' description = "Modmail is similar to Reddit's 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 e6d682bf49e95b63f67e7c8d5cd9161ddfe06642 Mon Sep 17 00:00:00 2001 From: Cyrus <54488650+RealCyGuy@users.noreply.github.com> Date: Sat, 5 Jun 2021 22:03:28 -0700 Subject: [PATCH 0883/1221] Update registry.json --- plugins/registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index a9f565a013..36b106bbc8 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -154,7 +154,7 @@ "suggest": { "repository": "realcyguy/modmail-plugins", "branch": "master", - "description": "Send suggestions to a selected server! It even has moderation...", + "description": "Send suggestions to a selected server! It has accepting, denying, and moderation-ing.", "bot_version": "3.4.1", "title": "Suggest stuff.", "icon_url": "https://i.imgur.com/qtE7AH8.png", From 882c60e7e11fe591301fe7cf630b9577f6f09cde Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 9 Jun 2021 22:47:31 +0800 Subject: [PATCH 0884/1221] Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f83d08ca53..292a4f434f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Ability to have group conversations. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) +### Internal + +- `thread.reply` now returns mod_message, user_message1, user_message2... It is no longer limited at a size 2 tuple. Potentially breaking if plugins depend on this behaviour. + ## Fixed - Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/kyb3r/modmail/issues/3022), [PR #3028](https://github.com/kyb3r/modmail/pull/3028)) From e48eed5ec8d9d577a1c0e4dcb6978739dd0d09b4 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 21 Jun 2021 05:21:42 +0800 Subject: [PATCH 0885/1221] Add solution to CERTIFICATE_VERIFY_FAILED --- core/clients.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/clients.py b/core/clients.py index db4e9936b4..1d4183eabb 100644 --- a/core/clients.py +++ b/core/clients.py @@ -407,14 +407,24 @@ async def setup_indexes(self): ) logger.debug("Successfully configured and verified database indexes.") - async def validate_database_connection(self): + async def validate_database_connection(self, *, ssl_retry=True): try: await self.db.command("buildinfo") except Exception as exc: logger.critical("Something went wrong while connecting to the database.") message = f"{type(exc).__name__}: {str(exc)}" logger.critical(message) - + if "CERTIFICATE_VERIFY_FAILED" in message and ssl_retry: + mongo_uri = self.bot.config["connection_uri"] + if mongo_uri is None: + mongo_uri = self.bot.config["mongo_uri"] + for _ in range(3): + logger.warning("FAILED TO VERIFY SSL CERTIFICATE, ATTEMPTING TO START WITHOUT SSL (UNSAFE).") + logger.warning("To fix this warning, check there's no proxies blocking SSL cert verification, " + "run \"Certificate.command\" on MacOS, " + "and check certifi is up to date \"pip3 install --upgrade certifi\".") + self.db = AsyncIOMotorClient(mongo_uri, tlsAllowInvalidCertificates=True).modmail_bot + return await self.validate_database_connection(ssl_retry=False) if "ServerSelectionTimeoutError" in message: logger.critical( "This may have been caused by not whitelisting " From 1a6ae31737016ee3d2c12a0dce2eb47ae40c60ab Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 26 Jun 2021 17:43:47 +0800 Subject: [PATCH 0886/1221] New issue forms --- .github/ISSUE_TEMPLATE/bug_report.md | 38 ---------------- .github/ISSUE_TEMPLATE/bug_report.yml | 51 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/command-request.md | 23 ---------- .github/ISSUE_TEMPLATE/config.yml | 5 +++ .github/ISSUE_TEMPLATE/feature_request.md | 23 ---------- .github/ISSUE_TEMPLATE/feature_request.yml | 46 +++++++++++++++++++ 6 files changed, 102 insertions(+), 84 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml delete mode 100644 .github/ISSUE_TEMPLATE/command-request.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 28a29c07b5..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[BUG] bug report title here" -labels: 'maybe: bug' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**Bot Info** -Bot version (check with `@modmail about`): -Host method (Heroku, self-host, etc): - -**To Reproduce** -Steps to reproduce the behavior: -1. Who can reproduce (ex. anyone, owners)? -2. Where can it be reproduced (ex. in thread channels, recipient DM's)? -3. Done what to cause the error? -4. Any recently made changes to your bot? -5. Errored - -**Error Logs** -If your Modmail bot is online, type `@modmail debug hastebin` and include the link here. -If your Modmail bot is not online or the previous command did not generate a link, do the following: - -1. Select your *bot* application at https://dashboard.heroku.com -2. [Restart your bot](https://i.imgur.com/3FcrlKz.png) -3. Reproduce the error to populate the error logs -4. [Copy and paste the logs](https://i.imgur.com/TTrhitm.png) - -**Screenshots** -Add screenshots to help explain your problem. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..77461f9d01 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,51 @@ +name: Bug Report +description: File a bug report +title: "[BUG]: your bug report title" +labels: "maybe: bug" +body: + - type: input + id: bot-info-version + attributes: + label: Bot Version + description: Check it with `@modmail about` + placeholder: eg. v3.9.4 + validations: + required: true + - type: dropdown + id: bot-info-hosting + attributes: + label: How are you hosting Modmail? + description: You can check it with `@modmail about` if you are unsure + options: + - Heroku + - Systemd + - PM2 + - Patreon + - Other + validations: + required: true + - type: input + id: logs + attributes: + label: Error logs + placeholder: https://hastebin.cc/placeholder + description: + "If your Modmail bot is online, type `@modmail debug hastebin` and include the link here. + If your Modmail bot is not online or the previous command did not generate a link, do the following: + + 1. Select your *bot* application at https://dashboard.heroku.com + 2. [Restart your bot](https://i.imgur.com/3FcrlKz.png) + 3. Reproduce the error to populate the error logs + 4. [Copy and paste the logs](https://i.imgur.com/TTrhitm.png)" + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: "[optional] You may add screenshots to further explain your problem." + - type: textarea + id: additional-info + attributes: + label: Additional Information + description: "[optional] You may provide additional context for us to better understand how this issue occured." \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/command-request.md b/.github/ISSUE_TEMPLATE/command-request.md deleted file mode 100644 index d3c1673a6b..0000000000 --- a/.github/ISSUE_TEMPLATE/command-request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Command request -about: Request a new command -title: "your title here" -labels: command-request -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Who will this benefit** -Does this feature apply to a great portion of users? - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..e9a2d9fcd4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Discord Server + url: https://discord.gg/etJNHCQ + about: Please ask hosting-related questions here before creating an issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 7cd7a506cd..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "your title here" -labels: feature-request -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Who will this benefit** -Does this feature apply to a great portion of users? - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..87daea485a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,46 @@ +name: Feature Request +description: Suggest an idea for this project +title: "your feature request title" +labels: "feature-request" +body: + - type: textarea + id: problem-relation + attributes: + label: Is your feature request related to a problem? Please elaborate. + description: A clear and concise description of what the problem is. + placeholder: eg. I'm always frustrated when... + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: checkboxes + id: complications + attributes: + label: Does your solution involve any of the following? + options: + - label: Logviewer + - label: New config option + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + - type: textarea + id: benefit + attributes: + label: Who will this benefit? + description: Does this feature apply to a great portion of users? + validations: + required: true + - type: textarea + id: additional-info + attributes: + label: Additional Information + description: "[optional] You may provide additional context or screenshots for us to better understand the need of the feature." From bbe4c00d5c2157c27dfb604e906a47d55887fe3e Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 26 Jun 2021 18:04:21 +0800 Subject: [PATCH 0887/1221] formatting --- .github/ISSUE_TEMPLATE/bug_report.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 77461f9d01..99779ab4f3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -27,15 +27,19 @@ body: - type: input id: logs attributes: - label: Error logs + label: Error Logs placeholder: https://hastebin.cc/placeholder description: "If your Modmail bot is online, type `@modmail debug hastebin` and include the link here. + If your Modmail bot is not online or the previous command did not generate a link, do the following: - 1. Select your *bot* application at https://dashboard.heroku.com - 2. [Restart your bot](https://i.imgur.com/3FcrlKz.png) - 3. Reproduce the error to populate the error logs + 1. Select your *bot* application at https://dashboard.heroku.com + + 2. [Restart your bot](https://i.imgur.com/3FcrlKz.png) + + 3. Reproduce the error to populate the error logs + 4. [Copy and paste the logs](https://i.imgur.com/TTrhitm.png)" validations: required: true @@ -48,4 +52,4 @@ body: id: additional-info attributes: label: Additional Information - description: "[optional] You may provide additional context for us to better understand how this issue occured." \ No newline at end of file + description: "[optional] You may provide additional context for us to better understand how this issue occured." From 5567071fbcd8eeabcd2044b8551b0210788ffb20 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Sat, 3 Jul 2021 06:08:31 +0800 Subject: [PATCH 0888/1221] Update SPONSORS.json --- SPONSORS.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 53034c57b3..1f1ed56d37 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -46,7 +46,7 @@ { "embed": { "description": "Quality Hosting at Prices You Deserve!", - "color": 50195251, + "color": 3137203, "footer": { "icon_url": "https://primeserversinc.com/images/Prime_Logo_P_Sassy.png", "text": "Prime Servers, Inc." From a69d79e4db86a691957eff79bc7a99e0cb24e638 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 4 Jul 2021 18:20:51 +0800 Subject: [PATCH 0889/1221] fix issue template --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 87daea485a..b6a4437f2d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,7 +1,7 @@ name: Feature Request description: Suggest an idea for this project title: "your feature request title" -labels: "feature-request" +labels: "feature request" body: - type: textarea id: problem-relation From 04dc08b387f70d6e9df37bf6e059516678a91f02 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Wed, 7 Jul 2021 12:13:06 +0800 Subject: [PATCH 0890/1221] Update requirements system and removed poetry --- Pipfile | 40 +-- Pipfile.lock | 791 +++++++++++++++++++++---------------------- poetry.lock | 455 ------------------------- pyproject.toml | 38 --- requirements.min.txt | 16 - requirements.txt | 29 ++ runtime.txt | 1 - 7 files changed, 436 insertions(+), 934 deletions(-) delete mode 100644 poetry.lock delete mode 100644 requirements.min.txt create mode 100644 requirements.txt delete mode 100644 runtime.txt diff --git a/Pipfile b/Pipfile index f9621c3493..eeb21b15e7 100644 --- a/Pipfile +++ b/Pipfile @@ -3,31 +3,27 @@ name = "pypi" url = "https://pypi.org/simple" verify_ssl = true -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - [dev-packages] -black = "==19.10b0" -pylint = "*" -bandit = "==1.6.2" -flake8 = "*" +black = "==21.6b0" +pylint = "~=2.9.3" +bandit = "~=1.7.0" +flake8 = "~=3.9.2" [packages] -colorama = ">=0.4.0" -python-dateutil = ">=2.7.0" -emoji = ">=0.2" -uvloop = {version = ">=0.12.0",sys_platform = "!= 'win32'"} -motor = ">=2.0.0" -natural = "==0.2.0" -isodate = ">=0.6.0" -dnspython = "~=1.16.0" -parsedatetime = "==2.6" -aiohttp = ">=3.6.0,<3.7.0" -python-dotenv = ">=0.10.3" -pipenv = "*" -"discord.py" = "==1.6.0" +colorama = "~=0.4.4" +python-dateutil = "~=2.8.1" +emoji = "~=1.2.0" +uvloop = {version = ">=0.15.2",sys_platform = "!= 'win32'"} +motor = "~=2.4.0" +natural = "~=0.2.0" +isodate = "~=0.6.0" +parsedatetime = "~=2.6" +aiohttp = "==3.7.4.post0" +python-dotenv = ">=0.18.0" +"discord.py" = "==1.7.3" [scripts] bot = "python bot.py" + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index aaa491011e..cfaece3094 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,16 +1,13 @@ { "_meta": { "hash": { - "sha256": "bfe06c9fe1db25178b01413114093b26f0d32e9a90a031c048ff3ddc7b6644b7" + "sha256": "0700f9473d2588f168a63141ac5f44596d303e7a55db173968edddd0ace2e2ac" }, "pipfile-spec": 6, - "requires": {}, + "requires": { + "python_version": "3.9" + }, "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - }, { "name": "pypi", "url": "https://pypi.org/simple", @@ -21,29 +18,46 @@ "default": { "aiohttp": { "hashes": [ - "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe", - "sha256:206c0ccfcea46e1bddc91162449c20c72f308aebdcef4977420ef329c8fcc599", - "sha256:2ad493de47a8f926386fa6d256832de3095ba285f325db917c7deae0b54a9fc8", - "sha256:319b490a5e2beaf06891f6711856ea10591cfe84fe9f3e71a721aa8f20a0872a", - "sha256:470e4c90da36b601676fe50c49a60d34eb8c6593780930b1aa4eea6f508dfa37", - "sha256:60f4caa3b7f7a477f66ccdd158e06901e1d235d572283906276e3803f6b098f5", - "sha256:66d64486172b032db19ea8522328b19cfb78a3e1e5b62ab6a0567f93f073dea0", - "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c", - "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645", - "sha256:797456399ffeef73172945708810f3277f794965eb6ec9bd3a0c007c0476be98", - "sha256:a885432d3cabc1287bcf88ea94e1826d3aec57fd5da4a586afae4591b061d40d", - "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81", - "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5" + "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", + "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe", + "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5", + "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8", + "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd", + "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb", + "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c", + "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87", + "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0", + "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290", + "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5", + "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287", + "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde", + "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf", + "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8", + "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16", + "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf", + "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809", + "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213", + "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f", + "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013", + "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b", + "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9", + "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5", + "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb", + "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df", + "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4", + "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439", + "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f", + "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22", + "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f", + "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5", + "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970", + "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009", + "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc", + "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", + "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" ], "index": "pypi", - "version": "==3.6.3" - }, - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" + "version": "==3.7.4.post0" }, "async-timeout": { "hashes": [ @@ -55,25 +69,19 @@ }, "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.3.0" - }, - "certifi": { - "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" ], - "version": "==2020.12.5" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" }, "colorama": { "hashes": [ @@ -85,48 +93,27 @@ }, "discord.py": { "hashes": [ - "sha256:3df148daf6fbcc7ab5b11042368a3cd5f7b730b62f09fb5d3cbceff59bcfbb12", - "sha256:ba8be99ff1b8c616f7b6dcb700460d0222b29d4c11048e74366954c465fdd05f" + "sha256:462cd0fe307aef8b29cbfa8dd613e548ae4b2cb581d46da9ac0d46fb6ea19408", + "sha256:c6f64db136de0e18e090f6752ea68bdd4ab0a61b82dfe7acecefa22d6477bb0c" ], "index": "pypi", - "version": "==1.6.0" - }, - "distlib": { - "hashes": [ - "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", - "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" - ], - "version": "==0.3.1" - }, - "dnspython": { - "hashes": [ - "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", - "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d" - ], - "index": "pypi", - "version": "==1.16.0" + "version": "==1.7.3" }, "emoji": { "hashes": [ - "sha256:e42da4f8d648f8ef10691bc246f682a1ec6b18373abfd9be10ec0b398823bd11" + "sha256:496f432058567985838c13d67dde84ca081614a8286c0b9cdc7d63dfa89d51a3", + "sha256:6b19b65da8d6f30551eead1705539cc0eadcd9e33a6ecbc421a29b87f96287eb" ], "index": "pypi", - "version": "==0.6.0" - }, - "filelock": { - "hashes": [ - "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", - "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" - ], - "version": "==3.0.12" + "version": "==1.2.0" }, "idna": { "hashes": [ - "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", - "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], - "markers": "python_version >= '3.4'", - "version": "==3.1" + "markers": "python_version >= '3.5'", + "version": "==3.2" }, "isodate": { "hashes": [ @@ -138,34 +125,54 @@ }, "motor": { "hashes": [ - "sha256:428d94750123d19fcd0a89b8671ff9b4656f205217bad9f44161748c64c5fc80", - "sha256:f1692b760d834707e3477996ce8d407af8cd61c1a2abedbf81c22ef14675e61a" + "sha256:1196db507142ef8f00d953efa2f37b39335ef2d72af6ce4fbccfd870b65c5e9f", + "sha256:839c11a43897dbec8e5ba0e87a9c9b877239803126877b2efa5cef89aa6b687a" ], "index": "pypi", - "version": "==2.3.0" + "version": "==2.4.0" }, "multidict": { "hashes": [ - "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", - "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", - "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", - "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", - "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", - "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", - "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", - "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", - "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", - "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", - "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", - "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", - "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", - "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", - "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", - "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", - "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", + "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93", + "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632", + "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656", + "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79", + "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7", + "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d", + "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5", + "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224", + "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26", + "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea", + "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348", + "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6", + "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76", + "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1", + "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f", + "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952", + "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a", + "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37", + "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9", + "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359", + "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8", + "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da", + "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3", + "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d", + "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf", + "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841", + "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d", + "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93", + "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f", + "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647", + "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635", + "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456", + "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda", + "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5", + "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", + "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], - "markers": "python_version >= '3.5'", - "version": "==4.7.6" + "markers": "python_version >= '3.6'", + "version": "==5.1.0" }, "natural": { "hashes": [ @@ -182,82 +189,74 @@ "index": "pypi", "version": "==2.6" }, - "pipenv": { - "hashes": [ - "sha256:4ab2f60742184d851ac44b9e1d423afe71dc2ea7a68bde07eb890c8b4ce5a420", - "sha256:8253fe6f9cfb3791a54da8a0571f73c918cb3457dd908684c1800a13a06ec4c1" - ], - "index": "pypi", - "version": "==2020.11.15" - }, "pymongo": { "hashes": [ - "sha256:019ddf7ced8e42cc6c8c608927c799be8097237596c94ffe551f6ef70e55237e", - "sha256:047c325c4a96e7be7d11acf58639bcf71a81ca212d9c6590e3369bc28678647a", - "sha256:047cc2007b280672ddfdf2e7b862aad8d898f481f65bbc9067bfa4e420a019a9", - "sha256:061d59f525831c4051af0b6dbafa62b0b8b168d4ef5b6e3c46d0811b8499d100", - "sha256:082832a59da18efab4d9148cca396451bac99da9757f31767f706e828b5b8500", - "sha256:0a53a751d977ad02f1bd22ddb6288bb4816c4758f44a50225462aeeae9cbf6a0", - "sha256:1222025db539641071a1b67f6950f65a6342a39db5b454bf306abd6954f1ad8a", - "sha256:1580fad512c678b720784e5c9018621b1b3bd37fb5b1633e874738862d6435c7", - "sha256:202ea1d4edc8a5439fc179802d807b49e7e563207fea5610779e56674ac770c6", - "sha256:21d7b48567a1c80f9266e0ab61c1218a31279d911da345679188733e354f81cc", - "sha256:264843ce2af0640994a4331148ef5312989bc004678c457460758766c9b4decc", - "sha256:270a1f6a331eac3a393090af06df68297cb31a8b2df0bdcbd97dc613c5758e78", - "sha256:29a6840c2ac778010547cad5870f3db2e080ad7fad01197b07fff993c08692c8", - "sha256:3646c2286d889618d43e01d9810ac1fc17709d2b4dec61366df5edc8ba228b3e", - "sha256:36b9b98a39565a8f33803c81569442b35e749a72fb1aa7d0bcdb1a33052f8bcc", - "sha256:3ec8f8e106a1476659d8c020228b45614daabdbdb6c6454a843a1d4f77d13339", - "sha256:422069f2cebf58c9dd9e8040b4768f7be4f228c95bc4505e8fa8e7b4f7191ad8", - "sha256:44376a657717de8847d5d71a9305f3595c7e78c91ac77edbb87058d12ede87a6", - "sha256:45728e6aae3023afb5b2829586d1d2bfd9f0d71cfd7d3c924b71a5e9aef617a8", - "sha256:46792b71ab802d9caf1fc9d52e83399ef8e1a36e91eef4d827c06e36b8df2230", - "sha256:4942a5659ae927bb764a123a6409870ca5dd572d83b3bfb71412c9a191bbf792", - "sha256:4be4fe9d18523da98deeb0b554ac76e1dc1562ee879d62572b34dda8593efcc1", - "sha256:523804bd8fcb5255508052b50073a27c701b90a73ea46e29be46dad5fe01bde6", - "sha256:540dafd6f4a0590fc966465c726b80fa7c0804490c39786ef29236fe68c94401", - "sha256:5980509801cbd2942df31714d055d89863684b4de26829c349362e610a48694e", - "sha256:5ad7b96c27acd7e256b33f47cf3d23bd7dd902f9c033ae43f32ffcbc37bebafd", - "sha256:6122470dfa61d4909b75c98012c1577404ba4ab860d0095e0c6980560cb3711f", - "sha256:6175fd105da74a09adb38f93be96e1f64873294c906e5e722cbbc5bd10c44e3b", - "sha256:646d4d30c5aa7c0ddbfe9b990f0f77a88621024a21ad0b792bd9d58caa9611f0", - "sha256:6700e251c6396cc05d7460dc05ef8e19e60a7b53b62c007725b48e123aaa2b1c", - "sha256:6aac7e0e8de92f11a410eb68c24a2decbac6f094e82fd95d22546d0168e7a18b", - "sha256:6e7a6057481a644970e43475292e1c0af095ca39a20fe83781196bd6e6690a38", - "sha256:76579fcf77052b39796fe4a11818d1289dd48cffe15951b3403288fa163c29f6", - "sha256:7e69fa025a1db189443428f345fea5555d16413df6addc056e17bb8c9794b006", - "sha256:7f0c507e1f108790840d6c4b594019ebf595025c324c9f7e9c9b2b15b41f884e", - "sha256:813db97e9955b6b1b50b5cebd18cb148580603bb9b067ea4c5cc656b333bc906", - "sha256:82d5ded5834b6c92380847860eb28dcaf20b847a27cee5811c4aaceef87fd280", - "sha256:82f6e42ba40440a7e0a20bfe12465a3b62d65966a4c7ad1a21b36ffff88de6fe", - "sha256:8d669c720891781e7c82d412cad39f9730ef277e3957b48a3344dae47d3caa03", - "sha256:944ed467feb949e103555863fa934fb84216a096b0004ca364d3ddf9d18e2b9e", - "sha256:96c6aef7ffb0d37206c0342abb82d874fa8cdc344267277ec63f562b94335c22", - "sha256:9be785bd4e1ba0148fb00ca84e4dbfbd1c74df3af3a648559adc60b0782f34de", - "sha256:9d19843568df9d263dc92ae4cc2279879add8a26996473f9155590cac635b321", - "sha256:a118a1df7280ffab7fe0f3eab325868339ff1c4d5b8e0750db0f0a796da8f849", - "sha256:b4294ddf76452459433ecfa6a93258608b5e462c76ef15e4695ed5e2762f009f", - "sha256:b50af6701b4a5288b77fb4db44a363aa9485caf2c3e7a40c0373fd45e34440af", - "sha256:b875bb4b438931dce550e170bfb558597189b8d0160f4ac60f14a21955161699", - "sha256:b95d2c2829b5956bf54d9a22ffec911dea75abf0f0f7e0a8a57423434bfbde91", - "sha256:c046e09e886f4539f8626afba17fa8f2e6552731f9384e2827154e3e3b7fda4e", - "sha256:c1d1992bbdf363b22b5a9543ab7d7c6f27a1498826d50d91319b803ddcf1142e", - "sha256:c2b67881392a9e85aa108e75f62cdbe372d5a3f17ea5f8d3436dcf4662052f14", - "sha256:c6cf288c9e03195d8e12b72a6388b32f18a5e9c2545622417a963e428e1fe496", - "sha256:c812b6e53344e92f10f12235219fb769c491a4a87a02c9c3f93fe632e493bda8", - "sha256:cc421babc687dc52ce0fc19787b2404518ca749d9db59576100946ff886f38ed", - "sha256:ce53c00be204ec4428d3c1f3c478ae89d388efec575544c27f57b61e9fa4a7f2", - "sha256:ce9964c117cbe5cf6269f30a2b334d28675956e988b7dbd0b4f7370924afda2e", - "sha256:d6f82e86896a8db70e8ae8fa4b7556a0f188f1d8a6c53b2ba229889d55a59308", - "sha256:d9d3ae537f61011191b2fd6f8527b9f9f8a848b37d4c85a0f7bb28004c42b546", - "sha256:e565d1e4388765c135052717f15f9e0314f9d172062444c6b3fc0002e93ed04b", - "sha256:ed98683d8f01f1c46ef2d02469e04e9a8fe9a73a9741a4e6e66677a73b59bec8", - "sha256:ef18aa15b1aa18c42933deed5233b3284186e9ed85c25d2704ceff5099a3964c", - "sha256:fa741e9c805567239f845c7e9a016aff797f9bb02ff9bc8ccd2fbd9eafefedd4", - "sha256:fc4946acb6cdada08f60aca103b61334995523da65be5fe816ea8571c9967d46", - "sha256:fcc66d17a3363b7bd6d2655de8706e25a3cd1be2bd1b8e8d8a5c504a6ef893ae" - ], - "version": "==3.11.2" + "sha256:03be7ad107d252bb7325d4af6309fdd2c025d08854d35f0e7abc8bf048f4245e", + "sha256:071552b065e809d24c5653fcc14968cfd6fde4e279408640d5ac58e3353a3c5f", + "sha256:08b8723248730599c9803ae4c97b8f3f76c55219104303c88cb962a31e3bb5ee", + "sha256:08bda7b2c522ff9f1e554570da16298271ebb0c56ab9699446aacba249008988", + "sha256:0aaf4d44f1f819360f9432df538d54bbf850f18152f34e20337c01b828479171", + "sha256:0cabfc297f4cf921f15bc789a8fbfd7115eb9f813d3f47a74b609894bc66ab0d", + "sha256:13acf6164ead81c9fc2afa0e1ea6d6134352973ce2bb35496834fee057063c04", + "sha256:15b083d1b789b230e5ac284442d9ecb113c93f3785a6824f748befaab803b812", + "sha256:161fcd3281c42f644aa8dec7753cca2af03ce654e17d76da4f0dab34a12480ca", + "sha256:1a994a42f49dab5b6287e499be7d3d2751776486229980d8857ad53b8333d469", + "sha256:20d75ea11527331a2980ab04762a9d960bcfea9475c54bbeab777af880de61cd", + "sha256:225c61e08fe517aede7912937939e09adf086c8e6f7e40d4c85ad678c2c2aea3", + "sha256:3135dd574ef1286189f3f04a36c8b7a256376914f8cbbce66b94f13125ded858", + "sha256:3491c7de09e44eded16824cb58cf9b5cc1dc6f066a0bb7aa69929d02aa53b828", + "sha256:3551912f5c34d8dd7c32c6bb00ae04192af47f7b9f653608f107d19c1a21a194", + "sha256:38a7b5140a48fc91681cdb5cb95b7cd64640b43d19259fdd707fa9d5a715f2b2", + "sha256:3a3498a8326111221560e930f198b495ea6926937e249f475052ffc6893a6680", + "sha256:3bfc7689a1bacb9bcd2f2d5185d99507aa29f667a58dd8adaa43b5a348139e46", + "sha256:421d13523d11c57f57f257152bc4a6bb463aadf7a3918e9c96fefdd6be8dbfb8", + "sha256:424799c71ff435094e5fb823c40eebb4500f0e048133311e9c026467e8ccebac", + "sha256:474e21d0e07cd09679e357d1dac76e570dab86665e79a9d3354b10a279ac6fb3", + "sha256:4c7e8c8e1e1918dcf6a652ac4b9d87164587c26fd2ce5dd81e73a5ab3b3d492f", + "sha256:506a6dab4c7ffdcacdf0b8e70bd20eb2e77fa994519547c9d88d676400fcad58", + "sha256:510cd3bfabb63a07405b7b79fae63127e34c118b7531a2cbbafc7a24fd878594", + "sha256:517ba47ca04a55b1f50ee8df9fd97f6c37df5537d118fb2718952b8623860466", + "sha256:539d4cb1b16b57026999c53e5aab857fe706e70ae5310cc8c232479923f932e6", + "sha256:5c36428cc4f7fae56354db7f46677fd21222fc3cb1e8829549b851172033e043", + "sha256:5db59223ed1e634d842a053325f85f908359c6dac9c8ddce8ef145061fae7df8", + "sha256:5e606846c049ed40940524057bfdf1105af6066688c0e6a1a3ce2038589bae70", + "sha256:6060794aac9f7b0644b299f46a9c6cbc0bc470bd01572f4134df140afd41ded6", + "sha256:62c29bc36a6d9be68fe7b5aaf1e120b4aa66a958d1e146601fcd583eb12cae7b", + "sha256:73326b211e7410c8bd6a74500b1e3f392f39cf10862e243d00937e924f112c01", + "sha256:78f07961f4f214ea8e80be63cffd5cc158eb06cd922ffbf6c7155b11728f28f9", + "sha256:7c97554ea521f898753d9773891d0347ebfaddcc1dee2ad94850b163171bf1f1", + "sha256:8898f6699f740ca93a0879ed07d8e6db02d68af889d0ebb3d13ab017e6b1af1e", + "sha256:8a41fdc751dc4707a4fafb111c442411816a7c225ebb5cadb57599534b5d5372", + "sha256:8e0004b0393d72d76de94b4792a006cb960c1c65c7659930fbf9a81ce4341982", + "sha256:977b1d4f868986b4ba5d03c317fde4d3b66e687d74473130cd598e3103db34fa", + "sha256:9a4f6e0b01df820ba9ed0b4e618ca83a1c089e48d4f268d0e00dcd49893d4549", + "sha256:9b9298964389c180a063a9e8bac8a80ed42de11d04166b20249bfa0a489e0e0f", + "sha256:a08c8b322b671857c81f4c30cd3c8df2895fd3c0e9358714f39e0ef8fb327702", + "sha256:ad31f184dcd3271de26ab1f9c51574afb99e1b0e484ab1da3641256b723e4994", + "sha256:aff3656af2add93f290731a6b8930b23b35c0c09569150130a58192b3ec6fc61", + "sha256:b2f41261b648cf5dee425f37ff14f4ad151c2f24b827052b402637158fd056ef", + "sha256:b413117210fa6d92664c3d860571e8e8727c3e8f2ff197276c5d0cb365abd3ad", + "sha256:b7efc7e7049ef366777cfd35437c18a4166bb50a5606a1c840ee3b9624b54fc9", + "sha256:b8f94acd52e530a38f25e4d5bf7ddfdd4bea9193e718f58419def0d4406b58d3", + "sha256:d0a70151d7de8a3194cdc906bcc1a42e14594787c64b0c1c9c975e5a2af3e251", + "sha256:d360e5d5dd3d55bf5d1776964625018d85b937d1032bae1926dd52253decd0db", + "sha256:d4e62417e89b717a7bcd8576ac3108cd063225942cc91c5b37ff5465fdccd386", + "sha256:d65bac5f6724d9ea6f0b5a0f0e4952fbbf209adcf6b5583b54c54bd2fcd74dc0", + "sha256:e02beaab433fd1104b2804f909e694cfbdb6578020740a9051597adc1cd4e19f", + "sha256:e4b631688dfbdd61b5610e20b64b99d25771c6d52d9da73349342d2a0f11c46a", + "sha256:e4e9db78b71db2b1684ee4ecc3e32c4600f18cdf76e6b9ae03e338e52ee4b168", + "sha256:eb4d176394c37a76e8b0afe54b12d58614a67a60a7f8c0dd3a5afbb013c01092", + "sha256:f08665d3cc5abc2f770f472a9b5f720a9b3ab0b8b3bb97c7c1487515e5653d39", + "sha256:f3d851af3852f16ad4adc7ee054fd9c90a7a5063de94d815b7f6a88477b9f4c6", + "sha256:f4ba58157e8ae33ee86fadf9062c506e535afd904f07f9be32731f4410a23b7f", + "sha256:f664ed7613b8b18f0ce5696b146776266a038c19c5cd6efffa08ecc189b01b73", + "sha256:f947b359cc4769af8b49be7e37af01f05fcf15b401da2528021148e4a54426d1", + "sha256:fe4189846448df013cd9df11bba38ddf78043f8c290a9f06430732a7a8601cce", + "sha256:fea5cb1c63efe1399f0812532c7cf65458d38fd011be350bc5021dfcac39fba8", + "sha256:fedf0dee7a412ca6d1d6d92c158fe9cbaa8ea0cae90d268f9ccc0744de7a97d0", + "sha256:fffff7bfb6799a763d3742c59c6ee7ffadda21abed557637bc44ed1080876484" + ], + "version": "==3.11.4" }, "python-dateutil": { "hashes": [ @@ -269,73 +268,87 @@ }, "python-dotenv": { "hashes": [ - "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e", - "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0" + "sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d", + "sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d" ], "index": "pypi", - "version": "==0.15.0" + "version": "==0.18.0" }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==1.15.0" - }, - "uvloop": { - "hashes": [ - "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", - "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", - "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", - "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", - "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", - "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", - "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", - "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", - "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "sys_platform != 'win32'", - "version": "==0.14.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" }, - "virtualenv": { + "typing-extensions": { "hashes": [ - "sha256:54b05fc737ea9c9ee9f8340f579e5da5b09fb64fd010ab5757eb90268616907c", - "sha256:b7a8ec323ee02fb2312f098b6b4c9de99559b462775bc8fe3627a73706603c1b" + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.2.2" + "version": "==3.10.0.0" }, - "virtualenv-clone": { + "uvloop": { "hashes": [ - "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", - "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" + "sha256:114543c84e95df1b4ff546e6e3a27521580466a30127f12172a3278172ad68bc", + "sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69", + "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01", + "sha256:42eda9f525a208fbc4f7cecd00fa15c57cc57646c76632b3ba2fe005004f051d", + "sha256:44cac8575bf168601424302045234d74e3561fbdbac39b2b54cc1d1d00b70760", + "sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c", + "sha256:7ae39b11a5f4cec1432d706c21ecc62f9e04d116883178b09671aa29c46f7a47", + "sha256:90e56f17755e41b425ad19a08c41dc358fa7bf1226c0f8e54d4d02d556f7af7c", + "sha256:b45218c99795803fb8bdbc9435ff7f54e3a591b44cd4c121b02fa83affb61c7c", + "sha256:e5e5f855c9bf483ee6cd1eb9a179b740de80cb0ae2988e3fa22309b78e2ea0e7" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.5.4" + "index": "pypi", + "markers": "sys_platform != 'win32'", + "version": "==0.15.2" }, "yarl": { "hashes": [ - "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", - "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", - "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", - "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", - "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", - "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", - "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", - "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", - "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", - "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", - "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", - "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", - "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", - "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", - "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", - "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", - "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", + "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", + "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", + "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", + "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", + "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", + "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", + "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", + "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", + "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", + "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", + "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", + "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", + "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", + "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", + "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", + "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", + "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", + "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", + "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", + "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", + "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", + "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", + "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", + "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", + "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", + "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", + "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", + "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", + "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", + "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", + "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", + "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", + "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", + "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", + "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", + "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], - "markers": "python_version >= '3.5'", - "version": "==1.5.1" + "markers": "python_version >= '3.6'", + "version": "==1.6.3" } }, "develop": { @@ -348,110 +361,95 @@ }, "astroid": { "hashes": [ - "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", - "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" - ], - "markers": "python_version >= '3.5'", - "version": "==2.4.2" - }, - "attrs": { - "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892", + "sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.3.0" + "markers": "python_version ~= '3.6'", + "version": "==2.6.2" }, "bandit": { "hashes": [ - "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", - "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065" + "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07", + "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608" ], "index": "pypi", - "version": "==1.6.2" + "version": "==1.7.0" }, "black": { "hashes": [ - "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", - "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" + "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04", + "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7" ], "index": "pypi", - "version": "==19.10b0" + "version": "==21.6b0" }, "click": { "hashes": [ - "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", - "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==7.1.2" - }, - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "index": "pypi", - "version": "==0.4.4" + "markers": "python_version >= '3.6'", + "version": "==8.0.1" }, "flake8": { "hashes": [ - "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", - "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" + "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", + "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" ], "index": "pypi", - "version": "==3.8.4" + "version": "==3.9.2" }, "gitdb": { "hashes": [ - "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", - "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" + "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", + "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" ], "markers": "python_version >= '3.4'", - "version": "==4.0.5" + "version": "==4.0.7" }, "gitpython": { "hashes": [ - "sha256:42dbefd8d9e2576c496ed0059f3103dcef7125b9ce16f9d5f9c834aed44a1dac", - "sha256:867ec3dfb126aac0f8296b19fb63b8c4a399f32b4b6fafe84c4b10af5fa9f7b5" + "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b", + "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8" ], - "markers": "python_version >= '3.4'", - "version": "==3.1.12" + "markers": "python_version >= '3.6'", + "version": "==3.1.18" }, "isort": { "hashes": [ - "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e", - "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc" + "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56", + "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==5.7.0" + "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", + "version": "==5.9.1" }, "lazy-object-proxy": { "hashes": [ - "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", - "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", - "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", - "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", - "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", - "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", - "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", - "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", - "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", - "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", - "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", - "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", - "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", - "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", - "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", - "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", - "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", - "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", - "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", - "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", - "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.3" + "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", + "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", + "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", + "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", + "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", + "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", + "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", + "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", + "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", + "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", + "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", + "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", + "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", + "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", + "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", + "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", + "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", + "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", + "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", + "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", + "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", + "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.6.0" }, "mccabe": { "hashes": [ @@ -460,6 +458,13 @@ ], "version": "==0.6.1" }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, "pathspec": { "hashes": [ "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", @@ -469,115 +474,132 @@ }, "pbr": { "hashes": [ - "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", - "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" + "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", + "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" ], "markers": "python_version >= '2.6'", - "version": "==5.5.1" + "version": "==5.6.0" }, "pycodestyle": { "hashes": [ - "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", - "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" + "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", + "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.6.0" + "version": "==2.7.0" }, "pyflakes": { "hashes": [ - "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", - "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", + "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.2.0" + "version": "==2.3.1" }, "pylint": { "hashes": [ - "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", - "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" + "sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a", + "sha256:5d46330e6b8886c31b5e3aba5ff48c10f4aa5e76cbf9002c6544306221e63fbc" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.9.3" }, "pyyaml": { "hashes": [ - "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", - "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", - "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", - "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", - "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", - "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", - "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", - "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", - "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", - "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", - "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", - "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", - "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" - ], - "version": "==5.3.1" + "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", + "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", + "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", + "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", + "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", + "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", + "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", + "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", + "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", + "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", + "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", + "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", + "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", + "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", + "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", + "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", + "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", + "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", + "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", + "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", + "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", + "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", + "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==5.4.1" }, "regex": { "hashes": [ - "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538", - "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4", - "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc", - "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa", - "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444", - "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1", - "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af", - "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8", - "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9", - "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88", - "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba", - "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364", - "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e", - "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7", - "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0", - "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31", - "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683", - "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee", - "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b", - "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884", - "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c", - "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e", - "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562", - "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85", - "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c", - "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6", - "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d", - "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b", - "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70", - "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b", - "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b", - "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f", - "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0", - "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5", - "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5", - "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f", - "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e", - "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512", - "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d", - "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917", - "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f" - ], - "version": "==2020.11.13" + "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f", + "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad", + "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a", + "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf", + "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59", + "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d", + "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895", + "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4", + "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3", + "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222", + "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0", + "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c", + "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417", + "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d", + "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d", + "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761", + "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0", + "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026", + "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854", + "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb", + "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d", + "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068", + "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde", + "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d", + "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec", + "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa", + "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd", + "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b", + "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26", + "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2", + "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f", + "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694", + "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0", + "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407", + "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874", + "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035", + "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d", + "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c", + "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5", + "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985", + "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58" + ], + "version": "==2021.7.6" }, "six": { "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==1.15.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" }, "smmap": { "hashes": [ - "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", - "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" + "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", + "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==3.0.4" + "markers": "python_version >= '3.5'", + "version": "==4.0.0" }, "stevedore": { "hashes": [ @@ -592,44 +614,9 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, - "typed-ast": { - "hashes": [ - "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", - "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", - "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", - "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", - "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", - "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", - "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", - "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", - "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", - "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", - "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", - "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", - "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", - "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", - "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", - "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", - "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", - "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", - "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", - "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", - "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", - "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", - "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", - "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", - "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", - "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", - "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", - "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", - "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", - "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" - ], - "version": "==1.4.2" - }, "wrapt": { "hashes": [ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 4aa8248546..0000000000 --- a/poetry.lock +++ /dev/null @@ -1,455 +0,0 @@ -[[package]] -category = "main" -description = "Async http client/server framework (asyncio)" -name = "aiohttp" -optional = false -python-versions = ">=3.5.3" -version = "3.6.2" - -[package.dependencies] -async-timeout = ">=3.0,<4.0" -attrs = ">=17.3.0" -chardet = ">=2.0,<4.0" -multidict = ">=4.5,<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.4" - -[[package]] -category = "dev" -description = "An abstract syntax tree for Python with inference support." -name = "astroid" -optional = false -python-versions = ">=3.5" -version = "2.4.1" - -[package.dependencies] -lazy-object-proxy = ">=1.4.0,<1.5.0" -six = ">=1.12,<2.0" -wrapt = ">=1.11,<2.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.10b0" - -[package.dependencies] -appdirs = "*" -attrs = ">=18.1.0" -click = ">=6.5" -pathspec = ">=0.6,<1" -regex = "*" -toml = ">=0.9.4" -typed-ast = ">=1.4.0" - -[[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.*, !=3.4.*" -version = "7.1.2" - -[[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.3.3" - -[package.dependencies] -aiohttp = ">=3.6.0,<3.7.0" -websockets = ">=6.0,<7.0 || >7.0,<8.0 || >8.0,<8.0.1 || >8.0.1,<9.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 = "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 = "gitdb" -optional = false -python-versions = ">=3.4" -version = "4.0.5" - -[package.dependencies] -smmap = ">=3.0.1,<4" - -[[package]] -category = "dev" -description = "Python Git Library" -name = "gitpython" -optional = false -python-versions = ">=3.4" -version = "3.1.3" - -[package.dependencies] -gitdb = ">=4.0.1,<5" - -[[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.9" - -[[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.6" - -[[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.6" - -[[package]] -category = "dev" -description = "Utility library for gitignore style pattern matching of file paths." -name = "pathspec" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.8.0" - -[[package]] -category = "dev" -description = "Python Build Reasonableness" -name = "pbr" -optional = false -python-versions = "*" -version = "5.4.5" - -[[package]] -category = "dev" -description = "python code static checker" -name = "pylint" -optional = false -python-versions = ">=3.5.*" -version = "2.5.2" - -[package.dependencies] -astroid = ">=2.4.0,<=2.5" -colorama = "*" -isort = ">=4.2.5,<5" -mccabe = ">=0.6,<0.7" -toml = ">=0.7.1" - -[[package]] -category = "main" -description = "Python driver for MongoDB " -name = "pymongo" -optional = true -python-versions = "*" -version = "3.10.1" - -[[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.5" - -[[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.*, !=3.4.*" -version = "5.3.1" - -[[package]] -category = "dev" -description = "Alternative regular expression module, to replace re." -name = "regex" -optional = false -python-versions = "*" -version = "2020.6.7" - -[[package]] -category = "main" -description = "Python 2 and 3 compatibility utilities" -name = "six" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.15.0" - -[[package]] -category = "dev" -description = "A pure Python implementation of a sliding window memory map manager" -name = "smmap" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.0.4" - -[[package]] -category = "dev" -description = "Manage dynamic plugins for Python applications" -name = "stevedore" -optional = false -python-versions = ">=3.6" -version = "2.0.0" - -[package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" - -[[package]] -category = "dev" -description = "Python Library for Tom's Obvious, Minimal Language" -name = "toml" -optional = false -python-versions = "*" -version = "0.10.1" - -[[package]] -category = "dev" -description = "a fork of Python 2 and 3 ast modules with type comment support" -name = "typed-ast" -optional = false -python-versions = "*" -version = "1.4.1" - -[[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.6.1" -version = "8.1" - -[[package]] -category = "dev" -description = "Module for decorators, wrappers and monkey patching." -name = "wrapt" -optional = false -python-versions = "*" -version = "1.12.1" - -[[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" - -[extras] -mongodb = ["motor"] - -[metadata] -content-hash = "68d5c15e62c4bf5f65fd3b0a9a2586b15557f724c3de5b756534bacd52cbfe40" -python-versions = "^3.7" - -[metadata.hashes] -aiohttp = ["1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e", "259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326", "2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a", "32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654", "344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a", "460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4", "4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17", "50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec", "6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd", "65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48", "ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59", "b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"] -appdirs = ["7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", "a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"] -astroid = ["4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1", "d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"] -async-timeout = ["0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"] -attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] -bandit = ["336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", "41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"] -black = ["1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", "c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"] -chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] -click = ["d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"] -colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"] -"discord.py" = ["406871b06d86c3dc49fba63238519f28628dac946fef8a0e22988ff58ec05580", "ad00e34c72d2faa8db2157b651d05f3c415d7d05078e7e41dc9e8dc240051beb"] -dnspython = ["36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", "f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d"] -emoji = ["60652d3a2dcee5b8af8acb097c31776fb6d808027aeb7221830f72cdafefc174"] -futures = ["3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b", "51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd", "c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"] -gitdb = ["91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"] -gitpython = ["e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a", "ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"] -idna = ["7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"] -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 = ["1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", "275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", "3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", "4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", "5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", "51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", "5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", "6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", "7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", "9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", "c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", "d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", "e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", "f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", "fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", "fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"] -natural = ["18c83662d2d33fd7e6eee4e3b0d7366e1ce86225664e3127a2aaf0a3233f7df2"] -parsedatetime = ["4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", "cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b"] -pathspec = ["7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", "da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"] -pbr = ["07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c", "579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"] -pylint = ["b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", "dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b"] -pymongo = ["01b4e10027aef5bb9ecefbc26f5df3368ce34aef81df43850f701e716e3fe16d", "0fc5aa1b1acf7f61af46fe0414e6a4d0c234b339db4c03a63da48599acf1cbfc", "1396eb7151e0558b1f817e4b9d7697d5599e5c40d839a9f7270bd90af994ad82", "18e84a3ec5e73adcb4187b8e5541b2ad61d716026ed9863267e650300d8bea33", "19adf2848b80cb349b9891cc854581bbf24c338be9a3260e73159bdeb2264464", "20ee0475aa2ba437b0a14806f125d696f90a8433d820fb558fdd6f052acde103", "26798795097bdeb571f13942beef7e0b60125397811c75b7aa9214d89880dd1d", "26e707a4eb851ec27bb969b5f1413b9b2eac28fe34271fa72329100317ea7c73", "2a3c7ad01553b27ec553688a1e6445e7f40355fb37d925c11fcb50b504e367f8", "2f07b27dbf303ea53f4147a7922ce91a26b34a0011131471d8aaf73151fdee9a", "316f0cf543013d0c085e15a2c8abe0db70f93c9722c0f99b6f3318ff69477d70", "31d11a600eea0c60de22c8bdcb58cda63c762891facdcb74248c36713240987f", "334ef3ffd0df87ea83a0054454336159f8ad9c1b389e19c0032d9cb8410660e6", "358ba4693c01022d507b96a980ded855a32dbdccc3c9331d0667be5e967f30ed", "3a6568bc53103df260f5c7d2da36dffc5202b9a36c85540bba1836a774943794", "444bf2f44264578c4085bb04493bfed0e5c1b4fe7c2704504d769f955cc78fe4", "47a00b22c52ee59dffc2aad02d0bbfb20c26ec5b8de8900492bf13ad6901cf35", "4c067db43b331fc709080d441cb2e157114fec60749667d12186cc3fc8e7a951", "4c092310f804a5d45a1bcaa4191d6d016c457b6ed3982a622c35f729ff1c7f6b", "53b711b33134e292ef8499835a3df10909c58df53a2a0308f598c432e9a62892", "568d6bee70652d8a5af1cd3eec48b4ca1696fb1773b80719ebbd2925b72cb8f6", "56fa55032782b7f8e0bf6956420d11e2d4e9860598dfe9c504edec53af0fc372", "5a2c492680c61b440272341294172fa3b3751797b1ab983533a770e4fb0a67ac", "61235cc39b5b2f593086d1d38f3fc130b2d125bd8fc8621d35bc5b6bdeb92bd2", "619ac9aaf681434b4d4718d1b31aa2f0fce64f2b3f8435688fcbdc0c818b6c54", "6238ac1f483494011abde5286282afdfacd8926659e222ba9b74c67008d3a58c", "63752a72ca4d4e1386278bd43d14232f51718b409e7ac86bcf8810826b531113", "6fdc5ccb43864065d40dd838437952e9e3da9821b7eac605ba46ada77f846bdf", "7abc3a6825a346fa4621a6f63e3b662bbb9e0f6ffc32d30a459d695f20fb1a8b", "7aef381bb9ae8a3821abd7f9d4d93978dbd99072b48522e181baeffcd95b56ae", "80df3caf251fe61a3f0c9614adc6e2bfcffd1cd3345280896766712fb4b4d6d7", "95f970f34b59987dee6f360d2e7d30e181d58957b85dff929eee4423739bd151", "993257f6ca3cde55332af1f62af3e04ca89ce63c08b56a387cdd46136c72f2fa", "9c0a57390549affc2b5dda24a38de03a5c7cbc58750cd161ff5d106c3c6eec80", "a0794e987d55d2f719cc95fcf980fc62d12b80e287e6a761c4be14c60bd9fecc", "a3b98121e68bf370dd8ea09df67e916f93ea95b52fc010902312168c4d1aff5d", "a60756d55f0887023b3899e6c2923ba5f0042fb11b1d17810b4e07395404f33e", "a676bd2fbc2309092b9bbb0083d35718b5420af3a42135ebb1e4c3633f56604d", "a732838c78554c1257ff2492f5c8c4c7312d0aecd7f732149e255f3749edd5ee", "ad3dc88dfe61f0f1f9b99c6bc833ea2f45203a937a18f0d2faa57c6952656012", "ae65d65fde4135ef423a2608587c9ef585a3551fc2e4e431e7c7e527047581be", "b070a4f064a9edb70f921bfdc270725cff7a78c22036dd37a767c51393fb956f", "b6da85949aa91e9f8c521681344bd2e163de894a5492337fba8b05c409225a4f", "bbf47110765b2a999803a7de457567389253f8670f7daafb98e059c899ce9764", "bd9c1e6f92b4888ae3ef7ae23262c513b962f09f3fb3b48581dde5df7d7a860a", "c06b3f998d2d7160db58db69adfb807d2ec307e883e2f17f6b87a1ef6c723f11", "c318fb70542be16d3d4063cde6010b1e4d328993a793529c15a619251f517c39", "c4aef42e5fa4c9d5a99f751fb79caa880dac7eaf8a65121549318b984676a1b7", "c9ca545e93a9c2a3bdaa2e6e21f7a43267ff0813e8055adf2b591c13164c0c57", "da2c3220eb55c4239dd8b982e213da0b79023cac59fe54ca09365f2bc7e4ad32", "dd8055da300535eefd446b30995c0813cc4394873c9509323762a93e97c04c03", "e2b46e092ea54b732d98c476720386ff2ccd126de1e52076b470b117bff7e409", "e334c4f39a2863a239d38b5829e442a87f241a92da9941861ee6ec5d6380b7fe", "e5c54f04ca42bbb5153aec5d4f2e3d9f81e316945220ac318abd4083308143f5", "f4d06764a06b137e48db6d569dc95614d9d225c89842c885669ee8abc9f28c7a", "f96333f9d2517c752c20a35ff95de5fc2763ac8cdb1653df0f6f45d281620606"] -python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"] -python-dotenv = ["440c7c23d53b7d352f9c94d6f70860242c2f071cf5c029dd661ccb22d64ae42b", "f254bfd0c970d64ccbb6c9ebef3667ab301a71473569c991253a481f1c98dddc"] -pyyaml = ["06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", "69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", "b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"] -regex = ["150125da109fccdcc8fec3b0b386b2a5d6ca7cff076f8b622486d1ca868b0c10", "163bc0805e46acfa098dfc8c0b07f371577d505f603e48afc425ff475cdac3a5", "20c513893ff80bdbe4b4ce11ea2e93d49481f05b270595d82af69ffc402010a6", "21fc17cb868c4264f0813f992f46f9ae6fc8c309d4741091de4153bd1f6a6176", "2c928bc8e0c453d73dffa3193a6e37ee752ea36df0dd4601e21024d98274dfad", "2d9beca70e36f9c60d679e108c5fe49f3d4da79d13a13f91e5e759443bd954f9", "5735f26cacdb50b3d6d35ebf8fdeb504bd8b381e2d079d2d9f12ce534fc14ecd", "6edc5c190248d3b612f2cca45448cf8ebc3621d41afcd1c5708853cbb1dbb3b3", "7606dba82435429641efe4fbc580574942f89cf2b9c5c1f8bc1eab2bacbf7e8b", "8d1ee3796795e609ef7a3a5a35eaf4728038d986aa12c06b3fd1b92ee81911f4", "8d9bb2d90e23c51aacbc58c1a11320f49b335cd67a91986cdbebcc3e843e4de8", "97d414c41f19fd2362e493810caa8445c05e0a2d63a14081c972aad66284a8d2", "9e37502817225ee99d91d8418f5119e98c380b00e772d06915690c05290f32ee", "af7209b2fcc79ee2b0ad4ea080d70bb748450ec4f282cc9e864861e469b1072e", "c0849b0864ff451f04c8afb5fc28e9ed592262e03debdd227cf0f53e04a55dcd", "c4ac9215650688e78dea29b46adbdafb7b85058eebe92ef6ea848e14466c915f", "dcda6d4e1bbfc939b177c237aee41c9678eaaf71df482688f8986e8251e12345", "dd8501b8d9ea1aba53c4bc7d47bc72933f9b4213d534cf400f16c1431f51c8ba", "ec0e509ed1877ff1cbc6f0864689bb60384a303502c4d72d9a635f8a4676fd3f", "f6c8c3f56fef719180464855346e6e80971b86dfd9e5a0e356664b5baca53072", "ffd4f80602490a309064cf2b203e220d581c51660e01055c64bf5da450485ee6"] -six = ["30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"] -smmap = ["54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"] -stevedore = ["001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9", "471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691"] -toml = ["926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", "bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"] -typed-ast = ["0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", "249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", "24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", "269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", "4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", "498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", "4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", "6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", "715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", "73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", "8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", "8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", "aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", "bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", "c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", "d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", "d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", "d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", "fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"] -uvloop = ["08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", "123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", "4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", "4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", "afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", "b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", "bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", "e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", "f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"] -websockets = ["0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", "1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", "20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", "295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", "2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", "3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", "3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", "3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", "4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", "5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", "5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", "751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", "7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", "965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", "9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", "9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", "c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", "c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", "ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", "d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", "e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", "f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"] -wrapt = ["b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"] -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 ff08d3ee31..a920956e85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,41 +18,3 @@ exclude = ''' )/ ) ''' - -[tool.poetry] -name = 'Modmail' -version = '3.9.4' -description = "Modmail is similar to Reddit's 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" = "discord.py==1.6.0" -uvloop = {version = ">=0.12.0", markers = "sys_platform != 'win32'"} -python-dotenv = ">=0.10.3" -parsedatetime = "^2.6" -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.7.0" - -[tool.poetry.dev-dependencies] -black = {version = "=19.10b0", allow-prereleases = true} -pylint = "^2.4" -bandit = "^1.6" - -[tool.poetry.extras] -mongodb = ["motor"] diff --git a/requirements.min.txt b/requirements.min.txt deleted file mode 100644 index d756599661..0000000000 --- a/requirements.min.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Generated as of March, 2021 -# This is the bare minimum requirements.txt for running Modmail. -# To install requirements.txt run: pip install -r requirements.min.txt - -aiohttp==3.6.2 -discord.py==1.6.0 -dnspython==1.16.0 -emoji==0.5.4 -isodate==0.6.0 -motor==2.1.0 -natural==0.2.0 -parsedatetime==2.6 -pymongo==3.10.1 -python-dateutil==2.8.1 -python-dotenv==0.14.0 -websockets==8.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..7e1360e73e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,29 @@ +# +# These requirements were autogenerated by pipenv +# To regenerate from the project's Pipfile, run: +# +# pipenv lock --requirements +# + +-i https://pypi.org/simple +aiohttp==3.7.4.post0 +async-timeout==3.0.1; python_full_version >= '3.5.3' +attrs==21.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +colorama==0.4.4 +discord.py==1.7.3 +emoji==1.2.0 +idna==3.2; python_version >= '3.5' +isodate==0.6.0 +motor==2.4.0 +multidict==5.1.0; python_version >= '3.6' +natural==0.2.0 +parsedatetime==2.6 +pymongo==3.11.4 +python-dateutil==2.8.1 +python-dotenv==0.18.0 +six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +typing-extensions==3.10.0.0 +uvloop==0.15.2; sys_platform != 'win32' +wheel==0.36.2 +yarl==1.6.3; python_version >= '3.6' diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index 87665291b8..0000000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.9.4 From f976165e8274a683c349c381ebbeabc851d7f915 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Wed, 7 Jul 2021 12:42:32 +0800 Subject: [PATCH 0891/1221] Sort pipfile --- Pipfile | 16 ++++++++-------- Pipfile.lock | 10 +++++----- bot.py | 4 ++-- requirements.txt | 3 +-- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Pipfile b/Pipfile index eeb21b15e7..5c1d2f8670 100644 --- a/Pipfile +++ b/Pipfile @@ -4,23 +4,23 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -black = "==21.6b0" -pylint = "~=2.9.3" bandit = "~=1.7.0" +black = "==21.6b0" flake8 = "~=3.9.2" +pylint = "~=2.9.3" [packages] +aiohttp = "==3.7.4.post0" colorama = "~=0.4.4" -python-dateutil = "~=2.8.1" +"discord.py" = "==1.7.3" emoji = "~=1.2.0" -uvloop = {version = ">=0.15.2",sys_platform = "!= 'win32'"} +isodate = "~=0.6.0" motor = "~=2.4.0" natural = "~=0.2.0" -isodate = "~=0.6.0" parsedatetime = "~=2.6" -aiohttp = "==3.7.4.post0" -python-dotenv = ">=0.18.0" -"discord.py" = "==1.7.3" +python-dateutil = "~=2.8.1" +python-dotenv = "~=0.18.0" +uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index cfaece3094..892d2fddfa 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0700f9473d2588f168a63141ac5f44596d303e7a55db173968edddd0ace2e2ac" + "sha256": "0ffab0587a7d21d864d2c5a7a789e7c883caada73c9c23ff275877aad567729e" }, "pipfile-spec": 6, "requires": { @@ -279,7 +279,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "typing-extensions": { @@ -420,7 +420,7 @@ "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56", "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c" ], - "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", + "markers": "python_full_version >= '3.6.1' and python_version < '4.0'", "version": "==5.9.1" }, "lazy-object-proxy": { @@ -590,7 +590,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "smmap": { @@ -614,7 +614,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==0.10.2" }, "wrapt": { diff --git a/bot.py b/bot.py index 1b9a9f2c1f..6102caa915 100644 --- a/bot.py +++ b/bot.py @@ -1695,9 +1695,9 @@ def main(): pass # check discord version - if discord.__version__ != "1.6.0": + if discord.__version__ != "1.7.3": logger.error( - "Dependencies are not updated, run pipenv install. discord.py version expected 1.6.0, received %s", + "Dependencies are not updated, run pipenv install. discord.py version expected 1.7.3, received %s", discord.__version__, ) sys.exit(0) diff --git a/requirements.txt b/requirements.txt index 7e1360e73e..0cc483cc3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,8 +22,7 @@ parsedatetime==2.6 pymongo==3.11.4 python-dateutil==2.8.1 python-dotenv==0.18.0 -six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' typing-extensions==3.10.0.0 uvloop==0.15.2; sys_platform != 'win32' -wheel==0.36.2 yarl==1.6.3; python_version >= '3.6' From b5f593035b4a1137438b8ca29dd9f98701953adc Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Wed, 7 Jul 2021 19:37:11 +0800 Subject: [PATCH 0892/1221] Remove flake8 from req, use new rec line-width of 110, black format --- .github/workflows/lints.yml | 15 ++-- Pipfile | 1 - Pipfile.lock | 34 ++------- bot.py | 127 +++++++++----------------------- cogs/modmail.py | 76 ++++++------------- cogs/plugins.py | 36 +++------ cogs/utility.py | 143 ++++++++++++------------------------ core/changelog.py | 12 +-- core/checks.py | 4 +- core/clients.py | 30 +++----- core/config.py | 16 +--- core/models.py | 4 +- core/thread.py | 103 ++++++++------------------ core/time.py | 4 +- core/utils.py | 3 +- pyproject.toml | 12 ++- 16 files changed, 195 insertions(+), 425 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 94b322d968..145a83d489 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -14,20 +14,19 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: 3.9 - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install bandit==1.6.2 pylint black==19.10b0 - continue-on-error: true + python -m pip install --upgrade pip pipenv + pipenv install --dev --system - name: Bandit syntax check run: bandit -r . -b .bandit_baseline.json - name: Pylint - run: pylint ./bot.py cogs/*.py core/*.py --disable=import-error --exit-zero -r y + run: pylint ./bot.py cogs/*.py core/*.py --exit-zero -r y continue-on-error: true - name: Black run: | diff --git a/Pipfile b/Pipfile index 5c1d2f8670..813b6700aa 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,6 @@ verify_ssl = true [dev-packages] bandit = "~=1.7.0" black = "==21.6b0" -flake8 = "~=3.9.2" pylint = "~=2.9.3" [packages] diff --git a/Pipfile.lock b/Pipfile.lock index 892d2fddfa..b10f516efc 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0ffab0587a7d21d864d2c5a7a789e7c883caada73c9c23ff275877aad567729e" + "sha256": "dedff7c4534f29ac1560a57f358852e90d6e6b9d04eb07a35f6daa2d661bc0c2" }, "pipfile-spec": 6, "requires": { @@ -279,7 +279,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "typing-extensions": { @@ -391,14 +391,6 @@ "markers": "python_version >= '3.6'", "version": "==8.0.1" }, - "flake8": { - "hashes": [ - "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", - "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" - ], - "index": "pypi", - "version": "==3.9.2" - }, "gitdb": { "hashes": [ "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", @@ -420,7 +412,7 @@ "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56", "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c" ], - "markers": "python_full_version >= '3.6.1' and python_version < '4.0'", + "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", "version": "==5.9.1" }, "lazy-object-proxy": { @@ -480,22 +472,6 @@ "markers": "python_version >= '2.6'", "version": "==5.6.0" }, - "pycodestyle": { - "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.7.0" - }, - "pyflakes": { - "hashes": [ - "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", - "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.3.1" - }, "pylint": { "hashes": [ "sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a", @@ -590,7 +566,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "smmap": { @@ -614,7 +590,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "wrapt": { diff --git a/bot.py b/bot.py index 6102caa915..ce7cfc529d 100644 --- a/bot.py +++ b/bot.py @@ -328,9 +328,7 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: try: 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 - ) + logger.warning("No log channel set, setting #%s to be the log channel.", channel.name) return channel except IndexError: pass @@ -392,9 +390,7 @@ def auto_triggers(self) -> typing.Dict[str, str]: def token(self) -> str: token = self.config["token"] if token is None: - logger.critical( - "TOKEN must be set, set this as bot token found on the Discord Developer Portal." - ) + logger.critical("TOKEN must be set, set this as bot token found on the Discord Developer Portal.") sys.exit(0) return token @@ -510,11 +506,7 @@ def command_perm(self, command_name: str) -> PermissionLevel: logger.debug("Command %s not found.", command_name) return PermissionLevel.INVALID level = next( - ( - check.permission_level - for check in command.checks - if hasattr(check, "permission_level") - ), + (check.permission_level for check in command.checks if hasattr(check, "permission_level")), None, ) if level is None: @@ -549,8 +541,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.bot_owner_ids + getattr(self.get_user(owner_id), "name", str(owner_id)) for owner_id in self.bot_owner_ids ) logger.info("Owners: %s", owners) logger.info("Prefix: %s", self.prefix) @@ -573,9 +564,7 @@ async def on_ready(self): 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 - ) + 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)) @@ -617,9 +606,7 @@ async def on_ready(self): if log_data: logger.debug("Successfully closed thread with channel %s.", log["channel_id"]) else: - logger.debug( - "Failed to close thread with channel %s, skipping.", log["channel_id"] - ) + logger.debug("Failed to close thread with channel %s, skipping.", log["channel_id"]) if self.config.get("data_collection"): self.metadata_loop = tasks.Loop( @@ -640,9 +627,7 @@ async def on_ready(self): self.autoupdate_loop.before_loop(self.before_autoupdate) self.autoupdate_loop.start() - other_guilds = [ - guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild} - ] + other_guilds = [guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild}] if any(other_guilds): logger.warning( "The bot is in more servers other than the main and staff server. " @@ -880,9 +865,7 @@ async def get_thread_cooldown(self, author: discord.Member): 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" - ) + cooldown = datetime.fromisoformat(last_log_closed_at) + self.config.remove("thread_cooldown") if cooldown > now: # User messaged before thread cooldown ended @@ -930,12 +913,8 @@ async def process_dm_modmail(self, message: discord.Message) -> None: color=self.error_color, description=self.config["disabled_new_thread_response"], ) - embed.set_footer( - text=self.config["disabled_new_thread_footer"], icon_url=self.guild.icon_url - ) - logger.info( - "A new thread was blocked from %s due to disabled Modmail.", message.author - ) + embed.set_footer(text=self.config["disabled_new_thread_footer"], icon_url=self.guild.icon_url) + logger.info("A new thread was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) return await message.channel.send(embed=embed) @@ -951,9 +930,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: text=self.config["disabled_current_thread_footer"], icon_url=self.guild.icon_url, ) - logger.info( - "A message was blocked from %s due to disabled Modmail.", message.author - ) + logger.info("A message was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) return await message.channel.send(embed=embed) @@ -1025,15 +1002,11 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) invoker = None if self.config.get("use_regex_autotrigger"): - trigger = next( - filter(lambda x: re.search(x, message.content), self.auto_triggers.keys()) - ) + trigger = next(filter(lambda x: re.search(x, message.content), self.auto_triggers.keys())) if trigger: invoker = re.search(trigger, message.content).group(0) else: - trigger = next( - filter(lambda x: x.lower() in message.content.lower(), self.auto_triggers.keys()) - ) + trigger = next(filter(lambda x: x.lower() in message.content.lower(), self.auto_triggers.keys())) if trigger: invoker = trigger.lower() @@ -1169,9 +1142,7 @@ async def process_commands(self, message): ctxs = await self.get_contexts(message) for ctx in ctxs: if ctx.command: - if not any( - 1 for check in ctx.command.checks if hasattr(check, "permission_level") - ): + if not any(1 for check in ctx.command.checks if hasattr(check, "permission_level")): logger.debug( "Command %s has no permissions check, adding invalid level.", ctx.command.qualified_name, @@ -1199,9 +1170,7 @@ async def process_commands(self, message): else: await self.api.append_log(message, type_="internal") elif ctx.invoked_with: - exc = commands.CommandNotFound( - 'Command "{}" is not found'.format(ctx.invoked_with) - ) + exc = commands.CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with)) self.dispatch("command_error", ctx, exc) async def on_typing(self, channel, user, _): @@ -1275,9 +1244,7 @@ async def handle_reaction_events(self, payload): 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 - ) + 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 @@ -1286,9 +1253,7 @@ async def handle_reaction_events(self, payload): if not thread: return try: - _, linked_message = await thread.find_linked_messages( - message.id, either_direction=True - ) + _, 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 @@ -1345,9 +1310,7 @@ async def on_raw_reaction_add(self, payload): ctx = await self.get_context(message) ctx.author = member - await ctx.invoke( - self.get_command("contact"), user=member, manual_trigger=False - ) + await ctx.invoke(self.get_command("contact"), user=member, manual_trigger=False) async def on_raw_reaction_remove(self, payload): if self.config["transfer_reactions"]: @@ -1373,9 +1336,7 @@ async def on_guild_channel_delete(self, channel): await self.config.update() return - audit_logs = self.modmail_guild.audit_logs( - limit=10, action=discord.AuditLogAction.channel_delete - ) + audit_logs = self.modmail_guild.audit_logs(limit=10, action=discord.AuditLogAction.channel_delete) entry = await audit_logs.find(lambda a: int(a.target.id) == channel.id) if entry is None: @@ -1413,9 +1374,7 @@ async def on_member_join(self, member): return thread = await self.threads.find(recipient=member) if thread: - embed = discord.Embed( - description="The recipient has joined the server.", color=self.mod_color - ) + embed = discord.Embed(description="The recipient has joined the server.", color=self.mod_color) await thread.channel.send(embed=embed) async def on_message_delete(self, message): @@ -1448,9 +1407,7 @@ async def on_message_delete(self, message): if not thread: return - audit_logs = self.modmail_guild.audit_logs( - limit=10, action=discord.AuditLogAction.message_delete - ) + audit_logs = self.modmail_guild.audit_logs(limit=10, action=discord.AuditLogAction.message_delete) entry = await audit_logs.find(lambda a: a.target == self.user) @@ -1459,15 +1416,11 @@ async def on_message_delete(self, message): try: await thread.delete_message(message, note=False) - embed = discord.Embed( - description="Successfully deleted message.", color=self.main_color - ) + embed = discord.Embed(description="Successfully deleted message.", color=self.main_color) except ValueError as e: if str(e) not in {"DM message not found.", "Malformed thread message."}: logger.debug("Failed to find linked message to delete: %s", e) - embed = discord.Embed( - description="Failed to delete message.", color=self.error_color - ) + embed = discord.Embed(description="Failed to delete message.", color=self.error_color) else: return except discord.NotFound: @@ -1495,9 +1448,7 @@ async def on_message_edit(self, before, after): _, blocked_emoji = await self.retrieve_emoji() await self.add_reaction(after, blocked_emoji) else: - embed = discord.Embed( - description="Successfully Edited Message", color=self.main_color - ) + 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) @@ -1508,9 +1459,7 @@ async def on_error(self, event_method, *args, **kwargs): async def on_command_error(self, context, exception): if isinstance(exception, commands.BadArgument): await context.trigger_typing() - await context.send( - embed=discord.Embed(color=self.error_color, description=str(exception)) - ) + await context.send(embed=discord.Embed(color=self.error_color, description=str(exception))) elif isinstance(exception, commands.CommandNotFound): logger.warning("CommandNotFound: %s", exception) elif isinstance(exception, commands.MissingRequiredArgument): @@ -1531,9 +1480,7 @@ async def on_command_error(self, context, exception): embed=discord.Embed(color=self.error_color, description=check.fail_msg) ) if hasattr(check, "permission_level"): - corrected_permission_level = self.command_perm( - context.command.qualified_name - ) + corrected_permission_level = self.command_perm(context.command.qualified_name) logger.warning( "User %s does not have permission to use this command: `%s` (%s).", context.author.name, @@ -1542,9 +1489,7 @@ async def on_command_error(self, context, exception): ) logger.warning("CheckFailure: %s", exception) elif isinstance(exception, commands.DisabledCommand): - logger.info( - "DisabledCommand: %s is trying to run eval but it's disabled", context.author.name - ) + logger.info("DisabledCommand: %s is trying to run eval but it's disabled", context.author.name) else: logger.error("Unexpected exception:", exc_info=exception) @@ -1568,9 +1513,7 @@ async def post_metadata(self): if info.team is not None: data.update( { - "owner_name": info.team.owner.name - if info.team.owner is not None - else "No Owner", + "owner_name": info.team.owner.name if info.team.owner is not None else "No Owner", "owner_id": info.team.owner_id, "team": True, } @@ -1632,7 +1575,11 @@ async def autoupdate(self): pass command = "git pull" - proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) + proc = await asyncio.create_subprocess_shell( + command, + stderr=PIPE, + stdout=PIPE, + ) err = await proc.stderr.read() err = err.decode("utf-8").rstrip() res = await proc.stdout.read() @@ -1648,9 +1595,7 @@ async def autoupdate(self): channel = self.update_channel if self.hosting_method in (HostingMethod.PM2, HostingMethod.SYSTEMD): embed = discord.Embed(title="Bot has been updated", color=self.main_color) - embed.set_footer( - text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" - ) + embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") if self.config["update_notifications"]: await channel.send(embed=embed) else: @@ -1659,9 +1604,7 @@ async def autoupdate(self): description="If you do not have an auto-restart setup, please manually start the bot.", color=self.main_color, ) - embed.set_footer( - text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" - ) + embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") if self.config["update_notifications"]: await channel.send(embed=embed) await self.logout() diff --git a/cogs/modmail.py b/cogs/modmail.py index 87b9d91e57..5b3f67bdd2 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -42,9 +42,7 @@ async def setup(self, ctx): """ if ctx.guild != self.bot.modmail_guild: - return await ctx.send( - f"You can only setup in the Modmail guild: {self.bot.modmail_guild}." - ) + return await ctx.send(f"You can only setup in the Modmail guild: {self.bot.modmail_guild}.") if self.bot.main_category is not None: logger.debug("Can't re-setup server, main_category is found.") @@ -79,15 +77,11 @@ async def setup(self, ctx): logger.info("Granting %s access to Modmail category.", key.name) overwrites[key] = discord.PermissionOverwrite(read_messages=True) - category = await self.bot.modmail_guild.create_category( - name="Modmail", overwrites=overwrites - ) + category = await self.bot.modmail_guild.create_category(name="Modmail", overwrites=overwrites) await category.edit(position=0) - log_channel = await self.bot.modmail_guild.create_text_channel( - name="bot-logs", category=category - ) + log_channel = await self.bot.modmail_guild.create_text_channel(name="bot-logs", category=category) embed = discord.Embed( title="Friendly Reminder", @@ -434,9 +428,7 @@ def parse_user_or_role(ctx, user_or_role): @commands.command(aliases=["alert"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def notify( - self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None - ): + async def notify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): """ Notify a user or role when the next thread message received. @@ -474,9 +466,7 @@ async def notify( @commands.command(aliases=["unalert"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def unnotify( - self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None - ): + async def unnotify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): """ Un-notify a user, role, or yourself from a thread. @@ -511,9 +501,7 @@ async def unnotify( @commands.command(aliases=["sub"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def subscribe( - self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None - ): + async def subscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): """ Notify a user, role, or yourself for every thread message received. @@ -551,9 +539,7 @@ async def subscribe( @commands.command(aliases=["unsub"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def unsubscribe( - self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None - ): + async def unsubscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower, None] = None): """ Unsubscribe a user, role, or yourself from a thread. @@ -638,7 +624,9 @@ def format_log_embeds(self, logs, avatar_url): prefix = self.bot.config["log_url_prefix"].strip("/") if prefix == "NONE": prefix = "" - log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{entry['key']}" + log_url = ( + f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{entry['key']}" + ) username = entry["recipient"]["name"] + "#" username += entry["recipient"]["discriminator"] @@ -946,9 +934,7 @@ async def note_persistent(self, ctx, *, msg: str = ""): async with ctx.typing(): msg = await ctx.thread.note(ctx.message, persistent=True) await msg.pin() - await self.bot.api.create_note( - recipient=ctx.thread.recipient, message=ctx.message, message_id=msg.id - ) + await self.bot.api.create_note(recipient=ctx.thread.recipient, message=ctx.message, message_id=msg.id) @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1011,17 +997,14 @@ async def contact( category = None if user.bot: - embed = discord.Embed( - color=self.bot.error_color, description="Cannot start a thread with a bot." - ) + embed = discord.Embed(color=self.bot.error_color, description="Cannot start a thread with a bot.") return await ctx.send(embed=embed, delete_after=3) exists = await self.bot.threads.find(recipient=user) if exists: embed = discord.Embed( color=self.bot.error_color, - description="A thread for this user already " - f"exists in {exists.channel.mention}.", + description="A thread for this user already " f"exists in {exists.channel.mention}.", ) await ctx.channel.send(embed=embed, delete_after=3) @@ -1045,7 +1028,9 @@ async def contact( description = f"{ctx.author.name} has opened a Modmail thread." em = discord.Embed( - title="New Thread", description=description, color=self.bot.main_color, + title="New Thread", + description=description, + color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: em.timestamp = datetime.utcnow() @@ -1153,9 +1138,7 @@ async def blocked(self, ctx): else: embeds[0].description = "Currently there are no blocked users." - embeds.append( - discord.Embed(title="Blocked Roles", color=self.bot.main_color, description="") - ) + embeds.append(discord.Embed(title="Blocked Roles", color=self.bot.main_color, description="")) if roles: embed = embeds[-1] @@ -1341,10 +1324,7 @@ async def unblock(self, ctx, *, user_or_role: Union[User, Role] = None): mention = getattr(user_or_role, "mention", f"`{user_or_role.id}`") name = getattr(user_or_role, "name", f"`{user_or_role.id}`") - if ( - not isinstance(user_or_role, discord.Role) - and str(user_or_role.id) in self.bot.blocked_users - ): + if not isinstance(user_or_role, discord.Role) and str(user_or_role.id) in self.bot.blocked_users: msg = self.bot.blocked_users.pop(str(user_or_role.id)) or "" await self.bot.config.update() @@ -1369,10 +1349,7 @@ async def unblock(self, ctx, *, user_or_role: Union[User, Role] = None): color=self.bot.main_color, description=f"{mention} is no longer blocked.", ) - elif ( - isinstance(user_or_role, discord.Role) - and str(user_or_role.id) in self.bot.blocked_roles - ): + elif isinstance(user_or_role, discord.Role) and str(user_or_role.id) in self.bot.blocked_roles: msg = self.bot.blocked_roles.pop(str(user_or_role.id)) or "" await self.bot.config.update() @@ -1465,12 +1442,8 @@ async def repair(self, ctx): 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}" - ) + 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 self.bot.add_reaction(ctx.message, sent_emoji) else: @@ -1482,8 +1455,7 @@ async def repair(self, ctx): if m is not None: users = set( filter( - lambda member: member.name == m.group(1) - and member.discriminator == m.group(2), + lambda member: member.name == m.group(1) and member.discriminator == m.group(2), ctx.guild.members, ) ) @@ -1508,9 +1480,7 @@ async def repair(self, ctx): except discord.HTTPException: pass if recipient is None: - self.bot.threads.cache[user.id] = thread = Thread( - self.bot.threads, user_id, ctx.channel - ) + 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 diff --git a/cogs/plugins.py b/cogs/plugins.py index 94a3f425c0..aab5dc8bd6 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -251,12 +251,8 @@ 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 - ) - raise InvalidPluginError( - f"Unable to download requirements: ```\n{stderr.decode()}\n```" - ) + logger.error("Failed to download requirements for %s.", plugin.ext_string, exc_info=True) + raise InvalidPluginError(f"Unable to download requirements: ```\n{stderr.decode()}\n```") if os.path.exists(USER_SITE): sys.path.insert(0, USER_SITE) @@ -347,9 +343,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): return if str(plugin) in self.bot.config["plugins"]: - embed = discord.Embed( - description="This plugin is already installed.", color=self.bot.error_color - ) + embed = discord.Embed(description="This plugin is already installed.", color=self.bot.error_color) return await ctx.send(embed=embed) if plugin.name in self.bot.cogs: @@ -433,9 +427,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): return if str(plugin) not in self.bot.config["plugins"]: - embed = discord.Embed( - description="Plugin is not installed.", color=self.bot.error_color - ) + embed = discord.Embed(description="Plugin is not installed.", color=self.bot.error_color) return await ctx.send(embed=embed) if self.bot.config.get("enable_plugins"): @@ -472,9 +464,7 @@ async def update_plugin(self, ctx, plugin_name): return if str(plugin) not in self.bot.config["plugins"]: - embed = discord.Embed( - description="Plugin is not installed.", color=self.bot.error_color - ) + embed = discord.Embed(description="Plugin is not installed.", color=self.bot.error_color) return await ctx.send(embed=embed) async with ctx.typing(): @@ -598,9 +588,7 @@ async def plugins_loaded(self, ctx): embeds = [] for page in pages: - embed = discord.Embed( - title="Loaded plugins:", description=page, color=self.bot.main_color - ) + embed = discord.Embed(title="Loaded plugins:", description=page, color=self.bot.main_color) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) await paginator.run() @@ -641,9 +629,7 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N matches = get_close_matches(plugin_name, self.registry.keys()) if matches: - embed.add_field( - name="Perhaps you meant:", value="\n".join(f"`{m}`" for m in matches) - ) + embed.add_field(name="Perhaps you meant:", value="\n".join(f"`{m}`" for m in matches)) return await ctx.send(embed=embed) @@ -661,13 +647,9 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N title=details["repository"], ) - embed.add_field( - name="Installation", value=f"```{self.bot.prefix}plugins add {name}```" - ) + embed.add_field(name="Installation", value=f"```{self.bot.prefix}plugins add {name}```") - embed.set_author( - name=details["title"], icon_url=details.get("icon_url"), url=plugin.link - ) + embed.set_author(name=details["title"], icon_url=details.get("icon_url"), url=plugin.link) if details.get("thumbnail_url"): embed.set_thumbnail(url=details.get("thumbnail_url")) diff --git a/cogs/utility.py b/cogs/utility.py index 7b39631dcd..0e03ab1ba1 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -184,9 +184,7 @@ 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: - embed = discord.Embed( - title=f"{command} is a snippet.", color=self.context.bot.main_color - ) + 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) @@ -206,9 +204,7 @@ async def send_error_message(self, error): await self.context.bot.config.update() else: if len(values) == 1: - embed = discord.Embed( - title=f"{command} is an alias.", color=self.context.bot.main_color - ) + 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( @@ -330,12 +326,8 @@ async def about(self, ctx): latest = changelog.latest_version if self.bot.version.is_prerelease: - stable = next( - filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions) - ) - footer = ( - f"You are on the prerelease version • the latest version is v{stable.version}." - ) + stable = next(filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions)) + footer = f"You are on the prerelease version • the latest version is v{stable.version}." elif self.bot.version < parse_version(latest.version): footer = f"A newer version is available v{latest.version}." else: @@ -389,9 +381,7 @@ async def debug(self, ctx): log_file_name = self.bot.token.split(".")[0] with open( - os.path.join( - os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" - ), + os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), "r+", ) as f: logs = f.read().strip() @@ -444,9 +434,7 @@ async def debug_hastebin(self, ctx): log_file_name = self.bot.token.split(".")[0] with open( - os.path.join( - os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" - ), + os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), "rb+", ) as f: logs = BytesIO(f.read().strip()) @@ -482,16 +470,12 @@ async def debug_clear(self, ctx): log_file_name = self.bot.token.split(".")[0] with open( - os.path.join( - os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" - ), + os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), "w", ): pass await ctx.send( - embed=discord.Embed( - color=self.bot.main_color, description="Cached logs are now cleared." - ) + embed=discord.Embed(color=self.bot.main_color, description="Cached logs are now cleared.") ) @commands.command(aliases=["presence"]) @@ -536,9 +520,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): except KeyError: raise commands.MissingRequiredArgument(SimpleNamespace(name="activity")) - activity, _ = await self.set_presence( - activity_type=activity_type, activity_message=message - ) + activity, _ = await self.set_presence(activity_type=activity_type, activity_message=message) self.bot.config["activity_type"] = activity.type.value self.bot.config["activity_message"] = activity.name @@ -603,9 +585,7 @@ async def set_presence(self, *, status=None, activity_type=None, activity_messag url = None activity_message = (activity_message or self.bot.config["activity_message"]).strip() if activity_type is not None and not activity_message: - logger.warning( - 'No activity message found whilst activity is provided, defaults to "Modmail".' - ) + logger.warning('No activity message found whilst activity is provided, defaults to "Modmail".') activity_message = "Modmail" if activity_type == ActivityType.listening: @@ -706,12 +686,14 @@ async def mention(self, ctx, *user_or_role: Union[discord.Role, discord.Member, option = user_or_role[0].lower() if option == "disable": embed = discord.Embed( - description=f"Disabled mention on thread creation.", color=self.bot.main_color, + description=f"Disabled mention on thread creation.", + color=self.bot.main_color, ) self.bot.config["mention"] = None else: embed = discord.Embed( - description="`mention` is reset to default.", color=self.bot.main_color, + description="`mention` is reset to default.", + color=self.bot.main_color, ) self.bot.config.remove("mention") await self.bot.config.update() @@ -747,9 +729,7 @@ async def prefix(self, ctx, *, prefix=None): """ current = self.bot.prefix - embed = discord.Embed( - title="Current prefix", color=self.bot.main_color, description=f"{current}" - ) + embed = discord.Embed(title="Current prefix", color=self.bot.main_color, description=f"{current}") if prefix is None: await ctx.send(embed=embed) @@ -786,9 +766,7 @@ async def config_options(self, ctx): """Return a list of valid configuration names you can change.""" embeds = [] for names in zip_longest(*(iter(sorted(self.bot.config.public_keys)),) * 15): - description = "\n".join( - f"`{name}`" for name in takewhile(lambda x: x is not None, names) - ) + description = "\n".join(f"`{name}`" for name in takewhile(lambda x: x is not None, names)) embed = discord.Embed( title="Available configuration keys:", color=self.bot.main_color, @@ -906,9 +884,7 @@ async def config_help(self, ctx, key: str.lower = None): 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) - ) + 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 @@ -1401,9 +1377,7 @@ async def permissions_remove( 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 - ): + if type_ not in {"command", "level", "override"} or (type_ != "override" and user_or_role is None): return await ctx.send_help(ctx.command) if type_ == "override": @@ -1566,15 +1540,9 @@ async def permissions_get( levels.append(level.name) 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 - else "No permission entries found." - ) + desc_cmd = ", ".join(map(lambda x: f"`{x}`", cmds)) if cmds else "No permission entries found." desc_level = ( - ", ".join(map(lambda x: f"`{x}`", levels)) - if levels - else "No permission entries found." + ", ".join(map(lambda x: f"`{x}`", levels)) if levels else "No permission entries found." ) embeds = [ @@ -1599,9 +1567,7 @@ async def permissions_get( for command in self.bot.walk_commands(): if command not in done: done.add(command) - level = self.bot.config["override_command_level"].get( - command.qualified_name - ) + level = self.bot.config["override_command_level"].get(command.qualified_name) if level is not None: overrides[command.qualified_name] = level @@ -1620,12 +1586,8 @@ async def permissions_get( ": ".join((f"`{name}`", level)) for name, level in takewhile(lambda x: x is not None, items) ) - embed = discord.Embed( - color=self.bot.main_color, description=description - ) - embed.set_author( - name="Permission Overrides", icon_url=ctx.guild.icon_url - ) + embed = discord.Embed(color=self.bot.main_color, description=description) + embed.set_author(name="Permission Overrides", icon_url=ctx.guild.icon_url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -1727,9 +1689,7 @@ async def oauth_whitelist(self, ctx, target: Union[discord.Role, utils.User]): if not hasattr(target, "mention"): 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 {target.mention} to view logs." - ) + embed.description = f"{'Un-w' if removed else 'W'}hitelisted {target.mention} to view logs." await ctx.send(embed=embed) @@ -1807,9 +1767,7 @@ async def autotrigger_add(self, ctx, keyword, *, command): async def autotrigger_edit(self, ctx, keyword, *, command): """Edits a pre-existing trigger to automatically trigger an alias-like command""" if keyword not in self.bot.auto_triggers: - embed = utils.create_not_found_embed( - keyword, self.bot.auto_triggers.keys(), "Autotrigger" - ) + embed = utils.create_not_found_embed(keyword, self.bot.auto_triggers.keys(), "Autotrigger") else: # command validation valid = False @@ -1895,7 +1853,11 @@ async def autotrigger_list(self, ctx): embeds = [] for keyword in self.bot.auto_triggers: command = self.bot.auto_triggers[keyword] - embed = discord.Embed(title=keyword, color=self.bot.main_color, description=command,) + embed = discord.Embed( + title=keyword, + color=self.bot.main_color, + description=command, + ) embeds.append(embed) if not embeds: @@ -1918,17 +1880,13 @@ async def github(self, ctx): data = await self.bot.api.get_user_info() if data: - embed = discord.Embed( - title="GitHub", description="Current User", color=self.bot.main_color - ) + embed = discord.Embed(title="GitHub", description="Current User", color=self.bot.main_color) user = data["user"] embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) embed.set_thumbnail(url=user["avatar_url"]) await ctx.send(embed=embed) else: - await ctx.send( - embed=discord.Embed(title="Invalid Github Token", color=self.bot.error_color) - ) + await ctx.send(embed=discord.Embed(title="Invalid Github Token", color=self.bot.error_color)) @commands.command() @checks.has_permissions(PermissionLevel.OWNER) @@ -1950,16 +1908,12 @@ async def update(self, ctx, *, flag: str = ""): ) if self.bot.version >= parse_version(latest.version) and flag.lower() != "force": - embed = discord.Embed( - title="Already up to date", description=desc, color=self.bot.main_color - ) + embed = discord.Embed(title="Already up to date", description=desc, color=self.bot.main_color) data = await self.bot.api.get_user_info() if data: user = data["user"] - embed.set_author( - name=user["username"], icon_url=user["avatar_url"], url=user["url"] - ) + embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) await ctx.send(embed=embed) else: if self.bot.hosting_method == HostingMethod.HEROKU: @@ -1971,9 +1925,7 @@ async def update(self, ctx, *, flag: str = ""): if commit_data and commit_data.get("html_url"): embed = discord.Embed(color=self.bot.main_color) - embed.set_footer( - text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}" - ) + embed.set_footer(text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}") embed.set_author( name=user["username"] + " - Updating bot", @@ -1995,9 +1947,7 @@ async def update(self, ctx, *, flag: str = ""): color=self.bot.main_color, ) embed.set_footer(text="Force update") - embed.set_author( - name=user["username"], icon_url=user["avatar_url"], url=user["url"] - ) + embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) await ctx.send(embed=embed) else: # update fork if gh_token exists @@ -2008,25 +1958,28 @@ async def update(self, ctx, *, flag: str = ""): command = "git pull" - proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) + proc = await asyncio.create_subprocess_shell( + command, + stderr=PIPE, + stdout=PIPE, + ) err = await proc.stderr.read() err = err.decode("utf-8").rstrip() res = await proc.stdout.read() res = res.decode("utf-8").rstrip() if err and not res: - embed = discord.Embed( - title="Update failed", description=err, color=self.bot.error_color - ) + embed = discord.Embed(title="Update failed", description=err, color=self.bot.error_color) await ctx.send(embed=embed) elif res != "Already up to date.": logger.info("Bot has been updated.") - embed = discord.Embed(title="Bot has been updated", color=self.bot.main_color,) - embed.set_footer( - text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}" + embed = discord.Embed( + title="Bot has been updated", + color=self.bot.main_color, ) + embed.set_footer(text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}") embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=truncate(value, 200)) @@ -2040,7 +1993,9 @@ async def update(self, ctx, *, flag: str = ""): await self.bot.logout() else: embed = discord.Embed( - title="Already up to date", description=desc, color=self.bot.main_color, + title="Already up to date", + description=desc, + color=self.bot.main_color, ) embed.set_footer(text="Force update") await ctx.send(embed=embed) diff --git a/core/changelog.py b/core/changelog.py index 878a19b26c..7c9af2e1bb 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -65,9 +65,7 @@ def parse(self) -> None: Parse the lines and split them into `description` and `fields`. """ self.description = re.match(self.DESCRIPTION_REGEX, self.lines, re.DOTALL) - self.description = ( - self.description.group(1).strip() if self.description is not None else "" - ) + self.description = self.description.group(1).strip() if self.description is not None else "" matches = re.finditer(self.ACTION_REGEX, self.lines, re.DOTALL) for m in matches: @@ -91,7 +89,9 @@ 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(): @@ -171,7 +171,9 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": """ # get branch via git cli if available proc = await asyncio.create_subprocess_shell( - "git branch --show-current", stderr=PIPE, stdout=PIPE, + "git branch --show-current", + stderr=PIPE, + stdout=PIPE, ) err = await proc.stderr.read() err = err.decode("utf-8").rstrip() diff --git a/core/checks.py b/core/checks.py index 74b7dc38cd..1079b55530 100644 --- a/core/checks.py +++ b/core/checks.py @@ -5,7 +5,9 @@ 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 db4e9936b4..1d90cbb0e3 100644 --- a/core/clients.py +++ b/core/clients.py @@ -305,9 +305,7 @@ async def get_log(self, channel_id: Union[str, int]) -> dict: async def get_log_link(self, channel_id: Union[str, int]) -> str: return NotImplemented - async def create_log_entry( - self, recipient: Member, channel: TextChannel, creator: Member - ) -> str: + async def create_log_entry(self, recipient: Member, channel: TextChannel, creator: Member) -> str: return NotImplemented async def delete_log_entry(self, key: str) -> bool: @@ -479,13 +477,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 - ) -> str: + async def create_log_entry(self, recipient: Member, channel: TextChannel, creator: Member) -> str: key = secrets.token_hex(6) await self.logs.insert_one( @@ -536,9 +530,7 @@ async def get_config(self) -> dict: async def update_config(self, data: dict): toset = self.bot.config.filter_valid(data) - unset = self.bot.config.filter_valid( - {k: 1 for k in self.bot.config.all_keys if k not in data} - ) + unset = self.bot.config.filter_valid({k: 1 for k in self.bot.config.all_keys if k not in data}) if toset and unset: return await self.db.config.update_one( @@ -635,17 +627,13 @@ async def find_notes(self, recipient: Member): async def update_note_ids(self, ids: dict): for object_id, message_id in ids.items(): - await self.db.notes.update_one( - {"_id": object_id}, {"$set": {"message_id": message_id}} - ) + await self.db.notes.update_one({"_id": object_id}, {"$set": {"message_id": message_id}}) async def delete_note(self, message_id: Union[int, str]): await self.db.notes.delete_one({"message_id": str(message_id)}) async def edit_note(self, message_id: Union[int, str], message: str): - await self.db.notes.update_one( - {"message_id": str(message_id)}, {"$set": {"message": message}} - ) + await self.db.notes.update_one({"message_id": str(message_id)}, {"$set": {"message": message}}) def get_plugin_partition(self, cog): cls_name = cog.__class__.__name__ @@ -656,7 +644,11 @@ async def update_repository(self) -> dict: data = await user.update_repository() return { "data": data, - "user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,}, + "user": { + "username": user.username, + "avatar_url": user.avatar_url, + "url": user.url, + }, } async def get_user_info(self) -> dict: diff --git a/core/config.py b/core/config.py index 30f364622d..255c3667de 100644 --- a/core/config.py +++ b/core/config.py @@ -210,28 +210,18 @@ def populate_cache(self) -> dict: # 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}) - config_json = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json" - ) + config_json = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json") if os.path.exists(config_json): logger.debug("Loading envs from config.json.") with open(config_json, "r", encoding="utf-8") as f: # Config json should override env vars try: - data.update( - { - k.lower(): v - for k, v in json.load(f).items() - if k.lower() in self.all_keys - } - ) + data.update({k.lower(): v for k, v in json.load(f).items() if k.lower() in self.all_keys}) except json.JSONDecodeError: logger.critical("Failed to load config.json env values.", exc_info=True) self._cache = data - config_help_json = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "config_help.json" - ) + config_help_json = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config_help.json") with open(config_help_json, "r", encoding="utf-8") as f: self.config_help = dict(sorted(json.load(f).items())) diff --git a/core/models.py b/core/models.py index 0238e051d4..d17e118e6c 100644 --- a/core/models.py +++ b/core/models.py @@ -203,9 +203,7 @@ async def convert(self, ctx, argument): except commands.ChannelNotFound: def check(c): - return isinstance(c, discord.CategoryChannel) and c.name.lower().startswith( - argument.lower() - ) + return isinstance(c, discord.CategoryChannel) and c.name.lower().startswith(argument.lower()) if guild: result = discord.utils.find(check, guild.categories) diff --git a/core/thread.py b/core/thread.py index 530e063e54..41d2cd96ba 100644 --- a/core/thread.py +++ b/core/thread.py @@ -109,9 +109,7 @@ async def setup(self, *, creator=None, category=None, initial_message=None): recipient = self.recipient # in case it creates a channel outside of category - overwrites = { - self.bot.modmail_guild.default_role: discord.PermissionOverwrite(read_messages=False) - } + overwrites = {self.bot.modmail_guild.default_role: discord.PermissionOverwrite(read_messages=False)} category = category or self.bot.main_category @@ -170,9 +168,7 @@ async def setup(self, *, creator=None, category=None, initial_message=None): mention = self.bot.config["mention"] async def send_genesis_message(): - info_embed = self._format_info_embed( - recipient, log_url, log_count, self.bot.main_color - ) + info_embed = self._format_info_embed(recipient, log_url, log_count, self.bot.main_color) try: msg = await channel.send(mention, embed=info_embed) self.bot.loop.create_task(msg.pin()) @@ -238,9 +234,7 @@ class Author: "author": Author(), } message = discord.Message(state=State(), channel=None, data=data) - ids[note["_id"]] = str( - (await self.note(message, persistent=True, thread_creation=True)).id - ) + ids[note["_id"]] = str((await self.note(message, persistent=True, thread_creation=True)).id) await self.bot.api.update_note_ids(ids) @@ -330,16 +324,12 @@ def _format_info_embed(self, user, log_url, log_count, color): mutual_guilds = [g for g in self.bot.guilds if user in g.members] if member is None or len(mutual_guilds) > 1: - embed.add_field( - name="Mutual Server(s)", value=", ".join(g.name for g in mutual_guilds) - ) + embed.add_field(name="Mutual Server(s)", value=", ".join(g.name for g in mutual_guilds)) return embed def _close_after(self, closer, silent, delete_channel, message): - return self.bot.loop.create_task( - self._close(closer, silent, delete_channel, message, True) - ) + return self.bot.loop.create_task(self._close(closer, silent, delete_channel, message, True)) async def close( self, @@ -372,9 +362,7 @@ async def close( self.bot.config["closures"][str(self.id)] = items await self.bot.config.update() - task = self.bot.loop.call_later( - after, self._close_after, closer, silent, delete_channel, message - ) + task = self.bot.loop.call_later(after, self._close_after, closer, silent, delete_channel, message) if auto_close: self.auto_close_task = task @@ -383,9 +371,7 @@ async def close( else: await self._close(closer, silent, delete_channel, message) - async def _close( - self, closer, silent=False, delete_channel=True, message=None, scheduled=False - ): + async def _close(self, closer, silent=False, delete_channel=True, message=None, scheduled=False): try: self.manager.cache.pop(self.id) except KeyError as e: @@ -425,7 +411,9 @@ async def _close( prefix = self.bot.config["log_url_prefix"].strip("/") if prefix == "NONE": prefix = "" - log_url = f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{log_data['key']}" + log_url = ( + f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{log_data['key']}" + ) if log_data["title"]: sneak_peak = log_data["title"] @@ -473,7 +461,8 @@ async def _close( # Thread closed message embed = discord.Embed( - title=self.bot.config["thread_close_title"], color=self.bot.error_color, + title=self.bot.config["thread_close_title"], + color=self.bot.error_color, ) if self.bot.config["show_timestamp"]: embed.timestamp = datetime.utcnow() @@ -531,9 +520,7 @@ async def _restart_close_timer(self): human_time = human_timedelta(dt=reset_time) if self.bot.config.get("thread_auto_close_silently"): - return await self.close( - closer=self.bot.user, silent=True, after=int(seconds), auto_close=True - ) + return await self.close(closer=self.bot.user, silent=True, after=int(seconds), auto_close=True) # Grab message close_message = self.bot.formatter.format( @@ -549,9 +536,7 @@ async def _restart_close_timer(self): time_marker_regex, ) - await self.close( - closer=self.bot.user, after=int(seconds), message=close_message, auto_close=True - ) + await self.close(closer=self.bot.user, after=int(seconds), message=close_message, auto_close=True) async def find_linked_messages( self, @@ -561,11 +546,7 @@ async def find_linked_messages( note: bool = True, ) -> typing.Tuple[discord.Message, typing.Optional[discord.Message]]: if message1 is not None: - if ( - not message1.embeds - or not message1.embeds[0].author.url - or message1.author != self.bot.user - ): + if not message1.embeds or not message1.embeds[0].author.url or message1.author != self.bot.user: raise ValueError("Malformed thread message.") elif message_id is not None: @@ -602,10 +583,7 @@ async def find_linked_messages( and message1.embeds[0].color and ( message1.embeds[0].color.value == self.bot.mod_color - or ( - either_direction - and message1.embeds[0].color.value == self.bot.recipient_color - ) + or (either_direction and message1.embeds[0].color.value == self.bot.recipient_color) ) and message1.embeds[0].author.url.split("#")[-1].isdigit() and message1.author == self.bot.user @@ -710,13 +688,9 @@ async def edit_dm_message(self, message: discord.Message, content: str) -> None: 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(self.bot.api.edit_message(message.id, content), linked_message.edit(embed=embed)) - async def note( - self, message: discord.Message, persistent=False, thread_creation=False - ) -> None: + async def note(self, message: discord.Message, persistent=False, thread_creation=False) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) @@ -729,16 +703,12 @@ async def note( ) self.bot.loop.create_task( - self.bot.api.append_log( - message, message_id=msg.id, channel_id=self.channel.id, type_="system" - ) + self.bot.api.append_log(message, message_id=msg.id, channel_id=self.channel.id, type_="system") ) return msg - async def reply( - self, message: discord.Message, anonymous: bool = False, plain: bool = False - ) -> None: + async def reply(self, message: discord.Message, anonymous: bool = False, plain: bool = False) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) if not any(g.get_member(self.id) for g in self.bot.guilds): @@ -777,7 +747,10 @@ async def reply( ) tasks.append( message.channel.send( - embed=discord.Embed(color=self.bot.error_color, description=description,) + embed=discord.Embed( + color=self.bot.error_color, + description=description, + ) ) ) else: @@ -825,9 +798,7 @@ async def send( thread_creation: bool = False, ) -> None: - self.bot.loop.create_task( - self._restart_close_timer() - ) # Start or restart thread auto close + self.bot.loop.create_task(self._restart_close_timer()) # Start or restart thread auto close if self.close_task is not None: # cancel closing if a thread message is sent. @@ -967,9 +938,7 @@ async def send( file_upload_count = 1 for url, filename, _ in attachments: - embed.add_field( - name=f"File upload ({file_upload_count})", value=f"[{filename}]({url})" - ) + embed.add_field(name=f"File upload ({file_upload_count})", value=f"[{filename}]({url})") file_upload_count += 1 if from_mod: @@ -1028,9 +997,7 @@ async def send( files = [] for i in embed.fields: if "Image" in i.name: - async with self.bot.session.get( - i.field[i.field.find("http") : -1] - ) as resp: + async with self.bot.session.get(i.field[i.field.find("http") : -1]) as resp: stream = io.BytesIO(await resp.read()) files.append(discord.File(stream)) @@ -1119,9 +1086,7 @@ async def find( return thread else: 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 - ) + 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) ) @@ -1192,9 +1157,7 @@ async def create( 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 - ) + 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) ) @@ -1263,15 +1226,11 @@ async def create( await confirm.remove_reaction(accept_emoji, self.bot.user) await asyncio.sleep(0.2) await confirm.remove_reaction(deny_emoji, self.bot.user) - await destination.send( - embed=discord.Embed(title="Cancelled", color=self.bot.error_color) - ) + await destination.send(embed=discord.Embed(title="Cancelled", color=self.bot.error_color)) del self.cache[recipient.id] return thread - self.bot.loop.create_task( - thread.setup(creator=creator, category=category, initial_message=message) - ) + self.bot.loop.create_task(thread.setup(creator=creator, category=category, initial_message=message)) return thread async def find_or_create(self, recipient) -> Thread: diff --git a/core/time.py b/core/time.py index 331e26349f..bdec8d2549 100644 --- a/core/time.py +++ b/core/time.py @@ -57,9 +57,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 - ) + dt = dt.replace(hour=now.hour, minute=now.minute, second=now.second, microsecond=now.microsecond) self.dt = dt self._past = dt < now diff --git a/core/utils.py b/core/utils.py index 2dfb319a8e..af47c8e2e6 100644 --- a/core/utils.py +++ b/core/utils.py @@ -355,8 +355,7 @@ def format_channel_name(bot, author, exclude_channel=None, force_null=False): name = "null" name = 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" ) + f"-{author.discriminator}" counter = 1 diff --git a/pyproject.toml b/pyproject.toml index a920956e85..994cdd7cca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [tool.black] -line-length = 99 -target-version = ['py37'] +line-length = "110" +target-version = ['py39'] include = '\.pyi?$' -exclude = ''' +extend-exclude = ''' ( /( \.eggs @@ -18,3 +18,9 @@ exclude = ''' )/ ) ''' + +[tool.pylint.messages_control] +disable = "C0330, C0326" + +[tool.pylint.format] +max-line-length = "110" From b3a2918c34587f1c45b590402e87f1c5ccde06ad Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Wed, 7 Jul 2021 19:46:36 +0800 Subject: [PATCH 0893/1221] Remove flake8 from req, use new rec line-width of 110, black format --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0cc483cc3c..39904e9180 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ parsedatetime==2.6 pymongo==3.11.4 python-dateutil==2.8.1 python-dotenv==0.18.0 -six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' +six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' typing-extensions==3.10.0.0 uvloop==0.15.2; sys_platform != 'win32' yarl==1.6.3; python_version >= '3.6' From 77a1ac41bd2799b8e81cd4e4f33fffbfabdabc9b Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Jul 2021 13:45:26 +0800 Subject: [PATCH 0894/1221] Add dnspython (pymongo[srv]) as req --- Pipfile | 3 ++- Pipfile.lock | 14 +++++++++++++- requirements.txt | 3 ++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Pipfile b/Pipfile index 813b6700aa..9c7b4e03b7 100644 --- a/Pipfile +++ b/Pipfile @@ -10,13 +10,14 @@ pylint = "~=2.9.3" [packages] aiohttp = "==3.7.4.post0" -colorama = "~=0.4.4" +colorama = "~=0.4.4" # Doesn't officially support Python 3.9 yet, v0.4.5 will support 3.9 "discord.py" = "==1.7.3" emoji = "~=1.2.0" isodate = "~=0.6.0" motor = "~=2.4.0" natural = "~=0.2.0" parsedatetime = "~=2.6" +pymongo = {version = "*", extras = ['srv']} # Required by motor python-dateutil = "~=2.8.1" python-dotenv = "~=0.18.0" uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} diff --git a/Pipfile.lock b/Pipfile.lock index b10f516efc..15fdcacc5c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "dedff7c4534f29ac1560a57f358852e90d6e6b9d04eb07a35f6daa2d661bc0c2" + "sha256": "0e726213f83b90d7c4e90a04cea6636dbdc5be2ad82049c96820535e5cc3d1ad" }, "pipfile-spec": 6, "requires": { @@ -99,6 +99,14 @@ "index": "pypi", "version": "==1.7.3" }, + "dnspython": { + "hashes": [ + "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", + "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, "emoji": { "hashes": [ "sha256:496f432058567985838c13d67dde84ca081614a8286c0b9cdc7d63dfa89d51a3", @@ -190,6 +198,9 @@ "version": "==2.6" }, "pymongo": { + "extras": [ + "srv" + ], "hashes": [ "sha256:03be7ad107d252bb7325d4af6309fdd2c025d08854d35f0e7abc8bf048f4245e", "sha256:071552b065e809d24c5653fcc14968cfd6fde4e279408640d5ac58e3353a3c5f", @@ -256,6 +267,7 @@ "sha256:fedf0dee7a412ca6d1d6d92c158fe9cbaa8ea0cae90d268f9ccc0744de7a97d0", "sha256:fffff7bfb6799a763d3742c59c6ee7ffadda21abed557637bc44ed1080876484" ], + "index": "pypi", "version": "==3.11.4" }, "python-dateutil": { diff --git a/requirements.txt b/requirements.txt index 39904e9180..d802cc0c46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ attrs==21.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' colorama==0.4.4 discord.py==1.7.3 +dnspython==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' emoji==1.2.0 idna==3.2; python_version >= '3.5' isodate==0.6.0 @@ -19,7 +20,7 @@ motor==2.4.0 multidict==5.1.0; python_version >= '3.6' natural==0.2.0 parsedatetime==2.6 -pymongo==3.11.4 +pymongo[srv]==3.11.4 python-dateutil==2.8.1 python-dotenv==0.18.0 six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' From 79acb3509127cad21d98457d313dcecfd6ac5b7d Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Jul 2021 13:48:33 +0800 Subject: [PATCH 0895/1221] Updates channel.move, bot.close, emoji changes --- bot.py | 8 ++++---- cogs/modmail.py | 3 ++- cogs/utility.py | 2 +- core/clients.py | 3 +-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bot.py b/bot.py index ce7cfc529d..3cb5bb2d5f 100644 --- a/bot.py +++ b/bot.py @@ -519,7 +519,7 @@ async def on_connect(self): await self.api.validate_database_connection() except Exception: logger.debug("Logging out due to failed database connection.") - return await self.logout() + return await self.close() logger.debug("Connected to gateway.") await self.config.refresh() @@ -534,7 +534,7 @@ async def on_ready(self): if self.guild is None: logger.error("Logging out due to invalid GUILD_ID.") - return await self.logout() + return await self.close() logger.line() logger.debug("Client ready.") @@ -640,7 +640,7 @@ async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() - if name not in UNICODE_EMOJI: + if name not in UNICODE_EMOJI['en']: try: name = await converter.convert(ctx, name.strip(":")) except commands.BadArgument as e: @@ -1607,7 +1607,7 @@ async def autoupdate(self): embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") if self.config["update_notifications"]: await channel.send(embed=embed) - await self.logout() + return await self.close() async def before_autoupdate(self): await self.wait_for_connected() diff --git a/cogs/modmail.py b/cogs/modmail.py index 5b3f67bdd2..55110c323e 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -322,7 +322,8 @@ async def move(self, ctx, *, arguments): silent_words = ["silent", "silently"] silent = any(word in silent_words for word in options.split()) - await thread.channel.edit(category=category, sync_permissions=True) + await thread.channel.move(category=category, end=True, sync_permissions=True, + reason=f"{ctx.author} moved this thread.") if self.bot.config["thread_move_notify"] and not silent: embed = discord.Embed( diff --git a/cogs/utility.py b/cogs/utility.py index 0e03ab1ba1..a00027eca3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1990,7 +1990,7 @@ async def update(self, ctx, *, flag: str = ""): ) await ctx.send(embed=embed) - await self.bot.logout() + return await self.bot.close() else: embed = discord.Embed( title="Already up to date", diff --git a/core/clients.py b/core/clients.py index 1d90cbb0e3..c8798e5423 100644 --- a/core/clients.py +++ b/core/clients.py @@ -377,9 +377,8 @@ def __init__(self, bot): except ConfigurationError as e: logger.critical( "Your MongoDB CONNECTION_URI might be copied wrong, try re-copying from the source again. " - "Otherwise noted in the following message:" + "Otherwise noted in the following message:\n%s", e ) - logger.critical(e) sys.exit(0) super().__init__(bot, db) From 69d69e63b8f336ce203aaef1854b32fbdbbab527 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Jul 2021 13:58:15 +0800 Subject: [PATCH 0896/1221] Keep more log files --- core/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/models.py b/core/models.py index d17e118e6c..76889809f7 100644 --- a/core/models.py +++ b/core/models.py @@ -127,7 +127,7 @@ def format(self, record): def configure_logging(name, level=None): global ch_debug, log_level - ch_debug = RotatingFileHandler(name, mode="a+", maxBytes=48000, backupCount=1) + ch_debug = RotatingFileHandler(name, mode="a+", maxBytes=78000, backupCount=10) formatter_debug = FileFormatter( "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", From 703f04f2d63d78facf6fb6808ac05e18b8aaff7c Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Jul 2021 13:59:33 +0800 Subject: [PATCH 0897/1221] Bump version --- CHANGELOG.md | 7 +++++++ README.md | 2 +- bot.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f1ddc9ef..fb9ee01487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.9.5dev1 + +## Misc + +- Bumped discord.py to v1.7.3, updated all other packages to latest. +- More debug log files are now kept. + # v3.9.4 ## Fixed diff --git a/README.md b/README.md index f61512d608..ea70af1ad2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 3cb5bb2d5f..dab03836a1 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.9.4" +__version__ = "3.9.5dev1" import asyncio From 3ed0eb7f870e9b056ea7901ea68558126b1b784d Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Jul 2021 14:01:45 +0800 Subject: [PATCH 0898/1221] Black format --- bot.py | 2 +- cogs/modmail.py | 5 +++-- core/clients.py | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bot.py b/bot.py index dab03836a1..1deb9b7d2a 100644 --- a/bot.py +++ b/bot.py @@ -640,7 +640,7 @@ async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() - if name not in UNICODE_EMOJI['en']: + if name not in UNICODE_EMOJI["en"]: try: name = await converter.convert(ctx, name.strip(":")) except commands.BadArgument as e: diff --git a/cogs/modmail.py b/cogs/modmail.py index 55110c323e..81fd2a911e 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -322,8 +322,9 @@ async def move(self, ctx, *, arguments): silent_words = ["silent", "silently"] silent = any(word in silent_words for word in options.split()) - await thread.channel.move(category=category, end=True, sync_permissions=True, - reason=f"{ctx.author} moved this thread.") + await thread.channel.move( + category=category, end=True, sync_permissions=True, reason=f"{ctx.author} moved this thread." + ) if self.bot.config["thread_move_notify"] and not silent: embed = discord.Embed( diff --git a/core/clients.py b/core/clients.py index c8798e5423..5c5acc8fad 100644 --- a/core/clients.py +++ b/core/clients.py @@ -377,7 +377,8 @@ def __init__(self, bot): except ConfigurationError as e: logger.critical( "Your MongoDB CONNECTION_URI might be copied wrong, try re-copying from the source again. " - "Otherwise noted in the following message:\n%s", e + "Otherwise noted in the following message:\n%s", + e, ) sys.exit(0) From 2ace13fc959bf6642aedff629a765096107f29c4 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Jul 2021 14:28:03 +0800 Subject: [PATCH 0899/1221] Updated sponsors --- README.md | 7 +++++++ SPONSORS.json | 24 ++++++++++++++++++++++-- cogs/utility.py | 17 ++++++++++++----- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f61512d608..81305c51db 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,13 @@ Real Madrid: +
      +
      +Discord Advice Center: +
      + + + Become a sponsor on [Patreon](https://patreon.com/kyber). diff --git a/SPONSORS.json b/SPONSORS.json index 1f1ed56d37..049998ca55 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -21,7 +21,7 @@ "value": "[**Click Here**](https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ?sub_confirmation=1)" }, { - "name": "Discord Server!", + "name": "Discord Server", "value": "[**Click Here**](https://discord.gg/V8ErqHb)" } ] @@ -37,7 +37,7 @@ }, "fields": [ { - "name": "Discord Server!", + "name": "Discord Server", "value": "[**Click here**](https://discord.gg/BanCwptMJV)" } ] @@ -70,5 +70,25 @@ } ] } + }, + { + "embed": { + "description": "──── 《𝐃𝐢𝐬𝐜𝐨𝐫𝐝 𝐀𝐝𝐯𝐢𝐜𝐞 𝐂𝐞𝐧𝐭𝐞𝐫 》 ────\n\n◈ We are a server aimed to meet your discord needs. We have tools, tricks and tips to grow your server and advertise your server. We offer professional server reviews and suggestions how to run it successfully as a part of our courtesy. Join the server and get the chance to add our very own BUMP BOT called DAC Advertise where you can advertise your server to other servers!\n", + "color": 53380, + "author": { + "name": "Discord Advice Center", + "url": "https://discord.gg/nkMDQfuK", + "icon_url": "https://i.imgur.com/cjVtRw5.jpg" + }, + "image": { + "url": "https://i.imgur.com/1hrjcHd.png" + }, + "fields": [ + { + "name": "Discord Server", + "value": "[**Click Here**](https://discord.gg/nkMDQfuK)" + } + ] + } } ] diff --git a/cogs/utility.py b/cogs/utility.py index 7b39631dcd..58b0449d7d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -356,18 +356,25 @@ async def about(self, ctx): inline=False, ) + embed.add_field( + name="Project Sponsors", + value=f"Checkout the people who supported Modmail with command `{self.bot.prefix}sponsors`!", + inline=False, + ) + embed.set_footer(text=footer) await ctx.send(embed=embed) - @commands.command() + @commands.command(aliases=["sponsor"]) @checks.has_permissions(PermissionLevel.REGULAR) @utils.trigger_typing async def sponsors(self, ctx): - """Shows a list of sponsors.""" - resp = await self.bot.session.get( + """Shows the sponsors of this project.""" + + async with self.bot.session.get( "https://raw.githubusercontent.com/kyb3r/modmail/master/SPONSORS.json" - ) - data = loads(await resp.text()) + ) as resp: + data = loads(await resp.text()) embeds = [] From 1efdc3403a622d70892f30dbffda894228f93ae8 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Fri, 9 Jul 2021 09:50:47 +0800 Subject: [PATCH 0900/1221] Improved dockerfile --- .dockerignore | 12 +++--------- Dockerfile | 21 +++++++++++++-------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.dockerignore b/.dockerignore index 99fd4a241b..74003e7e30 100644 --- a/.dockerignore +++ b/.dockerignore @@ -140,17 +140,11 @@ test.py # Other stuff .env.example .gitignore -.lint.py -.pylintrc -.travis.yml +.github/ app.json CHANGELOG.md -CODE_OF_CONDUCT.md -CONTRIBUTING.md -requirements.min.txt Procfile pyproject.toml README.md -runtime.txt -SPONSORS.json -stack.yml \ No newline at end of file +Pipfile +Pipfile.lock diff --git a/Dockerfile b/Dockerfile index 8165e06a53..232e84fb20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,16 @@ -FROM python:3.7-alpine +FROM python:3.9-slim as py + +FROM py as build + +RUN apt update && apt install -y g++ +COPY requirements.min.txt / +RUN pip install --prefix=/inst -U -r /requirements.min.txt + +FROM py + ENV USING_DOCKER yes +COPY --from=build /inst /usr/local + WORKDIR /modmailbot +CMD ["python", "bot.py"] COPY . /modmailbot -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 2c26227a12712c6b550245b9aa33e36b909c91b0 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Fri, 9 Jul 2021 10:20:16 +0800 Subject: [PATCH 0901/1221] Add restart and environment for bot --- docker-compose.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5d2061df6b..d3745c9b4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,20 +2,25 @@ version: "3.7" services: bot: image: kyb3rr/modmail + restart: always env_file: - .env + environment: + - CONNECTION_URI=mongodb://mongo depends_on: - mongo logviewer: image: kyb3rr/logviewer + restart: always depends_on: - mongo environment: - MONGO_URI=mongodb://mongo ports: - - 8000:8000 + - 80:8000 mongo: image: mongo + restart: always volumes: - mongodb:/data/db From 05cde283d31af5622da8ded6b81f68620eafda59 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Fri, 9 Jul 2021 10:26:03 +0800 Subject: [PATCH 0902/1221] Update sponsors --- README.md | 2 +- SPONSORS.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 81305c51db..701661c4d0 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ Real Madrid:
      Discord Advice Center:
      - + diff --git a/SPONSORS.json b/SPONSORS.json index 049998ca55..30081e24e5 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -86,7 +86,7 @@ "fields": [ { "name": "Discord Server", - "value": "[**Click Here**](https://discord.gg/nkMDQfuK)" + "value": "[**Click Here**](https://discord.gg/zmwZy5fd9v)" } ] } From 10f1bccf4722026d8c7aa0474e048d8a0b9ada2f Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sat, 10 Jul 2021 15:49:28 +0200 Subject: [PATCH 0903/1221] Update modmail.py fix typo { https://github.com/kyb3r/modmail/pull/3067 } { https://usagi.xn--6frz82g/6DEshJ } --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 87b9d91e57..8b91342fa4 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -537,7 +537,7 @@ async def subscribe( if mention in mentions: embed = discord.Embed( color=self.bot.error_color, - description=f"{mention} is not subscribed to this thread.", + description=f"{mention} is already subscribed to this thread.", ) else: mentions.append(mention) From 1a238b3099b5593a5465479ef63c6ffbadd9b07e Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 22 Jul 2021 16:17:06 +0800 Subject: [PATCH 0904/1221] Formatting and final updates to deps --- Pipfile.lock | 225 +++++++++++++++++++++++++++------------------- bot.py | 49 +++------- cogs/modmail.py | 84 +++++------------ cogs/plugins.py | 19 ++-- cogs/utility.py | 86 +++++------------- core/changelog.py | 10 +-- core/checks.py | 4 +- core/clients.py | 34 ++----- core/models.py | 7 +- core/thread.py | 32 ++----- 10 files changed, 210 insertions(+), 340 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 15fdcacc5c..a9f820119e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -202,81 +202,115 @@ "srv" ], "hashes": [ - "sha256:03be7ad107d252bb7325d4af6309fdd2c025d08854d35f0e7abc8bf048f4245e", - "sha256:071552b065e809d24c5653fcc14968cfd6fde4e279408640d5ac58e3353a3c5f", - "sha256:08b8723248730599c9803ae4c97b8f3f76c55219104303c88cb962a31e3bb5ee", - "sha256:08bda7b2c522ff9f1e554570da16298271ebb0c56ab9699446aacba249008988", - "sha256:0aaf4d44f1f819360f9432df538d54bbf850f18152f34e20337c01b828479171", - "sha256:0cabfc297f4cf921f15bc789a8fbfd7115eb9f813d3f47a74b609894bc66ab0d", - "sha256:13acf6164ead81c9fc2afa0e1ea6d6134352973ce2bb35496834fee057063c04", - "sha256:15b083d1b789b230e5ac284442d9ecb113c93f3785a6824f748befaab803b812", - "sha256:161fcd3281c42f644aa8dec7753cca2af03ce654e17d76da4f0dab34a12480ca", - "sha256:1a994a42f49dab5b6287e499be7d3d2751776486229980d8857ad53b8333d469", - "sha256:20d75ea11527331a2980ab04762a9d960bcfea9475c54bbeab777af880de61cd", - "sha256:225c61e08fe517aede7912937939e09adf086c8e6f7e40d4c85ad678c2c2aea3", - "sha256:3135dd574ef1286189f3f04a36c8b7a256376914f8cbbce66b94f13125ded858", - "sha256:3491c7de09e44eded16824cb58cf9b5cc1dc6f066a0bb7aa69929d02aa53b828", - "sha256:3551912f5c34d8dd7c32c6bb00ae04192af47f7b9f653608f107d19c1a21a194", - "sha256:38a7b5140a48fc91681cdb5cb95b7cd64640b43d19259fdd707fa9d5a715f2b2", - "sha256:3a3498a8326111221560e930f198b495ea6926937e249f475052ffc6893a6680", - "sha256:3bfc7689a1bacb9bcd2f2d5185d99507aa29f667a58dd8adaa43b5a348139e46", - "sha256:421d13523d11c57f57f257152bc4a6bb463aadf7a3918e9c96fefdd6be8dbfb8", - "sha256:424799c71ff435094e5fb823c40eebb4500f0e048133311e9c026467e8ccebac", - "sha256:474e21d0e07cd09679e357d1dac76e570dab86665e79a9d3354b10a279ac6fb3", - "sha256:4c7e8c8e1e1918dcf6a652ac4b9d87164587c26fd2ce5dd81e73a5ab3b3d492f", - "sha256:506a6dab4c7ffdcacdf0b8e70bd20eb2e77fa994519547c9d88d676400fcad58", - "sha256:510cd3bfabb63a07405b7b79fae63127e34c118b7531a2cbbafc7a24fd878594", - "sha256:517ba47ca04a55b1f50ee8df9fd97f6c37df5537d118fb2718952b8623860466", - "sha256:539d4cb1b16b57026999c53e5aab857fe706e70ae5310cc8c232479923f932e6", - "sha256:5c36428cc4f7fae56354db7f46677fd21222fc3cb1e8829549b851172033e043", - "sha256:5db59223ed1e634d842a053325f85f908359c6dac9c8ddce8ef145061fae7df8", - "sha256:5e606846c049ed40940524057bfdf1105af6066688c0e6a1a3ce2038589bae70", - "sha256:6060794aac9f7b0644b299f46a9c6cbc0bc470bd01572f4134df140afd41ded6", - "sha256:62c29bc36a6d9be68fe7b5aaf1e120b4aa66a958d1e146601fcd583eb12cae7b", - "sha256:73326b211e7410c8bd6a74500b1e3f392f39cf10862e243d00937e924f112c01", - "sha256:78f07961f4f214ea8e80be63cffd5cc158eb06cd922ffbf6c7155b11728f28f9", - "sha256:7c97554ea521f898753d9773891d0347ebfaddcc1dee2ad94850b163171bf1f1", - "sha256:8898f6699f740ca93a0879ed07d8e6db02d68af889d0ebb3d13ab017e6b1af1e", - "sha256:8a41fdc751dc4707a4fafb111c442411816a7c225ebb5cadb57599534b5d5372", - "sha256:8e0004b0393d72d76de94b4792a006cb960c1c65c7659930fbf9a81ce4341982", - "sha256:977b1d4f868986b4ba5d03c317fde4d3b66e687d74473130cd598e3103db34fa", - "sha256:9a4f6e0b01df820ba9ed0b4e618ca83a1c089e48d4f268d0e00dcd49893d4549", - "sha256:9b9298964389c180a063a9e8bac8a80ed42de11d04166b20249bfa0a489e0e0f", - "sha256:a08c8b322b671857c81f4c30cd3c8df2895fd3c0e9358714f39e0ef8fb327702", - "sha256:ad31f184dcd3271de26ab1f9c51574afb99e1b0e484ab1da3641256b723e4994", - "sha256:aff3656af2add93f290731a6b8930b23b35c0c09569150130a58192b3ec6fc61", - "sha256:b2f41261b648cf5dee425f37ff14f4ad151c2f24b827052b402637158fd056ef", - "sha256:b413117210fa6d92664c3d860571e8e8727c3e8f2ff197276c5d0cb365abd3ad", - "sha256:b7efc7e7049ef366777cfd35437c18a4166bb50a5606a1c840ee3b9624b54fc9", - "sha256:b8f94acd52e530a38f25e4d5bf7ddfdd4bea9193e718f58419def0d4406b58d3", - "sha256:d0a70151d7de8a3194cdc906bcc1a42e14594787c64b0c1c9c975e5a2af3e251", - "sha256:d360e5d5dd3d55bf5d1776964625018d85b937d1032bae1926dd52253decd0db", - "sha256:d4e62417e89b717a7bcd8576ac3108cd063225942cc91c5b37ff5465fdccd386", - "sha256:d65bac5f6724d9ea6f0b5a0f0e4952fbbf209adcf6b5583b54c54bd2fcd74dc0", - "sha256:e02beaab433fd1104b2804f909e694cfbdb6578020740a9051597adc1cd4e19f", - "sha256:e4b631688dfbdd61b5610e20b64b99d25771c6d52d9da73349342d2a0f11c46a", - "sha256:e4e9db78b71db2b1684ee4ecc3e32c4600f18cdf76e6b9ae03e338e52ee4b168", - "sha256:eb4d176394c37a76e8b0afe54b12d58614a67a60a7f8c0dd3a5afbb013c01092", - "sha256:f08665d3cc5abc2f770f472a9b5f720a9b3ab0b8b3bb97c7c1487515e5653d39", - "sha256:f3d851af3852f16ad4adc7ee054fd9c90a7a5063de94d815b7f6a88477b9f4c6", - "sha256:f4ba58157e8ae33ee86fadf9062c506e535afd904f07f9be32731f4410a23b7f", - "sha256:f664ed7613b8b18f0ce5696b146776266a038c19c5cd6efffa08ecc189b01b73", - "sha256:f947b359cc4769af8b49be7e37af01f05fcf15b401da2528021148e4a54426d1", - "sha256:fe4189846448df013cd9df11bba38ddf78043f8c290a9f06430732a7a8601cce", - "sha256:fea5cb1c63efe1399f0812532c7cf65458d38fd011be350bc5021dfcac39fba8", - "sha256:fedf0dee7a412ca6d1d6d92c158fe9cbaa8ea0cae90d268f9ccc0744de7a97d0", - "sha256:fffff7bfb6799a763d3742c59c6ee7ffadda21abed557637bc44ed1080876484" + "sha256:02dc0b0f48ed3cd06c13b7e31b066bf91e00dac5f8147b0a0a45f9009bfab857", + "sha256:053b4ebf91c7395d1fcd2ce6a9edff0024575b7b2de6781554a4114448a8adc9", + "sha256:070a4ef689c9438a999ec3830e69b208ff0d12251846e064d947f97d819d1d05", + "sha256:072ba7cb65c8aa4d5c5659bf6722ee85781c9d7816dc00679b8b6f3dff1ddafc", + "sha256:0b6055e0ef451ff73c93d0348d122a0750dddf323b9361de5835dac2f6cf7fc1", + "sha256:11f9e0cfc84ade088a38df2708d0b958bb76360181df1b2e1e1a41beaa57952b", + "sha256:18290649759f9db660972442aa606f845c368db9b08c4c73770f6da14113569b", + "sha256:186104a94d39b8412f8e3de385acd990a628346a4402d4f3a288a82b8660bd22", + "sha256:1970cfe2aec1bf74b40cf30c130ad10cd968941694630386db33e1d044c22a2e", + "sha256:19d4bd0fc29aa405bb1781456c9cfff9fceabb68543741eb17234952dbc2bbb0", + "sha256:1bab889ae7640eba739f67fcbf8eff252dddc60d4495e6ddd3a87cd9a95fdb52", + "sha256:1bc6fe7279ff40c6818db002bf5284aa03ec181ea1b1ceaeee33c289d412afa7", + "sha256:208debdcf76ed39ebf24f38509f50dc1c100e31e8653817fedb8e1f867850a13", + "sha256:2399a85b54f68008e483b2871f4a458b4c980469c7fe921595ede073e4844f1e", + "sha256:246ec420e4c8744fceb4e259f906211b9c198e1f345e6158dcd7cbad3737e11e", + "sha256:24f8aeec4d6b894a6128844e50ff423dd02462ee83addf503c598ee3a80ddf3d", + "sha256:255a35bf29185f44b412e31a927d9dcedda7c2c380127ecc4fbf2f61b72fa978", + "sha256:2dbfbbded947a83a3dffc2bd1ec4750c17e40904692186e2c55a3ad314ca0222", + "sha256:2e92aa32300a0b5e4175caec7769f482b292769807024a86d674b3f19b8e3755", + "sha256:316c1b8723afa9870567cd6dff35d440b2afeda53aa13da6c5ab85f98ed6f5ca", + "sha256:333bfad77aa9cd11711febfb75eed0bb537a1d022e1c252714dad38993590240", + "sha256:39dafa2eaf577d1969f289dc9a44501859a1897eb45bd589e93ce843fc610800", + "sha256:3ce83f17f641a62a4dfb0ba1b8a3c1ced7c842f511b5450d90c030c7828e3693", + "sha256:46d5ec90276f71af3a29917b30f2aec2315a2759b5f8d45b3b63a07ca8a070a3", + "sha256:48d5bc80ab0af6b60c4163c5617f5cd23f2f880d7600940870ea5055816af024", + "sha256:4ba0def4abef058c0e5101e05e3d5266e6fffb9795bbf8be0fe912a7361a0209", + "sha256:5af390fa9faf56c93252dab09ea57cd020c9123aa921b63a0ed51832fdb492e7", + "sha256:5e574664f1468872cd40f74e4811e22b1aa4de9399d6bcfdf1ee6ea94c017fcf", + "sha256:625befa3bc9b40746a749115cc6a15bf20b9bd7597ca55d646205b479a2c99c7", + "sha256:6261bee7c5abadeac7497f8f1c43e521da78dd13b0a2439f526a7b0fc3788824", + "sha256:657ad80de8ec9ed656f28844efc801a0802961e8c6a85038d97ff6f555ef4919", + "sha256:6b89dc51206e4971c5568c797991eaaef5dc2a6118d67165858ad11752dba055", + "sha256:6e66780f14c2efaf989cd3ac613b03ee6a8e3a0ba7b96c0bb14adca71a427e55", + "sha256:6fb3f85870ae26896bb44e67db94045f2ebf00c5d41e6b66cdcbb5afd644fc18", + "sha256:701e08457183da70ed96b35a6b43e6ba1df0b47c837b063cde39a1fbe1aeda81", + "sha256:70761fd3c576b027eec882b43ee0a8e5b22ff9c20cdf4d0400e104bc29e53e34", + "sha256:73b400fdc22de84bae0dbf1a22613928a41612ec0a3d6ed47caf7ad4d3d0f2ff", + "sha256:7412a36798966624dc4c57d64aa43c2d1100b348abd98daaac8e99e57d87e1d7", + "sha256:78ecb8d42f50d393af912bfb1fb1dcc9aabe9967973efb49ee577e8f1cea494c", + "sha256:7c6a9948916a7bbcc6d3a9f6fb75db1acb5546078023bfb3db6efabcd5a67527", + "sha256:7c72d08acdf573455b2b9d2b75b8237654841d63a48bc2327dc102c6ee89b75a", + "sha256:7d98ce3c42921bb91566121b658e0d9d59a9082a9bd6f473190607ff25ab637f", + "sha256:845a8b83798b2fb11b09928413cb32692866bfbc28830a433d9fa4c8c3720dd0", + "sha256:94d38eba4d1b5eb3e6bfece0651b855a35c44f32fd91f512ab4ba41b8c0d3e66", + "sha256:9a13661681d17e43009bb3e85e837aa1ec5feeea1e3654682a01b8821940f8b3", + "sha256:a0e5dff6701fa615f165306e642709e1c1550d5b237c5a7a6ea299886828bd50", + "sha256:a2239556ff7241584ce57be1facf25081669bb457a9e5cbe68cce4aae6567aa1", + "sha256:a325600c83e61e3c9cebc0c2b1c8c4140fa887f789085075e8f44c8ff2547eb9", + "sha256:a3566acfbcde46911c52810374ecc0354fdb841284a3efef6ff7105bc007e9a8", + "sha256:a634a4730ce0b0934ed75e45beba730968e12b4dafbb22f69b3b2f616d9e644e", + "sha256:a6d055f01b83b1a4df8bb0c61983d3bdffa913764488910af3620e5c2450bf83", + "sha256:a752ecd1a26000a6d67be7c9a2e93801994a8b3f866ac95b672fbc00225ca91a", + "sha256:a9ba2a63777027b06b116e1ea8248e66fd1bedc2c644f93124b81a91ddbf6d88", + "sha256:aaa038eafb7186a4abbb311fcf20724be9363645882bbce540bef4797e812a7a", + "sha256:af586e85144023686fb0af09c8cdf672484ea182f352e7ceead3d832de381e1b", + "sha256:b0a0cf39f589e52d801fdef418305562bc030cdf8929217463c8433c65fd5c2f", + "sha256:b1c4874331ab960429caca81acb9d2932170d66d6d6f87e65dc4507a85aca152", + "sha256:b3b5b3cbc3fdf4fcfa292529df2a85b5d9c7053913a739d3069af1e12e12219f", + "sha256:b542d56ed1b8d5cf3bb36326f814bd2fbe8812dfd2582b80a15689ea433c0e35", + "sha256:b6ea08758b6673610b3c5bdf47189286cf9c58b1077558706a2f6f8744922527", + "sha256:b754240daafecd9d5fce426b0fbaaed03f4ebb130745c8a4ae9231fffb8d75e5", + "sha256:b772bab31cbd9cb911e41e1a611ebc9497f9a32a7348e2747c38210f75c00f41", + "sha256:b88d1742159bc93a078733f9789f563cef26f5e370eba810476a71aa98e5fbc2", + "sha256:b8bf42d3b32f586f4c9e37541769993783a534ad35531ce8a4379f6fa664fba9", + "sha256:bc9ac81e73573516070d24ce15da91281922811f385645df32bd3c8a45ab4684", + "sha256:c188db6cf9e14dbbb42f5254292be96f05374a35e7dfa087cc2140f0ff4f10f6", + "sha256:c55782a55f4a013a78ac5b6ee4b8731a192dea7ab09f1b6b3044c96d5128edd4", + "sha256:c5cab230e7cabdae9ff23c12271231283efefb944c1b79bed79a91beb65ba547", + "sha256:cbf8672edeb7b7128c4a939274801f0e32bbf5159987815e3d1eace625264a46", + "sha256:cc2894fe91f31a513860238ede69fe47fada21f9e7ddfe73f7f9fef93a971e41", + "sha256:cda9e628b1315beec8341e8c04aac9a0b910650b05e0751e42e399d5694aeacb", + "sha256:ceae3ab9e11a27aaab42878f1d203600dfd24f0e43678b47298219a0f10c0d30", + "sha256:ced944dcdd561476deef7cb7bfd4987c69fffbfeff6d02ca4d5d4fd592d559b7", + "sha256:d04ca462cb99077e6c059e97c072957caf2918e6e4191e3161c01c439e0193de", + "sha256:d1131562ddc2ea8a446f66c2648d7dabec2b3816fc818528eb978a75a6d23b2e", + "sha256:d1740776b70367277323fafb76bcf09753a5cc9824f5d705bac22a34ff3668ea", + "sha256:d6e11ffd43184d529d6752d6dcb62b994f903038a17ea2168ef1910c96324d26", + "sha256:d73e10772152605f6648ba4410318594f1043bbfe36d2fadee7c4b8912eff7c5", + "sha256:da8288bc4a7807c6715416deed1c57d94d5e03e93537889e002bf985be503f1a", + "sha256:db93608a246da44d728842b8fa9e45aa9782db76955f634a707739a8d53ff544", + "sha256:dcd3d0009fbb6e454d729f8b22d0063bd9171c31a55e0f0271119bd4f2700023", + "sha256:dd1f49f949a658c4e8f81ed73f9aad25fcc7d4f62f767f591e749e30038c4e1d", + "sha256:dd6ff2192f34bd622883c745a56f492b1c9ccd44e14953e8051c33024a2947d5", + "sha256:e018a4921657c2d3f89c720b7b90b9182e277178a04a7e9542cc79d7d787ca51", + "sha256:e2b7670c0c8c6b501464150dd49dd0d6be6cb7f049e064124911cec5514fa19e", + "sha256:e7a33322e08021c37e89cae8ff06327503e8a1719e97c69f32c31cbf6c30d72c", + "sha256:e8a82e35d52ad6f867e88096a1a2b9bdc7ec4d5e65c7b4976a248bf2d1a32a93", + "sha256:e9faf8d4712d5ea301d74abfcf6dafe4b7f4af7936e91f283b0ad7bf69ed3e3a", + "sha256:ec5ca7c0007ce268048bbe0ffc6846ed1616cf3d8628b136e81d5e64ff3f52a2", + "sha256:eee42a1cc06565f6b21caa1f504ec15e07de7ebfd520ab57f8cb3308bc118e22", + "sha256:f2acf9bbcd514e901f82c4ca6926bbd2ae61716728f110b4343eb0a69612d018", + "sha256:f55c1ddcc1f6050b07d468ce594f55dbf6107b459e16f735d26818d7be1e9538", + "sha256:f6977a520bd96e097c8a37a8cbb9faa1ea99d21bf84190195056e25f688af73d", + "sha256:f94c7d22fb36b184734dded7345a04ec5f95130421c775b8b0c65044ef073f34", + "sha256:fa8957e9a1b202cb45e6b839c241cd986c897be1e722b81d2f32e9c6aeee80b0", + "sha256:fd3854148005c808c485c754a184c71116372263709958b42aefbef2e5dd373a", + "sha256:fe5872ce6f9627deac8314bdffd3862624227c3de4c17ef0cc78bbf0402999eb", + "sha256:ffbae429ba9e42d0582d3ac63fdb410338892468a2107d8ff68228ec9a39a0ed" ], "index": "pypi", - "version": "==3.11.4" + "version": "==3.12.0" }, "python-dateutil": { "hashes": [ - "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", - "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "index": "pypi", - "version": "==2.8.1" + "version": "==2.8.2" }, "python-dotenv": { "hashes": [ @@ -304,20 +338,19 @@ }, "uvloop": { "hashes": [ - "sha256:114543c84e95df1b4ff546e6e3a27521580466a30127f12172a3278172ad68bc", - "sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69", - "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01", - "sha256:42eda9f525a208fbc4f7cecd00fa15c57cc57646c76632b3ba2fe005004f051d", - "sha256:44cac8575bf168601424302045234d74e3561fbdbac39b2b54cc1d1d00b70760", - "sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c", - "sha256:7ae39b11a5f4cec1432d706c21ecc62f9e04d116883178b09671aa29c46f7a47", - "sha256:90e56f17755e41b425ad19a08c41dc358fa7bf1226c0f8e54d4d02d556f7af7c", - "sha256:b45218c99795803fb8bdbc9435ff7f54e3a591b44cd4c121b02fa83affb61c7c", - "sha256:e5e5f855c9bf483ee6cd1eb9a179b740de80cb0ae2988e3fa22309b78e2ea0e7" + "sha256:0de811931e90ae2da9e19ce70ffad73047ab0c1dba7c6e74f9ae1a3aabeb89bd", + "sha256:1ff05116ede1ebdd81802df339e5b1d4cab1dfbd99295bf27e90b4cec64d70e9", + "sha256:2d8ffe44ae709f839c54bacf14ed283f41bee90430c3b398e521e10f8d117b3a", + "sha256:5cda65fc60a645470b8525ce014516b120b7057b576fa876cdfdd5e60ab1efbb", + "sha256:63a3288abbc9c8ee979d7e34c34e780b2fbab3e7e53d00b6c80271119f277399", + "sha256:7522df4e45e4f25b50adbbbeb5bb9847495c438a628177099d2721f2751ff825", + "sha256:7f4b8a905df909a407c5791fb582f6c03b0d3b491ecdc1cdceaefbc9bf9e08f6", + "sha256:905f0adb0c09c9f44222ee02f6b96fd88b493478fffb7a345287f9444e926030", + "sha256:ae2b325c0f6d748027f7463077e457006b4fdb35a8788f01754aadba825285ee", + "sha256:e71fb9038bfcd7646ca126c5ef19b17e48d4af9e838b2bcfda7a9f55a6552a32" ], - "index": "pypi", "markers": "sys_platform != 'win32'", - "version": "==0.15.2" + "version": "==0.15.3" }, "yarl": { "hashes": [ @@ -373,11 +406,11 @@ }, "astroid": { "hashes": [ - "sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892", - "sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9" + "sha256:7b963d1c590d490f60d2973e57437115978d3a2529843f160b5003b721e1e925", + "sha256:83e494b02d75d07d4e347b27c066fd791c0c74fc96c613d1ea3de0c82c48168f" ], "markers": "python_version ~= '3.6'", - "version": "==2.6.2" + "version": "==2.6.5" }, "bandit": { "hashes": [ @@ -403,6 +436,14 @@ "markers": "python_version >= '3.6'", "version": "==8.0.1" }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "index": "pypi", + "version": "==0.4.4" + }, "gitdb": { "hashes": [ "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", @@ -421,11 +462,11 @@ }, "isort": { "hashes": [ - "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56", - "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c" + "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813", + "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e" ], "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", - "version": "==5.9.1" + "version": "==5.9.2" }, "lazy-object-proxy": { "hashes": [ @@ -471,10 +512,10 @@ }, "pathspec": { "hashes": [ - "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", - "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" ], - "version": "==0.8.1" + "version": "==0.9.0" }, "pbr": { "hashes": [ @@ -486,11 +527,11 @@ }, "pylint": { "hashes": [ - "sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a", - "sha256:5d46330e6b8886c31b5e3aba5ff48c10f4aa5e76cbf9002c6544306221e63fbc" + "sha256:1f333dc72ef7f5ea166b3230936ebcfb1f3b722e76c980cb9fe6b9f95e8d3172", + "sha256:748f81e5776d6273a6619506e08f1b48ff9bcb8198366a56821cf11aac14fc87" ], "index": "pypi", - "version": "==2.9.3" + "version": "==2.9.5" }, "pyyaml": { "hashes": [ diff --git a/bot.py b/bot.py index 1deb9b7d2a..131032ec80 100644 --- a/bot.py +++ b/bot.py @@ -506,8 +506,7 @@ def command_perm(self, command_name: str) -> PermissionLevel: logger.debug("Command %s not found.", command_name) return PermissionLevel.INVALID level = next( - (check.permission_level for check in command.checks if hasattr(check, "permission_level")), - None, + (check.permission_level for check in command.checks if hasattr(check, "permission_level")), None, ) if level is None: logger.debug("Command %s does not have a permission level.", command_name) @@ -610,13 +609,7 @@ async def on_ready(self): if self.config.get("data_collection"): self.metadata_loop = tasks.Loop( - self.post_metadata, - seconds=0, - minutes=0, - hours=1, - count=None, - reconnect=True, - loop=None, + self.post_metadata, seconds=0, minutes=0, hours=1, count=None, reconnect=True, loop=None, ) self.metadata_loop.before_loop(self.before_post_metadata) self.metadata_loop.start() @@ -769,8 +762,7 @@ def check_manual_blocked(self, author: discord.Member) -> bool: 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, + r"Deprecated time message for user %s, block and unblock again to update.", author.name, ) if end_time is not None: @@ -791,11 +783,7 @@ async def _process_blocked(self, message): return False async def is_blocked( - self, - author: discord.User, - *, - channel: discord.TextChannel = None, - send_message: bool = False, + self, author: discord.User, *, channel: discord.TextChannel = None, send_message: bool = False, ) -> typing.Tuple[bool, str]: member = self.guild.get_member(author.id) @@ -826,9 +814,7 @@ async def is_blocked( if send_message: await channel.send( embed=discord.Embed( - title="Message not sent!", - description=new_reason, - color=self.error_color, + title="Message not sent!", description=new_reason, color=self.error_color, ) ) return True @@ -927,8 +913,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: description=self.config["disabled_current_thread_response"], ) embed.set_footer( - text=self.config["disabled_current_thread_footer"], - icon_url=self.guild.icon_url, + text=self.config["disabled_current_thread_footer"], icon_url=self.guild.icon_url, ) logger.info("A message was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) @@ -1289,18 +1274,14 @@ async def on_raw_reaction_add(self, payload): await message.remove_reaction(payload.emoji, member) await message.add_reaction(emoji_fmt) # bot adds as well - if self.config["dm_disabled"] in ( - DMDisabled.NEW_THREADS, - DMDisabled.ALL_THREADS, - ): + if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS,): embed = discord.Embed( title=self.config["disabled_new_thread_title"], color=self.error_color, description=self.config["disabled_new_thread_response"], ) embed.set_footer( - text=self.config["disabled_new_thread_footer"], - icon_url=self.guild.icon_url, + text=self.config["disabled_new_thread_footer"], icon_url=self.guild.icon_url, ) logger.info( "A new thread using react to contact was blocked from %s due to disabled Modmail.", @@ -1359,9 +1340,7 @@ async def on_member_remove(self, member): if thread: if self.config["close_on_leave"]: await thread.close( - closer=member.guild.me, - message=self.config["close_on_leave_reason"], - silent=True, + closer=member.guild.me, message=self.config["close_on_leave_reason"], silent=True, ) else: embed = discord.Embed( @@ -1544,9 +1523,7 @@ async def autoupdate(self): commit_data = data["data"] user = data["user"] embed.set_author( - name=user["username"] + " - Updating Bot", - icon_url=user["avatar_url"], - url=user["url"], + name=user["username"] + " - Updating Bot", icon_url=user["avatar_url"], url=user["url"], ) embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") @@ -1575,11 +1552,7 @@ async def autoupdate(self): pass command = "git pull" - proc = await asyncio.create_subprocess_shell( - command, - stderr=PIPE, - stdout=PIPE, - ) + proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) err = await proc.stderr.read() err = err.decode("utf-8").rstrip() res = await proc.stdout.read() diff --git a/cogs/modmail.py b/cogs/modmail.py index 0e41b76677..0db8a85e2e 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -50,9 +50,7 @@ async def setup(self, ctx): if self.bot.modmail_guild is None: embed = discord.Embed( - title="Error", - description="Modmail functioning guild not found.", - color=self.bot.error_color, + title="Error", description="Modmail functioning guild not found.", color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -184,9 +182,7 @@ async def snippet_raw(self, ctx, *, name: str.lower): 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, + title=f'Raw snippet - "{name}":', description=f"```\n{val}```", color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -208,9 +204,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte """ if name in self.bot.snippets: embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"Snippet `{name}` already exists.", + title="Error", color=self.bot.error_color, description=f"Snippet `{name}` already exists.", ) return await ctx.send(embed=embed) @@ -234,9 +228,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte await self.bot.config.update() embed = discord.Embed( - title="Added snippet", - color=self.bot.main_color, - description="Successfully created snippet.", + title="Added snippet", color=self.bot.main_color, description="Successfully created snippet.", ) return await ctx.send(embed=embed) @@ -453,8 +445,7 @@ async def notify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower if mention in mentions: embed = discord.Embed( - color=self.bot.error_color, - description=f"{mention} is already going to be mentioned.", + color=self.bot.error_color, description=f"{mention} is already going to be mentioned.", ) else: mentions.append(mention) @@ -489,8 +480,7 @@ async def unnotify(self, ctx, *, user_or_role: Union[discord.Role, User, str.low if mention not in mentions: embed = discord.Embed( - color=self.bot.error_color, - description=f"{mention} does not have a pending notification.", + color=self.bot.error_color, description=f"{mention} does not have a pending notification.", ) else: mentions.remove(mention) @@ -526,8 +516,7 @@ async def subscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lo if mention in mentions: embed = discord.Embed( - color=self.bot.error_color, - description=f"{mention} is already subscribed to this thread.", + color=self.bot.error_color, description=f"{mention} is already subscribed to this thread.", ) else: mentions.append(mention) @@ -562,15 +551,13 @@ async def unsubscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str. if mention not in mentions: embed = discord.Embed( - color=self.bot.error_color, - description=f"{mention} is not subscribed to this thread.", + color=self.bot.error_color, description=f"{mention} is not 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 from this thread.", + color=self.bot.main_color, description=f"{mention} is now unsubscribed from this thread.", ) return await ctx.send(embed=embed) @@ -697,8 +684,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.", + color=self.bot.error_color, description="This user does not have any previous logs.", ) return await ctx.send(embed=embed) @@ -725,8 +711,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.", + color=self.bot.error_color, description="No log entries have been found for that query.", ) return await ctx.send(embed=embed) @@ -745,9 +730,7 @@ async def logs_delete(self, ctx, key_or_link: str): if not success: embed = discord.Embed( - title="Error", - description=f"Log entry `{key}` not found.", - color=self.bot.error_color, + title="Error", description=f"Log entry `{key}` not found.", color=self.bot.error_color, ) else: embed = discord.Embed( @@ -800,8 +783,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): if not embeds: embed = discord.Embed( - color=self.bot.error_color, - description="No log entries have been found for that query.", + color=self.bot.error_color, description="No log entries have been found for that query.", ) return await ctx.send(embed=embed) @@ -1012,10 +994,7 @@ async def contact( else: thread = await self.bot.threads.create( - recipient=user, - creator=ctx.author, - category=category, - manual_trigger=manual_trigger, + recipient=user, creator=ctx.author, category=category, manual_trigger=manual_trigger, ) if thread.cancelled: return @@ -1029,11 +1008,7 @@ async def contact( else: description = f"{ctx.author.name} has opened a Modmail thread." - em = discord.Embed( - title="New Thread", - description=description, - color=self.bot.main_color, - ) + em = discord.Embed(title="New Thread", description=description, color=self.bot.main_color,) if self.bot.config["show_timestamp"]: em.timestamp = datetime.utcnow() em.set_footer(icon_url=ctx.author.avatar_url) @@ -1075,8 +1050,7 @@ async def blocked(self, ctx): 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.", - id_, + r"Deprecated time message for user %s, block and unblock again to update.", id_, ) if end_time is not None: @@ -1107,8 +1081,7 @@ async def blocked(self, ctx): end_time = re.search(r"%([^%]+?)%", reason) if end_time is not None: logger.warning( - r"Deprecated time message for role %s, block and unblock again to update.", - id_, + r"Deprecated time message for role %s, block and unblock again to update.", id_, ) if end_time is not None: @@ -1130,9 +1103,7 @@ async def blocked(self, ctx): line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title="Blocked Users (Continued)", - color=self.bot.main_color, - description=line, + title="Blocked Users (Continued)", color=self.bot.main_color, description=line, ) embeds.append(embed) else: @@ -1149,9 +1120,7 @@ async def blocked(self, ctx): line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title="Blocked Roles (Continued)", - color=self.bot.main_color, - description=line, + title="Blocked Roles (Continued)", color=self.bot.main_color, description=line, ) embeds.append(embed) else: @@ -1211,9 +1180,7 @@ async def blocked_whitelist(self, ctx, *, user: User = None): ) else: embed = discord.Embed( - title="Success", - color=self.bot.main_color, - description=f"{mention} is now whitelisted.", + title="Success", color=self.bot.main_color, description=f"{mention} is now whitelisted.", ) return await ctx.send(embed=embed) @@ -1291,9 +1258,7 @@ async def block( ) else: embed = discord.Embed( - title="Success", - color=self.bot.main_color, - description=f"{mention} is now blocked {reason}", + title="Success", color=self.bot.main_color, description=f"{mention} is now blocked {reason}", ) if isinstance(user_or_role, discord.Role): @@ -1356,9 +1321,7 @@ async def unblock(self, ctx, *, user_or_role: Union[User, Role] = None): await self.bot.config.update() embed = discord.Embed( - title="Success", - color=self.bot.main_color, - description=f"{mention} is no longer blocked.", + title="Success", color=self.bot.main_color, description=f"{mention} is no longer blocked.", ) else: embed = discord.Embed( @@ -1415,8 +1378,7 @@ async def repair(self, ctx): # 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), + ((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.") diff --git a/cogs/plugins.py b/cogs/plugins.py index aab5dc8bd6..a9a4d247da 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -159,9 +159,7 @@ async def initial_load_plugins(self): except Exception: self.bot.config["plugins"].remove(plugin_name) logger.error( - "Error when loading plugin %s. Plugin removed from config.", - plugin, - exc_info=True, + "Error when loading plugin %s. Plugin removed from config.", plugin, exc_info=True, ) continue @@ -278,8 +276,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if not self._ready_event.is_set(): embed = discord.Embed( - description="Plugins are still loading, please try again later.", - color=self.bot.main_color, + description="Plugins are still loading, please try again later.", color=self.bot.main_color, ) await ctx.send(embed=embed) return @@ -349,20 +346,17 @@ async def plugins_add(self, ctx, *, plugin_name: str): if plugin.name in self.bot.cogs: # another class with the same name embed = discord.Embed( - description="Cannot install this plugin (dupe cog name).", - color=self.bot.error_color, + description="Cannot install this plugin (dupe cog name).", color=self.bot.error_color, ) return await ctx.send(embed=embed) if plugin.local: embed = discord.Embed( - description=f"Starting to load local plugin from {plugin.link}...", - color=self.bot.main_color, + description=f"Starting to load local plugin from {plugin.link}...", color=self.bot.main_color, ) else: embed = discord.Embed( - description=f"Starting to download plugin from {plugin.link}...", - color=self.bot.main_color, + description=f"Starting to download plugin from {plugin.link}...", color=self.bot.main_color, ) msg = await ctx.send(embed=embed) @@ -562,8 +556,7 @@ async def plugins_loaded(self, ctx): if not self._ready_event.is_set(): embed = discord.Embed( - description="Plugins are still loading, please try again later.", - color=self.bot.main_color, + description="Plugins are still loading, please try again later.", color=self.bot.main_color, ) return await ctx.send(embed=embed) diff --git a/cogs/utility.py b/cogs/utility.py index 759b573748..6f6ed89b47 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -305,9 +305,7 @@ async def about(self, ctx): """Shows information about this bot.""" embed = discord.Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) embed.set_author( - name="Modmail - About", - icon_url=self.bot.user.avatar_url, - url="https://discord.gg/F34cRU8", + name="Modmail - About", icon_url=self.bot.user.avatar_url, url="https://discord.gg/F34cRU8", ) embed.set_thumbnail(url=self.bot.user.avatar_url) @@ -388,8 +386,7 @@ async def debug(self, ctx): log_file_name = self.bot.token.split(".")[0] with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), - "r+", + os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), "r+", ) as f: logs = f.read().strip() @@ -441,8 +438,7 @@ async def debug_hastebin(self, ctx): log_file_name = self.bot.token.split(".")[0] with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), - "rb+", + os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), "rb+", ) as f: logs = BytesIO(f.read().strip()) @@ -455,9 +451,7 @@ async def debug_hastebin(self, ctx): logger.error(data["message"]) raise embed = discord.Embed( - title="Debug Logs", - color=self.bot.main_color, - description=f"{haste_url}/" + key, + title="Debug Logs", color=self.bot.main_color, description=f"{haste_url}/" + key, ) except (JSONDecodeError, ClientResponseError, IndexError, KeyError): embed = discord.Embed( @@ -477,8 +471,7 @@ async def debug_clear(self, ctx): log_file_name = self.bot.token.split(".")[0] with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), - "w", + os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), "w", ): pass await ctx.send( @@ -693,14 +686,12 @@ async def mention(self, ctx, *user_or_role: Union[discord.Role, discord.Member, option = user_or_role[0].lower() if option == "disable": embed = discord.Embed( - description=f"Disabled mention on thread creation.", - color=self.bot.main_color, + description=f"Disabled mention on thread creation.", color=self.bot.main_color, ) self.bot.config["mention"] = None else: embed = discord.Embed( - description="`mention` is reset to default.", - color=self.bot.main_color, + description="`mention` is reset to default.", color=self.bot.main_color, ) self.bot.config.remove("mention") await self.bot.config.update() @@ -775,9 +766,7 @@ async def config_options(self, ctx): for names in zip_longest(*(iter(sorted(self.bot.config.public_keys)),) * 15): description = "\n".join(f"`{name}`" for name in takewhile(lambda x: x is not None, names)) embed = discord.Embed( - title="Available configuration keys:", - color=self.bot.main_color, - description=description, + title="Available configuration keys:", color=self.bot.main_color, description=description, ) embeds.append(embed) @@ -820,9 +809,7 @@ async def config_remove(self, ctx, *, key: str.lower): self.bot.config.remove(key) await self.bot.config.update() embed = discord.Embed( - title="Success", - color=self.bot.main_color, - description=f"`{key}` had been reset to default.", + title="Success", color=self.bot.main_color, description=f"`{key}` had been reset to default.", ) else: embed = discord.Embed( @@ -851,9 +838,7 @@ async def config_get(self, ctx, *, key: str.lower = None): 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.", ) embed.set_footer( text=f'Type "{self.bot.prefix}config options" for a list of config variables.' @@ -886,9 +871,7 @@ async def config_help(self, ctx, key: str.lower = None): 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.", + 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)) @@ -898,9 +881,7 @@ async def config_help(self, ctx, key: str.lower = None): if key is not None and key not in config_help: embed = discord.Embed( - title="Error", - color=self.bot.error_color, - description=f"No help details found for `{key}`.", + title="Error", color=self.bot.error_color, description=f"No help details found for `{key}`.", ) return await ctx.send(embed=embed) @@ -992,9 +973,7 @@ async def alias(self, ctx, *, name: str.lower = None): 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, + color=self.bot.main_color, title=f'Alias - "{name}" - Step {i}:', description=val, ) embeds += [embed] session = EmbedPaginatorSession(ctx, *embeds) @@ -1269,9 +1248,7 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name ) else: logger.info( - "Updated command permission level for `%s` to `%s`.", - command.qualified_name, - level.name, + "Updated command permission level for `%s` to `%s`.", command.qualified_name, level.name, ) self.bot.config["override_command_level"][command.qualified_name] = level.name @@ -1287,12 +1264,7 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name @permissions.command(name="add", usage="[command/level] [name] [user/role]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add( - self, - ctx, - type_: str.lower, - name: str, - *, - user_or_role: Union[discord.Role, utils.User, str], + self, ctx, type_: str.lower, name: str, *, user_or_role: Union[discord.Role, utils.User, str], ): """ Add a permission to a command or a permission level. @@ -1361,12 +1333,7 @@ async def permissions_add( ) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove( - self, - ctx, - type_: str.lower, - name: str, - *, - user_or_role: Union[discord.Role, utils.User, str] = None, + self, ctx, type_: str.lower, name: str, *, user_or_role: Union[discord.Role, utils.User, str] = None, ): """ Remove permission to use a command, permission level, or command level override. @@ -1860,11 +1827,7 @@ async def autotrigger_list(self, ctx): embeds = [] for keyword in self.bot.auto_triggers: command = self.bot.auto_triggers[keyword] - embed = discord.Embed( - title=keyword, - color=self.bot.main_color, - description=command, - ) + embed = discord.Embed(title=keyword, color=self.bot.main_color, description=command,) embeds.append(embed) if not embeds: @@ -1965,11 +1928,7 @@ async def update(self, ctx, *, flag: str = ""): command = "git pull" - proc = await asyncio.create_subprocess_shell( - command, - stderr=PIPE, - stdout=PIPE, - ) + proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) err = await proc.stderr.read() err = err.decode("utf-8").rstrip() res = await proc.stdout.read() @@ -1982,10 +1941,7 @@ async def update(self, ctx, *, flag: str = ""): elif res != "Already up to date.": logger.info("Bot has been updated.") - embed = discord.Embed( - title="Bot has been updated", - color=self.bot.main_color, - ) + embed = discord.Embed(title="Bot has been updated", color=self.bot.main_color,) embed.set_footer(text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}") embed.description = latest.description for name, value in latest.fields.items(): @@ -2000,9 +1956,7 @@ async def update(self, ctx, *, flag: str = ""): return await self.bot.close() else: embed = discord.Embed( - title="Already up to date", - description=desc, - color=self.bot.main_color, + title="Already up to date", description=desc, color=self.bot.main_color, ) embed.set_footer(text="Force update") await ctx.send(embed=embed) diff --git a/core/changelog.py b/core/changelog.py index 7c9af2e1bb..5c23020eb6 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -89,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(): @@ -170,11 +168,7 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": The newly created `Changelog` parsed from the `url`. """ # get branch via git cli if available - proc = await asyncio.create_subprocess_shell( - "git branch --show-current", - stderr=PIPE, - stdout=PIPE, - ) + proc = await asyncio.create_subprocess_shell("git branch --show-current", stderr=PIPE, stdout=PIPE,) err = await proc.stderr.read() err = err.decode("utf-8").rstrip() res = await proc.stdout.read() diff --git a/core/checks.py b/core/checks.py index 1079b55530..74b7dc38cd 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 5c5acc8fad..61fcfdb487 100644 --- a/core/clients.py +++ b/core/clients.py @@ -321,12 +321,7 @@ async def edit_message(self, message_id: Union[int, str], new_content: str) -> N return NotImplemented async def append_log( - self, - message: Message, - *, - message_id: str = "", - channel_id: str = "", - type_: str = "thread_message", + self, message: Message, *, message_id: str = "", channel_id: str = "", type_: str = "thread_message", ) -> dict: return NotImplemented @@ -548,12 +543,7 @@ async def edit_message(self, message_id: Union[int, str], new_content: str) -> N ) async def append_log( - self, - message: Message, - *, - message_id: str = "", - channel_id: str = "", - 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) @@ -599,11 +589,7 @@ async def search_closed_by(self, user_id: Union[int, str]): async def search_by_text(self, text: str, limit: Optional[int]): return await self.bot.db.logs.find( - { - "guild_id": str(self.bot.guild_id), - "open": False, - "$text": {"$search": f'"{text}"'}, - }, + {"guild_id": str(self.bot.guild_id), "open": False, "$text": {"$search": f'"{text}"'},}, {"messages": {"$slice": 5}}, ).to_list(limit) @@ -644,11 +630,7 @@ async def update_repository(self) -> dict: data = await user.update_repository() return { "data": data, - "user": { - "username": user.username, - "avatar_url": user.avatar_url, - "url": user.url, - }, + "user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,}, } async def get_user_info(self) -> dict: @@ -657,13 +639,7 @@ async def get_user_info(self) -> dict: except InvalidConfigError: return None else: - return { - "user": { - "username": user.username, - "avatar_url": user.avatar_url, - "url": user.url, - } - } + return {"user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,}} class PluginDatabaseClient: diff --git a/core/models.py b/core/models.py index 76889809f7..19c8ccf1fc 100644 --- a/core/models.py +++ b/core/models.py @@ -87,9 +87,7 @@ def line(self, level="info"): level = logging.INFO if self.isEnabledFor(level): self._log( - level, - Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL, - [], + level, Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL, [], ) @@ -130,8 +128,7 @@ def configure_logging(name, level=None): ch_debug = RotatingFileHandler(name, mode="a+", maxBytes=78000, backupCount=10) formatter_debug = FileFormatter( - "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", + "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) ch_debug.setFormatter(formatter_debug) ch_debug.setLevel(logging.DEBUG) diff --git a/core/thread.py b/core/thread.py index 41d2cd96ba..7c49ee7425 100644 --- a/core/thread.py +++ b/core/thread.py @@ -181,9 +181,7 @@ async def send_recipient_genesis_message(): 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, + color=self.bot.mod_color, description=thread_creation_response, timestamp=channel.created_at, ) recipient_thread_close = self.bot.config.get("recipient_thread_close") @@ -460,10 +458,7 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, # Thread closed message - embed = discord.Embed( - title=self.bot.config["thread_close_title"], - color=self.bot.error_color, - ) + embed = discord.Embed(title=self.bot.config["thread_close_title"], color=self.bot.error_color,) if self.bot.config["show_timestamp"]: embed.timestamp = datetime.utcnow() @@ -695,11 +690,7 @@ async def note(self, message: discord.Message, persistent=False, thread_creation raise MissingRequiredArgument(SimpleNamespace(name="msg")) msg = await self.send( - message, - self.channel, - note=True, - persistent_note=persistent, - thread_creation=thread_creation, + message, self.channel, note=True, persistent_note=persistent, thread_creation=thread_creation, ) self.bot.loop.create_task( @@ -724,11 +715,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False, plain: try: user_msg = await self.send( - message, - destination=self.recipient, - from_mod=True, - anonymous=anonymous, - plain=plain, + message, destination=self.recipient, from_mod=True, anonymous=anonymous, plain=plain, ) except Exception as e: logger.error("Message delivery failed:", exc_info=True) @@ -747,10 +734,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False, plain: ) tasks.append( message.channel.send( - embed=discord.Embed( - color=self.bot.error_color, - description=description, - ) + embed=discord.Embed(color=self.bot.error_color, description=description,) ) ) else: @@ -774,8 +758,7 @@ async def reply(self, message: discord.Message, anonymous: bool = False, plain: tasks.append( self.channel.send( 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.", ) ) ) @@ -806,8 +789,7 @@ async def send( self.bot.loop.create_task( self.channel.send( 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.", ) ) ) From 8db612b1b983e895860835233af1b46fbca8aea2 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 22 Jul 2021 17:08:17 +0800 Subject: [PATCH 0905/1221] formatting --- Pipfile | 2 +- bot.py | 49 +++++++++++++++++++++------ cogs/modmail.py | 84 ++++++++++++++++++++++++++++++++------------- cogs/plugins.py | 19 +++++++---- cogs/utility.py | 86 ++++++++++++++++++++++++++++++++++++----------- core/changelog.py | 10 ++++-- core/checks.py | 4 ++- core/clients.py | 34 ++++++++++++++++--- core/models.py | 7 ++-- core/thread.py | 32 ++++++++++++++---- 10 files changed, 249 insertions(+), 78 deletions(-) diff --git a/Pipfile b/Pipfile index 9c7b4e03b7..4ed01f5b70 100644 --- a/Pipfile +++ b/Pipfile @@ -17,7 +17,7 @@ isodate = "~=0.6.0" motor = "~=2.4.0" natural = "~=0.2.0" parsedatetime = "~=2.6" -pymongo = {version = "*", extras = ['srv']} # Required by motor +pymongo = {extras = ["srv"], version = "*"} # Required by motor python-dateutil = "~=2.8.1" python-dotenv = "~=0.18.0" uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} diff --git a/bot.py b/bot.py index 131032ec80..1deb9b7d2a 100644 --- a/bot.py +++ b/bot.py @@ -506,7 +506,8 @@ def command_perm(self, command_name: str) -> PermissionLevel: logger.debug("Command %s not found.", command_name) return PermissionLevel.INVALID level = next( - (check.permission_level for check in command.checks if hasattr(check, "permission_level")), None, + (check.permission_level for check in command.checks if hasattr(check, "permission_level")), + None, ) if level is None: logger.debug("Command %s does not have a permission level.", command_name) @@ -609,7 +610,13 @@ async def on_ready(self): if self.config.get("data_collection"): self.metadata_loop = tasks.Loop( - self.post_metadata, seconds=0, minutes=0, hours=1, count=None, reconnect=True, loop=None, + self.post_metadata, + seconds=0, + minutes=0, + hours=1, + count=None, + reconnect=True, + loop=None, ) self.metadata_loop.before_loop(self.before_post_metadata) self.metadata_loop.start() @@ -762,7 +769,8 @@ def check_manual_blocked(self, author: discord.Member) -> bool: 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, + r"Deprecated time message for user %s, block and unblock again to update.", + author.name, ) if end_time is not None: @@ -783,7 +791,11 @@ async def _process_blocked(self, message): return False async def is_blocked( - self, author: discord.User, *, channel: discord.TextChannel = None, send_message: bool = False, + self, + author: discord.User, + *, + channel: discord.TextChannel = None, + send_message: bool = False, ) -> typing.Tuple[bool, str]: member = self.guild.get_member(author.id) @@ -814,7 +826,9 @@ async def is_blocked( if send_message: await channel.send( embed=discord.Embed( - title="Message not sent!", description=new_reason, color=self.error_color, + title="Message not sent!", + description=new_reason, + color=self.error_color, ) ) return True @@ -913,7 +927,8 @@ async def process_dm_modmail(self, message: discord.Message) -> None: description=self.config["disabled_current_thread_response"], ) embed.set_footer( - text=self.config["disabled_current_thread_footer"], icon_url=self.guild.icon_url, + text=self.config["disabled_current_thread_footer"], + icon_url=self.guild.icon_url, ) logger.info("A message was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) @@ -1274,14 +1289,18 @@ async def on_raw_reaction_add(self, payload): await message.remove_reaction(payload.emoji, member) await message.add_reaction(emoji_fmt) # bot adds as well - if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS,): + if self.config["dm_disabled"] in ( + DMDisabled.NEW_THREADS, + DMDisabled.ALL_THREADS, + ): embed = discord.Embed( title=self.config["disabled_new_thread_title"], color=self.error_color, description=self.config["disabled_new_thread_response"], ) embed.set_footer( - text=self.config["disabled_new_thread_footer"], icon_url=self.guild.icon_url, + text=self.config["disabled_new_thread_footer"], + icon_url=self.guild.icon_url, ) logger.info( "A new thread using react to contact was blocked from %s due to disabled Modmail.", @@ -1340,7 +1359,9 @@ async def on_member_remove(self, member): if thread: if self.config["close_on_leave"]: await thread.close( - closer=member.guild.me, message=self.config["close_on_leave_reason"], silent=True, + closer=member.guild.me, + message=self.config["close_on_leave_reason"], + silent=True, ) else: embed = discord.Embed( @@ -1523,7 +1544,9 @@ async def autoupdate(self): commit_data = data["data"] user = data["user"] embed.set_author( - name=user["username"] + " - Updating Bot", icon_url=user["avatar_url"], url=user["url"], + name=user["username"] + " - Updating Bot", + icon_url=user["avatar_url"], + url=user["url"], ) embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") @@ -1552,7 +1575,11 @@ async def autoupdate(self): pass command = "git pull" - proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) + proc = await asyncio.create_subprocess_shell( + command, + stderr=PIPE, + stdout=PIPE, + ) err = await proc.stderr.read() err = err.decode("utf-8").rstrip() res = await proc.stdout.read() diff --git a/cogs/modmail.py b/cogs/modmail.py index 0db8a85e2e..0e41b76677 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -50,7 +50,9 @@ async def setup(self, ctx): if self.bot.modmail_guild is None: embed = discord.Embed( - title="Error", description="Modmail functioning guild not found.", color=self.bot.error_color, + title="Error", + description="Modmail functioning guild not found.", + color=self.bot.error_color, ) return await ctx.send(embed=embed) @@ -182,7 +184,9 @@ async def snippet_raw(self, ctx, *, name: str.lower): 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, + title=f'Raw snippet - "{name}":', + description=f"```\n{val}```", + color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -204,7 +208,9 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte """ if name in self.bot.snippets: embed = discord.Embed( - title="Error", color=self.bot.error_color, description=f"Snippet `{name}` already exists.", + title="Error", + color=self.bot.error_color, + description=f"Snippet `{name}` already exists.", ) return await ctx.send(embed=embed) @@ -228,7 +234,9 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte await self.bot.config.update() embed = discord.Embed( - title="Added snippet", color=self.bot.main_color, description="Successfully created snippet.", + title="Added snippet", + color=self.bot.main_color, + description="Successfully created snippet.", ) return await ctx.send(embed=embed) @@ -445,7 +453,8 @@ async def notify(self, ctx, *, user_or_role: Union[discord.Role, User, str.lower if mention in mentions: embed = discord.Embed( - color=self.bot.error_color, description=f"{mention} is already going to be mentioned.", + color=self.bot.error_color, + description=f"{mention} is already going to be mentioned.", ) else: mentions.append(mention) @@ -480,7 +489,8 @@ async def unnotify(self, ctx, *, user_or_role: Union[discord.Role, User, str.low if mention not in mentions: embed = discord.Embed( - color=self.bot.error_color, description=f"{mention} does not have a pending notification.", + color=self.bot.error_color, + description=f"{mention} does not have a pending notification.", ) else: mentions.remove(mention) @@ -516,7 +526,8 @@ async def subscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str.lo if mention in mentions: embed = discord.Embed( - color=self.bot.error_color, description=f"{mention} is already subscribed to this thread.", + color=self.bot.error_color, + description=f"{mention} is already subscribed to this thread.", ) else: mentions.append(mention) @@ -551,13 +562,15 @@ async def unsubscribe(self, ctx, *, user_or_role: Union[discord.Role, User, str. if mention not in mentions: embed = discord.Embed( - color=self.bot.error_color, description=f"{mention} is not subscribed to this thread.", + color=self.bot.error_color, + description=f"{mention} is not 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 from this thread.", + color=self.bot.main_color, + description=f"{mention} is now unsubscribed from this thread.", ) return await ctx.send(embed=embed) @@ -684,7 +697,8 @@ 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.", + color=self.bot.error_color, + description="This user does not have any previous logs.", ) return await ctx.send(embed=embed) @@ -711,7 +725,8 @@ 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.", + color=self.bot.error_color, + description="No log entries have been found for that query.", ) return await ctx.send(embed=embed) @@ -730,7 +745,9 @@ async def logs_delete(self, ctx, key_or_link: str): if not success: embed = discord.Embed( - title="Error", description=f"Log entry `{key}` not found.", color=self.bot.error_color, + title="Error", + description=f"Log entry `{key}` not found.", + color=self.bot.error_color, ) else: embed = discord.Embed( @@ -783,7 +800,8 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): if not embeds: embed = discord.Embed( - color=self.bot.error_color, description="No log entries have been found for that query.", + color=self.bot.error_color, + description="No log entries have been found for that query.", ) return await ctx.send(embed=embed) @@ -994,7 +1012,10 @@ async def contact( else: thread = await self.bot.threads.create( - recipient=user, creator=ctx.author, category=category, manual_trigger=manual_trigger, + recipient=user, + creator=ctx.author, + category=category, + manual_trigger=manual_trigger, ) if thread.cancelled: return @@ -1008,7 +1029,11 @@ async def contact( else: description = f"{ctx.author.name} has opened a Modmail thread." - em = discord.Embed(title="New Thread", description=description, color=self.bot.main_color,) + em = discord.Embed( + title="New Thread", + description=description, + color=self.bot.main_color, + ) if self.bot.config["show_timestamp"]: em.timestamp = datetime.utcnow() em.set_footer(icon_url=ctx.author.avatar_url) @@ -1050,7 +1075,8 @@ async def blocked(self, ctx): 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.", id_, + r"Deprecated time message for user %s, block and unblock again to update.", + id_, ) if end_time is not None: @@ -1081,7 +1107,8 @@ async def blocked(self, ctx): end_time = re.search(r"%([^%]+?)%", reason) if end_time is not None: logger.warning( - r"Deprecated time message for role %s, block and unblock again to update.", id_, + r"Deprecated time message for role %s, block and unblock again to update.", + id_, ) if end_time is not None: @@ -1103,7 +1130,9 @@ async def blocked(self, ctx): line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title="Blocked Users (Continued)", color=self.bot.main_color, description=line, + title="Blocked Users (Continued)", + color=self.bot.main_color, + description=line, ) embeds.append(embed) else: @@ -1120,7 +1149,9 @@ async def blocked(self, ctx): line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title="Blocked Roles (Continued)", color=self.bot.main_color, description=line, + title="Blocked Roles (Continued)", + color=self.bot.main_color, + description=line, ) embeds.append(embed) else: @@ -1180,7 +1211,9 @@ async def blocked_whitelist(self, ctx, *, user: User = None): ) else: embed = discord.Embed( - title="Success", color=self.bot.main_color, description=f"{mention} is now whitelisted.", + title="Success", + color=self.bot.main_color, + description=f"{mention} is now whitelisted.", ) return await ctx.send(embed=embed) @@ -1258,7 +1291,9 @@ async def block( ) else: embed = discord.Embed( - title="Success", color=self.bot.main_color, description=f"{mention} is now blocked {reason}", + title="Success", + color=self.bot.main_color, + description=f"{mention} is now blocked {reason}", ) if isinstance(user_or_role, discord.Role): @@ -1321,7 +1356,9 @@ async def unblock(self, ctx, *, user_or_role: Union[User, Role] = None): await self.bot.config.update() embed = discord.Embed( - title="Success", color=self.bot.main_color, description=f"{mention} is no longer blocked.", + title="Success", + color=self.bot.main_color, + description=f"{mention} is no longer blocked.", ) else: embed = discord.Embed( @@ -1378,7 +1415,8 @@ async def repair(self, ctx): # 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), + ((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.") diff --git a/cogs/plugins.py b/cogs/plugins.py index a9a4d247da..aab5dc8bd6 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -159,7 +159,9 @@ async def initial_load_plugins(self): except Exception: self.bot.config["plugins"].remove(plugin_name) logger.error( - "Error when loading plugin %s. Plugin removed from config.", plugin, exc_info=True, + "Error when loading plugin %s. Plugin removed from config.", + plugin, + exc_info=True, ) continue @@ -276,7 +278,8 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if not self._ready_event.is_set(): embed = discord.Embed( - description="Plugins are still loading, please try again later.", color=self.bot.main_color, + description="Plugins are still loading, please try again later.", + color=self.bot.main_color, ) await ctx.send(embed=embed) return @@ -346,17 +349,20 @@ async def plugins_add(self, ctx, *, plugin_name: str): if plugin.name in self.bot.cogs: # another class with the same name embed = discord.Embed( - description="Cannot install this plugin (dupe cog name).", color=self.bot.error_color, + description="Cannot install this plugin (dupe cog name).", + color=self.bot.error_color, ) return await ctx.send(embed=embed) if plugin.local: embed = discord.Embed( - description=f"Starting to load local plugin from {plugin.link}...", color=self.bot.main_color, + description=f"Starting to load local plugin from {plugin.link}...", + color=self.bot.main_color, ) else: embed = discord.Embed( - description=f"Starting to download plugin from {plugin.link}...", color=self.bot.main_color, + description=f"Starting to download plugin from {plugin.link}...", + color=self.bot.main_color, ) msg = await ctx.send(embed=embed) @@ -556,7 +562,8 @@ async def plugins_loaded(self, ctx): if not self._ready_event.is_set(): embed = discord.Embed( - description="Plugins are still loading, please try again later.", color=self.bot.main_color, + description="Plugins are still loading, please try again later.", + color=self.bot.main_color, ) return await ctx.send(embed=embed) diff --git a/cogs/utility.py b/cogs/utility.py index 6f6ed89b47..759b573748 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -305,7 +305,9 @@ async def about(self, ctx): """Shows information about this bot.""" embed = discord.Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) embed.set_author( - name="Modmail - About", icon_url=self.bot.user.avatar_url, url="https://discord.gg/F34cRU8", + name="Modmail - About", + icon_url=self.bot.user.avatar_url, + url="https://discord.gg/F34cRU8", ) embed.set_thumbnail(url=self.bot.user.avatar_url) @@ -386,7 +388,8 @@ async def debug(self, ctx): log_file_name = self.bot.token.split(".")[0] with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), "r+", + os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), + "r+", ) as f: logs = f.read().strip() @@ -438,7 +441,8 @@ async def debug_hastebin(self, ctx): log_file_name = self.bot.token.split(".")[0] with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), "rb+", + os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), + "rb+", ) as f: logs = BytesIO(f.read().strip()) @@ -451,7 +455,9 @@ async def debug_hastebin(self, ctx): logger.error(data["message"]) raise embed = discord.Embed( - title="Debug Logs", color=self.bot.main_color, description=f"{haste_url}/" + key, + title="Debug Logs", + color=self.bot.main_color, + description=f"{haste_url}/" + key, ) except (JSONDecodeError, ClientResponseError, IndexError, KeyError): embed = discord.Embed( @@ -471,7 +477,8 @@ async def debug_clear(self, ctx): log_file_name = self.bot.token.split(".")[0] with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), "w", + os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), + "w", ): pass await ctx.send( @@ -686,12 +693,14 @@ async def mention(self, ctx, *user_or_role: Union[discord.Role, discord.Member, option = user_or_role[0].lower() if option == "disable": embed = discord.Embed( - description=f"Disabled mention on thread creation.", color=self.bot.main_color, + description=f"Disabled mention on thread creation.", + color=self.bot.main_color, ) self.bot.config["mention"] = None else: embed = discord.Embed( - description="`mention` is reset to default.", color=self.bot.main_color, + description="`mention` is reset to default.", + color=self.bot.main_color, ) self.bot.config.remove("mention") await self.bot.config.update() @@ -766,7 +775,9 @@ async def config_options(self, ctx): for names in zip_longest(*(iter(sorted(self.bot.config.public_keys)),) * 15): description = "\n".join(f"`{name}`" for name in takewhile(lambda x: x is not None, names)) embed = discord.Embed( - title="Available configuration keys:", color=self.bot.main_color, description=description, + title="Available configuration keys:", + color=self.bot.main_color, + description=description, ) embeds.append(embed) @@ -809,7 +820,9 @@ async def config_remove(self, ctx, *, key: str.lower): self.bot.config.remove(key) await self.bot.config.update() embed = discord.Embed( - title="Success", color=self.bot.main_color, description=f"`{key}` had been reset to default.", + title="Success", + color=self.bot.main_color, + description=f"`{key}` had been reset to default.", ) else: embed = discord.Embed( @@ -838,7 +851,9 @@ async def config_get(self, ctx, *, key: str.lower = None): 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.", ) embed.set_footer( text=f'Type "{self.bot.prefix}config options" for a list of config variables.' @@ -871,7 +886,9 @@ async def config_help(self, ctx, key: str.lower = None): 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.", + 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)) @@ -881,7 +898,9 @@ async def config_help(self, ctx, key: str.lower = None): if key is not None and key not in config_help: embed = discord.Embed( - title="Error", color=self.bot.error_color, description=f"No help details found for `{key}`.", + title="Error", + color=self.bot.error_color, + description=f"No help details found for `{key}`.", ) return await ctx.send(embed=embed) @@ -973,7 +992,9 @@ async def alias(self, ctx, *, name: str.lower = None): 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, + color=self.bot.main_color, + title=f'Alias - "{name}" - Step {i}:', + description=val, ) embeds += [embed] session = EmbedPaginatorSession(ctx, *embeds) @@ -1248,7 +1269,9 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name ) else: logger.info( - "Updated command permission level for `%s` to `%s`.", command.qualified_name, level.name, + "Updated command permission level for `%s` to `%s`.", + command.qualified_name, + level.name, ) self.bot.config["override_command_level"][command.qualified_name] = level.name @@ -1264,7 +1287,12 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name @permissions.command(name="add", usage="[command/level] [name] [user/role]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add( - self, ctx, type_: str.lower, name: str, *, user_or_role: Union[discord.Role, utils.User, str], + self, + ctx, + type_: str.lower, + name: str, + *, + user_or_role: Union[discord.Role, utils.User, str], ): """ Add a permission to a command or a permission level. @@ -1333,7 +1361,12 @@ async def permissions_add( ) @checks.has_permissions(PermissionLevel.OWNER) async def permissions_remove( - self, ctx, type_: str.lower, name: str, *, user_or_role: Union[discord.Role, utils.User, str] = None, + self, + ctx, + type_: str.lower, + name: str, + *, + user_or_role: Union[discord.Role, utils.User, str] = None, ): """ Remove permission to use a command, permission level, or command level override. @@ -1827,7 +1860,11 @@ async def autotrigger_list(self, ctx): embeds = [] for keyword in self.bot.auto_triggers: command = self.bot.auto_triggers[keyword] - embed = discord.Embed(title=keyword, color=self.bot.main_color, description=command,) + embed = discord.Embed( + title=keyword, + color=self.bot.main_color, + description=command, + ) embeds.append(embed) if not embeds: @@ -1928,7 +1965,11 @@ async def update(self, ctx, *, flag: str = ""): command = "git pull" - proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) + proc = await asyncio.create_subprocess_shell( + command, + stderr=PIPE, + stdout=PIPE, + ) err = await proc.stderr.read() err = err.decode("utf-8").rstrip() res = await proc.stdout.read() @@ -1941,7 +1982,10 @@ async def update(self, ctx, *, flag: str = ""): elif res != "Already up to date.": logger.info("Bot has been updated.") - embed = discord.Embed(title="Bot has been updated", color=self.bot.main_color,) + embed = discord.Embed( + title="Bot has been updated", + color=self.bot.main_color, + ) embed.set_footer(text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}") embed.description = latest.description for name, value in latest.fields.items(): @@ -1956,7 +2000,9 @@ async def update(self, ctx, *, flag: str = ""): return await self.bot.close() else: embed = discord.Embed( - title="Already up to date", description=desc, color=self.bot.main_color, + title="Already up to date", + description=desc, + color=self.bot.main_color, ) embed.set_footer(text="Force update") await ctx.send(embed=embed) diff --git a/core/changelog.py b/core/changelog.py index 5c23020eb6..7c9af2e1bb 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -89,7 +89,9 @@ 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(): @@ -168,7 +170,11 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": The newly created `Changelog` parsed from the `url`. """ # get branch via git cli if available - proc = await asyncio.create_subprocess_shell("git branch --show-current", stderr=PIPE, stdout=PIPE,) + proc = await asyncio.create_subprocess_shell( + "git branch --show-current", + stderr=PIPE, + stdout=PIPE, + ) err = await proc.stderr.read() err = err.decode("utf-8").rstrip() res = await proc.stdout.read() diff --git a/core/checks.py b/core/checks.py index 74b7dc38cd..1079b55530 100644 --- a/core/checks.py +++ b/core/checks.py @@ -5,7 +5,9 @@ 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 61fcfdb487..5c5acc8fad 100644 --- a/core/clients.py +++ b/core/clients.py @@ -321,7 +321,12 @@ async def edit_message(self, message_id: Union[int, str], new_content: str) -> N return NotImplemented async def append_log( - self, message: Message, *, message_id: str = "", channel_id: str = "", type_: str = "thread_message", + self, + message: Message, + *, + message_id: str = "", + channel_id: str = "", + type_: str = "thread_message", ) -> dict: return NotImplemented @@ -543,7 +548,12 @@ async def edit_message(self, message_id: Union[int, str], new_content: str) -> N ) async def append_log( - self, message: Message, *, message_id: str = "", channel_id: str = "", 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) @@ -589,7 +599,11 @@ async def search_closed_by(self, user_id: Union[int, str]): async def search_by_text(self, text: str, limit: Optional[int]): return await self.bot.db.logs.find( - {"guild_id": str(self.bot.guild_id), "open": False, "$text": {"$search": f'"{text}"'},}, + { + "guild_id": str(self.bot.guild_id), + "open": False, + "$text": {"$search": f'"{text}"'}, + }, {"messages": {"$slice": 5}}, ).to_list(limit) @@ -630,7 +644,11 @@ async def update_repository(self) -> dict: data = await user.update_repository() return { "data": data, - "user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,}, + "user": { + "username": user.username, + "avatar_url": user.avatar_url, + "url": user.url, + }, } async def get_user_info(self) -> dict: @@ -639,7 +657,13 @@ async def get_user_info(self) -> dict: except InvalidConfigError: return None else: - return {"user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,}} + return { + "user": { + "username": user.username, + "avatar_url": user.avatar_url, + "url": user.url, + } + } class PluginDatabaseClient: diff --git a/core/models.py b/core/models.py index 19c8ccf1fc..76889809f7 100644 --- a/core/models.py +++ b/core/models.py @@ -87,7 +87,9 @@ def line(self, level="info"): level = logging.INFO if self.isEnabledFor(level): self._log( - level, Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL, [], + level, + Fore.BLACK + Style.BRIGHT + "-------------------------" + Style.RESET_ALL, + [], ) @@ -128,7 +130,8 @@ def configure_logging(name, level=None): ch_debug = RotatingFileHandler(name, mode="a+", maxBytes=78000, backupCount=10) formatter_debug = FileFormatter( - "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", + "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", ) ch_debug.setFormatter(formatter_debug) ch_debug.setLevel(logging.DEBUG) diff --git a/core/thread.py b/core/thread.py index 7c49ee7425..41d2cd96ba 100644 --- a/core/thread.py +++ b/core/thread.py @@ -181,7 +181,9 @@ async def send_recipient_genesis_message(): 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, + color=self.bot.mod_color, + description=thread_creation_response, + timestamp=channel.created_at, ) recipient_thread_close = self.bot.config.get("recipient_thread_close") @@ -458,7 +460,10 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, # Thread closed message - embed = discord.Embed(title=self.bot.config["thread_close_title"], color=self.bot.error_color,) + embed = discord.Embed( + title=self.bot.config["thread_close_title"], + color=self.bot.error_color, + ) if self.bot.config["show_timestamp"]: embed.timestamp = datetime.utcnow() @@ -690,7 +695,11 @@ async def note(self, message: discord.Message, persistent=False, thread_creation raise MissingRequiredArgument(SimpleNamespace(name="msg")) msg = await self.send( - message, self.channel, note=True, persistent_note=persistent, thread_creation=thread_creation, + message, + self.channel, + note=True, + persistent_note=persistent, + thread_creation=thread_creation, ) self.bot.loop.create_task( @@ -715,7 +724,11 @@ async def reply(self, message: discord.Message, anonymous: bool = False, plain: try: user_msg = await self.send( - message, destination=self.recipient, from_mod=True, anonymous=anonymous, plain=plain, + message, + destination=self.recipient, + from_mod=True, + anonymous=anonymous, + plain=plain, ) except Exception as e: logger.error("Message delivery failed:", exc_info=True) @@ -734,7 +747,10 @@ async def reply(self, message: discord.Message, anonymous: bool = False, plain: ) tasks.append( message.channel.send( - embed=discord.Embed(color=self.bot.error_color, description=description,) + embed=discord.Embed( + color=self.bot.error_color, + description=description, + ) ) ) else: @@ -758,7 +774,8 @@ async def reply(self, message: discord.Message, anonymous: bool = False, plain: tasks.append( self.channel.send( 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.", ) ) ) @@ -789,7 +806,8 @@ async def send( self.bot.loop.create_task( self.channel.send( 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.", ) ) ) From cf65018c5bea0228a8ce38dd112308e7ea6e9958 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 22 Jul 2021 22:45:01 +0800 Subject: [PATCH 0906/1221] Fix SSL Error --- core/clients.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/clients.py b/core/clients.py index 0a527b59ce..667faf71bd 100644 --- a/core/clients.py +++ b/core/clients.py @@ -417,10 +417,14 @@ async def validate_database_connection(self, *, ssl_retry=True): if mongo_uri is None: mongo_uri = self.bot.config["mongo_uri"] for _ in range(3): - logger.warning("FAILED TO VERIFY SSL CERTIFICATE, ATTEMPTING TO START WITHOUT SSL (UNSAFE).") - logger.warning("To fix this warning, check there's no proxies blocking SSL cert verification, " - "run \"Certificate.command\" on MacOS, " - "and check certifi is up to date \"pip3 install --upgrade certifi\".") + logger.warning( + "FAILED TO VERIFY SSL CERTIFICATE, ATTEMPTING TO START WITHOUT SSL (UNSAFE)." + ) + logger.warning( + "To fix this warning, check there's no proxies blocking SSL cert verification, " + 'run "Certificate.command" on MacOS, ' + 'and check certifi is up to date "pip3 install --upgrade certifi".' + ) self.db = AsyncIOMotorClient(mongo_uri, tlsAllowInvalidCertificates=True).modmail_bot return await self.validate_database_connection(ssl_retry=False) if "ServerSelectionTimeoutError" in message: From 22fd835a76f48608f59146ac35776e2aace8b70b Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 22 Jul 2021 22:46:26 +0800 Subject: [PATCH 0907/1221] Changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb9ee01487..67bdc5a892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.9.5dev1 +# v3.9.5 -## Misc +## Internal - Bumped discord.py to v1.7.3, updated all other packages to latest. - More debug log files are now kept. +- Resolve SSL errors by retrying without SSL # v3.9.4 From 52c0fb3d5107c361a7317f1b8b5174c6cdc40202 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 23 Jul 2021 00:03:27 +0800 Subject: [PATCH 0908/1221] bump v --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 1deb9b7d2a..1c4fdd5354 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.9.5dev1" +__version__ = "3.9.5" import asyncio From c7514b1899c60d4456ab339199d15883f0f49c69 Mon Sep 17 00:00:00 2001 From: Matt Nikkel Date: Sun, 25 Jul 2021 23:00:55 -0400 Subject: [PATCH 0909/1221] Fix failing Docker build --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 232e84fb20..34aba25ce6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ FROM python:3.9-slim as py FROM py as build RUN apt update && apt install -y g++ -COPY requirements.min.txt / -RUN pip install --prefix=/inst -U -r /requirements.min.txt +COPY requirements.txt / +RUN pip install --prefix=/inst -U -r /requirements.txt FROM py From 3e04dd0627ce9aad9949bad4d600aaf53fe39fe0 Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sun, 1 Aug 2021 23:23:29 +0800 Subject: [PATCH 0910/1221] Fix UnicodeEncodeError on Windows. (#3043) * Fix UnionEncodeError on Windows when logging unicode emojis or special characters. * Formatting... --- bot.py | 11 ++++------- cogs/utility.py | 2 ++ core/models.py | 4 +++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bot.py b/bot.py index 07c654be3c..8961001ab1 100644 --- a/bot.py +++ b/bot.py @@ -122,12 +122,9 @@ def hosting_method(self) -> HostingMethod: def startup(self): logger.line() - if os.name != "nt": - logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") - logger.info("││││ │ │││││├─┤││") - logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") - else: - logger.info("MODMAIL") + logger.info("┌┬┐┌─┐┌┬┐┌┬┐┌─┐┬┬") + logger.info("││││ │ │││││├─┤││") + logger.info("┴ ┴└─┘─┴┘┴ ┴┴ ┴┴┴─┘") logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") logger.line() @@ -659,7 +656,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.", e) + logger.warning("%s is not a valid emoji. %s.", name, e) raise return name diff --git a/cogs/utility.py b/cogs/utility.py index 4ac02b9073..0790274c57 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -393,6 +393,7 @@ async def debug(self, ctx): os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "r+", + encoding="utf-8", ) as f: logs = f.read().strip() @@ -448,6 +449,7 @@ async def debug_hastebin(self, ctx): os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log" ), "rb+", + encoding="utf-8", ) as f: logs = BytesIO(f.read().strip()) diff --git a/core/models.py b/core/models.py index 0238e051d4..44e75043d1 100644 --- a/core/models.py +++ b/core/models.py @@ -127,7 +127,9 @@ def format(self, record): def configure_logging(name, level=None): global ch_debug, log_level - ch_debug = RotatingFileHandler(name, mode="a+", maxBytes=48000, backupCount=1) + ch_debug = RotatingFileHandler( + name, mode="a+", maxBytes=48000, backupCount=1, encoding="utf-8" + ) formatter_debug = FileFormatter( "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", From d50ec339ca56f2eb0d3809bc525a66e1740b1fef Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 2 Aug 2021 09:51:36 +0200 Subject: [PATCH 0911/1221] Plugins: add Python Discord's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **Case insensitive snippets**: Allow snippets to be ran even if with the wrong case. For example, `?Dm-RePort` will be recognized as `?dm-report`. - **Close message:** Add a `?closemessage` command that will close the thread after 15 minutes with a default message. - **MDLink**: Generate a ready to paste link to the thread logs. - **Reply cooldown**: Forbid you from sending the same message twice in ten seconds. - **Tagging**: Add a `?tag` command capable of adding a `$message|` header to the channel name. --- plugins/registry.json | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index a9f565a013..0ba98f499c 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1,4 +1,49 @@ { + "case_insensitive_snippets": { + "repository": "python-discord/modmail-plugins", + "branch": "main", + "description": "Allow snippets to be ran even if with the wrong case. For example, ?Dm-RePort will be recognized as ?dm-report.", + "bot_version": "2.20.1", + "title": "Case insensitive snippets", + "icon_url": "https://i.imgur.com/kaJYlMB.png", + "thumbnail_url": "https://i.imgur.com/kaJYlMB.png" + }, + "close_message": { + "repository": "python-discord/modmail-plugins", + "branch": "main", + "description": "Add a ?closemessage command that will close the thread after 15 minutes with a default message.", + "bot_version": "2.20.1", + "title": "Close message", + "icon_url": "https://i.imgur.com/ev7BFMz.png", + "thumbnail_url": "https://i.imgur.com/ev7BFMz.png" + }, + "mdlink": { + "repository": "python-discord/modmail-plugins", + "branch": "main", + "description": "Generate a ready to paste link to the thread logs.", + "bot_version": "2.20.1", + "title": "MDLink", + "icon_url": "https://i.imgur.com/JA2E63R.png", + "thumbnail_url": "https://i.imgur.com/JA2E63R.png" + }, + "reply_cooldown": { + "repository": "python-discord/modmail-plugins", + "branch": "main", + "description": "Forbid you from sending the same message twice in ten seconds.", + "bot_version": "2.20.1", + "title": "Reply cooldown", + "icon_url": "https://i.imgur.com/FtRQveT.png", + "thumbnail_url": "https://i.imgur.com/FtRQveT.png" + }, + "tagging": { + "repository": "python-discord/modmail-plugins", + "branch": "main", + "description": "Add a ?tag command capable of adding a $message| header to the channel name.", + "bot_version": "2.20.1", + "title": "Tagging", + "icon_url": "https://i.imgur.com/WajSLB1.png", + "thumbnail_url": "https://i.imgur.com/WajSLB1.png" + }, "dragory-migrate": { "repository": "kyb3r/modmail-plugins", "branch": "master", From d07b1a8f486a91de1a0105ae5a558360496da910 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Wed, 4 Aug 2021 01:31:57 +0530 Subject: [PATCH 0912/1221] Let snippets be invoked case-insensitively. --- bot.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 1c4fdd5354..b140dfe7d1 100644 --- a/bot.py +++ b/bot.py @@ -1132,8 +1132,11 @@ async def process_commands(self, message): cmd = message.content[len(self.prefix) :].strip() # Process snippets - if cmd in self.snippets: - snippet = self.snippets[cmd] + try: + snippet = self.snippets[cmd.lower()] + except KeyError: + pass + else: if self.config["anonymous_snippets"]: message.content = f"{self.prefix}fareply {snippet}" else: From f99245fb0e6fb86211e3e3d832aaf5d346a70d50 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 18:58:17 +0800 Subject: [PATCH 0913/1221] Update changelogs --- CHANGELOG.md | 3 ++- README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 073db1ef6e..8a9f22988c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.10.0-dev3 +# v3.10.0-dev4 ## Added @@ -20,6 +20,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/kyb3r/modmail/issues/3022), [PR #3028](https://github.com/kyb3r/modmail/pull/3028)) - Blocked users are now no longer allowed to use `?contact` and react to contact. ([COMMENT #819004157](https://github.com/kyb3r/modmail/issues/2969#issuecomment-819004157), [PR #3027](https://github.com/kyb3r/modmail/pull/3027)) +- UnicodeEncodeError will no longer be raised on Windows. ([PR #3043](https://github.com/kyb3r/modmail/pull/3043)) # v3.9.5 diff --git a/README.md b/README.md index 19931575c9..bd9d0b8f69 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ <<<<<<< HEAD - + ======= >>>>>>> master diff --git a/bot.py b/bot.py index 4c235dedad..837fe304cd 100644 --- a/bot.py +++ b/bot.py @@ -1,5 +1,5 @@ <<<<<<< HEAD -__version__ = "v3.10.0-dev3" +__version__ = "v3.10.0-dev4" ======= __version__ = "3.9.5" >>>>>>> master diff --git a/pyproject.toml b/pyproject.toml index 4bc1a22707..556c7b277c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ extend-exclude = ''' <<<<<<< HEAD [tool.poetry] name = 'Modmail' -version = '3.10.0-dev3' +version = '3.10.0-dev4' description = "Modmail is similar to Reddit's 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 dc7a61d1e09c27ccda3f40948d3b7ab8a1936e9f Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 19:00:01 +0800 Subject: [PATCH 0914/1221] Update changelog --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9f22988c..8429a26203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,16 +12,17 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Ability to have group conversations. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) -### Internal - -- `thread.reply` now returns mod_message, user_message1, user_message2... It is no longer limited at a size 2 tuple. Potentially breaking if plugins depend on this behaviour. - ## Fixed - Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/kyb3r/modmail/issues/3022), [PR #3028](https://github.com/kyb3r/modmail/pull/3028)) - Blocked users are now no longer allowed to use `?contact` and react to contact. ([COMMENT #819004157](https://github.com/kyb3r/modmail/issues/2969#issuecomment-819004157), [PR #3027](https://github.com/kyb3r/modmail/pull/3027)) - UnicodeEncodeError will no longer be raised on Windows. ([PR #3043](https://github.com/kyb3r/modmail/pull/3043)) +## Internal + +- `thread.reply` now returns mod_message, user_message1, user_message2... It is no longer limited at a size 2 tuple. Potentially breaking if plugins depend on this behaviour. +- Fix return types, type hints, and unresolved references ([PR #3009](https://github.com/kyb3r/modmail/pull/3009)) + # v3.9.5 ## Internal From 3358622fa0f997ac49062d23ae772229faf948c0 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 19:03:03 +0800 Subject: [PATCH 0915/1221] Improved way to do case insensitive snippets --- bot.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index b140dfe7d1..a054bbc83a 100644 --- a/bot.py +++ b/bot.py @@ -1132,11 +1132,9 @@ async def process_commands(self, message): cmd = message.content[len(self.prefix) :].strip() # Process snippets - try: - snippet = self.snippets[cmd.lower()] - except KeyError: - pass - else: + cmd = cmd.lower() + if cmd in self.snippets: + snippet = self.snippets[cmd] if self.config["anonymous_snippets"]: message.content = f"{self.prefix}fareply {snippet}" else: From c00e6be6cd1082a6e2c1ee3fbf4c8983c0caf744 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 19:14:21 +0800 Subject: [PATCH 0916/1221] Update changelog and merge issues --- CHANGELOG.md | 2 ++ README.md | 4 ---- bot.py | 14 -------------- cogs/modmail.py | 11 ----------- core/models.py | 4 ---- core/thread.py | 22 ---------------------- pyproject.toml | 5 ----- 7 files changed, 2 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8429a26203..c8ae76fbb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,14 @@ however, insignificant breaking changes do not guarantee a major version bump, s ## Added - Ability to have group conversations. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) +- Snippets are invoked case insensitively. ([GH #3077](https://github.com/kyb3r/modmail/issues/3077), [PR #3080](https://github.com/kyb3r/modmail/pull/3080)) ## Fixed - Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/kyb3r/modmail/issues/3022), [PR #3028](https://github.com/kyb3r/modmail/pull/3028)) - Blocked users are now no longer allowed to use `?contact` and react to contact. ([COMMENT #819004157](https://github.com/kyb3r/modmail/issues/2969#issuecomment-819004157), [PR #3027](https://github.com/kyb3r/modmail/pull/3027)) - UnicodeEncodeError will no longer be raised on Windows. ([PR #3043](https://github.com/kyb3r/modmail/pull/3043)) +- Notifications are no longer duplicated when using both `?notify` and `subscribe`. ([PR #3015](https://github.com/kyb3r/modmail/pull/3015)) ## Internal diff --git a/README.md b/README.md index bd9d0b8f69..7659898399 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,7 @@
      -<<<<<<< HEAD -======= - ->>>>>>> master
      diff --git a/bot.py b/bot.py index 93f407bcfa..fce5afa73c 100644 --- a/bot.py +++ b/bot.py @@ -1,8 +1,4 @@ -<<<<<<< HEAD __version__ = "v3.10.0-dev4" -======= -__version__ = "3.9.5" ->>>>>>> master import asyncio @@ -1268,13 +1264,9 @@ async def handle_reaction_events(self, payload): if not thread: return try: -<<<<<<< HEAD _, *linked_message = await thread.find_linked_messages( message.id, either_direction=True ) -======= - _, linked_message = await thread.find_linked_messages(message.id, either_direction=True) ->>>>>>> master except ValueError as e: logger.warning("Failed to find linked message for reactions: %s", e) return @@ -1305,7 +1297,6 @@ async def handle_react_to_contact(self, payload): else: emoji_fmt = f"<:{payload.emoji.name}:{payload.emoji.id}>" -<<<<<<< HEAD if emoji_fmt != react_message_emoji: return channel = self.get_channel(payload.channel_id) @@ -1338,11 +1329,6 @@ async def on_raw_reaction_add(self, payload): await asyncio.gather( self.handle_reaction_events(payload), self.handle_react_to_contact(payload), ) -======= - ctx = await self.get_context(message) - ctx.author = member - await ctx.invoke(self.get_command("contact"), user=member, manual_trigger=False) ->>>>>>> master async def on_raw_reaction_remove(self, payload): if self.config["transfer_reactions"]: diff --git a/cogs/modmail.py b/cogs/modmail.py index 4e6c1fc126..f10a9e50d3 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1046,18 +1046,11 @@ async def contact( exists = await self.bot.threads.find(recipient=user) if exists: -<<<<<<< HEAD desc = "A thread for this user already exists" if exists.channel: desc += f" in {exists.channel.mention}" desc += "." embed = discord.Embed(color=self.bot.error_color, description=desc) -======= - embed = discord.Embed( - color=self.bot.error_color, - description="A thread for this user already " f"exists in {exists.channel.mention}.", - ) ->>>>>>> master await ctx.channel.send(embed=embed, delete_after=3) else: @@ -1550,13 +1543,9 @@ async def repair(self, ctx): other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid) if recipient is None: -<<<<<<< HEAD self.bot.threads.cache[user.id] = thread = Thread( self.bot.threads, user_id, ctx.channel, other_recipients ) -======= - self.bot.threads.cache[user.id] = thread = Thread(self.bot.threads, user_id, ctx.channel) ->>>>>>> master else: self.bot.threads.cache[user.id] = thread = Thread( self.bot.threads, recipient, ctx.channel, other_recipients diff --git a/core/models.py b/core/models.py index 3b1e9ce481..7036498910 100644 --- a/core/models.py +++ b/core/models.py @@ -127,13 +127,9 @@ def format(self, record): def configure_logging(name, level=None): global ch_debug, log_level -<<<<<<< HEAD ch_debug = RotatingFileHandler( name, mode="a+", maxBytes=48000, backupCount=1, encoding="utf-8" ) -======= - ch_debug = RotatingFileHandler(name, mode="a+", maxBytes=78000, backupCount=10) ->>>>>>> master formatter_debug = FileFormatter( "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", diff --git a/core/thread.py b/core/thread.py index 2d24bd9132..b99a638a88 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1136,7 +1136,6 @@ async def find( logger.warning("Thread for %s cancelled.", recipient) return thread else: -<<<<<<< HEAD if not thread.cancelled and ( not thread.channel or not self.bot.get_channel(thread.channel.id) ): @@ -1144,13 +1143,6 @@ async def find( "Found existing thread for %s but the channel is invalid.", recipient_id ) await thread.close(closer=self.bot.user, silent=True, delete_channel=False) -======= - 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) - ) ->>>>>>> master thread = None else: channel = discord.utils.find( @@ -1224,17 +1216,10 @@ async def create( if thread.channel and self.bot.get_channel(thread.channel.id): logger.warning("Found an existing thread for %s, abort creating.", recipient) return thread -<<<<<<< HEAD - logger.warning( - "Found an existing thread for %s, closing previous thread.", recipient - ) - await thread.close(closer=self.bot.user, silent=True, delete_channel=False) -======= 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) ) ->>>>>>> master thread = Thread(self, recipient) @@ -1304,18 +1289,11 @@ async def remove_reactions(): for emoji in emojis: await confirm.remove_reaction(emoji, self.bot.user) await asyncio.sleep(0.2) -<<<<<<< HEAD self.bot.loop.create_task(remove_reactions()) if thread.cancelled: del self.cache[recipient.id] return thread -======= - await confirm.remove_reaction(deny_emoji, self.bot.user) - await destination.send(embed=discord.Embed(title="Cancelled", color=self.bot.error_color)) - del self.cache[recipient.id] - return thread ->>>>>>> master self.bot.loop.create_task(thread.setup(creator=creator, category=category, initial_message=message)) return thread diff --git a/pyproject.toml b/pyproject.toml index 556c7b277c..e038685a61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ extend-exclude = ''' ) ''' -<<<<<<< HEAD [tool.poetry] name = 'Modmail' version = '3.10.0-dev4' @@ -34,10 +33,6 @@ readme = 'README.md' repository = 'https://github.com/kyb3r/modmail' homepage = 'https://github.com/kyb3r/modmail' keywords = ['discord', 'modmail'] -======= -[tool.pylint.messages_control] -disable = "C0330, C0326" ->>>>>>> master [tool.pylint.format] max-line-length = "110" From 0c9c129793d044438bc15ebdc68364d434b8e3d6 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 19:14:38 +0800 Subject: [PATCH 0917/1221] Formatting --- bot.py | 15 ++++++--------- cogs/modmail.py | 5 ++++- core/models.py | 4 +--- core/thread.py | 30 +++++++++++------------------- core/utils.py | 4 +--- 5 files changed, 23 insertions(+), 35 deletions(-) diff --git a/bot.py b/bot.py index fce5afa73c..fb9a1a9f9d 100644 --- a/bot.py +++ b/bot.py @@ -1264,9 +1264,7 @@ async def handle_reaction_events(self, payload): if not thread: return try: - _, *linked_message = await thread.find_linked_messages( - message.id, either_direction=True - ) + _, *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 @@ -1287,10 +1285,7 @@ async def handle_reaction_events(self, payload): async def handle_react_to_contact(self, payload): react_message_id = tryint(self.config.get("react_to_contact_message")) react_message_emoji = self.config.get("react_to_contact_emoji") - if ( - not all((react_message_id, react_message_emoji)) - or payload.message_id != react_message_id - ): + if not all((react_message_id, react_message_emoji)) or payload.message_id != react_message_id: return if payload.emoji.is_unicode_emoji(): emoji_fmt = payload.emoji.name @@ -1314,7 +1309,8 @@ async def handle_react_to_contact(self, payload): 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 using react to contact was blocked from %s due to disabled Modmail.", @@ -1327,7 +1323,8 @@ async def handle_react_to_contact(self, payload): async def on_raw_reaction_add(self, payload): await asyncio.gather( - self.handle_reaction_events(payload), self.handle_react_to_contact(payload), + self.handle_reaction_events(payload), + self.handle_react_to_contact(payload), ) async def on_raw_reaction_remove(self, payload): diff --git a/cogs/modmail.py b/cogs/modmail.py index f10a9e50d3..adec3f5447 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1067,7 +1067,10 @@ async def contact( return await ctx.send(embed=embed) thread = await self.bot.threads.create( - recipient=user, creator=creator, category=category, manual_trigger=manual_trigger, + recipient=user, + creator=creator, + category=category, + manual_trigger=manual_trigger, ) if thread.cancelled: return diff --git a/core/models.py b/core/models.py index 7036498910..445f7793ae 100644 --- a/core/models.py +++ b/core/models.py @@ -127,9 +127,7 @@ def format(self, record): def configure_logging(name, level=None): global ch_debug, log_level - ch_debug = RotatingFileHandler( - name, mode="a+", maxBytes=48000, backupCount=1, encoding="utf-8" - ) + ch_debug = RotatingFileHandler(name, mode="a+", maxBytes=48000, backupCount=1, encoding="utf-8") formatter_debug = FileFormatter( "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", diff --git a/core/thread.py b/core/thread.py index b99a638a88..44ff156aed 100644 --- a/core/thread.py +++ b/core/thread.py @@ -111,15 +111,11 @@ def cancelled(self, flag: bool): i.cancel() @classmethod - async def from_channel( - cls, manager: "ThreadManager", channel: discord.TextChannel - ) -> "Thread": + async def from_channel(cls, manager: "ThreadManager", channel: discord.TextChannel) -> "Thread": recipient_id = match_user_id( channel.topic ) # there is a chance it grabs from another recipient's main thread - recipient = manager.bot.get_user(recipient_id) or await manager.bot.fetch_user( - recipient_id - ) + recipient = manager.bot.get_user(recipient_id) or await manager.bot.fetch_user(recipient_id) other_recipients = match_other_recipients(channel.topic) for n, uid in enumerate(other_recipients): @@ -761,7 +757,11 @@ async def reply(self, message: discord.Message, anonymous: bool = False, plain: for user in self.recipients: user_msg_tasks.append( self.send( - message, destination=user, from_mod=True, anonymous=anonymous, plain=plain, + message, + destination=user, + from_mod=True, + anonymous=anonymous, + plain=plain, ) ) @@ -1071,9 +1071,7 @@ async def set_title(self, title: str) -> None: user_id = match_user_id(self.channel.topic) ids = ",".join(i.id for i in self._other_recipients) - await self.channel.edit( - topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}" - ) + await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") async def add_user(self, user: typing.Union[discord.Member, discord.User]) -> None: title = match_title(self.channel.topic) @@ -1081,9 +1079,7 @@ async def add_user(self, user: typing.Union[discord.Member, discord.User]) -> No self._other_recipients.append(user) ids = ",".join(str(i.id) for i in self._other_recipients) - await self.channel.edit( - topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}" - ) + await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") class ThreadManager: @@ -1139,9 +1135,7 @@ async def find( if not thread.cancelled and ( 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 - ) + logger.warning("Found existing thread for %s but the channel is invalid.", recipient_id) await thread.close(closer=self.bot.user, silent=True, delete_channel=False) thread = None else: @@ -1280,9 +1274,7 @@ async def create( if str(r.emoji) == deny_emoji: thread.cancelled = True self.bot.loop.create_task( - destination.send( - embed=discord.Embed(title="Cancelled", color=self.bot.error_color) - ) + destination.send(embed=discord.Embed(title="Cancelled", color=self.bot.error_color)) ) async def remove_reactions(): diff --git a/core/utils.py b/core/utils.py index e80e298931..173eda25d5 100644 --- a/core/utils.py +++ b/core/utils.py @@ -218,9 +218,7 @@ def cleanup_code(content: str) -> str: return content.strip("` \n") -TOPIC_OTHER_RECIPIENTS_REGEX = re.compile( - r"Other Recipients:\s*((?:\d{17,21},*)+)", flags=re.IGNORECASE -) +TOPIC_OTHER_RECIPIENTS_REGEX = re.compile(r"Other Recipients:\s*((?:\d{17,21},*)+)", flags=re.IGNORECASE) TOPIC_TITLE_REGEX = re.compile(r"\bTitle: (.*)\n(?:User ID: )\b", flags=re.IGNORECASE | re.DOTALL) TOPIC_UID_REGEX = re.compile(r"\bUser ID:\s*(\d{17,21})\b", flags=re.IGNORECASE) From 323a5574eb412fc9bd9f7bcaa73e4493250e8db6 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 19:39:37 +0800 Subject: [PATCH 0918/1221] Fix contact with category and silent, #3076 --- CHANGELOG.md | 1 + bot.py | 2 +- cogs/modmail.py | 9 ++++++++- core/thread.py | 7 +++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ae76fbb6..41da5adcee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Blocked users are now no longer allowed to use `?contact` and react to contact. ([COMMENT #819004157](https://github.com/kyb3r/modmail/issues/2969#issuecomment-819004157), [PR #3027](https://github.com/kyb3r/modmail/pull/3027)) - UnicodeEncodeError will no longer be raised on Windows. ([PR #3043](https://github.com/kyb3r/modmail/pull/3043)) - Notifications are no longer duplicated when using both `?notify` and `subscribe`. ([PR #3015](https://github.com/kyb3r/modmail/pull/3015)) +- `?contact` now works properly with both category and silent. ([GH #3076](https://github.com/kyb3r/modmail/issues/3076)) ## Internal diff --git a/bot.py b/bot.py index fb9a1a9f9d..36675a1ae4 100644 --- a/bot.py +++ b/bot.py @@ -284,7 +284,7 @@ def _cancel_tasks(): loop.run_until_complete(loop.shutdown_asyncgens()) finally: logger.info("Closing the event loop.") - loop.close() + # loop.close() if not future.cancelled(): try: diff --git a/cogs/modmail.py b/cogs/modmail.py index adec3f5447..1dee52d941 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1038,7 +1038,14 @@ async def contact( if isinstance(category, str): if "silent" in category or "silently" in category: silent = True - category = None + category = category.strip("silently").strip("silent").strip() + try: + category = await SimilarCategoryConverter().convert(ctx, category) # attempt to find a category again + except commands.BadArgument: + category = None + + if isinstance(category, str): + category = None if user.bot: embed = discord.Embed(color=self.bot.error_color, description="Cannot start a thread with a bot.") diff --git a/core/thread.py b/core/thread.py index 44ff156aed..0c5339ff83 100644 --- a/core/thread.py +++ b/core/thread.py @@ -487,10 +487,17 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, if self.bot.config["show_timestamp"]: embed.timestamp = datetime.utcnow() + if not message: + if self.id == closer.id: + message = self.bot.config["thread_self_close_response"] + else: + 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 ) + embed.description = message footer = self.bot.config["thread_close_footer"] embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) From 0c49598ec8c7758ed5b64ad3ffa4544f793bbf18 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 19:48:43 +0800 Subject: [PATCH 0919/1221] Resolve close_on_leave_reason not properly working --- CHANGELOG.md | 1 + core/thread.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41da5adcee..ad69c7b7ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - UnicodeEncodeError will no longer be raised on Windows. ([PR #3043](https://github.com/kyb3r/modmail/pull/3043)) - Notifications are no longer duplicated when using both `?notify` and `subscribe`. ([PR #3015](https://github.com/kyb3r/modmail/pull/3015)) - `?contact` now works properly with both category and silent. ([GH #3076](https://github.com/kyb3r/modmail/issues/3076)) +- Resolves `close_on_leave_reason` not properly working when `close_on_leave` is enabled. ([GH #3075](https://github.com/kyb3r/modmail/issues/3075)) ## Internal diff --git a/core/thread.py b/core/thread.py index 0c5339ff83..2d1d5d18f9 100644 --- a/core/thread.py +++ b/core/thread.py @@ -414,7 +414,7 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, "title": match_title(self.channel.topic), "closed_at": str(datetime.utcnow()), "nsfw": self.channel.nsfw, - "close_message": message if not silent else None, + "close_message": message, "closer": { "id": str(closer.id), "name": closer.name, From adac1de293cd188a8b2e25d22ddb1fa01f0d6fc8 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 19:51:02 +0800 Subject: [PATCH 0920/1221] Invalid arguments are now properly catched and a proper error message is sent (BadUnionArg) --- CHANGELOG.md | 3 ++- bot.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad69c7b7ed..eaa0e668d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s - UnicodeEncodeError will no longer be raised on Windows. ([PR #3043](https://github.com/kyb3r/modmail/pull/3043)) - Notifications are no longer duplicated when using both `?notify` and `subscribe`. ([PR #3015](https://github.com/kyb3r/modmail/pull/3015)) - `?contact` now works properly with both category and silent. ([GH #3076](https://github.com/kyb3r/modmail/issues/3076)) -- Resolves `close_on_leave_reason` not properly working when `close_on_leave` is enabled. ([GH #3075](https://github.com/kyb3r/modmail/issues/3075)) +- `close_on_leave_reason` now works properly when `close_on_leave` is enabled. ([GH #3075](https://github.com/kyb3r/modmail/issues/3075)) +- Invalid arguments are now properly catched and a proper error message is sent. ## Internal diff --git a/bot.py b/bot.py index 36675a1ae4..26fac619d2 100644 --- a/bot.py +++ b/bot.py @@ -1472,7 +1472,7 @@ async def on_error(self, event_method, *args, **kwargs): logger.error("Unexpected exception:", exc_info=sys.exc_info()) async def on_command_error(self, context, exception): - if isinstance(exception, commands.BadArgument): + if isinstance(exception, (commands.BadArgument, commands.BadUnionArgument)): await context.trigger_typing() await context.send(embed=discord.Embed(color=self.error_color, description=str(exception))) elif isinstance(exception, commands.CommandNotFound): From dcfdfead8959a27389eb3764ba15b5d299491268 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 20:31:41 +0800 Subject: [PATCH 0921/1221] Removeuser (resolve #3065), silent adding/removing (#3054) --- CHANGELOG.md | 6 +++ cogs/modmail.py | 99 +++++++++++++++++++++++++++++++++++++++---------- core/thread.py | 37 +++++++++++++++--- 3 files changed, 117 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaa0e668d2..d398e71f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ however, insignificant breaking changes do not guarantee a major version bump, s # v3.10.0-dev4 +v3.10 adds group conversations while resolving othre bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. + +## Breaking + +- `Thread.recipient` (`str`) is now `Thread.recipients` (`List[str]`). + ## Added - Ability to have group conversations. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) diff --git a/cogs/modmail.py b/cogs/modmail.py index 1dee52d941..8b86a8215c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -675,8 +675,11 @@ async def title(self, ctx, *, name: str): @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @commands.cooldown(1, 600, BucketType.channel) - async def adduser(self, ctx, *, user: discord.Member): - """Adds a user to a modmail thread""" + async def adduser(self, ctx, user: discord.Member, *, options: str.lower = ""): + """Adds a user to a modmail thread + + `options` can be `silent` or `silently`. + """ curr_thread = await self.bot.threads.find(recipient=user) if curr_thread: @@ -686,30 +689,88 @@ async def adduser(self, ctx, *, user: discord.Member): color=self.bot.error_color, ) await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) else: + if 'silent' not in options and 'silently' not in options: + em = discord.Embed( + title="New Thread (Group)", + description=f"{ctx.author.name} has added you to a Modmail thread.", + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{ctx.author}", icon_url=ctx.author.avatar_url) + await user.send(embed=em) + + em = discord.Embed( + title="New User", + description=f"{ctx.author.name} has added {user.name} to the Modmail thread.", + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{user}", icon_url=user.avatar_url) + + for i in ctx.thread.recipients: + if i != user: + await i.send(embed=em) + + await ctx.thread.add_user(user) + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + + @commands.command(cooldown_after_parsing=True) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + @commands.cooldown(1, 600, BucketType.channel) + async def removeuser(self, ctx, user: discord.Member, *, options: str.lower = ""): + """Removes a user from a modmail thread + + `options` can be `silent` or `silently`. + """ + + curr_thread = await self.bot.threads.find(recipient=user) + if ctx.thread != curr_thread: em = discord.Embed( - title="New Thread (Group)", - description=f"{ctx.author.name} has added you to a Modmail thread.", - color=self.bot.main_color, + title="Error", + description="User is not in this thread.", + color=self.bot.error_color, ) - if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{ctx.author}", icon_url=ctx.author.avatar_url) - await user.send(embed=em) - + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + elif ctx.thread.recipient == user: em = discord.Embed( - title="New User", - description=f"{ctx.author.name} has added {user.name} to the Modmail thread.", - color=self.bot.main_color, + title="Error", + description="User is the main recipient of the thread and cannot be removed.", + color=self.bot.error_color, ) - if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{user}", icon_url=user.avatar_url) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + else: + if 'silent' not in options and 'silently' not in options: + em = discord.Embed( + title="Removed From Thread (Group)", + description=f"{ctx.author.name} has been removed from the Modmail thread.", + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{ctx.author}", icon_url=ctx.author.avatar_url) + await user.send(embed=em) - for i in ctx.thread.recipients: - await i.send(embed=em) + em = discord.Embed( + title="User Removed", + description=f"{ctx.author.name} has removed {user.name} from the Modmail thread.", + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{user}", icon_url=user.avatar_url) - await ctx.thread.add_user(user) + for i in ctx.thread.recipients: + await i.send(embed=em) + + await ctx.thread.remove_user(user) sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) diff --git a/core/thread.py b/core/thread.py index 2d1d5d18f9..bc47f606ed 100644 --- a/core/thread.py +++ b/core/thread.py @@ -57,7 +57,10 @@ def __init__( self._cancelled = False def __repr__(self): - return f'Thread(recipient="{self.recipient or self.id}", channel={self.channel.id}, other_recipienets={len(self._other_recipients)})' + return f'Thread(recipient="{self.recipient or self.id}", channel={self.channel.id}, other_recipients={len(self._other_recipients)})' + + def __eq__(self, other): + return self.id == other.id async def wait_until_ready(self) -> None: """Blocks execution until the thread is fully set up.""" @@ -115,13 +118,19 @@ async def from_channel(cls, manager: "ThreadManager", channel: discord.TextChann recipient_id = match_user_id( channel.topic ) # there is a chance it grabs from another recipient's main thread - recipient = manager.bot.get_user(recipient_id) or await manager.bot.fetch_user(recipient_id) - other_recipients = match_other_recipients(channel.topic) - for n, uid in enumerate(other_recipients): - other_recipients[n] = manager.bot.get_user(uid) or await manager.bot.fetch_user(uid) + if recipient_id in manager.cache: + thread = manager.cache[recipient_id] + else: + recipient = manager.bot.get_user(recipient_id) or await manager.bot.fetch_user(recipient_id) - return cls(manager, recipient or recipient_id, channel, other_recipients) + other_recipients = match_other_recipients(channel.topic) + for n, uid in enumerate(other_recipients): + other_recipients[n] = manager.bot.get_user(uid) or await manager.bot.fetch_user(uid) + + thread = cls(manager, recipient or recipient_id, channel, other_recipients) + + return thread async def setup(self, *, creator=None, category=None, initial_message=None): """Create the thread channel and other io related initialisation tasks""" @@ -1088,6 +1097,16 @@ async def add_user(self, user: typing.Union[discord.Member, discord.User]) -> No ids = ",".join(str(i.id) for i in self._other_recipients) await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") + async def remove_user(self, user: typing.Union[discord.Member, discord.User]) -> None: + title = match_title(self.channel.topic) + user_id = match_user_id(self.channel.topic) + self._other_recipients.remove(user) + + ids = ",".join(str(i.id) for i in self._other_recipients) + await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") + # if user.id in self.manager.cache: + # self.manager.cache.pop(self.id) + class ThreadManager: """Class that handles storing, finding and creating Modmail threads.""" @@ -1146,6 +1165,7 @@ async def find( await thread.close(closer=self.bot.user, silent=True, delete_channel=False) thread = None else: + print('in here') channel = discord.utils.find( lambda x: str(recipient_id) in x.topic if x.topic else False, self.bot.modmail_guild.text_channels, @@ -1157,6 +1177,11 @@ async def find( # only save if data is valid self.cache[recipient_id] = thread thread.ready = True + + if thread and recipient_id not in [x.id for x in thread.recipients]: + self.cache.pop(recipient_id) + thread = None + return thread async def _find_from_channel(self, channel): From 00eb779f2f9abe5f52c7413dad9656971fef9b3c Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 20:32:32 +0800 Subject: [PATCH 0922/1221] Formatting --- cogs/modmail.py | 8 +++++--- core/checks.py | 2 +- core/thread.py | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 8b86a8215c..bbff3b491a 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -691,7 +691,7 @@ async def adduser(self, ctx, user: discord.Member, *, options: str.lower = ""): await ctx.send(embed=em) ctx.command.reset_cooldown(ctx) else: - if 'silent' not in options and 'silently' not in options: + if "silent" not in options and "silently" not in options: em = discord.Embed( title="New Thread (Group)", description=f"{ctx.author.name} has added you to a Modmail thread.", @@ -747,7 +747,7 @@ async def removeuser(self, ctx, user: discord.Member, *, options: str.lower = "" await ctx.send(embed=em) ctx.command.reset_cooldown(ctx) else: - if 'silent' not in options and 'silently' not in options: + if "silent" not in options and "silently" not in options: em = discord.Embed( title="Removed From Thread (Group)", description=f"{ctx.author.name} has been removed from the Modmail thread.", @@ -1101,7 +1101,9 @@ async def contact( silent = True category = category.strip("silently").strip("silent").strip() try: - category = await SimilarCategoryConverter().convert(ctx, category) # attempt to find a category again + category = await SimilarCategoryConverter().convert( + ctx, category + ) # attempt to find a category again except commands.BadArgument: category = None diff --git a/core/checks.py b/core/checks.py index 1079b55530..3f3666538d 100644 --- a/core/checks.py +++ b/core/checks.py @@ -31,7 +31,7 @@ def has_permissions(permission_level: PermissionLevel = PermissionLevel.REGULAR) :: @has_permissions(PermissionLevel.OWNER) async def setup(ctx): - print("Success") + await ctx.send('Success') """ return commands.check(has_permissions_predicate(permission_level)) diff --git a/core/thread.py b/core/thread.py index bc47f606ed..29446a32c3 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1165,7 +1165,6 @@ async def find( await thread.close(closer=self.bot.user, silent=True, delete_channel=False) thread = None else: - print('in here') channel = discord.utils.find( lambda x: str(recipient_id) in x.topic if x.topic else False, self.bot.modmail_guild.text_channels, From 417f30326b315d85513b3426258a7f103d300c36 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 21:16:55 +0800 Subject: [PATCH 0923/1221] add multiple users to a thread (resolve #3066), new group config options --- cogs/modmail.py | 309 ++++++++++++++++++++++++++++++++---------- core/config.py | 13 ++ core/config_help.json | 144 ++++++++++++++++++++ core/thread.py | 15 +- 4 files changed, 401 insertions(+), 80 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index bbff3b491a..aeace73192 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -671,108 +671,271 @@ async def title(self, ctx, *, name: str): await ctx.message.pin() await self.bot.add_reaction(ctx.message, sent_emoji) - @commands.command(cooldown_after_parsing=True) + @commands.command(usage=" [options]", cooldown_after_parsing=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @commands.cooldown(1, 600, BucketType.channel) - async def adduser(self, ctx, user: discord.Member, *, options: str.lower = ""): + async def adduser(self, ctx, *users: Union[discord.Member, str]): """Adds a user to a modmail thread `options` can be `silent` or `silently`. """ - - curr_thread = await self.bot.threads.find(recipient=user) - if curr_thread: + silent = False + for u in users: + if isinstance(u, str): + if "silent" in u or "silently" in u: + silent = True + users.remove(u) # remove all strings so users is a List[Member] + else: + # u is a discord.Member + curr_thread = await self.bot.threads.find(recipient=u) + if curr_thread: + em = discord.Embed( + title="Error", + description=f"{u.mention} is already in a thread: {curr_thread.channel.mention}.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + if not silent: em = discord.Embed( - title="Error", - description=f"User is already in a thread: {curr_thread.channel.mention}.", - color=self.bot.error_color, + title=self.bot.config["private_added_to_group_title"], + description=self.bot.config["private_added_to_group_description"].format(moderator=ctx.author), + color=self.bot.main_color, ) - await ctx.send(embed=em) - ctx.command.reset_cooldown(ctx) - else: - if "silent" not in options and "silently" not in options: - em = discord.Embed( - title="New Thread (Group)", - description=f"{ctx.author.name} has added you to a Modmail thread.", - color=self.bot.main_color, - ) - if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{ctx.author}", icon_url=ctx.author.avatar_url) - await user.send(embed=em) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=str(ctx.author), icon_url=ctx.author.avatar_url) + for u in users: + await u.send(embed=em) - em = discord.Embed( - title="New User", - description=f"{ctx.author.name} has added {user.name} to the Modmail thread.", - color=self.bot.main_color, - ) - if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{user}", icon_url=user.avatar_url) + em = discord.Embed( + title=self.bot.config["public_added_to_group_title"], + description=self.bot.config["private_added_to_group_description"].format(moderator=ctx.author, users=', '.join(u.name for u in users)), + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) - for i in ctx.thread.recipients: - if i != user: - await i.send(embed=em) + for i in ctx.thread.recipients: + if i not in users: + await i.send(embed=em) - await ctx.thread.add_user(user) - sent_emoji, _ = await self.bot.retrieve_emoji() - await self.bot.add_reaction(ctx.message, sent_emoji) + await ctx.thread.add_users(users) + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) - @commands.command(cooldown_after_parsing=True) + @commands.command(usage=" [options]", cooldown_after_parsing=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @commands.cooldown(1, 600, BucketType.channel) - async def removeuser(self, ctx, user: discord.Member, *, options: str.lower = ""): + async def removeuser(self, ctx, *users: Union[discord.Member, str]): """Removes a user from a modmail thread `options` can be `silent` or `silently`. """ + silent = False + for u in users: + if isinstance(u, str): + if "silent" in u or "silently" in u: + silent = True + users.remove(u) # remove all strings so users is a List[Member] + else: + # u is a discord.Member + curr_thread = await self.bot.threads.find(recipient=u) + if ctx.thread != curr_thread: + em = discord.Embed( + title="Error", + description=f"{u.mention} is not in this thread.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + elif ctx.thread.recipient == u: + em = discord.Embed( + title="Error", + description=f"{u.mention} is the main recipient of the thread and cannot be removed.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return - curr_thread = await self.bot.threads.find(recipient=user) - if ctx.thread != curr_thread: + if not silent: em = discord.Embed( - title="Error", - description="User is not in this thread.", - color=self.bot.error_color, + title=self.bot.config["private_removed_from_group_title"], + description=self.bot.config["private_removed_from_group_description"].format(moderator=ctx.author), + color=self.bot.main_color, ) - await ctx.send(embed=em) - ctx.command.reset_cooldown(ctx) - elif ctx.thread.recipient == user: + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=str(ctx.author), icon_url=ctx.author.avatar_url) + for u in users: + await u.send(embed=em) + em = discord.Embed( - title="Error", - description="User is the main recipient of the thread and cannot be removed.", - color=self.bot.error_color, + title=self.bot.config["public_removed_from_group_title"], + description=self.bot.config["private_removed_from_group_description"].format(moderator=ctx.author, users=', '.join(u.name for u in users)), + color=self.bot.main_color, ) - await ctx.send(embed=em) - ctx.command.reset_cooldown(ctx) - else: - if "silent" not in options and "silently" not in options: - em = discord.Embed( - title="Removed From Thread (Group)", - description=f"{ctx.author.name} has been removed from the Modmail thread.", - color=self.bot.main_color, - ) - if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{ctx.author}", icon_url=ctx.author.avatar_url) - await user.send(embed=em) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) - em = discord.Embed( - title="User Removed", - description=f"{ctx.author.name} has removed {user.name} from the Modmail thread.", - color=self.bot.main_color, - ) - if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{user}", icon_url=user.avatar_url) + for i in ctx.thread.recipients: + if i not in users: + await i.send(embed=em) + + await ctx.thread.remove_users(users) + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + + @commands.command(usage=" [options]", cooldown_after_parsing=True) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + @commands.cooldown(1, 600, BucketType.channel) + async def anonadduser(self, ctx, *users: Union[discord.Member, str]): + """Adds a user to a modmail thread anonymously + + `options` can be `silent` or `silently`. + """ + silent = False + for u in users: + if isinstance(u, str): + if "silent" in u or "silently" in u: + silent = True + users.remove(u) # remove all strings so users is a List[Member] + else: + # u is a discord.Member + curr_thread = await self.bot.threads.find(recipient=u) + if curr_thread: + em = discord.Embed( + title="Error", + description=f"{u.mention} is already in a thread: {curr_thread.channel.mention}.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + if not silent: + em = discord.Embed( + title=self.bot.config["private_added_to_group_title"], + description=self.bot.config["private_added_to_group_description_anon"], + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + + tag = self.bot.config["mod_tag"] + if tag is None: + tag = str(ctx.author.top_role) + name = self.bot.config["anon_username"] + if name is None: + name = tag + avatar_url = self.bot.config["anon_avatar_url"] + if avatar_url is None: + avatar_url = self.bot.guild.icon_url + em.set_footer(text=name, icon_url=avatar_url) + + for u in users: + await u.send(embed=em) - for i in ctx.thread.recipients: + em = discord.Embed( + title=self.bot.config["public_added_to_group_title"], + description=self.bot.config["private_added_to_group_description_anon"].format(moderator=ctx.author, users=', '.join(u.name for u in users)), + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) + + for i in ctx.thread.recipients: + if i not in users: await i.send(embed=em) - await ctx.thread.remove_user(user) - sent_emoji, _ = await self.bot.retrieve_emoji() - await self.bot.add_reaction(ctx.message, sent_emoji) + await ctx.thread.add_users(users) + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + + @commands.command(usage=" [options]", cooldown_after_parsing=True) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + @commands.cooldown(1, 600, BucketType.channel) + async def anonremoveuser(self, ctx, *users: Union[discord.Member, str]): + """Removes a user from a modmail thread anonymously + + `options` can be `silent` or `silently`. + """ + silent = False + for u in users: + if isinstance(u, str): + if "silent" in u or "silently" in u: + silent = True + users.remove(u) # remove all strings so users is a List[Member] + else: + # u is a discord.Member + curr_thread = await self.bot.threads.find(recipient=u) + if ctx.thread != curr_thread: + em = discord.Embed( + title="Error", + description=f"{u.mention} is not in this thread.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + elif ctx.thread.recipient == u: + em = discord.Embed( + title="Error", + description=f"{u.mention} is the main recipient of the thread and cannot be removed.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + + if not silent: + em = discord.Embed( + title=self.bot.config["private_removed_from_group_title"], + description=self.bot.config["private_removed_from_group_description_anon"].format(moderator=ctx.author), + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + + tag = self.bot.config["mod_tag"] + if tag is None: + tag = str(ctx.author.top_role) + name = self.bot.config["anon_username"] + if name is None: + name = tag + avatar_url = self.bot.config["anon_avatar_url"] + if avatar_url is None: + avatar_url = self.bot.guild.icon_url + em.set_footer(text=name, icon_url=avatar_url) + + for u in users: + await u.send(embed=em) + + em = discord.Embed( + title=self.bot.config["public_removed_from_group_title"], + description=self.bot.config["private_removed_from_group_description"].format(moderator=ctx.author, users=', '.join(u.name for u in users)), + color=self.bot.main_color, + ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) + + for i in ctx.thread.recipients: + if i not in users: + await i.send(embed=em) + + await ctx.thread.remove_users(users) + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) diff --git a/core/config.py b/core/config.py index 255c3667de..829ce7ff3b 100644 --- a/core/config.py +++ b/core/config.py @@ -83,6 +83,19 @@ class ConfigManager: "silent_alert_on_mention": False, "show_timestamp": True, "anonymous_snippets": False, + # group conversations + "private_added_to_group_title": "New Thread (Group)", + "private_added_to_group_description": "{moderator.name} has added you to a Modmail thread.", + "private_added_to_group_description_anon": "A moderator has added you to a Modmail thread.", + "public_added_to_group_title": "New User", + "public_added_to_group_description": "{moderator.name} has added {users} to the Modmail thread.", + "public_added_to_group_description_anon": "A moderator has added {users} to the Modmail thread.", + "private_removed_from_group_title": "Removed From Thread (Group)", + "private_removed_from_group_description": "{moderator.name} has removed you from the Modmail thread.", + "private_removed_from_group_description_anon": "A moderator has removed you from the Modmail thread.", + "public_removed_from_group_title": "User Removed", + "public_removed_from_group_description": "{moderator.name} has removed {users} from the Modmail thread.", + "public_removed_from_group_description_anon": "A moderator has removed {users} from the Modmail thread.", # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), diff --git a/core/config_help.json b/core/config_help.json index ede81617ed..d6a4627d96 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -711,6 +711,150 @@ "See also: `anon_avatar_url`, `anon_tag`." ] }, + "private_added_to_group_title": { + "default": "New Thread (Group)", + "description": "This is the message embed title sent to the recipient that is just added to a thread.", + "examples": [ + "`{prefix}config set private_added_to_group_title Welcome to this new group thread!`" + ], + "notes": [ + "The public_ variant is used when sending to other thread recipients.", + "See also: `private_added_to_group_description`, `public_added_to_group_title`" + ] + }, + "private_added_to_group_description": { + "default": "\"{{moderator.name}} has added you to a Modmail thread.\"", + "description": "This is the message embed content sent to the recipient that is just added to a thread.", + "examples": [ + "`{prefix}config set private_added_to_group_description Any message sent here will be sent to all otherthread recipients.`" + ], + "notes": [ + "You may use the `{{moderator}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that added the user.", + "When anonadduser is used, `private_added_to_group_description_anon` is used instead.", + "The public_ variant is used when sending to other thread recipients.", + "See also: `private_added_to_group_title`, `public_added_to_group_description`" + ] + }, + "private_added_to_group_description_anon": { + "default": "A moderator has added you to a Modmail thread.", + "description": "This is the message embed content sent to the recipient that is just added to a thread when adduser is used anonymously.", + "examples": [ + "`{prefix}config set private_added_to_group_description_anon Any message sent here will be sent to all other thread recipients.`" + ], + "notes": [ + "When adduser (no anon) is used, `private_added_to_group_description` is used instead.", + "The public_ variant is used when sending to other thread recipients.", + "See also: `private_added_to_group_title`, `public_added_to_group_description_anon`" + ] + }, + "public_added_to_group_title": { + "default": "New User", + "description": "This is the message embed title sent to all other recipients when someone is added to the thread.", + "examples": [ + "`{prefix}config set public_added_to_group_title Welcome to our new user!`" + ], + "notes": [ + "The private_ variant is used when sending to the new user.", + "See also: `private_added_to_group_title`, `private_added_to_group_title`" + ] + }, + "public_added_to_group_description": { + "default": "\"{{moderator.name}} has added {{users}} to the Modmail thread.\"", + "description": "This is the message embed content sent to all other recipients when someone is added to the thread.", + "examples": [ + "`{prefix}config set public_added_to_group_description Welcome {users}!`" + ], + "notes": [ + "You may use the `{{moderator}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that added the user.", + "When anonadduser is used, `public_added_to_group_description_anon` is used instead.", + "The private_ variant is used when sending to the new user.", + "See also: `public_added_to_group_title`, `private_added_to_group_description`" + ] + }, + "public_added_to_group_description_anon": { + "default": "\"A moderator has added {{users}} to the Modmail thread.\"", + "description": "This is the message embed content sent to all other recipients when someone is added to the thread when adduser is used anonymously.", + "examples": [ + "`{prefix}config set public_added_to_group_description_anon Any message sent here will be sent to all other thread recipients.`" + ], + "notes": [ + "When adduser (no anon) is used, `public_added_to_group_description` is used instead.", + "The private_ variant is used when sending to the new user.", + "See also: `public_added_to_group_title`, `private_added_to_group_description_anon`" + ] + }, + "private_removed_from_group_title": { + "default": "Removed From Thread (Group)", + "description": "This is the message embed title sent to the recipient that is just removed from a thread.", + "examples": [ + "`{prefix}config set private_removed_from_group_title Welcome to this new group thread!`" + ], + "notes": [ + "The public_ variant is used when sending to other thread recipients.", + "See also: `private_removed_from_group_description`, `public_removed_from_group_title`" + ] + }, + "private_removed_from_group_description": { + "default": "\"{{moderator.name}} has removed you from the Modmail thread.\"", + "description": "This is the message embed content sent to the recipient that is just removed from a thread.", + "examples": [ + "`{prefix}config set private_removed_from_group_description Bye`" + ], + "notes": [ + "You may use the `{{moderator}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that added the user.", + "When anonremoveuser is used, `private_removed_from_group_description_anon` is used instead.", + "The public_ variant is used when sending to other thread recipients.", + "See also: `private_removed_from_group_title`, `public_removed_from_group_description`" + ] + }, + "private_removed_from_group_description_anon": { + "default": "A moderator has removed you from the Modmail thread.", + "description": "This is the message embed content sent to the recipient that is just removed from a thread when removeuser is used anonymously.", + "examples": [ + "`{prefix}config set private_removed_from_group_description_anon You are permenantly removed from this thread.`" + ], + "notes": [ + "When adduser (no anon) is used, `private_removed_from_group_description` is used instead.", + "The public_ variant is used when sending to other thread recipients.", + "See also: `private_removed_from_group_title`, `public_removed_from_group_description_anon`" + ] + }, + "public_removed_from_group_title": { + "default": "User Removed", + "description": "This is the message embed title sent to all other recipients when someone is removed from the thread.", + "examples": [ + "`{prefix}config set public_removed_from_group_title User is now gone!`" + ], + "notes": [ + "The private_ variant is used when sending to the new user.", + "See also: `private_removed_from_group_title`, `private_removed_from_group_title`" + ] + }, + "public_removed_from_group_description": { + "default": "\"{{moderator.name}} has removed {{users}} from the Modmail thread.\"", + "description": "This is the message embed content sent to all other recipients when someone is removed from the thread.", + "examples": [ + "`{prefix}config set public_removed_from_group_description Goodbye {users}!`" + ], + "notes": [ + "You may use the `{{moderator}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that added the user.", + "When anonremoveuser is used, `public_removed_from_group_description_anon` is used instead.", + "The private_ variant is used when sending to the new user.", + "See also: `public_removed_from_group_title`, `private_removed_from_group_description`" + ] + }, + "public_removed_from_group_description_anon": { + "default": "\"A moderator has removed {{users}} from the Modmail thread.\"", + "description": "This is the message embed content sent to all other recipients when someone is removed from the thread when removeuser is used anonymously.", + "examples": [ + "`{prefix}config set public_removed_from_group_description_anon Goodbye {users}!`" + ], + "notes": [ + "When adduser (no anon) is used, `public_removed_from_group_description` is used instead.", + "The private_ variant is used when sending to the new user.", + "See also: `public_removed_from_group_title`, `private_removed_from_group_description_anon`" + ] + }, "confirm_thread_creation": { "default": "No", "description": "Ensure users confirm that they want to create a new thread", diff --git a/core/thread.py b/core/thread.py index 29446a32c3..91f30417a8 100644 --- a/core/thread.py +++ b/core/thread.py @@ -60,7 +60,9 @@ def __repr__(self): return f'Thread(recipient="{self.recipient or self.id}", channel={self.channel.id}, other_recipients={len(self._other_recipients)})' def __eq__(self, other): - return self.id == other.id + if isinstance(other, Thread): + return self.id == other.id + return super().__eq__(other) async def wait_until_ready(self) -> None: """Blocks execution until the thread is fully set up.""" @@ -1089,23 +1091,22 @@ async def set_title(self, title: str) -> None: await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") - async def add_user(self, user: typing.Union[discord.Member, discord.User]) -> None: + async def add_users(self, users: typing.List[typing.Union[discord.Member, discord.User]]) -> None: title = match_title(self.channel.topic) user_id = match_user_id(self.channel.topic) - self._other_recipients.append(user) + self._other_recipients += users ids = ",".join(str(i.id) for i in self._other_recipients) await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") - async def remove_user(self, user: typing.Union[discord.Member, discord.User]) -> None: + async def remove_users(self, users: typing.List[typing.Union[discord.Member, discord.User]]) -> None: title = match_title(self.channel.topic) user_id = match_user_id(self.channel.topic) - self._other_recipients.remove(user) + for u in users: + self._other_recipients.remove(u) ids = ",".join(str(i.id) for i in self._other_recipients) await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") - # if user.id in self.manager.cache: - # self.manager.cache.pop(self.id) class ThreadManager: From d240e567bfffb9b80260e913b88256236040f0fb Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 21:17:10 +0800 Subject: [PATCH 0924/1221] Formatting --- cogs/modmail.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index aeace73192..e8e0a7cc02 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -701,7 +701,9 @@ async def adduser(self, ctx, *users: Union[discord.Member, str]): if not silent: em = discord.Embed( title=self.bot.config["private_added_to_group_title"], - description=self.bot.config["private_added_to_group_description"].format(moderator=ctx.author), + description=self.bot.config["private_added_to_group_description"].format( + moderator=ctx.author + ), color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -712,7 +714,9 @@ async def adduser(self, ctx, *users: Union[discord.Member, str]): em = discord.Embed( title=self.bot.config["public_added_to_group_title"], - description=self.bot.config["private_added_to_group_description"].format(moderator=ctx.author, users=', '.join(u.name for u in users)), + description=self.bot.config["private_added_to_group_description"].format( + moderator=ctx.author, users=", ".join(u.name for u in users) + ), color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -767,7 +771,9 @@ async def removeuser(self, ctx, *users: Union[discord.Member, str]): if not silent: em = discord.Embed( title=self.bot.config["private_removed_from_group_title"], - description=self.bot.config["private_removed_from_group_description"].format(moderator=ctx.author), + description=self.bot.config["private_removed_from_group_description"].format( + moderator=ctx.author + ), color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -778,7 +784,9 @@ async def removeuser(self, ctx, *users: Union[discord.Member, str]): em = discord.Embed( title=self.bot.config["public_removed_from_group_title"], - description=self.bot.config["private_removed_from_group_description"].format(moderator=ctx.author, users=', '.join(u.name for u in users)), + description=self.bot.config["private_removed_from_group_description"].format( + moderator=ctx.author, users=", ".join(u.name for u in users) + ), color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -845,7 +853,9 @@ async def anonadduser(self, ctx, *users: Union[discord.Member, str]): em = discord.Embed( title=self.bot.config["public_added_to_group_title"], - description=self.bot.config["private_added_to_group_description_anon"].format(moderator=ctx.author, users=', '.join(u.name for u in users)), + description=self.bot.config["private_added_to_group_description_anon"].format( + moderator=ctx.author, users=", ".join(u.name for u in users) + ), color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -900,7 +910,9 @@ async def anonremoveuser(self, ctx, *users: Union[discord.Member, str]): if not silent: em = discord.Embed( title=self.bot.config["private_removed_from_group_title"], - description=self.bot.config["private_removed_from_group_description_anon"].format(moderator=ctx.author), + description=self.bot.config["private_removed_from_group_description_anon"].format( + moderator=ctx.author + ), color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -922,7 +934,9 @@ async def anonremoveuser(self, ctx, *users: Union[discord.Member, str]): em = discord.Embed( title=self.bot.config["public_removed_from_group_title"], - description=self.bot.config["private_removed_from_group_description"].format(moderator=ctx.author, users=', '.join(u.name for u in users)), + description=self.bot.config["private_removed_from_group_description"].format( + moderator=ctx.author, users=", ".join(u.name for u in users) + ), color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: From ebcfe4ea118ba9907ed403e04f5fbb3d33393027 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 21:43:47 +0800 Subject: [PATCH 0925/1221] Fix formatter, allow add by roles (resolve #3053) --- cogs/modmail.py | 264 +++++++++++++++++++++++++++++------------------- 1 file changed, 161 insertions(+), 103 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index e8e0a7cc02..74b99dde76 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -671,39 +671,60 @@ async def title(self, ctx, *, name: str): await ctx.message.pin() await self.bot.add_reaction(ctx.message, sent_emoji) - @commands.command(usage=" [options]", cooldown_after_parsing=True) + @commands.command(usage=" [options]", cooldown_after_parsing=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @commands.cooldown(1, 600, BucketType.channel) - async def adduser(self, ctx, *users: Union[discord.Member, str]): + async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str]): """Adds a user to a modmail thread `options` can be `silent` or `silently`. """ silent = False - for u in users: + users = [] + for u in users_arg: if isinstance(u, str): if "silent" in u or "silently" in u: silent = True - users.remove(u) # remove all strings so users is a List[Member] - else: - # u is a discord.Member - curr_thread = await self.bot.threads.find(recipient=u) - if curr_thread: - em = discord.Embed( - title="Error", - description=f"{u.mention} is already in a thread: {curr_thread.channel.mention}.", - color=self.bot.error_color, - ) - await ctx.send(embed=em) - ctx.command.reset_cooldown(ctx) - return + elif isinstance(u, discord.Role): + users += u.members + elif isinstance(u, discord.Member): + users.append(u) + + for u in users: + # u is a discord.Member + curr_thread = await self.bot.threads.find(recipient=u) + if curr_thread == ctx.thread: + users.remove(u) + continue + + if curr_thread: + em = discord.Embed( + title="Error", + description=f"{u.mention} is already in a thread: {curr_thread.channel.mention}.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + + if not users: + em = discord.Embed( + title="Error", + description="All users are already in the thread.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + if not silent: + description = self.bot.formatter.format( + self.bot.config["private_added_to_group_description"], moderator=ctx.author + ) em = discord.Embed( title=self.bot.config["private_added_to_group_title"], - description=self.bot.config["private_added_to_group_description"].format( - moderator=ctx.author - ), + description=description, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -712,11 +733,14 @@ async def adduser(self, ctx, *users: Union[discord.Member, str]): for u in users: await u.send(embed=em) + description = self.bot.formatter.format( + self.bot.config["public_added_to_group_description"], + moderator=ctx.author, + users=", ".join(u.name for u in users), + ) em = discord.Embed( title=self.bot.config["public_added_to_group_title"], - description=self.bot.config["private_added_to_group_description"].format( - moderator=ctx.author, users=", ".join(u.name for u in users) - ), + description=description, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -731,49 +755,55 @@ async def adduser(self, ctx, *users: Union[discord.Member, str]): sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) - @commands.command(usage=" [options]", cooldown_after_parsing=True) + @commands.command(usage=" [options]", cooldown_after_parsing=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @commands.cooldown(1, 600, BucketType.channel) - async def removeuser(self, ctx, *users: Union[discord.Member, str]): + async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str]): """Removes a user from a modmail thread `options` can be `silent` or `silently`. """ silent = False - for u in users: + users = [] + for u in users_arg: if isinstance(u, str): if "silent" in u or "silently" in u: silent = True - users.remove(u) # remove all strings so users is a List[Member] - else: - # u is a discord.Member - curr_thread = await self.bot.threads.find(recipient=u) - if ctx.thread != curr_thread: - em = discord.Embed( - title="Error", - description=f"{u.mention} is not in this thread.", - color=self.bot.error_color, - ) - await ctx.send(embed=em) - ctx.command.reset_cooldown(ctx) - return - elif ctx.thread.recipient == u: - em = discord.Embed( - title="Error", - description=f"{u.mention} is the main recipient of the thread and cannot be removed.", - color=self.bot.error_color, - ) - await ctx.send(embed=em) - ctx.command.reset_cooldown(ctx) - return + elif isinstance(u, discord.Role): + users += u.members + elif isinstance(u, discord.Member): + users.append(u) + + for u in users: + # u is a discord.Member + curr_thread = await self.bot.threads.find(recipient=u) + if ctx.thread != curr_thread: + em = discord.Embed( + title="Error", + description=f"{u.mention} is not in this thread.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + elif ctx.thread.recipient == u: + em = discord.Embed( + title="Error", + description=f"{u.mention} is the main recipient of the thread and cannot be removed.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return if not silent: + description = self.bot.formatter.format( + self.bot.config["private_removed_from_group_description"], moderator=ctx.author + ) em = discord.Embed( title=self.bot.config["private_removed_from_group_title"], - description=self.bot.config["private_removed_from_group_description"].format( - moderator=ctx.author - ), + description=description, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -782,11 +812,14 @@ async def removeuser(self, ctx, *users: Union[discord.Member, str]): for u in users: await u.send(embed=em) + description = self.bot.formatter.format( + self.bot.config["public_removed_from_group_description"], + moderator=ctx.author, + users=", ".join(u.name for u in users), + ) em = discord.Embed( title=self.bot.config["public_removed_from_group_title"], - description=self.bot.config["private_removed_from_group_description"].format( - moderator=ctx.author, users=", ".join(u.name for u in users) - ), + description=description, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -801,33 +834,52 @@ async def removeuser(self, ctx, *users: Union[discord.Member, str]): sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) - @commands.command(usage=" [options]", cooldown_after_parsing=True) + @commands.command(usage=" [options]", cooldown_after_parsing=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @commands.cooldown(1, 600, BucketType.channel) - async def anonadduser(self, ctx, *users: Union[discord.Member, str]): + async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str]): """Adds a user to a modmail thread anonymously `options` can be `silent` or `silently`. """ silent = False - for u in users: + users = [] + for u in users_arg: if isinstance(u, str): if "silent" in u or "silently" in u: silent = True - users.remove(u) # remove all strings so users is a List[Member] - else: - # u is a discord.Member - curr_thread = await self.bot.threads.find(recipient=u) - if curr_thread: - em = discord.Embed( - title="Error", - description=f"{u.mention} is already in a thread: {curr_thread.channel.mention}.", - color=self.bot.error_color, - ) - await ctx.send(embed=em) - ctx.command.reset_cooldown(ctx) - return + elif isinstance(u, discord.Role): + users += u.members + elif isinstance(u, discord.Member): + users.append(u) + + for u in users: + curr_thread = await self.bot.threads.find(recipient=u) + if curr_thread == ctx.thread: + users.remove(u) + continue + + if curr_thread: + em = discord.Embed( + title="Error", + description=f"{u.mention} is already in a thread: {curr_thread.channel.mention}.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + + if not users: + em = discord.Embed( + title="Error", + description="All users are already in the thread.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + if not silent: em = discord.Embed( title=self.bot.config["private_added_to_group_title"], @@ -851,11 +903,13 @@ async def anonadduser(self, ctx, *users: Union[discord.Member, str]): for u in users: await u.send(embed=em) + description = self.bot.formatter.format( + self.bot.config["public_added_to_group_description_anon"], + users=", ".join(u.name for u in users), + ) em = discord.Embed( title=self.bot.config["public_added_to_group_title"], - description=self.bot.config["private_added_to_group_description_anon"].format( - moderator=ctx.author, users=", ".join(u.name for u in users) - ), + description=description, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -870,49 +924,51 @@ async def anonadduser(self, ctx, *users: Union[discord.Member, str]): sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) - @commands.command(usage=" [options]", cooldown_after_parsing=True) + @commands.command(usage=" [options]", cooldown_after_parsing=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() @commands.cooldown(1, 600, BucketType.channel) - async def anonremoveuser(self, ctx, *users: Union[discord.Member, str]): + async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str]): """Removes a user from a modmail thread anonymously `options` can be `silent` or `silently`. """ silent = False - for u in users: + users = [] + for u in users_arg: if isinstance(u, str): if "silent" in u or "silently" in u: silent = True - users.remove(u) # remove all strings so users is a List[Member] - else: - # u is a discord.Member - curr_thread = await self.bot.threads.find(recipient=u) - if ctx.thread != curr_thread: - em = discord.Embed( - title="Error", - description=f"{u.mention} is not in this thread.", - color=self.bot.error_color, - ) - await ctx.send(embed=em) - ctx.command.reset_cooldown(ctx) - return - elif ctx.thread.recipient == u: - em = discord.Embed( - title="Error", - description=f"{u.mention} is the main recipient of the thread and cannot be removed.", - color=self.bot.error_color, - ) - await ctx.send(embed=em) - ctx.command.reset_cooldown(ctx) - return + elif isinstance(u, discord.Role): + users += u.members + elif isinstance(u, discord.Member): + users.append(u) + + for u in users: + curr_thread = await self.bot.threads.find(recipient=u) + if ctx.thread != curr_thread: + em = discord.Embed( + title="Error", + description=f"{u.mention} is not in this thread.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + elif ctx.thread.recipient == u: + em = discord.Embed( + title="Error", + description=f"{u.mention} is the main recipient of the thread and cannot be removed.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return if not silent: em = discord.Embed( title=self.bot.config["private_removed_from_group_title"], - description=self.bot.config["private_removed_from_group_description_anon"].format( - moderator=ctx.author - ), + description=self.bot.config["private_removed_from_group_description_anon"], color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: @@ -932,11 +988,13 @@ async def anonremoveuser(self, ctx, *users: Union[discord.Member, str]): for u in users: await u.send(embed=em) + description = self.bot.formatter.format( + self.bot.config["public_removed_from_group_description_anon"], + users=", ".join(u.name for u in users), + ) em = discord.Embed( title=self.bot.config["public_removed_from_group_title"], - description=self.bot.config["private_removed_from_group_description"].format( - moderator=ctx.author, users=", ".join(u.name for u in users) - ), + description=description, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: From 835390b80c8f4a36bd19aa77d19370b899f5c8b6 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 21:53:04 +0800 Subject: [PATCH 0926/1221] Remove tagging and case insensitive plugins --- plugins/registry.json | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 0ba98f499c..a805360b57 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1,13 +1,4 @@ { - "case_insensitive_snippets": { - "repository": "python-discord/modmail-plugins", - "branch": "main", - "description": "Allow snippets to be ran even if with the wrong case. For example, ?Dm-RePort will be recognized as ?dm-report.", - "bot_version": "2.20.1", - "title": "Case insensitive snippets", - "icon_url": "https://i.imgur.com/kaJYlMB.png", - "thumbnail_url": "https://i.imgur.com/kaJYlMB.png" - }, "close_message": { "repository": "python-discord/modmail-plugins", "branch": "main", @@ -35,15 +26,6 @@ "icon_url": "https://i.imgur.com/FtRQveT.png", "thumbnail_url": "https://i.imgur.com/FtRQveT.png" }, - "tagging": { - "repository": "python-discord/modmail-plugins", - "branch": "main", - "description": "Add a ?tag command capable of adding a $message| header to the channel name.", - "bot_version": "2.20.1", - "title": "Tagging", - "icon_url": "https://i.imgur.com/WajSLB1.png", - "thumbnail_url": "https://i.imgur.com/WajSLB1.png" - }, "dragory-migrate": { "repository": "kyb3r/modmail-plugins", "branch": "master", From fe11759e5b3e11dc5f7623904a7a98c5e6d61d09 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 6 Aug 2021 21:54:28 +0800 Subject: [PATCH 0927/1221] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d398e71f2f..28e5151b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ v3.10 adds group conversations while resolving othre bugs and QOL changes. It is - `?contact` now works properly with both category and silent. ([GH #3076](https://github.com/kyb3r/modmail/issues/3076)) - `close_on_leave_reason` now works properly when `close_on_leave` is enabled. ([GH #3075](https://github.com/kyb3r/modmail/issues/3075)) - Invalid arguments are now properly catched and a proper error message is sent. +- Update database after resetting/purging all plugins. ([GH #3011](https://github.com/kyb3r/modmail/pull/3011)) ## Internal From 1a885bc56ef82e87fdec72ed540e3fb021567069 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 20:14:32 +0800 Subject: [PATCH 0928/1221] Use highest hoisted role as default tag, resolves #3014 --- CHANGELOG.md | 3 ++- README.md | 2 +- bot.py | 2 +- cogs/modmail.py | 4 ++-- core/thread.py | 5 +++-- core/utils.py | 8 ++++++++ pyproject.toml | 2 +- 7 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e5151b07..a18c8d8eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.10.0-dev4 +# v3.10.0-dev5 v3.10 adds group conversations while resolving othre bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. @@ -18,6 +18,7 @@ v3.10 adds group conversations while resolving othre bugs and QOL changes. It is - Ability to have group conversations. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) - Snippets are invoked case insensitively. ([GH #3077](https://github.com/kyb3r/modmail/issues/3077), [PR #3080](https://github.com/kyb3r/modmail/pull/3080)) +- Default tags now use top hoisted role. ([GH #3014](https://github.com/kyb3r/modmail/issues/3014)) ## Fixed diff --git a/README.md b/README.md index 7659898399..42a8f812ec 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 26fac619d2..9a61101707 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "v3.10.0-dev4" +__version__ = "v3.10.0-dev5" import asyncio diff --git a/cogs/modmail.py b/cogs/modmail.py index 74b99dde76..c7be64e046 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -891,7 +891,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, tag = self.bot.config["mod_tag"] if tag is None: - tag = str(ctx.author.top_role) + tag = str(get_top_hoisted_role(ctx.author)) name = self.bot.config["anon_username"] if name is None: name = tag @@ -976,7 +976,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro tag = self.bot.config["mod_tag"] if tag is None: - tag = str(ctx.author.top_role) + tag = str(get_top_hoisted_role(ctx.author)) name = self.bot.config["anon_username"] if name is None: name = tag diff --git a/core/thread.py b/core/thread.py index 91f30417a8..b2adbffe76 100644 --- a/core/thread.py +++ b/core/thread.py @@ -22,6 +22,7 @@ match_other_recipients, truncate, format_channel_name, + get_top_hoisted_role, ) logger = getLogger(__name__) @@ -888,7 +889,7 @@ async def send( # Anonymously sending to the user. tag = self.bot.config["mod_tag"] if tag is None: - tag = str(author.top_role) + tag = str(get_top_hoisted_role(author)) name = self.bot.config["anon_username"] if name is None: name = tag @@ -1005,7 +1006,7 @@ async def send( elif not anonymous: mod_tag = self.bot.config["mod_tag"] if mod_tag is None: - mod_tag = str(message.author.top_role) + mod_tag = str(get_top_hoisted_role(message.author)) embed.set_footer(text=mod_tag) # Normal messages else: embed.set_footer(text=self.bot.config["anon_tag"]) diff --git a/core/utils.py b/core/utils.py index 173eda25d5..8f4e1a01ff 100644 --- a/core/utils.py +++ b/core/utils.py @@ -32,6 +32,7 @@ "escape_code_block", "format_channel_name", "tryint", + "get_top_hoisted_role", ] @@ -394,3 +395,10 @@ def tryint(x): return int(x) except (ValueError, TypeError): return x + + +def get_top_hoisted_role(member: discord.Member): + roles = sorted(member.roles, key=lambda r: r.position, reverse=True) + for role in roles: + if role.hoist: + return role diff --git a/pyproject.toml b/pyproject.toml index e038685a61..1d97e42e33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.0-dev4' +version = '3.10.0-dev5' description = "Modmail is similar to Reddit's 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 3407e0510208a5b509d3f5059ef5f3d6598edc11 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 20:24:11 +0800 Subject: [PATCH 0929/1221] Reload thread cache only when it's the first on_ready trigger. Resolves #3037 --- CHANGELOG.md | 1 + bot.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a18c8d8eb7..d8768247c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ v3.10 adds group conversations while resolving othre bugs and QOL changes. It is - `thread.reply` now returns mod_message, user_message1, user_message2... It is no longer limited at a size 2 tuple. Potentially breaking if plugins depend on this behaviour. - Fix return types, type hints, and unresolved references ([PR #3009](https://github.com/kyb3r/modmail/pull/3009)) +- Reload thread cache only when it's the first on_ready trigger. ([GH #3037](https://github.com/kyb3r/modmail/issues/3037)) # v3.9.5 diff --git a/bot.py b/bot.py index 9a61101707..8aa3f00d1a 100644 --- a/bot.py +++ b/bot.py @@ -74,6 +74,7 @@ def __init__(self): self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] self._connected = asyncio.Event() self.start_time = datetime.utcnow() + self._started = False self.config = ConfigManager(self) self.config.populate_cache() @@ -534,6 +535,13 @@ async def on_ready(self): logger.error("Logging out due to invalid GUILD_ID.") return await self.close() + if self._started: + # Bot has started before + logger.line() + logger.warning("Bot restarted due to internal discord reloading.") + logger.line() + return + logger.line() logger.debug("Client ready.") logger.info("Logged in as: %s", self.user) @@ -634,6 +642,8 @@ async def on_ready(self): ) logger.warning("If the external servers are valid, you may ignore this message.") + self._started = True + async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() From 20b31f8e8b5497943513997fef788d72ae668438 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sat, 7 Aug 2021 20:48:35 +0800 Subject: [PATCH 0930/1221] Resolve linked messages issues, improve group functionality (bugfixes), resolves #3041 --- CHANGELOG.md | 1 + bot.py | 17 ++--- core/thread.py | 165 +++++++++++++++++++++++++++++-------------------- core/utils.py | 79 +++++++++++++++++++++-- 4 files changed, 179 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8768247c9..6952839164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ v3.10 adds group conversations while resolving othre bugs and QOL changes. It is - `close_on_leave_reason` now works properly when `close_on_leave` is enabled. ([GH #3075](https://github.com/kyb3r/modmail/issues/3075)) - Invalid arguments are now properly catched and a proper error message is sent. - Update database after resetting/purging all plugins. ([GH #3011](https://github.com/kyb3r/modmail/pull/3011)) +- `thread_auto_close` timer now only resets on non-note and replies from mods. ([GH #3030](https://github.com/kyb3r/modmail/issues/3030)) ## Internal diff --git a/bot.py b/bot.py index 8aa3f00d1a..b1b4ad046c 100644 --- a/bot.py +++ b/bot.py @@ -1265,7 +1265,7 @@ async def handle_reaction_events(self, payload): 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) + linked_messages = 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 @@ -1274,19 +1274,19 @@ async def handle_reaction_events(self, payload): if not thread: return try: - _, *linked_message = await thread.find_linked_messages(message.id, either_direction=True) + _, *linked_messages = 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 - if self.config["transfer_reactions"] and linked_message is not [None]: + if self.config["transfer_reactions"] and linked_messages is not [None]: if payload.event_type == "REACTION_ADD": - for msg in linked_message: + for msg in linked_messages: await self.add_reaction(msg, reaction) await self.add_reaction(message, reaction) else: try: - for msg in linked_message: + for msg in linked_messages: await msg.remove_reaction(reaction, self.user) await message.remove_reaction(reaction, self.user) except (discord.HTTPException, discord.InvalidArgument) as e: @@ -1432,13 +1432,6 @@ async def on_message_delete(self, message): if not thread: return - audit_logs = self.modmail_guild.audit_logs(limit=10, action=discord.AuditLogAction.message_delete) - - entry = await audit_logs.find(lambda a: a.target == self.user) - - if entry is None: - return - try: await thread.delete_message(message, note=False) embed = discord.Embed(description="Successfully deleted message.", color=self.main_color) diff --git a/core/thread.py b/core/thread.py index b2adbffe76..89cdceb903 100644 --- a/core/thread.py +++ b/core/thread.py @@ -21,8 +21,9 @@ match_user_id, match_other_recipients, truncate, - format_channel_name, get_top_hoisted_role, + create_thread_channel, + get_joint_id, ) logger = getLogger(__name__) @@ -36,7 +37,7 @@ def __init__( manager: "ThreadManager", recipient: typing.Union[discord.Member, discord.User, int], channel: typing.Union[discord.DMChannel, discord.TextChannel] = None, - other_recipients: typing.List[typing.Union[discord.Member, discord.User]] = [], + other_recipients: typing.List[typing.Union[discord.Member, discord.User]] = None, ): self.manager = manager self.bot = manager.bot @@ -48,7 +49,7 @@ def __init__( raise CommandError("Recipient cannot be a bot.") self._id = recipient.id self._recipient = recipient - self._other_recipients = other_recipients + self._other_recipients = other_recipients or [] self._channel = channel self.genesis_message = None self._ready_event = asyncio.Event() @@ -127,9 +128,13 @@ async def from_channel(cls, manager: "ThreadManager", channel: discord.TextChann else: recipient = manager.bot.get_user(recipient_id) or await manager.bot.fetch_user(recipient_id) - other_recipients = match_other_recipients(channel.topic) - for n, uid in enumerate(other_recipients): - other_recipients[n] = manager.bot.get_user(uid) or await manager.bot.fetch_user(uid) + other_recipients = [] + for uid in match_other_recipients(channel.topic): + try: + other_recipient = manager.bot.get_user(uid) or await manager.bot.fetch_user(uid) + except discord.NotFound: + continue + other_recipients.append(other_recipient) thread = cls(manager, recipient or recipient_id, channel, other_recipients) @@ -149,33 +154,19 @@ async def setup(self, *, creator=None, category=None, initial_message=None): overwrites = None try: - channel = await self.bot.modmail_guild.create_text_channel( - name=format_channel_name(self.bot, recipient), - category=category, - overwrites=overwrites, - reason="Creating a thread channel.", - ) - except discord.HTTPException as e: - # try again but null-discrim (name could be banned) - try: - channel = await self.bot.modmail_guild.create_text_channel( - name=format_channel_name(self.bot, recipient, force_null=True), - category=category, - overwrites=overwrites, - reason="Creating a thread channel.", - ) - 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) + channel = await create_thread_channel(self.bot, recipient, category, overwrites) + 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) - embed = discord.Embed(color=self.bot.error_color) - embed.title = "Error while trying to create a thread." - embed.description = str(e) - embed.add_field(name="Recipient", value=recipient.mention) + embed = discord.Embed(color=self.bot.error_color) + embed.title = "Error while trying to create a thread." + embed.description = str(e) + embed.add_field(name="Recipient", value=recipient.mention) - if self.bot.log_channel is not None: - await self.bot.log_channel.send(embed=embed) - return + if self.bot.log_channel is not None: + await self.bot.log_channel.send(embed=embed) + return self._channel = channel @@ -209,7 +200,7 @@ async def send_genesis_message(): logger.error("Failed unexpectedly:", exc_info=True) async def send_recipient_genesis_message(): - # Once thread is ready, tell the recipient. + # Once thread is ready, tell the recipient (don't send if using contact on others) thread_creation_response = self.bot.config["thread_creation_response"] embed = discord.Embed( @@ -585,7 +576,7 @@ async def find_linked_messages( either_direction: bool = False, message1: discord.Message = None, note: bool = True, - ) -> typing.Tuple[discord.Message, typing.Optional[discord.Message]]: + ) -> typing.Tuple[discord.Message, typing.List[typing.Optional[discord.Message]]]: if message1 is not None: if not message1.embeds or not message1.embeds[0].author.url or message1.author != self.bot.user: raise ValueError("Malformed thread message.") @@ -698,49 +689,84 @@ async def delete_message( if tasks: await asyncio.gather(*tasks) - async def find_linked_message_from_dm(self, message, either_direction=False): - if either_direction and message.embeds and message.embeds[0].author.url: - compare_url = message.embeds[0].author.url - compare_id = compare_url.split("#")[-1] - else: - compare_url = None - compare_id = None + async def find_linked_message_from_dm( + self, message, either_direction=False + ) -> typing.List[discord.Message]: + + joint_id = None + if either_direction: + joint_id = get_joint_id(message) + # could be None too, if that's the case we'll reassign this variable from + # thread message we fetch in the next step + linked_messages = [] if self.channel is not None: - async for linked_message in self.channel.history(): - if not linked_message.embeds: + async for msg in self.channel.history(): + if not msg.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] - if not msg_id.isdigit(): + msg_joint_id = get_joint_id(msg) + if msg_joint_id is None: continue - msg_id = int(msg_id) - if int(msg_id) == message.id: - return linked_message - if compare_id is not None and compare_id.isdigit(): - if int(msg_id) == int(compare_id): - return linked_message + if msg_joint_id == message.id: + linked_messages.append(msg) + break + if joint_id is not None and msg_joint_id == joint_id: + linked_messages.append(msg) + break + else: + raise ValueError("Thread channel message not found.") + else: raise ValueError("Thread channel message not found.") + if joint_id is None: + joint_id = get_joint_id(linked_messages[0]) + if joint_id is None: + # still None, supress this and return the thread message + logger.error("Malformed thread message.") + return linked_messages + + for user in self.recipients: + if user.dm_channel == message.channel: + continue + async for other_msg in user.history(): + if either_direction: + if other_msg.id == joint_id: + linked_messages.append(other_msg) + break + + if not other_msg.embeds: + continue + + other_joint_id = get_joint_id(other_msg) + if other_joint_id is not None and other_joint_id == joint_id: + linked_messages.append(other_msg) + break + else: + logger.error("Linked message from recipient %s not found.", user) + + return linked_messages + async def edit_dm_message(self, message: discord.Message, content: str) -> None: try: - linked_message = await self.find_linked_message_from_dm(message) + linked_messages = await self.find_linked_message_from_dm(message) except ValueError: logger.warning("Failed to edit message.", exc_info=True) raise - 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)) - async def note(self, message: discord.Message, persistent=False, thread_creation=False) -> None: + for msg in linked_messages: + embed = msg.embeds[0] + if isinstance(msg.channel, discord.TextChannel): + # just for thread channel, we put the old message in embed field + embed.add_field(name="**Edited, former message:**", value=embed.description) + embed.description = content + await asyncio.gather(self.bot.api.edit_message(message.id, content), msg.edit(embed=embed)) + + async def note( + self, message: discord.Message, persistent=False, thread_creation=False + ) -> discord.Message: if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) @@ -758,7 +784,9 @@ async def note(self, message: discord.Message, persistent=False, thread_creation return msg - async def reply(self, message: discord.Message, anonymous: bool = False, plain: bool = False) -> None: + async def reply( + self, message: discord.Message, anonymous: bool = False, plain: bool = False + ) -> typing.Tuple[discord.Message, discord.Message]: if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) if not any(g.get_member(self.id) for g in self.bot.guilds): @@ -854,7 +882,8 @@ async def send( thread_creation: bool = False, ) -> None: - self.bot.loop.create_task(self._restart_close_timer()) # Start or restart thread auto close + if not note and from_mod: + self.bot.loop.create_task(self._restart_close_timer()) # Start or restart thread auto close if self.close_task is not None: # cancel closing if a thread message is sent. @@ -1208,9 +1237,13 @@ async def _find_from_channel(self, channel): except discord.NotFound: recipient = None - other_recipients = match_other_recipients(channel.topic) - for n, uid in enumerate(other_recipients): - other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid) + other_recipients = [] + for uid in match_other_recipients(channel.topic): + try: + other_recipient = self.bot.get_user(uid) or await self.bot.fetch_user(uid) + except discord.NotFound: + continue + other_recipients.append(other_recipient) if recipient is None: thread = Thread(self, user_id, channel, other_recipients) diff --git a/core/utils.py b/core/utils.py index 8f4e1a01ff..3b75e11014 100644 --- a/core/utils.py +++ b/core/utils.py @@ -33,6 +33,7 @@ "format_channel_name", "tryint", "get_top_hoisted_role", + "get_joint_id", ] @@ -224,7 +225,7 @@ def cleanup_code(content: str) -> str: TOPIC_UID_REGEX = re.compile(r"\bUser ID:\s*(\d{17,21})\b", flags=re.IGNORECASE) -def match_title(text: str) -> int: +def match_title(text: str) -> str: """ Matches a title in the format of "Title: XXXX" @@ -236,7 +237,7 @@ def match_title(text: str) -> int: Returns ------- Optional[str] - The title if found + The title if found. """ match = TOPIC_TITLE_REGEX.search(text) if match is not None: @@ -263,7 +264,7 @@ def match_user_id(text: str) -> int: return -1 -def match_other_recipients(text: str) -> int: +def match_other_recipients(text: str) -> typing.List[int]: """ Matches a title in the format of "Other Recipients: XXXX,XXXX" @@ -274,8 +275,8 @@ def match_other_recipients(text: str) -> int: Returns ------- - Optional[str] - The title if found + List[int] + The list of other recipients IDs. """ match = TOPIC_OTHER_RECIPIENTS_REGEX.search(text) if match is not None: @@ -402,3 +403,71 @@ def get_top_hoisted_role(member: discord.Member): for role in roles: if role.hoist: return role + + +async def create_thread_channel(bot, recipient, category, overwrites, *, name=None, errors_raised=[]): + name = name or format_channel_name(bot, recipient) + try: + channel = await bot.modmail_guild.create_text_channel( + name=name, + category=category, + overwrites=overwrites, + reason="Creating a thread channel.", + ) + except discord.HTTPException as e: + if (e.text, (category, name)) in errors_raised: + # Just raise the error to prevent infinite recursion after retrying + raise + + errors_raised.append((e.text, (category, name))) + + if "Maximum number of channels in category reached" in e.text: + fallback_id = bot.config["fallback_category_id"] + if fallback_id: + fallback = discord.utils.get(cat.guild.categories, id=int(fallback_id)) + if fallback and len(fallback.channels) < 49: + category = fallback + + if not category: + category = await category.clone(name="Fallback Modmail") + bot.config.set("fallback_category_id", str(category.id)) + await bot.config.update() + + return await create_thread_channel( + bot, recipient, category, overwrites, errors_raised=errors_raised + ) + + if "Contains words not allowed" in e.text: + # try again but null-discrim (name could be banned) + return await create_thread_channel( + bot, + recipient, + category, + overwrites, + name=format_channel_name(bot, recipient, force_null=True), + errors_raised=errors_raised, + ) + + raise + + return channel + + +def get_joint_id(message: discord.Message) -> typing.Optional[int]: + """ + Get the joint ID from `discord.Embed().author.url`. + Parameters + ----------- + message : discord.Message + The discord.Message object. + Returns + ------- + int + The joint ID if found. Otherwise, None. + """ + if message.embeds: + try: + return int(getattr(message.embeds[0].author, "url", "").split("#")[-1]) + except ValueError: + pass + return None From ddbee35bc15e858e5b22e622f86ea6b9ea639213 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 21:23:24 +0800 Subject: [PATCH 0931/1221] Persistent notes are now properly deleted,resolve #3013 --- CHANGELOG.md | 2 ++ core/thread.py | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6952839164..e7dc84c3d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ v3.10 adds group conversations while resolving othre bugs and QOL changes. It is - Invalid arguments are now properly catched and a proper error message is sent. - Update database after resetting/purging all plugins. ([GH #3011](https://github.com/kyb3r/modmail/pull/3011)) - `thread_auto_close` timer now only resets on non-note and replies from mods. ([GH #3030](https://github.com/kyb3r/modmail/issues/3030)) +- Deleted messages are now deleted on both ends. ([GH #3041](https://github.com/kyb3r/modmail/issues/3041), [@JerrieAries](https://github.com/kyb3r/modmail/commit/20b31f8e8b5497943513997fef788d72ae668438)) +- Persistent notes are now properly deleted from the database. ([GH #3013](https://github.com/kyb3r/modmail/issues/3013)) ## Internal diff --git a/core/thread.py b/core/thread.py index 89cdceb903..b34e14ed01 100644 --- a/core/thread.py +++ b/core/thread.py @@ -679,13 +679,17 @@ async def delete_message( else: message1, *message2 = await self.find_linked_messages(message, note=note) tasks = [] + if not isinstance(message, discord.Message): tasks += [message1.delete()] - elif message2 is not [None]: - for m2 in message2: + + for m2 in message2: + if m2 is not None: tasks += [m2.delete()] - elif message1.embeds[0].author.name.startswith("Persistent Note"): + + if message1.embeds[0].author.name.startswith("Persistent Note"): tasks += [self.bot.api.delete_note(message1.id)] + if tasks: await asyncio.gather(*tasks) From a71c1c79fa183be387dbe3cea274fa327ab89e35 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 21:32:24 +0800 Subject: [PATCH 0932/1221] Fix changelog formatting --- CHANGELOG.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7dc84c3d1..1ba3fd326b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,19 +8,20 @@ however, insignificant breaking changes do not guarantee a major version bump, s # v3.10.0-dev5 -v3.10 adds group conversations while resolving othre bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. +v3.10 adds group conversations while resolving other bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. -## Breaking +### Breaking - `Thread.recipient` (`str`) is now `Thread.recipients` (`List[str]`). +- `thread.reply` now returns mod_message, user_message1, user_message2... It is no longer limited at a size 2 tuple. -## Added +### Added - Ability to have group conversations. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) - Snippets are invoked case insensitively. ([GH #3077](https://github.com/kyb3r/modmail/issues/3077), [PR #3080](https://github.com/kyb3r/modmail/pull/3080)) - Default tags now use top hoisted role. ([GH #3014](https://github.com/kyb3r/modmail/issues/3014)) -## Fixed +### Fixed - Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/kyb3r/modmail/issues/3022), [PR #3028](https://github.com/kyb3r/modmail/pull/3028)) - Blocked users are now no longer allowed to use `?contact` and react to contact. ([COMMENT #819004157](https://github.com/kyb3r/modmail/issues/2969#issuecomment-819004157), [PR #3027](https://github.com/kyb3r/modmail/pull/3027)) @@ -34,15 +35,14 @@ v3.10 adds group conversations while resolving othre bugs and QOL changes. It is - Deleted messages are now deleted on both ends. ([GH #3041](https://github.com/kyb3r/modmail/issues/3041), [@JerrieAries](https://github.com/kyb3r/modmail/commit/20b31f8e8b5497943513997fef788d72ae668438)) - Persistent notes are now properly deleted from the database. ([GH #3013](https://github.com/kyb3r/modmail/issues/3013)) -## Internal - -- `thread.reply` now returns mod_message, user_message1, user_message2... It is no longer limited at a size 2 tuple. Potentially breaking if plugins depend on this behaviour. -- Fix return types, type hints, and unresolved references ([PR #3009](https://github.com/kyb3r/modmail/pull/3009)) +### Internal +def c +- Fix return types, type hints and unresolved references ([PR #3009](https://github.com/kyb3r/modmail/pull/3009)) - Reload thread cache only when it's the first on_ready trigger. ([GH #3037](https://github.com/kyb3r/modmail/issues/3037)) # v3.9.5 -## Internal +### Internal - Bumped discord.py to v1.7.3, updated all other packages to latest. - More debug log files are now kept. @@ -50,28 +50,28 @@ v3.10 adds group conversations while resolving othre bugs and QOL changes. It is # v3.9.4 -## Fixed +### Fixed - Certain cases where fallback categories were not working as intended. ([GH #3002](https://github.com/kyb3r/modmail/issues/3002), [PR #3003](https://github.com/kyb3r/modmail/pull/3003)) - There is now a proper message when trying to contact a bot. -## Improved +### Improved - `?mention` can now be disabled with `?mention disable`. ([PR #2993](https://github.com/kyb3r/modmail/pull/2993/files)) - `?mention` now allows vague entries such as `everyone` or `all`. ([PR #2993](https://github.com/kyb3r/modmail/pull/2993/files)) -## Internal +### Internal - Change heroku python version to 3.9.4 ([PR #3001](https://github.com/kyb3r/modmail/pull/3001)) # v3.9.3 -## Added +### Added - New config: `use_user_id_channel_name`, when set to TRUE, channel names would get created with the recipient's ID instead of their name and discriminator. - This is now an option to better suit the needs of servers in Server Discovery -## Internal +### Internal - Signature of `format_channel_name` in core/util.py changed to: - `format_channel_name(bot, author, exclude_channel=None, force_null=False)` From 476994a7cb22c586dc2d44ef85b1d2ac36446eb4 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 21:33:54 +0800 Subject: [PATCH 0933/1221] Fix changelog formatting --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ba3fd326b..cfdb98fce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,6 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is - Persistent notes are now properly deleted from the database. ([GH #3013](https://github.com/kyb3r/modmail/issues/3013)) ### Internal -def c - Fix return types, type hints and unresolved references ([PR #3009](https://github.com/kyb3r/modmail/pull/3009)) - Reload thread cache only when it's the first on_ready trigger. ([GH #3037](https://github.com/kyb3r/modmail/issues/3037)) From 68ede9d5d41448a76666ca47ffd816fa83f3fc2d Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 21:34:03 +0800 Subject: [PATCH 0934/1221] Fix changelog formatting --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfdb98fce3..5cfaddb926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is - Persistent notes are now properly deleted from the database. ([GH #3013](https://github.com/kyb3r/modmail/issues/3013)) ### Internal + - Fix return types, type hints and unresolved references ([PR #3009](https://github.com/kyb3r/modmail/pull/3009)) - Reload thread cache only when it's the first on_ready trigger. ([GH #3037](https://github.com/kyb3r/modmail/issues/3037)) From daf04b053be29b615b783e92992ec16dc5df2297 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 22:06:50 +0800 Subject: [PATCH 0935/1221] New thread related config, resolves #3072 --- CHANGELOG.md | 3 +- README.md | 2 +- bot.py | 2 +- cogs/modmail.py | 16 +++++---- core/config.py | 15 +++++--- core/config_help.json | 79 ++++++++++++++++++++++++++++++++++++++++--- core/thread.py | 31 ++++++++++------- pyproject.toml | 2 +- 8 files changed, 119 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cfaddb926..93ee687cf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.10.0-dev5 +# v3.10.0-dev6 v3.10 adds group conversations while resolving other bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. @@ -20,6 +20,7 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is - Ability to have group conversations. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) - Snippets are invoked case insensitively. ([GH #3077](https://github.com/kyb3r/modmail/issues/3077), [PR #3080](https://github.com/kyb3r/modmail/pull/3080)) - Default tags now use top hoisted role. ([GH #3014](https://github.com/kyb3r/modmail/issues/3014)) +- New thread-related config - `thread_show_roles`, `thread_show_account_age`, `thread_show_join_age`, `thread_cancelled`, `thread_creation_contact_title`, `thread_creation_self_contact_response`, `thread_creation_contact_response`. ([GH #3072](https://github.com/kyb3r/modmail/issues/3072)) ### Fixed diff --git a/README.md b/README.md index 42a8f812ec..2751e10fe0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index b1b4ad046c..dd7dbd32f0 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "v3.10.0-dev5" +__version__ = "v3.10.0-dev6" import asyncio diff --git a/cogs/modmail.py b/cogs/modmail.py index c7be64e046..5e9813423c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -720,7 +720,7 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str if not silent: description = self.bot.formatter.format( - self.bot.config["private_added_to_group_description"], moderator=ctx.author + self.bot.config["private_added_to_group_response"], moderator=ctx.author ) em = discord.Embed( title=self.bot.config["private_added_to_group_title"], @@ -734,7 +734,7 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str await u.send(embed=em) description = self.bot.formatter.format( - self.bot.config["public_added_to_group_description"], + self.bot.config["public_added_to_group_response"], moderator=ctx.author, users=", ".join(u.name for u in users), ) @@ -799,7 +799,7 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, if not silent: description = self.bot.formatter.format( - self.bot.config["private_removed_from_group_description"], moderator=ctx.author + self.bot.config["private_removed_from_group_response"], moderator=ctx.author ) em = discord.Embed( title=self.bot.config["private_removed_from_group_title"], @@ -813,7 +813,7 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, await u.send(embed=em) description = self.bot.formatter.format( - self.bot.config["public_removed_from_group_description"], + self.bot.config["public_removed_from_group_response"], moderator=ctx.author, users=", ".join(u.name for u in users), ) @@ -1385,12 +1385,14 @@ async def contact( if not silent and not self.bot.config.get("thread_contact_silently"): if creator.id == user.id: - description = "You have opened a Modmail thread." + description = self.bot.config["thread_creation_self_contact_response"] else: - description = f"{creator.name} has opened a Modmail thread." + description = self.bot.formatter.format( + self.bot.config["thread_creation_contact_response"], creator=creator + ) em = discord.Embed( - title="New Thread", + title=self.bot.config["thread_creation_contact_title"], description=description, color=self.bot.main_color, ) diff --git a/core/config.py b/core/config.py index 829ce7ff3b..cb350b0b76 100644 --- a/core/config.py +++ b/core/config.py @@ -52,6 +52,10 @@ class ConfigManager: "close_emoji": "\N{LOCK}", "use_user_id_channel_name": False, "recipient_thread_close": False, + "thread_show_roles": True, + "thread_show_account_age": True, + "thread_show_join_age": True, + "thread_cancelled": "Cancelled", "thread_auto_close_silently": False, "thread_auto_close": isodate.Duration(), "thread_auto_close_response": "This thread has been closed automatically due to inactivity after {timeout}.", @@ -59,6 +63,9 @@ class ConfigManager: "thread_creation_footer": "Your message has been sent", "thread_contact_silently": False, "thread_self_closable_creation_footer": "Click the lock to close the thread", + "thread_creation_contact_title": "New Thread", + "thread_creation_self_contact_response": "You have opened a Modmail thread.", + "thread_creation_contact_response": "{creator.name} has opened a Modmail thread.", "thread_creation_title": "Thread Created", "thread_close_footer": "Replying will create a new thread", "thread_close_title": "Thread Closed", @@ -85,16 +92,16 @@ class ConfigManager: "anonymous_snippets": False, # group conversations "private_added_to_group_title": "New Thread (Group)", - "private_added_to_group_description": "{moderator.name} has added you to a Modmail thread.", + "private_added_to_group_response": "{moderator.name} has added you to a Modmail thread.", "private_added_to_group_description_anon": "A moderator has added you to a Modmail thread.", "public_added_to_group_title": "New User", - "public_added_to_group_description": "{moderator.name} has added {users} to the Modmail thread.", + "public_added_to_group_response": "{moderator.name} has added {users} to the Modmail thread.", "public_added_to_group_description_anon": "A moderator has added {users} to the Modmail thread.", "private_removed_from_group_title": "Removed From Thread (Group)", - "private_removed_from_group_description": "{moderator.name} has removed you from the Modmail thread.", + "private_removed_from_group_response": "{moderator.name} has removed you from the Modmail thread.", "private_removed_from_group_description_anon": "A moderator has removed you from the Modmail thread.", "public_removed_from_group_title": "User Removed", - "public_removed_from_group_description": "{moderator.name} has removed {users} from the Modmail thread.", + "public_removed_from_group_response": "{moderator.name} has removed {users} from the Modmail thread.", "public_removed_from_group_description_anon": "A moderator has removed {users} from the Modmail thread.", # moderation "recipient_color": str(discord.Color.gold()), diff --git a/core/config_help.json b/core/config_help.json index d6a4627d96..a679ba1c7c 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -266,6 +266,36 @@ "See also: `close_emoji`." ] }, + "thread_show_roles": { + "default": "Yes", + "description": "Shows roles on first message sent in thread channels to mods", + "examples":[ + "`{prefix}config set thread_show_roles no`" + ], + "notes": [ + "See also: `thread_show_account_age`, `thread_show_join_age`." + ] + }, + "thread_show_account_age": { + "default": "Yes", + "description": "Shows account age on first message sent in thread channels to mods", + "examples":[ + "`{prefix}config set thread_show_account_age no`" + ], + "notes": [ + "See also: `thread_show_roles`, `thread_show_join_age`." + ] + }, + "thread_show_join_age": { + "default": "Yes", + "description": "Shows join age on first message sent in thread channels to mods", + "examples":[ + "`{prefix}config set thread_show_join_age no`" + ], + "notes": [ + "See also: `thread_show_account_age`, `thread_show_roles`." + ] + }, "thread_auto_close_silently": { "default": "No", "description": "Setting this configuration will close silently when the thread auto-closes.", @@ -302,6 +332,14 @@ "To disable thread cooldown, do `{prefix}config del thread_cooldown`." ] }, + "thread_cancelled": { + "default": "\"Cancelled\"", + "description": "This is the message to display when a thread times out and creation is cancelled.", + "examples": [ + "`{prefix}config set thread_cancelled Gone.`" + ], + "notes": [] + }, "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.", @@ -359,6 +397,39 @@ "See also: `thread_creation_title`, `thread_creation_response`, `thread_creation_footer`." ] }, + "thread_creation_contact_title": { + "default": "\"New Thread\"", + "description": "This is the message embed title sent to recipients when contacted.", + "examples": [ + "`{prefix}config set thread_creation_contact_title New Message!`" + ], + "notes": [ + "See also: `thread_creation_self_contact_response`, `thread_creation_contact_response`." + ] + }, + "thread_creation_self_contact_response": { + "default": "\"You have opened a Modmail thread.\"", + "description": "This is the message embed description sent to recipients when self-contacted.", + "examples": [ + "`{prefix}config set thread_creation_contact_title You contacted yourself.`" + ], + "notes": [ + "`thread_creation_contact_response` is used when contacted by another user.", + "See also: `thread_creation_contact_title`, `thread_creation_contact_response`." + ] + }, + "thread_creation_contact_response": { + "default": "\"{creator.name} has opened a Modmail thread.\"", + "description": "This is the message embed description sent to recipients when contacted by a mod.", + "examples": [ + "`{prefix}config set thread_creation_contact_response New thread opened.`" + ], + "notes": [ + "You may use the `{{creator}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that created the thread.", + "`thread_creation_self_contact_response` is used when contacted by self.", + "See also: `thread_creation_contact_title`, `thread_creation_self_contact_response`." + ] + }, "thread_creation_title": { "default": "\"Thread Created\"", "description": "This is the message embed title sent to the recipient upon the creation of a new thread.", @@ -722,7 +793,7 @@ "See also: `private_added_to_group_description`, `public_added_to_group_title`" ] }, - "private_added_to_group_description": { + "private_added_to_group_response": { "default": "\"{{moderator.name}} has added you to a Modmail thread.\"", "description": "This is the message embed content sent to the recipient that is just added to a thread.", "examples": [ @@ -758,7 +829,7 @@ "See also: `private_added_to_group_title`, `private_added_to_group_title`" ] }, - "public_added_to_group_description": { + "public_added_to_group_response": { "default": "\"{{moderator.name}} has added {{users}} to the Modmail thread.\"", "description": "This is the message embed content sent to all other recipients when someone is added to the thread.", "examples": [ @@ -794,7 +865,7 @@ "See also: `private_removed_from_group_description`, `public_removed_from_group_title`" ] }, - "private_removed_from_group_description": { + "private_removed_from_group_response": { "default": "\"{{moderator.name}} has removed you from the Modmail thread.\"", "description": "This is the message embed content sent to the recipient that is just removed from a thread.", "examples": [ @@ -830,7 +901,7 @@ "See also: `private_removed_from_group_title`, `private_removed_from_group_title`" ] }, - "public_removed_from_group_description": { + "public_removed_from_group_response": { "default": "\"{{moderator.name}} has removed {{users}} from the Modmail thread.\"", "description": "This is the message embed content sent to all other recipients when someone is removed from the thread.", "examples": [ diff --git a/core/thread.py b/core/thread.py index b34e14ed01..a013ed8eff 100644 --- a/core/thread.py +++ b/core/thread.py @@ -286,7 +286,7 @@ def _format_info_embed(self, user, log_url, log_count, color): # key = log_url.split('/')[-1] role_names = "" - if member is not None: + if member is not None and self.bot.config["thread_show_roles"]: sep_server = self.bot.using_multiple_server_setup separator = ", " if sep_server else " " @@ -309,13 +309,11 @@ def _format_info_embed(self, user, log_url, log_count, color): role_names = separator.join(roles) created = str((time - user.created_at).days) - embed = discord.Embed( - color=color, description=f"{user.mention} was created {days(created)}", timestamp=time - ) + user_info = [] + if self.bot.config["thread_show_account_age"]: + user_info.append(f"was created {days(created)}") - # if not role_names: - # embed.add_field(name='Mention', value=user.mention) - # embed.add_field(name='Registered', value=created + days(created)) + embed = discord.Embed(color=color, description=user.mention, timestamp=time) if user.dm_channel: footer = f"User ID: {user.id} • DM ID: {user.dm_channel.id}" @@ -328,7 +326,8 @@ def _format_info_embed(self, user, log_url, log_count, color): if member is not None: joined = str((time - member.joined_at).days) # embed.add_field(name='Joined', value=joined + days(joined)) - embed.description += f", joined {days(joined)}" + if self.bot.config["thread_show_join_age"]: + user_info.append(f"joined {days(joined)}") if member.nick: embed.add_field(name="Nickname", value=member.nick, inline=True) @@ -338,10 +337,12 @@ def _format_info_embed(self, user, log_url, log_count, color): else: embed.set_footer(text=f"{footer} • (not in main server)") + embed.description += ", ".join(user_info) + if log_count is not None: - # embed.add_field(name="Past logs", value=f"{log_count}") + connector = "with" if user_info else "has" thread = "thread" if log_count == 1 else "threads" - embed.description += f" with **{log_count or 'no'}** past {thread}." + embed.description += f" {connector} **{log_count or 'no'}** past {thread}." else: embed.description += "." @@ -1336,7 +1337,9 @@ async def create( self.bot.loop.create_task( destination.send( embed=discord.Embed( - title="Cancelled", description="Timed out", color=self.bot.error_color + title=self.bot.config["thread_cancelled"], + description="Timed out", + color=self.bot.error_color, ) ) ) @@ -1344,7 +1347,11 @@ async def create( if str(r.emoji) == deny_emoji: thread.cancelled = True self.bot.loop.create_task( - destination.send(embed=discord.Embed(title="Cancelled", color=self.bot.error_color)) + destination.send( + embed=discord.Embed( + title=self.bot.config["thread_cancelled"], color=self.bot.error_color + ) + ) ) async def remove_reactions(): diff --git a/pyproject.toml b/pyproject.toml index 1d97e42e33..20e1a16ac4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.0-dev5' +version = '3.10.0-dev6' description = "Modmail is similar to Reddit's 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 145788e5e46a909f3c0e4224dce1ba3c89c16c11 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 22:11:54 +0800 Subject: [PATCH 0936/1221] Resolve issues with deleting messages --- bot.py | 3 ++- core/thread.py | 6 +++++- core/utils.py | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index dd7dbd32f0..33508e5eab 100644 --- a/bot.py +++ b/bot.py @@ -1415,11 +1415,12 @@ async def on_message_delete(self, message): if not thread: return try: - message = await thread.find_linked_message_from_dm(message) + message = await thread.find_linked_message_from_dm(message, get_thread_channel=True) except ValueError as e: if str(e) != "Thread channel message not found.": logger.debug("Failed to find linked message to delete: %s", e) return + message = message[0] embed = message.embeds[0] embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=embed.footer.icon_url) await message.edit(embed=embed) diff --git a/core/thread.py b/core/thread.py index a013ed8eff..700654e314 100644 --- a/core/thread.py +++ b/core/thread.py @@ -695,7 +695,7 @@ async def delete_message( await asyncio.gather(*tasks) async def find_linked_message_from_dm( - self, message, either_direction=False + self, message, either_direction=False, get_thread_channel=False ) -> typing.List[discord.Message]: joint_id = None @@ -726,6 +726,10 @@ async def find_linked_message_from_dm( else: raise ValueError("Thread channel message not found.") + if get_thread_channel: + # end early as we only want the main message from thread channel + return linked_messages + if joint_id is None: joint_id = get_joint_id(linked_messages[0]) if joint_id is None: diff --git a/core/utils.py b/core/utils.py index 3b75e11014..30948f1b0a 100644 --- a/core/utils.py +++ b/core/utils.py @@ -467,7 +467,9 @@ def get_joint_id(message: discord.Message) -> typing.Optional[int]: """ if message.embeds: try: - return int(getattr(message.embeds[0].author, "url", "").split("#")[-1]) + url = getattr(message.embeds[0].author, "url", "") + if url: + return int(url.split("#")[-1]) except ValueError: pass return None From 93432988e7fc0cdbcc4489dcf7ef1d0e2514ac06 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 22:20:06 +0800 Subject: [PATCH 0937/1221] use_timestamp_channel_name config --- core/config.py | 6 ++++++ core/utils.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/core/config.py b/core/config.py index cb350b0b76..d8593a02ef 100644 --- a/core/config.py +++ b/core/config.py @@ -51,6 +51,7 @@ class ConfigManager: "blocked_emoji": "\N{NO ENTRY SIGN}", "close_emoji": "\N{LOCK}", "use_user_id_channel_name": False, + "use_timestamp_channel_name": False, "recipient_thread_close": False, "thread_show_roles": True, "thread_show_account_age": True, @@ -179,6 +180,7 @@ class ConfigManager: booleans = { "use_user_id_channel_name", + "use_timestamp_channel_name", "user_typing", "mod_typing", "reply_without_command", @@ -203,6 +205,10 @@ class ConfigManager: "update_notifications", "thread_contact_silently", "anonymous_snippets", + "recipient_thread_close", + "thread_show_roles", + "thread_show_account_age", + "thread_show_join_age", } enums = { diff --git a/core/utils.py b/core/utils.py index 30948f1b0a..5ae1977f6e 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,4 +1,5 @@ import base64 +from datetime import datetime import functools import re import string @@ -373,6 +374,8 @@ def format_channel_name(bot, author, exclude_channel=None, force_null=False): else: if bot.config["use_user_id_channel_name"]: name = new_name = str(author.id) + elif bot.config["use_timestamp_channel_name"]: + name = new_name = datetime.utcnow().isoformat(sep='-', timesep='minutes') else: name = author.name.lower() if force_null: From 0ebf364a8350a64c60be3994601d87fa9945dafc Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 22:20:17 +0800 Subject: [PATCH 0938/1221] formatting --- core/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/utils.py b/core/utils.py index 5ae1977f6e..cf5aac5ec2 100644 --- a/core/utils.py +++ b/core/utils.py @@ -375,7 +375,7 @@ def format_channel_name(bot, author, exclude_channel=None, force_null=False): if bot.config["use_user_id_channel_name"]: name = new_name = str(author.id) elif bot.config["use_timestamp_channel_name"]: - name = new_name = datetime.utcnow().isoformat(sep='-', timesep='minutes') + name = new_name = datetime.utcnow().isoformat(sep="-", timesep="minutes") else: name = author.name.lower() if force_null: From fcc3fad65c411c5d012b19c4741695de51af354a Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 7 Aug 2021 22:27:16 +0800 Subject: [PATCH 0939/1221] add documentation and fix timestamp related bugs --- CHANGELOG.md | 1 + core/config_help.json | 17 ++++++++++++++++- core/utils.py | 3 +-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ee687cf6..1febcceb03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is - Snippets are invoked case insensitively. ([GH #3077](https://github.com/kyb3r/modmail/issues/3077), [PR #3080](https://github.com/kyb3r/modmail/pull/3080)) - Default tags now use top hoisted role. ([GH #3014](https://github.com/kyb3r/modmail/issues/3014)) - New thread-related config - `thread_show_roles`, `thread_show_account_age`, `thread_show_join_age`, `thread_cancelled`, `thread_creation_contact_title`, `thread_creation_self_contact_response`, `thread_creation_contact_response`. ([GH #3072](https://github.com/kyb3r/modmail/issues/3072)) +- `use_timestamp_channel_name` config to create thread channels by timestamp. ### Fixed diff --git a/core/config_help.json b/core/config_help.json index a679ba1c7c..1a57d3a3e8 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -105,7 +105,22 @@ "`{prefix}config set use_user_id_channel_name no`" ], "notes": [ - "This config is suitable for servers in Server Discovery to comply with channel name restrictions." + "This config is suitable for servers in Server Discovery to comply with channel name restrictions.", + "This cannot be applied with `use_timestamp_channel_name`.", + "See also: `use_timestamp_channel_name`." + ] + }, + "use_timestamp_channel_name": { + "default": "No", + "description": "When this is set to `yes`, new thread channels will be named with the recipient's account creation date instead of the recipient's name.", + "examples": [ + "`{prefix}config set use_timestamp_channel_name yes`", + "`{prefix}config set use_timestamp_channel_name no`" + ], + "notes": [ + "This config is suitable for servers in Server Discovery to comply with channel name restrictions.", + "This cannot be applied with `use_user_id_channel_name`.", + "See also: `use_user_id_channel_name`." ] }, "mod_typing": { diff --git a/core/utils.py b/core/utils.py index cf5aac5ec2..474bc9db4f 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,5 +1,4 @@ import base64 -from datetime import datetime import functools import re import string @@ -375,7 +374,7 @@ def format_channel_name(bot, author, exclude_channel=None, force_null=False): if bot.config["use_user_id_channel_name"]: name = new_name = str(author.id) elif bot.config["use_timestamp_channel_name"]: - name = new_name = datetime.utcnow().isoformat(sep="-", timesep="minutes") + name = new_name = author.created_at.isoformat(sep="-", timespec="minutes") else: name = author.name.lower() if force_null: From 55b311f294bb7570d2b5ae5b77eccea4e0518cd6 Mon Sep 17 00:00:00 2001 From: popeeyy <29686338+popeeyy@users.noreply.github.com> Date: Sat, 7 Aug 2021 19:44:29 -0700 Subject: [PATCH 0940/1221] Initial commit --- bot.py | 28 +++++++++++++++++++++++++--- cogs/modmail.py | 3 +++ cogs/utility.py | 18 ++++++++++++++++++ core/thread.py | 1 + 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 1c4fdd5354..12e28c8571 100644 --- a/bot.py +++ b/bot.py @@ -1010,6 +1010,7 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) if trigger: invoker = trigger.lower() + print("looking for auto trigger", trigger, self.auto_triggers[trigger]) alias = self.auto_triggers[trigger] ctxs = [] @@ -1019,24 +1020,45 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) if not aliases: logger.warning("Alias %s is invalid as called in autotrigger.", invoker) + print("Aliases", aliases) + for alias in aliases: view = StringView(invoked_prefix + alias) + invoked_with = view.get_word().lower() + invoked_with = invoked_with[1:] + print("Looking for", invoked_with) + found_command = self.all_commands.get(invoked_with) + + # Check for alias + if not found_command: + print("INVOKED WITH", invoked_with) + command_alias = self.aliases.get(invoked_with)[1:-1] + view = StringView(invoked_prefix + command_alias) + split_cmd = command_alias.split(" ") + found_command = self.all_commands.get(split_cmd[0]) + ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message) + ctx_.command = found_command + ctx_.invoked_with = invoked_with ctx_.thread = thread discord.utils.find(view.skip_string, await self.get_prefix()) - ctx_.invoked_with = view.get_word().lower() - ctx_.command = self.all_commands.get(ctx_.invoked_with) + + print("Command info:", view, ctx_, ctx_.thread, ctx_.invoked_with, ctx_.command) ctxs += [ctx_] for ctx in ctxs: if ctx.command: + print("Found command") old_checks = copy.copy(ctx.command.checks) + print("Old checks set") ctx.command.checks = [checks.has_permissions(PermissionLevel.INVALID)] - + print("Command checks added, invoking...", ctx) await self.invoke(ctx) ctx.command.checks = old_checks continue + else: + print("unable to find command") async def get_context(self, message, *, cls=commands.Context): """ diff --git a/cogs/modmail.py b/cogs/modmail.py index 0e41b76677..35e80bdf9b 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -818,7 +818,10 @@ async def reply(self, ctx, *, msg: str = ""): Supports attachments and images as well as automatically embedding image URLs. """ + ctx.message.content = msg + print("MSG IS", msg) + print("Sending", ctx.message, msg) async with ctx.typing(): await ctx.thread.reply(ctx.message) diff --git a/cogs/utility.py b/cogs/utility.py index 759b573748..d068b6cc1e 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1750,6 +1750,16 @@ async def autotrigger_add(self, ctx, keyword, *, command): print(self.bot.get_command(" ".join(split_cmd[0:n]))) valid = True break + + print("Split command", split_cmd, "Range", range(1, len(split_cmd) + 1), self.bot.aliases) + + if not valid and self.bot.aliases: + for n in range(1, len(split_cmd) + 1): + print(" ".join(split_cmd[0:n]), self.bot.aliases.get(" ".join(split_cmd[0:n]))) + if self.bot.aliases.get(" ".join(split_cmd[0:n])): + print(self.bot.aliases.get(" ".join(split_cmd[0:n]))) + valid = True + break if valid: self.bot.auto_triggers[keyword] = command @@ -1784,6 +1794,14 @@ async def autotrigger_edit(self, ctx, keyword, *, command): valid = True break + if not valid and self.bot.aliases: + for n in range(1, len(split_cmd) + 1): + print(" ".join(split_cmd[0:n]), self.bot.aliases.get(" ".join(split_cmd[0:n]))) + if self.bot.aliases.get(" ".join(split_cmd[0:n])): + print(self.bot.aliases.get(" ".join(split_cmd[0:n]))) + valid = True + break + if valid: self.bot.auto_triggers[keyword] = command await self.bot.config.update() diff --git a/core/thread.py b/core/thread.py index 41d2cd96ba..a494891965 100644 --- a/core/thread.py +++ b/core/thread.py @@ -241,6 +241,7 @@ class Author: async def activate_auto_triggers(): if initial_message: message = DummyMessage(copy.copy(initial_message)) + try: return await self.bot.trigger_auto_triggers(message, channel) except RuntimeError: From 115ccfadb797d876b8d7cbba91f311576695365a Mon Sep 17 00:00:00 2001 From: popeeyy <29686338+popeeyy@users.noreply.github.com> Date: Sat, 7 Aug 2021 20:17:11 -0700 Subject: [PATCH 0941/1221] Bug fixes and debug removal --- bot.py | 22 +++++++--------------- cogs/modmail.py | 5 ++--- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/bot.py b/bot.py index 12e28c8571..8c9d20dd87 100644 --- a/bot.py +++ b/bot.py @@ -1010,7 +1010,6 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) if trigger: invoker = trigger.lower() - print("looking for auto trigger", trigger, self.auto_triggers[trigger]) alias = self.auto_triggers[trigger] ctxs = [] @@ -1020,39 +1019,32 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) if not aliases: logger.warning("Alias %s is invalid as called in autotrigger.", invoker) - print("Aliases", aliases) - for alias in aliases: + print("Initial view", invoked_prefix + alias) view = StringView(invoked_prefix + alias) - invoked_with = view.get_word().lower() - invoked_with = invoked_with[1:] - print("Looking for", invoked_with) + invoked_with = view.get_word().lower()[1:] found_command = self.all_commands.get(invoked_with) # Check for alias if not found_command: - print("INVOKED WITH", invoked_with) - command_alias = self.aliases.get(invoked_with)[1:-1] - view = StringView(invoked_prefix + command_alias) - split_cmd = command_alias.split(" ") - found_command = self.all_commands.get(split_cmd[0]) + invoked_with = self.aliases.get(invoked_with)[1:-1] # Get command linked to alias + view = StringView(invoked_prefix + invoked_with) # Create StringView for new command + invoked_with = view.get_word().lower()[1:] # Parse the new command + found_command = self.all_commands.get(invoked_with) # Get the command function ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message) ctx_.command = found_command + ctx_.invoked_with = invoked_with ctx_.thread = thread discord.utils.find(view.skip_string, await self.get_prefix()) - print("Command info:", view, ctx_, ctx_.thread, ctx_.invoked_with, ctx_.command) ctxs += [ctx_] for ctx in ctxs: if ctx.command: - print("Found command") old_checks = copy.copy(ctx.command.checks) - print("Old checks set") ctx.command.checks = [checks.has_permissions(PermissionLevel.INVALID)] - print("Command checks added, invoking...", ctx) await self.invoke(ctx) ctx.command.checks = old_checks diff --git a/cogs/modmail.py b/cogs/modmail.py index 35e80bdf9b..66d2f675da 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -818,10 +818,9 @@ async def reply(self, ctx, *, msg: str = ""): Supports attachments and images as well as automatically embedding image URLs. """ - + print("MSG IS", msg, ctx.message.content) ctx.message.content = msg - print("MSG IS", msg) - print("Sending", ctx.message, msg) + async with ctx.typing(): await ctx.thread.reply(ctx.message) From a24cae3dd6c0a0b6a88e2bdb405374458d8e12d3 Mon Sep 17 00:00:00 2001 From: popeeyy <29686338+popeeyy@users.noreply.github.com> Date: Sat, 7 Aug 2021 20:18:22 -0700 Subject: [PATCH 0942/1221] Remove debug --- bot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot.py b/bot.py index 8c9d20dd87..20b8483128 100644 --- a/bot.py +++ b/bot.py @@ -1020,7 +1020,6 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) logger.warning("Alias %s is invalid as called in autotrigger.", invoker) for alias in aliases: - print("Initial view", invoked_prefix + alias) view = StringView(invoked_prefix + alias) invoked_with = view.get_word().lower()[1:] found_command = self.all_commands.get(invoked_with) From 4c0d8717192da6d57dbb13bfc8e4890849e6ef24 Mon Sep 17 00:00:00 2001 From: popeeyy <29686338+popeeyy@users.noreply.github.com> Date: Sat, 7 Aug 2021 20:20:27 -0700 Subject: [PATCH 0943/1221] Remove additional debug --- bot.py | 2 -- cogs/modmail.py | 2 +- cogs/utility.py | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/bot.py b/bot.py index 20b8483128..a9b463fb0d 100644 --- a/bot.py +++ b/bot.py @@ -1048,8 +1048,6 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) ctx.command.checks = old_checks continue - else: - print("unable to find command") async def get_context(self, message, *, cls=commands.Context): """ diff --git a/cogs/modmail.py b/cogs/modmail.py index 66d2f675da..efbb90c0b1 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -818,7 +818,7 @@ async def reply(self, ctx, *, msg: str = ""): Supports attachments and images as well as automatically embedding image URLs. """ - print("MSG IS", msg, ctx.message.content) + ctx.message.content = msg async with ctx.typing(): diff --git a/cogs/utility.py b/cogs/utility.py index d068b6cc1e..3c3318cb6d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1750,12 +1750,9 @@ async def autotrigger_add(self, ctx, keyword, *, command): print(self.bot.get_command(" ".join(split_cmd[0:n]))) valid = True break - - print("Split command", split_cmd, "Range", range(1, len(split_cmd) + 1), self.bot.aliases) if not valid and self.bot.aliases: for n in range(1, len(split_cmd) + 1): - print(" ".join(split_cmd[0:n]), self.bot.aliases.get(" ".join(split_cmd[0:n]))) if self.bot.aliases.get(" ".join(split_cmd[0:n])): print(self.bot.aliases.get(" ".join(split_cmd[0:n]))) valid = True From ed0631c07cb3b551c5b2862de9a68b65afd56a44 Mon Sep 17 00:00:00 2001 From: popeeyy <29686338+popeeyy@users.noreply.github.com> Date: Sat, 7 Aug 2021 20:21:40 -0700 Subject: [PATCH 0944/1221] Update bot.py --- bot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot.py b/bot.py index a9b463fb0d..01e9503c46 100644 --- a/bot.py +++ b/bot.py @@ -1044,6 +1044,7 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) if ctx.command: old_checks = copy.copy(ctx.command.checks) ctx.command.checks = [checks.has_permissions(PermissionLevel.INVALID)] + await self.invoke(ctx) ctx.command.checks = old_checks From 25cba86522b261ac6be406728313e31a416adb85 Mon Sep 17 00:00:00 2001 From: popeeyy <29686338+popeeyy@users.noreply.github.com> Date: Sat, 7 Aug 2021 20:24:16 -0700 Subject: [PATCH 0945/1221] Edit error message --- cogs/utility.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 3c3318cb6d..59e5d507dd 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1771,7 +1771,7 @@ async def autotrigger_add(self, ctx, keyword, *, command): embed = discord.Embed( title="Error", color=self.bot.error_color, - description="Invalid command. Note that autotriggers do not work with aliases.", + description="Invalid command. Please provide a valid command or alias.", ) await ctx.send(embed=embed) @@ -1812,7 +1812,7 @@ async def autotrigger_edit(self, ctx, keyword, *, command): embed = discord.Embed( title="Error", color=self.bot.error_color, - description="Invalid command. Note that autotriggers do not work with aliases.", + description="Invalid command. Please provide a valid command or alias.", ) await ctx.send(embed=embed) From bc451185aaabb1d97b510d82bb4894e9537ffce2 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 8 Aug 2021 15:27:14 +0800 Subject: [PATCH 0946/1221] move format_channel_name to Bot, resolve #2982 --- CHANGELOG.md | 5 +++-- bot.py | 32 +++++++++++++++++++++++++++++++- cogs/modmail.py | 2 +- core/utils.py | 34 ++-------------------------------- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1febcceb03..e7be4a292f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.10.0-dev6 +# v3.10.0-dev7 v3.10 adds group conversations while resolving other bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. ### Breaking - `Thread.recipient` (`str`) is now `Thread.recipients` (`List[str]`). -- `thread.reply` now returns mod_message, user_message1, user_message2... It is no longer limited at a size 2 tuple. +- `Thread.reply` now returns mod_message, user_message1, user_message2... It is no longer limited at a size 2 tuple. ### Added @@ -41,6 +41,7 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is - Fix return types, type hints and unresolved references ([PR #3009](https://github.com/kyb3r/modmail/pull/3009)) - Reload thread cache only when it's the first on_ready trigger. ([GH #3037](https://github.com/kyb3r/modmail/issues/3037)) +- `format_channel_name` is now extendable to plugins. Modify `Bot.format_channel_name(bot, author, exclude_channel=None, force_null=False):`. ([GH #2982](https://github.com/kyb3r/modmail/issues/2982)) # v3.9.5 diff --git a/bot.py b/bot.py index 33508e5eab..4ec694a1a5 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "v3.10.0-dev6" +__version__ = "3.10.0-dev7" import asyncio @@ -1645,6 +1645,36 @@ async def before_autoupdate(self): logger.warning("Autoupdates disabled.") self.autoupdate_loop.cancel() + def format_channel_name(self, author, exclude_channel=None, force_null=False): + """Sanitises a username for use with text channel names + + Placed in main bot class to be extendable to plugins""" + guild = self.modmail_guild + + if force_null: + name = new_name = "null" + else: + if self.config["use_user_id_channel_name"]: + name = new_name = str(author.id) + elif self.config["use_timestamp_channel_name"]: + name = new_name = author.created_at.isoformat(sep="-", timespec="minutes") + else: + name = author.name.lower() + if force_null: + name = "null" + + 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 + def main(): try: diff --git a/cogs/modmail.py b/cogs/modmail.py index 5e9813423c..57f7f017d6 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1829,7 +1829,7 @@ async def repair(self, ctx): ) if len(users) == 1: user = users.pop() - name = format_channel_name(self.bot, user, exclude_channel=ctx.channel) + name = self.bot.format_channel_name(user, 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] diff --git a/core/utils.py b/core/utils.py index 474bc9db4f..717b683546 100644 --- a/core/utils.py +++ b/core/utils.py @@ -30,7 +30,6 @@ "format_description", "trigger_typing", "escape_code_block", - "format_channel_name", "tryint", "get_top_hoisted_role", "get_joint_id", @@ -364,35 +363,6 @@ def escape_code_block(text): return re.sub(r"```", "`\u200b``", text) -def format_channel_name(bot, author, exclude_channel=None, force_null=False): - """Sanitises a username for use with text channel names""" - guild = bot.modmail_guild - - if force_null: - name = new_name = "null" - else: - if bot.config["use_user_id_channel_name"]: - name = new_name = str(author.id) - elif bot.config["use_timestamp_channel_name"]: - name = new_name = author.created_at.isoformat(sep="-", timespec="minutes") - else: - name = author.name.lower() - if force_null: - name = "null" - - 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 - - def tryint(x): try: return int(x) @@ -408,7 +378,7 @@ def get_top_hoisted_role(member: discord.Member): async def create_thread_channel(bot, recipient, category, overwrites, *, name=None, errors_raised=[]): - name = name or format_channel_name(bot, recipient) + name = name or bot.format_channel_name(recipient) try: channel = await bot.modmail_guild.create_text_channel( name=name, @@ -446,7 +416,7 @@ async def create_thread_channel(bot, recipient, category, overwrites, *, name=No recipient, category, overwrites, - name=format_channel_name(bot, recipient, force_null=True), + name=bot.format_channel_name(recipient, force_null=True), errors_raised=errors_raised, ) From cc890c4b5fb1b642c34874a9c8cb4c37c12325f4 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 8 Aug 2021 15:28:02 +0800 Subject: [PATCH 0947/1221] Formatting --- bot.py | 12 ++++++------ cogs/modmail.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bot.py b/bot.py index 01e9503c46..e066bc068a 100644 --- a/bot.py +++ b/bot.py @@ -1026,14 +1026,14 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) # Check for alias if not found_command: - invoked_with = self.aliases.get(invoked_with)[1:-1] # Get command linked to alias - view = StringView(invoked_prefix + invoked_with) # Create StringView for new command - invoked_with = view.get_word().lower()[1:] # Parse the new command - found_command = self.all_commands.get(invoked_with) # Get the command function + invoked_with = self.aliases.get(invoked_with)[1:-1] # Get command linked to alias + view = StringView(invoked_prefix + invoked_with) # Create StringView for new command + invoked_with = view.get_word().lower()[1:] # Parse the new command + found_command = self.all_commands.get(invoked_with) # Get the command function ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message) ctx_.command = found_command - + ctx_.invoked_with = invoked_with ctx_.thread = thread discord.utils.find(view.skip_string, await self.get_prefix()) @@ -1044,7 +1044,7 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) if ctx.command: old_checks = copy.copy(ctx.command.checks) ctx.command.checks = [checks.has_permissions(PermissionLevel.INVALID)] - + await self.invoke(ctx) ctx.command.checks = old_checks diff --git a/cogs/modmail.py b/cogs/modmail.py index efbb90c0b1..367c30f3c8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -820,7 +820,7 @@ async def reply(self, ctx, *, msg: str = ""): """ ctx.message.content = msg - + async with ctx.typing(): await ctx.thread.reply(ctx.message) From f3c22461d4a4eaf331ae637fbae9f017fa0270d6 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 8 Aug 2021 15:57:24 +0800 Subject: [PATCH 0948/1221] bump ver --- README.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2751e10fe0..e71432b5ca 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/pyproject.toml b/pyproject.toml index 20e1a16ac4..1c3ba056de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.0-dev6' +version = '3.10.0-dev7' description = "Modmail is similar to Reddit's 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 8ab48fc935f0597819d6227f2760356c44cb2100 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 8 Aug 2021 18:49:23 +0800 Subject: [PATCH 0949/1221] Bug fixes --- CHANGELOG.md | 2 +- README.md | 2 +- bot.py | 3 ++- core/utils.py | 3 +-- pyproject.toml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7be4a292f..b1745e32aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.10.0-dev7 +# v3.10.0-dev8 v3.10 adds group conversations while resolving other bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. diff --git a/README.md b/README.md index e71432b5ca..21e655a23c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 4ec694a1a5..8f581ab4e3 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.0-dev7" +__version__ = "3.10.0-dev8" import asyncio @@ -7,6 +7,7 @@ import os import re import signal +import string import sys import typing from datetime import datetime diff --git a/core/utils.py b/core/utils.py index 717b683546..0fa74e457a 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,7 +1,6 @@ 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 @@ -396,7 +395,7 @@ async def create_thread_channel(bot, recipient, category, overwrites, *, name=No if "Maximum number of channels in category reached" in e.text: fallback_id = bot.config["fallback_category_id"] if fallback_id: - fallback = discord.utils.get(cat.guild.categories, id=int(fallback_id)) + fallback = discord.utils.get(category.guild.categories, id=int(fallback_id)) if fallback and len(fallback.channels) < 49: category = fallback diff --git a/pyproject.toml b/pyproject.toml index 1c3ba056de..67a9dbd4f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.0-dev7' +version = '3.10.0-dev8' description = "Modmail is similar to Reddit's 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 736863b49f981992f2e9f13add9552231f0d8c9b Mon Sep 17 00:00:00 2001 From: popeeyy <29686338+popeeyy@users.noreply.github.com> Date: Sun, 8 Aug 2021 19:39:57 -0700 Subject: [PATCH 0950/1221] Redo alias conversion --- bot.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/bot.py b/bot.py index e066bc068a..e5008d51d1 100644 --- a/bot.py +++ b/bot.py @@ -1013,32 +1013,20 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) alias = self.auto_triggers[trigger] ctxs = [] + if alias is not None: ctxs = [] aliases = normalize_alias(alias) if not aliases: logger.warning("Alias %s is invalid as called in autotrigger.", invoker) - for alias in aliases: - view = StringView(invoked_prefix + alias) - invoked_with = view.get_word().lower()[1:] - found_command = self.all_commands.get(invoked_with) - - # Check for alias - if not found_command: - invoked_with = self.aliases.get(invoked_with)[1:-1] # Get command linked to alias - view = StringView(invoked_prefix + invoked_with) # Create StringView for new command - invoked_with = view.get_word().lower()[1:] # Parse the new command - found_command = self.all_commands.get(invoked_with) # Get the command function + message.author = thread.recipient # Allow for get_contexts to work - ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message) - ctx_.command = found_command - - ctx_.invoked_with = invoked_with - ctx_.thread = thread - discord.utils.find(view.skip_string, await self.get_prefix()) + for alias in aliases: + message.content = invoked_prefix + alias + ctxs += await self.get_contexts(message) - ctxs += [ctx_] + message.author = self.modmail_guild.me # Fix message so commands execute properly for ctx in ctxs: if ctx.command: From e31cc7681c2ea7ec23ca0e8b5fb696206a3bfe01 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 10 Aug 2021 23:07:44 +0800 Subject: [PATCH 0951/1221] Remove redundant category checks --- core/thread.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/core/thread.py b/core/thread.py index 700654e314..4d627226b7 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1294,20 +1294,6 @@ async def create( self.cache[recipient.id] = thread - # Schedule thread setup for later - cat = self.bot.main_category - if category is None and len(cat.channels) >= 49: - fallback_id = self.bot.config["fallback_category_id"] - if fallback_id: - fallback = discord.utils.get(cat.guild.categories, id=int(fallback_id)) - if fallback and len(fallback.channels) < 49: - category = fallback - - if not category: - category = await cat.clone(name="Fallback Modmail") - self.bot.config.set("fallback_category_id", str(category.id)) - await self.bot.config.update() - if (message or not manual_trigger) and self.bot.config["confirm_thread_creation"]: if not manual_trigger: destination = recipient From 0110130600ce86240a197dcd719e892dfc47c09b Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 17 Aug 2021 15:01:20 +0800 Subject: [PATCH 0952/1221] ?contact accepts multiple users, or role #3082 --- CHANGELOG.md | 9 ++- README.md | 2 +- bot.py | 2 +- cogs/modmail.py | 155 +++++++++++++++++++++++++++++------------------- cogs/utility.py | 6 +- pyproject.toml | 2 +- 6 files changed, 107 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1745e32aa..18d56cc081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,23 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.10.0-dev8 +# v3.10.0-dev9 v3.10 adds group conversations while resolving other bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. ### Breaking - `Thread.recipient` (`str`) is now `Thread.recipients` (`List[str]`). -- `Thread.reply` now returns mod_message, user_message1, user_message2... It is no longer limited at a size 2 tuple. +- `Thread.reply` now returns `mod_message, user_message1, user_message2`... It is no longer limited at a size 2 tuple. ### Added -- Ability to have group conversations. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) +- Ability to have group conversations with up to 5 users. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) - Snippets are invoked case insensitively. ([GH #3077](https://github.com/kyb3r/modmail/issues/3077), [PR #3080](https://github.com/kyb3r/modmail/pull/3080)) - Default tags now use top hoisted role. ([GH #3014](https://github.com/kyb3r/modmail/issues/3014)) - New thread-related config - `thread_show_roles`, `thread_show_account_age`, `thread_show_join_age`, `thread_cancelled`, `thread_creation_contact_title`, `thread_creation_self_contact_response`, `thread_creation_contact_response`. ([GH #3072](https://github.com/kyb3r/modmail/issues/3072)) - `use_timestamp_channel_name` config to create thread channels by timestamp. +### Improved +- `?contact` now accepts a role or multiple users (creates a group conversation). ([GH #3082](https://github.com/kyb3r/modmail/issues/3082)) + ### Fixed - Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/kyb3r/modmail/issues/3022), [PR #3028](https://github.com/kyb3r/modmail/pull/3028)) diff --git a/README.md b/README.md index 21e655a23c..d45da9823e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 8f581ab4e3..f8fa9e2b35 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.0-dev8" +__version__ = "3.10.0-dev9" import asyncio diff --git a/cogs/modmail.py b/cogs/modmail.py index 57f7f017d6..2c8dedcbe7 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -718,6 +718,16 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str ctx.command.reset_cooldown(ctx) return + if len(users + ctx.thread.recipients) > 5: + em = discord.Embed( + title="Error", + description="Only 5 users are allowed in a group conversation", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + if not silent: description = self.bot.formatter.format( self.bot.config["private_added_to_group_response"], moderator=ctx.author @@ -1308,14 +1318,14 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): @checks.has_permissions(PermissionLevel.REGULAR) async def selfcontact(self, ctx): """Creates a thread with yourself""" - await ctx.invoke(self.contact, user=ctx.author) + await ctx.invoke(self.contact, users=[ctx.author]) @commands.command(usage=" [category] [options]") @checks.has_permissions(PermissionLevel.SUPPORTER) async def contact( self, ctx, - user: Union[discord.Member, discord.User], + users: commands.Greedy[Union[discord.Member, discord.User, discord.Role]], *, category: Union[SimilarCategoryConverter, str] = None, manual_trigger=True, @@ -1327,7 +1337,8 @@ async def contact( will be created in that specified category. `category`, if specified, may be a category ID, mention, or name. - `user` may be a user ID, mention, or name. + `users` may be a user ID, mention, or name. If multiple users are specified, a group thread will start. + A maximum of 5 users are allowed. `options` can be `silent` or `silently`. """ silent = False @@ -1345,75 +1356,99 @@ async def contact( if isinstance(category, str): category = None - if user.bot: - embed = discord.Embed(color=self.bot.error_color, description="Cannot start a thread with a bot.") - return await ctx.send(embed=embed, delete_after=3) + errors = [] + for u in list(users): + if isinstance(u, discord.Role): + users += u.members + users.remove(u) - exists = await self.bot.threads.find(recipient=user) - if exists: - desc = "A thread for this user already exists" - if exists.channel: - desc += f" in {exists.channel.mention}" - desc += "." - embed = discord.Embed(color=self.bot.error_color, description=desc) - await ctx.channel.send(embed=embed, delete_after=3) + for u in list(users): + exists = await self.bot.threads.find(recipient=u) + if exists: + errors.append(f"A thread for {u} already exists.") + if exists.channel: + errors[-1] += f" in {exists.channel.mention}" + errors[-1] += "." + users.remove(u) + elif u.bot: + errors.append(f"{u} is a bot, cannot add to thread.") + users.remove(u) + elif await self.bot.is_blocked(u): + ref = f"{u.mention} is" if ctx.author != u else "You are" + errors.append(f"{ref} currently blocked from contacting {self.bot.user.name}.") + users.remove(u) - else: - creator = ctx.author if manual_trigger else user - if await self.bot.is_blocked(user): - if not manual_trigger: # react to contact - return + if len(users) > 5: + errors.append("Group conversations only support 5 users.") + users = [] - ref = f"{user.mention} is" if creator != user else "You are" - embed = discord.Embed( - color=self.bot.error_color, - description=f"{ref} currently blocked from contacting {self.bot.user.name}.", - ) - return await ctx.send(embed=embed) + if errors or not users: + if not users: + # no users left + title = "Thread not created" + else: + title = None - thread = await self.bot.threads.create( - recipient=user, - creator=creator, - category=category, - manual_trigger=manual_trigger, - ) - if thread.cancelled: + if manual_trigger: # not react to contact + embed = discord.Embed(title=title, color=self.bot.error_color, description="\n".join(errors)) + await ctx.send(embed=embed, delete_after=10) + + if not users: + # end return - if self.bot.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): - logger.info("Contacting user %s when Modmail DM is disabled.", user) + creator = ctx.author if manual_trigger else users[0] - if not silent and not self.bot.config.get("thread_contact_silently"): - if creator.id == user.id: - description = self.bot.config["thread_creation_self_contact_response"] - else: - description = self.bot.formatter.format( - self.bot.config["thread_creation_contact_response"], creator=creator - ) + thread = await self.bot.threads.create( + recipient=users[0], + creator=creator, + category=category, + manual_trigger=manual_trigger, + ) - em = discord.Embed( - title=self.bot.config["thread_creation_contact_title"], - description=description, - color=self.bot.main_color, + if thread.cancelled: + return + + if self.bot.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + logger.info("Contacting user %s when Modmail DM is disabled.", users[0]) + + if not silent and not self.bot.config.get("thread_contact_silently"): + if creator.id == users[0].id: + description = self.bot.config["thread_creation_self_contact_response"] + else: + description = self.bot.formatter.format( + self.bot.config["thread_creation_contact_response"], creator=creator ) - if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{creator}", icon_url=creator.avatar_url) - await user.send(embed=em) - embed = discord.Embed( - title="Created Thread", - description=f"Thread started by {creator.mention} for {user.mention}.", + em = discord.Embed( + title=self.bot.config["thread_creation_contact_title"], + description=description, color=self.bot.main_color, ) - await thread.wait_until_ready() - await thread.channel.send(embed=embed) - - if manual_trigger: - sent_emoji, _ = await self.bot.retrieve_emoji() - await self.bot.add_reaction(ctx.message, sent_emoji) - await asyncio.sleep(5) - await ctx.message.delete() + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() + em.set_footer(text=f"{creator}", icon_url=creator.avatar_url) + + for u in users: + await u.send(embed=em) + + embed = discord.Embed( + title="Created Thread", + description=f"Thread started by {creator.mention} for {', '.join(u.mention for u in users)}.", + color=self.bot.main_color, + ) + await thread.wait_until_ready() + + if users[1:]: + await thread.add_users(users[1:]) + + await thread.channel.send(embed=embed) + + if manual_trigger: + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + await asyncio.sleep(5) + await ctx.message.delete() @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) diff --git a/cogs/utility.py b/cogs/utility.py index db3fa8e746..41bf5ce655 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -695,7 +695,7 @@ async def mention(self, ctx, *user_or_role: Union[discord.Role, discord.Member, option = user_or_role[0].lower() if option == "disable": embed = discord.Embed( - description=f"Disabled mention on thread creation.", + description="Disabled mention on thread creation.", color=self.bot.main_color, ) self.bot.config["mention"] = None @@ -893,7 +893,7 @@ async def config_help(self, ctx, key: str.lower = None): 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)) + embed.add_field(name="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 @@ -1850,7 +1850,7 @@ async def autotrigger_test(self, ctx, *, text): embed = discord.Embed( title="Keyword Not Found", color=self.bot.error_color, - description=f"No autotrigger keyword found.", + description="No autotrigger keyword found.", ) return await ctx.send(embed=embed) diff --git a/pyproject.toml b/pyproject.toml index 67a9dbd4f7..8288e6dd2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.0-dev8' +version = '3.10.0-dev9' description = "Modmail is similar to Reddit's 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 936585a5ba96bd61cde91ffc69edce5c542e27ac Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 21 Aug 2021 12:18:19 +0800 Subject: [PATCH 0953/1221] Changelog and fix bot perm level --- CHANGELOG.md | 4 +++- README.md | 2 +- bot.py | 2 +- cogs/utility.py | 3 --- core/checks.py | 2 +- pyproject.toml | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18d56cc081..d965aa8b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.10.0-dev9 +# v3.10.0-dev10 v3.10 adds group conversations while resolving other bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. @@ -25,6 +25,7 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Improved - `?contact` now accepts a role or multiple users (creates a group conversation). ([GH #3082](https://github.com/kyb3r/modmail/issues/3082)) +- Aliases are now supported in autotrigger. ([GH #3081](https://github.com/kyb3r/modmail/pull/3081)) ### Fixed @@ -39,6 +40,7 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is - `thread_auto_close` timer now only resets on non-note and replies from mods. ([GH #3030](https://github.com/kyb3r/modmail/issues/3030)) - Deleted messages are now deleted on both ends. ([GH #3041](https://github.com/kyb3r/modmail/issues/3041), [@JerrieAries](https://github.com/kyb3r/modmail/commit/20b31f8e8b5497943513997fef788d72ae668438)) - Persistent notes are now properly deleted from the database. ([GH #3013](https://github.com/kyb3r/modmail/issues/3013)) +- Modmail Bot is now recognized to have `OWNER` permission level. This affects what can be run in autotriggers. ### Internal diff --git a/README.md b/README.md index d45da9823e..62e9496873 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 27231da3cc..98af158c5a 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.0-dev9" +__version__ = "3.10.0-dev10" import asyncio diff --git a/cogs/utility.py b/cogs/utility.py index ca5c9acfae..b0b2a344fc 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1755,7 +1755,6 @@ async def autotrigger_add(self, ctx, keyword, *, command): if not valid and self.bot.aliases: for n in range(1, len(split_cmd) + 1): if self.bot.aliases.get(" ".join(split_cmd[0:n])): - print(self.bot.aliases.get(" ".join(split_cmd[0:n]))) valid = True break @@ -1794,9 +1793,7 @@ async def autotrigger_edit(self, ctx, keyword, *, command): if not valid and self.bot.aliases: for n in range(1, len(split_cmd) + 1): - print(" ".join(split_cmd[0:n]), self.bot.aliases.get(" ".join(split_cmd[0:n]))) if self.bot.aliases.get(" ".join(split_cmd[0:n])): - print(self.bot.aliases.get(" ".join(split_cmd[0:n]))) valid = True break diff --git a/core/checks.py b/core/checks.py index 3f3666538d..15dcb098da 100644 --- a/core/checks.py +++ b/core/checks.py @@ -39,7 +39,7 @@ async def setup(ctx): async def check_permissions(ctx, command_name) -> bool: """Logic for checking permissions for a command for a user""" - if await ctx.bot.is_owner(ctx.author): + if await ctx.bot.is_owner(ctx.author) or ctx.author.id == ctx.bot.user.id: # Bot owner(s) (and creator) has absolute power over the bot return True diff --git a/pyproject.toml b/pyproject.toml index 8288e6dd2f..d06f3ea7c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.0-dev9' +version = '3.10.0-dev10' description = "Modmail is similar to Reddit's 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 7a4582d65df21eca107c967ebe0f098614afc6dc Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 21 Aug 2021 12:19:22 +0800 Subject: [PATCH 0954/1221] Remove extra loop.close --- bot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot.py b/bot.py index 98af158c5a..9894913126 100644 --- a/bot.py +++ b/bot.py @@ -286,7 +286,6 @@ def _cancel_tasks(): loop.run_until_complete(loop.shutdown_asyncgens()) finally: logger.info("Closing the event loop.") - # loop.close() if not future.cancelled(): try: From 4e54fd45d3e9cc84d3890994eac22a58a8cb4784 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 5 Sep 2021 00:24:45 +0800 Subject: [PATCH 0955/1221] Push version to 3.10 --- CHANGELOG.md | 2 +- README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d965aa8b18..6164ce1dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.10.0-dev10 +# v3.10.0 v3.10 adds group conversations while resolving other bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. diff --git a/README.md b/README.md index 62e9496873..a57b07f33b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 9894913126..0f178989cd 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.0-dev10" +__version__ = "3.10.0" import asyncio diff --git a/pyproject.toml b/pyproject.toml index d06f3ea7c4..2c5870d4a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.0-dev10' +version = '3.10.0' description = "Modmail is similar to Reddit's 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 b30e0d7634e7ec3ddac329904c242ff65c329e29 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 5 Sep 2021 00:36:21 +0800 Subject: [PATCH 0956/1221] Quick bugfix on config help and debug hastebin --- cogs/utility.py | 1 - core/config_help.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index b0b2a344fc..bc813e9d6e 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -444,7 +444,6 @@ async def debug_hastebin(self, ctx): with open( os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), "rb+", - encoding="utf-8", ) as f: logs = BytesIO(f.read().strip()) diff --git a/core/config_help.json b/core/config_help.json index 1a57d3a3e8..ee91e3d37d 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -434,7 +434,7 @@ ] }, "thread_creation_contact_response": { - "default": "\"{creator.name} has opened a Modmail thread.\"", + "default": "\"{{creator.name}} has opened a Modmail thread.\"", "description": "This is the message embed description sent to recipients when contacted by a mod.", "examples": [ "`{prefix}config set thread_creation_contact_response New thread opened.`" From 4ef803337b5c6359d6711cbd71f3c5188ec54918 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 5 Sep 2021 00:44:00 +0800 Subject: [PATCH 0957/1221] Fix bug where snippet add did not check command name --- cogs/modmail.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index c4244a5b12..bb3860bfde 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -206,7 +206,13 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte {prefix}snippet add "two word" this is a two word snippet. ``` """ - if name in self.bot.snippets: + 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.snippets: embed = discord.Embed( title="Error", color=self.bot.error_color, From 83225e939b514d3e7b1b9baab6d692a26410e453 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 5 Sep 2021 01:06:29 +0800 Subject: [PATCH 0958/1221] v3.10.1 - fix edit cmd --- CHANGELOG.md | 9 +++++++++ README.md | 2 +- bot.py | 2 +- core/thread.py | 2 +- pyproject.toml | 2 +- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6164ce1dbf..73cafb190f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.10.1 + +This is a hotfix for the edit command. + +### Fixed + +- `?edit` now works properly. + # v3.10.0 v3.10 adds group conversations while resolving other bugs and QOL changes. It is potentially breaking to some plugins that adds functionality to threads. @@ -24,6 +32,7 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is - `use_timestamp_channel_name` config to create thread channels by timestamp. ### Improved + - `?contact` now accepts a role or multiple users (creates a group conversation). ([GH #3082](https://github.com/kyb3r/modmail/issues/3082)) - Aliases are now supported in autotrigger. ([GH #3081](https://github.com/kyb3r/modmail/pull/3081)) diff --git a/README.md b/README.md index a57b07f33b..2604990a87 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 0f178989cd..7917bb73cf 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.0" +__version__ = "3.10.1" import asyncio diff --git a/core/thread.py b/core/thread.py index 1cde667c6a..54509cdc36 100644 --- a/core/thread.py +++ b/core/thread.py @@ -665,7 +665,7 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> tasks = [self.bot.api.edit_message(message1.id, message), message1.edit(embed=embed1)] if message2 is not [None]: for m2 in message2: - embed2 = message2.embeds[0] + embed2 = m2.embeds[0] embed2.description = message tasks += [m2.edit(embed=embed2)] elif message1.embeds[0].author.name.startswith("Persistent Note"): diff --git a/pyproject.toml b/pyproject.toml index 2c5870d4a2..ec51bb12a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.0' +version = '3.10.1' description = "Modmail is similar to Reddit's 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 a2f57311ade38a6554579dad0b4259d7bc4f5469 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 5 Sep 2021 01:19:48 +0800 Subject: [PATCH 0959/1221] Fix contact --- bot.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 7917bb73cf..20b7b05cb7 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.1" +__version__ = "3.10.0" import asyncio @@ -1329,7 +1329,7 @@ async def handle_react_to_contact(self, payload): return await member.send(embed=embed) ctx = await self.get_context(message) - await ctx.invoke(self.get_command("contact"), user=member, manual_trigger=False) + await ctx.invoke(self.get_command("contact"), users=[member], manual_trigger=False) async def on_raw_reaction_add(self, payload): await asyncio.gather( diff --git a/pyproject.toml b/pyproject.toml index ec51bb12a4..1d60fc9d97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,5 +34,5 @@ repository = 'https://github.com/kyb3r/modmail' homepage = 'https://github.com/kyb3r/modmail' keywords = ['discord', 'modmail'] -[tool.pylint.format] +[tool.pylint.format]react_to_contact_message max-line-length = "110" From ed7a2ae7031b78cc647db9979b1a7bb885562c16 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 5 Sep 2021 01:21:03 +0800 Subject: [PATCH 0960/1221] bump ver --- CHANGELOG.md | 7 +++++++ README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73cafb190f..af6e0cc407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.10.2 +This is a hotfix for react to contact. + +### Fixed + +- React to contact now works properly. + # v3.10.1 This is a hotfix for the edit command. diff --git a/README.md b/README.md index 2604990a87..f4b8e8d2ff 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 20b7b05cb7..7d4248173c 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.0" +__version__ = "3.10.2" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 1d60fc9d97..e7438ca373 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.1' +version = '3.10.2' description = "Modmail is similar to Reddit's 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 2682d58d20145aa0d462f732cc361d29d85e65ab Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 5 Sep 2021 01:21:34 +0800 Subject: [PATCH 0961/1221] oops --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e7438ca373..4dc7d411a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,5 +34,5 @@ repository = 'https://github.com/kyb3r/modmail' homepage = 'https://github.com/kyb3r/modmail' keywords = ['discord', 'modmail'] -[tool.pylint.format]react_to_contact_message +[tool.pylint.format] max-line-length = "110" From 778574ef52de95b593e22014c9aa36f402ae0b27 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 5 Sep 2021 01:21:46 +0800 Subject: [PATCH 0962/1221] oops --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e7438ca373..4dc7d411a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,5 +34,5 @@ repository = 'https://github.com/kyb3r/modmail' homepage = 'https://github.com/kyb3r/modmail' keywords = ['discord', 'modmail'] -[tool.pylint.format]react_to_contact_message +[tool.pylint.format] max-line-length = "110" From e311828d32cd13ab0ce6a33d3362cd52ea49b69d Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 6 Sep 2021 01:10:18 +0800 Subject: [PATCH 0963/1221] Improved UI for group conversations, LOG_DISCORD, snippet fix --- CHANGELOG.md | 14 ++++++++++++++ README.md | 2 +- bot.py | 11 ++++++++++- cogs/modmail.py | 11 +++++++++++ core/thread.py | 37 +++++++++++++++++++++++++++++++++++-- pyproject.toml | 2 +- 6 files changed, 72 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af6e0cc407..85fc813322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.10.3 + +### Improved + +- Thread genesis message now shows other recipients. + +### Fixed + +- `?snippet add` now properly blocks command names. + +### Internal + +- Set `LOG_DISCORD` environment variable to the logger level and log discord events. + # v3.10.2 This is a hotfix for react to contact. diff --git a/README.md b/README.md index f4b8e8d2ff..9730cd1339 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 7d4248173c..86ef132b40 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.2" +__version__ = "3.10.3" import asyncio @@ -1694,6 +1694,15 @@ def main(): ) sys.exit(0) + if os.environ.get("LOG_DISCORD"): + logger.debug(f"Discord logging enabled: {os.environ['LOG_DISCORD'].upper()}") + d_logger = logging.getLogger("discord") + + d_logger.setLevel(os.environ["LOG_DISCORD"].upper()) + handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w") + handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s")) + d_logger.addHandler(handler) + bot = ModmailBot() bot.run() diff --git a/cogs/modmail.py b/cogs/modmail.py index bb3860bfde..4a974b26cc 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -212,6 +212,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte color=self.bot.error_color, description=f"A command with the same name already exists: `{name}`.", ) + return await ctx.send(embed=embed) elif name in self.bot.snippets: embed = discord.Embed( title="Error", @@ -813,6 +814,16 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, ctx.command.reset_cooldown(ctx) return + if not users: + em = discord.Embed( + title="Error", + description="No valid users to remove.", + color=self.bot.error_color, + ) + await ctx.send(embed=em) + ctx.command.reset_cooldown(ctx) + return + if not silent: description = self.bot.formatter.format( self.bot.config["private_removed_from_group_response"], moderator=ctx.author diff --git a/core/thread.py b/core/thread.py index 54509cdc36..fc48f88f5b 100644 --- a/core/thread.py +++ b/core/thread.py @@ -51,7 +51,7 @@ def __init__( self._recipient = recipient self._other_recipients = other_recipients or [] self._channel = channel - self.genesis_message = None + self._genesis_message = None self._ready_event = asyncio.Event() self.wait_tasks = [] self.close_task = None @@ -140,6 +140,15 @@ async def from_channel(cls, manager: "ThreadManager", channel: discord.TextChann return thread + async def get_genesis_message(self) -> discord.Message: + if self._genesis_message is None: + async for m in self.channel.history(limit=5, oldest_first=True): + if m.author == self.bot.user: + if m.embeds and m.embeds[0].fields and m.embeds[0].fields[0].name == "Roles": + self._genesis_message = m + + return self._genesis_message + async def setup(self, *, creator=None, category=None, initial_message=None): """Create the thread channel and other io related initialisation tasks""" self.bot.dispatch("thread_initiate", self, creator, category, initial_message) @@ -195,7 +204,7 @@ async def send_genesis_message(): try: msg = await channel.send(mention, embed=info_embed) self.bot.loop.create_task(msg.pin()) - self.genesis_message = msg + self._genesis_message = msg except Exception: logger.error("Failed unexpectedly:", exc_info=True) @@ -1131,6 +1140,26 @@ async def set_title(self, title: str) -> None: await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") + async def _update_users_genesis(self): + genesis_message = await self.get_genesis_message() + embed = genesis_message.embeds[0] + value = " ".join(x.mention for x in self._other_recipients) + index = None + for n, field in enumerate(embed.fields): + if field.name == "Other Recipients": + index = n + break + + if index is None and value: + embed.add_field(name="Other Recipients", value=value, inline=False) + else: + if value: + embed.set_field_at(index, value=value) + else: + embed.remove_field(index) + + await genesis_message.edit(embed=embed) + async def add_users(self, users: typing.List[typing.Union[discord.Member, discord.User]]) -> None: title = match_title(self.channel.topic) user_id = match_user_id(self.channel.topic) @@ -1139,6 +1168,8 @@ async def add_users(self, users: typing.List[typing.Union[discord.Member, discor ids = ",".join(str(i.id) for i in self._other_recipients) await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") + await self._update_users_genesis() + async def remove_users(self, users: typing.List[typing.Union[discord.Member, discord.User]]) -> None: title = match_title(self.channel.topic) user_id = match_user_id(self.channel.topic) @@ -1148,6 +1179,8 @@ async def remove_users(self, users: typing.List[typing.Union[discord.Member, dis ids = ",".join(str(i.id) for i in self._other_recipients) await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") + await self._update_users_genesis() + class ThreadManager: """Class that handles storing, finding and creating Modmail threads.""" diff --git a/pyproject.toml b/pyproject.toml index 4dc7d411a5..341e506413 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.2' +version = '3.10.3' description = "Modmail is similar to Reddit's 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 6431ab04980eae662151e7d7d4ed3ea02d08c280 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 6 Sep 2021 01:20:22 +0800 Subject: [PATCH 0964/1221] Fix typo in config_help --- core/config_help.json | 2 +- core/thread.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/config_help.json b/core/config_help.json index ee91e3d37d..1bfd65a92b 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -426,7 +426,7 @@ "default": "\"You have opened a Modmail thread.\"", "description": "This is the message embed description sent to recipients when self-contacted.", "examples": [ - "`{prefix}config set thread_creation_contact_title You contacted yourself.`" + "`{prefix}config set thread_creation_self_contact_response You contacted yourself.`" ], "notes": [ "`thread_creation_contact_response` is used when contacted by another user.", diff --git a/core/thread.py b/core/thread.py index fc48f88f5b..e17a581630 100644 --- a/core/thread.py +++ b/core/thread.py @@ -805,7 +805,8 @@ async def note( async def reply( self, message: discord.Message, anonymous: bool = False, plain: bool = False - ) -> typing.Tuple[discord.Message, discord.Message]: + ) -> typing.Tuple[typing.List[discord.Message], discord.Message]: + """Returns List[user_dm_msg] and thread_channel_msg""" if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) if not any(g.get_member(self.id) for g in self.bot.guilds): From fc156f9601798246b2dca04764f0c861069d1ec2 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 6 Sep 2021 01:22:20 +0800 Subject: [PATCH 0965/1221] Fix syntax --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index e17a581630..6bc7adc6cb 100644 --- a/core/thread.py +++ b/core/thread.py @@ -806,7 +806,7 @@ async def note( async def reply( self, message: discord.Message, anonymous: bool = False, plain: bool = False ) -> typing.Tuple[typing.List[discord.Message], discord.Message]: - """Returns List[user_dm_msg] and thread_channel_msg""" + """Returns List[user_dm_msg] and thread_channel_msg""" if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) if not any(g.get_member(self.id) for g in self.bot.guilds): From 68827f7ad13f6842dfe19f4715d1f63c479c1cf7 Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Tue, 7 Sep 2021 09:01:53 +0800 Subject: [PATCH 0966/1221] Fix `?contact` command bug where the channel is created inside random category when `silent` or `silently` is passed in. Resolve #3091 --- cogs/modmail.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 4a974b26cc..937ef61ca5 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1362,9 +1362,16 @@ async def contact( """ silent = False if isinstance(category, str): - if "silent" in category or "silently" in category: + category = category.split() + + # just check the last element in the list + if category[-1].lower() in ("silent", "silently"): silent = True - category = category.strip("silently").strip("silent").strip() + # remove the last element as we no longer need it + category.pop() + + category = " ".join(category) + if category: try: category = await SimilarCategoryConverter().convert( ctx, category From 7922a97939c1d78da4a57bf8ed793527572c36b1 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Tue, 7 Sep 2021 13:09:44 +1000 Subject: [PATCH 0967/1221] Allow top role's hoisted check to be configurable. --- cogs/modmail.py | 4 ++-- core/config.py | 2 ++ core/thread.py | 6 +++--- core/utils.py | 6 ++++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 4a974b26cc..fdbc27429b 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -918,7 +918,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, tag = self.bot.config["mod_tag"] if tag is None: - tag = str(get_top_hoisted_role(ctx.author)) + tag = str(get_top_role(ctx.author, self.bot.config["use_hoisted_top_role"])) name = self.bot.config["anon_username"] if name is None: name = tag @@ -1003,7 +1003,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro tag = self.bot.config["mod_tag"] if tag is None: - tag = str(get_top_hoisted_role(ctx.author)) + tag = str(get_top_role(ctx.author, self.bot.config["use_hoisted_top_role"])) name = self.bot.config["anon_username"] if name is None: name = tag diff --git a/core/config.py b/core/config.py index d8593a02ef..e9de7d516d 100644 --- a/core/config.py +++ b/core/config.py @@ -123,6 +123,7 @@ class ConfigManager: "confirm_thread_creation_deny": "\N{NO ENTRY SIGN}", # regex "use_regex_autotrigger": False, + "use_hoisted_top_role": True, } private_keys = { @@ -209,6 +210,7 @@ class ConfigManager: "thread_show_roles", "thread_show_account_age", "thread_show_join_age", + "use_hoisted_top_role", } enums = { diff --git a/core/thread.py b/core/thread.py index 6bc7adc6cb..e20a7d1110 100644 --- a/core/thread.py +++ b/core/thread.py @@ -21,7 +21,7 @@ match_user_id, match_other_recipients, truncate, - get_top_hoisted_role, + get_top_role, create_thread_channel, get_joint_id, ) @@ -938,7 +938,7 @@ async def send( # Anonymously sending to the user. tag = self.bot.config["mod_tag"] if tag is None: - tag = str(get_top_hoisted_role(author)) + tag = str(get_top_role(author, self.bot.config["use_hoisted_top_role"])) name = self.bot.config["anon_username"] if name is None: name = tag @@ -1055,7 +1055,7 @@ async def send( elif not anonymous: mod_tag = self.bot.config["mod_tag"] if mod_tag is None: - mod_tag = str(get_top_hoisted_role(message.author)) + mod_tag = str(get_top_role(message.author, self.bot.config["use_hoisted_top_role"])) embed.set_footer(text=mod_tag) # Normal messages else: embed.set_footer(text=self.bot.config["anon_tag"]) diff --git a/core/utils.py b/core/utils.py index 0fa74e457a..44df043938 100644 --- a/core/utils.py +++ b/core/utils.py @@ -30,7 +30,7 @@ "trigger_typing", "escape_code_block", "tryint", - "get_top_hoisted_role", + "get_top_role", "get_joint_id", ] @@ -369,9 +369,11 @@ def tryint(x): return x -def get_top_hoisted_role(member: discord.Member): +def get_top_role(member: discord.Member, hoisted=True): roles = sorted(member.roles, key=lambda r: r.position, reverse=True) for role in roles: + if not hoisted: + return role if role.hoist: return role From 655e063e7ba200fcce770b4d8d3d103f236d7e1d Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Tue, 7 Sep 2021 18:55:32 +1000 Subject: [PATCH 0968/1221] Add use_hoisted_top_role config help details. --- core/config_help.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/config_help.json b/core/config_help.json index 1bfd65a92b..d7bbcf5da8 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -1121,5 +1121,16 @@ "notes": [ "This configuration can only to be set through `.env` file or environment (config) variables." ] + }, + "use_hoisted_top_role": { + "default": "Yes", + "description": "Controls if only hoisted roles are evaluated when finding top role.", + "examples": [ + ], + "notes": [ + "Top role is displayed in embeds when replying or adding/removing users to a thread in the case mod_tag and anon_username are not set.", + "If this configuration is enabled, only roles that are hoisted (displayed seperately in member list) will be used. If a user has no hoisted roles, it will return 'None'.", + "If you would like to display the top role of a user regardless of if it's hoisted or not, disable `use_hoisted_top_role`." + ] } } From c5b311ba6c545627304c43c432628f5ccb14d299 Mon Sep 17 00:00:00 2001 From: Andy <49132750+matrix2113@users.noreply.github.com> Date: Wed, 8 Sep 2021 20:23:16 -0400 Subject: [PATCH 0969/1221] Fix typo --- core/config_help.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config_help.json b/core/config_help.json index ee91e3d37d..88d0e2e47e 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -227,7 +227,7 @@ "default": "Yes", "description": "This is the channel where update notifications are sent to.", "examples": [ - "`{prefix}config set update_notifications no" + "`{prefix}config set update_notifications no`" ], "notes": [ "This has no effect unless `disable_autoupdates` is set to no.", From c65a5b785473786e96949c5237e688ca5c46b6a9 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Tue, 21 Sep 2021 21:26:48 +0530 Subject: [PATCH 0970/1221] Fetch thread closers if needed. --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 7d4248173c..a04661cc5f 100644 --- a/bot.py +++ b/bot.py @@ -582,7 +582,7 @@ async def on_ready(self): continue await thread.close( - closer=self.get_user(items["closer_id"]), + closer=self.get_user(items["closer_id"]) or await self.fetch_user(items["closer_id"]), after=after, silent=items["silent"], delete_channel=items["delete_channel"], From e87669c214ece58558e2cf9566e632e21f826634 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Tue, 21 Sep 2021 21:27:08 +0530 Subject: [PATCH 0971/1221] Add the get_or_fetch_user method. --- bot.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot.py b/bot.py index a04661cc5f..8cafda33b0 100644 --- a/bot.py +++ b/bot.py @@ -656,6 +656,15 @@ async def convert_emoji(self, name: str) -> str: raise return name + async def get_or_fetch_user(self, id: int) -> discord.User: + """ + Retrieve a User based on their ID. + + This tries getting the user from the cache and falls back to making + an API call if they're not found in the cache. + """ + return self.get_user(id) or await self.fetch_user(id) + async def retrieve_emoji(self) -> typing.Tuple[str, str]: sent_emoji = self.config["sent_emoji"] From 0e4c3749e4ccd4aa05837f9531b506d052fec781 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Tue, 21 Sep 2021 21:34:41 +0530 Subject: [PATCH 0972/1221] Use the method where possible to tidy up code. --- bot.py | 2 +- cogs/modmail.py | 19 ++++++++----------- core/thread.py | 8 ++++---- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/bot.py b/bot.py index 8cafda33b0..f19725a954 100644 --- a/bot.py +++ b/bot.py @@ -582,7 +582,7 @@ async def on_ready(self): continue await thread.close( - closer=self.get_user(items["closer_id"]) or await self.fetch_user(items["closer_id"]), + closer=await self.get_or_fetch_user(items["closer_id"]), after=after, silent=items["silent"], delete_channel=items["delete_channel"], diff --git a/cogs/modmail.py b/cogs/modmail.py index bb3860bfde..814113f617 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1042,7 +1042,7 @@ async def logs(self, ctx, *, user: User = None): thread = ctx.thread if not thread: raise commands.MissingRequiredArgument(SimpleNamespace(name="member")) - user = thread.recipient or await self.bot.fetch_user(thread.id) + user = thread.recipient or await self.bot.get_or_fetch_user(thread.id) default_avatar = "https://cdn.discordapp.com/embed/avatars/0.png" icon_url = getattr(user, "avatar_url", default_avatar) @@ -1492,15 +1492,12 @@ async def blocked(self, ctx): logger.debug("No longer blocked, user %s.", id_) continue - user = self.bot.get_user(int(id_)) - if user: - users.append((user.mention, reason)) + try: + user = await self.bot.get_or_fetch_user(int(id_)) + except discord.NotFound: + users.append((id_, reason)) else: - try: - user = await self.bot.fetch_user(id_) - users.append((user.mention, reason)) - except discord.NotFound: - users.append((id_, reason)) + users.append((user.mention, reason)) blocked_roles = list(self.bot.blocked_roles.items()) for id_, reason in blocked_roles: @@ -1840,7 +1837,7 @@ async def repair(self, ctx): user_id = match_user_id(message.embeds[0].footer.text) other_recipients = match_other_recipients(ctx.channel.topic) for n, uid in enumerate(other_recipients): - other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid) + other_recipients[n] = await self.bot.get_or_fetch_user(uid) if user_id != -1: recipient = self.bot.get_user(user_id) @@ -1893,7 +1890,7 @@ async def repair(self, ctx): other_recipients = match_other_recipients(ctx.channel.topic) for n, uid in enumerate(other_recipients): - other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid) + other_recipients[n] = await self.bot.get_or_fetch_user(uid) if recipient is None: self.bot.threads.cache[user.id] = thread = Thread( diff --git a/core/thread.py b/core/thread.py index 54509cdc36..c4fa7bbf07 100644 --- a/core/thread.py +++ b/core/thread.py @@ -126,12 +126,12 @@ async def from_channel(cls, manager: "ThreadManager", channel: discord.TextChann if recipient_id in manager.cache: thread = manager.cache[recipient_id] else: - recipient = manager.bot.get_user(recipient_id) or await manager.bot.fetch_user(recipient_id) + recipient = await manager.bot.get_or_fetch_user(recipient_id) other_recipients = [] for uid in match_other_recipients(channel.topic): try: - other_recipient = manager.bot.get_user(uid) or await manager.bot.fetch_user(uid) + other_recipient = await manager.bot.get_or_fetch_user(uid) except discord.NotFound: continue other_recipients.append(other_recipient) @@ -1243,14 +1243,14 @@ async def _find_from_channel(self, channel): return self.cache[user_id] try: - recipient = self.bot.get_user(user_id) or await self.bot.fetch_user(user_id) + recipient = await self.bot.get_or_fetch_user(user_id) except discord.NotFound: recipient = None other_recipients = [] for uid in match_other_recipients(channel.topic): try: - other_recipient = self.bot.get_user(uid) or await self.bot.fetch_user(uid) + other_recipient = await self.bot.get_or_fetch_user(uid) except discord.NotFound: continue other_recipients.append(other_recipient) From 9d0014284b8d31df96aa983a74e2a1ed1ec0f79a Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sun, 3 Oct 2021 03:16:09 +0800 Subject: [PATCH 0973/1221] Rename `category` to `fallback` due to shadowing names. --- core/utils.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/utils.py b/core/utils.py index 0fa74e457a..aaaf7e463d 100644 --- a/core/utils.py +++ b/core/utils.py @@ -393,19 +393,20 @@ async def create_thread_channel(bot, recipient, category, overwrites, *, name=No errors_raised.append((e.text, (category, name))) if "Maximum number of channels in category reached" in e.text: + fallback = None fallback_id = bot.config["fallback_category_id"] if fallback_id: fallback = discord.utils.get(category.guild.categories, id=int(fallback_id)) - if fallback and len(fallback.channels) < 49: - category = fallback + if fallback and len(fallback.channels) >= 49: + fallback = None - if not category: - category = await category.clone(name="Fallback Modmail") + if not fallback: + fallback = await category.clone(name="Fallback Modmail") bot.config.set("fallback_category_id", str(category.id)) await bot.config.update() return await create_thread_channel( - bot, recipient, category, overwrites, errors_raised=errors_raised + bot, recipient, fallback, overwrites, errors_raised=errors_raised ) if "Contains words not allowed" in e.text: From 8b69b67f22430b2adf719d5c6f0fa0ba1d605943 Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Wed, 6 Oct 2021 01:55:59 +0800 Subject: [PATCH 0974/1221] Forgot to update the reference in `.config.set()`. --- core/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/utils.py b/core/utils.py index aaaf7e463d..2c58fe69d6 100644 --- a/core/utils.py +++ b/core/utils.py @@ -402,7 +402,7 @@ async def create_thread_channel(bot, recipient, category, overwrites, *, name=No if not fallback: fallback = await category.clone(name="Fallback Modmail") - bot.config.set("fallback_category_id", str(category.id)) + bot.config.set("fallback_category_id", str(fallback.id)) await bot.config.update() return await create_thread_channel( From b388f0eeca0ccf1200684cb278a0747fa6ada239 Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Wed, 6 Oct 2021 01:57:01 +0800 Subject: [PATCH 0975/1221] Change default value for `errors_raised` parameter to `None`. --- core/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/utils.py b/core/utils.py index 2c58fe69d6..1fa1a95e61 100644 --- a/core/utils.py +++ b/core/utils.py @@ -376,8 +376,10 @@ def get_top_hoisted_role(member: discord.Member): return role -async def create_thread_channel(bot, recipient, category, overwrites, *, name=None, errors_raised=[]): +async def create_thread_channel(bot, recipient, category, overwrites, *, name=None, errors_raised=None): name = name or bot.format_channel_name(recipient) + errors_raised = errors_raised or [] + try: channel = await bot.modmail_guild.create_text_channel( name=name, From 3f393e03a550c1137ac66e3170cdf9145669b28c Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sun, 10 Oct 2021 00:47:01 +0800 Subject: [PATCH 0976/1221] New regex to properly match/parse channel topics. --- cogs/modmail.py | 2 +- core/thread.py | 52 +++++++++++++++++++++++------------- core/utils.py | 71 ++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 91 insertions(+), 34 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 4a974b26cc..e3c19d8182 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1848,7 +1848,7 @@ async def repair(self, ctx): 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) + user_id = match_user_id(message.embeds[0].footer.text, any_string=True) other_recipients = match_other_recipients(ctx.channel.topic) for n, uid in enumerate(other_recipients): other_recipients[n] = self.bot.get_user(uid) or await self.bot.fetch_user(uid) diff --git a/core/thread.py b/core/thread.py index 6bc7adc6cb..5bb5ad577b 100644 --- a/core/thread.py +++ b/core/thread.py @@ -17,9 +17,9 @@ from core.utils import ( is_image_url, days, + parse_channel_topic, match_title, match_user_id, - match_other_recipients, truncate, get_top_hoisted_role, create_thread_channel, @@ -119,9 +119,8 @@ def cancelled(self, flag: bool): @classmethod async def from_channel(cls, manager: "ThreadManager", channel: discord.TextChannel) -> "Thread": - recipient_id = match_user_id( - channel.topic - ) # there is a chance it grabs from another recipient's main thread + # there is a chance it grabs from another recipient's main thread + _, recipient_id, other_ids = parse_channel_topic(channel.topic) if recipient_id in manager.cache: thread = manager.cache[recipient_id] @@ -129,7 +128,7 @@ async def from_channel(cls, manager: "ThreadManager", channel: discord.TextChann recipient = manager.bot.get_user(recipient_id) or await manager.bot.fetch_user(recipient_id) other_recipients = [] - for uid in match_other_recipients(channel.topic): + for uid in other_ids: try: other_recipient = manager.bot.get_user(uid) or await manager.bot.fetch_user(uid) except discord.NotFound: @@ -1162,23 +1161,31 @@ async def _update_users_genesis(self): await genesis_message.edit(embed=embed) async def add_users(self, users: typing.List[typing.Union[discord.Member, discord.User]]) -> None: - title = match_title(self.channel.topic) - user_id = match_user_id(self.channel.topic) + title, user_id, _ = parse_channel_topic(self.channel.topic) + if title is not None: + title = f"Title: {title}\n" + else: + title = "" + self._other_recipients += users ids = ",".join(str(i.id) for i in self._other_recipients) - await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") + await self.channel.edit(topic=f"{title}User ID: {user_id}\nOther Recipients: {ids}") await self._update_users_genesis() async def remove_users(self, users: typing.List[typing.Union[discord.Member, discord.User]]) -> None: - title = match_title(self.channel.topic) - user_id = match_user_id(self.channel.topic) + title, user_id, _ = parse_channel_topic(self.channel.topic) + if title is not None: + title = f"Title: {title}\n" + else: + title = "" + for u in users: self._other_recipients.remove(u) ids = ",".join(str(i.id) for i in self._other_recipients) - await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") + await self.channel.edit(topic=f"{title}User ID: {user_id}\nOther Recipients: {ids}") await self._update_users_genesis() @@ -1240,16 +1247,24 @@ async def find( await thread.close(closer=self.bot.user, silent=True, delete_channel=False) thread = None else: + + def check(topic): + _, user_id, other_ids = parse_channel_topic(topic) + return recipient_id == user_id or recipient_id in other_ids + channel = discord.utils.find( - lambda x: str(recipient_id) in x.topic if x.topic else False, + lambda x: (check(x.topic)) if x.topic else False, self.bot.modmail_guild.text_channels, ) if channel: thread = await Thread.from_channel(self, channel) if thread.recipient: - # only save if data is valid - self.cache[recipient_id] = thread + # only save if data is valid. + # also the recipient_id here could belong to other recipient, + # it would be wrong if we set it as the dict key, + # so we use the thread id instead + self.cache[thread.id] = thread thread.ready = True if thread and recipient_id not in [x.id for x in thread.recipients]: @@ -1265,10 +1280,11 @@ async def _find_from_channel(self, channel): searching channel history for genesis embed and extracts user_id from that. """ - user_id = -1 - if channel.topic: - user_id = match_user_id(channel.topic) + if not channel.topic: + return None + + _, user_id, other_ids = parse_channel_topic(channel.topic) if user_id == -1: return None @@ -1282,7 +1298,7 @@ async def _find_from_channel(self, channel): recipient = None other_recipients = [] - for uid in match_other_recipients(channel.topic): + for uid in other_ids: try: other_recipient = self.bot.get_user(uid) or await self.bot.fetch_user(uid) except discord.NotFound: diff --git a/core/utils.py b/core/utils.py index 0fa74e457a..c87635b861 100644 --- a/core/utils.py +++ b/core/utils.py @@ -20,9 +20,11 @@ "human_join", "days", "cleanup_code", + "parse_channel_topic", "match_title", "match_user_id", "match_other_recipients", + "create_thread_channel", "create_not_found_embed", "parse_alias", "normalize_alias", @@ -218,9 +220,45 @@ def cleanup_code(content: str) -> str: return content.strip("` \n") -TOPIC_OTHER_RECIPIENTS_REGEX = re.compile(r"Other Recipients:\s*((?:\d{17,21},*)+)", flags=re.IGNORECASE) -TOPIC_TITLE_REGEX = re.compile(r"\bTitle: (.*)\n(?:User ID: )\b", flags=re.IGNORECASE | re.DOTALL) -TOPIC_UID_REGEX = re.compile(r"\bUser ID:\s*(\d{17,21})\b", flags=re.IGNORECASE) +TOPIC_REGEX = re.compile( + r"\b(Title: (?P.*)\n)?" + r"\bUser ID:\s*(?P<user_id>\d{17,21})\b" + r"(\nOther Recipients:\s*(?P<other_ids>(\d{17,21},?)+))?", + flags=re.IGNORECASE | re.DOTALL, +) +UID_REGEX = re.compile(r"\bUser ID:\s*(\d{17,21})\b", flags=re.IGNORECASE) + + +def parse_channel_topic(text: str) -> typing.Tuple[typing.Optional[str], int, typing.List[int]]: + """ + A helper to parse channel topics and respectivefully returns all the required values + at once. + + Parameters + ---------- + text : str + The text of channel topic. + + Returns + ------- + Tuple[Optional[str], int, List[int]] + A tuple of title, user ID, and other recipients IDs. + """ + title, user_id, other_ids = None, -1, [] + match = TOPIC_REGEX.search(text) + if match is not None: + groupdict = match.groupdict() + title = groupdict["title"] + + # user ID string is the required one in regex, so if match is found + # the value of this won't be None + user_id = int(groupdict["user_id"]) + + oth_ids = groupdict["other_ids"] + if oth_ids: + other_ids = list(map(int, oth_ids.split(","))) + + return title, user_id, other_ids def match_title(text: str) -> str: @@ -237,12 +275,10 @@ def match_title(text: str) -> str: Optional[str] The title if found. """ - match = TOPIC_TITLE_REGEX.search(text) - if match is not None: - return match.group(1) + return parse_channel_topic(text)[0] -def match_user_id(text: str) -> int: +def match_user_id(text: str, any_string: bool = False) -> int: """ Matches a user ID in the format of "User ID: 12345". @@ -250,16 +286,24 @@ def match_user_id(text: str) -> int: ---------- text : str The text of the user ID. + any_string: bool + Whether to search any string that matches the UID_REGEX, e.g. not from channel topic. + Defaults to False. Returns ------- int The user ID if found. Otherwise, -1. """ - match = TOPIC_UID_REGEX.search(text) - if match is not None: - return int(match.group(1)) - return -1 + user_id = -1 + if any_string: + match = UID_REGEX.search(text) + if match is not None: + user_id = int(match.group(1)) + else: + user_id = parse_channel_topic(text)[1] + + return user_id def match_other_recipients(text: str) -> typing.List[int]: @@ -276,10 +320,7 @@ def match_other_recipients(text: str) -> typing.List[int]: List[int] The list of other recipients IDs. """ - match = TOPIC_OTHER_RECIPIENTS_REGEX.search(text) - if match is not None: - return list(map(int, match.group(1).split(","))) - return [] + return parse_channel_topic(text)[2] def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discord.Embed: From 219ef56e512273561497998b0f61c2959a789e38 Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Mon, 11 Oct 2021 07:09:39 +0800 Subject: [PATCH 0977/1221] Only add title and other recipients strings in channel topic if any. --- core/thread.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/core/thread.py b/core/thread.py index 5bb5ad577b..2a2a243ba1 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1135,10 +1135,16 @@ def get_notifications(self) -> str: return " ".join(set(mentions)) async def set_title(self, title: str) -> None: + topic = f"Title: {title}\n" + user_id = match_user_id(self.channel.topic) - ids = ",".join(i.id for i in self._other_recipients) + topic += f"User ID: {user_id}" + + if self._other_recipients: + ids = ",".join(str(i.id) for i in self._other_recipients) + topic += f"\nOther Recipients: {ids}" - await self.channel.edit(topic=f"Title: {title}\nUser ID: {user_id}\nOther Recipients: {ids}") + await self.channel.edit(topic=topic) async def _update_users_genesis(self): genesis_message = await self.get_genesis_message() @@ -1161,32 +1167,37 @@ async def _update_users_genesis(self): await genesis_message.edit(embed=embed) async def add_users(self, users: typing.List[typing.Union[discord.Member, discord.User]]) -> None: + topic = "" title, user_id, _ = parse_channel_topic(self.channel.topic) if title is not None: - title = f"Title: {title}\n" - else: - title = "" + topic += f"Title: {title}\n" - self._other_recipients += users + topic += f"User ID: {user_id}" + self._other_recipients += users ids = ",".join(str(i.id) for i in self._other_recipients) - await self.channel.edit(topic=f"{title}User ID: {user_id}\nOther Recipients: {ids}") + topic += f"\nOther Recipients: {ids}" + + await self.channel.edit(topic=topic) await self._update_users_genesis() async def remove_users(self, users: typing.List[typing.Union[discord.Member, discord.User]]) -> None: + topic = "" title, user_id, _ = parse_channel_topic(self.channel.topic) if title is not None: - title = f"Title: {title}\n" - else: - title = "" + topic += f"Title: {title}\n" + + topic += f"User ID: {user_id}" for u in users: self._other_recipients.remove(u) - ids = ",".join(str(i.id) for i in self._other_recipients) - await self.channel.edit(topic=f"{title}User ID: {user_id}\nOther Recipients: {ids}") + if self._other_recipients: + ids = ",".join(str(i.id) for i in self._other_recipients) + topic += f"\nOther Recipients: {ids}" + await self.channel.edit(topic=topic) await self._update_users_genesis() From 3d494bb32b7b0578406c5a8f5e62b1c20d642791 Mon Sep 17 00:00:00 2001 From: kato <78689486+TheDiscordHistorian@users.noreply.github.com> Date: Thu, 21 Oct 2021 09:39:16 +0600 Subject: [PATCH 0978/1221] Add phish checker plugin --- plugins/registry.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index b39ac345bd..ad8dbebaf9 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -247,5 +247,13 @@ "title": "Claim Thread", "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" + }, + "phishchecker": { + "repository": "TheDiscordHistorian/historian-cogs", + "branch": "main", + "description": "Deletes scam links from your server and optionally kick / ban the user.", + "title": "Scam Link Detector", + "icon_url": "https://cdn.discordapp.com/attachments/576521645540245505/895661244743299102/antifish.png", + "thumbnail_url": "https://cdn.discordapp.com/attachments/576521645540245505/895661244743299102/antifish.png" } } From 7be70c0150ad94881335623ff58eb20025a9dfed Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Tue, 2 Nov 2021 07:56:59 +0800 Subject: [PATCH 0979/1221] Fix regex to offer a bit more flexibility. Suggested by @Taaku18 --- core/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/utils.py b/core/utils.py index c87635b861..9f99333cca 100644 --- a/core/utils.py +++ b/core/utils.py @@ -221,9 +221,9 @@ def cleanup_code(content: str) -> str: TOPIC_REGEX = re.compile( - r"\b(Title: (?P<title>.*)\n)?" + r"(?:\bTitle:\s*(?P<title>.*)\n)?" r"\bUser ID:\s*(?P<user_id>\d{17,21})\b" - r"(\nOther Recipients:\s*(?P<other_ids>(\d{17,21},?)+))?", + r"(?:\nOther Recipients:\s*(?P<other_ids>\d{17,21}(?:(?:\s*,\s*)\d{17,21})*)\b)?", flags=re.IGNORECASE | re.DOTALL, ) UID_REGEX = re.compile(r"\bUser ID:\s*(\d{17,21})\b", flags=re.IGNORECASE) From b02934dc64ad4fd272bd37370eb613ab66b94ef4 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Sun, 7 Nov 2021 18:05:15 -0800 Subject: [PATCH 0980/1221] Added sponsor --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index f4b8e8d2ff..2b4f44ea62 100644 --- a/README.md +++ b/README.md @@ -190,12 +190,20 @@ Real Madrid: </a> <br> <br> +Advertise Your Server: +<br> +<a href='https://discord.gg/zP8KcF4VQz'> + <img height=100 src='https://user-images.githubusercontent.com/45324516/140673115-dd3e873c-36b6-4383-9eb4-db42e1986ab3.png' style='margin:5px'> +</a> +<br> +<br> Discord Advice Center: <br> <a href='https://discord.gg/zmwZy5fd9v'> <img height=100 src='https://i.imgur.com/1hrjcHd.png' style='margin:5px'> </a> + Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins From 80dafcad0c689888d3e1a8e86f99b04f045ab7ed Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Sun, 7 Nov 2021 18:14:07 -0800 Subject: [PATCH 0981/1221] Update SPONSORS.json --- SPONSORS.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/SPONSORS.json b/SPONSORS.json index 30081e24e5..a1ec35d6dd 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -90,5 +90,20 @@ } ] } + }, + { + "embed": { + "title": "Advertise Your Server", + "description": "Advertise Your Server is the leading advertising and growth Discord Server. With over 60,000 members we can help grow your community with our range of services.\n\n__**Advertise Your Server offers everything you need to grow and find servers:**__\n\n:chart_with_upwards_trend: **Discord Growth Experts** to give you advice on how to __grow your server.__ (server/advert reviews, growth tips)\n:dividers: Over 40 different channels for **different server categories.**\n:robot: Our own __custom__ **bump bot.** (Liam)\n:bar_chart: Currently the __BIGGEST__ advertising server on Discord.\n:computer: Our own server __Listing Site__!\n:ticket: Small Servers Program for servers with less than 300 members.\n:dvd: Weekly Podcast, Blog, Email Newsletter and YouTube Tutorials. \n\nhttps://discord.gg/zP8KcF4VQz\nhttps://aysdiscord.com", + "author": { + "name": "Advertise Your Server", + "icon_url": "https://cdn.discordapp.com/attachments/563522692418895872/907067815486427176/logo4.png" + }, + "color": 431075, + "footer": { + "text": "Grow Your Discord Server" + }, + "image": "https://cdn.discordapp.com/attachments/472811257913933834/907068966311166043/unknown_2.png" + } } ] From 08b27ce731d5ac4f80a0557e836ba6d75cf9e983 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 19 Nov 2021 16:53:57 +0800 Subject: [PATCH 0982/1221] Fix typos #3116 --- bot.py | 1 + core/thread.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 86ef132b40..b47a694042 100644 --- a/bot.py +++ b/bot.py @@ -1694,6 +1694,7 @@ def main(): ) sys.exit(0) + # Set up discord.py internal logging if os.environ.get("LOG_DISCORD"): logger.debug(f"Discord logging enabled: {os.environ['LOG_DISCORD'].upper()}") d_logger = logging.getLogger("discord") diff --git a/core/thread.py b/core/thread.py index 6bc7adc6cb..3b4a1ef224 100644 --- a/core/thread.py +++ b/core/thread.py @@ -321,7 +321,7 @@ def _format_info_embed(self, user, log_url, log_count, color): created = str((time - user.created_at).days) user_info = [] if self.bot.config["thread_show_account_age"]: - user_info.append(f"was created {days(created)}") + user_info.append(f" was created {days(created)}") embed = discord.Embed(color=color, description=user.mention, timestamp=time) From b15a77b502a6ec13637fc2ec4da1b3cf6eedd96b Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Fri, 19 Nov 2021 17:08:38 +0800 Subject: [PATCH 0983/1221] Update changelog for PR-based changes --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85fc813322..8f5fd59670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.11.0 + +### Added + +- `use_hoisted_top_role` config to use change how default mod tags work, see `v3.10.0#Added` for details. ([PR #3093](https://github.com/kyb3r/modmail/pull/3093)) + +### Fixed + +- Several minor typos. ([PR #3095](https://github.com/kyb3r/modmail/pull/3095), [PR #3116](https://github.com/kyb3r/modmail/pull/3116)) +- Certain cases where fallback categories were not working as intended. ([PR #3109](https://github.com/kyb3r/modmail/pull/3109)) +- `?contact` would create in a random category in silent mode. ([GH #3091](https://github.com/kyb3r/modmail/issues/3091), [PR #3092](https://github.com/kyb3r/modmail/pull/3092)) +- Certain cases where `?close` would fail if closer isn't in cache. ([GH #3104](https://github.com/kyb3r/modmail/issues/3104), [PR #3105](https://github.com/kyb3r/modmail/pull/3105)) + +### Internal + +- Improve regex parsing of channel topics. ([PR #3111](https://github.com/kyb3r/modmail/pull/3111)) + # v3.10.3 ### Improved From 25ebb56605c576a0203875b62198f29f77be1d93 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 20 Nov 2021 02:42:37 +0800 Subject: [PATCH 0984/1221] Update version --- README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9730cd1339..75a3652454 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v3.10.3-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v3.11.0-dev1-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> diff --git a/bot.py b/bot.py index a0de88f8f7..432e6fdd11 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.3" +__version__ = "3.11.0-dev1" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 341e506413..797650d754 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.3' +version = '3.11.0-dev1' description = "Modmail is similar to Reddit's 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 f46d077ca7330d0c11ce537fcf17a8ce6e2d4d2a Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 00:55:09 +0800 Subject: [PATCH 0985/1221] [BREAKING] Update discord.py to v2-master --- CHANGELOG.md | 6 + Pipfile | 2 +- Pipfile.lock | 733 ++++++++++++++++++++++++++-------------------- README.md | 6 +- bot.py | 96 +++--- cogs/modmail.py | 50 ++-- cogs/plugins.py | 2 +- cogs/utility.py | 29 +- core/changelog.py | 4 +- core/clients.py | 16 +- core/paginator.py | 16 +- core/thread.py | 34 +-- core/time.py | 10 +- core/utils.py | 6 +- requirements.txt | 18 +- 15 files changed, 570 insertions(+), 458 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f5fd59670..fe9ec8e3e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ however, insignificant breaking changes do not guarantee a major version bump, s # v3.11.0 +### Breaking + +- Upgraded to discord.py v2.0 master ([internal changes](https://gist.github.com/apple502j/f75b4f24652f04de85c7084ffd73ec58)). +- Python 3.8 or higher is required. + ### Added - `use_hoisted_top_role` config to use change how default mod tags work, see `v3.10.0#Added` for details. ([PR #3093](https://github.com/kyb3r/modmail/pull/3093)) @@ -22,6 +27,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Internal - Improve regex parsing of channel topics. ([PR #3111](https://github.com/kyb3r/modmail/pull/3111)) +- Add warning if deploying on a developmental version. # v3.10.3 diff --git a/Pipfile b/Pipfile index 4ed01f5b70..51569d26e5 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ pylint = "~=2.9.3" [packages] aiohttp = "==3.7.4.post0" colorama = "~=0.4.4" # Doesn't officially support Python 3.9 yet, v0.4.5 will support 3.9 -"discord.py" = "==1.7.3" +"discord.py" = {ref = "45d498c1b76deaf3b394d17ccf56112fa691d160", git = "https://github.com/Rapptz/discord.py.git"} emoji = "~=1.2.0" isodate = "~=0.6.0" motor = "~=2.4.0" diff --git a/Pipfile.lock b/Pipfile.lock index a9f820119e..e1a77d86b5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "0e726213f83b90d7c4e90a04cea6636dbdc5be2ad82049c96820535e5cc3d1ad" + "sha256": "aaa53487befe4f19feda64131fbc5bde140c1ec1a59ff2656d2ad1da2c50b390" }, "pipfile-spec": 6, "requires": { @@ -91,21 +91,21 @@ "index": "pypi", "version": "==0.4.4" }, + "discord-py": { + "git": "https://github.com/Rapptz/discord.py.git", + "ref": "45d498c1b76deaf3b394d17ccf56112fa691d160" + }, "discord.py": { - "hashes": [ - "sha256:462cd0fe307aef8b29cbfa8dd613e548ae4b2cb581d46da9ac0d46fb6ea19408", - "sha256:c6f64db136de0e18e090f6752ea68bdd4ab0a61b82dfe7acecefa22d6477bb0c" - ], - "index": "pypi", - "version": "==1.7.3" + "git": "https://github.com/Rapptz/discord.py.git", + "ref": "45d498c1b76deaf3b394d17ccf56112fa691d160" }, "dnspython": { "hashes": [ - "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", - "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d" + "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216", + "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "version": "==2.1.0" }, "emoji": { "hashes": [ @@ -117,11 +117,11 @@ }, "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], "markers": "python_version >= '3.5'", - "version": "==3.2" + "version": "==3.3" }, "isodate": { "hashes": [ @@ -141,46 +141,81 @@ }, "multidict": { "hashes": [ - "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", - "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93", - "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632", - "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656", - "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79", - "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7", - "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d", - "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5", - "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224", - "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26", - "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea", - "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348", - "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6", - "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76", - "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1", - "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f", - "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952", - "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a", - "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37", - "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9", - "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359", - "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8", - "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da", - "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3", - "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d", - "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf", - "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841", - "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d", - "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93", - "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f", - "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647", - "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635", - "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456", - "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda", - "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5", - "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", - "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" + "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b", + "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031", + "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0", + "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce", + "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda", + "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858", + "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5", + "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8", + "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22", + "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac", + "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e", + "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6", + "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5", + "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0", + "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11", + "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a", + "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55", + "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341", + "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b", + "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704", + "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b", + "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1", + "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621", + "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d", + "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5", + "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7", + "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac", + "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d", + "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef", + "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0", + "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f", + "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02", + "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b", + "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37", + "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23", + "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d", + "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065", + "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86", + "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6", + "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded", + "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4", + "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7", + "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a", + "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17", + "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3", + "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21", + "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24", + "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940", + "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac", + "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c", + "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422", + "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628", + "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0", + "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf", + "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e", + "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677", + "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f", + "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c", + "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4", + "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b", + "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747", + "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0", + "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01", + "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8", + "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9", + "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64", + "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d", + "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0", + "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52", + "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1", + "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae", + "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d" ], "markers": "python_version >= '3.6'", - "version": "==5.1.0" + "version": "==5.2.0" }, "natural": { "hashes": [ @@ -202,107 +237,116 @@ "srv" ], "hashes": [ - "sha256:02dc0b0f48ed3cd06c13b7e31b066bf91e00dac5f8147b0a0a45f9009bfab857", - "sha256:053b4ebf91c7395d1fcd2ce6a9edff0024575b7b2de6781554a4114448a8adc9", - "sha256:070a4ef689c9438a999ec3830e69b208ff0d12251846e064d947f97d819d1d05", - "sha256:072ba7cb65c8aa4d5c5659bf6722ee85781c9d7816dc00679b8b6f3dff1ddafc", - "sha256:0b6055e0ef451ff73c93d0348d122a0750dddf323b9361de5835dac2f6cf7fc1", - "sha256:11f9e0cfc84ade088a38df2708d0b958bb76360181df1b2e1e1a41beaa57952b", - "sha256:18290649759f9db660972442aa606f845c368db9b08c4c73770f6da14113569b", - "sha256:186104a94d39b8412f8e3de385acd990a628346a4402d4f3a288a82b8660bd22", - "sha256:1970cfe2aec1bf74b40cf30c130ad10cd968941694630386db33e1d044c22a2e", - "sha256:19d4bd0fc29aa405bb1781456c9cfff9fceabb68543741eb17234952dbc2bbb0", - "sha256:1bab889ae7640eba739f67fcbf8eff252dddc60d4495e6ddd3a87cd9a95fdb52", - "sha256:1bc6fe7279ff40c6818db002bf5284aa03ec181ea1b1ceaeee33c289d412afa7", - "sha256:208debdcf76ed39ebf24f38509f50dc1c100e31e8653817fedb8e1f867850a13", - "sha256:2399a85b54f68008e483b2871f4a458b4c980469c7fe921595ede073e4844f1e", - "sha256:246ec420e4c8744fceb4e259f906211b9c198e1f345e6158dcd7cbad3737e11e", - "sha256:24f8aeec4d6b894a6128844e50ff423dd02462ee83addf503c598ee3a80ddf3d", - "sha256:255a35bf29185f44b412e31a927d9dcedda7c2c380127ecc4fbf2f61b72fa978", - "sha256:2dbfbbded947a83a3dffc2bd1ec4750c17e40904692186e2c55a3ad314ca0222", - "sha256:2e92aa32300a0b5e4175caec7769f482b292769807024a86d674b3f19b8e3755", - "sha256:316c1b8723afa9870567cd6dff35d440b2afeda53aa13da6c5ab85f98ed6f5ca", - "sha256:333bfad77aa9cd11711febfb75eed0bb537a1d022e1c252714dad38993590240", - "sha256:39dafa2eaf577d1969f289dc9a44501859a1897eb45bd589e93ce843fc610800", - "sha256:3ce83f17f641a62a4dfb0ba1b8a3c1ced7c842f511b5450d90c030c7828e3693", - "sha256:46d5ec90276f71af3a29917b30f2aec2315a2759b5f8d45b3b63a07ca8a070a3", - "sha256:48d5bc80ab0af6b60c4163c5617f5cd23f2f880d7600940870ea5055816af024", - "sha256:4ba0def4abef058c0e5101e05e3d5266e6fffb9795bbf8be0fe912a7361a0209", - "sha256:5af390fa9faf56c93252dab09ea57cd020c9123aa921b63a0ed51832fdb492e7", - "sha256:5e574664f1468872cd40f74e4811e22b1aa4de9399d6bcfdf1ee6ea94c017fcf", - "sha256:625befa3bc9b40746a749115cc6a15bf20b9bd7597ca55d646205b479a2c99c7", - "sha256:6261bee7c5abadeac7497f8f1c43e521da78dd13b0a2439f526a7b0fc3788824", - "sha256:657ad80de8ec9ed656f28844efc801a0802961e8c6a85038d97ff6f555ef4919", - "sha256:6b89dc51206e4971c5568c797991eaaef5dc2a6118d67165858ad11752dba055", - "sha256:6e66780f14c2efaf989cd3ac613b03ee6a8e3a0ba7b96c0bb14adca71a427e55", - "sha256:6fb3f85870ae26896bb44e67db94045f2ebf00c5d41e6b66cdcbb5afd644fc18", - "sha256:701e08457183da70ed96b35a6b43e6ba1df0b47c837b063cde39a1fbe1aeda81", - "sha256:70761fd3c576b027eec882b43ee0a8e5b22ff9c20cdf4d0400e104bc29e53e34", - "sha256:73b400fdc22de84bae0dbf1a22613928a41612ec0a3d6ed47caf7ad4d3d0f2ff", - "sha256:7412a36798966624dc4c57d64aa43c2d1100b348abd98daaac8e99e57d87e1d7", - "sha256:78ecb8d42f50d393af912bfb1fb1dcc9aabe9967973efb49ee577e8f1cea494c", - "sha256:7c6a9948916a7bbcc6d3a9f6fb75db1acb5546078023bfb3db6efabcd5a67527", - "sha256:7c72d08acdf573455b2b9d2b75b8237654841d63a48bc2327dc102c6ee89b75a", - "sha256:7d98ce3c42921bb91566121b658e0d9d59a9082a9bd6f473190607ff25ab637f", - "sha256:845a8b83798b2fb11b09928413cb32692866bfbc28830a433d9fa4c8c3720dd0", - "sha256:94d38eba4d1b5eb3e6bfece0651b855a35c44f32fd91f512ab4ba41b8c0d3e66", - "sha256:9a13661681d17e43009bb3e85e837aa1ec5feeea1e3654682a01b8821940f8b3", - "sha256:a0e5dff6701fa615f165306e642709e1c1550d5b237c5a7a6ea299886828bd50", - "sha256:a2239556ff7241584ce57be1facf25081669bb457a9e5cbe68cce4aae6567aa1", - "sha256:a325600c83e61e3c9cebc0c2b1c8c4140fa887f789085075e8f44c8ff2547eb9", - "sha256:a3566acfbcde46911c52810374ecc0354fdb841284a3efef6ff7105bc007e9a8", - "sha256:a634a4730ce0b0934ed75e45beba730968e12b4dafbb22f69b3b2f616d9e644e", - "sha256:a6d055f01b83b1a4df8bb0c61983d3bdffa913764488910af3620e5c2450bf83", - "sha256:a752ecd1a26000a6d67be7c9a2e93801994a8b3f866ac95b672fbc00225ca91a", - "sha256:a9ba2a63777027b06b116e1ea8248e66fd1bedc2c644f93124b81a91ddbf6d88", - "sha256:aaa038eafb7186a4abbb311fcf20724be9363645882bbce540bef4797e812a7a", - "sha256:af586e85144023686fb0af09c8cdf672484ea182f352e7ceead3d832de381e1b", - "sha256:b0a0cf39f589e52d801fdef418305562bc030cdf8929217463c8433c65fd5c2f", - "sha256:b1c4874331ab960429caca81acb9d2932170d66d6d6f87e65dc4507a85aca152", - "sha256:b3b5b3cbc3fdf4fcfa292529df2a85b5d9c7053913a739d3069af1e12e12219f", - "sha256:b542d56ed1b8d5cf3bb36326f814bd2fbe8812dfd2582b80a15689ea433c0e35", - "sha256:b6ea08758b6673610b3c5bdf47189286cf9c58b1077558706a2f6f8744922527", - "sha256:b754240daafecd9d5fce426b0fbaaed03f4ebb130745c8a4ae9231fffb8d75e5", - "sha256:b772bab31cbd9cb911e41e1a611ebc9497f9a32a7348e2747c38210f75c00f41", - "sha256:b88d1742159bc93a078733f9789f563cef26f5e370eba810476a71aa98e5fbc2", - "sha256:b8bf42d3b32f586f4c9e37541769993783a534ad35531ce8a4379f6fa664fba9", - "sha256:bc9ac81e73573516070d24ce15da91281922811f385645df32bd3c8a45ab4684", - "sha256:c188db6cf9e14dbbb42f5254292be96f05374a35e7dfa087cc2140f0ff4f10f6", - "sha256:c55782a55f4a013a78ac5b6ee4b8731a192dea7ab09f1b6b3044c96d5128edd4", - "sha256:c5cab230e7cabdae9ff23c12271231283efefb944c1b79bed79a91beb65ba547", - "sha256:cbf8672edeb7b7128c4a939274801f0e32bbf5159987815e3d1eace625264a46", - "sha256:cc2894fe91f31a513860238ede69fe47fada21f9e7ddfe73f7f9fef93a971e41", - "sha256:cda9e628b1315beec8341e8c04aac9a0b910650b05e0751e42e399d5694aeacb", - "sha256:ceae3ab9e11a27aaab42878f1d203600dfd24f0e43678b47298219a0f10c0d30", - "sha256:ced944dcdd561476deef7cb7bfd4987c69fffbfeff6d02ca4d5d4fd592d559b7", - "sha256:d04ca462cb99077e6c059e97c072957caf2918e6e4191e3161c01c439e0193de", - "sha256:d1131562ddc2ea8a446f66c2648d7dabec2b3816fc818528eb978a75a6d23b2e", - "sha256:d1740776b70367277323fafb76bcf09753a5cc9824f5d705bac22a34ff3668ea", - "sha256:d6e11ffd43184d529d6752d6dcb62b994f903038a17ea2168ef1910c96324d26", - "sha256:d73e10772152605f6648ba4410318594f1043bbfe36d2fadee7c4b8912eff7c5", - "sha256:da8288bc4a7807c6715416deed1c57d94d5e03e93537889e002bf985be503f1a", - "sha256:db93608a246da44d728842b8fa9e45aa9782db76955f634a707739a8d53ff544", - "sha256:dcd3d0009fbb6e454d729f8b22d0063bd9171c31a55e0f0271119bd4f2700023", - "sha256:dd1f49f949a658c4e8f81ed73f9aad25fcc7d4f62f767f591e749e30038c4e1d", - "sha256:dd6ff2192f34bd622883c745a56f492b1c9ccd44e14953e8051c33024a2947d5", - "sha256:e018a4921657c2d3f89c720b7b90b9182e277178a04a7e9542cc79d7d787ca51", - "sha256:e2b7670c0c8c6b501464150dd49dd0d6be6cb7f049e064124911cec5514fa19e", - "sha256:e7a33322e08021c37e89cae8ff06327503e8a1719e97c69f32c31cbf6c30d72c", - "sha256:e8a82e35d52ad6f867e88096a1a2b9bdc7ec4d5e65c7b4976a248bf2d1a32a93", - "sha256:e9faf8d4712d5ea301d74abfcf6dafe4b7f4af7936e91f283b0ad7bf69ed3e3a", - "sha256:ec5ca7c0007ce268048bbe0ffc6846ed1616cf3d8628b136e81d5e64ff3f52a2", - "sha256:eee42a1cc06565f6b21caa1f504ec15e07de7ebfd520ab57f8cb3308bc118e22", - "sha256:f2acf9bbcd514e901f82c4ca6926bbd2ae61716728f110b4343eb0a69612d018", - "sha256:f55c1ddcc1f6050b07d468ce594f55dbf6107b459e16f735d26818d7be1e9538", - "sha256:f6977a520bd96e097c8a37a8cbb9faa1ea99d21bf84190195056e25f688af73d", - "sha256:f94c7d22fb36b184734dded7345a04ec5f95130421c775b8b0c65044ef073f34", - "sha256:fa8957e9a1b202cb45e6b839c241cd986c897be1e722b81d2f32e9c6aeee80b0", - "sha256:fd3854148005c808c485c754a184c71116372263709958b42aefbef2e5dd373a", - "sha256:fe5872ce6f9627deac8314bdffd3862624227c3de4c17ef0cc78bbf0402999eb", - "sha256:ffbae429ba9e42d0582d3ac63fdb410338892468a2107d8ff68228ec9a39a0ed" + "sha256:02e0c088f189ca69fac094cb5f851b43bbbd7cec42114495777d4d8f297f7f8a", + "sha256:138248c542051eb462f88b50b0267bd5286d6661064bab06faa0ef6ac30cdb4b", + "sha256:13a7c6d055af58a1e9c505e736da8b6a2e95ccc8cec10b008143f7a536e5de8a", + "sha256:13d74bf3435c1e58d8fafccc0d5e87f246ae2c6e9cbef4b35e32a1c3759e354f", + "sha256:15dae01341571d0af51526b7a21648ca575e9375e16ba045c9860848dfa8952f", + "sha256:17238115e6d37f5423b046cb829f1ca02c4ea7edb163f5b8b88e0c975dc3fec9", + "sha256:180b405e17b90a877ea5dbc5efe7f4c171af4c89323148e100c0f12cedb86f12", + "sha256:1821ce4e5a293313947fd017bbd2d2535aa6309680fa29b33d0442d15da296ec", + "sha256:1a7b138a04fdd17849930dc8bf664002e17db38448850bfb96d200c9c5a8b3a1", + "sha256:1c4e51a3b69789b6f468a8e881a13f2d1e8f5e99e41f80fd44845e6ec0f701e1", + "sha256:1d55982e5335925c55e2b87467043866ce72bd30ea7e7e3eeed6ec3d95a806d4", + "sha256:1fa6f08ddb6975371777f97592d35c771e713ee2250e55618148a5e57e260aff", + "sha256:2174d3279b8e2b6d7613b338f684cd78ff7adf1e7ec5b7b7bde5609a129c9898", + "sha256:2462a68f6675da548e333fa299d8e9807e00f95a4d198cfe9194d7be69f40c9b", + "sha256:25fd76deabe9ea37c8360c362b32f702cc095a208dd1c5328189938ca7685847", + "sha256:287c2a0063267c1458c4ddf528b44063ce7f376a6436eea5bccd7f625bbc3b5e", + "sha256:2d3abe548a280b49269c7907d5b71199882510c484d680a5ea7860f30c4a695f", + "sha256:2fa101bb23619120673899694a65b094364269e597f551a87c4bdae3a474d726", + "sha256:2fda3b3fb5c0d159195ab834b322a23808f1b059bcc7e475765abeddee6a2529", + "sha256:303531649fa45f96b694054c1aa02f79bda32ef57affe42c5c339336717eed74", + "sha256:36806ee53a85c3ba73939652f2ced2961e6a77cfbae385cd83f2e24cd97964b7", + "sha256:37a63da5ee623acdf98e6d511171c8a5827a6106b0712c18af4441ef4f11e6be", + "sha256:3a2fcbd04273a509fa85285d9eccf17ab65ce440bd4f5e5a58c978e563cd9e9a", + "sha256:3b40e36d3036bfe69ba63ec8e746a390721f75467085a0384b528e1dda532c69", + "sha256:4168b6c425d783e81723fc3dc382d374a228ff29530436a472a36d9f27593e73", + "sha256:444c00ebc20f2f9dc62e34f7dc9453dc2f5f5a72419c8dccad6e26d546c35712", + "sha256:45d6b47d70ed44e3c40bef618ed61866c48176e7e5dff80d06d8b1a6192e8584", + "sha256:460bdaa3f65ddb5b7474ae08589a1763b5da1a78b8348351b9ba1c63b459d67d", + "sha256:47ed77f62c8417a86f9ad158b803f3459a636386cb9d3d4e9e7d6a82d051f907", + "sha256:48722e91981bb22a16b0431ea01da3e1cc5b96805634d3b8d3c2a5315c1ce7f1", + "sha256:49b0d92724d3fce1174fd30b0b428595072d5c6b14d6203e46a9ea347ae7b439", + "sha256:4a2d73a9281faefb273a5448f6d25f44ebd311ada9eb79b6801ae890508fe231", + "sha256:4f4bc64fe9cbd70d46f519f1e88c9e4677f7af18ab9cd4942abce2bcfa7549c3", + "sha256:5067c04d3b19c820faac6342854d887ade58e8d38c3db79b68c2a102bbb100e7", + "sha256:51437c77030bed72d57d8a61e22758e3c389b13fea7787c808030002bb05ca39", + "sha256:515e4708d6567901ffc06476a38abe2c9093733f52638235d9f149579c1d3de0", + "sha256:5183b698d6542219e4135de583b57bc6286bd37df7f645b688278eb919bfa785", + "sha256:56feb80ea1f5334ccab9bd16a5161571ab70392e51fcc752fb8a1dc67125f663", + "sha256:573e2387d0686976642142c50740dfc4d3494cc627e2a7d22782b99f70879055", + "sha256:58a67b3800476232f9989e533d0244060309451b436d46670a53e6d189f1a7e7", + "sha256:5e3833c001a04aa06a28c6fd9628256862a654c09b0f81c07734b5629bc014ab", + "sha256:5f5fe59328838fa28958cc06ecf94be585726b97d637012f168bc3c7abe4fd81", + "sha256:6235bf2157aa46e53568ed79b70603aa8874baa202d5d1de82fa0eb917696e73", + "sha256:63be03f7ae1e15e72a234637ec7941ef229c7ab252c9ff6af48bba1e5418961c", + "sha256:65f159c445761cab04b665fc448b3fc008aebc98e54fdcbfd1aff195ef1b1408", + "sha256:67e0b2ad3692f6d0335ae231a40de55ec395b6c2e971ad6f55b162244d1ec542", + "sha256:68409171ab2aa7ccd6e8e839233e4b8ddeec246383c9a3698614e814739356f9", + "sha256:6a96c04ce39d66df60d9ce89f4c254c4967bc7d9e2e2c52adc58f47be826ee96", + "sha256:6ead0126fb4424c6c6a4fdc603d699a9db7c03cdb8eac374c352a75fec8a820a", + "sha256:6eb6789f26c398c383225e1313c8e75a7d290d323b8eaf65f3f3ddd0eb8a5a3c", + "sha256:6f07888e3b73c0dfa46f12d098760494f5f23fd66923a6615edfe486e6a7649c", + "sha256:6f0f0a10f128ea0898e607d351ebfabf70941494fc94e87f12c76e2894d8e6c4", + "sha256:704879b6a54c45ad76cea7c6789c1ae7185050acea7afd15b58318fa1932ed45", + "sha256:7117bfd8827cfe550f65a3c399dcd6e02226197a91c6d11a3540c3e8efc686d6", + "sha256:712de1876608fd5d76abc3fc8ec55077278dd5044073fbe9492631c9a2c58351", + "sha256:75c7ef67b4b8ec070e7a4740764f6c03ec9246b59d95e2ae45c029d41cb9efa1", + "sha256:77dddf596fb065de29fb39992fbc81301f7fd0003be649b7fa7448c77ca53bed", + "sha256:7abc87e45b572eb6d17a50422e69a9e5d6f13e691e821fe2312df512500faa50", + "sha256:7d8cdd2f070c71366e64990653522cce84b08dc26ab0d1fa19aa8d14ee0cf9ba", + "sha256:81ce5f871f5d8e82615c8bd0b34b68a9650204c8b1a04ce7890d58c98eb66e39", + "sha256:837cdef094f39c6f4a2967abc646a412999c2540fbf5d3cce1dd3b671f4b876c", + "sha256:849e641cfed05c75d772f9e9018f42c5fbd00655d43d52da1b9c56346fd3e4cc", + "sha256:87114b995506e7584cf3daf891e419b5f6e7e383e7df6267494da3a76312aa22", + "sha256:87db421c9eb915b8d9a9a13c5b2ee338350e36ee83e26ff0adfc48abc5db3ac3", + "sha256:8851544168703fb519e95556e3b463fca4beeef7ed3f731d81a68c8268515d9d", + "sha256:891f541c7ed29b95799da0cd249ae1db1842777b564e8205a197b038c5df6135", + "sha256:8f87f53c9cd89010ae45490ec2c963ff18b31f5f290dc08b04151709589fe8d9", + "sha256:9641be893ccce7d192a0094efd0a0d9f1783a1ebf314b4128f8a27bfadb8a77c", + "sha256:979e34db4f3dc5710c18db437aaf282f691092b352e708cb2afd4df287698c76", + "sha256:9b62d84478f471fdb0dcea3876acff38f146bd23cbdbed15074fb4622064ec2e", + "sha256:a472ca3d43d33e596ff5836c6cc71c3e61be33f44fe1cfdab4a1100f4af60333", + "sha256:a5dbeeea6a375fbd79448b48a54c46fc9351611a03ef8398d2a40b684ce46194", + "sha256:a7430f3987d232e782304c109be1d0e6fff46ca6405cb2479e4d8d08cd29541e", + "sha256:a81e52dbf95f236a0c89a5abcd2b6e1331da0c0312f471c73fae76c79d2acf6b", + "sha256:aa434534cc91f51a85e3099dc257ee8034b3d2be77f2ca58fb335a686e3a681f", + "sha256:ab27d6d7d41a66d9e54269a290d27cd5c74f08e9add0054a754b4821026c4f42", + "sha256:adb37bf22d25a51b84d989a2a5c770d4514ac590201eea1cb50ce8c9c5257f1d", + "sha256:afb16330ab6efbbf995375ad94e970fa2f89bb46bd10d854b7047620fdb0d67d", + "sha256:b1b06038c9940a49c73db0aeb0f6809b308e198da1326171768cf68d843af521", + "sha256:b1e6d1cf4bd6552b5f519432cce1530c09e6b0aab98d44803b991f7e880bd332", + "sha256:bf2d9d62178bb5c05e77d40becf89c309b1966fbcfb5c306238f81bf1ec2d6a2", + "sha256:bfd073fea04061019a103a288847846b5ef40dfa2f73b940ed61e399ca95314f", + "sha256:c04e84ccf590933a266180286d8b6a5fc844078a5d934432628301bd8b5f9ca7", + "sha256:c0947d7be30335cb4c3d5d0983d8ebc8294ae52503cf1d596c926f7e7183900b", + "sha256:c2a17752f97a942bdb4ff4a0516a67c5ade1658ebe1ab2edacdec0b42e39fa75", + "sha256:c4653830375ab019b86d218c749ad38908b74182b2863d09936aa8d7f990d30e", + "sha256:c660fd1e4a4b52f79f7d134a3d31d452948477b7f46ff5061074a534c5805ba6", + "sha256:cb48ff6cc6109190e1ccf8ea1fc71cc244c9185813ce7d1c415dce991cfb8709", + "sha256:cef2675004d85d85a4ccc24730b73a99931547368d18ceeed1259a2d9fcddbc1", + "sha256:d1b98539b0de822b6f717498e59ae3e5ae2e7f564370ab513e6d0c060753e447", + "sha256:d6c6989c10008ac70c2bb2ad2b940fcfe883712746c89f7e3308c14c213a70d7", + "sha256:db3efec9dcecd96555d752215797816da40315d61878f90ca39c8e269791bf17", + "sha256:dc4749c230a71b34db50ac2481d9008bb17b67c92671c443c3b40e192fbea78e", + "sha256:dcf906c1f7a33e4222e4bff18da1554d69323bc4dd95fe867a6fa80709ee5f93", + "sha256:e2bccadbe313b11704160aaba5eec95d2da1aa663f02f41d2d1520d02bbbdcd5", + "sha256:e30cce3cc86d6082c8596b3fbee0d4f54bc4d337a4fa1bf536920e2e319e24f0", + "sha256:e5d6428b8b422ba5205140e8be11722fa7292a0bedaa8bc80fb34c92eb19ba45", + "sha256:e841695b5dbea38909ab2dbf17e91e9a823412d8d88d1ef77f1b94a7bc551c0f", + "sha256:eb65ec0255a0fccc47c87d44e505ef5180bfd71690bd5f84161b1f23949fb209", + "sha256:ed20ec5a01c43254f6047c5d8124b70d28e39f128c8ad960b437644fe94e1827", + "sha256:ed751a20840a31242e7bea566fcf93ba75bc11b33afe2777bbf46069c1af5094", + "sha256:ef8b927813c27c3bdfc82c55682d7767403bcdadfd9f9c0fc49f4be4553a877b", + "sha256:f43cacda46fc188f998e6d308afe1c61ff41dcb300949f4cbf731e9a0a5eb2d3", + "sha256:f44bea60fd2178d7153deef9621c4b526a93939da30010bba24d3408a98b0f79", + "sha256:fcc021530b7c71069132fe4846d95a3cdd74d143adc2f7e398d5fabf610f111c", + "sha256:fe16517b275031d61261a4e3941c411fb7c46a9cd012f02381b56e7907cc9e06", + "sha256:fe3ae4294d593da54862f0140fdcc89d1aeeb94258ca97f094119ed7f0e5882d" ], "index": "pypi", - "version": "==3.12.0" + "version": "==3.12.1" }, "python-dateutil": { "hashes": [ @@ -330,70 +374,111 @@ }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed", + "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9" ], - "version": "==3.10.0.0" + "markers": "python_version >= '3.6'", + "version": "==4.0.0" }, "uvloop": { "hashes": [ - "sha256:0de811931e90ae2da9e19ce70ffad73047ab0c1dba7c6e74f9ae1a3aabeb89bd", - "sha256:1ff05116ede1ebdd81802df339e5b1d4cab1dfbd99295bf27e90b4cec64d70e9", - "sha256:2d8ffe44ae709f839c54bacf14ed283f41bee90430c3b398e521e10f8d117b3a", - "sha256:5cda65fc60a645470b8525ce014516b120b7057b576fa876cdfdd5e60ab1efbb", - "sha256:63a3288abbc9c8ee979d7e34c34e780b2fbab3e7e53d00b6c80271119f277399", - "sha256:7522df4e45e4f25b50adbbbeb5bb9847495c438a628177099d2721f2751ff825", - "sha256:7f4b8a905df909a407c5791fb582f6c03b0d3b491ecdc1cdceaefbc9bf9e08f6", - "sha256:905f0adb0c09c9f44222ee02f6b96fd88b493478fffb7a345287f9444e926030", - "sha256:ae2b325c0f6d748027f7463077e457006b4fdb35a8788f01754aadba825285ee", - "sha256:e71fb9038bfcd7646ca126c5ef19b17e48d4af9e838b2bcfda7a9f55a6552a32" + "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450", + "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897", + "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861", + "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c", + "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805", + "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d", + "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464", + "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f", + "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9", + "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab", + "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f", + "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638", + "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64", + "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee", + "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382", + "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228" ], "markers": "sys_platform != 'win32'", - "version": "==0.15.3" + "version": "==0.16.0" }, "yarl": { "hashes": [ - "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", - "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", - "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", - "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", - "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", - "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", - "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", - "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", - "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", - "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", - "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", - "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", - "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", - "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", - "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", - "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", - "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", - "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", - "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", - "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", - "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", - "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", - "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", - "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", - "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", - "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", - "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", - "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", - "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", - "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", - "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", - "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", - "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", - "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", - "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", - "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", - "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" + "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", + "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8", + "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e", + "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746", + "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98", + "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125", + "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d", + "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d", + "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986", + "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d", + "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec", + "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8", + "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee", + "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3", + "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1", + "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd", + "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b", + "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de", + "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0", + "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8", + "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6", + "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245", + "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23", + "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332", + "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1", + "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c", + "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4", + "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0", + "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8", + "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832", + "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58", + "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6", + "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1", + "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52", + "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92", + "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185", + "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d", + "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d", + "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b", + "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739", + "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05", + "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63", + "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d", + "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa", + "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913", + "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe", + "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b", + "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b", + "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656", + "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1", + "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4", + "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e", + "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63", + "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271", + "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed", + "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d", + "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda", + "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265", + "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f", + "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c", + "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba", + "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c", + "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b", + "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523", + "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a", + "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef", + "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95", + "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72", + "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794", + "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41", + "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576", + "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59" ], "markers": "python_version >= '3.6'", - "version": "==1.6.3" + "version": "==1.7.2" } }, "develop": { @@ -406,19 +491,19 @@ }, "astroid": { "hashes": [ - "sha256:7b963d1c590d490f60d2973e57437115978d3a2529843f160b5003b721e1e925", - "sha256:83e494b02d75d07d4e347b27c066fd791c0c74fc96c613d1ea3de0c82c48168f" + "sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334", + "sha256:ab7f36e8a78b8e54a62028ba6beef7561db4cdb6f2a5009ecc44a6f42b5697ef" ], "markers": "python_version ~= '3.6'", - "version": "==2.6.5" + "version": "==2.6.6" }, "bandit": { "hashes": [ - "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07", - "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608" + "sha256:a81b00b5436e6880fa8ad6799bc830e02032047713cbb143a12939ac67eb756c", + "sha256:f5acd838e59c038a159b5c621cf0f8270b279e884eadd7b782d7491c02add0d4" ], "index": "pypi", - "version": "==1.7.0" + "version": "==1.7.1" }, "black": { "hashes": [ @@ -430,11 +515,11 @@ }, "click": { "hashes": [ - "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", - "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" ], "markers": "python_version >= '3.6'", - "version": "==8.0.1" + "version": "==8.0.3" }, "colorama": { "hashes": [ @@ -446,27 +531,27 @@ }, "gitdb": { "hashes": [ - "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0", - "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005" + "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd", + "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa" ], - "markers": "python_version >= '3.4'", - "version": "==4.0.7" + "markers": "python_version >= '3.6'", + "version": "==4.0.9" }, "gitpython": { "hashes": [ - "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b", - "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8" + "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647", + "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5" ], - "markers": "python_version >= '3.6'", - "version": "==3.1.18" + "markers": "python_version >= '3.7'", + "version": "==3.1.24" }, "isort": { "hashes": [ - "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813", - "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e" + "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", + "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" ], "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", - "version": "==5.9.2" + "version": "==5.10.1" }, "lazy-object-proxy": { "hashes": [ @@ -519,124 +604,128 @@ }, "pbr": { "hashes": [ - "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", - "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" + "sha256:176e8560eaf61e127817ef93d8a844803abb27a4d4637f0ff3bb783129be2e0a", + "sha256:672d8ebee84921862110f23fcec2acea191ef58543d34dfe9ef3d9f13c31cddf" ], "markers": "python_version >= '2.6'", - "version": "==5.6.0" + "version": "==5.8.0" }, "pylint": { "hashes": [ - "sha256:1f333dc72ef7f5ea166b3230936ebcfb1f3b722e76c980cb9fe6b9f95e8d3172", - "sha256:748f81e5776d6273a6619506e08f1b48ff9bcb8198366a56821cf11aac14fc87" + "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594", + "sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e" ], "index": "pypi", - "version": "==2.9.5" + "version": "==2.9.6" }, "pyyaml": { "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", - "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", - "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", - "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" + "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", + "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", + "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", + "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", + "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", + "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", + "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", + "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", + "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", + "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", + "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", + "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", + "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", + "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", + "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", + "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", + "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", + "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", + "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", + "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", + "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", + "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", + "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", + "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", + "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", + "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", + "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==5.4.1" + "markers": "python_version >= '3.6'", + "version": "==6.0" }, "regex": { "hashes": [ - "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f", - "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad", - "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a", - "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf", - "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59", - "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d", - "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895", - "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4", - "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3", - "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222", - "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0", - "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c", - "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417", - "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d", - "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d", - "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761", - "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0", - "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026", - "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854", - "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb", - "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d", - "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068", - "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde", - "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d", - "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec", - "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa", - "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd", - "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b", - "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26", - "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2", - "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f", - "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694", - "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0", - "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407", - "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874", - "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035", - "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d", - "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c", - "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5", - "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985", - "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58" - ], - "version": "==2021.7.6" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f", + "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc", + "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4", + "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4", + "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8", + "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f", + "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a", + "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef", + "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f", + "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc", + "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50", + "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d", + "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d", + "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733", + "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36", + "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345", + "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0", + "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12", + "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646", + "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667", + "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244", + "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29", + "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec", + "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf", + "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4", + "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449", + "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a", + "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d", + "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb", + "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e", + "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83", + "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e", + "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a", + "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94", + "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc", + "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e", + "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965", + "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0", + "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36", + "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec", + "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23", + "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7", + "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe", + "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6", + "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b", + "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb", + "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b", + "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30", + "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e" + ], + "version": "==2021.11.10" }, "smmap": { "hashes": [ - "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182", - "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2" + "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", + "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" ], - "markers": "python_version >= '3.5'", - "version": "==4.0.0" + "markers": "python_version >= '3.6'", + "version": "==5.0.0" }, "stevedore": { "hashes": [ - "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee", - "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a" + "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c", + "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335" ], "markers": "python_version >= '3.6'", - "version": "==3.3.0" + "version": "==3.5.0" }, "toml": { "hashes": [ @@ -646,6 +735,14 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, + "typing-extensions": { + "hashes": [ + "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed", + "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.0" + }, "wrapt": { "hashes": [ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" diff --git a/README.md b/README.md index 75a3652454..81cc75929c 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,11 @@ </a> <a href="https://patreon.com/kyber"> - <img src="https://img.shields.io/badge/patreon-donate-orange.svg?style=for-the-badge&logo=Patreon" alt="Python 3.7"> + <img src="https://img.shields.io/badge/patreon-donate-orange.svg?style=for-the-badge&logo=Patreon" alt="Python 3.8"> </a> <a href="https://www.python.org/downloads/"> - <img src="https://img.shields.io/badge/Made%20With-Python%203.7-blue.svg?style=for-the-badge&logo=Python" alt="Made with Python 3.7"> + <img src="https://img.shields.io/badge/Made%20With-Python%203.8-blue.svg?style=for-the-badge&logo=Python" alt="Made with Python 3.8"> </a> <a href="https://github.com/ambv/black"> @@ -108,7 +108,7 @@ If you don't want to go through the trouble of setting up your very own Modmail ### Locally -Local hosting of Modmail is also possible. First, you will need [`Python 3.7`](https://www.python.org/downloads/release/python-376/). +Local hosting of Modmail is also possible. First, you will need [`Python 3.8`](https://www.python.org/downloads/release/python-376/). Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. diff --git a/bot.py b/bot.py index 432e6fdd11..a0d4043b55 100644 --- a/bot.py +++ b/bot.py @@ -22,8 +22,6 @@ from emoji import UNICODE_EMOJI from pkg_resources import parse_version -from core.utils import tryint - try: # noinspection PyUnresolvedReferences @@ -48,7 +46,7 @@ ) from core.thread import ThreadManager from core.time import human_timedelta -from core.utils import normalize_alias, truncate +from core.utils import normalize_alias, truncate, tryint logger = getLogger(__name__) @@ -69,12 +67,10 @@ def __init__(self): super().__init__(command_prefix=None, intents=intents) # implemented in `get_prefix` self._session = None self._api = None - self.metadata_loop = None - self.autoupdate_loop = None self.formatter = SafeFormatter() self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] self._connected = asyncio.Event() - self.start_time = datetime.utcnow() + self.start_time = discord.utils.utcnow() self._started = False self.config = ConfigManager(self) @@ -90,7 +86,7 @@ def __init__(self): @property def uptime(self) -> str: - now = datetime.utcnow() + now = discord.utils.utcnow() delta = now - self.start_time hours, remainder = divmod(int(delta.total_seconds()), 3600) minutes, seconds = divmod(remainder, 60) @@ -241,11 +237,7 @@ def stop_loop_on_completion(f): loop.stop() def _cancel_tasks(): - if sys.version_info < (3, 8): - task_retriever = asyncio.Task.all_tasks - else: - task_retriever = asyncio.all_tasks - + task_retriever = asyncio.all_tasks tasks = {t for t in task_retriever(loop=loop) if not t.done()} if not tasks: @@ -557,6 +549,13 @@ async def on_ready(self): logger.info("Receiving guild ID: %s", self.modmail_guild.id) logger.line() + if "dev" in __version__: + logger.warning( + "You are running a developmental version. This should not be used in production. (v%s)", + __version__, + ) + logger.line() + await self.threads.populate_cache() # closures @@ -565,7 +564,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"]) - discord.utils.utcnow()).total_seconds() if after <= 0: logger.debug("Closing thread for recipient %s.", recipient_id) after = 0 @@ -598,13 +597,13 @@ async def on_ready(self): { "open": False, "title": None, - "closed_at": str(datetime.utcnow()), + "closed_at": str(discord.utils.utcnow()), "close_message": "Channel has been deleted, no closer found.", "closer": { "id": str(self.user.id), "name": self.user.name, "discriminator": self.user.discriminator, - "avatar_url": str(self.user.avatar_url), + "avatar_url": self.user.display_avatar.url, "mod": True, }, }, @@ -614,25 +613,6 @@ async def on_ready(self): else: logger.debug("Failed to close thread with channel %s, skipping.", log["channel_id"]) - if self.config.get("data_collection"): - self.metadata_loop = tasks.Loop( - self.post_metadata, - seconds=0, - minutes=0, - hours=1, - count=None, - reconnect=True, - loop=None, - ) - self.metadata_loop.before_loop(self.before_post_metadata) - self.metadata_loop.start() - - self.autoupdate_loop = tasks.Loop( - self.autoupdate, seconds=0, minutes=0, hours=1, count=None, reconnect=True, loop=None - ) - self.autoupdate_loop.before_loop(self.before_autoupdate) - self.autoupdate_loop.start() - other_guilds = [guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild}] if any(other_guilds): logger.warning( @@ -690,7 +670,7 @@ async def retrieve_emoji(self) -> typing.Tuple[str, str]: def check_account_age(self, author: discord.Member) -> bool: account_age = self.config.get("account_age") - now = datetime.utcnow() + now = discord.utils.utcnow() try: min_account_age = author.created_at + account_age @@ -712,7 +692,7 @@ def check_account_age(self, author: discord.Member) -> bool: def check_guild_age(self, author: discord.Member) -> bool: guild_age = self.config.get("guild_age") - now = datetime.utcnow() + now = discord.utils.utcnow() if not hasattr(author, "joined_at"): logger.warning("Not in guild, cannot verify guild_age, %s.", author.name) @@ -742,7 +722,7 @@ def check_manual_blocked_roles(self, author: discord.Member) -> bool: if str(r.id) in self.blocked_roles: blocked_reason = self.blocked_roles.get(str(r.id)) or "" - now = datetime.utcnow() + now = discord.utils.utcnow() # etc "blah blah blah... until 2019-10-14T21:12:45.559948." end_time = re.search(r"until ([^`]+?)\.$", blocked_reason) @@ -772,7 +752,7 @@ def check_manual_blocked(self, author: discord.Member) -> bool: return True blocked_reason = self.blocked_users.get(str(author.id)) or "" - now = datetime.utcnow() + now = discord.utils.utcnow() if blocked_reason.startswith("System Message:"): # Met the limits already, otherwise it would've been caught by the previous checks @@ -861,7 +841,7 @@ async def is_blocked( async def get_thread_cooldown(self, author: discord.Member): thread_cooldown = self.config.get("thread_cooldown") - now = datetime.utcnow() + now = discord.utils.utcnow() if thread_cooldown == isodate.Duration(): return @@ -932,7 +912,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: color=self.error_color, description=self.config["disabled_new_thread_response"], ) - embed.set_footer(text=self.config["disabled_new_thread_footer"], icon_url=self.guild.icon_url) + embed.set_footer(text=self.config["disabled_new_thread_footer"], icon_url=self.guild.icon.url) logger.info("A new thread was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) return await message.channel.send(embed=embed) @@ -947,7 +927,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: ) embed.set_footer( text=self.config["disabled_current_thread_footer"], - icon_url=self.guild.icon_url, + icon_url=self.guild.icon.url, ) logger.info("A message was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) @@ -982,7 +962,7 @@ async def get_contexts(self, message, *, cls=commands.Context): ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) thread = await self.threads.find(channel=ctx.channel) - if self._skip_check(message.author.id, self.user.id): + if message.author.id == self.user.id: # type: ignore return [ctx] prefixes = await self.get_prefix() @@ -1139,7 +1119,7 @@ async def on_message(self, message): color=self.main_color, ) if self.config["show_timestamp"]: - em.timestamp = datetime.utcnow() + em.timestamp = discord.utils.utcnow() if not self.config["silent_alert_on_mention"]: content = self.config["mention"] @@ -1329,7 +1309,7 @@ async def handle_react_to_contact(self, payload): ) embed.set_footer( text=self.config["disabled_new_thread_footer"], - icon_url=self.guild.icon_url, + icon_url=self.guild.icon.url, ) logger.info( "A new thread using react to contact was blocked from %s due to disabled Modmail.", @@ -1431,7 +1411,13 @@ async def on_message_delete(self, message): return message = message[0] embed = message.embeds[0] - embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=embed.footer.icon_url) + + if embed.footer.icon: + icon_url = embed.footer.icon.url + else: + icon_url = discord.Embed.Empty + + embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=icon_url) await message.edit(embed=embed) return @@ -1521,21 +1507,23 @@ async def on_command_error(self, context, exception): else: logger.error("Unexpected exception:", exc_info=exception) + @tasks.loop(hours=1) async def post_metadata(self): info = await self.application_info() + delta = discord.utils.utcnow() - self.start_time data = { "bot_id": self.user.id, "bot_name": str(self.user), - "avatar_url": str(self.user.avatar_url), + "avatar_url": self.user.display_avatar.url, "guild_id": self.guild_id, "guild_name": self.guild.name, "member_count": len(self.guild.members), - "uptime": (datetime.utcnow() - self.start_time).total_seconds(), + "uptime": delta.total_seconds(), "latency": f"{self.ws.latency * 1000:.4f}", "version": str(self.version), "selfhosted": True, - "last_updated": str(datetime.utcnow()), + "last_updated": str(discord.utils.utcnow()), } if info.team is not None: @@ -1552,13 +1540,16 @@ async def post_metadata(self): async with self.session.post("https://api.modmail.dev/metadata", json=data): logger.debug("Uploading metadata to Modmail server.") + @post_metadata.before_loop async def before_post_metadata(self): await self.wait_for_connected() + if not self.config.get("data_collection") or not self.guild: + self.post_metadata.cancel() + logger.debug("Starting metadata loop.") logger.line("debug") - if not self.guild: - self.metadata_loop.cancel() + @tasks.loop(hours=1) async def autoupdate(self): changelog = await Changelog.from_url(self) latest = changelog.latest_version @@ -1637,6 +1628,7 @@ async def autoupdate(self): await channel.send(embed=embed) return await self.close() + @autoupdate.before_loop async def before_autoupdate(self): await self.wait_for_connected() logger.debug("Starting autoupdate loop") @@ -1696,9 +1688,9 @@ def main(): pass # check discord version - if discord.__version__ != "1.7.3": + if discord.__version__ != "2.0.0a": logger.error( - "Dependencies are not updated, run pipenv install. discord.py version expected 1.7.3, received %s", + "Dependencies are not updated, run pipenv install. discord.py version expected 2.0.0a, received %s", discord.__version__, ) sys.exit(0) diff --git a/cogs/modmail.py b/cogs/modmail.py index 6da8ce0772..60c18594ac 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -158,7 +158,7 @@ async def snippet(self, ctx, *, name: str.lower = None): 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 add" to add a snippet.') - embed.set_author(name="Snippets", icon_url=ctx.guild.icon_url) + embed.set_author(name="Snippets", icon_url=ctx.guild.icon.url) return await ctx.send(embed=embed) embeds = [] @@ -166,7 +166,7 @@ async def snippet(self, ctx, *, name: str.lower = None): 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) + embed.set_author(name="Snippets", icon_url=ctx.guild.icon.url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -396,7 +396,7 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): thread = ctx.thread - now = datetime.utcnow() + now = discord.utils.utcnow() close_after = (after.dt - now).total_seconds() if after else 0 message = after.arg if after else None @@ -643,7 +643,7 @@ def format_log_embeds(self, logs, avatar_url): 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.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=discord.utils.utcnow())) closer = entry.get("closer") if closer is None: closer_msg = "Unknown" @@ -745,8 +745,8 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=str(ctx.author), icon_url=ctx.author.avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar.url) for u in users: await u.send(embed=em) @@ -761,8 +761,8 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer(text=f"{users[0]}", icon_url=users[0].display_avatar.url) for i in ctx.thread.recipients: if i not in users: @@ -834,8 +834,8 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=str(ctx.author), icon_url=ctx.author.avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar.url) for u in users: await u.send(embed=em) @@ -850,8 +850,8 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer(text=f"{users[0]}", icon_url=users[0].display_avatar.url) for i in ctx.thread.recipients: if i not in users: @@ -914,7 +914,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() + em.timestamp = discord.utils.utcnow() tag = self.bot.config["mod_tag"] if tag is None: @@ -924,7 +924,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, name = tag avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.guild.icon_url + avatar_url = self.bot.guild.icon.url em.set_footer(text=name, icon_url=avatar_url) for u in users: @@ -940,8 +940,8 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer(text=f"{users[0]}", icon_url=users[0].display_avatar.url) for i in ctx.thread.recipients: if i not in users: @@ -999,7 +999,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() + em.timestamp = discord.utils.utcnow() tag = self.bot.config["mod_tag"] if tag is None: @@ -1009,7 +1009,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro name = tag avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.guild.icon_url + avatar_url = self.bot.guild.icon.url em.set_footer(text=name, icon_url=avatar_url) for u in users: @@ -1025,8 +1025,8 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer(text=f"{users[0]}", icon_url=users[0].display_avatar.url) for i in ctx.thread.recipients: if i not in users: @@ -1086,7 +1086,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): user = user if user is not None else ctx.author entries = await self.bot.api.search_closed_by(user.id) - embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon.url) if not embeds: embed = discord.Embed( @@ -1136,7 +1136,7 @@ async def logs_responded(self, ctx, *, user: User = None): entries = await self.bot.api.get_responded_logs(user.id) - embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon.url) if not embeds: embed = discord.Embed( @@ -1161,7 +1161,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): entries = await self.bot.api.search_by_text(query, limit) - embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon_url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon.url) if not embeds: embed = discord.Embed( @@ -1452,8 +1452,8 @@ async def contact( color=self.bot.main_color, ) if self.bot.config["show_timestamp"]: - em.timestamp = datetime.utcnow() - em.set_footer(text=f"{creator}", icon_url=creator.avatar_url) + em.timestamp = discord.utils.utcnow() + em.set_footer(text=f"{creator}", icon_url=creator.display_avatar.url) for u in users: await u.send(embed=em) diff --git a/cogs/plugins.py b/cogs/plugins.py index 4cdd0aba3f..e9adf2a240 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -730,7 +730,7 @@ async def plugins_registry_compact(self, ctx): for page in pages: embed = discord.Embed(color=self.bot.main_color, description=page) - embed.set_author(name="Plugin Registry", icon_url=self.bot.user.avatar_url) + embed.set_author(name="Plugin Registry", icon_url=self.bot.user.display_avatar.url) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) diff --git a/cogs/utility.py b/cogs/utility.py index bc813e9d6e..7278c350fc 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -5,7 +5,6 @@ import re import traceback from contextlib import redirect_stdout -from datetime import datetime from difflib import get_close_matches from io import BytesIO, StringIO from itertools import takewhile, zip_longest @@ -54,7 +53,7 @@ async def command_callback(self, ctx, *, command=None): async def format_cog_help(self, cog, *, no_cog=False): bot = self.context.bot - prefix = self.clean_prefix + prefix = self.context.clean_prefix formats = [""] for cmd in await self.filter_commands( @@ -92,7 +91,7 @@ async def format_cog_help(self, cog, *, no_cog=False): continued = " (Continued)" if embeds else "" 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_author(name=name + continued, icon_url=bot.user.display_avatar.url) embed.set_footer( text=f'Type "{prefix}{self.command_attrs["name"]} command" ' @@ -102,7 +101,7 @@ async def format_cog_help(self, cog, *, no_cog=False): return embeds def process_help_msg(self, help_: str): - return help_.format(prefix=self.clean_prefix) if help_ else "No help message." + return help_.format(prefix=self.context.clean_prefix) if help_ else "No help message." async def send_bot_help(self, mapping): embeds = [] @@ -174,7 +173,7 @@ async def send_group_help(self, group): embed.add_field(name="Sub Command(s)", value=format_[:1024], inline=False) embed.set_footer( - text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} command" ' + text=f'Type "{self.context.clean_prefix}{self.command_attrs["name"]} command" ' "for more info on a command." ) @@ -216,7 +215,7 @@ async def send_error_message(self, error): embed.add_field(name=f"Step {i}:", value=val) embed.set_footer( - text=f'Type "{self.clean_prefix}{self.command_attrs["name"]} alias" ' + text=f'Type "{self.context.clean_prefix}{self.command_attrs["name"]} alias" ' "for more details on aliases." ) return await self.get_destination().send(embed=embed) @@ -238,7 +237,7 @@ async def send_error_message(self, error): else: embed.title = "Cannot find command or category" embed.set_footer( - text=f'Type "{self.clean_prefix}{self.command_attrs["name"]}" ' + text=f'Type "{self.context.clean_prefix}{self.command_attrs["name"]}" ' "for a list of all available commands." ) await self.get_destination().send(embed=embed) @@ -303,13 +302,13 @@ async def changelog(self, ctx, version: str.lower = ""): @utils.trigger_typing async def about(self, ctx): """Shows information about this bot.""" - embed = discord.Embed(color=self.bot.main_color, timestamp=datetime.utcnow()) + embed = discord.Embed(color=self.bot.main_color, timestamp=discord.utils.utcnow()) embed.set_author( name="Modmail - About", - icon_url=self.bot.user.avatar_url, + icon_url=self.bot.user.display_avatar.url, url="https://discord.gg/F34cRU8", ) - embed.set_thumbnail(url=self.bot.user.avatar_url) + embed.set_thumbnail(url=self.bot.user.display_avatar.url) desc = "This is an open source Discord bot that serves as a means for " desc += "members to easily communicate with server administrators in " @@ -848,7 +847,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.display_avatar.url) else: embed = discord.Embed( @@ -865,7 +864,7 @@ async def config_get(self, ctx, *, key: str.lower = None): 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) + embed.set_author(name="Current config(s):", icon_url=self.bot.user.display_avatar.url) config = self.bot.config.filter_default(self.bot.config) for name, value in config.items(): @@ -1006,7 +1005,7 @@ async def alias(self, ctx, *, name: str.lower = None): 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) + embed.set_author(name="Aliases", icon_url=ctx.guild.icon.url) return await ctx.send(embed=embed) embeds = [] @@ -1014,7 +1013,7 @@ async def alias(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = utils.format_description(i, names) embed = discord.Embed(color=self.bot.main_color, description=description) - embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon_url) + embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon.url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -1595,7 +1594,7 @@ async def permissions_get( for name, level in takewhile(lambda x: x is not None, items) ) embed = discord.Embed(color=self.bot.main_color, description=description) - embed.set_author(name="Permission Overrides", icon_url=ctx.guild.icon_url) + embed.set_author(name="Permission Overrides", icon_url=ctx.guild.icon.url) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) diff --git a/core/changelog.py b/core/changelog.py index 7c9af2e1bb..4ede9f09f1 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -90,14 +90,14 @@ 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, + icon_url=self.bot.user.display_avatar.url, url=self.url, ) for name, value in self.fields.items(): embed.add_field(name=name, value=truncate(value, 1024), inline=False) embed.set_footer(text=f"Current version: v{self.bot.version}") - embed.set_thumbnail(url=self.bot.user.avatar_url) + embed.set_thumbnail(url=self.bot.user.display_avatar.url) return embed diff --git a/core/clients.py b/core/clients.py index 489954e3e4..5caf58f4d2 100644 --- a/core/clients.py +++ b/core/clients.py @@ -1,9 +1,9 @@ import secrets import sys -from datetime import datetime from json import JSONDecodeError from typing import Union, Optional +import discord from discord import Member, DMChannel, TextChannel, Message from discord.ext import commands @@ -507,7 +507,7 @@ async def create_log_entry(self, recipient: Member, channel: TextChannel, creato "_id": key, "key": key, "open": True, - "created_at": str(datetime.utcnow()), + "created_at": str(discord.utils.utcnow()), "closed_at": None, "channel_id": str(channel.id), "guild_id": str(self.bot.guild_id), @@ -516,14 +516,14 @@ async def create_log_entry(self, recipient: Member, channel: TextChannel, creato "id": str(recipient.id), "name": recipient.name, "discriminator": recipient.discriminator, - "avatar_url": str(recipient.avatar_url), + "avatar_url": recipient.display_avatar.url, "mod": False, }, "creator": { "id": str(creator.id), "name": creator.name, "discriminator": creator.discriminator, - "avatar_url": str(creator.avatar_url), + "avatar_url": creator.display_avatar.url, "mod": isinstance(creator, Member), }, "closer": None, @@ -585,7 +585,7 @@ async def append_log( "id": str(message.author.id), "name": message.author.name, "discriminator": message.author.discriminator, - "avatar_url": str(message.author.avatar_url), + "avatar_url": message.author.display_avatar.url, "mod": not isinstance(message.channel, DMChannel), }, "content": message.content, @@ -635,7 +635,7 @@ async def create_note(self, recipient: Member, message: Message, message_id: Uni "id": str(message.author.id), "name": message.author.name, "discriminator": message.author.discriminator, - "avatar_url": str(message.author.avatar_url), + "avatar_url": message.author.display_avatar.url, }, "message": message.content, "message_id": str(message_id), @@ -666,7 +666,7 @@ async def update_repository(self) -> dict: "data": data, "user": { "username": user.username, - "avatar_url": user.avatar_url, + "avatar_url": user.display_avatar.url, "url": user.url, }, } @@ -680,7 +680,7 @@ async def get_user_info(self) -> Optional[dict]: return { "user": { "username": user.username, - "avatar_url": user.avatar_url, + "avatar_url": user.display_avatar.url, "url": user.url, } } diff --git a/core/paginator.py b/core/paginator.py index 7ba1c98b60..934b4670be 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -209,7 +209,13 @@ def __init__(self, ctx: commands.Context, *embeds, **options): footer_text = f"Page {i + 1} of {len(self.pages)}" if embed.footer.text: footer_text = footer_text + " • " + embed.footer.text - embed.set_footer(text=footer_text, icon_url=embed.footer.icon_url) + + if embed.footer.icon: + icon_url = embed.footer.icon.url + else: + icon_url = Embed.Empty + + embed.set_footer(text=footer_text, icon_url=icon_url) def add_page(self, item: Embed) -> None: if isinstance(item, Embed): @@ -241,7 +247,13 @@ def _set_footer(self): footer_text = f"Page {self.current+1} of {len(self.pages)}" if self.footer_text: footer_text = footer_text + " • " + self.footer_text - self.embed.set_footer(text=footer_text, icon_url=self.embed.footer.icon_url) + + if self.embed.footer.icon: + icon_url = self.embed.footer.icon.url + else: + icon_url = Embed.Empty + + self.embed.set_footer(text=footer_text, icon_url=icon_url) async def _create_base(self, item: str) -> None: self._set_footer() diff --git a/core/thread.py b/core/thread.py index 536e68b595..2550ab0ad9 100644 --- a/core/thread.py +++ b/core/thread.py @@ -4,7 +4,7 @@ import re import time import typing -from datetime import datetime, timedelta +from datetime import timedelta from types import SimpleNamespace import isodate @@ -159,7 +159,7 @@ async def setup(self, *, creator=None, category=None, initial_message=None): category = category or self.bot.main_category if category is not None: - overwrites = None + overwrites = {} try: channel = await create_thread_channel(self.bot, recipient, category, overwrites) @@ -224,7 +224,7 @@ async def send_recipient_genesis_message(): else: footer = self.bot.config["thread_creation_footer"] - embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) + embed.set_footer(text=footer, icon_url=self.bot.guild.icon.url) embed.title = self.bot.config["thread_creation_title"] if creator is None or creator == recipient: @@ -250,7 +250,7 @@ class Author: name = author["name"] id = author["id"] discriminator = author["discriminator"] - avatar_url = author["avatar_url"] + display_avatar = SimpleNamespace(url=author["avatar_url"]) data = { "id": round(time.time() * 1000 - discord.utils.DISCORD_EPOCH) << 22, @@ -264,7 +264,7 @@ class Author: "content": note["message"], "author": Author(), } - message = discord.Message(state=State(), channel=None, data=data) + message = discord.Message(state=State(), channel=self.channel, data=data) ids[note["_id"]] = str((await self.note(message, persistent=True, thread_creation=True)).id) await self.bot.api.update_note_ids(ids) @@ -290,7 +290,7 @@ def _format_info_embed(self, user, log_url, log_count, color): """Get information about a member of a server supports users from the guild or not.""" member = self.bot.guild.get_member(user.id) - time = datetime.utcnow() + time = discord.utils.utcnow() # key = log_url.split('/')[-1] @@ -329,7 +329,7 @@ def _format_info_embed(self, user, log_url, log_count, color): else: footer = f"User ID: {user.id}" - embed.set_author(name=str(user), icon_url=user.avatar_url, url=log_url) + embed.set_author(name=str(user), icon_url=user.display_avatar.url, url=log_url) # embed.set_thumbnail(url=avi) if member is not None: @@ -382,7 +382,7 @@ async def close( if after > 0: # TODO: Add somewhere to clean up broken closures # (when channel is already deleted) - now = datetime.utcnow() + now = discord.utils.utcnow() items = { # 'initiation_time': now.isoformat(), "time": (now + timedelta(seconds=after)).isoformat(), @@ -425,14 +425,14 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, { "open": False, "title": match_title(self.channel.topic), - "closed_at": str(datetime.utcnow()), + "closed_at": str(discord.utils.utcnow()), "nsfw": self.channel.nsfw, "close_message": message, "closer": { "id": str(closer.id), "name": closer.name, "discriminator": closer.discriminator, - "avatar_url": str(closer.avatar_url), + "avatar_url": closer.display_avatar.url, "mod": True, }, }, @@ -483,8 +483,8 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, event = "Thread Closed as Scheduled" if scheduled else "Thread Closed" # embed.set_author(name=f"Event: {event}", url=log_url) - embed.set_footer(text=f"{event} by {_closer}", icon_url=closer.avatar_url) - embed.timestamp = datetime.utcnow() + embed.set_footer(text=f"{event} by {_closer}", icon_url=closer.display_avatar.url) + embed.timestamp = discord.utils.utcnow() tasks = [self.bot.config.update()] @@ -498,7 +498,7 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, color=self.bot.error_color, ) if self.bot.config["show_timestamp"]: - embed.timestamp = datetime.utcnow() + embed.timestamp = discord.utils.utcnow() if not message: if self.id == closer.id: @@ -512,7 +512,7 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, embed.description = message footer = self.bot.config["thread_close_footer"] - embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) + embed.set_footer(text=footer, icon_url=self.bot.guild.icon.url) if not silent: for user in self.recipients: @@ -558,7 +558,7 @@ async def _restart_close_timer(self): # Set timeout seconds seconds = timeout.total_seconds() # seconds = 20 # Uncomment to debug with just 20 seconds - reset_time = datetime.utcnow() + timedelta(seconds=seconds) + reset_time = discord.utils.utcnow() + timedelta(seconds=seconds) human_time = human_timedelta(dt=reset_time) if self.bot.config.get("thread_auto_close_silently"): @@ -943,7 +943,7 @@ async def send( name = tag avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.guild.icon_url + avatar_url = self.bot.guild.icon.url embed.set_author( name=name, icon_url=avatar_url, @@ -952,7 +952,7 @@ async def send( else: # Normal message name = str(author) - avatar_url = author.avatar_url + avatar_url = author.display_avatar.url embed.set_author( name=name, icon_url=avatar_url, diff --git a/core/time.py b/core/time.py index bdec8d2549..376f2074eb 100644 --- a/core/time.py +++ b/core/time.py @@ -6,6 +6,7 @@ import re from datetime import datetime +import discord from discord.ext.commands import BadArgument, Converter import parsedatetime as pdt @@ -13,6 +14,7 @@ from core.models import getLogger + logger = getLogger(__name__) @@ -36,7 +38,7 @@ def __init__(self, argument): raise BadArgument("Invalid time provided.") data = {k: int(v) for k, v in match.groupdict(default="0").items()} - now = datetime.utcnow() + now = discord.utils.utcnow() self.dt = now + relativedelta(**data) @@ -50,7 +52,7 @@ class HumanTime: calendar = pdt.Calendar(version=pdt.VERSION_CONTEXT_STYLE) def __init__(self, argument): - now = datetime.utcnow() + now = discord.utils.utcnow() dt, status = self.calendar.parseDT(argument, sourceTime=now) if not status.hasDateOrTime: raise BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".') @@ -104,7 +106,7 @@ def convert(self, ctx, argument): try: calendar = HumanTime.calendar regex = ShortTime.compiled - self.dt = self.now = datetime.utcnow() + self.dt = self.now = discord.utils.utcnow() match = regex.match(argument) if match is not None and match.group(0): @@ -186,7 +188,7 @@ async def convert(self, ctx, argument): def human_timedelta(dt, *, source=None): - now = source or datetime.utcnow() + now = source or discord.utils.utcnow() if dt > now: delta = relativedelta(dt, now) suffix = "" diff --git a/core/utils.py b/core/utils.py index 1440b9c8eb..9b56cbe31d 100644 --- a/core/utils.py +++ b/core/utils.py @@ -245,7 +245,11 @@ def parse_channel_topic(text: str) -> typing.Tuple[typing.Optional[str], int, ty A tuple of title, user ID, and other recipients IDs. """ title, user_id, other_ids = None, -1, [] - match = TOPIC_REGEX.search(text) + if isinstance(text, str): + match = TOPIC_REGEX.search(text) + else: + match = None + if match is not None: groupdict = match.groupdict() title = groupdict["title"] diff --git a/requirements.txt b/requirements.txt index d802cc0c46..fb43d23e2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,19 +11,19 @@ async-timeout==3.0.1; python_full_version >= '3.5.3' attrs==21.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' colorama==0.4.4 -discord.py==1.7.3 -dnspython==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +dnspython==2.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' emoji==1.2.0 -idna==3.2; python_version >= '3.5' +git+https://github.com/Rapptz/discord.py.git@45d498c1b76deaf3b394d17ccf56112fa691d160#egg=discord-py +idna==3.3; python_version >= '3.5' isodate==0.6.0 motor==2.4.0 -multidict==5.1.0; python_version >= '3.6' +multidict==5.2.0; python_version >= '3.6' natural==0.2.0 parsedatetime==2.6 -pymongo[srv]==3.11.4 -python-dateutil==2.8.1 +pymongo[srv]==3.12.1 +python-dateutil==2.8.2 python-dotenv==0.18.0 six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -typing-extensions==3.10.0.0 -uvloop==0.15.2; sys_platform != 'win32' -yarl==1.6.3; python_version >= '3.6' +typing-extensions==4.0.0; python_version >= '3.6' +uvloop==0.16.0; sys_platform != 'win32' +yarl==1.7.2; python_version >= '3.6' \ No newline at end of file From 96a9af069554050387e2b02c69949c07e1ad0b40 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 00:58:58 +0800 Subject: [PATCH 0986/1221] Update bandit baseline --- .bandit_baseline.json | 62 +++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/.bandit_baseline.json b/.bandit_baseline.json index 28a4e47b9c..12aa666e27 100644 --- a/.bandit_baseline.json +++ b/.bandit_baseline.json @@ -1,6 +1,6 @@ { "errors": [], - "generated_at": "2020-11-26T11:00:36Z", + "generated_at": "2021-11-20T16:58:19Z", "metrics": { "./bot.py": { "CONFIDENCE.HIGH": 1.0, @@ -11,7 +11,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1321, + "loc": 1412, "nosec": 0 }, "./cogs/modmail.py": { @@ -23,7 +23,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1273, + "loc": 1688, "nosec": 0 }, "./cogs/plugins.py": { @@ -35,7 +35,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 578, + "loc": 597, "nosec": 0 }, "./cogs/utility.py": { @@ -47,7 +47,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 1.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1755, + "loc": 1767, "nosec": 0 }, "./core/_color_data.py": { @@ -71,7 +71,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 155, + "loc": 159, "nosec": 0 }, "./core/checks.py": { @@ -83,7 +83,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 90, + "loc": 105, "nosec": 0 }, "./core/clients.py": { @@ -95,7 +95,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 587, + "loc": 598, "nosec": 0 }, "./core/config.py": { @@ -107,7 +107,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 352, + "loc": 377, "nosec": 0 }, "./core/decorators.py": { @@ -131,7 +131,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 202, + "loc": 204, "nosec": 0 }, "./core/paginator.py": { @@ -143,7 +143,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 209, + "loc": 217, "nosec": 0 }, "./core/thread.py": { @@ -155,7 +155,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 996, + "loc": 1138, "nosec": 0 }, "./core/time.py": { @@ -167,7 +167,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 158, + "loc": 157, "nosec": 0 }, "./core/utils.py": { @@ -179,19 +179,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 282, - "nosec": 0 - }, - "./plugins/kyb3r/modmail-plugins/profanity-filter-master/profanity-filter.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 81, + "loc": 389, "nosec": 0 }, "_totals": { @@ -203,20 +191,20 @@ "SEVERITY.LOW": 5.0, "SEVERITY.MEDIUM": 1.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 9214, + "loc": 9983, "nosec": 0 } }, "results": [ { - "code": "11 from datetime import datetime\n12 from subprocess import PIPE\n13 from types import SimpleNamespace\n", + "code": "13 from datetime import datetime\n14 from subprocess import PIPE\n15 from types import SimpleNamespace\n", "filename": "./bot.py", "issue_confidence": "HIGH", "issue_severity": "LOW", "issue_text": "Consider possible security implications associated with PIPE module.", - "line_number": 12, + "line_number": 14, "line_range": [ - 12 + 14 ], "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", "test_id": "B404", @@ -238,28 +226,28 @@ "test_name": "blacklist" }, { - "code": "13 from json import JSONDecodeError, loads\n14 from subprocess import PIPE\n15 from textwrap import indent\n", + "code": "11 from json import JSONDecodeError, loads\n12 from subprocess import PIPE\n13 from textwrap import indent\n", "filename": "./cogs/utility.py", "issue_confidence": "HIGH", "issue_severity": "LOW", "issue_text": "Consider possible security implications associated with PIPE module.", - "line_number": 14, + "line_number": 12, "line_range": [ - 14 + 12 ], "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", "test_id": "B404", "test_name": "blacklist" }, { - "code": "2039 try:\n2040 exec(to_compile, env) # pylint: disable=exec-used\n2041 except Exception as exc:\n", + "code": "2060 try:\n2061 exec(to_compile, env) # pylint: disable=exec-used\n2062 except Exception as exc:\n", "filename": "./cogs/utility.py", "issue_confidence": "HIGH", "issue_severity": "MEDIUM", "issue_text": "Use of exec detected.", - "line_number": 2040, + "line_number": 2061, "line_range": [ - 2040 + 2061 ], "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b102_exec_used.html", "test_id": "B102", @@ -280,7 +268,7 @@ "test_name": "blacklist" }, { - "code": "67 \n68 def __init__(self, bot, access_token: str = \"\", username: str = \"\", **kwargs):\n69 self.bot = bot\n70 self.session = bot.session\n71 self.headers: dict = None\n72 self.access_token = access_token\n73 self.username = username\n74 self.avatar_url: str = kwargs.pop(\"avatar_url\", \"\")\n75 self.url: str = kwargs.pop(\"url\", \"\")\n76 if self.access_token:\n77 self.headers = {\"Authorization\": \"token \" + str(access_token)}\n78 \n79 @property\n80 def BRANCH(self):\n", + "code": "67 \n68 def __init__(self, bot, access_token: str = \"\", username: str = \"\", **kwargs):\n69 self.bot = bot\n70 self.session = bot.session\n71 self.headers: Optional[dict] = None\n72 self.access_token = access_token\n73 self.username = username\n74 self.avatar_url: str = kwargs.pop(\"avatar_url\", \"\")\n75 self.url: str = kwargs.pop(\"url\", \"\")\n76 if self.access_token:\n77 self.headers = {\"Authorization\": \"token \" + str(access_token)}\n78 \n79 @property\n80 def BRANCH(self):\n", "filename": "./core/clients.py", "issue_confidence": "MEDIUM", "issue_severity": "LOW", From a1aacbfb817f6410d9c8b4fce41bbd0e1e55b6b3 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 20 Nov 2021 19:07:11 +0200 Subject: [PATCH 0987/1221] Update bandit baseline: --- .bandit_baseline.json | 60 +++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/.bandit_baseline.json b/.bandit_baseline.json index 28a4e47b9c..76153c873f 100644 --- a/.bandit_baseline.json +++ b/.bandit_baseline.json @@ -1,6 +1,6 @@ { "errors": [], - "generated_at": "2020-11-26T11:00:36Z", + "generated_at": "2021-11-20T17:06:28Z", "metrics": { "./bot.py": { "CONFIDENCE.HIGH": 1.0, @@ -11,7 +11,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1321, + "loc": 1406, "nosec": 0 }, "./cogs/modmail.py": { @@ -23,7 +23,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1273, + "loc": 1678, "nosec": 0 }, "./cogs/plugins.py": { @@ -35,7 +35,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 578, + "loc": 597, "nosec": 0 }, "./cogs/utility.py": { @@ -47,7 +47,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 1.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1755, + "loc": 1768, "nosec": 0 }, "./core/_color_data.py": { @@ -71,7 +71,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 155, + "loc": 159, "nosec": 0 }, "./core/checks.py": { @@ -83,7 +83,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 90, + "loc": 105, "nosec": 0 }, "./core/clients.py": { @@ -95,7 +95,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 587, + "loc": 598, "nosec": 0 }, "./core/config.py": { @@ -107,7 +107,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 352, + "loc": 375, "nosec": 0 }, "./core/decorators.py": { @@ -131,7 +131,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 202, + "loc": 204, "nosec": 0 }, "./core/paginator.py": { @@ -155,7 +155,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 996, + "loc": 1097, "nosec": 0 }, "./core/time.py": { @@ -167,7 +167,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 158, + "loc": 156, "nosec": 0 }, "./core/utils.py": { @@ -179,19 +179,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 282, - "nosec": 0 - }, - "./plugins/kyb3r/modmail-plugins/profanity-filter-master/profanity-filter.py": { - "CONFIDENCE.HIGH": 0.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 0.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, - "SEVERITY.MEDIUM": 0.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 81, + "loc": 351, "nosec": 0 }, "_totals": { @@ -203,20 +191,20 @@ "SEVERITY.LOW": 5.0, "SEVERITY.MEDIUM": 1.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 9214, + "loc": 9878, "nosec": 0 } }, "results": [ { - "code": "11 from datetime import datetime\n12 from subprocess import PIPE\n13 from types import SimpleNamespace\n", + "code": "13 from datetime import datetime\n14 from subprocess import PIPE\n15 from types import SimpleNamespace\n", "filename": "./bot.py", "issue_confidence": "HIGH", "issue_severity": "LOW", "issue_text": "Consider possible security implications associated with PIPE module.", - "line_number": 12, + "line_number": 14, "line_range": [ - 12 + 14 ], "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", "test_id": "B404", @@ -238,28 +226,28 @@ "test_name": "blacklist" }, { - "code": "13 from json import JSONDecodeError, loads\n14 from subprocess import PIPE\n15 from textwrap import indent\n", + "code": "12 from json import JSONDecodeError, loads\n13 from subprocess import PIPE\n14 from textwrap import indent\n", "filename": "./cogs/utility.py", "issue_confidence": "HIGH", "issue_severity": "LOW", "issue_text": "Consider possible security implications associated with PIPE module.", - "line_number": 14, + "line_number": 13, "line_range": [ - 14 + 13 ], "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", "test_id": "B404", "test_name": "blacklist" }, { - "code": "2039 try:\n2040 exec(to_compile, env) # pylint: disable=exec-used\n2041 except Exception as exc:\n", + "code": "2061 try:\n2062 exec(to_compile, env) # pylint: disable=exec-used\n2063 except Exception as exc:\n", "filename": "./cogs/utility.py", "issue_confidence": "HIGH", "issue_severity": "MEDIUM", "issue_text": "Use of exec detected.", - "line_number": 2040, + "line_number": 2062, "line_range": [ - 2040 + 2062 ], "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b102_exec_used.html", "test_id": "B102", @@ -280,7 +268,7 @@ "test_name": "blacklist" }, { - "code": "67 \n68 def __init__(self, bot, access_token: str = \"\", username: str = \"\", **kwargs):\n69 self.bot = bot\n70 self.session = bot.session\n71 self.headers: dict = None\n72 self.access_token = access_token\n73 self.username = username\n74 self.avatar_url: str = kwargs.pop(\"avatar_url\", \"\")\n75 self.url: str = kwargs.pop(\"url\", \"\")\n76 if self.access_token:\n77 self.headers = {\"Authorization\": \"token \" + str(access_token)}\n78 \n79 @property\n80 def BRANCH(self):\n", + "code": "67 \n68 def __init__(self, bot, access_token: str = \"\", username: str = \"\", **kwargs):\n69 self.bot = bot\n70 self.session = bot.session\n71 self.headers: Optional[dict] = None\n72 self.access_token = access_token\n73 self.username = username\n74 self.avatar_url: str = kwargs.pop(\"avatar_url\", \"\")\n75 self.url: str = kwargs.pop(\"url\", \"\")\n76 if self.access_token:\n77 self.headers = {\"Authorization\": \"token \" + str(access_token)}\n78 \n79 @property\n80 def BRANCH(self):\n", "filename": "./core/clients.py", "issue_confidence": "MEDIUM", "issue_severity": "LOW", From 126455760831a019283943fc00ca75334a475da0 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 01:25:45 +0800 Subject: [PATCH 0988/1221] Support PNG/APNG stickers --- CHANGELOG.md | 1 + core/thread.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe9ec8e3e1..4e4cd95bde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Certain cases where fallback categories were not working as intended. ([PR #3109](https://github.com/kyb3r/modmail/pull/3109)) - `?contact` would create in a random category in silent mode. ([GH #3091](https://github.com/kyb3r/modmail/issues/3091), [PR #3092](https://github.com/kyb3r/modmail/pull/3092)) - Certain cases where `?close` would fail if closer isn't in cache. ([GH #3104](https://github.com/kyb3r/modmail/issues/3104), [PR #3105](https://github.com/kyb3r/modmail/pull/3105)) +- Stickers were not working in Modmail. ### Internal diff --git a/core/thread.py b/core/thread.py index 2550ab0ad9..eb0a23cd2c 100644 --- a/core/thread.py +++ b/core/thread.py @@ -989,8 +989,8 @@ async def send( images.extend(image_urls) images.extend( ( - str(i.image_url) if isinstance(i.image_url, discord.Asset) else i.image_url, - f"{i.name} Sticker", + i.url if i.format in (discord.StickerFormatType.png, discord.StickerFormatType.apng) else None, + i.name, True, ) for i in message.stickers @@ -1012,10 +1012,10 @@ async def send( if filename: if is_sticker: if url is None: - description = "Unable to retrieve sticker image" + description = f"{filename}: Unable to retrieve sticker image" else: - description = "\u200b" - embed.add_field(name=filename, value=description) + description = f"[{filename}]({url})" + embed.add_field(name="Sticker", value=description) else: embed.add_field(name="Image", value=f"[{filename}]({url})") embedded_image = True From 0ee363f64394f4ce9ceecc9b162a78e83b30288f Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sat, 20 Nov 2021 19:28:50 +0200 Subject: [PATCH 0989/1221] Update baseline --- .bandit_baseline.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bandit_baseline.json b/.bandit_baseline.json index 12aa666e27..c36cbc9f8e 100644 --- a/.bandit_baseline.json +++ b/.bandit_baseline.json @@ -1,6 +1,6 @@ { "errors": [], - "generated_at": "2021-11-20T16:58:19Z", + "generated_at": "2021-11-20T17:28:26Z", "metrics": { "./bot.py": { "CONFIDENCE.HIGH": 1.0, From 9121dce1c49fa3f64bd6b7ceb6fbef48d99c1463 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 01:31:31 +0800 Subject: [PATCH 0990/1221] Disable bandit --- .github/workflows/lints.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 145a83d489..091e012dd9 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -23,8 +23,8 @@ jobs: run: | python -m pip install --upgrade pip pipenv pipenv install --dev --system - - name: Bandit syntax check - run: bandit -r . -b .bandit_baseline.json + # - name: Bandit syntax check + # run: bandit -r . -b .bandit_baseline.json - name: Pylint run: pylint ./bot.py cogs/*.py core/*.py --exit-zero -r y continue-on-error: true From 50446d4fa02ae72d62ef713ad1dd1d54f179e7b8 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 01:33:49 +0800 Subject: [PATCH 0991/1221] Resolve linting --- core/thread.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index eb0a23cd2c..f7bf634435 100644 --- a/core/thread.py +++ b/core/thread.py @@ -989,7 +989,9 @@ async def send( images.extend(image_urls) images.extend( ( - i.url if i.format in (discord.StickerFormatType.png, discord.StickerFormatType.apng) else None, + i.url + if i.format in (discord.StickerFormatType.png, discord.StickerFormatType.apng) + else None, i.name, True, ) From f429c225ff5aeae5c05d41a5fa012848558602e5 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 01:40:35 +0800 Subject: [PATCH 0992/1221] Bump ver to 3.11.0-dev2 --- README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 81cc75929c..cf62cfe5d6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v3.11.0-dev1-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v3.11.0-dev2-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> diff --git a/bot.py b/bot.py index a0d4043b55..fbb6ec9605 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.11.0-dev1" +__version__ = "3.11.0-dev2" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 797650d754..f1de75c772 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.11.0-dev1' +version = '3.11.0-dev2' description = "Modmail is similar to Reddit's 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 fb2e9046e677eea0c5a2a0f0f5bcdb423a349d6a Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 16:37:07 +0800 Subject: [PATCH 0993/1221] check for g.name==None #3088 --- CHANGELOG.md | 1 + bot.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4cd95bde..75862c62da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?contact` would create in a random category in silent mode. ([GH #3091](https://github.com/kyb3r/modmail/issues/3091), [PR #3092](https://github.com/kyb3r/modmail/pull/3092)) - Certain cases where `?close` would fail if closer isn't in cache. ([GH #3104](https://github.com/kyb3r/modmail/issues/3104), [PR #3105](https://github.com/kyb3r/modmail/pull/3105)) - Stickers were not working in Modmail. +- Large server sizes results in Guild.name == None. ([GH #3088](https://github.com/kyb3r/modmail/issues/3088)) ### Internal diff --git a/bot.py b/bot.py index fbb6ec9605..fb9836010f 100644 --- a/bot.py +++ b/bot.py @@ -618,7 +618,7 @@ async def on_ready(self): logger.warning( "The bot is in more servers other than the main and staff server. " "This may cause data compromise (%s).", - ", ".join(guild.name for guild in other_guilds), + ", ".join(str(guild.name) for guild in other_guilds), ) logger.warning("If the external servers are valid, you may ignore this message.") From 33adfc30f7c4b309b21a45a76854126f13db84e6 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 17:10:01 +0800 Subject: [PATCH 0994/1221] Fix attachments in plain messages #3102 --- CHANGELOG.md | 1 + bot.py | 4 ++++ core/thread.py | 24 +++++++++++++----------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75862c62da..18935f9f23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Certain cases where `?close` would fail if closer isn't in cache. ([GH #3104](https://github.com/kyb3r/modmail/issues/3104), [PR #3105](https://github.com/kyb3r/modmail/pull/3105)) - Stickers were not working in Modmail. - Large server sizes results in Guild.name == None. ([GH #3088](https://github.com/kyb3r/modmail/issues/3088)) +- Attachments did not work on plain replies. ([GH #3102](https://github.com/kyb3r/modmail/issues/3102)) ### Internal diff --git a/bot.py b/bot.py index fb9836010f..c2e3ee5adf 100644 --- a/bot.py +++ b/bot.py @@ -50,6 +50,10 @@ logger = getLogger(__name__) +# # prevent "coroutine noop was never awaited" warning. +# asyncio.coroutines._DEBUG = False # type: ignore + + temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp") if not os.path.exists(temp_dir): os.mkdir(temp_dir) diff --git a/core/thread.py b/core/thread.py index f7bf634435..ff2ea791cd 100644 --- a/core/thread.py +++ b/core/thread.py @@ -4,6 +4,7 @@ import re import time import typing +import warnings from datetime import timedelta from types import SimpleNamespace @@ -835,6 +836,7 @@ async def reply( user_msg = await asyncio.gather(*user_msg_tasks) except Exception as e: logger.error("Message delivery failed:", exc_info=True) + user_msg = None if isinstance(e, discord.Forbidden): description = ( "Your message could not be delivered as " @@ -848,12 +850,10 @@ async def reply( "to an unknown error. Check `?debug` for " "more information" ) - tasks.append( - message.channel.send( - embed=discord.Embed( - color=self.bot.error_color, - description=description, - ) + msg = await message.channel.send( + embed=discord.Embed( + color=self.bot.error_color, + description=description, ) ) else: @@ -1095,17 +1095,19 @@ async def send( if plain: if from_mod and not isinstance(destination, discord.TextChannel): # Plain to user + with warnings.catch_warnings(): + # Catch coroutines not awaited warning + warnings.simplefilter("ignore") + additional_images = [] + if embed.footer.text: plain_message = f"**({embed.footer.text}) " else: plain_message = "**" plain_message += f"{embed.author.name}:** {embed.description}" files = [] - for i in embed.fields: - if "Image" in i.name: - async with self.bot.session.get(i.field[i.field.find("http") : -1]) as resp: - stream = io.BytesIO(await resp.read()) - files.append(discord.File(stream)) + for i in message.attachments: + files.append(await i.to_file()) msg = await destination.send(plain_message, files=files) else: From 2f762025d07c26092e90a2e4f5e45e5d6900364d Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 18:49:41 +0800 Subject: [PATCH 0995/1221] Support LOTTIE images, resolves #3119 --- CHANGELOG.md | 1 + Pipfile | 1 + Pipfile.lock | 167 +++++++++++++++++++++++++++++++++++++++++++++- README.md | 2 +- bot.py | 11 ++- core/changelog.py | 1 + core/thread.py | 58 +++++++++++++--- 7 files changed, 226 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18935f9f23..5db8a4b950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Stickers were not working in Modmail. - Large server sizes results in Guild.name == None. ([GH #3088](https://github.com/kyb3r/modmail/issues/3088)) - Attachments did not work on plain replies. ([GH #3102](https://github.com/kyb3r/modmail/issues/3102)) +- Support LOTTIE stickers. ([GH #3119](https://github.com/kyb3r/modmail/issues/3119)) ### Internal diff --git a/Pipfile b/Pipfile index 51569d26e5..a78db95d18 100644 --- a/Pipfile +++ b/Pipfile @@ -21,6 +21,7 @@ pymongo = {extras = ["srv"], version = "*"} # Required by motor python-dateutil = "~=2.8.1" python-dotenv = "~=0.18.0" uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} +lottie = {version = "==0.6.10", extras = ["pdf"]} [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index e1a77d86b5..1fa9e5e64a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "aaa53487befe4f19feda64131fbc5bde140c1ec1a59ff2656d2ad1da2c50b390" + "sha256": "29d6f377cdb874102deb4df1406ff6d0a66834fcb89660f3c9599a2343cda038" }, "pipfile-spec": 6, "requires": { @@ -75,6 +75,76 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==21.2.0" }, + "cairocffi": { + "hashes": [ + "sha256:108a3a7cb09e203bdd8501d9baad91d786d204561bd71e9364e8b34897c47b91" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.0" + }, + "cairosvg": { + "hashes": [ + "sha256:98c276b7e4f0caf01e5c7176765c104ffa1aa1461d63b2053b04ab663cf7052b", + "sha256:b0b9929cf5dba005178d746a8036fcf0025550f498ca54db61873322384783bc" + ], + "markers": "python_version >= '3.5'", + "version": "==2.5.2" + }, + "cffi": { + "hashes": [ + "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", + "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", + "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", + "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", + "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", + "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", + "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", + "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", + "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", + "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", + "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", + "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", + "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", + "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", + "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", + "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", + "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", + "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", + "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", + "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", + "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", + "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", + "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", + "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", + "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", + "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", + "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", + "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", + "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", + "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", + "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", + "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", + "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", + "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", + "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", + "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", + "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", + "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", + "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", + "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", + "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", + "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", + "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", + "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", + "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", + "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", + "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", + "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", + "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", + "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" + ], + "version": "==1.15.0" + }, "chardet": { "hashes": [ "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", @@ -91,6 +161,22 @@ "index": "pypi", "version": "==0.4.4" }, + "cssselect2": { + "hashes": [ + "sha256:2f4a9f20965367bae459e3bb42561f7927e0cfe5b7ea1692757cf67ef5d7dace", + "sha256:93fbb9af860e95dd40bf18c3b2b6ed99189a07c0f29ba76f9c5be71344664ec8" + ], + "markers": "python_version >= '3.6'", + "version": "==0.4.1" + }, + "defusedxml": { + "hashes": [ + "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.7.1" + }, "discord-py": { "git": "https://github.com/Rapptz/discord.py.git", "ref": "45d498c1b76deaf3b394d17ccf56112fa691d160" @@ -131,6 +217,16 @@ "index": "pypi", "version": "==0.6.0" }, + "lottie": { + "extras": [ + "pdf" + ], + "hashes": [ + "sha256:d3281d09a8db94da49f60267123b9e0473e3a51c282da2d5e61908d33843f1cc" + ], + "index": "pypi", + "version": "==0.6.10" + }, "motor": { "hashes": [ "sha256:1196db507142ef8f00d953efa2f37b39335ef2d72af6ce4fbccfd870b65c5e9f", @@ -232,6 +328,60 @@ "index": "pypi", "version": "==2.6" }, + "pillow": { + "hashes": [ + "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76", + "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585", + "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b", + "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8", + "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55", + "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc", + "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645", + "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff", + "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc", + "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b", + "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6", + "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20", + "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e", + "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a", + "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779", + "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02", + "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39", + "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f", + "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a", + "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409", + "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c", + "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488", + "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b", + "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d", + "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09", + "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b", + "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153", + "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9", + "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad", + "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df", + "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df", + "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed", + "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed", + "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698", + "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29", + "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649", + "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49", + "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b", + "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2", + "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a", + "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78" + ], + "markers": "python_version >= '3.6'", + "version": "==8.4.0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, "pymongo": { "extras": [ "srv" @@ -372,6 +522,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, + "tinycss2": { + "hashes": [ + "sha256:0353b5234bcaee7b1ac7ca3dea7e02cd338a9f8dcbb8f2dcd32a5795ec1e5f9a", + "sha256:fbdcac3044d60eb85fdb2aa840ece43cf7dbe798e373e6ee0be545d4d134e18a" + ], + "markers": "python_version >= '3.6'", + "version": "==1.1.0" + }, "typing-extensions": { "hashes": [ "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed", @@ -402,6 +560,13 @@ "markers": "sys_platform != 'win32'", "version": "==0.16.0" }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, "yarl": { "hashes": [ "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", diff --git a/README.md b/README.md index cf62cfe5d6..7492a53116 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v3.11.0-dev2-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v3.11.0-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> diff --git a/bot.py b/bot.py index c2e3ee5adf..38bd6423b2 100644 --- a/bot.py +++ b/bot.py @@ -50,9 +50,6 @@ logger = getLogger(__name__) -# # prevent "coroutine noop was never awaited" warning. -# asyncio.coroutines._DEBUG = False # type: ignore - temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp") if not os.path.exists(temp_dir): @@ -1691,6 +1688,14 @@ def main(): except ImportError: pass + try: + import cairosvg + except OSError: + logger.error( + "Unable to import cairosvg, install GTK Installer for Windows: https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases/latest" + ) + sys.exit(0) + # check discord version if discord.__version__ != "2.0.0a": logger.error( diff --git a/core/changelog.py b/core/changelog.py index 4ede9f09f1..a4f88ed323 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -97,6 +97,7 @@ def embed(self) -> Embed: for name, value in self.fields.items(): embed.add_field(name=name, value=truncate(value, 1024), inline=False) embed.set_footer(text=f"Current version: v{self.bot.version}") + embed.set_thumbnail(url=self.bot.user.display_avatar.url) return embed diff --git a/core/thread.py b/core/thread.py index ff2ea791cd..09f8bd3d41 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1,8 +1,11 @@ import asyncio +import base64 import copy +import functools import io import re import time +import traceback import typing import warnings from datetime import timedelta @@ -12,6 +15,8 @@ import discord from discord.ext.commands import MissingRequiredArgument, CommandError +from lottie.importers import importers as l_importers +from lottie.exporters import exporters as l_exporters from core.models import DMDisabled, DummyMessage, getLogger from core.time import human_timedelta @@ -987,16 +992,49 @@ async def send( if is_image_url(url, convert_size=False) ] images.extend(image_urls) - images.extend( - ( - i.url - if i.format in (discord.StickerFormatType.png, discord.StickerFormatType.apng) - else None, - i.name, - True, - ) - for i in message.stickers - ) + + def lottie_to_png(data): + importer = l_importers.get("lottie") + exporter = l_exporters.get("png") + with io.BytesIO() as stream: + stream.write(data) + stream.seek(0) + an = importer.process(stream) + + with io.BytesIO() as stream: + exporter.process(an, stream) + stream.seek(0) + return stream.read() + + for i in message.stickers: + if i.format in (discord.StickerFormatType.png, discord.StickerFormatType.apng): + images.append((i.url, i.name, True)) + elif i.format == discord.StickerFormatType.lottie: + # save the json lottie representation + try: + async with self.bot.session.get(i.url) as resp: + data = await resp.read() + + # convert to a png + img_data = await self.bot.loop.run_in_executor(None, functools.partial(lottie_to_png, data)) + b64_data = base64.b64encode(img_data).decode() + + # upload to imgur + async with self.bot.session.post( + "https://api.imgur.com/3/image", + headers={"Authorization": "Client-ID 50e96145ac5e085"}, + data={"image": b64_data}, + ) as resp: + result = await resp.json() + url = result["data"]["link"] + + except Exception: + traceback.print_exc() + images.append((None, i.name, True)) + else: + images.append((url, i.name, True)) + else: + images.append((None, i.name, True)) embedded_image = False From 0de26a3e0cd4dd20e36a4b5df70076e4b38bd5d8 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 18:51:36 +0800 Subject: [PATCH 0996/1221] Bump version to 3.11-dev3 --- bot.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 38bd6423b2..b3364ad1f1 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.11.0-dev2" +__version__ = "3.11.0-dev3" import asyncio diff --git a/pyproject.toml b/pyproject.toml index f1de75c772..1e1a13f5c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.11.0-dev2' +version = '3.11.0-dev3' description = "Modmail is similar to Reddit's 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 8d76a3279ef70c06afc3743c5b53922664a2c0f4 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 18:55:18 +0800 Subject: [PATCH 0997/1221] Fix editing notes, resolve #3094 --- CHANGELOG.md | 1 + core/thread.py | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db8a4b950..aa0a6d3121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Large server sizes results in Guild.name == None. ([GH #3088](https://github.com/kyb3r/modmail/issues/3088)) - Attachments did not work on plain replies. ([GH #3102](https://github.com/kyb3r/modmail/issues/3102)) - Support LOTTIE stickers. ([GH #3119](https://github.com/kyb3r/modmail/issues/3119)) +- Editing notes now work. ([GH #3094](https://github.com/kyb3r/modmail/issues/3094)) ### Internal diff --git a/core/thread.py b/core/thread.py index 09f8bd3d41..5209b1b615 100644 --- a/core/thread.py +++ b/core/thread.py @@ -677,13 +677,14 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> embed1.description = message tasks = [self.bot.api.edit_message(message1.id, message), message1.edit(embed=embed1)] - if message2 is not [None]: - for m2 in message2: - embed2 = m2.embeds[0] - embed2.description = message - tasks += [m2.edit(embed=embed2)] - elif message1.embeds[0].author.name.startswith("Persistent Note"): + if message1.embeds[0].author.name.startswith("Persistent Note"): tasks += [self.bot.api.edit_note(message1.id, message)] + else: + for m2 in message2: + if m2 is not None: + embed2 = m2.embeds[0] + embed2.description = message + tasks += [m2.edit(embed=embed2)] await asyncio.gather(*tasks) From 8e4d4b033a8fc6b3eff08d583f065662549d39ab Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 19:12:15 +0800 Subject: [PATCH 0998/1221] Test potential fix for addusers #3090 --- cogs/modmail.py | 33 +++++++++++++++++++++++++-------- core/thread.py | 8 +++++--- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 60c18594ac..7080a7bdf5 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,4 +1,5 @@ import asyncio +from asyncio import tasks import re from datetime import datetime from itertools import zip_longest @@ -735,6 +736,7 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str ctx.command.reset_cooldown(ctx) return + tasks = [] if not silent: description = self.bot.formatter.format( self.bot.config["private_added_to_group_response"], moderator=ctx.author @@ -748,7 +750,7 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str em.timestamp = discord.utils.utcnow() em.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar.url) for u in users: - await u.send(embed=em) + tasks.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_added_to_group_response"], @@ -766,9 +768,12 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str for i in ctx.thread.recipients: if i not in users: - await i.send(embed=em) + tasks.append(i.send(embed=em)) await ctx.thread.add_users(users) + if tasks: + await asyncio.gather(*tasks) + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -824,6 +829,7 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, ctx.command.reset_cooldown(ctx) return + tasks = [] if not silent: description = self.bot.formatter.format( self.bot.config["private_removed_from_group_response"], moderator=ctx.author @@ -837,7 +843,7 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, em.timestamp = discord.utils.utcnow() em.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar.url) for u in users: - await u.send(embed=em) + tasks.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_removed_from_group_response"], @@ -855,9 +861,12 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, for i in ctx.thread.recipients: if i not in users: - await i.send(embed=em) + tasks.append(i.send(embed=em)) await ctx.thread.remove_users(users) + if tasks: + await asyncio.gather(*tasks) + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -907,6 +916,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, ctx.command.reset_cooldown(ctx) return + tasks = [] if not silent: em = discord.Embed( title=self.bot.config["private_added_to_group_title"], @@ -928,7 +938,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, em.set_footer(text=name, icon_url=avatar_url) for u in users: - await u.send(embed=em) + tasks.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_added_to_group_description_anon"], @@ -945,9 +955,12 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, for i in ctx.thread.recipients: if i not in users: - await i.send(embed=em) + tasks.append(i.send(embed=em)) await ctx.thread.add_users(users) + if tasks: + await asyncio.gather(*tasks) + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -992,6 +1005,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro ctx.command.reset_cooldown(ctx) return + tasks = [] if not silent: em = discord.Embed( title=self.bot.config["private_removed_from_group_title"], @@ -1013,7 +1027,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro em.set_footer(text=name, icon_url=avatar_url) for u in users: - await u.send(embed=em) + tasks.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_removed_from_group_description_anon"], @@ -1030,9 +1044,12 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro for i in ctx.thread.recipients: if i not in users: - await i.send(embed=em) + tasks.append(i.send(embed=em)) await ctx.thread.remove_users(users) + if tasks: + await asyncio.gather(*tasks) + sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) diff --git a/core/thread.py b/core/thread.py index 5209b1b615..4aea00734e 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1203,7 +1203,7 @@ async def _update_users_genesis(self): embed.add_field(name="Other Recipients", value=value, inline=False) else: if value: - embed.set_field_at(index, value=value) + embed.set_field_at(index, name="Other Recipients", value=value, inline=False) else: embed.remove_field(index) @@ -1211,13 +1211,15 @@ async def _update_users_genesis(self): async def add_users(self, users: typing.List[typing.Union[discord.Member, discord.User]]) -> None: topic = "" - title, user_id, _ = parse_channel_topic(self.channel.topic) + title, _, _ = parse_channel_topic(self.channel.topic) if title is not None: topic += f"Title: {title}\n" - topic += f"User ID: {user_id}" + topic += f"User ID: {self._id}" self._other_recipients += users + self._other_recipients = list(set(self._other_recipients)) + ids = ",".join(str(i.id) for i in self._other_recipients) topic += f"\nOther Recipients: {ids}" From a89a3fe593011fab26557daf6350f79175a7fdf8 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 19:19:10 +0800 Subject: [PATCH 0999/1221] Update changelog --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0a6d3121..9613ba44c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,17 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Breaking -- Upgraded to discord.py v2.0 master ([internal changes](https://gist.github.com/apple502j/f75b4f24652f04de85c7084ffd73ec58)). +- Upgraded to discord.py v2.0 master ([internal changes](https://gist.github.com/apple502j/f75b4f24652f04de85c7084ffd73ec58), [GH #2990](https://github.com/kyb3r/modmail/issues/2990)). - Python 3.8 or higher is required. ### Added - `use_hoisted_top_role` config to use change how default mod tags work, see `v3.10.0#Added` for details. ([PR #3093](https://github.com/kyb3r/modmail/pull/3093)) +### Improved + +- Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/kyb3r/modmail/issues/3048)) + ### Fixed - Several minor typos. ([PR #3095](https://github.com/kyb3r/modmail/pull/3095), [PR #3116](https://github.com/kyb3r/modmail/pull/3116)) @@ -31,7 +35,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Internal -- Improve regex parsing of channel topics. ([PR #3111](https://github.com/kyb3r/modmail/pull/3111)) +- Improve regex parsing of channel topics. ([GH #3114](https://github.com/kyb3r/modmail/issues/3114), [PR #3111](https://github.com/kyb3r/modmail/pull/3111)) - Add warning if deploying on a developmental version. # v3.10.3 From 1444c1d97444e4c3cc4a6caa01c5d35e2be7a1df Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 19:19:47 +0800 Subject: [PATCH 1000/1221] Formatting --- core/thread.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 4aea00734e..145864e815 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1017,7 +1017,9 @@ def lottie_to_png(data): data = await resp.read() # convert to a png - img_data = await self.bot.loop.run_in_executor(None, functools.partial(lottie_to_png, data)) + img_data = await self.bot.loop.run_in_executor( + None, functools.partial(lottie_to_png, data) + ) b64_data = base64.b64encode(img_data).decode() # upload to imgur From 13a22cf003333acfb61bbadd7f81b0a711c89a79 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 21:49:26 +0800 Subject: [PATCH 1001/1221] require_close_reason config, resolves #3107 --- CHANGELOG.md | 5 +++-- bot.py | 2 +- cogs/modmail.py | 3 +++ core/config.py | 2 ++ core/config_help.json | 8 ++++++++ 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9613ba44c4..ff96521f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Added - `use_hoisted_top_role` config to use change how default mod tags work, see `v3.10.0#Added` for details. ([PR #3093](https://github.com/kyb3r/modmail/pull/3093)) +- `require_close_reason` to require a reason to close a thread. ([GH #3107](https://github.com/kyb3r/modmail/issues/3107)) ### Improved @@ -27,9 +28,9 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Certain cases where fallback categories were not working as intended. ([PR #3109](https://github.com/kyb3r/modmail/pull/3109)) - `?contact` would create in a random category in silent mode. ([GH #3091](https://github.com/kyb3r/modmail/issues/3091), [PR #3092](https://github.com/kyb3r/modmail/pull/3092)) - Certain cases where `?close` would fail if closer isn't in cache. ([GH #3104](https://github.com/kyb3r/modmail/issues/3104), [PR #3105](https://github.com/kyb3r/modmail/pull/3105)) -- Stickers were not working in Modmail. +- Stickers now work in Modmail. - Large server sizes results in Guild.name == None. ([GH #3088](https://github.com/kyb3r/modmail/issues/3088)) -- Attachments did not work on plain replies. ([GH #3102](https://github.com/kyb3r/modmail/issues/3102)) +- Attachments now work on plain replies. ([GH #3102](https://github.com/kyb3r/modmail/issues/3102)) - Support LOTTIE stickers. ([GH #3119](https://github.com/kyb3r/modmail/issues/3119)) - Editing notes now work. ([GH #3094](https://github.com/kyb3r/modmail/issues/3094)) diff --git a/bot.py b/bot.py index b3364ad1f1..38bd6423b2 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.11.0-dev3" +__version__ = "3.11.0-dev2" import asyncio diff --git a/cogs/modmail.py b/cogs/modmail.py index 7080a7bdf5..5871f76fb6 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -419,6 +419,9 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): return await ctx.send(embed=embed) + if self.bot.config["require_close_reason"] and message is None: + raise commands.BadArgument("Provide a reason for closing the thread.") + if after and after.dt > now: await self.send_scheduled_close_message(ctx, after, silent) diff --git a/core/config.py b/core/config.py index e9de7d516d..d06443a6a3 100644 --- a/core/config.py +++ b/core/config.py @@ -91,6 +91,7 @@ class ConfigManager: "silent_alert_on_mention": False, "show_timestamp": True, "anonymous_snippets": False, + "require_close_reason": False, # group conversations "private_added_to_group_title": "New Thread (Group)", "private_added_to_group_response": "{moderator.name} has added you to a Modmail thread.", @@ -206,6 +207,7 @@ class ConfigManager: "update_notifications", "thread_contact_silently", "anonymous_snippets", + "require_close_reason", "recipient_thread_close", "thread_show_roles", "thread_show_account_age", diff --git a/core/config_help.json b/core/config_help.json index 04f177cd97..ee03aca24a 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -797,6 +797,14 @@ "See also: `anon_avatar_url`, `anon_tag`." ] }, + "require_close_reason": { + "default" : "No", + "description": "Require a reason to close threads.", + "examples": [ + "`{prefix}config set require_close_reason yes`" + ], + "notes": [] + }, "private_added_to_group_title": { "default": "New Thread (Group)", "description": "This is the message embed title sent to the recipient that is just added to a thread.", From e44fb12e90dbb5974a7e9d066a68d0d39d6e023c Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 21:58:17 +0800 Subject: [PATCH 1002/1221] plain_snippets, ?fpareply and ?fpreply, resolves #3083 --- CHANGELOG.md | 4 +- bot.py | 8 ++-- cogs/modmail.py | 85 ++++++++++++++++++++++++++++++++----------- core/config.py | 2 + core/config_help.json | 12 +++++- 5 files changed, 85 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff96521f8d..42a1744d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Added - `use_hoisted_top_role` config to use change how default mod tags work, see `v3.10.0#Added` for details. ([PR #3093](https://github.com/kyb3r/modmail/pull/3093)) -- `require_close_reason` to require a reason to close a thread. ([GH #3107](https://github.com/kyb3r/modmail/issues/3107)) +- `require_close_reason` config to require a reason to close a thread. ([GH #3107](https://github.com/kyb3r/modmail/issues/3107)) +- `plain_snippets` config to force all snippets to be plain. ([GH #3083](https://github.com/kyb3r/modmail/issues/3083)) +- `?fpareply` and `?fpreply` to reply to messages with variables plainly. ### Improved diff --git a/bot.py b/bot.py index 38bd6423b2..fcdf8011aa 100644 --- a/bot.py +++ b/bot.py @@ -1144,10 +1144,12 @@ async def process_commands(self, message): cmd = cmd.lower() if cmd in self.snippets: snippet = self.snippets[cmd] + modifiers = "f" + if self.config["plain_snippets"]: + modifiers += "p" if self.config["anonymous_snippets"]: - message.content = f"{self.prefix}fareply {snippet}" - else: - message.content = f"{self.prefix}freply {snippet}" + modifiers += "a" + message.content = f"{self.prefix}{modifiers}reply {snippet}" ctxs = await self.get_contexts(message) for ctx in ctxs: diff --git a/cogs/modmail.py b/cogs/modmail.py index 5871f76fb6..c17b580370 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,5 +1,4 @@ import asyncio -from asyncio import tasks import re from datetime import datetime from itertools import zip_longest @@ -739,7 +738,7 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str ctx.command.reset_cooldown(ctx) return - tasks = [] + to_exec = [] if not silent: description = self.bot.formatter.format( self.bot.config["private_added_to_group_response"], moderator=ctx.author @@ -753,7 +752,7 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str em.timestamp = discord.utils.utcnow() em.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar.url) for u in users: - tasks.append(u.send(embed=em)) + to_exec.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_added_to_group_response"], @@ -771,11 +770,11 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str for i in ctx.thread.recipients: if i not in users: - tasks.append(i.send(embed=em)) + to_exec.append(i.send(embed=em)) await ctx.thread.add_users(users) - if tasks: - await asyncio.gather(*tasks) + if to_exec: + await asyncio.gather(*to_exec) sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -832,7 +831,7 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, ctx.command.reset_cooldown(ctx) return - tasks = [] + to_exec = [] if not silent: description = self.bot.formatter.format( self.bot.config["private_removed_from_group_response"], moderator=ctx.author @@ -846,7 +845,7 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, em.timestamp = discord.utils.utcnow() em.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar.url) for u in users: - tasks.append(u.send(embed=em)) + to_exec.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_removed_from_group_response"], @@ -864,11 +863,11 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, for i in ctx.thread.recipients: if i not in users: - tasks.append(i.send(embed=em)) + to_exec.append(i.send(embed=em)) await ctx.thread.remove_users(users) - if tasks: - await asyncio.gather(*tasks) + if to_exec: + await asyncio.gather(*to_exec) sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -919,7 +918,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, ctx.command.reset_cooldown(ctx) return - tasks = [] + to_exec = [] if not silent: em = discord.Embed( title=self.bot.config["private_added_to_group_title"], @@ -941,7 +940,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, em.set_footer(text=name, icon_url=avatar_url) for u in users: - tasks.append(u.send(embed=em)) + to_exec.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_added_to_group_description_anon"], @@ -958,11 +957,11 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, for i in ctx.thread.recipients: if i not in users: - tasks.append(i.send(embed=em)) + to_exec.append(i.send(embed=em)) await ctx.thread.add_users(users) - if tasks: - await asyncio.gather(*tasks) + if to_exec: + await asyncio.gather(*to_exec) sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -1008,7 +1007,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro ctx.command.reset_cooldown(ctx) return - tasks = [] + to_exec = [] if not silent: em = discord.Embed( title=self.bot.config["private_removed_from_group_title"], @@ -1030,7 +1029,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro em.set_footer(text=name, icon_url=avatar_url) for u in users: - tasks.append(u.send(embed=em)) + to_exec.append(u.send(embed=em)) description = self.bot.formatter.format( self.bot.config["public_removed_from_group_description_anon"], @@ -1047,11 +1046,11 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro for i in ctx.thread.recipients: if i not in users: - tasks.append(i.send(embed=em)) + to_exec.append(i.send(embed=em)) await ctx.thread.remove_users(users) - if tasks: - await asyncio.gather(*tasks) + if to_exec: + await asyncio.gather(*to_exec) sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) @@ -1253,6 +1252,50 @@ async def fareply(self, ctx, *, msg: str = ""): async with ctx.typing(): await ctx.thread.reply(ctx.message, anonymous=True) + @commands.command(aliases=["formatplainreply"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def fpreply(self, ctx, *, msg: str = ""): + """ + Reply to a Modmail thread with variables and a plain message. + + Works just like `{prefix}areply`, 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, plain=True) + + @commands.command(aliases=["formatplainanonreply"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def fpareply(self, ctx, *, msg: str = ""): + """ + Anonymously reply to a Modmail thread with variables and a plain message. + + Works just like `{prefix}areply`, 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, anonymous=True, plain=True) + @commands.command(aliases=["anonreply", "anonymousreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() diff --git a/core/config.py b/core/config.py index d06443a6a3..4af16e8272 100644 --- a/core/config.py +++ b/core/config.py @@ -91,6 +91,7 @@ class ConfigManager: "silent_alert_on_mention": False, "show_timestamp": True, "anonymous_snippets": False, + "plain_snippets": False, "require_close_reason": False, # group conversations "private_added_to_group_title": "New Thread (Group)", @@ -207,6 +208,7 @@ class ConfigManager: "update_notifications", "thread_contact_silently", "anonymous_snippets", + "plain_snippets", "require_close_reason", "recipient_thread_close", "thread_show_roles", diff --git a/core/config_help.json b/core/config_help.json index ee03aca24a..c31be337d6 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -794,7 +794,17 @@ "`{prefix}config set anonymous_snippets yes`" ], "notes": [ - "See also: `anon_avatar_url`, `anon_tag`." + "See also: `anon_avatar_url`, `anon_tag`, `plain_snippets`." + ] + }, + "plain_snippets": { + "default": "No", + "description": "Sends snippets with a plain interface.", + "examples":[ + "`{prefix}config set plain_snippets yes`" + ], + "notes": [ + "See also: `anonymous_snippets`." ] }, "require_close_reason": { From ec8fb29f7553f9961495b1fe36e09ec0e38429b9 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:08:33 +0800 Subject: [PATCH 1003/1221] use_nickname_channel_name, resolves #3112 --- CHANGELOG.md | 1 + bot.py | 9 +++++++-- core/config.py | 2 ++ core/config_help.json | 21 +++++++++++++++++---- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a1744d12..4744e57bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `require_close_reason` config to require a reason to close a thread. ([GH #3107](https://github.com/kyb3r/modmail/issues/3107)) - `plain_snippets` config to force all snippets to be plain. ([GH #3083](https://github.com/kyb3r/modmail/issues/3083)) - `?fpareply` and `?fpreply` to reply to messages with variables plainly. +- `use_nickname_channel_name` config to use nicknames instead of usernames for channel names. ([GH #3112](https://github.com/kyb3r/modmail/issues/3112)) ### Improved diff --git a/bot.py b/bot.py index fcdf8011aa..dcd447eed4 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.11.0-dev2" +__version__ = "3.11.0-dev3" import asyncio @@ -1663,7 +1663,12 @@ def format_channel_name(self, author, exclude_channel=None, force_null=False): elif self.config["use_timestamp_channel_name"]: name = new_name = author.created_at.isoformat(sep="-", timespec="minutes") else: - name = author.name.lower() + if self.config["use_nickname_channel_name"]: + author_member = self.guild.get_member(author.id) + name = author_member.display_name.lower() + else: + name = author.name.lower() + if force_null: name = "null" diff --git a/core/config.py b/core/config.py index 4af16e8272..6d2a2eb1cd 100644 --- a/core/config.py +++ b/core/config.py @@ -52,6 +52,7 @@ class ConfigManager: "close_emoji": "\N{LOCK}", "use_user_id_channel_name": False, "use_timestamp_channel_name": False, + "use_nickname_channel_name": False, "recipient_thread_close": False, "thread_show_roles": True, "thread_show_account_age": True, @@ -184,6 +185,7 @@ class ConfigManager: booleans = { "use_user_id_channel_name", "use_timestamp_channel_name", + "use_nickname_channel_name", "user_typing", "mod_typing", "reply_without_command", diff --git a/core/config_help.json b/core/config_help.json index c31be337d6..53d1d4daec 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -106,8 +106,8 @@ ], "notes": [ "This config is suitable for servers in Server Discovery to comply with channel name restrictions.", - "This cannot be applied with `use_timestamp_channel_name`.", - "See also: `use_timestamp_channel_name`." + "This cannot be applied with `use_timestamp_channel_name` or `use_nickname_channel_name`.", + "See also: `use_timestamp_channel_name`, `use_nickname_channel_name`." ] }, "use_timestamp_channel_name": { @@ -119,8 +119,21 @@ ], "notes": [ "This config is suitable for servers in Server Discovery to comply with channel name restrictions.", - "This cannot be applied with `use_user_id_channel_name`.", - "See also: `use_user_id_channel_name`." + "This cannot be applied with `use_user_id_channel_name` or `use_nickname_channel_name`.", + "See also: `use_user_id_channel_name`, `use_nickname_channel_name`." + ] + }, + "use_nickname_channel_name": { + "default": "No", + "description": "When this is set to `yes`, new thread channels will be named with the recipient's nickname instead of the recipient's name.", + "examples": [ + "`{prefix}config set use_nickname_channel_name yes`", + "`{prefix}config set use_nickname_channel_name no`" + ], + "notes": [ + "This config is suitable for servers in Server Discovery to comply with channel name restrictions.", + "This cannot be applied with `use_timestamp_channel_name` or `use_user_id_channel_name`.", + "See also: `use_timestamp_channel_name`, `use_user_id_channel_name`." ] }, "mod_typing": { From d741738e1e71ed8b047bfe96502308d95119d851 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:29:45 +0800 Subject: [PATCH 1004/1221] Use discord relative timedeltas, resolves #3046 --- CHANGELOG.md | 1 + bot.py | 4 +-- cogs/modmail.py | 9 +++--- core/config.py | 2 +- core/config_help.json | 2 +- core/thread.py | 2 +- core/time.py | 66 +++++++++++++++++++++++-------------------- 7 files changed, 45 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4744e57bba..2ef596f888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Improved - Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/kyb3r/modmail/issues/3048)) +- Use discord relative timedeltas. ([GH #3046](https://github.com/kyb3r/modmail/issues/3046)) ### Fixed diff --git a/bot.py b/bot.py index dcd447eed4..45da7fa953 100644 --- a/bot.py +++ b/bot.py @@ -685,7 +685,7 @@ def check_account_age(self, author: discord.Member) -> bool: logger.debug("Blocked due to account age, user %s.", author.name) if str(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. User can try again {delta}." self.blocked_users[str(author.id)] = new_reason return False @@ -711,7 +711,7 @@ def check_guild_age(self, author: discord.Member) -> bool: logger.debug("Blocked due to guild age, user %s.", author.name) if str(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. User can try again {delta}." self.blocked_users[str(author.id)] = new_reason return False diff --git a/cogs/modmail.py b/cogs/modmail.py index c17b580370..7900f0eeef 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,6 +1,6 @@ import asyncio import re -from datetime import datetime +from datetime import datetime, timezone from itertools import zip_longest from typing import Optional, Union from types import SimpleNamespace @@ -12,7 +12,6 @@ from discord.utils import escape_markdown from dateutil import parser -from natural.date import duration from core import checks from core.models import DMDisabled, PermissionLevel, SimilarCategoryConverter, getLogger @@ -359,7 +358,7 @@ async def send_scheduled_close_message(self, ctx, after, silent=False): embed = discord.Embed( title="Scheduled close", - description=f"This thread will close {silent}in {human_delta}.", + description=f"This thread will close {silent}{human_delta}.", color=self.bot.error_color, ) @@ -631,7 +630,7 @@ def format_log_embeds(self, logs, avatar_url): title = f"Total Results Found ({len(logs)})" for entry in logs: - created_at = parser.parse(entry["created_at"]) + created_at = parser.parse(entry["created_at"]).astimezone(timezone.utc) prefix = self.bot.config["log_url_prefix"].strip("/") if prefix == "NONE": @@ -646,7 +645,7 @@ def format_log_embeds(self, logs, avatar_url): 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.url = log_url - embed.add_field(name="Created", value=duration(created_at, now=discord.utils.utcnow())) + embed.add_field(name="Created", value=human_timedelta(created_at)) closer = entry.get("closer") if closer is None: closer_msg = "Unknown" diff --git a/core/config.py b/core/config.py index 6d2a2eb1cd..399592e055 100644 --- a/core/config.py +++ b/core/config.py @@ -78,7 +78,7 @@ class ConfigManager: "thread_move_notify_mods": False, "thread_move_response": "This thread has been moved.", "cooldown_thread_title": "Message not sent!", - "cooldown_thread_response": "You must wait for {delta} before you can contact me again.", + "cooldown_thread_response": "Your cooldown ends {delta}. Try contacting me then.", "disabled_new_thread_title": "Not Delivered", "disabled_new_thread_response": "We are not accepting new threads.", "disabled_new_thread_footer": "Please try again later...", diff --git a/core/config_help.json b/core/config_help.json index 53d1d4daec..6ee84eee15 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -571,7 +571,7 @@ ] }, "cooldown_thread_response": { - "default": "You must wait for {delta} before you can contact me again.", + "default": "Your cooldown ends {delta}. Try contacting me then.", "description": "The description of the message embed when the user has a cooldown before creating a new thread.", "examples": [ "`{prefix}config set cooldown_thread_response Be patient! You are on cooldown, wait {delta} more.`" diff --git a/core/thread.py b/core/thread.py index 145864e815..9fa504152e 100644 --- a/core/thread.py +++ b/core/thread.py @@ -565,7 +565,7 @@ async def _restart_close_timer(self): seconds = timeout.total_seconds() # seconds = 20 # Uncomment to debug with just 20 seconds reset_time = discord.utils.utcnow() + timedelta(seconds=seconds) - human_time = human_timedelta(dt=reset_time) + human_time = human_timedelta(dt=reset_time, spec="manual") if self.bot.config.get("thread_auto_close_silently"): return await self.close(closer=self.bot.user, silent=True, after=int(seconds), auto_close=True) diff --git a/core/time.py b/core/time.py index 376f2074eb..26b41b9043 100644 --- a/core/time.py +++ b/core/time.py @@ -187,35 +187,39 @@ async def convert(self, ctx, argument): return super().convert(ctx, argument) -def human_timedelta(dt, *, source=None): - now = source or discord.utils.utcnow() - if dt > now: - delta = relativedelta(dt, now) - suffix = "" - else: - delta = relativedelta(now, dt) - suffix = " ago" - - if delta.microseconds and delta.seconds: - delta = delta + relativedelta(seconds=+1) - - attrs = ["years", "months", "days", "hours", "minutes", "seconds"] - - output = [] - for attr in attrs: - elem = getattr(delta, attr) - if not elem: - continue - - if elem > 1: - output.append(f"{elem} {attr}") +def human_timedelta(dt, *, spec="R"): + if spec == "manual": + now = discord.utils.utcnow() + if dt > now: + delta = relativedelta(dt, now) + suffix = "" else: - output.append(f"{elem} {attr[:-1]}") - - if not output: - return "now" - if len(output) == 1: - return output[0] + suffix - if len(output) == 2: - return f"{output[0]} and {output[1]}{suffix}" - return f"{output[0]}, {output[1]} and {output[2]}{suffix}" + delta = relativedelta(now, dt) + suffix = " ago" + + if delta.microseconds and delta.seconds: + delta = delta + relativedelta(seconds=+1) + + attrs = ["years", "months", "days", "hours", "minutes", "seconds"] + + output = [] + for attr in attrs: + elem = getattr(delta, attr) + if not elem: + continue + + if elem > 1: + output.append(f"{elem} {attr}") + else: + output.append(f"{elem} {attr[:-1]}") + + if not output: + return "now" + if len(output) == 1: + return output[0] + suffix + if len(output) == 2: + return f"{output[0]} and {output[1]}{suffix}" + return f"{output[0]}, {output[1]} and {output[2]}{suffix}" + else: + unixtime = int(dt.timestamp()) + return f"<t:{unixtime}:{spec}>" From 3af772f0c1bd9fd034b4e2b5ce7e119ea1c14f28 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:40:28 +0800 Subject: [PATCH 1005/1221] Properly implement per-server avatars --- core/thread.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/thread.py b/core/thread.py index 9fa504152e..eb47ba9cdc 100644 --- a/core/thread.py +++ b/core/thread.py @@ -335,10 +335,9 @@ def _format_info_embed(self, user, log_url, log_count, color): else: footer = f"User ID: {user.id}" - embed.set_author(name=str(user), icon_url=user.display_avatar.url, url=log_url) - # embed.set_thumbnail(url=avi) - if member is not None: + embed.set_author(name=str(user), icon_url=member.display_avatar.url, url=log_url) + joined = str((time - member.joined_at).days) # embed.add_field(name='Joined', value=joined + days(joined)) if self.bot.config["thread_show_join_age"]: @@ -350,6 +349,7 @@ def _format_info_embed(self, user, log_url, log_count, color): embed.add_field(name="Roles", value=role_names, inline=True) embed.set_footer(text=footer) else: + embed.set_author(name=str(user), icon_url=user.display_avatar.url, url=log_url) embed.set_footer(text=f"{footer} • (not in main server)") embed.description += ", ".join(user_info) @@ -931,6 +931,11 @@ async def send( destination = destination or self.channel author = message.author + member = self.bot.guild.get_member(author.id) + if member: + avatar_url = member.display_avatar.url + else: + avatar_url = author.display_avatar.url embed = discord.Embed(description=message.content) if self.bot.config["show_timestamp"]: @@ -958,7 +963,7 @@ async def send( else: # Normal message name = str(author) - avatar_url = author.display_avatar.url + avatar_url = avatar_url embed.set_author( name=name, icon_url=avatar_url, From 998d938f33ed9fe3bfe59ff6934c1ffd0bb67116 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:49:42 +0800 Subject: [PATCH 1006/1221] Modmail now work in threads --- CHANGELOG.md | 1 + core/thread.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ef596f888..c2fca71128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Attachments now work on plain replies. ([GH #3102](https://github.com/kyb3r/modmail/issues/3102)) - Support LOTTIE stickers. ([GH #3119](https://github.com/kyb3r/modmail/issues/3119)) - Editing notes now work. ([GH #3094](https://github.com/kyb3r/modmail/issues/3094)) +- Commands now work in threads. ### Internal diff --git a/core/thread.py b/core/thread.py index eb47ba9cdc..1d41c0a18a 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1281,7 +1281,7 @@ async def find( recipient_id: int = None, ) -> typing.Optional[Thread]: """Finds a thread from cache or from discord channel topics.""" - if recipient is None and channel is not None: + if recipient is None and channel is not None and isinstance(channel, discord.TextChannel): thread = await self._find_from_channel(channel) if thread is None: user_id, thread = next( @@ -1310,7 +1310,6 @@ async def find( await thread.close(closer=self.bot.user, silent=True, delete_channel=False) thread = None else: - def check(topic): _, user_id, other_ids = parse_channel_topic(topic) return recipient_id == user_id or recipient_id in other_ids From 67781cae9cc149d2d847e96292377fab0341c029 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:54:20 +0800 Subject: [PATCH 1007/1221] Formatting --- core/thread.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/thread.py b/core/thread.py index 1d41c0a18a..c95ce09cb6 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1310,6 +1310,7 @@ async def find( await thread.close(closer=self.bot.user, silent=True, delete_channel=False) thread = None else: + def check(topic): _, user_id, other_ids = parse_channel_topic(topic) return recipient_id == user_id or recipient_id in other_ids From ae1af88c2966606611386b76cf46836be5fab6be Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 22:58:16 +0800 Subject: [PATCH 1008/1221] Bunp version to v4-dev4 --- CHANGELOG.md | 2 +- README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2fca71128..a4409e12bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.11.0 +# v4.0.0 ### Breaking diff --git a/README.md b/README.md index 7492a53116..4487438a5c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v3.11.0-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v4.0.0-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> diff --git a/bot.py b/bot.py index 45da7fa953..77d6327a4f 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.11.0-dev3" +__version__ = "4.0.0-dev4" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 1e1a13f5c9..9b541f6243 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.11.0-dev3' +version = '4.0.0-dev4' description = "Modmail is similar to Reddit's 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 68bc5733cbf5a57394879e2b370023e550ba061c Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 23:26:43 +0800 Subject: [PATCH 1009/1221] Fix timezone mismatch --- bot.py | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot.py b/bot.py index 77d6327a4f..c9ad1ee638 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev4" +__version__ = "4.0.0-dev5" import asyncio @@ -10,7 +10,7 @@ import string import sys import typing -from datetime import datetime +from datetime import datetime, timezone from subprocess import PIPE from types import SimpleNamespace @@ -565,7 +565,7 @@ async def on_ready(self): logger.line() for recipient_id, items in tuple(closures.items()): - after = (datetime.fromisoformat(items["time"]) - discord.utils.utcnow()).total_seconds() + after = (datetime.fromisoformat(items["time"]).astimezone(timezone.utc) - discord.utils.utcnow()).total_seconds() if after <= 0: logger.debug("Closing thread for recipient %s.", recipient_id) after = 0 diff --git a/pyproject.toml b/pyproject.toml index 9b541f6243..9f722227eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev4' +version = '4.0.0-dev5' description = "Modmail is similar to Reddit's 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 459b81dbec086193a59c4c5490800ba5e9182cf6 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 21 Nov 2021 23:38:21 +0800 Subject: [PATCH 1010/1221] format --- bot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index c9ad1ee638..fb4f8b04a2 100644 --- a/bot.py +++ b/bot.py @@ -565,7 +565,9 @@ async def on_ready(self): logger.line() for recipient_id, items in tuple(closures.items()): - after = (datetime.fromisoformat(items["time"]).astimezone(timezone.utc) - discord.utils.utcnow()).total_seconds() + after = ( + datetime.fromisoformat(items["time"]).astimezone(timezone.utc) - discord.utils.utcnow() + ).total_seconds() if after <= 0: logger.debug("Closing thread for recipient %s.", recipient_id) after = 0 From da92d04a5325a644e475f514f01d20a78a5c2431 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Nov 2021 00:52:33 +0800 Subject: [PATCH 1011/1221] improve cairo warnings --- bot.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index fb4f8b04a2..c553686852 100644 --- a/bot.py +++ b/bot.py @@ -8,6 +8,7 @@ import re import signal import string +import struct import sys import typing from datetime import datetime, timezone @@ -1690,7 +1691,7 @@ def format_channel_name(self, author, exclude_channel=None, force_null=False): def main(): try: # noinspection PyUnresolvedReferences - import uvloop + import uvloop # type: ignore logger.debug("Setting up with uvloop.") uvloop.install() @@ -1698,11 +1699,21 @@ def main(): pass try: - import cairosvg + import cairosvg # noqa: F401 except OSError: - logger.error( - "Unable to import cairosvg, install GTK Installer for Windows: https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases/latest" - ) + if os.name == "nt": + if struct.calcsize("P") * 8 != 64: + logger.error( + "Unable to import cairosvg, ensure your Python is a 64-bit version: https://www.python.org/downloads/" + ) + else: + logger.error( + "Unable to import cairosvg, install GTK Installer for Windows: https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases/latest" + ) + else: + logger.error( + "Unable to import cairosvg, report on our support server with your OS details: https://discord.gg/etJNHCQ" + ) sys.exit(0) # check discord version From 28d86764f46807cbdd18f271c37734d2f66bfaad Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Nov 2021 17:32:51 +0800 Subject: [PATCH 1012/1221] show_log_url_button for log urls resolves #3122 --- CHANGELOG.md | 1 + core/config.py | 2 ++ core/config_help.json | 8 ++++++++ core/thread.py | 7 ++++++- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4409e12bb..b3dae23d78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `plain_snippets` config to force all snippets to be plain. ([GH #3083](https://github.com/kyb3r/modmail/issues/3083)) - `?fpareply` and `?fpreply` to reply to messages with variables plainly. - `use_nickname_channel_name` config to use nicknames instead of usernames for channel names. ([GH #3112](https://github.com/kyb3r/modmail/issues/3112)) +- `show_log_url_button` config to show Log URL button. ([GH #3122](https://github.com/kyb3r/modmail/issues/3122)) ### Improved diff --git a/core/config.py b/core/config.py index 399592e055..14f86ecd2e 100644 --- a/core/config.py +++ b/core/config.py @@ -94,6 +94,7 @@ class ConfigManager: "anonymous_snippets": False, "plain_snippets": False, "require_close_reason": False, + "show_log_url_button": False, # group conversations "private_added_to_group_title": "New Thread (Group)", "private_added_to_group_response": "{moderator.name} has added you to a Modmail thread.", @@ -191,6 +192,7 @@ class ConfigManager: "reply_without_command", "anon_reply_without_command", "plain_reply_without_command", + "show_log_url_button", "recipient_thread_close", "thread_auto_close_silently", "thread_move_notify", diff --git a/core/config_help.json b/core/config_help.json index 6ee84eee15..342652456d 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -828,6 +828,14 @@ ], "notes": [] }, + "show_log_url_button": { + "default" : "No", + "description": "Shows the button to open the Log URL.", + "examples": [ + "`{prefix}config set show_log_url_button yes`" + ], + "notes": [] + }, "private_added_to_group_title": { "default": "New Thread (Group)", "description": "This is the message embed title sent to the recipient that is just added to a thread.", diff --git a/core/thread.py b/core/thread.py index c95ce09cb6..67458f0017 100644 --- a/core/thread.py +++ b/core/thread.py @@ -495,7 +495,12 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, tasks = [self.bot.config.update()] if self.bot.log_channel is not None and self.channel is not None: - tasks.append(self.bot.log_channel.send(embed=embed)) + if self.bot.config["show_log_url_button"]: + view = discord.ui.View() + view.add_item(discord.ui.Button(label='Log link', url=log_url, style=discord.ButtonStyle.url)) + else: + view = None + tasks.append(self.bot.log_channel.send(embed=embed, view=view)) # Thread closed message From 9866141817bd9d63adee3b238979d743878b1216 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Nov 2021 18:54:27 +0800 Subject: [PATCH 1013/1221] Use discord native buttons for pagination --- CHANGELOG.md | 1 + core/paginator.py | 259 ++++++++++++++++++++++++++++------------------ core/thread.py | 2 +- 3 files changed, 159 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3dae23d78..641b3ffcf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/kyb3r/modmail/issues/3048)) - Use discord relative timedeltas. ([GH #3046](https://github.com/kyb3r/modmail/issues/3046)) +- Use discord native buttons for all paginator sessions. ### Fixed diff --git a/core/paginator.py b/core/paginator.py index 934b4670be..b208bff0a3 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -1,8 +1,8 @@ import typing -import asyncio -from discord import User, Reaction, Message, Embed -from discord import HTTPException, InvalidArgument +import discord +from discord import Message, Embed, ButtonStyle, Interaction +from discord.ui import View, Button from discord.ext import commands @@ -33,8 +33,8 @@ class PaginatorSession: The `Message` of the `Embed`. current : int The current page number. - reaction_map : Dict[str, method] - A mapping for reaction to method. + callback_map : Dict[str, method] + A mapping for text to method. """ def __init__(self, ctx: commands.Context, *pages, **options): @@ -45,38 +45,15 @@ def __init__(self, ctx: commands.Context, *pages, **options): self.current = 0 self.pages = list(pages) self.destination = options.get("destination", ctx) - self.reaction_map = { - "⏮": self.first_page, - "◀": self.previous_page, - "▶": self.next_page, - "⏭": self.last_page, - "🛑": self.close, - } - - def add_page(self, item) -> None: - """ - Add a page. - """ - raise NotImplementedError - - async def create_base(self, item) -> None: - """ - Create a base `Message`. - """ - await self._create_base(item) + self.view = None - if len(self.pages) == 1: - self.running = False - return - - self.running = True - for reaction in self.reaction_map: - if len(self.pages) == 2 and reaction in "⏮⏭": - continue - await self.ctx.bot.add_reaction(self.base, reaction) - - async def _create_base(self, item) -> None: - raise NotImplementedError + self.callback_map = { + "<<": self.first_page, + "<": self.previous_page, + ">": self.next_page, + ">>": self.last_page, + } + self._buttons_map = {"<<": None, "<": None, ">": None, ">>": None} async def show_page(self, index: int) -> None: """ @@ -98,66 +75,77 @@ async def show_page(self, index: int) -> None: else: await self.create_base(page) + self.update_disabled_status() + + def update_disabled_status(self): + if self.current == self.first_page(): + # disable << button + if self._buttons_map["<<"]: + self._buttons_map["<<"].disabled = True + + self._buttons_map["<"].disabled = True + else: + if self._buttons_map["<<"]: + self._buttons_map["<<"].disabled = False + + self._buttons_map["<"].disabled = False + + if self.current == self.last_page(): + # disable >> button + if self._buttons_map[">>"] is not None: + self._buttons_map[">>"].disabled = True + + self._buttons_map[">"].disabled = True + else: + if self._buttons_map[">>"] is not None: + self._buttons_map[">>"].disabled = False + + self._buttons_map[">"].disabled = False + + async def create_base(self, item) -> None: + """ + Create a base `Message`. + """ + if len(self.pages) == 1: + self.view = None + self.running = False + else: + self.view = PaginatorView(self, timeout=self.timeout) + self.update_disabled_status() + self.running = True + + await self._create_base(item, self.view) + + async def _create_base(self, item, view: View) -> None: + raise NotImplementedError + async def _show_page(self, page): raise NotImplementedError - def react_check(self, reaction: Reaction, user: User) -> bool: - """ + def first_page(self): + """Returns the index of the first page""" + return 0 - Parameters - ---------- - reaction : Reaction - The `Reaction` object of the reaction. - user : User - The `User` or `Member` object of who sent the reaction. + def next_page(self): + """Returns the index of the next page""" + return min(self.current + 1, self.last_page()) - Returns - ------- - bool - """ - return ( - reaction.message.id == self.base.id - and user.id == self.ctx.author.id - and reaction.emoji in self.reaction_map.keys() - ) + def previous_page(self): + """Returns the index of the previous page""" + return max(self.current - 1, self.first_page()) + + def last_page(self): + """Returns the index of the last page""" + return len(self.pages) - 1 async def run(self) -> typing.Optional[Message]: """ Starts the pagination session. - - Returns - ------- - Optional[Message] - If it's closed before running ends. """ if not self.running: await self.show_page(self.current) - while self.running: - try: - reaction, user = await self.ctx.bot.wait_for( - "reaction_add", check=self.react_check, timeout=self.timeout - ) - except asyncio.TimeoutError: - return await self.close(delete=False) - else: - action = self.reaction_map.get(reaction.emoji) - await action() - try: - await self.base.remove_reaction(reaction, user) - except (HTTPException, InvalidArgument): - pass - - async def previous_page(self) -> None: - """ - Go to the previous page. - """ - await self.show_page(self.current - 1) - - async def next_page(self) -> None: - """ - Go to the next page. - """ - await self.show_page(self.current + 1) + await self.view.wait() + await self.close(delete=False) async def close(self, delete: bool = True) -> typing.Optional[Message]: """ @@ -179,25 +167,92 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: sent_emoji, _ = await self.ctx.bot.retrieve_emoji() await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji) + self.view.clear_items() + self.view.stop() if delete: return await self.base.delete() - try: - await self.base.clear_reactions() - except HTTPException: - pass - async def first_page(self) -> None: - """ - Go to the first page. - """ - await self.show_page(0) +class PaginatorView(View): + """ + View that is used for pagination. - async def last_page(self) -> None: - """ - Go to the last page. - """ - await self.show_page(len(self.pages) - 1) + Parameters + ---------- + handler : PaginatorSession + The paginator session that spawned this view. + timeout : float + How long to wait for before the session closes. + + Attributes + ---------- + handler : PaginatorSession + The paginator session that spawned this view. + timeout : float + How long to wait for before the session closes. + """ + + def __init__(self, handler: PaginatorSession, *args, **kwargs): + super().__init__(*args, **kwargs) + self.handler = handler + self.clear_items() + self.fill_items() + + @discord.ui.button(label="Stop", style=ButtonStyle.danger) + async def stop_button(self, button: Button, interaction: Interaction): + await interaction.response.edit_message(view=self) + await self.handler.close() + + def fill_items(self): + for label, callback in self.handler.callback_map.items(): + if len(self.handler.pages) == 2 and label in ("<<", ">>"): + continue + + if label in ("<<", ">>"): + style = ButtonStyle.secondary + else: + style = ButtonStyle.primary + + button = PageButton(self.handler, callback, label=label, style=style) + + self.handler._buttons_map[label] = button + self.add_item(button) + self.add_item(self.stop_button) + + async def interaction_check(self, interaction: Interaction): + """Only allow the message author to interact""" + if interaction.user != self.handler.ctx.author: + return False + return True + + +class PageButton(Button): + """ + A button that has a callback to jump to the next page + + Parameters + ---------- + handler : PaginatorSession + The paginator session that spawned this view. + page_callback : Callable + A callable that returns an int of the page to go to. + + Attributes + ---------- + handler : PaginatorSession + The paginator session that spawned this view. + page_callback : Callable + A callable that returns an int of the page to go to. + """ + + def __init__(self, handler, page_callback, **kwargs): + super().__init__(**kwargs) + self.handler = handler + self.page_callback = page_callback + + async def callback(self, interaction: Interaction): + await self.handler.show_page(self.page_callback()) + await interaction.response.edit_message(view=self.view) class EmbedPaginatorSession(PaginatorSession): @@ -223,8 +278,8 @@ def add_page(self, item: Embed) -> None: else: raise TypeError("Page must be an Embed object.") - async def _create_base(self, item: Embed) -> None: - self.base = await self.destination.send(embed=item) + async def _create_base(self, item: Embed, view: View) -> None: + self.base = await self.destination.send(embed=item, view=view) async def _show_page(self, page): await self.base.edit(embed=page) @@ -255,9 +310,9 @@ def _set_footer(self): self.embed.set_footer(text=footer_text, icon_url=icon_url) - async def _create_base(self, item: str) -> None: + async def _create_base(self, item: str, view: View) -> None: self._set_footer() - self.base = await self.ctx.send(content=item, embed=self.embed) + self.base = await self.ctx.send(content=item, embed=self.embed, view=view) async def _show_page(self, page) -> None: self._set_footer() diff --git a/core/thread.py b/core/thread.py index 67458f0017..87ffc45ee3 100644 --- a/core/thread.py +++ b/core/thread.py @@ -497,7 +497,7 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, if self.bot.log_channel is not None and self.channel is not None: if self.bot.config["show_log_url_button"]: view = discord.ui.View() - view.add_item(discord.ui.Button(label='Log link', url=log_url, style=discord.ButtonStyle.url)) + view.add_item(discord.ui.Button(label="Log link", url=log_url, style=discord.ButtonStyle.url)) else: view = None tasks.append(self.bot.log_channel.send(embed=embed, view=view)) From e50cf1fdb60fd10e2952a5cc40b2b648cda9e3d4 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Nov 2021 19:24:41 +0800 Subject: [PATCH 1014/1221] Select menus for some embed paginations #2913 --- CHANGELOG.md | 1 + core/paginator.py | 56 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 641b3ffcf4..aa4dea5219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?fpareply` and `?fpreply` to reply to messages with variables plainly. - `use_nickname_channel_name` config to use nicknames instead of usernames for channel names. ([GH #3112](https://github.com/kyb3r/modmail/issues/3112)) - `show_log_url_button` config to show Log URL button. ([GH #3122](https://github.com/kyb3r/modmail/issues/3122)) +- Select menus for certain paginators. ### Improved diff --git a/core/paginator.py b/core/paginator.py index b208bff0a3..bb84aa350f 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -1,8 +1,9 @@ +from collections import OrderedDict import typing import discord from discord import Message, Embed, ButtonStyle, Interaction -from discord.ui import View, Button +from discord.ui import View, Button, Select from discord.ext import commands @@ -35,6 +36,10 @@ class PaginatorSession: The current page number. callback_map : Dict[str, method] A mapping for text to method. + view : PaginatorView + The view that is sent along with the base message. + select_menu : Select + A select menu that will be added to the View. """ def __init__(self, ctx: commands.Context, *pages, **options): @@ -46,6 +51,7 @@ def __init__(self, ctx: commands.Context, *pages, **options): self.pages = list(pages) self.destination = options.get("destination", ctx) self.view = None + self.select_menu = None self.callback_map = { "<<": self.first_page, @@ -195,7 +201,7 @@ class PaginatorView(View): def __init__(self, handler: PaginatorSession, *args, **kwargs): super().__init__(*args, **kwargs) self.handler = handler - self.clear_items() + self.clear_items() # clear first so we can control the order self.fill_items() @discord.ui.button(label="Stop", style=ButtonStyle.danger) @@ -204,6 +210,9 @@ async def stop_button(self, button: Button, interaction: Interaction): await self.handler.close() def fill_items(self): + if self.handler.select_menu is not None: + self.add_item(self.handler.select_menu) + for label, callback in self.handler.callback_map.items(): if len(self.handler.pages) == 2 and label in ("<<", ">>"): continue @@ -255,11 +264,29 @@ async def callback(self, interaction: Interaction): await interaction.response.edit_message(view=self.view) +class PageSelect(Select): + def __init__(self, handler: PaginatorSession, pages: typing.List[typing.Tuple[str]]): + self.handler = handler + options = [] + for n, (label, description) in enumerate(pages): + options.append(discord.SelectOption(label=label, description=description, value=str(n))) + + options = options[:25] # max 25 options + super().__init__(placeholder="Select a page", min_values=1, max_values=1, options=options) + + async def callback(self, interaction: Interaction): + page = int(self.values[0]) + await self.handler.show_page(page) + await interaction.response.edit_message(view=self.view) + + class EmbedPaginatorSession(PaginatorSession): def __init__(self, ctx: commands.Context, *embeds, **options): super().__init__(ctx, *embeds, **options) if len(self.pages) > 1: + select_options = [] + create_select = True for i, embed in enumerate(self.pages): footer_text = f"Page {i + 1} of {len(self.pages)}" if embed.footer.text: @@ -269,9 +296,32 @@ def __init__(self, ctx: commands.Context, *embeds, **options): icon_url = embed.footer.icon.url else: icon_url = Embed.Empty - embed.set_footer(text=footer_text, icon_url=icon_url) + # select menu + if embed.author.name: + title = embed.author.name[:30].strip() + if len(embed.author.name) > 30: + title += "..." + else: + title = embed.title[:30].strip() + if len(embed.title) > 30: + title += "..." + if not title: + create_select = False + + if embed.description: + description = embed.description[:40].replace("*", "").replace("`", "").strip() + if len(embed.description) > 40: + description += "..." + else: + description = "" + select_options.append((title, description)) + + if create_select: + if len(set(x[0] for x in select_options)) != 1: # must have unique authors + self.select_menu = PageSelect(self, select_options) + def add_page(self, item: Embed) -> None: if isinstance(item, Embed): self.pages.append(item) From 77ed926f87949e2fa55bb1c2e6b6b56c2be667ab Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Nov 2021 19:26:15 +0800 Subject: [PATCH 1015/1221] Bump version to v4-dev6 --- bot.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index c553686852..3e3a6ec5cf 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev5" +__version__ = "4.0.0-dev6" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 9f722227eb..031fc5073f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev5' +version = '4.0.0-dev6' description = "Modmail is similar to Reddit's 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 0bdaba666c7540dd9b7b0c2be2fb99677278a040 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Nov 2021 19:26:36 +0800 Subject: [PATCH 1016/1221] Remove unused import --- core/paginator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/paginator.py b/core/paginator.py index bb84aa350f..c4a4772337 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -1,4 +1,3 @@ -from collections import OrderedDict import typing import discord From a743321e01a8339b07f1dd5e8d2dac46b9f21960 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Nov 2021 19:56:28 +0800 Subject: [PATCH 1017/1221] Remove unused import --- core/paginator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/paginator.py b/core/paginator.py index c4a4772337..3126ddc527 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -173,6 +173,8 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji) self.view.clear_items() + await self.message.edit(view=self.view) + self.view.stop() if delete: return await self.base.delete() From 8011ccd6b71bd2a4b9902e13625a50dc52cddc0a Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Nov 2021 20:04:39 +0800 Subject: [PATCH 1018/1221] Fix closing paginators --- core/paginator.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/core/paginator.py b/core/paginator.py index 3126ddc527..3fbd990c4a 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -167,17 +167,18 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: Optional[Message] If `delete` is `True`. """ - self.running = False - - sent_emoji, _ = await self.ctx.bot.retrieve_emoji() - await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji) + if self.running: + self.running = False - self.view.clear_items() - await self.message.edit(view=self.view) + self.view.stop() + if delete: + await self.base.delete() + else: + self.view.clear_items() + await self.base.edit(view=self.view) - self.view.stop() - if delete: - return await self.base.delete() + sent_emoji, _ = await self.ctx.bot.retrieve_emoji() + await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji) class PaginatorView(View): From f7a882a62fa7375f89449e1669168fc14c84e1f1 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Nov 2021 20:15:13 +0800 Subject: [PATCH 1019/1221] Optimise paginators page switching --- core/paginator.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/core/paginator.py b/core/paginator.py index 3fbd990c4a..6525d0ddd0 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -60,7 +60,7 @@ def __init__(self, ctx: commands.Context, *pages, **options): } self._buttons_map = {"<<": None, "<": None, ">": None, ">>": None} - async def show_page(self, index: int) -> None: + async def show_page(self, index: int) -> typing.Optional[typing.Dict]: """ Show a page by page number. @@ -74,13 +74,15 @@ async def show_page(self, index: int) -> None: self.current = index page = self.pages[index] + result = None if self.running: - await self._show_page(page) + result = self._show_page(page) else: await self.create_base(page) self.update_disabled_status() + return result def update_disabled_status(self): if self.current == self.first_page(): @@ -124,7 +126,7 @@ async def create_base(self, item) -> None: async def _create_base(self, item, view: View) -> None: raise NotImplementedError - async def _show_page(self, page): + def _show_page(self, page): raise NotImplementedError def first_page(self): @@ -233,6 +235,7 @@ def fill_items(self): async def interaction_check(self, interaction: Interaction): """Only allow the message author to interact""" if interaction.user != self.handler.ctx.author: + await interaction.response.send_message("Only the original author can control this!", ephemeral=True) return False return True @@ -262,8 +265,8 @@ def __init__(self, handler, page_callback, **kwargs): self.page_callback = page_callback async def callback(self, interaction: Interaction): - await self.handler.show_page(self.page_callback()) - await interaction.response.edit_message(view=self.view) + kwargs = await self.handler.show_page(self.page_callback()) + await interaction.response.edit_message(**kwargs, view=self.view) class PageSelect(Select): @@ -278,8 +281,8 @@ def __init__(self, handler: PaginatorSession, pages: typing.List[typing.Tuple[st async def callback(self, interaction: Interaction): page = int(self.values[0]) - await self.handler.show_page(page) - await interaction.response.edit_message(view=self.view) + kwargs = await self.handler.show_page(page) + await interaction.response.edit_message(**kwargs, view=self.view) class EmbedPaginatorSession(PaginatorSession): @@ -333,8 +336,8 @@ def add_page(self, item: Embed) -> None: async def _create_base(self, item: Embed, view: View) -> None: self.base = await self.destination.send(embed=item, view=view) - async def _show_page(self, page): - await self.base.edit(embed=page) + def _show_page(self, page): + return dict(embed=page) class MessagePaginatorSession(PaginatorSession): @@ -366,6 +369,6 @@ async def _create_base(self, item: str, view: View) -> None: self._set_footer() self.base = await self.ctx.send(content=item, embed=self.embed, view=view) - async def _show_page(self, page) -> None: + def _show_page(self, page) -> typing.Dict: self._set_footer() - await self.base.edit(content=page, embed=self.embed) + return dict(content=page, embed=self.embed) From c16ff261f204f5831da577d26a011db5d65d4610 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Nov 2021 20:21:56 +0800 Subject: [PATCH 1020/1221] Optimise Stop pagination --- core/paginator.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/core/paginator.py b/core/paginator.py index 6525d0ddd0..b9cd29a26b 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -154,7 +154,9 @@ async def run(self) -> typing.Optional[Message]: await self.view.wait() await self.close(delete=False) - async def close(self, delete: bool = True) -> typing.Optional[Message]: + async def close( + self, delete: bool = True, *, interaction: Interaction = None + ) -> typing.Optional[Message]: """ Closes the pagination session. @@ -170,17 +172,22 @@ async def close(self, delete: bool = True) -> typing.Optional[Message]: If `delete` is `True`. """ if self.running: + sent_emoji, _ = await self.ctx.bot.retrieve_emoji() + await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji) + + if interaction: + message = interaction.message + else: + message = self.base + self.running = False self.view.stop() if delete: - await self.base.delete() + await message.delete() else: self.view.clear_items() - await self.base.edit(view=self.view) - - sent_emoji, _ = await self.ctx.bot.retrieve_emoji() - await self.ctx.bot.add_reaction(self.ctx.message, sent_emoji) + await message.edit(view=self.view) class PaginatorView(View): @@ -210,8 +217,7 @@ def __init__(self, handler: PaginatorSession, *args, **kwargs): @discord.ui.button(label="Stop", style=ButtonStyle.danger) async def stop_button(self, button: Button, interaction: Interaction): - await interaction.response.edit_message(view=self) - await self.handler.close() + await self.handler.close(interaction=interaction) def fill_items(self): if self.handler.select_menu is not None: @@ -235,7 +241,9 @@ def fill_items(self): async def interaction_check(self, interaction: Interaction): """Only allow the message author to interact""" if interaction.user != self.handler.ctx.author: - await interaction.response.send_message("Only the original author can control this!", ephemeral=True) + await interaction.response.send_message( + "Only the original author can control this!", ephemeral=True + ) return False return True From 6adcd048d5cd82e6f68487d039249933a51006ab Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Mon, 22 Nov 2021 22:48:40 +0100 Subject: [PATCH 1021/1221] make config readable --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 7278c350fc..dbd3ccf279 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -913,7 +913,7 @@ 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"{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) From f8ad5fd16e3b660d1e303dc52b87152af4b5878e Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Tue, 23 Nov 2021 13:15:33 +0530 Subject: [PATCH 1022/1221] Process snippets in get_contexts --- bot.py | 54 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/bot.py b/bot.py index fb4f8b04a2..d65eee3a48 100644 --- a/bot.py +++ b/bot.py @@ -955,6 +955,16 @@ async def process_dm_modmail(self, message: discord.Message) -> None: await self.add_reaction(message, sent_emoji) self.dispatch("thread_reply", thread, False, message, False, False) + def _get_snippet_command(self) -> commands.Command: + """Get the correct reply command based on the snippet config""" + modifiers = "f" + if self.config["plain_snippets"]: + modifiers += "p" + if self.config["anonymous_snippets"]: + modifiers += "a" + + return self.get_command(f'{modifiers}reply') + async def get_contexts(self, message, *, cls=commands.Context): """ Returns all invocation contexts from the message. @@ -986,18 +996,40 @@ async def get_contexts(self, message, *, cls=commands.Context): self.aliases.pop(invoker) for alias in aliases: - view = StringView(invoked_prefix + alias) + command = None + try: + snippet_text = self.snippets[alias] + except KeyError: + command_invocation_text = alias + else: + command = self._get_snippet_command() + command_invocation_text = f'{invoked_prefix}{command} {snippet_text}' + + view = StringView(invoked_prefix + command_invocation_text) 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) + ctx_.command = command or 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) + + try: + snippet_text = self.snippets[invoker] + except KeyError: + # Process regular commands + ctx.command = self.all_commands.get(invoker) + ctx.invoked_with = invoker + else: + # Process snippets + ctx.command = self._get_snippet_command() + reply_view = StringView(f'{invoked_prefix}{ctx.command} {snippet_text}') + discord.utils.find(reply_view.skip_string, prefixes) + ctx.invoked_with = reply_view.get_word().lower() + ctx.view = reply_view + return [ctx] async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context): @@ -1139,20 +1171,6 @@ async def process_commands(self, message): if isinstance(message.channel, discord.DMChannel): return await self.process_dm_modmail(message) - if message.content.startswith(self.prefix): - cmd = message.content[len(self.prefix) :].strip() - - # Process snippets - cmd = cmd.lower() - if cmd in self.snippets: - snippet = self.snippets[cmd] - modifiers = "f" - if self.config["plain_snippets"]: - modifiers += "p" - if self.config["anonymous_snippets"]: - modifiers += "a" - message.content = f"{self.prefix}{modifiers}reply {snippet}" - ctxs = await self.get_contexts(message) for ctx in ctxs: if ctx.command: From 4de349111ea1e070bb886e1d69a2cb1d1a038dae Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Tue, 23 Nov 2021 13:17:59 +0530 Subject: [PATCH 1023/1221] Allow the usage of snippets in `alias add` --- cogs/utility.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 7278c350fc..89c4467109 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1070,7 +1070,9 @@ async def make_alias(self, name, value, action): linked_command = view.get_word().lower() message = view.read_rest() - if not self.bot.get_command(linked_command): + is_snippet = linked_command in self.bot.snippets and not message + + if not self.bot.get_command(linked_command) and not is_snippet: alias_command = self.bot.aliases.get(linked_command) if alias_command is not None: save_aliases.extend(utils.normalize_alias(alias_command, message)) From 55e90f564dc5fe097f71685e4c1adb29eff9989a Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Sat, 27 Nov 2021 21:42:25 +0530 Subject: [PATCH 1024/1221] Handle snippet deletions and resolve aliases --- bot.py | 26 +++++++++++- cogs/modmail.py | 104 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 120 insertions(+), 10 deletions(-) diff --git a/bot.py b/bot.py index d65eee3a48..df513ed8ad 100644 --- a/bot.py +++ b/bot.py @@ -46,7 +46,7 @@ ) from core.thread import ThreadManager from core.time import human_timedelta -from core.utils import normalize_alias, truncate, tryint +from core.utils import normalize_alias, parse_alias, truncate, tryint logger = getLogger(__name__) @@ -85,6 +85,30 @@ def __init__(self): self.plugin_db = PluginDatabaseClient(self) # Deprecated self.startup() + def _resolve_snippet(self, name: str) -> typing.Optional[str]: + """ + Get actual snippet names from direct aliases to snippets. + + If the provided name is a snippet, it's returned unchanged. + If there is an alias by this name, it is parsed to see if it + refers only to a snippet, in which case that snippet name is + returned. + + If no snippets were found, None is returned. + """ + if name in self.snippets: + return name + + try: + command, = parse_alias(self.aliases[name]) + except (KeyError, ValueError): + # There is either no alias by this name present or the + # alias has multiple steps. + pass + else: + if command in self.snippets: + return command + @property def uptime(self) -> str: now = discord.utils.utcnow() diff --git a/cogs/modmail.py b/cogs/modmail.py index 7900f0eeef..3d29523421 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -7,6 +7,7 @@ import discord from discord.ext import commands +from discord.ext.commands.view import StringView from discord.ext.commands.cooldowns import BucketType from discord.role import Role from discord.utils import escape_markdown @@ -143,12 +144,14 @@ async def snippet(self, ctx, *, name: str.lower = None): """ if name is not None: - val = self.bot.snippets.get(name) - if val is None: + snippet_name = self.bot._resolve_snippet(name) + + if snippet_name is None: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") else: + val = self.bot.snippets[snippet_name] embed = discord.Embed( - title=f'Snippet - "{name}":', description=val, color=self.bot.main_color + title=f'Snippet - "{snippet_name}":', description=val, color=self.bot.main_color ) return await ctx.send(embed=embed) @@ -177,13 +180,13 @@ async def snippet_raw(self, ctx, *, name: str.lower): """ View the raw content of a snippet. """ - val = self.bot.snippets.get(name) - if val is None: + snippet_name = self.bot._resolve_snippet(name) + if snippet_name is None: embed = create_not_found_embed(name, self.bot.snippets.keys(), "Snippet") else: - val = truncate(escape_code_block(val), 2048 - 7) + val = truncate(escape_code_block(self.bot.snippets[snippet_name]), 2048 - 7) embed = discord.Embed( - title=f'Raw snippet - "{name}":', + title=f'Raw snippet - "{snippet_name}":', description=f"```\n{val}```", color=self.bot.main_color, ) @@ -246,16 +249,99 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte ) return await ctx.send(embed=embed) + def _fix_aliases(self, snippet_being_deleted: str) -> tuple[list[str]]: + """ + Remove references to the snippet being deleted from aliases. + + Direct aliases to snippets are deleted, and aliases having + other steps are edited. + + A tuple of dictionaries are returned. The first dictionary + contains a mapping of alias names which were deleted to their + original value, and the second dictionary contains a mapping + of alias names which were edited to their original value. + """ + deleted = {} + edited = {} + + # Using a copy since we might need to delete aliases + for alias, val in self.bot.aliases.copy().items(): + values = parse_alias(val) + + save_aliases = [] + + for val in values: + view = StringView(val) + linked_command = view.get_word().lower() + message = view.read_rest() + + if linked_command == snippet_being_deleted: + continue + + is_valid_snippet = snippet_being_deleted in self.bot.snippets + + if not self.bot.get_command(linked_command) and not is_valid_snippet: + alias_command = self.bot.aliases[linked_command] + save_aliases.extend(normalize_alias(alias_command, message)) + else: + save_aliases.append(val) + + if not save_aliases: + original_value = self.bot.aliases.pop(alias) + deleted[alias] = original_value + else: + original_alias = self.bot.aliases[alias] + new_alias = " && ".join(f'"{a}"' for a in save_aliases) + + if original_alias != new_alias: + self.bot.aliases[alias] = new_alias + edited[alias] = original_alias + + return deleted, edited + @snippet.command(name="remove", aliases=["del", "delete"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippet_remove(self, ctx, *, name: str.lower): """Remove a snippet.""" - if name in self.bot.snippets: + deleted_aliases, edited_aliases = self._fix_aliases(name) + + deleted_aliases_string = ','.join(f'`{alias}`' for alias in deleted_aliases) + if len(deleted_aliases) == 1: + deleted_aliases_output = f'The `{deleted_aliases_string}` direct alias has been removed.' + elif deleted_aliases: + deleted_aliases_output = f'The following direct aliases have been removed: {deleted_aliases_string}.' + else: + deleted_aliases_output = None + + if len(edited_aliases) == 1: + alias, val = edited_aliases.popitem() + edited_aliases_output = ( + f'Steps pointing to this snippet have been removed from the `{alias}` alias' + f' (previous value: `{val}`).`' + ) + elif edited_aliases: + alias_list = '\n'.join([ + f'- `{alias_name}` (previous value: `{val}`)' + for alias_name, val in edited_aliases.items() + ]) + edited_aliases_output = ( + f'Steps pointing to this snippet have been removed from the following aliases:' + f'\n\n{alias_list}' + ) + else: + edited_aliases_output = None + + description = f"Snippet `{name}` is now deleted." + if deleted_aliases_output: + description += f'\n\n{deleted_aliases_output}' + if edited_aliases_output: + description += f'\n\n{edited_aliases_output}' + embed = discord.Embed( title="Removed snippet", color=self.bot.main_color, - description=f"Snippet `{name}` is now deleted.", + description=description, ) self.bot.snippets.pop(name) await self.bot.config.update() From 3599f02874a8611ca3e3979d7596fecacd0c1faa Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Sat, 27 Nov 2021 21:42:41 +0530 Subject: [PATCH 1025/1221] make the help output for snippets list its aliases --- cogs/utility.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 89c4467109..e38fc0984b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -184,7 +184,20 @@ async def send_error_message(self, error): val = self.context.bot.snippets.get(command) if val is not None: 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) + embed.add_field(name=f"`{command}` will send:", value=val, inline=False) + + snippet_aliases = [] + for alias in self.context.bot.aliases: + if self.context.bot._resolve_snippet(alias) == command: + snippet_aliases.append(f'`{alias}`') + + if snippet_aliases: + embed.add_field( + name=f'Aliases to this snippet:', + value=','.join(snippet_aliases), + inline=False + ) + return await self.get_destination().send(embed=embed) val = self.context.bot.aliases.get(command) From ba93b9c4cf1cd279f5bcf1a148c25435a748dec3 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Sat, 27 Nov 2021 23:54:44 +0530 Subject: [PATCH 1026/1221] Handle multi-word snippets --- bot.py | 22 +++++++++++++--------- cogs/utility.py | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/bot.py b/bot.py index df513ed8ad..f808448429 100644 --- a/bot.py +++ b/bot.py @@ -1010,9 +1010,17 @@ async def get_contexts(self, message, *, cls=commands.Context): invoker = view.get_word().lower() + # Check if a snippet is being called. + # This needs to be done before checking for aliases since + # snippets can have multiple words. + try: + snippet_text = self.snippets[message.content.removeprefix(invoked_prefix)] + except KeyError: + snippet_text = None + # Check if there is any aliases being called. alias = self.aliases.get(invoker) - if alias is not None: + if alias is not None and snippet_text is None: ctxs = [] aliases = normalize_alias(alias, message.content[len(f"{invoked_prefix}{invoker}") :]) if not aliases: @@ -1028,7 +1036,6 @@ async def get_contexts(self, message, *, cls=commands.Context): else: command = self._get_snippet_command() command_invocation_text = f'{invoked_prefix}{command} {snippet_text}' - view = StringView(invoked_prefix + command_invocation_text) ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message) ctx_.thread = thread @@ -1040,19 +1047,16 @@ async def get_contexts(self, message, *, cls=commands.Context): ctx.thread = thread - try: - snippet_text = self.snippets[invoker] - except KeyError: - # Process regular commands - ctx.command = self.all_commands.get(invoker) - ctx.invoked_with = invoker - else: + if snippet_text is not None: # Process snippets ctx.command = self._get_snippet_command() reply_view = StringView(f'{invoked_prefix}{ctx.command} {snippet_text}') discord.utils.find(reply_view.skip_string, prefixes) ctx.invoked_with = reply_view.get_word().lower() ctx.view = reply_view + else: + ctx.command = self.all_commands.get(invoker) + ctx.invoked_with = invoker return [ctx] diff --git a/cogs/utility.py b/cogs/utility.py index e38fc0984b..a3f5597519 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1083,7 +1083,7 @@ async def make_alias(self, name, value, action): linked_command = view.get_word().lower() message = view.read_rest() - is_snippet = linked_command in self.bot.snippets and not message + is_snippet = val in self.bot.snippets if not self.bot.get_command(linked_command) and not is_snippet: alias_command = self.bot.aliases.get(linked_command) From 6041d37fbfe2990eb7f9ef2aedce40065b8ea341 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Sun, 28 Nov 2021 00:08:18 +0530 Subject: [PATCH 1027/1221] Update the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4409e12bb..803b5f0c1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/kyb3r/modmail/issues/3048)) - Use discord relative timedeltas. ([GH #3046](https://github.com/kyb3r/modmail/issues/3046)) +- Snippets can be used in aliases. ([GH #3108](https://github.com/kyb3r/modmail/issues/3108)) ### Fixed From 893acc982f738c2384ddd2d274c974322bb8a697 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:45:43 +0530 Subject: [PATCH 1028/1221] Lint --- bot.py | 8 ++++---- cogs/modmail.py | 30 +++++++++++++++++------------- cogs/utility.py | 8 +++----- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/bot.py b/bot.py index f808448429..b01bc30ee9 100644 --- a/bot.py +++ b/bot.py @@ -100,7 +100,7 @@ def _resolve_snippet(self, name: str) -> typing.Optional[str]: return name try: - command, = parse_alias(self.aliases[name]) + (command,) = parse_alias(self.aliases[name]) except (KeyError, ValueError): # There is either no alias by this name present or the # alias has multiple steps. @@ -987,7 +987,7 @@ def _get_snippet_command(self) -> commands.Command: if self.config["anonymous_snippets"]: modifiers += "a" - return self.get_command(f'{modifiers}reply') + return self.get_command(f"{modifiers}reply") async def get_contexts(self, message, *, cls=commands.Context): """ @@ -1035,7 +1035,7 @@ async def get_contexts(self, message, *, cls=commands.Context): command_invocation_text = alias else: command = self._get_snippet_command() - command_invocation_text = f'{invoked_prefix}{command} {snippet_text}' + command_invocation_text = f"{invoked_prefix}{command} {snippet_text}" view = StringView(invoked_prefix + command_invocation_text) ctx_ = cls(prefix=self.prefix, view=view, bot=self, message=message) ctx_.thread = thread @@ -1050,7 +1050,7 @@ async def get_contexts(self, message, *, cls=commands.Context): if snippet_text is not None: # Process snippets ctx.command = self._get_snippet_command() - reply_view = StringView(f'{invoked_prefix}{ctx.command} {snippet_text}') + reply_view = StringView(f"{invoked_prefix}{ctx.command} {snippet_text}") discord.utils.find(reply_view.skip_string, prefixes) ctx.invoked_with = reply_view.get_word().lower() ctx.view = reply_view diff --git a/cogs/modmail.py b/cogs/modmail.py index 3d29523421..f06dc4339c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -306,37 +306,41 @@ async def snippet_remove(self, ctx, *, name: str.lower): if name in self.bot.snippets: deleted_aliases, edited_aliases = self._fix_aliases(name) - deleted_aliases_string = ','.join(f'`{alias}`' for alias in deleted_aliases) + deleted_aliases_string = ",".join(f"`{alias}`" for alias in deleted_aliases) if len(deleted_aliases) == 1: - deleted_aliases_output = f'The `{deleted_aliases_string}` direct alias has been removed.' + deleted_aliases_output = f"The `{deleted_aliases_string}` direct alias has been removed." elif deleted_aliases: - deleted_aliases_output = f'The following direct aliases have been removed: {deleted_aliases_string}.' + deleted_aliases_output = ( + f"The following direct aliases have been removed: {deleted_aliases_string}." + ) else: deleted_aliases_output = None if len(edited_aliases) == 1: alias, val = edited_aliases.popitem() edited_aliases_output = ( - f'Steps pointing to this snippet have been removed from the `{alias}` alias' - f' (previous value: `{val}`).`' + f"Steps pointing to this snippet have been removed from the `{alias}` alias" + f" (previous value: `{val}`).`" ) elif edited_aliases: - alias_list = '\n'.join([ - f'- `{alias_name}` (previous value: `{val}`)' - for alias_name, val in edited_aliases.items() - ]) + alias_list = "\n".join( + [ + f"- `{alias_name}` (previous value: `{val}`)" + for alias_name, val in edited_aliases.items() + ] + ) edited_aliases_output = ( - f'Steps pointing to this snippet have been removed from the following aliases:' - f'\n\n{alias_list}' + f"Steps pointing to this snippet have been removed from the following aliases:" + f"\n\n{alias_list}" ) else: edited_aliases_output = None description = f"Snippet `{name}` is now deleted." if deleted_aliases_output: - description += f'\n\n{deleted_aliases_output}' + description += f"\n\n{deleted_aliases_output}" if edited_aliases_output: - description += f'\n\n{edited_aliases_output}' + description += f"\n\n{edited_aliases_output}" embed = discord.Embed( title="Removed snippet", diff --git a/cogs/utility.py b/cogs/utility.py index a3f5597519..26ae936ffc 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -189,13 +189,11 @@ async def send_error_message(self, error): snippet_aliases = [] for alias in self.context.bot.aliases: if self.context.bot._resolve_snippet(alias) == command: - snippet_aliases.append(f'`{alias}`') + snippet_aliases.append(f"`{alias}`") if snippet_aliases: embed.add_field( - name=f'Aliases to this snippet:', - value=','.join(snippet_aliases), - inline=False + name=f"Aliases to this snippet:", value=",".join(snippet_aliases), inline=False ) return await self.get_destination().send(embed=embed) @@ -1083,7 +1081,7 @@ async def make_alias(self, name, value, action): linked_command = view.get_word().lower() message = view.read_rest() - is_snippet = val in self.bot.snippets + is_snippet = val in self.bot.snippets if not self.bot.get_command(linked_command) and not is_snippet: alias_command = self.bot.aliases.get(linked_command) From aeb1128d83b2a9136cee411e7abe68dce428a18a Mon Sep 17 00:00:00 2001 From: Chris Lovering <chris.lovering.95@gmail.com> Date: Sun, 5 Dec 2021 21:28:56 +0000 Subject: [PATCH 1029/1221] Add git to Dockerfile Since discord.py is installed via git in requirements.txt the Dockerfile needs to have git installed to download it. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 34aba25ce6..76992efcc5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.9-slim as py FROM py as build -RUN apt update && apt install -y g++ +RUN apt update && apt install -y g++ git COPY requirements.txt / RUN pip install --prefix=/inst -U -r /requirements.txt From d90c1ecf0d05d2ae6e8aa95a62ab534a32cc3b02 Mon Sep 17 00:00:00 2001 From: Chris Lovering <chris.lovering.95@gmail.com> Date: Sun, 5 Dec 2021 23:04:15 +0000 Subject: [PATCH 1030/1221] Regenerate reqs.txt and use full python image This is so that all of the requirements for lottie are available within the image --- Dockerfile | 2 +- requirements.txt | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 76992efcc5..2906f4508f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim as py +FROM python:3.9 as py FROM py as build diff --git a/requirements.txt b/requirements.txt index fb43d23e2b..737a58c219 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,21 +9,31 @@ aiohttp==3.7.4.post0 async-timeout==3.0.1; python_full_version >= '3.5.3' attrs==21.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +cairocffi==1.3.0; python_version >= '3.7' +cairosvg==2.5.2; python_version >= '3.5' +cffi==1.15.0 chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' colorama==0.4.4 +cssselect2==0.4.1; python_version >= '3.6' +defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' dnspython==2.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' emoji==1.2.0 git+https://github.com/Rapptz/discord.py.git@45d498c1b76deaf3b394d17ccf56112fa691d160#egg=discord-py idna==3.3; python_version >= '3.5' isodate==0.6.0 +lottie[pdf]==0.6.10 motor==2.4.0 multidict==5.2.0; python_version >= '3.6' natural==0.2.0 parsedatetime==2.6 +pillow==8.4.0; python_version >= '3.6' +pycparser==2.21 pymongo[srv]==3.12.1 python-dateutil==2.8.2 python-dotenv==0.18.0 six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +tinycss2==1.1.0; python_version >= '3.6' typing-extensions==4.0.0; python_version >= '3.6' uvloop==0.16.0; sys_platform != 'win32' +webencodings==0.5.1 yarl==1.7.2; python_version >= '3.6' \ No newline at end of file From 9b58b99cbc2dc7deb84aad73bfbab09ed696ef80 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 8 Feb 2022 01:59:54 -0800 Subject: [PATCH 1031/1221] 3.10.3 contact fix --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- bot.py | 2 +- core/models.py | 13 ++++++++----- pyproject.toml | 2 +- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af6e0cc407..8f9b461169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.10.3 +This is a hotfix for contact command. + +### Fixed + +- Fixed a bug where contacting with no category argument defaults to the top category. + + # v3.10.2 This is a hotfix for react to contact. diff --git a/README.md b/README.md index 2b4f44ea62..465b13f1ab 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v3.10.2-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v3.10.3-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> diff --git a/bot.py b/bot.py index 7d4248173c..0ee38be340 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.2" +__version__ = "3.10.3" import asyncio diff --git a/core/models.py b/core/models.py index 445f7793ae..670a23c743 100644 --- a/core/models.py +++ b/core/models.py @@ -2,6 +2,7 @@ import re import sys import os +from difflib import get_close_matches from enum import IntEnum from logging.handlers import RotatingFileHandler from string import Formatter @@ -202,13 +203,15 @@ async def convert(self, ctx, argument): return await super().convert(ctx, argument) except commands.ChannelNotFound: - def check(c): - return isinstance(c, discord.CategoryChannel) and c.name.lower().startswith(argument.lower()) - if guild: - result = discord.utils.find(check, guild.categories) + categories = {c.name.casefold(): c for c in guild.categories} else: - result = discord.utils.find(check, bot.get_all_channels()) + categories = {c.name.casefold(): c for c in bot.get_all_channels() + if isinstance(c, discord.CategoryChannel)} + + result = get_close_matches(argument.casefold(), categories.keys(), n=1, cutoff=0.75) + if result: + result = categories[result[0]] if not isinstance(result, discord.CategoryChannel): raise commands.ChannelNotFound(argument) diff --git a/pyproject.toml b/pyproject.toml index 4dc7d411a5..341e506413 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.2' +version = '3.10.3' description = "Modmail is similar to Reddit's 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 fa718e4023a2b52f90d228e37556e7a5e90225e8 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 21 Mar 2022 15:11:20 +0800 Subject: [PATCH 1032/1221] Update to dpy@97fe07edb2f3d0f5a980917fb6ee479bf396fd94 & async changes --- CHANGELOG.md | 4 + Pipfile | 2 +- Pipfile.lock | 708 ++++++++++++++++++++++++---------------------- bot.py | 162 +++++------ cogs/modmail.py | 4 +- cogs/plugins.py | 18 +- cogs/utility.py | 8 +- core/paginator.py | 4 +- core/thread.py | 11 +- pyproject.toml | 2 +- 10 files changed, 466 insertions(+), 457 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa4dea5219..209bbcfcea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,10 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Breaking +- Modmail now requires [`Message Content` privileged intent](https://support-dev.discord.com/hc/en-us/articles/4404772028055-Message-Content-Privileged-Intent-for-Verified-Bots). - Upgraded to discord.py v2.0 master ([internal changes](https://gist.github.com/apple502j/f75b4f24652f04de85c7084ffd73ec58), [GH #2990](https://github.com/kyb3r/modmail/issues/2990)). - Python 3.8 or higher is required. +- Asyncio changes ([gist](https://gist.github.com/Rapptz/6706e1c8f23ac27c98cee4dd985c8120)) ### Added @@ -41,11 +43,13 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Support LOTTIE stickers. ([GH #3119](https://github.com/kyb3r/modmail/issues/3119)) - Editing notes now work. ([GH #3094](https://github.com/kyb3r/modmail/issues/3094)) - Commands now work in threads. +- Audit log searching now properly works. ### Internal - Improve regex parsing of channel topics. ([GH #3114](https://github.com/kyb3r/modmail/issues/3114), [PR #3111](https://github.com/kyb3r/modmail/pull/3111)) - Add warning if deploying on a developmental version. +- Extensions are now loaded `on_connect`. # v3.10.3 diff --git a/Pipfile b/Pipfile index a78db95d18..04d998cbc8 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ pylint = "~=2.9.3" [packages] aiohttp = "==3.7.4.post0" colorama = "~=0.4.4" # Doesn't officially support Python 3.9 yet, v0.4.5 will support 3.9 -"discord.py" = {ref = "45d498c1b76deaf3b394d17ccf56112fa691d160", git = "https://github.com/Rapptz/discord.py.git"} +"discord.py" = {ref = "97fe07edb2f3d0f5a980917fb6ee479bf396fd94", git = "https://github.com/Rapptz/discord.py.git"} emoji = "~=1.2.0" isodate = "~=0.6.0" motor = "~=2.4.0" diff --git a/Pipfile.lock b/Pipfile.lock index 1fa9e5e64a..5cd3d63616 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "29d6f377cdb874102deb4df1406ff6d0a66834fcb89660f3c9599a2343cda038" + "sha256": "5f3ca86a89bbc9bcda36bdb3503c91f81ff9c5e12409c3ad056524aa920f4fad" }, "pipfile-spec": 6, "requires": { @@ -69,11 +69,11 @@ }, "attrs": { "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", + "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" + "version": "==21.4.0" }, "cairocffi": { "hashes": [ @@ -163,11 +163,11 @@ }, "cssselect2": { "hashes": [ - "sha256:2f4a9f20965367bae459e3bb42561f7927e0cfe5b7ea1692757cf67ef5d7dace", - "sha256:93fbb9af860e95dd40bf18c3b2b6ed99189a07c0f29ba76f9c5be71344664ec8" + "sha256:8d4690bce5f25013262997e64cef3e7bade877d3ef126f9cc624e5b1f294d934", + "sha256:d98a7bbdd8ebc46093279195d669a3359bd5a23f90c19e82c19d9eeef333e617" ], - "markers": "python_version >= '3.6'", - "version": "==0.4.1" + "markers": "python_version >= '3.7'", + "version": "==0.5.0" }, "defusedxml": { "hashes": [ @@ -179,19 +179,19 @@ }, "discord-py": { "git": "https://github.com/Rapptz/discord.py.git", - "ref": "45d498c1b76deaf3b394d17ccf56112fa691d160" + "ref": "97fe07edb2f3d0f5a980917fb6ee479bf396fd94" }, "discord.py": { "git": "https://github.com/Rapptz/discord.py.git", - "ref": "45d498c1b76deaf3b394d17ccf56112fa691d160" + "ref": "97fe07edb2f3d0f5a980917fb6ee479bf396fd94" }, "dnspython": { "hashes": [ - "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216", - "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4" + "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", + "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.1.0" + "version": "==2.2.1" }, "emoji": { "hashes": [ @@ -211,11 +211,11 @@ }, "isodate": { "hashes": [ - "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", - "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81" + "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96", + "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9" ], "index": "pypi", - "version": "==0.6.0" + "version": "==0.6.1" }, "lottie": { "extras": [ @@ -237,81 +237,68 @@ }, "multidict": { "hashes": [ - "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b", - "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031", - "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0", - "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce", - "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda", - "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858", - "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5", - "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8", - "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22", - "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac", - "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e", - "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6", - "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5", - "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0", - "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11", - "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a", - "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55", - "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341", - "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b", - "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704", - "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b", - "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1", - "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621", - "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d", - "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5", - "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7", - "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac", - "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d", - "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef", - "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0", - "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f", - "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02", - "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b", - "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37", - "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23", - "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d", - "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065", - "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86", - "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6", - "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded", - "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4", - "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7", - "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a", - "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17", - "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3", - "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21", - "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24", - "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940", - "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac", - "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c", - "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422", - "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628", - "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0", - "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf", - "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e", - "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677", - "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f", - "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c", - "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4", - "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b", - "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747", - "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0", - "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01", - "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8", - "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9", - "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64", - "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d", - "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0", - "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52", - "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1", - "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae", - "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d" + "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60", + "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c", + "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672", + "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51", + "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032", + "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2", + "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b", + "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80", + "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88", + "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a", + "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d", + "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389", + "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c", + "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9", + "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c", + "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516", + "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b", + "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43", + "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee", + "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227", + "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d", + "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae", + "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7", + "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4", + "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9", + "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f", + "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013", + "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9", + "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e", + "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693", + "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a", + "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15", + "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb", + "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96", + "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87", + "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376", + "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658", + "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0", + "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071", + "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360", + "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc", + "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3", + "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba", + "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8", + "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9", + "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2", + "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3", + "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68", + "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8", + "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d", + "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49", + "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608", + "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57", + "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86", + "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20", + "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293", + "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849", + "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937", + "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d" ], - "markers": "python_version >= '3.6'", - "version": "==5.2.0" + "markers": "python_version >= '3.7'", + "version": "==6.0.2" }, "natural": { "hashes": [ @@ -330,50 +317,44 @@ }, "pillow": { "hashes": [ - "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76", - "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585", - "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b", - "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8", - "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55", - "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc", - "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645", - "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff", - "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc", - "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b", - "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6", - "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20", - "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e", - "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a", - "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779", - "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02", - "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39", - "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f", - "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a", - "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409", - "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c", - "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488", - "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b", - "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d", - "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09", - "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b", - "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153", - "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9", - "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad", - "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df", - "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df", - "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed", - "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed", - "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698", - "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29", - "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649", - "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49", - "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b", - "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2", - "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a", - "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78" + "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97", + "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049", + "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c", + "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae", + "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28", + "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030", + "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56", + "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976", + "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e", + "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e", + "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f", + "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b", + "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a", + "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e", + "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa", + "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7", + "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00", + "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838", + "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360", + "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b", + "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a", + "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd", + "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4", + "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70", + "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204", + "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc", + "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b", + "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669", + "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7", + "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e", + "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c", + "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092", + "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c", + "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5", + "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac" ], - "markers": "python_version >= '3.6'", - "version": "==8.4.0" + "markers": "python_version >= '3.7'", + "version": "==9.0.1" }, "pycparser": { "hashes": [ @@ -387,116 +368,116 @@ "srv" ], "hashes": [ - "sha256:02e0c088f189ca69fac094cb5f851b43bbbd7cec42114495777d4d8f297f7f8a", - "sha256:138248c542051eb462f88b50b0267bd5286d6661064bab06faa0ef6ac30cdb4b", - "sha256:13a7c6d055af58a1e9c505e736da8b6a2e95ccc8cec10b008143f7a536e5de8a", - "sha256:13d74bf3435c1e58d8fafccc0d5e87f246ae2c6e9cbef4b35e32a1c3759e354f", - "sha256:15dae01341571d0af51526b7a21648ca575e9375e16ba045c9860848dfa8952f", - "sha256:17238115e6d37f5423b046cb829f1ca02c4ea7edb163f5b8b88e0c975dc3fec9", - "sha256:180b405e17b90a877ea5dbc5efe7f4c171af4c89323148e100c0f12cedb86f12", - "sha256:1821ce4e5a293313947fd017bbd2d2535aa6309680fa29b33d0442d15da296ec", - "sha256:1a7b138a04fdd17849930dc8bf664002e17db38448850bfb96d200c9c5a8b3a1", - "sha256:1c4e51a3b69789b6f468a8e881a13f2d1e8f5e99e41f80fd44845e6ec0f701e1", - "sha256:1d55982e5335925c55e2b87467043866ce72bd30ea7e7e3eeed6ec3d95a806d4", - "sha256:1fa6f08ddb6975371777f97592d35c771e713ee2250e55618148a5e57e260aff", - "sha256:2174d3279b8e2b6d7613b338f684cd78ff7adf1e7ec5b7b7bde5609a129c9898", - "sha256:2462a68f6675da548e333fa299d8e9807e00f95a4d198cfe9194d7be69f40c9b", - "sha256:25fd76deabe9ea37c8360c362b32f702cc095a208dd1c5328189938ca7685847", - "sha256:287c2a0063267c1458c4ddf528b44063ce7f376a6436eea5bccd7f625bbc3b5e", - "sha256:2d3abe548a280b49269c7907d5b71199882510c484d680a5ea7860f30c4a695f", - "sha256:2fa101bb23619120673899694a65b094364269e597f551a87c4bdae3a474d726", - "sha256:2fda3b3fb5c0d159195ab834b322a23808f1b059bcc7e475765abeddee6a2529", - "sha256:303531649fa45f96b694054c1aa02f79bda32ef57affe42c5c339336717eed74", - "sha256:36806ee53a85c3ba73939652f2ced2961e6a77cfbae385cd83f2e24cd97964b7", - "sha256:37a63da5ee623acdf98e6d511171c8a5827a6106b0712c18af4441ef4f11e6be", - "sha256:3a2fcbd04273a509fa85285d9eccf17ab65ce440bd4f5e5a58c978e563cd9e9a", - "sha256:3b40e36d3036bfe69ba63ec8e746a390721f75467085a0384b528e1dda532c69", - "sha256:4168b6c425d783e81723fc3dc382d374a228ff29530436a472a36d9f27593e73", - "sha256:444c00ebc20f2f9dc62e34f7dc9453dc2f5f5a72419c8dccad6e26d546c35712", - "sha256:45d6b47d70ed44e3c40bef618ed61866c48176e7e5dff80d06d8b1a6192e8584", - "sha256:460bdaa3f65ddb5b7474ae08589a1763b5da1a78b8348351b9ba1c63b459d67d", - "sha256:47ed77f62c8417a86f9ad158b803f3459a636386cb9d3d4e9e7d6a82d051f907", - "sha256:48722e91981bb22a16b0431ea01da3e1cc5b96805634d3b8d3c2a5315c1ce7f1", - "sha256:49b0d92724d3fce1174fd30b0b428595072d5c6b14d6203e46a9ea347ae7b439", - "sha256:4a2d73a9281faefb273a5448f6d25f44ebd311ada9eb79b6801ae890508fe231", - "sha256:4f4bc64fe9cbd70d46f519f1e88c9e4677f7af18ab9cd4942abce2bcfa7549c3", - "sha256:5067c04d3b19c820faac6342854d887ade58e8d38c3db79b68c2a102bbb100e7", - "sha256:51437c77030bed72d57d8a61e22758e3c389b13fea7787c808030002bb05ca39", - "sha256:515e4708d6567901ffc06476a38abe2c9093733f52638235d9f149579c1d3de0", - "sha256:5183b698d6542219e4135de583b57bc6286bd37df7f645b688278eb919bfa785", - "sha256:56feb80ea1f5334ccab9bd16a5161571ab70392e51fcc752fb8a1dc67125f663", - "sha256:573e2387d0686976642142c50740dfc4d3494cc627e2a7d22782b99f70879055", - "sha256:58a67b3800476232f9989e533d0244060309451b436d46670a53e6d189f1a7e7", - "sha256:5e3833c001a04aa06a28c6fd9628256862a654c09b0f81c07734b5629bc014ab", - "sha256:5f5fe59328838fa28958cc06ecf94be585726b97d637012f168bc3c7abe4fd81", - "sha256:6235bf2157aa46e53568ed79b70603aa8874baa202d5d1de82fa0eb917696e73", - "sha256:63be03f7ae1e15e72a234637ec7941ef229c7ab252c9ff6af48bba1e5418961c", - "sha256:65f159c445761cab04b665fc448b3fc008aebc98e54fdcbfd1aff195ef1b1408", - "sha256:67e0b2ad3692f6d0335ae231a40de55ec395b6c2e971ad6f55b162244d1ec542", - "sha256:68409171ab2aa7ccd6e8e839233e4b8ddeec246383c9a3698614e814739356f9", - "sha256:6a96c04ce39d66df60d9ce89f4c254c4967bc7d9e2e2c52adc58f47be826ee96", - "sha256:6ead0126fb4424c6c6a4fdc603d699a9db7c03cdb8eac374c352a75fec8a820a", - "sha256:6eb6789f26c398c383225e1313c8e75a7d290d323b8eaf65f3f3ddd0eb8a5a3c", - "sha256:6f07888e3b73c0dfa46f12d098760494f5f23fd66923a6615edfe486e6a7649c", - "sha256:6f0f0a10f128ea0898e607d351ebfabf70941494fc94e87f12c76e2894d8e6c4", - "sha256:704879b6a54c45ad76cea7c6789c1ae7185050acea7afd15b58318fa1932ed45", - "sha256:7117bfd8827cfe550f65a3c399dcd6e02226197a91c6d11a3540c3e8efc686d6", - "sha256:712de1876608fd5d76abc3fc8ec55077278dd5044073fbe9492631c9a2c58351", - "sha256:75c7ef67b4b8ec070e7a4740764f6c03ec9246b59d95e2ae45c029d41cb9efa1", - "sha256:77dddf596fb065de29fb39992fbc81301f7fd0003be649b7fa7448c77ca53bed", - "sha256:7abc87e45b572eb6d17a50422e69a9e5d6f13e691e821fe2312df512500faa50", - "sha256:7d8cdd2f070c71366e64990653522cce84b08dc26ab0d1fa19aa8d14ee0cf9ba", - "sha256:81ce5f871f5d8e82615c8bd0b34b68a9650204c8b1a04ce7890d58c98eb66e39", - "sha256:837cdef094f39c6f4a2967abc646a412999c2540fbf5d3cce1dd3b671f4b876c", - "sha256:849e641cfed05c75d772f9e9018f42c5fbd00655d43d52da1b9c56346fd3e4cc", - "sha256:87114b995506e7584cf3daf891e419b5f6e7e383e7df6267494da3a76312aa22", - "sha256:87db421c9eb915b8d9a9a13c5b2ee338350e36ee83e26ff0adfc48abc5db3ac3", - "sha256:8851544168703fb519e95556e3b463fca4beeef7ed3f731d81a68c8268515d9d", - "sha256:891f541c7ed29b95799da0cd249ae1db1842777b564e8205a197b038c5df6135", - "sha256:8f87f53c9cd89010ae45490ec2c963ff18b31f5f290dc08b04151709589fe8d9", - "sha256:9641be893ccce7d192a0094efd0a0d9f1783a1ebf314b4128f8a27bfadb8a77c", - "sha256:979e34db4f3dc5710c18db437aaf282f691092b352e708cb2afd4df287698c76", - "sha256:9b62d84478f471fdb0dcea3876acff38f146bd23cbdbed15074fb4622064ec2e", - "sha256:a472ca3d43d33e596ff5836c6cc71c3e61be33f44fe1cfdab4a1100f4af60333", - "sha256:a5dbeeea6a375fbd79448b48a54c46fc9351611a03ef8398d2a40b684ce46194", - "sha256:a7430f3987d232e782304c109be1d0e6fff46ca6405cb2479e4d8d08cd29541e", - "sha256:a81e52dbf95f236a0c89a5abcd2b6e1331da0c0312f471c73fae76c79d2acf6b", - "sha256:aa434534cc91f51a85e3099dc257ee8034b3d2be77f2ca58fb335a686e3a681f", - "sha256:ab27d6d7d41a66d9e54269a290d27cd5c74f08e9add0054a754b4821026c4f42", - "sha256:adb37bf22d25a51b84d989a2a5c770d4514ac590201eea1cb50ce8c9c5257f1d", - "sha256:afb16330ab6efbbf995375ad94e970fa2f89bb46bd10d854b7047620fdb0d67d", - "sha256:b1b06038c9940a49c73db0aeb0f6809b308e198da1326171768cf68d843af521", - "sha256:b1e6d1cf4bd6552b5f519432cce1530c09e6b0aab98d44803b991f7e880bd332", - "sha256:bf2d9d62178bb5c05e77d40becf89c309b1966fbcfb5c306238f81bf1ec2d6a2", - "sha256:bfd073fea04061019a103a288847846b5ef40dfa2f73b940ed61e399ca95314f", - "sha256:c04e84ccf590933a266180286d8b6a5fc844078a5d934432628301bd8b5f9ca7", - "sha256:c0947d7be30335cb4c3d5d0983d8ebc8294ae52503cf1d596c926f7e7183900b", - "sha256:c2a17752f97a942bdb4ff4a0516a67c5ade1658ebe1ab2edacdec0b42e39fa75", - "sha256:c4653830375ab019b86d218c749ad38908b74182b2863d09936aa8d7f990d30e", - "sha256:c660fd1e4a4b52f79f7d134a3d31d452948477b7f46ff5061074a534c5805ba6", - "sha256:cb48ff6cc6109190e1ccf8ea1fc71cc244c9185813ce7d1c415dce991cfb8709", - "sha256:cef2675004d85d85a4ccc24730b73a99931547368d18ceeed1259a2d9fcddbc1", - "sha256:d1b98539b0de822b6f717498e59ae3e5ae2e7f564370ab513e6d0c060753e447", - "sha256:d6c6989c10008ac70c2bb2ad2b940fcfe883712746c89f7e3308c14c213a70d7", - "sha256:db3efec9dcecd96555d752215797816da40315d61878f90ca39c8e269791bf17", - "sha256:dc4749c230a71b34db50ac2481d9008bb17b67c92671c443c3b40e192fbea78e", - "sha256:dcf906c1f7a33e4222e4bff18da1554d69323bc4dd95fe867a6fa80709ee5f93", - "sha256:e2bccadbe313b11704160aaba5eec95d2da1aa663f02f41d2d1520d02bbbdcd5", - "sha256:e30cce3cc86d6082c8596b3fbee0d4f54bc4d337a4fa1bf536920e2e319e24f0", - "sha256:e5d6428b8b422ba5205140e8be11722fa7292a0bedaa8bc80fb34c92eb19ba45", - "sha256:e841695b5dbea38909ab2dbf17e91e9a823412d8d88d1ef77f1b94a7bc551c0f", - "sha256:eb65ec0255a0fccc47c87d44e505ef5180bfd71690bd5f84161b1f23949fb209", - "sha256:ed20ec5a01c43254f6047c5d8124b70d28e39f128c8ad960b437644fe94e1827", - "sha256:ed751a20840a31242e7bea566fcf93ba75bc11b33afe2777bbf46069c1af5094", - "sha256:ef8b927813c27c3bdfc82c55682d7767403bcdadfd9f9c0fc49f4be4553a877b", - "sha256:f43cacda46fc188f998e6d308afe1c61ff41dcb300949f4cbf731e9a0a5eb2d3", - "sha256:f44bea60fd2178d7153deef9621c4b526a93939da30010bba24d3408a98b0f79", - "sha256:fcc021530b7c71069132fe4846d95a3cdd74d143adc2f7e398d5fabf610f111c", - "sha256:fe16517b275031d61261a4e3941c411fb7c46a9cd012f02381b56e7907cc9e06", - "sha256:fe3ae4294d593da54862f0140fdcc89d1aeeb94258ca97f094119ed7f0e5882d" + "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", + "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", + "sha256:0a02313e71b7c370c43056f6b16c45effbb2d29a44d24403a3d5ba6ed322fa3f", + "sha256:0a89cadc0062a5e53664dde043f6c097172b8c1c5f0094490095282ff9995a5f", + "sha256:0be605bfb8461384a4cb81e80f51eb5ca1b89851f2d0e69a75458c788a7263a4", + "sha256:0d52a70350ec3dfc39b513df12b03b7f4c8f8ec6873bbf958299999db7b05eb1", + "sha256:0e7a5d0b9077e8c3e57727f797ee8adf12e1d5e7534642230d98980d160d1320", + "sha256:145d78c345a38011497e55aff22c0f8edd40ee676a6810f7e69563d68a125e83", + "sha256:14dee106a10b77224bba5efeeb6aee025aabe88eb87a2b850c46d3ee55bdab4a", + "sha256:176fdca18391e1206c32fb1d8265628a84d28333c20ad19468d91e3e98312cd1", + "sha256:1b4c535f524c9d8c86c3afd71d199025daa070859a2bdaf94a298120b0de16db", + "sha256:1b5cb75d2642ff7db823f509641f143f752c0d1ab03166cafea1e42e50469834", + "sha256:1c6c71e198b36f0f0dfe354f06d3655ecfa30d69493a1da125a9a54668aad652", + "sha256:1c771f1a8b3cd2d697baaf57e9cfa4ae42371cacfbea42ea01d9577c06d92f96", + "sha256:208a61db8b8b647fb5b1ff3b52b4ed6dbced01eac3b61009958adb203596ee99", + "sha256:2157d68f85c28688e8b723bbe70c8013e0aba5570e08c48b3562f74d33fc05c4", + "sha256:2301051701b27aff2cbdf83fae22b7ca883c9563dfd088033267291b46196643", + "sha256:2567885ff0c8c7c0887ba6cefe4ae4af96364a66a7069f924ce0cd12eb971d04", + "sha256:2577b8161eeae4dd376d13100b2137d883c10bb457dd08935f60c9f9d4b5c5f6", + "sha256:27e5ea64332385385b75414888ce9d1a9806be8616d7cef4ef409f4f256c6d06", + "sha256:28bfd5244d32faf3e49b5a8d1fab0631e922c26e8add089312e4be19fb05af50", + "sha256:295a5beaecb7bf054c1c6a28749ed72b19f4d4b61edcd8a0815d892424baf780", + "sha256:2c46a0afef69d61938a6fe32c3afd75b91dec3ab3056085dc72abbeedcc94166", + "sha256:3100a2352bdded6232b385ceda0c0a4624598c517d52c2d8cf014b7abbebd84d", + "sha256:320a1fe403dd83a35709fcf01083d14bc1462e9789b711201349a9158db3a87e", + "sha256:320f8734553c50cffe8a8e1ae36dfc7d7be1941c047489db20a814d2a170d7b5", + "sha256:33ab8c031f788609924e329003088831045f683931932a52a361d4a955b7dce2", + "sha256:3492ae1f97209c66af70e863e6420e6301cecb0a51a5efa701058aa73a8ca29e", + "sha256:351a2efe1c9566c348ad0076f4bf541f4905a0ebe2d271f112f60852575f3c16", + "sha256:3f0ac6e0203bd88863649e6ed9c7cfe53afab304bc8225f2597c4c0a74e4d1f0", + "sha256:3fedad05147b40ff8a93fcd016c421e6c159f149a2a481cfa0b94bfa3e473bab", + "sha256:4294f2c1cd069b793e31c2e6d7ac44b121cf7cedccd03ebcc30f3fc3417b314a", + "sha256:463b974b7f49d65a16ca1435bc1c25a681bb7d630509dd23b2e819ed36da0b7f", + "sha256:4e0a3ea7fd01cf0a36509f320226bd8491e0f448f00b8cb89f601c109f6874e1", + "sha256:514e78d20d8382d5b97f32b20c83d1d0452c302c9a135f0a9022236eb9940fda", + "sha256:517b09b1dd842390a965a896d1327c55dfe78199c9f5840595d40facbcd81854", + "sha256:51d1d061df3995c2332ae78f036492cc188cb3da8ef122caeab3631a67bb477e", + "sha256:5296669bff390135528001b4e48d33a7acaffcd361d98659628ece7f282f11aa", + "sha256:5296e5e69243ffd76bd919854c4da6630ae52e46175c804bc4c0e050d937b705", + "sha256:58db209da08a502ce6948841d522dcec80921d714024354153d00b054571993c", + "sha256:5b779e87300635b8075e8d5cfd4fdf7f46078cd7610c381d956bca5556bb8f97", + "sha256:5cf113a46d81cff0559d57aa66ffa473d57d1a9496f97426318b6b5b14fdec1c", + "sha256:5d20072d81cbfdd8e15e6a0c91fc7e3a4948c71e0adebfc67d3b4bcbe8602711", + "sha256:5d67dbc8da2dac1644d71c1839d12d12aa333e266a9964d5b1a49feed036bc94", + "sha256:5f530f35e1a57d4360eddcbed6945aecdaee2a491cd3f17025e7b5f2eea88ee7", + "sha256:5fdffb0cfeb4dc8646a5381d32ec981ae8472f29c695bf09e8f7a8edb2db12ca", + "sha256:602284e652bb56ca8760f8e88a5280636c5b63d7946fca1c2fe0f83c37dffc64", + "sha256:648fcfd8e019b122b7be0e26830a3a2224d57c3e934f19c1e53a77b8380e6675", + "sha256:64b9122be1c404ce4eb367ad609b590394587a676d84bfed8e03c3ce76d70560", + "sha256:6526933760ee1e6090db808f1690a111ec409699c1990efc96f134d26925c37f", + "sha256:6632b1c63d58cddc72f43ab9f17267354ddce563dd5e11eadabd222dcc808808", + "sha256:6f93dbfa5a461107bc3f5026e0d5180499e13379e9404f07a9f79eb5e9e1303d", + "sha256:71c0db2c313ea8a80825fb61b7826b8015874aec29ee6364ade5cb774fe4511b", + "sha256:71c5c200fd37a5322706080b09c3ec8907cf01c377a7187f354fc9e9e13abc73", + "sha256:7738147cd9dbd6d18d5593b3491b4620e13b61de975fd737283e4ad6c255c273", + "sha256:7a6e4dccae8ef5dd76052647d78f02d5d0ffaff1856277d951666c54aeba3ad2", + "sha256:7b4a9fcd95e978cd3c96cdc2096aa54705266551422cf0883c12a4044def31c6", + "sha256:80710d7591d579442c67a3bc7ae9dcba9ff95ea8414ac98001198d894fc4ff46", + "sha256:81a3ebc33b1367f301d1c8eda57eec4868e951504986d5d3fe437479dcdac5b2", + "sha256:8455176fd1b86de97d859fed4ae0ef867bf998581f584c7a1a591246dfec330f", + "sha256:845b178bd127bb074835d2eac635b980c58ec5e700ebadc8355062df708d5a71", + "sha256:87e18f29bac4a6be76a30e74de9c9005475e27100acf0830679420ce1fd9a6fd", + "sha256:89d7baa847383b9814de640c6f1a8553d125ec65e2761ad146ea2e75a7ad197c", + "sha256:8c7ad5cab282f53b9d78d51504330d1c88c83fbe187e472c07e6908a0293142e", + "sha256:8d92c6bb9174d47c2257528f64645a00bbc6324a9ff45a626192797aff01dc14", + "sha256:9252c991e8176b5a2fa574c5ab9a841679e315f6e576eb7cf0bd958f3e39b0ad", + "sha256:93111fd4e08fa889c126aa8baf5c009a941880a539c87672e04583286517450a", + "sha256:95d15cf81cd2fb926f2a6151a9f94c7aacc102b415e72bc0e040e29332b6731c", + "sha256:9d5b66d457d2c5739c184a777455c8fde7ab3600a56d8bbebecf64f7c55169e1", + "sha256:a055d29f1302892a9389a382bed10a3f77708bcf3e49bfb76f7712fa5f391cc6", + "sha256:a1ba93be779a9b8e5e44f5c133dc1db4313661cead8a2fd27661e6cb8d942ee9", + "sha256:a283425e6a474facd73072d8968812d1d9058490a5781e022ccf8895500b83ce", + "sha256:a351986d6c9006308f163c359ced40f80b6cffb42069f3e569b979829951038d", + "sha256:a766157b195a897c64945d4ff87b050bb0e763bb78f3964e996378621c703b00", + "sha256:a8a3540e21213cb8ce232e68a7d0ee49cdd35194856c50b8bd87eeb572fadd42", + "sha256:a8e0a086dbbee406cc6f603931dfe54d1cb2fba585758e06a2de01037784b737", + "sha256:ab23b0545ec71ea346bf50a5d376d674f56205b729980eaa62cdb7871805014b", + "sha256:b0db9a4691074c347f5d7ee830ab3529bc5ad860939de21c1f9c403daf1eda9a", + "sha256:b1b5be40ebf52c3c67ee547e2c4435ed5bc6352f38d23e394520b686641a6be4", + "sha256:b3e08aef4ea05afbc0a70cd23c13684e7f5e074f02450964ec5cfa1c759d33d2", + "sha256:b7df0d99e189b7027d417d4bfd9b8c53c9c7ed5a0a1495d26a6f547d820eca88", + "sha256:be1f10145f7ea76e3e836fdc5c8429c605675bdcddb0bca9725ee6e26874c00c", + "sha256:bf254a1a95e95fdf4eaa25faa1ea450a6533ed7a997f9f8e49ab971b61ea514d", + "sha256:bfc2d763d05ec7211313a06e8571236017d3e61d5fef97fcf34ec4b36c0b6556", + "sha256:c164eda0be9048f83c24b9b2656900041e069ddf72de81c17d874d0c32f6079f", + "sha256:c22591cff80188dd8543be0b559d0c807f7288bd353dc0bcfe539b4588b3a5cd", + "sha256:c5f83bb59d0ff60c6fdb1f8a7b0288fbc4640b1f0fd56f5ae2387749c35d34e3", + "sha256:c7e8221278e5f9e2b6d3893cfc3a3e46c017161a57bb0e6f244826e4cee97916", + "sha256:c8d6bf6fcd42cde2f02efb8126812a010c297eacefcd090a609639d2aeda6185", + "sha256:c8f7dd025cb0bf19e2f60a64dfc24b513c8330e0cfe4a34ccf941eafd6194d9e", + "sha256:c9d212e2af72d5c8d082775a43eb726520e95bf1c84826440f74225843975136", + "sha256:cebb3d8bcac4a6b48be65ebbc5c9881ed4a738e27bb96c86d9d7580a1fb09e05", + "sha256:d3082e5c4d7b388792124f5e805b469109e58f1ab1eb1fbd8b998e8ab766ffb7", + "sha256:d81047341ab56061aa4b6823c54d4632579c3b16e675089e8f520e9b918a133b", + "sha256:d81299f63dc33cc172c26faf59cc54dd795fc6dd5821a7676cca112a5ee8bbd6", + "sha256:dfa217bf8cf3ff6b30c8e6a89014e0c0e7b50941af787b970060ae5ba04a4ce5", + "sha256:dfec57f15f53d677b8e4535695ff3f37df7f8fe431f2efa8c3c8c4025b53d1eb", + "sha256:e099b79ccf7c40f18b149a64d3d10639980035f9ceb223169dd806ff1bb0d9cc", + "sha256:e1fc4d3985868860b6585376e511bb32403c5ffb58b0ed913496c27fd791deea", + "sha256:e2b4c95c47fb81b19ea77dc1c50d23af3eba87c9628fcc2e03d44124a3d336ea", + "sha256:e4e5d163e6644c2bc84dd9f67bfa89288c23af26983d08fefcc2cbc22f6e57e6", + "sha256:e66b3c9f8b89d4fd58a59c04fdbf10602a17c914fbaaa5e6ea593f1d54b06362", + "sha256:ed7d11330e443aeecab23866055e08a5a536c95d2c25333aeb441af2dbac38d2", + "sha256:f340a2a908644ea6cccd399be0fb308c66e05d2800107345f9f0f0d59e1731c4", + "sha256:f38b35ecd2628bf0267761ed659e48af7e620a7fcccfccf5774e7308fb18325c", + "sha256:f6d5443104f89a840250087863c91484a72f254574848e951d1bdd7d8b2ce7c9", + "sha256:fc2048d13ff427605fea328cbe5369dce549b8c7657b0e22051a5b8831170af6" ], "index": "pypi", - "version": "==3.12.1" + "version": "==3.12.3" }, "python-dateutil": { "hashes": [ @@ -524,19 +505,19 @@ }, "tinycss2": { "hashes": [ - "sha256:0353b5234bcaee7b1ac7ca3dea7e02cd338a9f8dcbb8f2dcd32a5795ec1e5f9a", - "sha256:fbdcac3044d60eb85fdb2aa840ece43cf7dbe798e373e6ee0be545d4d134e18a" + "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf", + "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8" ], "markers": "python_version >= '3.6'", - "version": "==1.1.0" + "version": "==1.1.1" }, "typing-extensions": { "hashes": [ - "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed", - "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9" + "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", + "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" ], "markers": "python_version >= '3.6'", - "version": "==4.0.0" + "version": "==4.1.1" }, "uvloop": { "hashes": [ @@ -664,11 +645,11 @@ }, "bandit": { "hashes": [ - "sha256:a81b00b5436e6880fa8ad6799bc830e02032047713cbb143a12939ac67eb756c", - "sha256:f5acd838e59c038a159b5c621cf0f8270b279e884eadd7b782d7491c02add0d4" + "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2", + "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a" ], "index": "pypi", - "version": "==1.7.1" + "version": "==1.7.4" }, "black": { "hashes": [ @@ -680,11 +661,11 @@ }, "click": { "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", + "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" ], "markers": "python_version >= '3.6'", - "version": "==8.0.3" + "version": "==8.0.4" }, "colorama": { "hashes": [ @@ -704,11 +685,11 @@ }, "gitpython": { "hashes": [ - "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647", - "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5" + "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704", + "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d" ], "markers": "python_version >= '3.7'", - "version": "==3.1.24" + "version": "==3.1.27" }, "isort": { "hashes": [ @@ -720,31 +701,46 @@ }, "lazy-object-proxy": { "hashes": [ - "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", - "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", - "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", - "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", - "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", - "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", - "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", - "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", - "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", - "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", - "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", - "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", - "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", - "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", - "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", - "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", - "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", - "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", - "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", - "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", - "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", - "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.6.0" + "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7", + "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a", + "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c", + "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc", + "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f", + "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09", + "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442", + "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e", + "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029", + "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61", + "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb", + "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0", + "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35", + "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42", + "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1", + "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad", + "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443", + "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd", + "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9", + "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148", + "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38", + "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55", + "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36", + "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a", + "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b", + "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44", + "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6", + "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69", + "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4", + "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84", + "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de", + "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28", + "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c", + "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1", + "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8", + "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b", + "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb" + ], + "markers": "python_version >= '3.6'", + "version": "==1.7.1" }, "mccabe": { "hashes": [ @@ -769,11 +765,11 @@ }, "pbr": { "hashes": [ - "sha256:176e8560eaf61e127817ef93d8a844803abb27a4d4637f0ff3bb783129be2e0a", - "sha256:672d8ebee84921862110f23fcec2acea191ef58543d34dfe9ef3d9f13c31cddf" + "sha256:27108648368782d07bbf1cb468ad2e2eeef29086affd14087a6d04b7de8af4ec", + "sha256:66bc5a34912f408bb3925bf21231cb6f59206267b7f63f3503ef865c1a292e25" ], "markers": "python_version >= '2.6'", - "version": "==5.8.0" + "version": "==5.8.1" }, "pylint": { "hashes": [ @@ -824,57 +820,91 @@ }, "regex": { "hashes": [ - "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f", - "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc", - "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4", - "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4", - "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8", - "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f", - "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a", - "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef", - "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f", - "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc", - "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50", - "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d", - "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d", - "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733", - "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36", - "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345", - "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0", - "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12", - "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646", - "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667", - "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244", - "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29", - "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec", - "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf", - "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4", - "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449", - "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a", - "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d", - "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb", - "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e", - "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83", - "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e", - "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a", - "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94", - "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc", - "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e", - "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965", - "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0", - "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36", - "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec", - "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23", - "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7", - "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe", - "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6", - "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b", - "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb", - "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b", - "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30", - "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e" - ], - "version": "==2021.11.10" + "sha256:0066a6631c92774391f2ea0f90268f0d82fffe39cb946f0f9c6b382a1c61a5e5", + "sha256:0100f0ded953b6b17f18207907159ba9be3159649ad2d9b15535a74de70359d3", + "sha256:01c913cf573d1da0b34c9001a94977273b5ee2fe4cb222a5d5b320f3a9d1a835", + "sha256:0214ff6dff1b5a4b4740cfe6e47f2c4c92ba2938fca7abbea1359036305c132f", + "sha256:029e9e7e0d4d7c3446aa92474cbb07dafb0b2ef1d5ca8365f059998c010600e6", + "sha256:0317eb6331146c524751354ebef76a7a531853d7207a4d760dfb5f553137a2a4", + "sha256:04b5ee2b6d29b4a99d38a6469aa1db65bb79d283186e8460542c517da195a8f6", + "sha256:04c09b9651fa814eeeb38e029dc1ae83149203e4eeb94e52bb868fadf64852bc", + "sha256:058054c7a54428d5c3e3739ac1e363dc9347d15e64833817797dc4f01fb94bb8", + "sha256:060f9066d2177905203516c62c8ea0066c16c7342971d54204d4e51b13dfbe2e", + "sha256:0a7b75cc7bb4cc0334380053e4671c560e31272c9d2d5a6c4b8e9ae2c9bd0f82", + "sha256:0e2630ae470d6a9f8e4967388c1eda4762706f5750ecf387785e0df63a4cc5af", + "sha256:174d964bc683b1e8b0970e1325f75e6242786a92a22cedb2a6ec3e4ae25358bd", + "sha256:25ecb1dffc5e409ca42f01a2b2437f93024ff1612c1e7983bad9ee191a5e8828", + "sha256:286908cbe86b1a0240a867aecfe26a439b16a1f585d2de133540549831f8e774", + "sha256:303b15a3d32bf5fe5a73288c316bac5807587f193ceee4eb6d96ee38663789fa", + "sha256:34bb30c095342797608727baf5c8aa122406aa5edfa12107b8e08eb432d4c5d7", + "sha256:3e265b388cc80c7c9c01bb4f26c9e536c40b2c05b7231fbb347381a2e1c8bf43", + "sha256:3e4d710ff6539026e49f15a3797c6b1053573c2b65210373ef0eec24480b900b", + "sha256:42eb13b93765c6698a5ab3bcd318d8c39bb42e5fa8a7fcf7d8d98923f3babdb1", + "sha256:48081b6bff550fe10bcc20c01cf6c83dbca2ccf74eeacbfac240264775fd7ecf", + "sha256:491fc754428514750ab21c2d294486223ce7385446f2c2f5df87ddbed32979ae", + "sha256:4d1445824944e642ffa54c4f512da17a953699c563a356d8b8cbdad26d3b7598", + "sha256:530a3a16e57bd3ea0dff5ec2695c09632c9d6c549f5869d6cf639f5f7153fb9c", + "sha256:591d4fba554f24bfa0421ba040cd199210a24301f923ed4b628e1e15a1001ff4", + "sha256:5a86cac984da35377ca9ac5e2e0589bd11b3aebb61801204bd99c41fac516f0d", + "sha256:5b1ceede92400b3acfebc1425937454aaf2c62cd5261a3fabd560c61e74f6da3", + "sha256:5b2e24f3ae03af3d8e8e6d824c891fea0ca9035c5d06ac194a2700373861a15c", + "sha256:6504c22c173bb74075d7479852356bb7ca80e28c8e548d4d630a104f231e04fb", + "sha256:673f5a393d603c34477dbad70db30025ccd23996a2d0916e942aac91cc42b31a", + "sha256:6ca6dcd17f537e9f3793cdde20ac6076af51b2bd8ad5fe69fa54373b17b48d3c", + "sha256:6e1d8ed9e61f37881c8db383a124829a6e8114a69bd3377a25aecaeb9b3538f8", + "sha256:75a5e6ce18982f0713c4bac0704bf3f65eed9b277edd3fb9d2b0ff1815943327", + "sha256:76435a92e444e5b8f346aed76801db1c1e5176c4c7e17daba074fbb46cb8d783", + "sha256:764e66a0e382829f6ad3bbce0987153080a511c19eb3d2f8ead3f766d14433ac", + "sha256:78ce90c50d0ec970bd0002462430e00d1ecfd1255218d52d08b3a143fe4bde18", + "sha256:794a6bc66c43db8ed06698fc32aaeaac5c4812d9f825e9589e56f311da7becd9", + "sha256:797437e6024dc1589163675ae82f303103063a0a580c6fd8d0b9a0a6708da29e", + "sha256:7b7494df3fdcc95a1f76cf134d00b54962dd83189520fd35b8fcd474c0aa616d", + "sha256:7d1a6e403ac8f1d91d8f51c441c3f99367488ed822bda2b40836690d5d0059f5", + "sha256:7f63877c87552992894ea1444378b9c3a1d80819880ae226bb30b04789c0828c", + "sha256:8923e1c5231549fee78ff9b2914fad25f2e3517572bb34bfaa3aea682a758683", + "sha256:8afcd1c2297bc989dceaa0379ba15a6df16da69493635e53431d2d0c30356086", + "sha256:8b1cc70e31aacc152a12b39245974c8fccf313187eead559ee5966d50e1b5817", + "sha256:8d1f3ea0d1924feb4cf6afb2699259f658a08ac6f8f3a4a806661c2dfcd66db1", + "sha256:940570c1a305bac10e8b2bc934b85a7709c649317dd16520471e85660275083a", + "sha256:947a8525c0a95ba8dc873191f9017d1b1e3024d4dc757f694e0af3026e34044a", + "sha256:9beb03ff6fe509d6455971c2489dceb31687b38781206bcec8e68bdfcf5f1db2", + "sha256:9c144405220c5ad3f5deab4c77f3e80d52e83804a6b48b6bed3d81a9a0238e4c", + "sha256:a98ae493e4e80b3ded6503ff087a8492db058e9c68de371ac3df78e88360b374", + "sha256:aa2ce79f3889720b46e0aaba338148a1069aea55fda2c29e0626b4db20d9fcb7", + "sha256:aa5eedfc2461c16a092a2fabc5895f159915f25731740c9152a1b00f4bcf629a", + "sha256:ab5d89cfaf71807da93c131bb7a19c3e19eaefd613d14f3bce4e97de830b15df", + "sha256:b4829db3737480a9d5bfb1c0320c4ee13736f555f53a056aacc874f140e98f64", + "sha256:b52771f05cff7517f7067fef19ffe545b1f05959e440d42247a17cd9bddae11b", + "sha256:b8248f19a878c72d8c0a785a2cd45d69432e443c9f10ab924c29adda77b324ae", + "sha256:b9809404528a999cf02a400ee5677c81959bc5cb938fdc696b62eb40214e3632", + "sha256:c155a1a80c5e7a8fa1d9bb1bf3c8a953532b53ab1196092749bafb9d3a7cbb60", + "sha256:c33ce0c665dd325200209340a88438ba7a470bd5f09f7424e520e1a3ff835b52", + "sha256:c5adc854764732dbd95a713f2e6c3e914e17f2ccdc331b9ecb777484c31f73b6", + "sha256:cb374a2a4dba7c4be0b19dc7b1adc50e6c2c26c3369ac629f50f3c198f3743a4", + "sha256:cd00859291658fe1fda48a99559fb34da891c50385b0bfb35b808f98956ef1e7", + "sha256:ce3057777a14a9a1399b81eca6a6bfc9612047811234398b84c54aeff6d536ea", + "sha256:d0a5a1fdc9f148a8827d55b05425801acebeeefc9e86065c7ac8b8cc740a91ff", + "sha256:dad3991f0678facca1a0831ec1ddece2eb4d1dd0f5150acb9440f73a3b863907", + "sha256:dc7b7c16a519d924c50876fb152af661a20749dcbf653c8759e715c1a7a95b18", + "sha256:dcbb7665a9db9f8d7642171152c45da60e16c4f706191d66a1dc47ec9f820aed", + "sha256:df037c01d68d1958dad3463e2881d3638a0d6693483f58ad41001aa53a83fcea", + "sha256:f08a7e4d62ea2a45557f561eea87c907222575ca2134180b6974f8ac81e24f06", + "sha256:f16cf7e4e1bf88fecf7f41da4061f181a6170e179d956420f84e700fb8a3fd6b", + "sha256:f2c53f3af011393ab5ed9ab640fa0876757498aac188f782a0c620e33faa2a3d", + "sha256:f320c070dea3f20c11213e56dbbd7294c05743417cde01392148964b7bc2d31a", + "sha256:f553a1190ae6cd26e553a79f6b6cfba7b8f304da2071052fa33469da075ea625", + "sha256:fc8c7958d14e8270171b3d72792b609c057ec0fa17d507729835b5cff6b7f69a" + ], + "markers": "python_version >= '3.6'", + "version": "==2022.3.15" + }, + "setuptools": { + "hashes": [ + "sha256:6599055eeb23bfef457d5605d33a4d68804266e6cb430b0fb12417c5efeae36c", + "sha256:782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96" + ], + "markers": "python_version >= '3.7'", + "version": "==60.10.0" }, "smmap": { "hashes": [ @@ -900,14 +930,6 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, - "typing-extensions": { - "hashes": [ - "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed", - "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.0" - }, "wrapt": { "hashes": [ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" diff --git a/bot.py b/bot.py index 3e3a6ec5cf..c691cea068 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev6" +__version__ = "4.0.0-dev7" import asyncio @@ -67,7 +67,7 @@ class ModmailBot(commands.Bot): def __init__(self): intents = discord.Intents.all() super().__init__(command_prefix=None, intents=intents) # implemented in `get_prefix` - self._session = None + self.session = None self._api = None self.formatter = SafeFormatter() self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] @@ -131,10 +131,11 @@ def startup(self): logger.info("discord.py: v%s", discord.__version__) logger.line() + async def load_extensions(self): for cog in self.loaded_cogs: logger.debug("Loading %s.", cog) try: - self.load_extension(cog) + await self.load_extension(cog) logger.debug("Successfully loaded %s.", cog) except Exception: logger.exception("Failed to load %s.", cog) @@ -167,12 +168,6 @@ def _configure_logging(self): def version(self): return parse_version(__version__) - @property - def session(self) -> ClientSession: - if self._session is None: - self._session = ClientSession(loop=self.loop) - return self._session - @property def api(self) -> ApiClient: if self._api is None: @@ -192,102 +187,85 @@ async def get_prefix(self, message=None): return [self.prefix, f"<@{self.user.id}> ", f"<@!{self.user.id}> "] def run(self): - loop = self.loop - - try: - loop.add_signal_handler(signal.SIGINT, lambda: loop.stop()) - loop.add_signal_handler(signal.SIGTERM, lambda: loop.stop()) - except NotImplementedError: - pass - async def runner(): - try: - retry_intents = False + async with self: try: - await self.start(self.token) + retry_intents = False + try: + await self.start(self.token) + except discord.PrivilegedIntentsRequired: + retry_intents = True + if retry_intents: + await self.http.close() + if self.ws is not None and self.ws.open: + await self.ws.close(code=1000) + self._ready.clear() + + intents = discord.Intents.default() + intents.members = True + intents.message_content = True + # Try again with members intent + self._connection._intents = intents + logger.warning( + "Attempting to login with only the server members and message content privileged intent. Some plugins might not work correctly." + ) + await self.start(self.token) except discord.PrivilegedIntentsRequired: - retry_intents = True - if retry_intents: - await self.http.close() - if self.ws is not None and self.ws.open: - await self.ws.close(code=1000) - self._ready.clear() - intents = discord.Intents.default() - intents.members = True - # Try again with members intent - self._connection._intents = intents - logger.warning( - "Attempting to login with only the server members privileged intent. Some plugins might not work correctly." + logger.critical( + "Privileged intents are not explicitly granted in the discord developers dashboard." ) - await self.start(self.token) - except discord.PrivilegedIntentsRequired: - logger.critical( - "Privileged intents are not explicitly granted in the discord developers dashboard." - ) - except discord.LoginFailure: - logger.critical("Invalid token") - except Exception: - logger.critical("Fatal exception", exc_info=True) - finally: - if not self.is_closed(): - await self.close() - if self._session: - await self._session.close() - - # noinspection PyUnusedLocal - def stop_loop_on_completion(f): - loop.stop() - - def _cancel_tasks(): - task_retriever = asyncio.all_tasks - tasks = {t for t in task_retriever(loop=loop) if not t.done()} - - if not tasks: - return + except discord.LoginFailure: + logger.critical("Invalid token") + except Exception: + logger.critical("Fatal exception", exc_info=True) + finally: + if not self.is_closed(): + await self.close() + if self.session: + await self.session.close() + + async def _cancel_tasks(): + async with self: + task_retriever = asyncio.all_tasks + loop = self.loop + tasks = {t for t in task_retriever() if not t.done() and t.get_coro() != cancel_tasks_coro} + + if not tasks: + return - logger.info("Cleaning up after %d tasks.", len(tasks)) - for task in tasks: - task.cancel() - - loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) - logger.info("All tasks finished cancelling.") - - for task in tasks: - if task.cancelled(): - continue - if task.exception() is not None: - loop.call_exception_handler( - { - "message": "Unhandled exception during Client.run shutdown.", - "exception": task.exception(), - "task": task, - } - ) + logger.info("Cleaning up after %d tasks.", len(tasks)) + for task in tasks: + task.cancel() + + await asyncio.gather(*tasks, return_exceptions=True) + logger.info("All tasks finished cancelling.") + + for task in tasks: + try: + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "Unhandled exception during Client.run shutdown.", + "exception": task.exception(), + "task": task, + } + ) + except (asyncio.InvalidStateError, asyncio.CancelledError): + pass - future = asyncio.ensure_future(runner(), loop=loop) - future.add_done_callback(stop_loop_on_completion) try: - loop.run_forever() - except KeyboardInterrupt: + asyncio.run(runner()) + except (KeyboardInterrupt, SystemExit): logger.info("Received signal to terminate bot and event loop.") finally: - future.remove_done_callback(stop_loop_on_completion) logger.info("Cleaning up tasks.") try: - _cancel_tasks() - if sys.version_info >= (3, 6): - loop.run_until_complete(loop.shutdown_asyncgens()) + cancel_tasks_coro = _cancel_tasks() + asyncio.run(cancel_tasks_coro) finally: logger.info("Closing the event loop.") - if not future.cancelled(): - try: - return future.result() - except KeyboardInterrupt: - # I am unsure why this gets raised here but suppress it anyway - return None - @property def bot_owner_ids(self): owner_ids = self.config["owners"] @@ -517,6 +495,8 @@ async def on_connect(self): logger.debug("Connected to gateway.") await self.config.refresh() await self.api.setup_indexes() + await self.load_extensions() + self.session = ClientSession(loop=self.loop) self._connected.set() async def on_ready(self): @@ -1421,7 +1401,7 @@ async def on_message_delete(self, message): if embed.footer.icon: icon_url = embed.footer.icon.url else: - icon_url = discord.Embed.Empty + icon_url = None embed.set_footer(text=f"{embed.footer.text} (deleted)", icon_url=icon_url) await message.edit(embed=embed) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7900f0eeef..8ef853963c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -2091,5 +2091,5 @@ async def isenable(self, ctx): return await ctx.send(embed=embed) -def setup(bot): - bot.add_cog(Modmail(bot)) +async def setup(bot): + await bot.add_cog(Modmail(bot)) diff --git a/cogs/plugins.py b/cogs/plugins.py index e9adf2a240..37e71a872b 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -123,10 +123,10 @@ def __init__(self, bot): self.loaded_plugins = set() self._ready_event = asyncio.Event() - self.bot.loop.create_task(self.populate_registry()) - + async def async_init(self): + await self.populate_registry() if self.bot.config.get("enable_plugins"): - self.bot.loop.create_task(self.initial_load_plugins()) + await self.initial_load_plugins() else: logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") @@ -258,7 +258,7 @@ async def load_plugin(self, plugin): sys.path.insert(0, USER_SITE) try: - self.bot.load_extension(plugin.ext_string) + await self.bot.load_extension(plugin.ext_string) logger.info("Loaded plugin: %s", plugin.ext_string.split(".")[-1]) self.loaded_plugins.add(plugin) @@ -432,7 +432,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): if self.bot.config.get("enable_plugins"): try: - self.bot.unload_extension(plugin.ext_string) + await self.bot.unload_extension(plugin.ext_string) self.loaded_plugins.remove(plugin) except (commands.ExtensionNotLoaded, KeyError): logger.warning("Plugin was never loaded.") @@ -474,7 +474,7 @@ async def update_plugin(self, ctx, plugin_name): await self.download_plugin(plugin, force=True) if self.bot.config.get("enable_plugins"): try: - self.bot.unload_extension(plugin.ext_string) + await self.bot.unload_extension(plugin.ext_string) except commands.ExtensionError: logger.warning("Plugin unload fail.", exc_info=True) try: @@ -525,7 +525,7 @@ async def plugins_reset(self, ctx): continue try: logger.error("Unloading plugin: %s.", ext) - self.bot.unload_extension(ext) + await self.bot.unload_extension(ext) except Exception: logger.error("Failed to unload plugin: %s.", ext) else: @@ -737,5 +737,5 @@ async def plugins_registry_compact(self, ctx): await paginator.run() -def setup(bot): - bot.add_cog(Plugins(bot)) +async def setup(bot): + await bot.add_cog(Plugins(bot)) diff --git a/cogs/utility.py b/cogs/utility.py index 7278c350fc..aa8adaaf2d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -256,11 +256,13 @@ def __init__(self, bot): }, ) self.bot.help_command.cog = self - self.loop_presence.start() # pylint: disable=no-member if not self.bot.config.get("enable_eval"): self.eval_.enabled = False logger.info("Eval disabled. enable_eval=False") + async def async_init(self): + self.loop_presence.start() # pylint: disable=no-member + def cog_unload(self): self.bot.help_command = self._original_help_command @@ -2099,5 +2101,5 @@ def paginate(text: str): await self.bot.add_reaction(ctx.message, "\u2705") -def setup(bot): - bot.add_cog(Utility(bot)) +async def setup(bot): + await bot.add_cog(Utility(bot)) diff --git a/core/paginator.py b/core/paginator.py index b9cd29a26b..39782817b5 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -308,7 +308,7 @@ def __init__(self, ctx: commands.Context, *embeds, **options): if embed.footer.icon: icon_url = embed.footer.icon.url else: - icon_url = Embed.Empty + icon_url = None embed.set_footer(text=footer_text, icon_url=icon_url) # select menu @@ -369,7 +369,7 @@ def _set_footer(self): if self.embed.footer.icon: icon_url = self.embed.footer.icon.url else: - icon_url = Embed.Empty + icon_url = None self.embed.set_footer(text=footer_text, icon_url=icon_url) diff --git a/core/thread.py b/core/thread.py index 87ffc45ee3..7edfbc1d39 100644 --- a/core/thread.py +++ b/core/thread.py @@ -75,7 +75,7 @@ def __eq__(self, other): async def wait_until_ready(self) -> None: """Blocks execution until the thread is fully set up.""" # timeout after 30 seconds - task = asyncio.create_task(asyncio.wait_for(self._ready_event.wait(), timeout=25)) + task = self.bot.loop.create_task(asyncio.wait_for(self._ready_event.wait(), timeout=25)) self.wait_tasks.append(task) try: await task @@ -367,7 +367,8 @@ def _format_info_embed(self, user, log_url, log_count, color): return embed - def _close_after(self, closer, silent, delete_channel, message): + async def _close_after(self, after, closer, silent, delete_channel, message): + await asyncio.sleep(after) return self.bot.loop.create_task(self._close(closer, silent, delete_channel, message, True)) async def close( @@ -401,7 +402,7 @@ async def close( self.bot.config["closures"][str(self.id)] = items await self.bot.config.update() - task = self.bot.loop.call_later(after, self._close_after, closer, silent, delete_channel, message) + task = asyncio.create_task(self._close_after(after, closer, silent, delete_channel, message)) if auto_close: self.auto_close_task = task @@ -1139,7 +1140,7 @@ def lottie_to_png(data): raise if not from_mod and not note: - mentions = self.get_notifications() + mentions = await self.get_notifications() else: mentions = None @@ -1176,7 +1177,7 @@ def lottie_to_png(data): return msg - def get_notifications(self) -> str: + async def get_notifications(self) -> str: key = str(self.id) mentions = [] diff --git a/pyproject.toml b/pyproject.toml index 031fc5073f..d552a33dee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev6' +version = '4.0.0-dev7' description = "Modmail is similar to Reddit's 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 d6a3982b823a6cd9cbcb979db5d8503d3edaad46 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Mon, 28 Mar 2022 13:46:32 -0700 Subject: [PATCH 1033/1221] Update SPONSORS.json --- SPONSORS.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/SPONSORS.json b/SPONSORS.json index a1ec35d6dd..2cd08053ec 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -105,5 +105,27 @@ }, "image": "https://cdn.discordapp.com/attachments/472811257913933834/907068966311166043/unknown_2.png" } + }, + { + "embed": { + "footer": { + "text": "Join noch heute!" + }, + "thumbnail": { + "url": "https://i.imgur.com/bp0xfyK.png" + }, + "fields": [ + { + "inline": false, + "name": "Viele Verschiedene Talks", + "value": "Gro\u00dfe Community\nGewinnspiele" + } + ], + "color": 61532, + "type": "rich", + "description": "Die etwas andere Community", + "url": "https://discord.gg/uncommon", + "title": "uncommon community" + } } ] From e0032fd9b9806db75f21167e2c50f36258fbbc99 Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Mon, 28 Mar 2022 13:47:43 -0700 Subject: [PATCH 1034/1221] Update SPONSORS.json --- SPONSORS.json | 1 - 1 file changed, 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 2cd08053ec..1177699d10 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -122,7 +122,6 @@ } ], "color": 61532, - "type": "rich", "description": "Die etwas andere Community", "url": "https://discord.gg/uncommon", "title": "uncommon community" From 57349c6eafa8c8af0f285bd907968669ac60439b Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Apr 2022 17:45:28 -0700 Subject: [PATCH 1035/1221] Update sponsors list --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 465b13f1ab..b95dc335a4 100644 --- a/README.md +++ b/README.md @@ -162,13 +162,6 @@ $ docker run --env-file .env kyb3rr/modmail Special thanks to our sponsors for supporting the project. -Kingdom Gaming Discord: -<br> -<a href='https://discord.gg/kingdomgaming'> - <img height=100 src='https://i.imgur.com/GLXfl4E.png' style='margin:5px'> -</a> -<br> -<br> SirReddit: <br> <a href='https://www.youtube.com/channel/UCgSmBJD9imASmJRleycTCwQ/featured'> From be14cca89742cd691f6b91f0d0931a074ad81e3b Mon Sep 17 00:00:00 2001 From: Taku 3 Animals <45324516+Taaku18@users.noreply.github.com> Date: Fri, 15 Apr 2022 17:50:10 -0700 Subject: [PATCH 1036/1221] Update sponsors syntax issue --- SPONSORS.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 1177699d10..ce34dc3f9d 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -103,7 +103,9 @@ "footer": { "text": "Grow Your Discord Server" }, - "image": "https://cdn.discordapp.com/attachments/472811257913933834/907068966311166043/unknown_2.png" + "image": { + "url": "https://cdn.discordapp.com/attachments/472811257913933834/907068966311166043/unknown_2.png" + } } }, { From d9c884e55c8d65175ebfe604e25098add7bdaf55 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 19 Apr 2022 23:30:31 +0800 Subject: [PATCH 1037/1221] dpy-ecb2cf & Fix plugins not loading, resolve #3141 --- Pipfile | 2 +- bot.py | 4 ++-- cogs/plugins.py | 4 +--- cogs/utility.py | 2 +- pyproject.toml | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Pipfile b/Pipfile index 04d998cbc8..1241feeb40 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ pylint = "~=2.9.3" [packages] aiohttp = "==3.7.4.post0" colorama = "~=0.4.4" # Doesn't officially support Python 3.9 yet, v0.4.5 will support 3.9 -"discord.py" = {ref = "97fe07edb2f3d0f5a980917fb6ee479bf396fd94", git = "https://github.com/Rapptz/discord.py.git"} +"discord.py" = {ref = "987235d5649e7c2b1a927637bab6547244ecb2cf", git = "https://github.com/Rapptz/discord.py.git"} emoji = "~=1.2.0" isodate = "~=0.6.0" motor = "~=2.4.0" diff --git a/bot.py b/bot.py index c691cea068..6c0719243b 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev7" +__version__ = "4.0.0-dev8" import asyncio @@ -189,6 +189,7 @@ async def get_prefix(self, message=None): def run(self): async def runner(): async with self: + self.session = ClientSession(loop=self.loop) try: retry_intents = False try: @@ -496,7 +497,6 @@ async def on_connect(self): await self.config.refresh() await self.api.setup_indexes() await self.load_extensions() - self.session = ClientSession(loop=self.loop) self._connected.set() async def on_ready(self): diff --git a/cogs/plugins.py b/cogs/plugins.py index 37e71a872b..2bfac509af 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -123,7 +123,7 @@ def __init__(self, bot): self.loaded_plugins = set() self._ready_event = asyncio.Event() - async def async_init(self): + async def cog_load(self): await self.populate_registry() if self.bot.config.get("enable_plugins"): await self.initial_load_plugins() @@ -136,8 +136,6 @@ async def populate_registry(self): self.registry = json.loads(await resp.text()) async def initial_load_plugins(self): - await self.bot.wait_for_connected() - for plugin_name in list(self.bot.config["plugins"]): try: plugin = Plugin.from_string(plugin_name, strict=True) diff --git a/cogs/utility.py b/cogs/utility.py index aa8adaaf2d..6d1ddd0f00 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -260,7 +260,7 @@ def __init__(self, bot): self.eval_.enabled = False logger.info("Eval disabled. enable_eval=False") - async def async_init(self): + async def cog_load(self): self.loop_presence.start() # pylint: disable=no-member def cog_unload(self): diff --git a/pyproject.toml b/pyproject.toml index d552a33dee..ad792dc399 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev7' +version = '4.0.0-dev8' description = "Modmail is similar to Reddit's 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 f03e20318f52f3682c9173c0cade49ded9a44c88 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 19 Apr 2022 23:43:45 +0800 Subject: [PATCH 1038/1221] fix blocked, resolve #3131 --- CHANGELOG.md | 1 + bot.py | 1 - cogs/modmail.py | 4 +++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 209bbcfcea..fb025baca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Editing notes now work. ([GH #3094](https://github.com/kyb3r/modmail/issues/3094)) - Commands now work in threads. - Audit log searching now properly works. +- Old data causing `?blocked` to fail. ([GH #3131](https://github.com/kyb3r/modmail/issues/3131)) ### Internal diff --git a/bot.py b/bot.py index 6c0719243b..da1e7117a3 100644 --- a/bot.py +++ b/bot.py @@ -6,7 +6,6 @@ import logging import os import re -import signal import string import struct import sys diff --git a/cogs/modmail.py b/cogs/modmail.py index 8ef853963c..63997b2824 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1565,7 +1565,9 @@ async def blocked(self, ctx): ) if end_time is not None: - after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() + after = ( + datetime.fromisoformat(end_time.group(1)).replace(tzinfo=timezone.utc) - now + ).total_seconds() if after <= 0: # No longer blocked self.bot.blocked_users.pop(str(id_)) From 71daedc219d07c797133adf77eff1f4918faf4b0 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 20 Apr 2022 21:02:32 +0800 Subject: [PATCH 1039/1221] update reqs.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fb43d23e2b..f0e1933ce0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2 colorama==0.4.4 dnspython==2.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' emoji==1.2.0 -git+https://github.com/Rapptz/discord.py.git@45d498c1b76deaf3b394d17ccf56112fa691d160#egg=discord-py +git+https://github.com/Rapptz/discord.py.git@987235d5649e7c2b1a927637bab6547244ecb2cf#egg=discord-py idna==3.3; python_version >= '3.5' isodate==0.6.0 motor==2.4.0 From 0e05f440526c87f486c9ce53f41876dcfd6017f3 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 20 Apr 2022 22:29:13 +0800 Subject: [PATCH 1040/1221] Fix stop button in paginator --- core/paginator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/paginator.py b/core/paginator.py index 39782817b5..39d0c434a3 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -216,7 +216,7 @@ def __init__(self, handler: PaginatorSession, *args, **kwargs): self.fill_items() @discord.ui.button(label="Stop", style=ButtonStyle.danger) - async def stop_button(self, button: Button, interaction: Interaction): + async def stop_button(self, interaction: Interaction, button: Button): await self.handler.close(interaction=interaction) def fill_items(self): From c57e3421acc8c0aa0aa7120412e00c3f7b15c406 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 20 Apr 2022 22:49:33 +0800 Subject: [PATCH 1041/1221] Fix one-page paginator --- bot.py | 2 +- core/paginator.py | 34 +++++++++++++++++++++------------- pyproject.toml | 2 +- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/bot.py b/bot.py index da1e7117a3..592dbcb3b9 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev8" +__version__ = "4.0.0-dev9" import asyncio diff --git a/core/paginator.py b/core/paginator.py index 39d0c434a3..5a6844f382 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -87,27 +87,31 @@ async def show_page(self, index: int) -> typing.Optional[typing.Dict]: def update_disabled_status(self): if self.current == self.first_page(): # disable << button - if self._buttons_map["<<"]: + if self._buttons_map["<<"] is not None: self._buttons_map["<<"].disabled = True - self._buttons_map["<"].disabled = True + if self._buttons_map["<"] is not None: + self._buttons_map["<"].disabled = True else: - if self._buttons_map["<<"]: + if self._buttons_map["<<"] is not None: self._buttons_map["<<"].disabled = False - self._buttons_map["<"].disabled = False + if self._buttons_map["<"] is not None: + self._buttons_map["<"].disabled = False if self.current == self.last_page(): # disable >> button if self._buttons_map[">>"] is not None: self._buttons_map[">>"].disabled = True - self._buttons_map[">"].disabled = True + if self._buttons_map[">"] is not None: + self._buttons_map[">"].disabled = True else: if self._buttons_map[">>"] is not None: self._buttons_map[">>"].disabled = False - self._buttons_map[">"].disabled = False + if self._buttons_map[">"] is not None: + self._buttons_map[">"].disabled = False async def create_base(self, item) -> None: """ @@ -151,7 +155,10 @@ async def run(self) -> typing.Optional[Message]: """ if not self.running: await self.show_page(self.current) - await self.view.wait() + + if self.view is not None: + await self.view.wait() + await self.close(delete=False) async def close( @@ -182,12 +189,13 @@ async def close( self.running = False - self.view.stop() - if delete: - await message.delete() - else: - self.view.clear_items() - await message.edit(view=self.view) + if self.view is not None: + self.view.stop() + if delete: + await message.delete() + else: + self.view.clear_items() + await message.edit(view=self.view) class PaginatorView(View): diff --git a/pyproject.toml b/pyproject.toml index ad792dc399..3b821dd6f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev8' +version = '4.0.0-dev9' description = "Modmail is similar to Reddit's 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 d54b687bc69933f05f6056d1600331a051bfb65a Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 21:40:55 +0800 Subject: [PATCH 1042/1221] fix gh cmd, resolve #3149 --- core/clients.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/clients.py b/core/clients.py index 5caf58f4d2..a9722776f2 100644 --- a/core/clients.py +++ b/core/clients.py @@ -680,7 +680,7 @@ async def get_user_info(self) -> Optional[dict]: return { "user": { "username": user.username, - "avatar_url": user.display_avatar.url, + "avatar_url": user.avatar_url, "url": user.url, } } From 46f711af61962551cf8c8d03850528efa8b2bdb5 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 21:43:47 +0800 Subject: [PATCH 1043/1221] add title to ?logs, resolve #3142 --- cogs/modmail.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cogs/modmail.py b/cogs/modmail.py index 63997b2824..ea36bef7b9 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -656,6 +656,9 @@ 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']}>") + if entry["title"]: + embed.add_field(name="Title", value=entry["title"], inline=False) + embed.add_field(name="Preview", value=format_preview(entry["messages"]), inline=False) if closer is not None: From a11c0ed9525dee63522a393b49d30833b559e95a Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 21:47:56 +0800 Subject: [PATCH 1044/1221] fix delete channel autoclose functionality --- CHANGELOG.md | 2 ++ bot.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb025baca2..4b75c286aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `use_nickname_channel_name` config to use nicknames instead of usernames for channel names. ([GH #3112](https://github.com/kyb3r/modmail/issues/3112)) - `show_log_url_button` config to show Log URL button. ([GH #3122](https://github.com/kyb3r/modmail/issues/3122)) - Select menus for certain paginators. +- `Title` field in `?logs`. ([GH #3142](https://github.com/kyb3r/modmail/issues/3142)) ### Improved @@ -45,6 +46,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Commands now work in threads. - Audit log searching now properly works. - Old data causing `?blocked` to fail. ([GH #3131](https://github.com/kyb3r/modmail/issues/3131)) +- Delete channel auto close functionality now works. ### Internal diff --git a/bot.py b/bot.py index 592dbcb3b9..4ff3e69091 100644 --- a/bot.py +++ b/bot.py @@ -1336,9 +1336,13 @@ async def on_guild_channel_delete(self, channel): return audit_logs = self.modmail_guild.audit_logs(limit=10, action=discord.AuditLogAction.channel_delete) - entry = await audit_logs.find(lambda a: int(a.target.id) == channel.id) + found_entry = False + async for entry in audit_logs: + if int(entry.target.id) == channel.id: + found_entry = True + break - if entry is None: + if not found_entry: logger.debug("Cannot find the audit log entry for channel delete of %d.", channel.id) return From 4aed3b42d3d94161073d840941419444aac5eb5e Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 21:57:55 +0800 Subject: [PATCH 1045/1221] Update dpy@a14b43f2fda863ed6555374eb872bf014bdd1adf --- Pipfile | 2 +- Pipfile.lock | 260 ++++++++++++++++++++++++----------------------- bot.py | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 5 files changed, 135 insertions(+), 133 deletions(-) diff --git a/Pipfile b/Pipfile index 1241feeb40..c74f6cb1a7 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ pylint = "~=2.9.3" [packages] aiohttp = "==3.7.4.post0" colorama = "~=0.4.4" # Doesn't officially support Python 3.9 yet, v0.4.5 will support 3.9 -"discord.py" = {ref = "987235d5649e7c2b1a927637bab6547244ecb2cf", git = "https://github.com/Rapptz/discord.py.git"} +"discord.py" = {git = "https://github.com/Rapptz/discord.py.git"} emoji = "~=1.2.0" isodate = "~=0.6.0" motor = "~=2.4.0" diff --git a/Pipfile.lock b/Pipfile.lock index 5cd3d63616..d163dd5ae1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5f3ca86a89bbc9bcda36bdb3503c91f81ff9c5e12409c3ad056524aa920f4fad" + "sha256": "c0006885019e90564b0907b571e5a7bcd5f8b47d1dc3c535959f2600d4d15ba6" }, "pipfile-spec": 6, "requires": { @@ -163,11 +163,11 @@ }, "cssselect2": { "hashes": [ - "sha256:8d4690bce5f25013262997e64cef3e7bade877d3ef126f9cc624e5b1f294d934", - "sha256:d98a7bbdd8ebc46093279195d669a3359bd5a23f90c19e82c19d9eeef333e617" + "sha256:3a83b2a68370c69c9cd3fcb88bbfaebe9d22edeef2c22d1ff3e1ed9c7fa45ed8", + "sha256:5b5d6dea81a5eb0c9ca39f116c8578dd413778060c94c1f51196371618909325" ], "markers": "python_version >= '3.7'", - "version": "==0.5.0" + "version": "==0.6.0" }, "defusedxml": { "hashes": [ @@ -179,11 +179,10 @@ }, "discord-py": { "git": "https://github.com/Rapptz/discord.py.git", - "ref": "97fe07edb2f3d0f5a980917fb6ee479bf396fd94" + "ref": "a14b43f2fda863ed6555374eb872bf014bdd1adf" }, "discord.py": { - "git": "https://github.com/Rapptz/discord.py.git", - "ref": "97fe07edb2f3d0f5a980917fb6ee479bf396fd94" + "git": "https://github.com/Rapptz/discord.py.git" }, "dnspython": { "hashes": [ @@ -317,44 +316,47 @@ }, "pillow": { "hashes": [ - "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97", - "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049", - "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c", - "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae", - "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28", - "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030", - "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56", - "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976", - "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e", - "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e", - "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f", - "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b", - "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a", - "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e", - "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa", - "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7", - "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00", - "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838", - "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360", - "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b", - "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a", - "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd", - "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4", - "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70", - "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204", - "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc", - "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b", - "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669", - "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7", - "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e", - "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c", - "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092", - "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c", - "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5", - "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac" + "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e", + "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595", + "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512", + "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c", + "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477", + "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a", + "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4", + "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e", + "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5", + "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378", + "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a", + "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652", + "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7", + "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a", + "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a", + "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6", + "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165", + "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160", + "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331", + "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b", + "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458", + "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033", + "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8", + "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481", + "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58", + "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7", + "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3", + "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea", + "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34", + "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3", + "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8", + "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581", + "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244", + "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef", + "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0", + "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2", + "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97", + "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717" ], "markers": "python_version >= '3.7'", - "version": "==9.0.1" + "version": "==9.1.0" }, "pycparser": { "hashes": [ @@ -513,11 +515,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", - "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" + "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", + "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" ], - "markers": "python_version >= '3.6'", - "version": "==4.1.1" + "markers": "python_version >= '3.7'", + "version": "==4.2.0" }, "uvloop": { "hashes": [ @@ -661,11 +663,11 @@ }, "click": { "hashes": [ - "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", - "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" + "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", + "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72" ], - "markers": "python_version >= '3.6'", - "version": "==8.0.4" + "markers": "python_version >= '3.7'", + "version": "==8.1.2" }, "colorama": { "hashes": [ @@ -820,91 +822,91 @@ }, "regex": { "hashes": [ - "sha256:0066a6631c92774391f2ea0f90268f0d82fffe39cb946f0f9c6b382a1c61a5e5", - "sha256:0100f0ded953b6b17f18207907159ba9be3159649ad2d9b15535a74de70359d3", - "sha256:01c913cf573d1da0b34c9001a94977273b5ee2fe4cb222a5d5b320f3a9d1a835", - "sha256:0214ff6dff1b5a4b4740cfe6e47f2c4c92ba2938fca7abbea1359036305c132f", - "sha256:029e9e7e0d4d7c3446aa92474cbb07dafb0b2ef1d5ca8365f059998c010600e6", - "sha256:0317eb6331146c524751354ebef76a7a531853d7207a4d760dfb5f553137a2a4", - "sha256:04b5ee2b6d29b4a99d38a6469aa1db65bb79d283186e8460542c517da195a8f6", - "sha256:04c09b9651fa814eeeb38e029dc1ae83149203e4eeb94e52bb868fadf64852bc", - "sha256:058054c7a54428d5c3e3739ac1e363dc9347d15e64833817797dc4f01fb94bb8", - "sha256:060f9066d2177905203516c62c8ea0066c16c7342971d54204d4e51b13dfbe2e", - "sha256:0a7b75cc7bb4cc0334380053e4671c560e31272c9d2d5a6c4b8e9ae2c9bd0f82", - "sha256:0e2630ae470d6a9f8e4967388c1eda4762706f5750ecf387785e0df63a4cc5af", - "sha256:174d964bc683b1e8b0970e1325f75e6242786a92a22cedb2a6ec3e4ae25358bd", - "sha256:25ecb1dffc5e409ca42f01a2b2437f93024ff1612c1e7983bad9ee191a5e8828", - "sha256:286908cbe86b1a0240a867aecfe26a439b16a1f585d2de133540549831f8e774", - "sha256:303b15a3d32bf5fe5a73288c316bac5807587f193ceee4eb6d96ee38663789fa", - "sha256:34bb30c095342797608727baf5c8aa122406aa5edfa12107b8e08eb432d4c5d7", - "sha256:3e265b388cc80c7c9c01bb4f26c9e536c40b2c05b7231fbb347381a2e1c8bf43", - "sha256:3e4d710ff6539026e49f15a3797c6b1053573c2b65210373ef0eec24480b900b", - "sha256:42eb13b93765c6698a5ab3bcd318d8c39bb42e5fa8a7fcf7d8d98923f3babdb1", - "sha256:48081b6bff550fe10bcc20c01cf6c83dbca2ccf74eeacbfac240264775fd7ecf", - "sha256:491fc754428514750ab21c2d294486223ce7385446f2c2f5df87ddbed32979ae", - "sha256:4d1445824944e642ffa54c4f512da17a953699c563a356d8b8cbdad26d3b7598", - "sha256:530a3a16e57bd3ea0dff5ec2695c09632c9d6c549f5869d6cf639f5f7153fb9c", - "sha256:591d4fba554f24bfa0421ba040cd199210a24301f923ed4b628e1e15a1001ff4", - "sha256:5a86cac984da35377ca9ac5e2e0589bd11b3aebb61801204bd99c41fac516f0d", - "sha256:5b1ceede92400b3acfebc1425937454aaf2c62cd5261a3fabd560c61e74f6da3", - "sha256:5b2e24f3ae03af3d8e8e6d824c891fea0ca9035c5d06ac194a2700373861a15c", - "sha256:6504c22c173bb74075d7479852356bb7ca80e28c8e548d4d630a104f231e04fb", - "sha256:673f5a393d603c34477dbad70db30025ccd23996a2d0916e942aac91cc42b31a", - "sha256:6ca6dcd17f537e9f3793cdde20ac6076af51b2bd8ad5fe69fa54373b17b48d3c", - "sha256:6e1d8ed9e61f37881c8db383a124829a6e8114a69bd3377a25aecaeb9b3538f8", - "sha256:75a5e6ce18982f0713c4bac0704bf3f65eed9b277edd3fb9d2b0ff1815943327", - "sha256:76435a92e444e5b8f346aed76801db1c1e5176c4c7e17daba074fbb46cb8d783", - "sha256:764e66a0e382829f6ad3bbce0987153080a511c19eb3d2f8ead3f766d14433ac", - "sha256:78ce90c50d0ec970bd0002462430e00d1ecfd1255218d52d08b3a143fe4bde18", - "sha256:794a6bc66c43db8ed06698fc32aaeaac5c4812d9f825e9589e56f311da7becd9", - "sha256:797437e6024dc1589163675ae82f303103063a0a580c6fd8d0b9a0a6708da29e", - "sha256:7b7494df3fdcc95a1f76cf134d00b54962dd83189520fd35b8fcd474c0aa616d", - "sha256:7d1a6e403ac8f1d91d8f51c441c3f99367488ed822bda2b40836690d5d0059f5", - "sha256:7f63877c87552992894ea1444378b9c3a1d80819880ae226bb30b04789c0828c", - "sha256:8923e1c5231549fee78ff9b2914fad25f2e3517572bb34bfaa3aea682a758683", - "sha256:8afcd1c2297bc989dceaa0379ba15a6df16da69493635e53431d2d0c30356086", - "sha256:8b1cc70e31aacc152a12b39245974c8fccf313187eead559ee5966d50e1b5817", - "sha256:8d1f3ea0d1924feb4cf6afb2699259f658a08ac6f8f3a4a806661c2dfcd66db1", - "sha256:940570c1a305bac10e8b2bc934b85a7709c649317dd16520471e85660275083a", - "sha256:947a8525c0a95ba8dc873191f9017d1b1e3024d4dc757f694e0af3026e34044a", - "sha256:9beb03ff6fe509d6455971c2489dceb31687b38781206bcec8e68bdfcf5f1db2", - "sha256:9c144405220c5ad3f5deab4c77f3e80d52e83804a6b48b6bed3d81a9a0238e4c", - "sha256:a98ae493e4e80b3ded6503ff087a8492db058e9c68de371ac3df78e88360b374", - "sha256:aa2ce79f3889720b46e0aaba338148a1069aea55fda2c29e0626b4db20d9fcb7", - "sha256:aa5eedfc2461c16a092a2fabc5895f159915f25731740c9152a1b00f4bcf629a", - "sha256:ab5d89cfaf71807da93c131bb7a19c3e19eaefd613d14f3bce4e97de830b15df", - "sha256:b4829db3737480a9d5bfb1c0320c4ee13736f555f53a056aacc874f140e98f64", - "sha256:b52771f05cff7517f7067fef19ffe545b1f05959e440d42247a17cd9bddae11b", - "sha256:b8248f19a878c72d8c0a785a2cd45d69432e443c9f10ab924c29adda77b324ae", - "sha256:b9809404528a999cf02a400ee5677c81959bc5cb938fdc696b62eb40214e3632", - "sha256:c155a1a80c5e7a8fa1d9bb1bf3c8a953532b53ab1196092749bafb9d3a7cbb60", - "sha256:c33ce0c665dd325200209340a88438ba7a470bd5f09f7424e520e1a3ff835b52", - "sha256:c5adc854764732dbd95a713f2e6c3e914e17f2ccdc331b9ecb777484c31f73b6", - "sha256:cb374a2a4dba7c4be0b19dc7b1adc50e6c2c26c3369ac629f50f3c198f3743a4", - "sha256:cd00859291658fe1fda48a99559fb34da891c50385b0bfb35b808f98956ef1e7", - "sha256:ce3057777a14a9a1399b81eca6a6bfc9612047811234398b84c54aeff6d536ea", - "sha256:d0a5a1fdc9f148a8827d55b05425801acebeeefc9e86065c7ac8b8cc740a91ff", - "sha256:dad3991f0678facca1a0831ec1ddece2eb4d1dd0f5150acb9440f73a3b863907", - "sha256:dc7b7c16a519d924c50876fb152af661a20749dcbf653c8759e715c1a7a95b18", - "sha256:dcbb7665a9db9f8d7642171152c45da60e16c4f706191d66a1dc47ec9f820aed", - "sha256:df037c01d68d1958dad3463e2881d3638a0d6693483f58ad41001aa53a83fcea", - "sha256:f08a7e4d62ea2a45557f561eea87c907222575ca2134180b6974f8ac81e24f06", - "sha256:f16cf7e4e1bf88fecf7f41da4061f181a6170e179d956420f84e700fb8a3fd6b", - "sha256:f2c53f3af011393ab5ed9ab640fa0876757498aac188f782a0c620e33faa2a3d", - "sha256:f320c070dea3f20c11213e56dbbd7294c05743417cde01392148964b7bc2d31a", - "sha256:f553a1190ae6cd26e553a79f6b6cfba7b8f304da2071052fa33469da075ea625", - "sha256:fc8c7958d14e8270171b3d72792b609c057ec0fa17d507729835b5cff6b7f69a" + "sha256:02543d6d5c32d361b7cc468079ba4cddaaf4a6544f655901ba1ff9d8e3f18755", + "sha256:036d1c1fbe69eba3ee253c107e71749cdbb4776db93d674bc0d5e28f30300734", + "sha256:071bcb625e890f28b7c4573124a6512ea65107152b1d3ca101ce33a52dad4593", + "sha256:0f8da3145f4b72f7ce6181c804eaa44cdcea313c8998cdade3d9e20a8717a9cb", + "sha256:0fb6cb16518ac7eff29d1e0b0cce90275dfae0f17154165491058c31d58bdd1d", + "sha256:0fd464e547dbabf4652ca5fe9d88d75ec30182981e737c07b3410235a44b9939", + "sha256:12af15b6edb00e425f713160cfd361126e624ec0de86e74f7cad4b97b7f169b3", + "sha256:165cc75cfa5aa0f12adb2ac6286330e7229a06dc0e6c004ec35da682b5b89579", + "sha256:1a07e8366115069f26822c47732122ab61598830a69f5629a37ea8881487c107", + "sha256:1c2de7f32fa87d04d40f54bce3843af430697aba51c3a114aa62837a0772f219", + "sha256:253f858a0255cd91a0424a4b15c2eedb12f20274f85731b0d861c8137e843065", + "sha256:275afc7352982ee947fc88f67a034b52c78395977b5fc7c9be15f7dc95b76f06", + "sha256:2bde99f2cdfd6db1ec7e02d68cadd384ffe7413831373ea7cc68c5415a0cb577", + "sha256:3241db067a7f69da57fba8bca543ac8a7ca415d91e77315690202749b9fdaba1", + "sha256:37903d5ca11fa47577e8952d2e2c6de28553b11c70defee827afb941ab2c6729", + "sha256:3dfbadb7b74d95f72f9f9dbf9778f7de92722ab520a109ceaf7927461fa85b10", + "sha256:3e35c50b27f36176c792738cb9b858523053bc495044d2c2b44db24376b266f1", + "sha256:3e9e983fc8e0d4d5ded7caa5aed39ca2cf6026d7e39801ef6f0af0b1b6cd9276", + "sha256:3f6bd8178cce5bb56336722d5569d19c50bba5915a69a2050c497fb921e7cb0f", + "sha256:43ee0df35925ae4b0cc6ee3f60b73369e559dd2ac40945044da9394dd9d3a51d", + "sha256:45b761406777a681db0c24686178532134c937d24448d9e085279b69e9eb7da4", + "sha256:46cbc5b23f85e94161b093dba1b49035697cf44c7db3c930adabfc0e6d861b95", + "sha256:4f2e2cef324ca9355049ee1e712f68e2e92716eba24275e6767b9bfa15f1f478", + "sha256:50b77622016f03989cd06ecf6b602c7a6b4ed2e3ce04133876b041d109c934ee", + "sha256:582ea06079a03750b5f71e20a87cd99e646d796638b5894ff85987ebf5e04924", + "sha256:58521abdab76583bd41ef47e5e2ddd93b32501aee4ee8cee71dee10a45ba46b1", + "sha256:5b9c7b6895a01204296e9523b3e12b43e013835a9de035a783907c2c1bc447f0", + "sha256:6165e737acb3bea3271372e8aa5ebe7226c8a8e8da1b94af2d6547c5a09d689d", + "sha256:66fb765b2173d90389384708e3e1d3e4be1148bd8d4d50476b1469da5a2f0229", + "sha256:68aed3fb0c61296bd6d234f558f78c51671f79ccb069cbcd428c2eea6fee7a5b", + "sha256:6a0ef57cccd8089b4249eebad95065390e56c04d4a92c51316eab4131bca96a9", + "sha256:709396c0c95b95045fac89b94f997410ff39b81a09863fe21002f390d48cc7d3", + "sha256:73ed1b06abadbf6b61f6033a07c06f36ec0ddca117e41ef2ac37056705e46458", + "sha256:7a608022f4593fc67518c6c599ae5abdb03bb8acd75993c82cd7a4c8100eff81", + "sha256:7c4d9770e579eb11b582b2e2fd19fa204a15cb1589ae73cd4dcbb63b64f3e828", + "sha256:7dbc96419ef0fb6ac56626014e6d3a345aeb8b17a3df8830235a88626ffc8d84", + "sha256:7f271d0831d8ebc56e17b37f9fa1824b0379221d1238ae77c18a6e8c47f1fdce", + "sha256:82b7fc67e49fdce671bdbec1127189fc979badf062ce6e79dc95ef5e07a8bf92", + "sha256:85b7ee4d0c7a46296d884f6b489af8b960c4291d76aea4b22fd4fbe05e6ec08e", + "sha256:8b747cef8e5dcdaf394192d43a0c02f5825aeb0ecd3d43e63ae500332ab830b0", + "sha256:8bf867ba71856414a482e4b683500f946c300c4896e472e51d3db8dfa8dc8f32", + "sha256:8e0da7ef160d4f3eb3d4d3e39a02c3c42f7dbcfce62c81f784cc99fc7059765f", + "sha256:8e7d33f93cdd01868327d834d0f5bb029241cd293b47d51b96814dec27fc9b4b", + "sha256:92183e9180c392371079262879c6532ccf55f808e6900df5d9f03c9ca8807255", + "sha256:92ad03f928675ca05b79d3b1d3dfc149e2226d57ed9d57808f82105d511d0212", + "sha256:97af238389cb029d63d5f2d931a7e8f5954ad96e812de5faaed373b68e74df86", + "sha256:9913bcf730eb6e9b441fb176832eea9acbebab6035542c7c89d90c803f5cd3be", + "sha256:9dae5affbb66178dad6c6fd5b02221ca9917e016c75ee3945e9a9563eb1fbb6f", + "sha256:a850f5f369f1e3b6239da7fb43d1d029c1e178263df671819889c47caf7e4ff3", + "sha256:aa6daa189db9104787ff1fd7a7623ce017077aa59eaac609d0d25ba95ed251a0", + "sha256:aabc28f7599f781ddaeac168d0b566d0db82182cc3dcf62129f0a4fc2927b811", + "sha256:af1e687ffab18a75409e5e5d6215b6ccd41a5a1a0ea6ce9665e01253f737a0d3", + "sha256:b1d53835922cd0f9b74b2742453a444865a70abae38d12eb41c59271da66f38d", + "sha256:b2df3ede85d778c949d9bd2a50237072cee3df0a423c91f5514f78f8035bde87", + "sha256:b415b82e5be7389ec5ee7ee35431e4a549ea327caacf73b697c6b3538cb5c87f", + "sha256:b7ba3c304a4a5d8112dbd30df8b3e4ef59b4b07807957d3c410d9713abaee9a8", + "sha256:bcc6f7a3a95119c3568c572ca167ada75f8319890706283b9ba59b3489c9bcb3", + "sha256:be392d9cd5309509175a9d7660dc17bf57084501108dbff0c5a8bfc3646048c3", + "sha256:bea61de0c688198e3d9479344228c7accaa22a78b58ec408e41750ebafee6c08", + "sha256:bedb3d01ad35ea1745bdb1d57f3ee0f996f988c98f5bbae9d068c3bb3065d210", + "sha256:c36906a7855ec33a9083608e6cd595e4729dab18aeb9aad0dd0b039240266239", + "sha256:c4fdf837666f7793a5c3cfa2f2f39f03eb6c7e92e831bc64486c2f547580c2b3", + "sha256:cfad3a770839aa456ff9a9aa0e253d98b628d005a3ccb37da1ff9be7c84fee16", + "sha256:d128e278e5e554c5c022c7bed410ca851e00bacebbb4460de546a73bc53f8de4", + "sha256:dffd9114ade73137ab2b79a8faf864683dbd2dbbb6b23a305fbbd4cbaeeb2187", + "sha256:e2acf5c66fbb62b5fe4c40978ddebafa50818f00bf79d60569d9762f6356336e", + "sha256:e65580ae3137bce712f505ec7c2d700aef0014a3878c4767b74aff5895fc454f", + "sha256:e944268445b5694f5d41292c9228f0ca46d5a32a67f195d5f8547c1f1d91f4bc", + "sha256:ed26c3d2d62c6588e0dad175b8d8cc0942a638f32d07b80f92043e5d73b7db67", + "sha256:ed625205f5f26984382b68e4cbcbc08e6603c9e84c14b38457170b0cc71c823b", + "sha256:f2a5d9f612091812dee18375a45d046526452142e7b78c4e21ab192db15453d5", + "sha256:f86aef546add4ff1202e1f31e9bb54f9268f17d996b2428877283146bf9bc013", + "sha256:f89d26e50a4c7453cb8c415acd09e72fbade2610606a9c500a1e48c43210a42d", + "sha256:fb7107faf0168de087f62a2f2ed00f9e9da12e0b801582b516ddac236b871cda" ], "markers": "python_version >= '3.6'", - "version": "==2022.3.15" + "version": "==2022.4.24" }, "setuptools": { "hashes": [ - "sha256:6599055eeb23bfef457d5605d33a4d68804266e6cb430b0fb12417c5efeae36c", - "sha256:782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96" + "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", + "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" ], "markers": "python_version >= '3.7'", - "version": "==60.10.0" + "version": "==62.1.0" }, "smmap": { "hashes": [ diff --git a/bot.py b/bot.py index 4ff3e69091..2887fd79dc 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev9" +__version__ = "4.0.0-dev10" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 3b821dd6f8..9d08c1d2b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev9' +version = '4.0.0-dev10' description = "Modmail is similar to Reddit's 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 = [ diff --git a/requirements.txt b/requirements.txt index f0e1933ce0..5a595bb521 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2 colorama==0.4.4 dnspython==2.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' emoji==1.2.0 -git+https://github.com/Rapptz/discord.py.git@987235d5649e7c2b1a927637bab6547244ecb2cf#egg=discord-py +git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord-py idna==3.3; python_version >= '3.5' isodate==0.6.0 motor==2.4.0 From 16816f32158b8b1819ab645b52b3dda7d0cc47a8 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 22:01:35 +0800 Subject: [PATCH 1046/1221] formatting --- cogs/utility.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 7facfbf1d2..df904cd331 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -914,9 +914,7 @@ def fmt(val): for i, (current_key, info) in enumerate(config_help.items()): if current_key == key: index = i - embed = discord.Embed( - title=f"{current_key}", color=self.bot.main_color - ) + embed = discord.Embed(title=f"{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) if info["examples"]: From 4f15cf6666f59f915d9ed6eeab03fe1154151203 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 22:06:27 +0800 Subject: [PATCH 1047/1221] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4696b7067c..55a23a8db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,13 +25,13 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `show_log_url_button` config to show Log URL button. ([GH #3122](https://github.com/kyb3r/modmail/issues/3122)) - Select menus for certain paginators. - `Title` field in `?logs`. ([GH #3142](https://github.com/kyb3r/modmail/issues/3142)) +- Snippets can be used in aliases. ([GH #3108](https://github.com/kyb3r/modmail/issues/3108), [PR #3124](https://github.com/kyb3r/modmail/pull/3124)) ### Improved - Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/kyb3r/modmail/issues/3048)) - Use discord relative timedeltas. ([GH #3046](https://github.com/kyb3r/modmail/issues/3046)) - Use discord native buttons for all paginator sessions. -- Snippets can be used in aliases. ([GH #3108](https://github.com/kyb3r/modmail/issues/3108), [PR #3124](https://github.com/kyb3r/modmail/pull/3124)) ### Fixed From 70decd82e835e942be08e8552567e3f97e830763 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 22:16:59 +0800 Subject: [PATCH 1048/1221] Fix reqs --- Pipfile | 5 +-- Pipfile.lock | 9 +++--- requirements.txt | 79 ++++++++++++++++++++++++------------------------ 3 files changed, 45 insertions(+), 48 deletions(-) diff --git a/Pipfile b/Pipfile index c74f6cb1a7..9c537d8bd2 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ pylint = "~=2.9.3" [packages] aiohttp = "==3.7.4.post0" colorama = "~=0.4.4" # Doesn't officially support Python 3.9 yet, v0.4.5 will support 3.9 -"discord.py" = {git = "https://github.com/Rapptz/discord.py.git"} +"discord.py" = {ref = "a14b43f2fda863ed6555374eb872bf014bdd1adf", git = "https://github.com/Rapptz/discord.py.git"} emoji = "~=1.2.0" isodate = "~=0.6.0" motor = "~=2.4.0" @@ -25,6 +25,3 @@ lottie = {version = "==0.6.10", extras = ["pdf"]} [scripts] bot = "python bot.py" - -[requires] -python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index d163dd5ae1..ec6f1fc5c6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "c0006885019e90564b0907b571e5a7bcd5f8b47d1dc3c535959f2600d4d15ba6" + "sha256": "5280f0d7e4c267e164f83139d89bec37c4691a5815b831b871a3091db1af56eb" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -182,7 +180,8 @@ "ref": "a14b43f2fda863ed6555374eb872bf014bdd1adf" }, "discord.py": { - "git": "https://github.com/Rapptz/discord.py.git" + "git": "https://github.com/Rapptz/discord.py.git", + "ref": "a14b43f2fda863ed6555374eb872bf014bdd1adf" }, "dnspython": { "hashes": [ diff --git a/requirements.txt b/requirements.txt index 8b6e2499cc..34852a4e37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,39 +1,40 @@ -# -# These requirements were autogenerated by pipenv -# To regenerate from the project's Pipfile, run: -# -# pipenv lock --requirements -# - --i https://pypi.org/simple -aiohttp==3.7.4.post0 -async-timeout==3.0.1; python_full_version >= '3.5.3' -attrs==21.2.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -cairocffi==1.3.0; python_version >= '3.7' -cairosvg==2.5.2; python_version >= '3.5' -cffi==1.15.0 -chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -colorama==0.4.4 -cssselect2==0.4.1; python_version >= '3.6' -defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -dnspython==2.1.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -emoji==1.2.0 -git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord-py -idna==3.3; python_version >= '3.5' -isodate==0.6.0 -lottie[pdf]==0.6.10 -motor==2.4.0 -multidict==5.2.0; python_version >= '3.6' -natural==0.2.0 -parsedatetime==2.6 -pillow==8.4.0; python_version >= '3.6' -pycparser==2.21 -pymongo[srv]==3.12.1 -python-dateutil==2.8.2 -python-dotenv==0.18.0 -six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -tinycss2==1.1.0; python_version >= '3.6' -typing-extensions==4.0.0; python_version >= '3.6' -uvloop==0.16.0; sys_platform != 'win32' -webencodings==0.5.1 -yarl==1.7.2; python_version >= '3.6' \ No newline at end of file +# +# These requirements were autogenerated by pipenv +# To regenerate from the project's Pipfile, run: +# +# pipenv lock --requirements +# + +-i https://pypi.org/simple +aiohttp==3.7.4.post0 +async-timeout==3.0.1; python_full_version >= '3.5.3' +attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +cairocffi==1.3.0; python_version >= '3.7' +cairosvg==2.5.2; python_version >= '3.5' +cffi==1.15.0 +chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +colorama==0.4.4 +cssselect2==0.6.0; python_version >= '3.7' +defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +dnspython==2.2.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +emoji==1.2.0 +git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord-py +git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord.py +idna==3.3; python_version >= '3.5' +isodate==0.6.1 +lottie[pdf]==0.6.10 +motor==2.4.0 +multidict==6.0.2; python_version >= '3.7' +natural==0.2.0 +parsedatetime==2.6 +pillow==9.1.0; python_version >= '3.7' +pycparser==2.21 +pymongo[srv]==3.12.3 +python-dateutil==2.8.2 +python-dotenv==0.18.0 +six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +tinycss2==1.1.1; python_version >= '3.6' +typing-extensions==4.2.0; python_version >= '3.7' +uvloop==0.16.0; sys_platform != 'win32' +webencodings==0.5.1 +yarl==1.7.2; python_version >= '3.6' From ef100c283fdb84da6b4c6e52f21f9b2440036719 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 22:19:59 +0800 Subject: [PATCH 1049/1221] another attempt to fix reqs --- .github/workflows/lints.yml | 2 +- requirements.txt | 80 ++++++++++++++++++------------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 091e012dd9..8498875fd3 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -21,7 +21,7 @@ jobs: python-version: 3.9 - name: Install dependencies run: | - python -m pip install --upgrade pip pipenv + python -m pip install --upgrade pip pipenv click==8.1.2 pipenv install --dev --system # - name: Bandit syntax check # run: bandit -r . -b .bandit_baseline.json diff --git a/requirements.txt b/requirements.txt index 34852a4e37..ecff8c80c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,40 +1,40 @@ -# -# These requirements were autogenerated by pipenv -# To regenerate from the project's Pipfile, run: -# -# pipenv lock --requirements -# - --i https://pypi.org/simple -aiohttp==3.7.4.post0 -async-timeout==3.0.1; python_full_version >= '3.5.3' -attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -cairocffi==1.3.0; python_version >= '3.7' -cairosvg==2.5.2; python_version >= '3.5' -cffi==1.15.0 -chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -colorama==0.4.4 -cssselect2==0.6.0; python_version >= '3.7' -defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -dnspython==2.2.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -emoji==1.2.0 -git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord-py -git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord.py -idna==3.3; python_version >= '3.5' -isodate==0.6.1 -lottie[pdf]==0.6.10 -motor==2.4.0 -multidict==6.0.2; python_version >= '3.7' -natural==0.2.0 -parsedatetime==2.6 -pillow==9.1.0; python_version >= '3.7' -pycparser==2.21 -pymongo[srv]==3.12.3 -python-dateutil==2.8.2 -python-dotenv==0.18.0 -six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -tinycss2==1.1.1; python_version >= '3.6' -typing-extensions==4.2.0; python_version >= '3.7' -uvloop==0.16.0; sys_platform != 'win32' -webencodings==0.5.1 -yarl==1.7.2; python_version >= '3.6' +# +# These requirements were autogenerated by pipenv +# To regenerate from the project's Pipfile, run: +# +# pipenv lock --requirements +# + +-i https://pypi.org/simple + +aiohttp==3.7.4.post0 +async-timeout==3.0.1; python_full_version >= '3.5.3' +attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +cairocffi==1.3.0; python_version >= '3.7' +cairosvg==2.5.2; python_version >= '3.5' +cffi==1.15.0 +chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +colorama==0.4.4 +cssselect2==0.6.0; python_version >= '3.7' +defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +dnspython==2.2.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +emoji==1.2.0 +git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord.py +idna==3.3; python_version >= '3.5' +isodate==0.6.1 +lottie[pdf]==0.6.10 +motor==2.4.0 +multidict==6.0.2; python_version >= '3.7' +natural==0.2.0 +parsedatetime==2.6 +pillow==9.1.0; python_version >= '3.7' +pycparser==2.21 +pymongo[srv]==3.12.3 +python-dateutil==2.8.2 +python-dotenv==0.18.0 +six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +tinycss2==1.1.1; python_version >= '3.6' +typing-extensions==4.2.0; python_version >= '3.7' +uvloop==0.16.0; sys_platform != 'win32' +webencodings==0.5.1 +yarl==1.7.2; python_version >= '3.6' From c8b1a78caa12dee7239c1d7bda92e7ad1c6dbc91 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 22:31:01 +0800 Subject: [PATCH 1050/1221] bump black https://github.com/psf/black/issues/2964 --- .github/workflows/lints.yml | 2 +- Pipfile | 2 +- Pipfile.lock | 132 +++++++++++------------------------- 3 files changed, 43 insertions(+), 93 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 8498875fd3..091e012dd9 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -21,7 +21,7 @@ jobs: python-version: 3.9 - name: Install dependencies run: | - python -m pip install --upgrade pip pipenv click==8.1.2 + python -m pip install --upgrade pip pipenv pipenv install --dev --system # - name: Bandit syntax check # run: bandit -r . -b .bandit_baseline.json diff --git a/Pipfile b/Pipfile index 9c537d8bd2..268523dd92 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ verify_ssl = true [dev-packages] bandit = "~=1.7.0" -black = "==21.6b0" +black = "==22.3.0" pylint = "~=2.9.3" [packages] diff --git a/Pipfile.lock b/Pipfile.lock index ec6f1fc5c6..f60ea59424 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5280f0d7e4c267e164f83139d89bec37c4691a5815b831b871a3091db1af56eb" + "sha256": "d7f737bdd78a969ba477bc9d9b334a1429a70cb78fec53d8b95c5732bbb6a3bd" }, "pipfile-spec": 6, "requires": {}, @@ -629,13 +629,6 @@ } }, "develop": { - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" - }, "astroid": { "hashes": [ "sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334", @@ -654,11 +647,32 @@ }, "black": { "hashes": [ - "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04", - "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7" + "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", + "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", + "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", + "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", + "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", + "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", + "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", + "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", + "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", + "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", + "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", + "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", + "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", + "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", + "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", + "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", + "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", + "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", + "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", + "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", + "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", + "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", + "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" ], "index": "pypi", - "version": "==21.6b0" + "version": "==22.3.0" }, "click": { "hashes": [ @@ -772,6 +786,14 @@ "markers": "python_version >= '2.6'", "version": "==5.8.1" }, + "platformdirs": { + "hashes": [ + "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", + "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" + ], + "markers": "python_version >= '3.7'", + "version": "==2.5.2" + }, "pylint": { "hashes": [ "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594", @@ -819,86 +841,6 @@ "markers": "python_version >= '3.6'", "version": "==6.0" }, - "regex": { - "hashes": [ - "sha256:02543d6d5c32d361b7cc468079ba4cddaaf4a6544f655901ba1ff9d8e3f18755", - "sha256:036d1c1fbe69eba3ee253c107e71749cdbb4776db93d674bc0d5e28f30300734", - "sha256:071bcb625e890f28b7c4573124a6512ea65107152b1d3ca101ce33a52dad4593", - "sha256:0f8da3145f4b72f7ce6181c804eaa44cdcea313c8998cdade3d9e20a8717a9cb", - "sha256:0fb6cb16518ac7eff29d1e0b0cce90275dfae0f17154165491058c31d58bdd1d", - "sha256:0fd464e547dbabf4652ca5fe9d88d75ec30182981e737c07b3410235a44b9939", - "sha256:12af15b6edb00e425f713160cfd361126e624ec0de86e74f7cad4b97b7f169b3", - "sha256:165cc75cfa5aa0f12adb2ac6286330e7229a06dc0e6c004ec35da682b5b89579", - "sha256:1a07e8366115069f26822c47732122ab61598830a69f5629a37ea8881487c107", - "sha256:1c2de7f32fa87d04d40f54bce3843af430697aba51c3a114aa62837a0772f219", - "sha256:253f858a0255cd91a0424a4b15c2eedb12f20274f85731b0d861c8137e843065", - "sha256:275afc7352982ee947fc88f67a034b52c78395977b5fc7c9be15f7dc95b76f06", - "sha256:2bde99f2cdfd6db1ec7e02d68cadd384ffe7413831373ea7cc68c5415a0cb577", - "sha256:3241db067a7f69da57fba8bca543ac8a7ca415d91e77315690202749b9fdaba1", - "sha256:37903d5ca11fa47577e8952d2e2c6de28553b11c70defee827afb941ab2c6729", - "sha256:3dfbadb7b74d95f72f9f9dbf9778f7de92722ab520a109ceaf7927461fa85b10", - "sha256:3e35c50b27f36176c792738cb9b858523053bc495044d2c2b44db24376b266f1", - "sha256:3e9e983fc8e0d4d5ded7caa5aed39ca2cf6026d7e39801ef6f0af0b1b6cd9276", - "sha256:3f6bd8178cce5bb56336722d5569d19c50bba5915a69a2050c497fb921e7cb0f", - "sha256:43ee0df35925ae4b0cc6ee3f60b73369e559dd2ac40945044da9394dd9d3a51d", - "sha256:45b761406777a681db0c24686178532134c937d24448d9e085279b69e9eb7da4", - "sha256:46cbc5b23f85e94161b093dba1b49035697cf44c7db3c930adabfc0e6d861b95", - "sha256:4f2e2cef324ca9355049ee1e712f68e2e92716eba24275e6767b9bfa15f1f478", - "sha256:50b77622016f03989cd06ecf6b602c7a6b4ed2e3ce04133876b041d109c934ee", - "sha256:582ea06079a03750b5f71e20a87cd99e646d796638b5894ff85987ebf5e04924", - "sha256:58521abdab76583bd41ef47e5e2ddd93b32501aee4ee8cee71dee10a45ba46b1", - "sha256:5b9c7b6895a01204296e9523b3e12b43e013835a9de035a783907c2c1bc447f0", - "sha256:6165e737acb3bea3271372e8aa5ebe7226c8a8e8da1b94af2d6547c5a09d689d", - "sha256:66fb765b2173d90389384708e3e1d3e4be1148bd8d4d50476b1469da5a2f0229", - "sha256:68aed3fb0c61296bd6d234f558f78c51671f79ccb069cbcd428c2eea6fee7a5b", - "sha256:6a0ef57cccd8089b4249eebad95065390e56c04d4a92c51316eab4131bca96a9", - "sha256:709396c0c95b95045fac89b94f997410ff39b81a09863fe21002f390d48cc7d3", - "sha256:73ed1b06abadbf6b61f6033a07c06f36ec0ddca117e41ef2ac37056705e46458", - "sha256:7a608022f4593fc67518c6c599ae5abdb03bb8acd75993c82cd7a4c8100eff81", - "sha256:7c4d9770e579eb11b582b2e2fd19fa204a15cb1589ae73cd4dcbb63b64f3e828", - "sha256:7dbc96419ef0fb6ac56626014e6d3a345aeb8b17a3df8830235a88626ffc8d84", - "sha256:7f271d0831d8ebc56e17b37f9fa1824b0379221d1238ae77c18a6e8c47f1fdce", - "sha256:82b7fc67e49fdce671bdbec1127189fc979badf062ce6e79dc95ef5e07a8bf92", - "sha256:85b7ee4d0c7a46296d884f6b489af8b960c4291d76aea4b22fd4fbe05e6ec08e", - "sha256:8b747cef8e5dcdaf394192d43a0c02f5825aeb0ecd3d43e63ae500332ab830b0", - "sha256:8bf867ba71856414a482e4b683500f946c300c4896e472e51d3db8dfa8dc8f32", - "sha256:8e0da7ef160d4f3eb3d4d3e39a02c3c42f7dbcfce62c81f784cc99fc7059765f", - "sha256:8e7d33f93cdd01868327d834d0f5bb029241cd293b47d51b96814dec27fc9b4b", - "sha256:92183e9180c392371079262879c6532ccf55f808e6900df5d9f03c9ca8807255", - "sha256:92ad03f928675ca05b79d3b1d3dfc149e2226d57ed9d57808f82105d511d0212", - "sha256:97af238389cb029d63d5f2d931a7e8f5954ad96e812de5faaed373b68e74df86", - "sha256:9913bcf730eb6e9b441fb176832eea9acbebab6035542c7c89d90c803f5cd3be", - "sha256:9dae5affbb66178dad6c6fd5b02221ca9917e016c75ee3945e9a9563eb1fbb6f", - "sha256:a850f5f369f1e3b6239da7fb43d1d029c1e178263df671819889c47caf7e4ff3", - "sha256:aa6daa189db9104787ff1fd7a7623ce017077aa59eaac609d0d25ba95ed251a0", - "sha256:aabc28f7599f781ddaeac168d0b566d0db82182cc3dcf62129f0a4fc2927b811", - "sha256:af1e687ffab18a75409e5e5d6215b6ccd41a5a1a0ea6ce9665e01253f737a0d3", - "sha256:b1d53835922cd0f9b74b2742453a444865a70abae38d12eb41c59271da66f38d", - "sha256:b2df3ede85d778c949d9bd2a50237072cee3df0a423c91f5514f78f8035bde87", - "sha256:b415b82e5be7389ec5ee7ee35431e4a549ea327caacf73b697c6b3538cb5c87f", - "sha256:b7ba3c304a4a5d8112dbd30df8b3e4ef59b4b07807957d3c410d9713abaee9a8", - "sha256:bcc6f7a3a95119c3568c572ca167ada75f8319890706283b9ba59b3489c9bcb3", - "sha256:be392d9cd5309509175a9d7660dc17bf57084501108dbff0c5a8bfc3646048c3", - "sha256:bea61de0c688198e3d9479344228c7accaa22a78b58ec408e41750ebafee6c08", - "sha256:bedb3d01ad35ea1745bdb1d57f3ee0f996f988c98f5bbae9d068c3bb3065d210", - "sha256:c36906a7855ec33a9083608e6cd595e4729dab18aeb9aad0dd0b039240266239", - "sha256:c4fdf837666f7793a5c3cfa2f2f39f03eb6c7e92e831bc64486c2f547580c2b3", - "sha256:cfad3a770839aa456ff9a9aa0e253d98b628d005a3ccb37da1ff9be7c84fee16", - "sha256:d128e278e5e554c5c022c7bed410ca851e00bacebbb4460de546a73bc53f8de4", - "sha256:dffd9114ade73137ab2b79a8faf864683dbd2dbbb6b23a305fbbd4cbaeeb2187", - "sha256:e2acf5c66fbb62b5fe4c40978ddebafa50818f00bf79d60569d9762f6356336e", - "sha256:e65580ae3137bce712f505ec7c2d700aef0014a3878c4767b74aff5895fc454f", - "sha256:e944268445b5694f5d41292c9228f0ca46d5a32a67f195d5f8547c1f1d91f4bc", - "sha256:ed26c3d2d62c6588e0dad175b8d8cc0942a638f32d07b80f92043e5d73b7db67", - "sha256:ed625205f5f26984382b68e4cbcbc08e6603c9e84c14b38457170b0cc71c823b", - "sha256:f2a5d9f612091812dee18375a45d046526452142e7b78c4e21ab192db15453d5", - "sha256:f86aef546add4ff1202e1f31e9bb54f9268f17d996b2428877283146bf9bc013", - "sha256:f89d26e50a4c7453cb8c415acd09e72fbade2610606a9c500a1e48c43210a42d", - "sha256:fb7107faf0168de087f62a2f2ed00f9e9da12e0b801582b516ddac236b871cda" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.4.24" - }, "setuptools": { "hashes": [ "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", @@ -931,6 +873,14 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, "wrapt": { "hashes": [ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" From fe074f1d86f206e6ad7f457cc79a4d9ae099ee12 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 22:54:43 +0800 Subject: [PATCH 1051/1221] bump range of dependencies, resolve #3126 --- CHANGELOG.md | 5 + Pipfile | 10 +- Pipfile.lock | 391 ++++++++++++++++++++++----------------------------- 3 files changed, 177 insertions(+), 229 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55a23a8db3..e97b127b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,11 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Improve regex parsing of channel topics. ([GH #3114](https://github.com/kyb3r/modmail/issues/3114), [PR #3111](https://github.com/kyb3r/modmail/pull/3111)) - Add warning if deploying on a developmental version. - Extensions are now loaded `on_connect`. +- MongoDB v5.0 clients are now supported. ([GH #3126](https://github.com/kyb3r/modmail/issues/3126)) +- Bump python-dotenv to v0.20.0, support for python 3.10 +- Bump emoji to v1.7.0 +- Bump aiohttp to v3.8.1 +- Bump lottie to v0.6.11 # v3.10.3 diff --git a/Pipfile b/Pipfile index 268523dd92..99d50e47e4 100644 --- a/Pipfile +++ b/Pipfile @@ -9,19 +9,19 @@ black = "==22.3.0" pylint = "~=2.9.3" [packages] -aiohttp = "==3.7.4.post0" +aiohttp = "==3.8.1" colorama = "~=0.4.4" # Doesn't officially support Python 3.9 yet, v0.4.5 will support 3.9 "discord.py" = {ref = "a14b43f2fda863ed6555374eb872bf014bdd1adf", git = "https://github.com/Rapptz/discord.py.git"} -emoji = "~=1.2.0" +emoji = "==1.7.0" isodate = "~=0.6.0" -motor = "~=2.4.0" +motor = "==2.5.1" natural = "~=0.2.0" parsedatetime = "~=2.6" pymongo = {extras = ["srv"], version = "*"} # Required by motor python-dateutil = "~=2.8.1" -python-dotenv = "~=0.18.0" +python-dotenv = "==0.20.0" uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} -lottie = {version = "==0.6.10", extras = ["pdf"]} +lottie = "==0.6.11" [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index f60ea59424..3ce9602d5e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d7f737bdd78a969ba477bc9d9b334a1429a70cb78fec53d8b95c5732bbb6a3bd" + "sha256": "dea4d7675ab1e4a2c4d3f4f8ddcff27db7c0b3851a37e598b10dc429c2eaa5df" }, "pipfile-spec": 6, "requires": {}, @@ -16,54 +16,97 @@ "default": { "aiohttp": { "hashes": [ - "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", - "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe", - "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5", - "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8", - "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd", - "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb", - "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c", - "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87", - "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0", - "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290", - "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5", - "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287", - "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde", - "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf", - "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8", - "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16", - "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf", - "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809", - "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213", - "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f", - "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013", - "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b", - "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9", - "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5", - "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb", - "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df", - "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4", - "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439", - "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f", - "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22", - "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f", - "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5", - "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970", - "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009", - "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc", - "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", - "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" + "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3", + "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782", + "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75", + "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf", + "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7", + "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675", + "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1", + "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785", + "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4", + "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf", + "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5", + "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15", + "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca", + "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8", + "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac", + "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8", + "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef", + "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516", + "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700", + "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2", + "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8", + "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0", + "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676", + "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad", + "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155", + "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db", + "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd", + "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091", + "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602", + "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411", + "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93", + "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd", + "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec", + "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51", + "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7", + "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17", + "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d", + "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00", + "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923", + "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440", + "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32", + "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e", + "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1", + "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724", + "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a", + "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8", + "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2", + "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33", + "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b", + "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2", + "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632", + "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b", + "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2", + "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316", + "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74", + "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96", + "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866", + "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44", + "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950", + "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa", + "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c", + "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a", + "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd", + "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd", + "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9", + "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421", + "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2", + "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922", + "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4", + "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237", + "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642", + "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578" ], "index": "pypi", - "version": "==3.7.4.post0" + "version": "==3.8.1" + }, + "aiosignal": { + "hashes": [ + "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a", + "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.0" }, "async-timeout": { "hashes": [ - "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", - "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", + "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" ], - "markers": "python_full_version >= '3.5.3'", - "version": "==3.0.1" + "markers": "python_version >= '3.6'", + "version": "==4.0.2" }, "attrs": { "hashes": [ @@ -73,83 +116,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==21.4.0" }, - "cairocffi": { + "charset-normalizer": { "hashes": [ - "sha256:108a3a7cb09e203bdd8501d9baad91d786d204561bd71e9364e8b34897c47b91" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.0" - }, - "cairosvg": { - "hashes": [ - "sha256:98c276b7e4f0caf01e5c7176765c104ffa1aa1461d63b2053b04ab663cf7052b", - "sha256:b0b9929cf5dba005178d746a8036fcf0025550f498ca54db61873322384783bc" + "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], "markers": "python_version >= '3.5'", - "version": "==2.5.2" - }, - "cffi": { - "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "version": "==1.15.0" - }, - "chardet": { - "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.0.0" + "version": "==2.0.12" }, "colorama": { "hashes": [ @@ -159,22 +132,6 @@ "index": "pypi", "version": "==0.4.4" }, - "cssselect2": { - "hashes": [ - "sha256:3a83b2a68370c69c9cd3fcb88bbfaebe9d22edeef2c22d1ff3e1ed9c7fa45ed8", - "sha256:5b5d6dea81a5eb0c9ca39f116c8578dd413778060c94c1f51196371618909325" - ], - "markers": "python_version >= '3.7'", - "version": "==0.6.0" - }, - "defusedxml": { - "hashes": [ - "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", - "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.7.1" - }, "discord-py": { "git": "https://github.com/Rapptz/discord.py.git", "ref": "a14b43f2fda863ed6555374eb872bf014bdd1adf" @@ -193,11 +150,75 @@ }, "emoji": { "hashes": [ - "sha256:496f432058567985838c13d67dde84ca081614a8286c0b9cdc7d63dfa89d51a3", - "sha256:6b19b65da8d6f30551eead1705539cc0eadcd9e33a6ecbc421a29b87f96287eb" + "sha256:65c54533ea3c78f30d0729288998715f418d7467de89ec258a31c0ce8660a1d1" ], "index": "pypi", - "version": "==1.2.0" + "version": "==1.7.0" + }, + "frozenlist": { + "hashes": [ + "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e", + "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08", + "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b", + "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486", + "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78", + "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468", + "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1", + "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953", + "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3", + "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d", + "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a", + "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141", + "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08", + "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07", + "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa", + "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa", + "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868", + "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f", + "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b", + "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b", + "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1", + "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f", + "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478", + "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58", + "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01", + "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8", + "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d", + "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676", + "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274", + "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab", + "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8", + "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24", + "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a", + "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2", + "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f", + "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f", + "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93", + "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1", + "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51", + "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846", + "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5", + "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d", + "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c", + "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e", + "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae", + "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02", + "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0", + "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b", + "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3", + "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b", + "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa", + "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a", + "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d", + "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed", + "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148", + "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9", + "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c", + "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2", + "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.0" }, "idna": { "hashes": [ @@ -216,22 +237,20 @@ "version": "==0.6.1" }, "lottie": { - "extras": [ - "pdf" - ], + "extras": [], "hashes": [ - "sha256:d3281d09a8db94da49f60267123b9e0473e3a51c282da2d5e61908d33843f1cc" + "sha256:d53e96265887aa9187c7c707fd612b3d52f38da64c81ea82297783efb47f7e3f" ], "index": "pypi", - "version": "==0.6.10" + "version": "==0.6.11" }, "motor": { "hashes": [ - "sha256:1196db507142ef8f00d953efa2f37b39335ef2d72af6ce4fbccfd870b65c5e9f", - "sha256:839c11a43897dbec8e5ba0e87a9c9b877239803126877b2efa5cef89aa6b687a" + "sha256:663473f4498f955d35db7b6f25651cb165514c247136f368b84419cb7635f6b8", + "sha256:961fdceacaae2c7236c939166f66415be81be8bbb762da528386738de3a0f509" ], "index": "pypi", - "version": "==2.4.0" + "version": "==2.5.1" }, "multidict": { "hashes": [ @@ -313,61 +332,8 @@ "index": "pypi", "version": "==2.6" }, - "pillow": { - "hashes": [ - "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e", - "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595", - "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512", - "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c", - "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477", - "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a", - "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4", - "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e", - "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5", - "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378", - "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a", - "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652", - "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7", - "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a", - "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a", - "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6", - "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165", - "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160", - "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331", - "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b", - "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458", - "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033", - "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8", - "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481", - "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58", - "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7", - "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3", - "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea", - "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34", - "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3", - "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8", - "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581", - "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244", - "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef", - "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0", - "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2", - "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97", - "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717" - ], - "markers": "python_version >= '3.7'", - "version": "==9.1.0" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "version": "==2.21" - }, "pymongo": { - "extras": [ - "srv" - ], + "extras": [], "hashes": [ "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", @@ -490,11 +456,11 @@ }, "python-dotenv": { "hashes": [ - "sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d", - "sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d" + "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f", + "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" ], "index": "pypi", - "version": "==0.18.0" + "version": "==0.20.0" }, "six": { "hashes": [ @@ -504,22 +470,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, - "tinycss2": { - "hashes": [ - "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf", - "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8" - ], - "markers": "python_version >= '3.6'", - "version": "==1.1.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", - "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" - ], - "markers": "python_version >= '3.7'", - "version": "==4.2.0" - }, "uvloop": { "hashes": [ "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450", @@ -542,13 +492,6 @@ "markers": "sys_platform != 'win32'", "version": "==0.16.0" }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, "yarl": { "hashes": [ "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", From ab8662b9b5625f2fbc1792252dc7c2851deb6ae3 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 23:02:29 +0800 Subject: [PATCH 1052/1221] complete deps --- Pipfile | 1 + Pipfile.lock | 15 ++++++++++++--- requirements.txt | 28 ++++++++++------------------ 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Pipfile b/Pipfile index 99d50e47e4..15e7bec22a 100644 --- a/Pipfile +++ b/Pipfile @@ -22,6 +22,7 @@ python-dateutil = "~=2.8.1" python-dotenv = "==0.20.0" uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} lottie = "==0.6.11" +typing-extensions = "==4.2.0" [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 3ce9602d5e..5b55df924e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "dea4d7675ab1e4a2c4d3f4f8ddcff27db7c0b3851a37e598b10dc429c2eaa5df" + "sha256": "8fca2257520573284e4ecaa536309d9e21bee280144e019b7ed97eae0160deee" }, "pipfile-spec": 6, "requires": {}, @@ -237,7 +237,6 @@ "version": "==0.6.1" }, "lottie": { - "extras": [], "hashes": [ "sha256:d53e96265887aa9187c7c707fd612b3d52f38da64c81ea82297783efb47f7e3f" ], @@ -333,7 +332,9 @@ "version": "==2.6" }, "pymongo": { - "extras": [], + "extras": [ + "srv" + ], "hashes": [ "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", @@ -470,6 +471,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, + "typing-extensions": { + "hashes": [ + "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", + "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" + ], + "index": "pypi", + "version": "==4.2.0" + }, "uvloop": { "hashes": [ "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450", diff --git a/requirements.txt b/requirements.txt index ecff8c80c8..f17ff47f04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,35 +6,27 @@ # -i https://pypi.org/simple - -aiohttp==3.7.4.post0 -async-timeout==3.0.1; python_full_version >= '3.5.3' +aiohttp==3.8.1 +aiosignal==1.2.0; python_version >= '3.6' +async-timeout==4.0.2; python_version >= '3.6' attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -cairocffi==1.3.0; python_version >= '3.7' -cairosvg==2.5.2; python_version >= '3.5' -cffi==1.15.0 -chardet==4.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' +charset-normalizer==2.0.12; python_version >= '3.5' colorama==0.4.4 -cssselect2==0.6.0; python_version >= '3.7' -defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' dnspython==2.2.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -emoji==1.2.0 +emoji==1.7.0 +frozenlist==1.3.0; python_version >= '3.7' git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord.py idna==3.3; python_version >= '3.5' isodate==0.6.1 -lottie[pdf]==0.6.10 -motor==2.4.0 +lottie==0.6.11 +motor==2.5.1 multidict==6.0.2; python_version >= '3.7' natural==0.2.0 parsedatetime==2.6 -pillow==9.1.0; python_version >= '3.7' -pycparser==2.21 pymongo[srv]==3.12.3 python-dateutil==2.8.2 -python-dotenv==0.18.0 +python-dotenv==0.20.0 six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -tinycss2==1.1.1; python_version >= '3.6' -typing-extensions==4.2.0; python_version >= '3.7' +typing-extensions==4.2.0 uvloop==0.16.0; sys_platform != 'win32' -webencodings==0.5.1 yarl==1.7.2; python_version >= '3.6' From 99a6a5ed917ccc11d6cd3e04db2c5c3f55e42925 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 23:06:37 +0800 Subject: [PATCH 1053/1221] Fix lottie req & bump ver --- Pipfile | 2 +- Pipfile.lock | 160 +++++++++++++++++++++++++++++++++++++++++++++-- bot.py | 2 +- pyproject.toml | 2 +- requirements.txt | 16 ++++- 5 files changed, 172 insertions(+), 10 deletions(-) diff --git a/Pipfile b/Pipfile index 15e7bec22a..6d1693e1a4 100644 --- a/Pipfile +++ b/Pipfile @@ -21,7 +21,7 @@ pymongo = {extras = ["srv"], version = "*"} # Required by motor python-dateutil = "~=2.8.1" python-dotenv = "==0.20.0" uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} -lottie = "==0.6.11" +lottie = {version = "==0.6.11", extras = ["pdf"]} typing-extensions = "==4.2.0" [scripts] diff --git a/Pipfile.lock b/Pipfile.lock index 5b55df924e..c0ceed0b44 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8fca2257520573284e4ecaa536309d9e21bee280144e019b7ed97eae0160deee" + "sha256": "2d6a4df6161cc1c6fb1c60ceb7bc39d5c4d5a7371564b1b8c2a517c22685d56c" }, "pipfile-spec": 6, "requires": {}, @@ -116,6 +116,75 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==21.4.0" }, + "cairocffi": { + "hashes": [ + "sha256:108a3a7cb09e203bdd8501d9baad91d786d204561bd71e9364e8b34897c47b91" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.0" + }, + "cairosvg": { + "hashes": [ + "sha256:98c276b7e4f0caf01e5c7176765c104ffa1aa1461d63b2053b04ab663cf7052b", + "sha256:b0b9929cf5dba005178d746a8036fcf0025550f498ca54db61873322384783bc" + ], + "version": "==2.5.2" + }, + "cffi": { + "hashes": [ + "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", + "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", + "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", + "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", + "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", + "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", + "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", + "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", + "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", + "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", + "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", + "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", + "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", + "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", + "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", + "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", + "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", + "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", + "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", + "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", + "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", + "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", + "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", + "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", + "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", + "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", + "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", + "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", + "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", + "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", + "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", + "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", + "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", + "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", + "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", + "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", + "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", + "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", + "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", + "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", + "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", + "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", + "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", + "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", + "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", + "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", + "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", + "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", + "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", + "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" + ], + "version": "==1.15.0" + }, "charset-normalizer": { "hashes": [ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", @@ -132,6 +201,22 @@ "index": "pypi", "version": "==0.4.4" }, + "cssselect2": { + "hashes": [ + "sha256:3a83b2a68370c69c9cd3fcb88bbfaebe9d22edeef2c22d1ff3e1ed9c7fa45ed8", + "sha256:5b5d6dea81a5eb0c9ca39f116c8578dd413778060c94c1f51196371618909325" + ], + "markers": "python_version >= '3.7'", + "version": "==0.6.0" + }, + "defusedxml": { + "hashes": [ + "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.7.1" + }, "discord-py": { "git": "https://github.com/Rapptz/discord.py.git", "ref": "a14b43f2fda863ed6555374eb872bf014bdd1adf" @@ -237,6 +322,9 @@ "version": "==0.6.1" }, "lottie": { + "extras": [ + "pdf" + ], "hashes": [ "sha256:d53e96265887aa9187c7c707fd612b3d52f38da64c81ea82297783efb47f7e3f" ], @@ -331,10 +419,59 @@ "index": "pypi", "version": "==2.6" }, - "pymongo": { - "extras": [ - "srv" + "pillow": { + "hashes": [ + "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e", + "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595", + "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512", + "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c", + "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477", + "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a", + "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4", + "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e", + "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5", + "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378", + "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a", + "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652", + "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7", + "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a", + "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a", + "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6", + "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165", + "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160", + "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331", + "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b", + "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458", + "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033", + "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8", + "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481", + "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58", + "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7", + "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3", + "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea", + "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34", + "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3", + "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8", + "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581", + "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244", + "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef", + "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0", + "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2", + "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97", + "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717" ], + "markers": "python_version >= '3.7'", + "version": "==9.1.0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pymongo": { + "extras": [], "hashes": [ "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", @@ -471,6 +608,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, + "tinycss2": { + "hashes": [ + "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf", + "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8" + ], + "markers": "python_version >= '3.6'", + "version": "==1.1.1" + }, "typing-extensions": { "hashes": [ "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", @@ -501,6 +646,13 @@ "markers": "sys_platform != 'win32'", "version": "==0.16.0" }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, "yarl": { "hashes": [ "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", diff --git a/bot.py b/bot.py index 4fb57f1886..55670607fe 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev10" +__version__ = "4.0.0-dev11" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 9d08c1d2b2..80217af8fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev10' +version = '4.0.0-dev11' description = "Modmail is similar to Reddit's 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 = [ diff --git a/requirements.txt b/requirements.txt index f17ff47f04..541a8411c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,23 +10,33 @@ aiohttp==3.8.1 aiosignal==1.2.0; python_version >= '3.6' async-timeout==4.0.2; python_version >= '3.6' attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -charset-normalizer==2.0.12; python_version >= '3.5' +cairocffi==1.3.0; python_version >= '3.7' +cairosvg==2.5.2 +cffi==1.15.0 +charset-normalizer==2.0.12; python_version >= '3.5' colorama==0.4.4 +cssselect2==0.6.0; python_version >= '3.7' +defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' dnspython==2.2.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' emoji==1.7.0 frozenlist==1.3.0; python_version >= '3.7' +git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord-py git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord.py idna==3.3; python_version >= '3.5' isodate==0.6.1 -lottie==0.6.11 +lottie[pdf]==0.6.11 motor==2.5.1 multidict==6.0.2; python_version >= '3.7' natural==0.2.0 parsedatetime==2.6 -pymongo[srv]==3.12.3 +pillow==9.1.0; python_version >= '3.7' +pycparser==2.21 +pymongo==3.12.3 python-dateutil==2.8.2 python-dotenv==0.20.0 six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +tinycss2==1.1.1; python_version >= '3.6' typing-extensions==4.2.0 uvloop==0.16.0; sys_platform != 'win32' +webencodings==0.5.1 yarl==1.7.2; python_version >= '3.6' From c6ed564db714c3296a74046ac98df2ef27bcc2c8 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 23:17:51 +0800 Subject: [PATCH 1054/1221] shift to devpkgs --- Pipfile | 2 +- Pipfile.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Pipfile b/Pipfile index 6d1693e1a4..e9b1e58f76 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ verify_ssl = true bandit = "~=1.7.0" black = "==22.3.0" pylint = "~=2.9.3" +typing-extensions = "==4.2.0" [packages] aiohttp = "==3.8.1" @@ -22,7 +23,6 @@ python-dateutil = "~=2.8.1" python-dotenv = "==0.20.0" uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} lottie = {version = "==0.6.11", extras = ["pdf"]} -typing-extensions = "==4.2.0" [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index c0ceed0b44..8dff48027a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2d6a4df6161cc1c6fb1c60ceb7bc39d5c4d5a7371564b1b8c2a517c22685d56c" + "sha256": "b6bc030eedf7606f6ceeda324e896c29d5771108afa224c712d4c92d81875899" }, "pipfile-spec": 6, "requires": {}, @@ -616,14 +616,6 @@ "markers": "python_version >= '3.6'", "version": "==1.1.1" }, - "typing-extensions": { - "hashes": [ - "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", - "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" - ], - "index": "pypi", - "version": "==4.2.0" - }, "uvloop": { "hashes": [ "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450", @@ -985,6 +977,14 @@ "markers": "python_version < '3.11'", "version": "==2.0.1" }, + "typing-extensions": { + "hashes": [ + "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", + "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" + ], + "index": "pypi", + "version": "==4.2.0" + }, "wrapt": { "hashes": [ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" From fff100dd11ee259f1897d2864e8ad03bac58ca97 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Thu, 28 Apr 2022 23:19:11 +0800 Subject: [PATCH 1055/1221] update reqs.txt --- requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 541a8411c2..e7223057ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,6 @@ defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, dnspython==2.2.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' emoji==1.7.0 frozenlist==1.3.0; python_version >= '3.7' -git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord-py git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord.py idna==3.3; python_version >= '3.5' isodate==0.6.1 @@ -36,7 +35,6 @@ python-dateutil==2.8.2 python-dotenv==0.20.0 six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' tinycss2==1.1.1; python_version >= '3.6' -typing-extensions==4.2.0 uvloop==0.16.0; sys_platform != 'win32' webencodings==0.5.1 yarl==1.7.2; python_version >= '3.6' From 17b2f89e9ae402b27516c86562a7d67c0008ed7f Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 22:23:47 +0800 Subject: [PATCH 1056/1221] add use_random_channel_name, resolve #3143 --- CHANGELOG.md | 1 + bot.py | 9 +++++++-- core/config.py | 2 ++ core/config_help.json | 25 +++++++++++++++++++------ pyproject.toml | 2 +- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e97b127b95..fa5d0ad75a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `plain_snippets` config to force all snippets to be plain. ([GH #3083](https://github.com/kyb3r/modmail/issues/3083)) - `?fpareply` and `?fpreply` to reply to messages with variables plainly. - `use_nickname_channel_name` config to use nicknames instead of usernames for channel names. ([GH #3112](https://github.com/kyb3r/modmail/issues/3112)) +- `use_random_channel_name` config to use random nicknames vaguely tied to user ID. It is unable to be computed in reverse. ([GH #3143](https://github.com/kyb3r/modmail/issues/3143) - `show_log_url_button` config to show Log URL button. ([GH #3122](https://github.com/kyb3r/modmail/issues/3122)) - Select menus for certain paginators. - `Title` field in `?logs`. ([GH #3142](https://github.com/kyb3r/modmail/issues/3142)) diff --git a/bot.py b/bot.py index 55670607fe..bcd6acfee9 100644 --- a/bot.py +++ b/bot.py @@ -1,8 +1,9 @@ -__version__ = "4.0.0-dev11" +__version__ = "4.0.0-dev12" import asyncio import copy +import hashlib import logging import os import re @@ -1690,7 +1691,11 @@ def format_channel_name(self, author, exclude_channel=None, force_null=False): if force_null: name = new_name = "null" else: - if self.config["use_user_id_channel_name"]: + if self.config["use_random_channel_name"]: + to_hash = self.token.split(".")[-1] + str(author.id) + digest = hashlib.md5(to_hash.encode("utf8")) + name = new_name = digest.hexdigest()[-8:] + elif self.config["use_user_id_channel_name"]: name = new_name = str(author.id) elif self.config["use_timestamp_channel_name"]: name = new_name = author.created_at.isoformat(sep="-", timespec="minutes") diff --git a/core/config.py b/core/config.py index 14f86ecd2e..6cff7f0bd7 100644 --- a/core/config.py +++ b/core/config.py @@ -53,6 +53,7 @@ class ConfigManager: "use_user_id_channel_name": False, "use_timestamp_channel_name": False, "use_nickname_channel_name": False, + "use_random_channel_name": False, "recipient_thread_close": False, "thread_show_roles": True, "thread_show_account_age": True, @@ -187,6 +188,7 @@ class ConfigManager: "use_user_id_channel_name", "use_timestamp_channel_name", "use_nickname_channel_name", + "use_random_channel_name", "user_typing", "mod_typing", "reply_without_command", diff --git a/core/config_help.json b/core/config_help.json index 342652456d..e7ebb9590d 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -106,8 +106,8 @@ ], "notes": [ "This config is suitable for servers in Server Discovery to comply with channel name restrictions.", - "This cannot be applied with `use_timestamp_channel_name` or `use_nickname_channel_name`.", - "See also: `use_timestamp_channel_name`, `use_nickname_channel_name`." + "This cannot be applied with `use_timestamp_channel_name`, `use_random_channel_name` or `use_nickname_channel_name`.", + "See also: `use_timestamp_channel_name`, `use_nickname_channel_name`, `use_random_channel_name`." ] }, "use_timestamp_channel_name": { @@ -119,8 +119,8 @@ ], "notes": [ "This config is suitable for servers in Server Discovery to comply with channel name restrictions.", - "This cannot be applied with `use_user_id_channel_name` or `use_nickname_channel_name`.", - "See also: `use_user_id_channel_name`, `use_nickname_channel_name`." + "This cannot be applied with `use_user_id_channel_name`, `use_random_channel_name` or `use_nickname_channel_name`.", + "See also: `use_user_id_channel_name`, `use_nickname_channel_name`, `use_random_channel_name`." ] }, "use_nickname_channel_name": { @@ -132,8 +132,21 @@ ], "notes": [ "This config is suitable for servers in Server Discovery to comply with channel name restrictions.", - "This cannot be applied with `use_timestamp_channel_name` or `use_user_id_channel_name`.", - "See also: `use_timestamp_channel_name`, `use_user_id_channel_name`." + "This cannot be applied with `use_timestamp_channel_name`, `use_random_channel_name` or `use_user_id_channel_name`.", + "See also: `use_timestamp_channel_name`, `use_user_id_channel_name`, `use_random_channel_name`." + ] + }, + "use_random_channel_name": { + "default": "No", + "description": "When this is set to `yes`, new thread channels will be named with random characters tied to their user ID.", + "examples": [ + "`{prefix}config set use_random_channel_name yes`", + "`{prefix}config set use_random_channel_name no`" + ], + "notes": [ + "This config is suitable for servers in Server Discovery to comply with channel name restrictions.", + "This cannot be applied with `use_timestamp_channel_name`, `use_nickname_channel_name`, or `use_user_id_channel_name`.", + "See also: `use_timestamp_channel_name`, `use_user_id_channel_name`, `use_nickname_channel_name`." ] }, "mod_typing": { diff --git a/pyproject.toml b/pyproject.toml index 80217af8fb..aafdcd0d49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev11' +version = '4.0.0-dev12' description = "Modmail is similar to Reddit's 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 2214cbe1b7a79895367292b43d27965ed9d3242c Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 22:33:41 +0800 Subject: [PATCH 1057/1221] bump dpy@e9c7c09ebfe780d8f9de1e9e4d3c2dddf85eccfd --- Pipfile | 2 +- Pipfile.lock | 20 +++++++++++--------- requirements.txt | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Pipfile b/Pipfile index e9b1e58f76..88d49f59fd 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,7 @@ typing-extensions = "==4.2.0" [packages] aiohttp = "==3.8.1" colorama = "~=0.4.4" # Doesn't officially support Python 3.9 yet, v0.4.5 will support 3.9 -"discord.py" = {ref = "a14b43f2fda863ed6555374eb872bf014bdd1adf", git = "https://github.com/Rapptz/discord.py.git"} +"discord.py" = {ref = "e9c7c09ebfe780d8f9de1e9e4d3c2dddf85eccfd", git = "https://github.com/Rapptz/discord.py.git"} emoji = "==1.7.0" isodate = "~=0.6.0" motor = "==2.5.1" diff --git a/Pipfile.lock b/Pipfile.lock index 8dff48027a..163d09bdcd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b6bc030eedf7606f6ceeda324e896c29d5771108afa224c712d4c92d81875899" + "sha256": "4035fc1c98c21d437f41fcd2f32ca38089131e4a60ef0f4453caf4019dae7bd3" }, "pipfile-spec": 6, "requires": {}, @@ -190,7 +190,7 @@ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==2.0.12" }, "colorama": { @@ -219,11 +219,11 @@ }, "discord-py": { "git": "https://github.com/Rapptz/discord.py.git", - "ref": "a14b43f2fda863ed6555374eb872bf014bdd1adf" + "ref": "e9c7c09ebfe780d8f9de1e9e4d3c2dddf85eccfd" }, "discord.py": { "git": "https://github.com/Rapptz/discord.py.git", - "ref": "a14b43f2fda863ed6555374eb872bf014bdd1adf" + "ref": "e9c7c09ebfe780d8f9de1e9e4d3c2dddf85eccfd" }, "dnspython": { "hashes": [ @@ -310,7 +310,7 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==3.3" }, "isodate": { @@ -471,7 +471,9 @@ "version": "==2.21" }, "pymongo": { - "extras": [], + "extras": [ + "srv" + ], "hashes": [ "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", @@ -772,11 +774,11 @@ }, "click": { "hashes": [ - "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", - "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72" + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" ], "markers": "python_version >= '3.7'", - "version": "==8.1.2" + "version": "==8.1.3" }, "colorama": { "hashes": [ diff --git a/requirements.txt b/requirements.txt index e7223057ad..edc784029b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, dnspython==2.2.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' emoji==1.7.0 frozenlist==1.3.0; python_version >= '3.7' -git+https://github.com/Rapptz/discord.py.git@a14b43f2fda863ed6555374eb872bf014bdd1adf#egg=discord.py +git+https://github.com/Rapptz/discord.py.git@e9c7c09ebfe780d8f9de1e9e4d3c2dddf85eccfd#egg=discord.py idna==3.3; python_version >= '3.5' isodate==0.6.1 lottie[pdf]==0.6.11 From 97c70bfb475cccd2453d8e73486e377f26706920 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 22:40:00 +0800 Subject: [PATCH 1058/1221] fix typing --- bot.py | 6 +++--- cogs/modmail.py | 4 ++-- core/thread.py | 2 +- core/utils.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bot.py b/bot.py index bcd6acfee9..c8eb75512c 100644 --- a/bot.py +++ b/bot.py @@ -1227,7 +1227,7 @@ async def on_typing(self, channel, user, _): thread = await self.threads.find(recipient=user) if thread: - await thread.channel.trigger_typing() + await thread.channel.typing() else: if not self.config.get("mod_typing"): return @@ -1237,7 +1237,7 @@ async def on_typing(self, channel, user, _): for user in thread.recipients: if await self.is_blocked(user): continue - await user.trigger_typing() + await user.typing() async def handle_reaction_events(self, payload): user = self.get_user(payload.user_id) @@ -1508,7 +1508,7 @@ async def on_error(self, event_method, *args, **kwargs): async def on_command_error(self, context, exception): if isinstance(exception, (commands.BadArgument, commands.BadUnionArgument)): - await context.trigger_typing() + await context.typing() await context.send(embed=discord.Embed(color=self.error_color, description=str(exception))) elif isinstance(exception, commands.CommandNotFound): logger.warning("CommandNotFound: %s", exception) diff --git a/cogs/modmail.py b/cogs/modmail.py index bb8c90e29e..f1c6c20ee8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1158,7 +1158,7 @@ async def logs(self, ctx, *, user: User = None): `user` may be a user ID, mention, or name. """ - await ctx.trigger_typing() + await ctx.typing() if not user: thread = ctx.thread @@ -1268,7 +1268,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): Provide a `limit` to specify the maximum number of logs the bot should find. """ - await ctx.trigger_typing() + await ctx.typing() entries = await self.bot.api.search_by_text(query, limit) diff --git a/core/thread.py b/core/thread.py index 7edfbc1d39..e39076d702 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1134,7 +1134,7 @@ def lottie_to_png(data): logger.info("Sending a message to %s when DM disabled is set.", self.recipient) try: - await destination.trigger_typing() + await destination.typing() except discord.NotFound: logger.warning("Channel not found.") raise diff --git a/core/utils.py b/core/utils.py index 9b56cbe31d..425b245511 100644 --- a/core/utils.py +++ b/core/utils.py @@ -397,8 +397,8 @@ def format_description(i, names): def trigger_typing(func): @functools.wraps(func) async def wrapper(self, ctx: commands.Context, *args, **kwargs): - await ctx.trigger_typing() - return await func(self, ctx, *args, **kwargs) + async with ctx.typing(): + return await func(self, ctx, *args, **kwargs) return wrapper From afb9e3a935c5fe6c11911d2c4a7f1202be6e0a19 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 22:40:42 +0800 Subject: [PATCH 1059/1221] remove deprecated decorators.py --- core/decorators.py | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 core/decorators.py diff --git a/core/decorators.py b/core/decorators.py deleted file mode 100644 index 0107a6b2d6..0000000000 --- a/core/decorators.py +++ /dev/null @@ -1,12 +0,0 @@ -import warnings - -from core.utils import trigger_typing as _trigger_typing - - -def trigger_typing(func): - warnings.warn( - "trigger_typing has been moved to core.utils.trigger_typing, this will be removed.", - DeprecationWarning, - stacklevel=2, - ) - return _trigger_typing(func) From 8d1e4f70fed97f9f49526d24290c3481597a76e7 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 22:41:30 +0800 Subject: [PATCH 1060/1221] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5d0ad75a..f592f3278c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Bump emoji to v1.7.0 - Bump aiohttp to v3.8.1 - Bump lottie to v0.6.11 +- Remove deprecated `core/decorators.py` from v3.3.0 # v3.10.3 From ecc92e4bbf44d0f44d2df902e16a30734320070e Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 22:48:20 +0800 Subject: [PATCH 1061/1221] fix <3.9 features --- bot.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index c8eb75512c..f1dc8b6b59 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev12" +__version__ = "4.0.0-dev13" import asyncio @@ -995,7 +995,7 @@ async def get_contexts(self, message, *, cls=commands.Context): # This needs to be done before checking for aliases since # snippets can have multiple words. try: - snippet_text = self.snippets[message.content.removeprefix(invoked_prefix)] + snippet_text = self.snippets[message.content.strip(invoked_prefix)] except KeyError: snippet_text = None diff --git a/pyproject.toml b/pyproject.toml index aafdcd0d49..c69f704341 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev12' +version = '4.0.0-dev13' description = "Modmail is similar to Reddit's 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 e1efa8c2f65ba3c09a594604dba0c98eed942321 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 22:53:33 +0800 Subject: [PATCH 1062/1221] update jobs to cover 3.7-3.10 and all platforms --- .github/workflows/lints.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 091e012dd9..1be351818e 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -5,20 +5,21 @@ on: [push, pull_request] jobs: code-style: -# runs-on: ${{ matrix.os }} -# strategy: -# fail-fast: false -# matrix: -# os: [ubuntu-latest, windows-latest, macOS-latest] -# python-version: [3.6, 3.7] + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + python-version: [3.7, 3.8, 3.9, 3.10] + + name: Python ${{ matrix.python-version }} Test - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 with: - python-version: 3.9 + python-version: ${{ matrix.python-version }} + architecture: x64 - name: Install dependencies run: | python -m pip install --upgrade pip pipenv From 927fd37b0e4b8d6c76ed233968e38f0a8c860d70 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 22:56:04 +0800 Subject: [PATCH 1063/1221] fix linting file --- .github/workflows/lints.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 1be351818e..3ededb294a 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -4,12 +4,11 @@ on: [push, pull_request] jobs: code-style: - - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] - python-version: [3.7, 3.8, 3.9, 3.10] + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + python-version: [3.7, 3.8, 3.9, 3.10] name: Python ${{ matrix.python-version }} Test From 0a7f86281aa327767d5168cb9b0ce541c4577343 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 22:57:27 +0800 Subject: [PATCH 1064/1221] fix lint gh-action --- .github/workflows/lints.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 3ededb294a..fddccac8f1 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -8,9 +8,9 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - python-version: [3.7, 3.8, 3.9, 3.10] + python-version: ['3.7', '3.8', '3.9', '3.10'] - name: Python ${{ matrix.python-version }} Test + name: Python ${{ matrix.python-version }} on ${{ matrix.os }} steps: - uses: actions/checkout@v3 From daa4ce7e357163c270c1f10ed5478b25e2f5b0a4 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 23:00:40 +0800 Subject: [PATCH 1065/1221] only formally support Python 3.8+ --- .github/workflows/lints.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index fddccac8f1..a7418930be 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10'] name: Python ${{ matrix.python-version }} on ${{ matrix.os }} diff --git a/README.md b/README.md index 4487438a5c..1558a04755 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ If you don't want to go through the trouble of setting up your very own Modmail ### Locally -Local hosting of Modmail is also possible. First, you will need [`Python 3.8`](https://www.python.org/downloads/release/python-376/). +Local hosting of Modmail is also possible. First, you will need at least [`Python 3.8`](https://www.python.org/downloads/release/python-376/). Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. From 5a32966c10b96c31febcdacfddbe851aed7f40b4 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 23:02:45 +0800 Subject: [PATCH 1066/1221] fix type annotation for PY3.8 --- cogs/modmail.py | 4 ++-- runtime.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 runtime.txt diff --git a/cogs/modmail.py b/cogs/modmail.py index f1c6c20ee8..a45b44e573 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -2,7 +2,7 @@ import re from datetime import datetime, timezone from itertools import zip_longest -from typing import Optional, Union +from typing import Optional, Union, List, Tuple from types import SimpleNamespace import discord @@ -249,7 +249,7 @@ async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_conte ) return await ctx.send(embed=embed) - def _fix_aliases(self, snippet_being_deleted: str) -> tuple[list[str]]: + def _fix_aliases(self, snippet_being_deleted: str) -> Tuple[List[str]]: """ Remove references to the snippet being deleted from aliases. diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000000..30e81e3130 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.10.3 From b8df285b6826d4f3d4dd59f3415ab0e228c564be Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 23:16:18 +0800 Subject: [PATCH 1067/1221] improve multi-page pagination dropdown and titles --- cogs/modmail.py | 33 +++++++++++++++++++++------------ cogs/utility.py | 8 ++++++-- core/utils.py | 4 ++-- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index a45b44e573..19a6842dc7 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1637,8 +1637,6 @@ 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="")] - roles = [] users = [] now = ctx.message.created_at @@ -1700,43 +1698,54 @@ async def blocked(self, ctx): if role: roles.append((role.mention, reason)) + user_embeds = [discord.Embed(title="Blocked Users", color=self.bot.main_color, description="")] + if users: - embed = embeds[0] + embed = user_embeds[0] for mention, reason in users: line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: embed = discord.Embed( - title="Blocked Users (Continued)", + title="Blocked Users", color=self.bot.main_color, description=line, ) - embeds.append(embed) + user_embeds.append(embed) else: embed.description += line else: - embeds[0].description = "Currently there are no blocked users." + user_embeds[0].description = "Currently there are no blocked users." + + if len(user_embeds) > 1: + for n, em in enumerate(user_embeds): + em.title = f'{em.title} [{n + 1}]' - embeds.append(discord.Embed(title="Blocked Roles", color=self.bot.main_color, description="")) + role_embeds = [discord.Embed(title="Blocked Roles", color=self.bot.main_color, description="")] if roles: - embed = embeds[-1] + embed = role_embeds[-1] for mention, reason in roles: line = mention + f" - {reason or 'No Reason Provided'}\n" if len(embed.description) + len(line) > 2048: + role_embeds[-1].set_author() embed = discord.Embed( - title="Blocked Roles (Continued)", + title="Blocked Roles", color=self.bot.main_color, description=line, ) - embeds.append(embed) + role_embeds.append(embed) else: embed.description += line else: - embeds[-1].description = "Currently there are no blocked roles." + role_embeds[-1].description = "Currently there are no blocked roles." - session = EmbedPaginatorSession(ctx, *embeds) + if len(role_embeds) > 1: + for n, em in enumerate(role_embeds): + em.title = f'{em.title} [{n + 1}]' + + session = EmbedPaginatorSession(ctx, *user_embeds, *role_embeds) await session.run() diff --git a/cogs/utility.py b/cogs/utility.py index 23827aa0c4..538ddc18e1 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -89,15 +89,19 @@ 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" - embed.set_author(name=name + continued, icon_url=bot.user.display_avatar.url) + embed.set_author(name=name, icon_url=bot.user.display_avatar.url) embed.set_footer( text=f'Type "{prefix}{self.command_attrs["name"]} command" ' "for more info on a specific command." ) embeds.append(embed) + + if len(embeds) > 1: + for n, em in enumerate(embeds): + em.set_author(name=f'{em.author.name} [{n + 1}]', icon_url=em.author.icon_url) + return embeds def process_help_msg(self, help_: str): diff --git a/core/utils.py b/core/utils.py index 425b245511..32f12c72e8 100644 --- a/core/utils.py +++ b/core/utils.py @@ -397,8 +397,8 @@ def format_description(i, names): def trigger_typing(func): @functools.wraps(func) async def wrapper(self, ctx: commands.Context, *args, **kwargs): - async with ctx.typing(): - return await func(self, ctx, *args, **kwargs) + await ctx.typing() + return await func(self, ctx, *args, **kwargs) return wrapper From 77489be6a21cf4befeab1dc7fa2c2d2098104cbc Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 23:16:44 +0800 Subject: [PATCH 1068/1221] bump version --- bot.py | 2 +- cogs/modmail.py | 4 ++-- cogs/utility.py | 2 +- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index f1dc8b6b59..a506addfd9 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev13" +__version__ = "4.0.0-dev14" import asyncio diff --git a/cogs/modmail.py b/cogs/modmail.py index 19a6842dc7..deb29d160d 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1719,7 +1719,7 @@ async def blocked(self, ctx): if len(user_embeds) > 1: for n, em in enumerate(user_embeds): - em.title = f'{em.title} [{n + 1}]' + em.title = f"{em.title} [{n + 1}]" role_embeds = [discord.Embed(title="Blocked Roles", color=self.bot.main_color, description="")] @@ -1743,7 +1743,7 @@ async def blocked(self, ctx): if len(role_embeds) > 1: for n, em in enumerate(role_embeds): - em.title = f'{em.title} [{n + 1}]' + em.title = f"{em.title} [{n + 1}]" session = EmbedPaginatorSession(ctx, *user_embeds, *role_embeds) diff --git a/cogs/utility.py b/cogs/utility.py index 538ddc18e1..0e86934af2 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -100,7 +100,7 @@ async def format_cog_help(self, cog, *, no_cog=False): if len(embeds) > 1: for n, em in enumerate(embeds): - em.set_author(name=f'{em.author.name} [{n + 1}]', icon_url=em.author.icon_url) + em.set_author(name=f"{em.author.name} [{n + 1}]", icon_url=em.author.icon_url) return embeds diff --git a/pyproject.toml b/pyproject.toml index c69f704341..56f30163c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev13' +version = '4.0.0-dev14' description = "Modmail is similar to Reddit's 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 454bdae898da6e431783b1f14d2ae8b51cdf2580 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 3 May 2022 23:31:10 +0800 Subject: [PATCH 1069/1221] fix typos --- CHANGELOG.md | 3 ++- cogs/utility.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f592f3278c..c096d05d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `plain_snippets` config to force all snippets to be plain. ([GH #3083](https://github.com/kyb3r/modmail/issues/3083)) - `?fpareply` and `?fpreply` to reply to messages with variables plainly. - `use_nickname_channel_name` config to use nicknames instead of usernames for channel names. ([GH #3112](https://github.com/kyb3r/modmail/issues/3112)) -- `use_random_channel_name` config to use random nicknames vaguely tied to user ID. It is unable to be computed in reverse. ([GH #3143](https://github.com/kyb3r/modmail/issues/3143) +- `use_random_channel_name` config to use random nicknames vaguely tied to user ID. It is unable to be computed in reverse. ([GH #3143](https://github.com/kyb3r/modmail/issues/3143)) - `show_log_url_button` config to show Log URL button. ([GH #3122](https://github.com/kyb3r/modmail/issues/3122)) - Select menus for certain paginators. - `Title` field in `?logs`. ([GH #3142](https://github.com/kyb3r/modmail/issues/3142)) @@ -33,6 +33,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/kyb3r/modmail/issues/3048)) - Use discord relative timedeltas. ([GH #3046](https://github.com/kyb3r/modmail/issues/3046)) - Use discord native buttons for all paginator sessions. +- `?help` and `?blocked` paginator sessions now have better multi-page UI. ### Fixed diff --git a/cogs/utility.py b/cogs/utility.py index 0e86934af2..0baa82cfcd 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -39,7 +39,7 @@ class ModmailHelpCommand(commands.HelpCommand): async def command_callback(self, ctx, *, command=None): - """Ovrwrites original command_callback to ensure `help` without any arguments + """Overwrites original command_callback to ensure `help` without any arguments returns with checks, `help all` returns without checks""" if command is None: self.verify_checks = True From e3571cdb3a75a27898c1622a3d78a8285f4cc90a Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 4 May 2022 00:33:27 +0800 Subject: [PATCH 1070/1221] update gtk message --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index a506addfd9..14ac7b4d8f 100644 --- a/bot.py +++ b/bot.py @@ -1742,7 +1742,7 @@ def main(): ) else: logger.error( - "Unable to import cairosvg, install GTK Installer for Windows: https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases/latest" + "Unable to import cairosvg, install GTK Installer for Windows and restart your system (https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases/latest)" ) else: logger.error( From fa135eb53b278148fb411a2b1282c2fb28efad10 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <hidzrie@gmail.com> Date: Tue, 7 Jun 2022 00:52:22 +0000 Subject: [PATCH 1071/1221] Improve and error handling for update and autoupdate features. --- bot.py | 65 ++++++++++++++++++--------------- cogs/utility.py | 38 +++++++++++++------- core/clients.py | 96 +++++++++++++++++++++++++++++++++++++------------ 3 files changed, 136 insertions(+), 63 deletions(-) diff --git a/bot.py b/bot.py index a506addfd9..5db1aab550 100644 --- a/bot.py +++ b/bot.py @@ -17,7 +17,7 @@ import discord import isodate -from aiohttp import ClientSession +from aiohttp import ClientSession, ClientResponseError from discord.ext import commands, tasks from discord.ext.commands.view import StringView from emoji import UNICODE_EMOJI @@ -630,6 +630,8 @@ async def on_ready(self): ) logger.warning("If the external servers are valid, you may ignore this message.") + self.post_metadata.start() + self.autoupdate.start() self._started = True async def convert_emoji(self, name: str) -> str: @@ -1581,6 +1583,7 @@ async def before_post_metadata(self): await self.wait_for_connected() if not self.config.get("data_collection") or not self.guild: self.post_metadata.cancel() + return logger.debug("Starting metadata loop.") logger.line("debug") @@ -1591,44 +1594,50 @@ async def autoupdate(self): latest = changelog.latest_version if self.version < parse_version(latest.version): - if self.hosting_method == HostingMethod.HEROKU: + try: + # update fork if gh_token exists data = await self.api.update_repository() + except InvalidConfigError: + data = {} + except ClientResponseError as exc: + logger.error(f"Autoupdate failed! Status {exc.status}.") + logger.error(f"Message: {exc.message}") + self.autoupdate.cancel() + return + if self.hosting_method == HostingMethod.HEROKU: + commit_data = data.get("data") + if not commit_data: + return - embed = discord.Embed(color=self.main_color) + logger.info("Bot has been updated.") + + if not self.config["update_notifications"]: + return - commit_data = data["data"] + embed = discord.Embed(color=self.main_color) + message = commit_data["commit"]["message"] + html_url = commit_data["html_url"] + short_sha = commit_data["sha"][:6] user = data["user"] + embed.add_field( + name="Merge Commit", + value=f"[`{short_sha}`]({html_url}) " f"{message} - {user['username']}", + ) embed.set_author( name=user["username"] + " - Updating Bot", icon_url=user["avatar_url"], url=user["url"], ) - embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + embed.set_footer(text=f"Updating Modmail v{self.version} -> v{latest.version}") embed.description = latest.description for name, value in latest.fields.items(): embed.add_field(name=name, value=value) - if commit_data: - message = commit_data["commit"]["message"] - html_url = commit_data["html_url"] - short_sha = commit_data["sha"][:6] - embed.add_field( - name="Merge Commit", - value=f"[`{short_sha}`]({html_url}) " f"{message} - {user['username']}", - ) - logger.info("Bot has been updated.") - channel = self.log_channel - if self.config["update_notifications"]: - await channel.send(embed=embed) + channel = self.update_channel + await channel.send(embed=embed) else: - try: - # update fork if gh_token exists - await self.api.update_repository() - except InvalidConfigError: - pass - command = "git pull" proc = await asyncio.create_subprocess_shell( command, @@ -1642,7 +1651,7 @@ async def autoupdate(self): if err and not res: logger.warning(f"Autoupdate failed: {err}") - self.autoupdate_loop.cancel() + self.autoupdate.cancel() return elif res != "Already up to date.": @@ -1659,7 +1668,7 @@ async def autoupdate(self): description="If you do not have an auto-restart setup, please manually start the bot.", color=self.main_color, ) - embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + embed.set_footer(text=f"Updating Modmail v{self.version} -> v{latest.version}") if self.config["update_notifications"]: await channel.send(embed=embed) return await self.close() @@ -1671,16 +1680,16 @@ async def before_autoupdate(self): if self.config.get("disable_autoupdates"): logger.warning("Autoupdates disabled.") - self.autoupdate_loop.cancel() + self.autoupdate.cancel() if self.hosting_method == HostingMethod.DOCKER: logger.warning("Autoupdates disabled as using Docker.") - self.autoupdate_loop.cancel() + self.autoupdate.cancel() if not self.config.get("github_token") and self.hosting_method == HostingMethod.HEROKU: logger.warning("GitHub access token not found.") logger.warning("Autoupdates disabled.") - self.autoupdate_loop.cancel() + self.autoupdate.cancel() def format_channel_name(self, author, exclude_channel=None, force_null=False): """Sanitises a username for use with text channel names diff --git a/cogs/utility.py b/cogs/utility.py index 0e86934af2..5d35a20211 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1931,7 +1931,7 @@ async def github(self, ctx): async def update(self, ctx, *, flag: str = ""): """ Update Modmail. - To stay up-to-date with the latest commit rom GitHub, specify "force" as the flag. + To stay up-to-date with the latest commit from GitHub, specify "force" as the flag. """ changelog = await Changelog.from_url(self.bot) @@ -1939,7 +1939,7 @@ async def update(self, ctx, *, flag: str = ""): desc = ( f"The latest version is [`{self.bot.version}`]" - "(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)" + "(https://github.com/kyb3r/modmail/blob/master/bot.py#L1)" ) if self.bot.version >= parse_version(latest.version) and flag.lower() != "force": @@ -1951,16 +1951,35 @@ async def update(self, ctx, *, flag: str = ""): embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) await ctx.send(embed=embed) else: - if self.bot.hosting_method == HostingMethod.HEROKU: + try: + # update fork if gh_token exists data = await self.bot.api.update_repository() + except InvalidConfigError: + data = {} + except ClientResponseError as exc: + embed = discord.Embed( + title="Update failed", + description=f"Error status {exc.status}. {exc.message}", + color=self.bot.error_color, + ) + return await ctx.send(embed=embed) + + if self.bot.hosting_method == HostingMethod.HEROKU: + if not data: + # invalid gh_token + embed = discord.Embed( + title="Update failed", + description="Invalid Github token.", + color=self.bot.error_color, + ) + return await ctx.send(embed=embed) commit_data = data["data"] user = data["user"] - if commit_data and commit_data.get("html_url"): embed = discord.Embed(color=self.bot.main_color) - embed.set_footer(text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}") + embed.set_footer(text=f"Updating Modmail v{self.bot.version} -> v{latest.version}") embed.set_author( name=user["username"] + " - Updating bot", @@ -1978,21 +1997,14 @@ async def update(self, ctx, *, flag: str = ""): else: embed = discord.Embed( title="Already up to date", - description="No further updates required", + description="No further updates required.", color=self.bot.main_color, ) embed.set_footer(text="Force update") embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) await ctx.send(embed=embed) else: - # update fork if gh_token exists - try: - await self.bot.api.update_repository() - except InvalidConfigError: - pass - command = "git pull" - proc = await asyncio.create_subprocess_shell( command, stderr=PIPE, diff --git a/core/clients.py b/core/clients.py index a9722776f2..862c665310 100644 --- a/core/clients.py +++ b/core/clients.py @@ -1,7 +1,7 @@ import secrets import sys from json import JSONDecodeError -from typing import Union, Optional +from typing import Any, Dict, Union, Optional import discord from discord import Member, DMChannel, TextChannel, Message @@ -19,6 +19,7 @@ class GitHub: """ The client for interacting with GitHub API. + Parameters ---------- bot : Bot @@ -31,6 +32,7 @@ class GitHub: URL to the avatar in GitHub. url : str, optional URL to the GitHub profile. + Attributes ---------- bot : Bot @@ -43,6 +45,7 @@ class GitHub: URL to the avatar in GitHub. url : str URL to the GitHub profile. + Class Attributes ---------------- BASE : str @@ -77,7 +80,7 @@ def __init__(self, bot, access_token: str = "", username: str = "", **kwargs): self.headers = {"Authorization": "token " + str(access_token)} @property - def BRANCH(self): + def BRANCH(self) -> str: return "master" if not self.bot.version.is_prerelease else "development" async def request( @@ -85,11 +88,13 @@ async def request( url: str, method: str = "GET", payload: dict = None, - return_response: bool = False, headers: dict = None, - ) -> Union[ClientResponse, dict, str]: + return_response: bool = False, + read_before_return: bool = True, + ) -> Union[ClientResponse, Dict[str, Any], str]: """ Makes a HTTP request. + Parameters ---------- url : str @@ -98,16 +103,20 @@ async def request( The HTTP method (POST, GET, PUT, DELETE, FETCH, etc.). payload : Dict[str, Any] The json payload to be sent along the request. - return_response : bool - Whether the `ClientResponse` object should be returned. headers : Dict[str, str] Additional headers to `headers`. + return_response : bool + Whether the `ClientResponse` object should be returned. + read_before_return : bool + Whether to perform `.read()` method before returning the `ClientResponse` object. + Only valid if `return_response` is set to `True`. + Returns ------- ClientResponse or Dict[str, Any] or List[Any] or str `ClientResponse` if `return_response` is `True`. - `dict` if the returned data is a json object. - `list` if the returned data is a json list. + `Dict[str, Any]` if the returned data is a json object. + `List[Any]` if the returned data is a json list. `str` if the returned data is not a valid json data, the raw response. """ @@ -117,19 +126,24 @@ async def request( headers = self.headers async with self.session.request(method, url, headers=headers, json=payload) as resp: if return_response: + if read_before_return: + await resp.read() return resp + try: return await resp.json() except (JSONDecodeError, ClientResponseError): return await resp.text() - def filter_valid(self, data): + def filter_valid(self, data) -> Dict[str, Any]: """ Filters configuration keys that are accepted. + Parameters ---------- data : Dict[str, Any] The data that needs to be cleaned. + Returns ------- Dict[str, Any] @@ -138,42 +152,73 @@ def filter_valid(self, data): valid_keys = self.bot.config.valid_keys.difference(self.bot.config.protected_keys) return {k: v for k, v in data.items() if k in valid_keys} - async def update_repository(self, sha: str = None) -> Optional[dict]: + async def update_repository(self, sha: str = None) -> Dict[str, Any]: """ Update the repository from Modmail main repo. + Parameters ---------- - sha : Optional[str], optional - The commit SHA to update the repository. + sha : Optional[str] + The commit SHA to update the repository. If `None`, the latest + commit SHA will be fetched. + Returns ------- - Optional[dict] - If the response is a dict. + Dict[str, Any] + A dictionary that contains response data. """ if not self.username: raise commands.CommandInvokeError("Username not found.") if sha is None: - resp: dict = await self.request(self.REPO + "/git/refs/heads/" + self.BRANCH) + resp = await self.request(self.REPO + "/git/refs/heads/" + self.BRANCH) sha = resp["object"]["sha"] payload = {"base": self.BRANCH, "head": sha, "commit_message": "Updating bot"} merge_url = self.MERGE_URL.format(username=self.username) - resp = await self.request(merge_url, method="POST", payload=payload) - if isinstance(resp, dict): - return resp + resp = await self.request( + merge_url, + method="POST", + payload=payload, + return_response=True, + read_before_return=True, + ) + + repo_url = self.BASE + f"/repos/{self.username}/modmail" + status_map = { + 201: "Successful response.", + 204: "Already merged.", + 403: "Forbidden.", + 404: f"Repository '{repo_url}' not found.", + 409: "There is a merge conflict.", + 422: "Validation failed.", + } + # source https://docs.github.com/en/rest/branches/branches#merge-a-branch + + status = resp.status + if status in (201, 204): + try: + return await resp.json() + except (JSONDecodeError, ClientResponseError): + return await resp.text() + + args = (resp.request_info, resp.history) + kwargs = {"status": status, "message": status_map.get(status)} + # just raise + raise ClientResponseError(*args, **kwargs) async def fork_repository(self) -> None: """ Forks Modmail's repository. """ - await self.request(self.FORK_URL, method="POST") + await self.request(self.FORK_URL, method="POST", return_response=True) async def has_starred(self) -> bool: """ Checks if shared Modmail. + Returns ------- bool @@ -187,23 +232,30 @@ async def star_repository(self) -> None: """ Stars Modmail's repository. """ - await self.request(self.STAR_URL, method="PUT", headers={"Content-Length": "0"}) + await self.request( + self.STAR_URL, + method="PUT", + headers={"Content-Length": "0"}, + return_response=True, + ) @classmethod async def login(cls, bot) -> "GitHub": """ Logs in to GitHub with configuration variable information. + Parameters ---------- bot : Bot The Modmail bot. + Returns ------- GitHub The newly created `GitHub` object. """ self = cls(bot, bot.config.get("github_token")) - resp: dict = await self.request("https://api.github.com/user") + resp: Dict[str, Any] = await self.request(self.BASE + "/user") if resp.get("login"): self.username = resp["login"] self.avatar_url = resp["avatar_url"] @@ -666,7 +718,7 @@ async def update_repository(self) -> dict: "data": data, "user": { "username": user.username, - "avatar_url": user.display_avatar.url, + "avatar_url": user.avatar_url, "url": user.url, }, } From 23221a2fd691733ede405a44b9da5f593e2d74c7 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <hidzrie@gmail.com> Date: Tue, 7 Jun 2022 14:36:09 +0000 Subject: [PATCH 1072/1221] Fix previous commit. - Return only if hosting method is HEROKU. - Try to get the response error message first if any. --- bot.py | 18 +++++++++++++----- cogs/utility.py | 18 +++++++++++------- core/clients.py | 32 +++++++++++++++++++++++--------- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/bot.py b/bot.py index 5db1aab550..f3dc071fab 100644 --- a/bot.py +++ b/bot.py @@ -1594,17 +1594,22 @@ async def autoupdate(self): latest = changelog.latest_version if self.version < parse_version(latest.version): + error = None + data = {} try: # update fork if gh_token exists data = await self.api.update_repository() except InvalidConfigError: - data = {} + pass except ClientResponseError as exc: - logger.error(f"Autoupdate failed! Status {exc.status}.") - logger.error(f"Message: {exc.message}") - self.autoupdate.cancel() - return + error = exc if self.hosting_method == HostingMethod.HEROKU: + if error is not None: + logger.error(f"Autoupdate failed! Status: {error.status}.") + logger.error(f"Error message: {error.message}") + self.autoupdate.cancel() + return + commit_data = data.get("data") if not commit_data: return @@ -1681,15 +1686,18 @@ async def before_autoupdate(self): if self.config.get("disable_autoupdates"): logger.warning("Autoupdates disabled.") self.autoupdate.cancel() + return if self.hosting_method == HostingMethod.DOCKER: logger.warning("Autoupdates disabled as using Docker.") self.autoupdate.cancel() + return if not self.config.get("github_token") and self.hosting_method == HostingMethod.HEROKU: logger.warning("GitHub access token not found.") logger.warning("Autoupdates disabled.") self.autoupdate.cancel() + return def format_channel_name(self, author, exclude_channel=None, force_null=False): """Sanitises a username for use with text channel names diff --git a/cogs/utility.py b/cogs/utility.py index 5d35a20211..ba60253370 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1951,20 +1951,24 @@ async def update(self, ctx, *, flag: str = ""): embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) await ctx.send(embed=embed) else: + error = None + data = {} try: # update fork if gh_token exists data = await self.bot.api.update_repository() except InvalidConfigError: - data = {} + pass except ClientResponseError as exc: - embed = discord.Embed( - title="Update failed", - description=f"Error status {exc.status}. {exc.message}", - color=self.bot.error_color, - ) - return await ctx.send(embed=embed) + error = exc if self.bot.hosting_method == HostingMethod.HEROKU: + if error is not None: + embed = discord.Embed( + title="Update failed", + description=f"Error status: {error.status}.\nError message: {error.message}", + color=self.bot.error_color, + ) + return await ctx.send(embed=embed) if not data: # invalid gh_token embed = discord.Embed( diff --git a/core/clients.py b/core/clients.py index 862c665310..df7e1b7b56 100644 --- a/core/clients.py +++ b/core/clients.py @@ -130,10 +130,18 @@ async def request( await resp.read() return resp - try: - return await resp.json() - except (JSONDecodeError, ClientResponseError): - return await resp.text() + return await self._get_response_data(resp) + + @staticmethod + async def _get_response_data(response: ClientResponse) -> Union[Dict[str, Any], str]: + """ + Internal method to convert the response data to `dict` if the data is a + json object, or to `str` (raw response) if the data is not a valid json. + """ + try: + return await response.json() + except (JSONDecodeError, ClientResponseError): + return await response.text() def filter_valid(self, data) -> Dict[str, Any]: """ @@ -198,14 +206,20 @@ async def update_repository(self, sha: str = None) -> Dict[str, Any]: # source https://docs.github.com/en/rest/branches/branches#merge-a-branch status = resp.status + data = await self._get_response_data(resp) if status in (201, 204): - try: - return await resp.json() - except (JSONDecodeError, ClientResponseError): - return await resp.text() + return data args = (resp.request_info, resp.history) - kwargs = {"status": status, "message": status_map.get(status)} + try: + # try to get the response error message if any + message = data.get("message") + except AttributeError: + message = None + kwargs = { + "status": status, + "message": message if message else status_map.get(status), + } # just raise raise ClientResponseError(*args, **kwargs) From f2865d22d489334893a679312cd5451203243e19 Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Sat, 25 Jun 2022 09:06:34 +0530 Subject: [PATCH 1073/1221] Remove the set title after creating thread --- core/thread.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index e39076d702..606a204b37 100644 --- a/core/thread.py +++ b/core/thread.py @@ -196,7 +196,6 @@ async def setup(self, *, creator=None, category=None, initial_message=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 is not None and creator != recipient: From 80fe41e3d6468eb7da440b3671ff1455bf919cc0 Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Sat, 25 Jun 2022 09:07:26 +0530 Subject: [PATCH 1074/1221] Add the set title during creating the thread --- core/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/utils.py b/core/utils.py index 32f12c72e8..30efb05e75 100644 --- a/core/utils.py +++ b/core/utils.py @@ -432,6 +432,7 @@ async def create_thread_channel(bot, recipient, category, overwrites, *, name=No name=name, category=category, overwrites=overwrites, + topic=f"User ID: {recipient.id}", reason="Creating a thread channel.", ) except discord.HTTPException as e: From c05725dcf251871910a691d535352a6464e2f977 Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Sat, 25 Jun 2022 10:09:31 +0530 Subject: [PATCH 1075/1221] Update bot.py --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index a506addfd9..e234eace55 100644 --- a/bot.py +++ b/bot.py @@ -1099,7 +1099,7 @@ async def get_context(self, message, *, cls=commands.Context): view = StringView(message.content) ctx = cls(prefix=self.prefix, view=view, bot=self, message=message) - if self._skip_check(message.author.id, self.user.id): + if message.author.id == self.user.id: return ctx ctx.thread = await self.threads.find(channel=ctx.channel) From 19b42f03f1c63c05e7c17f376b3ffee6e587eae1 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <hidzrie@gmail.com> Date: Sat, 25 Jun 2022 08:24:35 +0000 Subject: [PATCH 1076/1221] Fix Future attached to different loop, issue #3165 --- bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index a506addfd9..8b912f4286 100644 --- a/bot.py +++ b/bot.py @@ -71,7 +71,7 @@ def __init__(self): self._api = None self.formatter = SafeFormatter() self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] - self._connected = asyncio.Event() + self._connected = None self.start_time = discord.utils.utcnow() self._started = False @@ -213,6 +213,7 @@ async def get_prefix(self, message=None): def run(self): async def runner(): async with self: + self._connected = asyncio.Event() self.session = ClientSession(loop=self.loop) try: retry_intents = False From 30a0daa356b220181babad3b0d92e5acd6801b2b Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 3 Jul 2022 21:50:56 +0800 Subject: [PATCH 1077/1221] update changelog w new PRs --- CHANGELOG.md | 1 + bot.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c096d05d7f..054f126f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Audit log searching now properly works. - Old data causing `?blocked` to fail. ([GH #3131](https://github.com/kyb3r/modmail/issues/3131)) - Delete channel auto close functionality now works. +- Improved error handling for autoupdate. ([PR #3161](https://github.com/kyb3r/modmail/pull/3161)) ### Internal diff --git a/bot.py b/bot.py index 788ba64604..3723e94325 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev14" +__version__ = "4.0.0-dev15" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 56f30163c2..8b306b2d18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev14' +version = '4.0.0-dev15' description = "Modmail is similar to Reddit's 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 109197d9f509eb4ae5b1c8e7c0d59e79a222ea0b Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Sun, 3 Jul 2022 21:54:30 +0800 Subject: [PATCH 1078/1221] update contributing.md --- .github/CONTRIBUTING.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 568e5f2175..b1309b6402 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -33,6 +33,17 @@ In short, when you submit code changes, your submissions are understood to be un ## Report bugs using [Github Issues](https://github.com/kyb3r/modmail/issues) We use GitHub issues to track public bugs. Report a bug by [opening a new Issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! +## Find pre-existing issues to tackle +Check out our [unstaged issue tracker](https://github.com/kyb3r/modmail/issues?q=is%3Aissue+is%3Aopen+-label%3Astaged) and start helping out! + +Ways to help out: +- Help out new members +- Highlight invalid bugs/unsupported use cases +- Code review of pull requests +- Add on new use cases or reproduction steps +- Point out duplicate issues and guide them to the right direction +- Create a pull request to resolve the issue! + ## Write bug reports with detail, background, and sample code **Great Bug Reports** tend to have: @@ -43,7 +54,6 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new Issue] - What *actually* happens - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) - ## Use a Consistent Coding Style We use [black](https://github.com/python/black) for a unified code style. From 3fe1ce8d5e495c06d814ed718ec40e7e2aa8e79a Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 6 Jul 2022 01:18:32 +0800 Subject: [PATCH 1079/1221] update dpy@37c9ab7 and colorama --- Pipfile | 4 +- Pipfile.lock | 260 +++++++++++++++++++++++++++++---------------------- 2 files changed, 148 insertions(+), 116 deletions(-) diff --git a/Pipfile b/Pipfile index 88d49f59fd..82ec35158d 100644 --- a/Pipfile +++ b/Pipfile @@ -11,8 +11,8 @@ typing-extensions = "==4.2.0" [packages] aiohttp = "==3.8.1" -colorama = "~=0.4.4" # Doesn't officially support Python 3.9 yet, v0.4.5 will support 3.9 -"discord.py" = {ref = "e9c7c09ebfe780d8f9de1e9e4d3c2dddf85eccfd", git = "https://github.com/Rapptz/discord.py.git"} +colorama = "~=0.4.5" +"discord.py" = {ref = "9fe19dcc6923e4b6133bdedafb33658bf37c9ab7", git = "https://github.com/Rapptz/discord.py.git"} emoji = "==1.7.0" isodate = "~=0.6.0" motor = "==2.5.1" diff --git a/Pipfile.lock b/Pipfile.lock index 163d09bdcd..87a2f247fc 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4035fc1c98c21d437f41fcd2f32ca38089131e4a60ef0f4453caf4019dae7bd3" + "sha256": "89578b9ae8a9a0b580e0af21767bd2f062388ad43c9bdc6e5d113de91361bea3" }, "pipfile-spec": 6, "requires": {}, @@ -132,74 +132,88 @@ }, "cffi": { "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "version": "==1.15.0" + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" }, "charset-normalizer": { "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", + "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" ], - "markers": "python_full_version >= '3.5.0'", - "version": "==2.0.12" + "markers": "python_version >= '3.6'", + "version": "==2.1.0" }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", + "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" ], "index": "pypi", - "version": "==0.4.4" + "version": "==0.4.5" }, "cssselect2": { "hashes": [ @@ -219,11 +233,11 @@ }, "discord-py": { "git": "https://github.com/Rapptz/discord.py.git", - "ref": "e9c7c09ebfe780d8f9de1e9e4d3c2dddf85eccfd" + "ref": "9fe19dcc6923e4b6133bdedafb33658bf37c9ab7" }, "discord.py": { "git": "https://github.com/Rapptz/discord.py.git", - "ref": "e9c7c09ebfe780d8f9de1e9e4d3c2dddf85eccfd" + "ref": "9fe19dcc6923e4b6133bdedafb33658bf37c9ab7" }, "dnspython": { "hashes": [ @@ -310,7 +324,7 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_full_version >= '3.5.0'", + "markers": "python_version >= '3.5'", "version": "==3.3" }, "isodate": { @@ -421,47 +435,67 @@ }, "pillow": { "hashes": [ - "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e", - "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595", - "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512", - "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c", - "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477", - "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a", - "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4", - "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e", - "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5", - "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378", - "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a", - "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652", - "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7", - "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a", - "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a", - "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6", - "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165", - "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160", - "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331", - "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b", - "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458", - "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033", - "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8", - "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481", - "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58", - "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7", - "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3", - "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea", - "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34", - "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3", - "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8", - "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581", - "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244", - "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef", - "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0", - "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2", - "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97", - "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717" + "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927", + "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14", + "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc", + "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58", + "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60", + "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76", + "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c", + "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac", + "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490", + "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1", + "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f", + "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d", + "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f", + "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069", + "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402", + "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885", + "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e", + "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be", + "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8", + "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff", + "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da", + "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004", + "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f", + "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20", + "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d", + "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c", + "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544", + "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9", + "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3", + "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04", + "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c", + "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5", + "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4", + "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb", + "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4", + "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c", + "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467", + "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e", + "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421", + "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b", + "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8", + "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb", + "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3", + "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf", + "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1", + "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a", + "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28", + "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0", + "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1", + "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8", + "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd", + "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4", + "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8", + "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f", + "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013", + "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59", + "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc", + "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4" ], "markers": "python_version >= '3.7'", - "version": "==9.1.0" + "version": "==9.2.0" }, "pycparser": { "hashes": [ @@ -471,9 +505,7 @@ "version": "==2.21" }, "pymongo": { - "extras": [ - "srv" - ], + "extras": [], "hashes": [ "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", @@ -782,11 +814,11 @@ }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", + "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" ], "index": "pypi", - "version": "==0.4.4" + "version": "==0.4.5" }, "gitdb": { "hashes": [ @@ -878,11 +910,11 @@ }, "pbr": { "hashes": [ - "sha256:27108648368782d07bbf1cb468ad2e2eeef29086affd14087a6d04b7de8af4ec", - "sha256:66bc5a34912f408bb3925bf21231cb6f59206267b7f63f3503ef865c1a292e25" + "sha256:e547125940bcc052856ded43be8e101f63828c2d94239ffbe2b327ba3d5ccf0a", + "sha256:e8dca2f4b43560edef58813969f52a56cef023146cbb8931626db80e6c1c4308" ], "markers": "python_version >= '2.6'", - "version": "==5.8.1" + "version": "==5.9.0" }, "platformdirs": { "hashes": [ @@ -941,11 +973,11 @@ }, "setuptools": { "hashes": [ - "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", - "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" + "sha256:16923d366ced322712c71ccb97164d07472abeecd13f3a6c283f6d5d26722793", + "sha256:db3b8e2f922b2a910a29804776c643ea609badb6a32c4bcc226fd4fd902cce65" ], "markers": "python_version >= '3.7'", - "version": "==62.1.0" + "version": "==63.1.0" }, "smmap": { "hashes": [ From a93d81eac495df7fe26a6aabc04c88019f35b3b2 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 6 Jul 2022 01:18:57 +0800 Subject: [PATCH 1080/1221] fix typo https://github.com/kyb3r/modmail/pull/3161#discussion_r912533027 --- core/clients.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/clients.py b/core/clients.py index df7e1b7b56..eebe3bcff6 100644 --- a/core/clients.py +++ b/core/clients.py @@ -90,7 +90,7 @@ async def request( payload: dict = None, headers: dict = None, return_response: bool = False, - read_before_return: bool = True, + read_before_return: bool = False, ) -> Union[ClientResponse, Dict[str, Any], str]: """ Makes a HTTP request. From ee47143fd01cbc204275397c46b67c7e17efd341 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 6 Jul 2022 01:19:47 +0800 Subject: [PATCH 1081/1221] update version dv16 --- bot.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 3723e94325..f3d7198428 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev15" +__version__ = "4.0.0-dev16" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 8b306b2d18..7c1dc0e952 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev15' +version = '4.0.0-dev16' description = "Modmail is similar to Reddit's 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 07c6dc12bec2ae0a4735c5965ee00d686622a21e Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Fri, 22 Jul 2022 23:38:18 +0530 Subject: [PATCH 1082/1221] Prevents loaded cogs from being loaded again Fixes #3171 Haven't tested this, but pretty sure it should work. --- bot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot.py b/bot.py index f3d7198428..5a1c0a18a5 100644 --- a/bot.py +++ b/bot.py @@ -157,6 +157,8 @@ def startup(self): async def load_extensions(self): for cog in self.loaded_cogs: + if cog in self.bot.extensions: + continue logger.debug("Loading %s.", cog) try: await self.load_extension(cog) From f274a79b98fdd67cafdd3b00fd3888639b9da80f Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Sat, 23 Jul 2022 19:03:26 +0530 Subject: [PATCH 1083/1221] Update bot.py --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 5a1c0a18a5..bcb0685f08 100644 --- a/bot.py +++ b/bot.py @@ -157,7 +157,7 @@ def startup(self): async def load_extensions(self): for cog in self.loaded_cogs: - if cog in self.bot.extensions: + if cog in self.extensions: continue logger.debug("Loading %s.", cog) try: From c7072eff17c9a35dc9fbf44855970df0da66ea1f Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sat, 6 Aug 2022 19:20:00 -0400 Subject: [PATCH 1084/1221] Add aliases to snippet add command. --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index deb29d160d..7fc9e5b975 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -193,7 +193,7 @@ async def snippet_raw(self, ctx, *, name: str.lower): return await ctx.send(embed=embed) - @snippet.command(name="add") + @snippet.command(name="add", aliases=["create", "make"]) @checks.has_permissions(PermissionLevel.SUPPORTER) async def snippet_add(self, ctx, name: str.lower, *, value: commands.clean_content): """ From 7bc33401aa56523fc60d9345fe222ef90f2df4cb Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Aug 2022 22:17:15 +0800 Subject: [PATCH 1085/1221] Bump dpy-2.0 --- CHANGELOG.md | 2 +- Pipfile | 2 +- Pipfile.lock | 307 ++++++++++++++++++++++++------------------------- bot.py | 6 +- pyproject.toml | 2 +- 5 files changed, 154 insertions(+), 165 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 054f126f7f..0070eecf0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Breaking - Modmail now requires [`Message Content` privileged intent](https://support-dev.discord.com/hc/en-us/articles/4404772028055-Message-Content-Privileged-Intent-for-Verified-Bots). -- Upgraded to discord.py v2.0 master ([internal changes](https://gist.github.com/apple502j/f75b4f24652f04de85c7084ffd73ec58), [GH #2990](https://github.com/kyb3r/modmail/issues/2990)). +- Upgraded to discord.py v2.0 ([internal changes](https://discordpy.readthedocs.io/en/latest/migrating.html), [GH #2990](https://github.com/kyb3r/modmail/issues/2990)). - Python 3.8 or higher is required. - Asyncio changes ([gist](https://gist.github.com/Rapptz/6706e1c8f23ac27c98cee4dd985c8120)) diff --git a/Pipfile b/Pipfile index 82ec35158d..12de060a72 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,7 @@ typing-extensions = "==4.2.0" [packages] aiohttp = "==3.8.1" colorama = "~=0.4.5" -"discord.py" = {ref = "9fe19dcc6923e4b6133bdedafb33658bf37c9ab7", git = "https://github.com/Rapptz/discord.py.git"} +"discord.py" = "==2.0.0" emoji = "==1.7.0" isodate = "~=0.6.0" motor = "==2.5.1" diff --git a/Pipfile.lock b/Pipfile.lock index 87a2f247fc..ed19b5883c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "89578b9ae8a9a0b580e0af21767bd2f062388ad43c9bdc6e5d113de91361bea3" + "sha256": "aa752693f18b9d6058209883bdcece4e37a1a7a62a5fd564d64e551d91ea49f4" }, "pipfile-spec": 6, "requires": {}, @@ -110,11 +110,11 @@ }, "attrs": { "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" + "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", + "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" + "markers": "python_version >= '3.5'", + "version": "==22.1.0" }, "cairocffi": { "hashes": [ @@ -201,11 +201,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", - "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], "markers": "python_version >= '3.6'", - "version": "==2.1.0" + "version": "==2.1.1" }, "colorama": { "hashes": [ @@ -231,13 +231,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.7.1" }, - "discord-py": { - "git": "https://github.com/Rapptz/discord.py.git", - "ref": "9fe19dcc6923e4b6133bdedafb33658bf37c9ab7" - }, "discord.py": { - "git": "https://github.com/Rapptz/discord.py.git", - "ref": "9fe19dcc6923e4b6133bdedafb33658bf37c9ab7" + "hashes": [ + "sha256:18b06870bdc85d29e0d55f4a4b2abe9d7cdae2b197e23d49f82886ba27ba1aec", + "sha256:c36f26935938194c3465c2abf8ecfbbf5560c50b189f1b746d6f00d1e78c0d3b" + ], + "index": "pypi", + "version": "==2.0.0" }, "dnspython": { "hashes": [ @@ -256,68 +256,68 @@ }, "frozenlist": { "hashes": [ - "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e", - "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08", - "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b", - "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486", - "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78", - "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468", - "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1", - "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953", - "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3", - "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d", - "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a", - "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141", - "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08", - "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07", - "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa", - "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa", - "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868", - "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f", - "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b", - "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b", - "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1", - "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f", - "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478", - "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58", - "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01", - "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8", - "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d", - "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676", - "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274", - "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab", - "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8", - "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24", - "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a", - "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2", - "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f", - "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f", - "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93", - "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1", - "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51", - "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846", - "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5", - "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d", - "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c", - "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e", - "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae", - "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02", - "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0", - "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b", - "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3", - "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b", - "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa", - "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a", - "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d", - "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed", - "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148", - "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9", - "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c", - "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2", - "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951" + "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e", + "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04", + "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944", + "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845", + "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f", + "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f", + "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb", + "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2", + "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3", + "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f", + "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a", + "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131", + "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1", + "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa", + "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8", + "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b", + "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2", + "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e", + "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b", + "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b", + "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10", + "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170", + "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8", + "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b", + "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989", + "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd", + "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03", + "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519", + "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189", + "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b", + "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca", + "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff", + "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96", + "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d", + "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5", + "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2", + "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204", + "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a", + "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8", + "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141", + "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792", + "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9", + "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c", + "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c", + "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221", + "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d", + "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc", + "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f", + "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9", + "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef", + "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3", + "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab", + "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b", + "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db", + "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988", + "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6", + "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc", + "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83", + "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be" ], "markers": "python_version >= '3.7'", - "version": "==1.3.0" + "version": "==1.3.1" }, "idna": { "hashes": [ @@ -505,7 +505,9 @@ "version": "==2.21" }, "pymongo": { - "extras": [], + "extras": [ + "srv" + ], "hashes": [ "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", @@ -681,81 +683,68 @@ }, "yarl": { "hashes": [ - "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", - "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8", - "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e", - "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746", - "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98", - "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125", - "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d", - "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d", - "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986", - "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d", - "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec", - "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8", - "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee", - "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3", - "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1", - "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd", - "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b", - "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de", - "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0", - "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8", - "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6", - "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245", - "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23", - "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332", - "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1", - "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c", - "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4", - "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0", - "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8", - "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832", - "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58", - "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6", - "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1", - "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52", - "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92", - "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185", - "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d", - "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d", - "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b", - "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739", - "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05", - "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63", - "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d", - "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa", - "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913", - "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe", - "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b", - "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b", - "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656", - "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1", - "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4", - "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e", - "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63", - "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271", - "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed", - "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d", - "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda", - "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265", - "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f", - "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c", - "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba", - "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c", - "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b", - "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523", - "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a", - "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef", - "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95", - "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72", - "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794", - "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41", - "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576", - "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59" + "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb", + "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3", + "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035", + "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453", + "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d", + "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a", + "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231", + "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f", + "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae", + "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b", + "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3", + "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507", + "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd", + "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae", + "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe", + "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c", + "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4", + "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64", + "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357", + "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54", + "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461", + "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4", + "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497", + "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0", + "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1", + "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957", + "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350", + "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780", + "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843", + "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548", + "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6", + "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40", + "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee", + "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b", + "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6", + "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0", + "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e", + "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880", + "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc", + "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e", + "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead", + "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28", + "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf", + "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd", + "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae", + "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0", + "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0", + "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae", + "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda", + "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546", + "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802", + "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be", + "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07", + "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936", + "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272", + "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc", + "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a", + "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28", + "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b" ], - "markers": "python_version >= '3.6'", - "version": "==1.7.2" + "markers": "python_version >= '3.7'", + "version": "==1.8.1" } }, "develop": { @@ -910,11 +899,11 @@ }, "pbr": { "hashes": [ - "sha256:e547125940bcc052856ded43be8e101f63828c2d94239ffbe2b327ba3d5ccf0a", - "sha256:e8dca2f4b43560edef58813969f52a56cef023146cbb8931626db80e6c1c4308" + "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a", + "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf" ], "markers": "python_version >= '2.6'", - "version": "==5.9.0" + "version": "==5.10.0" }, "platformdirs": { "hashes": [ @@ -973,11 +962,11 @@ }, "setuptools": { "hashes": [ - "sha256:16923d366ced322712c71ccb97164d07472abeecd13f3a6c283f6d5d26722793", - "sha256:db3b8e2f922b2a910a29804776c643ea609badb6a32c4bcc226fd4fd902cce65" + "sha256:7f4bc85450898a09f76ebf28b72fa25bc7111f6c7d665d514a60bba9c75ef2a9", + "sha256:a3ca5857c89f82f5c9410e8508cb32f4872a3bafd4aa7ae122a24ca33bccc750" ], "markers": "python_version >= '3.7'", - "version": "==63.1.0" + "version": "==65.2.0" }, "smmap": { "hashes": [ @@ -989,11 +978,11 @@ }, "stevedore": { "hashes": [ - "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c", - "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335" + "sha256:87e4d27fe96d0d7e4fc24f0cbe3463baae4ec51e81d95fbe60d2474636e0c7d8", + "sha256:f82cc99a1ff552310d19c379827c2c64dd9f85a38bcd5559db2470161867b786" ], - "markers": "python_version >= '3.6'", - "version": "==3.5.0" + "markers": "python_version >= '3.8'", + "version": "==4.0.0" }, "toml": { "hashes": [ diff --git a/bot.py b/bot.py index f3d7198428..fb373f9e76 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev16" +__version__ = "4.0.0-dev17" import asyncio @@ -1769,9 +1769,9 @@ def main(): sys.exit(0) # check discord version - if discord.__version__ != "2.0.0a": + if discord.__version__ != "2.0.0": logger.error( - "Dependencies are not updated, run pipenv install. discord.py version expected 2.0.0a, received %s", + "Dependencies are not updated, run pipenv install. discord.py version expected 2.0.0, received %s", discord.__version__, ) sys.exit(0) diff --git a/pyproject.toml b/pyproject.toml index 7c1dc0e952..b3dea8293f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev16' +version = '4.0.0-dev17' description = "Modmail is similar to Reddit's 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 ac2e36ced9ce3fbc27a02ef9f8653c211629a1ee Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Aug 2022 22:20:30 +0800 Subject: [PATCH 1086/1221] Bump dpy-2.0 --- CHANGELOG.md | 2 +- Pipfile | 2 +- Pipfile.lock | 307 ++++++++++++++++++++++++------------------------- bot.py | 6 +- pyproject.toml | 2 +- 5 files changed, 154 insertions(+), 165 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 054f126f7f..0070eecf0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Breaking - Modmail now requires [`Message Content` privileged intent](https://support-dev.discord.com/hc/en-us/articles/4404772028055-Message-Content-Privileged-Intent-for-Verified-Bots). -- Upgraded to discord.py v2.0 master ([internal changes](https://gist.github.com/apple502j/f75b4f24652f04de85c7084ffd73ec58), [GH #2990](https://github.com/kyb3r/modmail/issues/2990)). +- Upgraded to discord.py v2.0 ([internal changes](https://discordpy.readthedocs.io/en/latest/migrating.html), [GH #2990](https://github.com/kyb3r/modmail/issues/2990)). - Python 3.8 or higher is required. - Asyncio changes ([gist](https://gist.github.com/Rapptz/6706e1c8f23ac27c98cee4dd985c8120)) diff --git a/Pipfile b/Pipfile index 82ec35158d..12de060a72 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,7 @@ typing-extensions = "==4.2.0" [packages] aiohttp = "==3.8.1" colorama = "~=0.4.5" -"discord.py" = {ref = "9fe19dcc6923e4b6133bdedafb33658bf37c9ab7", git = "https://github.com/Rapptz/discord.py.git"} +"discord.py" = "==2.0.0" emoji = "==1.7.0" isodate = "~=0.6.0" motor = "==2.5.1" diff --git a/Pipfile.lock b/Pipfile.lock index 87a2f247fc..ed19b5883c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "89578b9ae8a9a0b580e0af21767bd2f062388ad43c9bdc6e5d113de91361bea3" + "sha256": "aa752693f18b9d6058209883bdcece4e37a1a7a62a5fd564d64e551d91ea49f4" }, "pipfile-spec": 6, "requires": {}, @@ -110,11 +110,11 @@ }, "attrs": { "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" + "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", + "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" + "markers": "python_version >= '3.5'", + "version": "==22.1.0" }, "cairocffi": { "hashes": [ @@ -201,11 +201,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", - "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], "markers": "python_version >= '3.6'", - "version": "==2.1.0" + "version": "==2.1.1" }, "colorama": { "hashes": [ @@ -231,13 +231,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.7.1" }, - "discord-py": { - "git": "https://github.com/Rapptz/discord.py.git", - "ref": "9fe19dcc6923e4b6133bdedafb33658bf37c9ab7" - }, "discord.py": { - "git": "https://github.com/Rapptz/discord.py.git", - "ref": "9fe19dcc6923e4b6133bdedafb33658bf37c9ab7" + "hashes": [ + "sha256:18b06870bdc85d29e0d55f4a4b2abe9d7cdae2b197e23d49f82886ba27ba1aec", + "sha256:c36f26935938194c3465c2abf8ecfbbf5560c50b189f1b746d6f00d1e78c0d3b" + ], + "index": "pypi", + "version": "==2.0.0" }, "dnspython": { "hashes": [ @@ -256,68 +256,68 @@ }, "frozenlist": { "hashes": [ - "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e", - "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08", - "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b", - "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486", - "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78", - "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468", - "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1", - "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953", - "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3", - "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d", - "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a", - "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141", - "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08", - "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07", - "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa", - "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa", - "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868", - "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f", - "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b", - "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b", - "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1", - "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f", - "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478", - "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58", - "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01", - "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8", - "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d", - "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676", - "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274", - "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab", - "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8", - "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24", - "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a", - "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2", - "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f", - "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f", - "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93", - "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1", - "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51", - "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846", - "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5", - "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d", - "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c", - "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e", - "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae", - "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02", - "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0", - "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b", - "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3", - "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b", - "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa", - "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a", - "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d", - "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed", - "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148", - "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9", - "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c", - "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2", - "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951" + "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e", + "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04", + "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944", + "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845", + "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f", + "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f", + "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb", + "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2", + "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3", + "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f", + "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a", + "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131", + "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1", + "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa", + "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8", + "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b", + "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2", + "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e", + "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b", + "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b", + "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10", + "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170", + "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8", + "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b", + "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989", + "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd", + "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03", + "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519", + "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189", + "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b", + "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca", + "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff", + "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96", + "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d", + "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5", + "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2", + "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204", + "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a", + "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8", + "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141", + "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792", + "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9", + "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c", + "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c", + "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221", + "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d", + "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc", + "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f", + "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9", + "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef", + "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3", + "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab", + "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b", + "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db", + "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988", + "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6", + "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc", + "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83", + "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be" ], "markers": "python_version >= '3.7'", - "version": "==1.3.0" + "version": "==1.3.1" }, "idna": { "hashes": [ @@ -505,7 +505,9 @@ "version": "==2.21" }, "pymongo": { - "extras": [], + "extras": [ + "srv" + ], "hashes": [ "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", @@ -681,81 +683,68 @@ }, "yarl": { "hashes": [ - "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac", - "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8", - "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e", - "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746", - "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98", - "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125", - "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d", - "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d", - "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986", - "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d", - "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec", - "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8", - "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee", - "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3", - "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1", - "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd", - "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b", - "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de", - "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0", - "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8", - "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6", - "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245", - "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23", - "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332", - "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1", - "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c", - "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4", - "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0", - "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8", - "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832", - "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58", - "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6", - "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1", - "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52", - "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92", - "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185", - "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d", - "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d", - "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b", - "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739", - "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05", - "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63", - "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d", - "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa", - "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913", - "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe", - "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b", - "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b", - "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656", - "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1", - "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4", - "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e", - "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63", - "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271", - "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed", - "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d", - "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda", - "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265", - "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f", - "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c", - "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba", - "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c", - "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b", - "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523", - "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a", - "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef", - "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95", - "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72", - "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794", - "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41", - "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576", - "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59" + "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb", + "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3", + "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035", + "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453", + "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d", + "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a", + "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231", + "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f", + "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae", + "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b", + "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3", + "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507", + "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd", + "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae", + "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe", + "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c", + "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4", + "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64", + "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357", + "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54", + "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461", + "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4", + "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497", + "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0", + "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1", + "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957", + "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350", + "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780", + "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843", + "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548", + "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6", + "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40", + "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee", + "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b", + "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6", + "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0", + "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e", + "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880", + "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc", + "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e", + "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead", + "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28", + "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf", + "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd", + "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae", + "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0", + "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0", + "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae", + "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda", + "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546", + "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802", + "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be", + "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07", + "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936", + "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272", + "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc", + "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a", + "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28", + "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b" ], - "markers": "python_version >= '3.6'", - "version": "==1.7.2" + "markers": "python_version >= '3.7'", + "version": "==1.8.1" } }, "develop": { @@ -910,11 +899,11 @@ }, "pbr": { "hashes": [ - "sha256:e547125940bcc052856ded43be8e101f63828c2d94239ffbe2b327ba3d5ccf0a", - "sha256:e8dca2f4b43560edef58813969f52a56cef023146cbb8931626db80e6c1c4308" + "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a", + "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf" ], "markers": "python_version >= '2.6'", - "version": "==5.9.0" + "version": "==5.10.0" }, "platformdirs": { "hashes": [ @@ -973,11 +962,11 @@ }, "setuptools": { "hashes": [ - "sha256:16923d366ced322712c71ccb97164d07472abeecd13f3a6c283f6d5d26722793", - "sha256:db3b8e2f922b2a910a29804776c643ea609badb6a32c4bcc226fd4fd902cce65" + "sha256:7f4bc85450898a09f76ebf28b72fa25bc7111f6c7d665d514a60bba9c75ef2a9", + "sha256:a3ca5857c89f82f5c9410e8508cb32f4872a3bafd4aa7ae122a24ca33bccc750" ], "markers": "python_version >= '3.7'", - "version": "==63.1.0" + "version": "==65.2.0" }, "smmap": { "hashes": [ @@ -989,11 +978,11 @@ }, "stevedore": { "hashes": [ - "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c", - "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335" + "sha256:87e4d27fe96d0d7e4fc24f0cbe3463baae4ec51e81d95fbe60d2474636e0c7d8", + "sha256:f82cc99a1ff552310d19c379827c2c64dd9f85a38bcd5559db2470161867b786" ], - "markers": "python_version >= '3.6'", - "version": "==3.5.0" + "markers": "python_version >= '3.8'", + "version": "==4.0.0" }, "toml": { "hashes": [ diff --git a/bot.py b/bot.py index f3d7198428..fb373f9e76 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev16" +__version__ = "4.0.0-dev17" import asyncio @@ -1769,9 +1769,9 @@ def main(): sys.exit(0) # check discord version - if discord.__version__ != "2.0.0a": + if discord.__version__ != "2.0.0": logger.error( - "Dependencies are not updated, run pipenv install. discord.py version expected 2.0.0a, received %s", + "Dependencies are not updated, run pipenv install. discord.py version expected 2.0.0, received %s", discord.__version__, ) sys.exit(0) diff --git a/pyproject.toml b/pyproject.toml index 7c1dc0e952..b3dea8293f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev16' +version = '4.0.0-dev17' description = "Modmail is similar to Reddit's 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 57eca05d471fd5e68b8e25c889e33dcb6522bcef Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Aug 2022 22:21:20 +0800 Subject: [PATCH 1087/1221] Skip loading of already-loaded cog #3172 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0070eecf0a..44097a7527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Old data causing `?blocked` to fail. ([GH #3131](https://github.com/kyb3r/modmail/issues/3131)) - Delete channel auto close functionality now works. - Improved error handling for autoupdate. ([PR #3161](https://github.com/kyb3r/modmail/pull/3161)) +- Skip loading of already-loaded cog. ([PR #3172](https://github.com/kyb3r/modmail/pull/3172)) ### Internal From 48ad5c4ee6f92b86d32737568ae94d8637f70219 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Mon, 22 Aug 2022 22:22:47 +0800 Subject: [PATCH 1088/1221] snippet make create aliases, resolve #3172, close #3174 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44097a7527..27165ebda8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Select menus for certain paginators. - `Title` field in `?logs`. ([GH #3142](https://github.com/kyb3r/modmail/issues/3142)) - Snippets can be used in aliases. ([GH #3108](https://github.com/kyb3r/modmail/issues/3108), [PR #3124](https://github.com/kyb3r/modmail/pull/3124)) +- `?snippet make/create` as aliases to `?snippet add`. ([GH #3172](https://github.com/kyb3r/modmail/issues/3173), [PR #3174](https://github.com/kyb3r/modmail/pull/3174)) ### Improved From f68d8a20861eb851f2713b796f639b44ba6b16c0 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <hidzrie@gmail.com> Date: Wed, 24 Aug 2022 14:10:48 +0000 Subject: [PATCH 1089/1221] Add `unhandled_by_cog` parameter to `ModmailBot.on_command_error`: - Resolve #3170 --- bot.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 5b71c9bc71..6ea42499cb 100644 --- a/bot.py +++ b/bot.py @@ -1511,7 +1511,17 @@ async def on_error(self, event_method, *args, **kwargs): logger.error("Ignoring exception in %s.", event_method) logger.error("Unexpected exception:", exc_info=sys.exc_info()) - async def on_command_error(self, context, exception): + async def on_command_error( + self, context: commands.Context, exception: Exception, *, unhandled_by_cog: bool = False + ) -> None: + if not unhandled_by_cog: + command = context.command + if command and command.has_error_handler(): + return + cog = context.cog + if cog and cog.has_error_handler(): + return + if isinstance(exception, (commands.BadArgument, commands.BadUnionArgument)): await context.typing() await context.send(embed=discord.Embed(color=self.error_color, description=str(exception))) From fa3ad60af794c5fe766697f6c070a43d21840cfe Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 6 Sep 2022 22:36:26 +0800 Subject: [PATCH 1090/1221] Update to discord.py 2.0.1 and fix time module --- Pipfile | 2 +- Pipfile.lock | 27 +-- bot.py | 15 +- cogs/modmail.py | 10 +- core/config.py | 7 +- core/time.py | 436 ++++++++++++++++++++++++++++++----------------- core/utils.py | 17 +- pyproject.toml | 2 +- requirements.txt | 2 +- 9 files changed, 333 insertions(+), 185 deletions(-) diff --git a/Pipfile b/Pipfile index 12de060a72..573f06cb76 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,7 @@ typing-extensions = "==4.2.0" [packages] aiohttp = "==3.8.1" colorama = "~=0.4.5" -"discord.py" = "==2.0.0" +"discord.py" = "==2.0.1" emoji = "==1.7.0" isodate = "~=0.6.0" motor = "==2.5.1" diff --git a/Pipfile.lock b/Pipfile.lock index ed19b5883c..9657e2b273 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "aa752693f18b9d6058209883bdcece4e37a1a7a62a5fd564d64e551d91ea49f4" + "sha256": "c074bd9564d93aac78141ccfec1138ffeb561dfa0956f60ee5b10322d156d20f" }, "pipfile-spec": 6, "requires": {}, @@ -233,11 +233,11 @@ }, "discord.py": { "hashes": [ - "sha256:18b06870bdc85d29e0d55f4a4b2abe9d7cdae2b197e23d49f82886ba27ba1aec", - "sha256:c36f26935938194c3465c2abf8ecfbbf5560c50b189f1b746d6f00d1e78c0d3b" + "sha256:309146476e986cb8faf038cd5d604d4b3834ef15c2d34df697ce5064bf5cd779", + "sha256:aeb186348bf011708b085b2715cf92bbb72c692eb4f59c4c0b488130cc4c4b7e" ], "index": "pypi", - "version": "==2.0.0" + "version": "==2.0.1" }, "dnspython": { "hashes": [ @@ -450,6 +450,7 @@ "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f", "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069", "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402", + "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437", "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885", "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e", "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be", @@ -478,6 +479,7 @@ "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8", "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb", "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3", + "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc", "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf", "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1", "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a", @@ -505,9 +507,7 @@ "version": "==2.21" }, "pymongo": { - "extras": [ - "srv" - ], + "extras": [], "hashes": [ "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", @@ -892,10 +892,11 @@ }, "pathspec": { "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93", + "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d" ], - "version": "==0.9.0" + "markers": "python_version >= '3.7'", + "version": "==0.10.1" }, "pbr": { "hashes": [ @@ -962,11 +963,11 @@ }, "setuptools": { "hashes": [ - "sha256:7f4bc85450898a09f76ebf28b72fa25bc7111f6c7d665d514a60bba9c75ef2a9", - "sha256:a3ca5857c89f82f5c9410e8508cb32f4872a3bafd4aa7ae122a24ca33bccc750" + "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", + "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57" ], "markers": "python_version >= '3.7'", - "version": "==65.2.0" + "version": "==65.3.0" }, "smmap": { "hashes": [ diff --git a/bot.py b/bot.py index 5b71c9bc71..ffc2c01dc6 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev17" +__version__ = "4.0.0-dev18" import asyncio @@ -18,6 +18,7 @@ import discord import isodate from aiohttp import ClientSession, ClientResponseError +from discord.utils import MISSING from discord.ext import commands, tasks from discord.ext.commands.view import StringView from emoji import UNICODE_EMOJI @@ -247,10 +248,10 @@ async def runner(): except Exception: logger.critical("Fatal exception", exc_info=True) finally: - if not self.is_closed(): - await self.close() if self.session: await self.session.close() + if not self.is_closed(): + await self.close() async def _cancel_tasks(): async with self: @@ -282,7 +283,7 @@ async def _cancel_tasks(): pass try: - asyncio.run(runner()) + asyncio.run(runner(), debug=bool(os.getenv("DEBUG_ASYNCIO"))) except (KeyboardInterrupt, SystemExit): logger.info("Received signal to terminate bot and event loop.") finally: @@ -1771,9 +1772,11 @@ def main(): sys.exit(0) # check discord version - if discord.__version__ != "2.0.0": + discord_version = "2.0.1" + if discord.__version__ != discord_version: logger.error( - "Dependencies are not updated, run pipenv install. discord.py version expected 2.0.0, received %s", + "Dependencies are not updated, run pipenv install. discord.py version expected %s, received %s", + discord_version, discord.__version__, ) sys.exit(0) diff --git a/cogs/modmail.py b/cogs/modmail.py index 7fc9e5b975..a623ed3cdd 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -2,6 +2,7 @@ import re from datetime import datetime, timezone from itertools import zip_longest +import time from typing import Optional, Union, List, Tuple from types import SimpleNamespace @@ -1848,14 +1849,17 @@ async def block( return await ctx.send(embed=embed) reason = f"by {escape_markdown(ctx.author.name)}#{ctx.author.discriminator}" + now = discord.utils.utcnow() if after is not None: if "%" in reason: raise commands.BadArgument('The reason contains illegal character "%".') + unixtime = int(after.dt.replace(tzinfo=timezone.utc).timestamp()) + if after.arg: - reason += f" for `{after.arg}`" - if after.dt > after.now: - reason += f" until {after.dt.isoformat()}" + reason += f" until: <t:{unixtime}:R>" + if after.dt > now: + reason += f" until <t:{unixtime}:f>" reason += "." diff --git a/core/config.py b/core/config.py index 6cff7f0bd7..dd76776092 100644 --- a/core/config.py +++ b/core/config.py @@ -13,7 +13,7 @@ from core._color_data import ALL_COLORS from core.models import DMDisabled, InvalidConfigError, Default, getLogger -from core.time import UserFriendlyTimeSync +from core.time import UserFriendlyTime from core.utils import strtobool logger = getLogger(__name__) @@ -400,7 +400,7 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: isodate.parse_duration(item) except isodate.ISO8601Error: try: - converter = UserFriendlyTimeSync() + converter = UserFriendlyTime() time = converter.convert(None, item) if time.arg: raise ValueError @@ -412,7 +412,8 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: "Unrecognized time, please use ISO-8601 duration format " 'string or a simpler "human readable" time.' ) - item = isodate.duration_isoformat(time.dt - converter.now) + now = discord.utils.utcnow() + item = isodate.duration_isoformat(time.dt - now) return self.__setitem__(key, item) if key in self.booleans: diff --git a/core/time.py b/core/time.py index 26b41b9043..b66f84757f 100644 --- a/core/time.py +++ b/core/time.py @@ -3,223 +3,353 @@ Source: https://github.com/Rapptz/RoboDanny/blob/rewrite/cogs/utils/time.py """ -import re -from datetime import datetime - -import discord -from discord.ext.commands import BadArgument, Converter +from __future__ import annotations +import datetime +from typing import TYPE_CHECKING, Any, Optional, Union import parsedatetime as pdt from dateutil.relativedelta import relativedelta +from .utils import human_join +from discord.ext import commands +from discord import app_commands +import re + +# Monkey patch mins and secs into the units +units = pdt.pdtLocales["en_US"].units +units["minutes"].append("mins") +units["seconds"].append("secs") -from core.models import getLogger +if TYPE_CHECKING: + from typing_extensions import Self + from cogs.utils.context import Context -logger = getLogger(__name__) +class plural: + """https://github.com/Rapptz/RoboDanny/blob/bf7d4226350dff26df4981dd53134eeb2aceeb87/cogs/utils/formats.py#L8-L18""" + + def __init__(self, value: int): + self.value: int = value + + def __format__(self, format_spec: str) -> str: + v = self.value + singular, sep, plural = format_spec.partition("|") + plural = plural or f"{singular}s" + if abs(v) != 1: + return f"{v} {plural}" + return f"{v} {singular}" + + +def format_dt(dt: datetime.datetime, style: Optional[str] = None) -> str: + """https://github.com/Rapptz/RoboDanny/blob/bf7d4226350dff26df4981dd53134eeb2aceeb87/cogs/utils/formats.py#L89-L95""" + if dt.tzinfo is None: + dt = dt.replace(tzinfo=datetime.timezone.utc) + + if style is None: + return f"<t:{int(dt.timestamp())}>" + return f"<t:{int(dt.timestamp())}:{style}>" class ShortTime: compiled = re.compile( - r""" - (?:(?P<years>[0-9])(?:years?|y))? # e.g. 2y - (?:(?P<months>[0-9]{1,2})(?:months?|mo))? # e.g. 9mo - (?:(?P<weeks>[0-9]{1,4})(?:weeks?|w))? # e.g. 10w - (?:(?P<days>[0-9]{1,5})(?:days?|d))? # e.g. 14d - (?:(?P<hours>[0-9]{1,5})(?:hours?|h))? # e.g. 12h - (?:(?P<minutes>[0-9]{1,5})(?:min(?:ute)?s?|m))? # e.g. 10m - (?:(?P<seconds>[0-9]{1,5})(?:sec(?:ond)?s?|s))? # e.g. 15s - """, + """ + (?:(?P<years>[0-9])(?:years?|y))? # e.g. 2y + (?:(?P<months>[0-9]{1,2})(?:months?|mo))? # e.g. 2months + (?:(?P<weeks>[0-9]{1,4})(?:weeks?|w))? # e.g. 10w + (?:(?P<days>[0-9]{1,5})(?:days?|d))? # e.g. 14d + (?:(?P<hours>[0-9]{1,5})(?:hours?|h))? # e.g. 12h + (?:(?P<minutes>[0-9]{1,5})(?:minutes?|m))? # e.g. 10m + (?:(?P<seconds>[0-9]{1,5})(?:seconds?|s))? # e.g. 15s + """, re.VERBOSE, ) - def __init__(self, argument): + discord_fmt = re.compile(r"<t:(?P<ts>[0-9]+)(?:\:?[RFfDdTt])?>") + + dt: datetime.datetime + + def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None): match = self.compiled.fullmatch(argument) if match is None or not match.group(0): - raise BadArgument("Invalid time provided.") + match = self.discord_fmt.fullmatch(argument) + if match is not None: + self.dt = datetime.datetime.fromtimestamp(int(match.group("ts")), tz=datetime.timezone.utc) + return + else: + raise commands.BadArgument("invalid time provided") - data = {k: int(v) for k, v in match.groupdict(default="0").items()} - now = discord.utils.utcnow() + data = {k: int(v) for k, v in match.groupdict(default=0).items()} + now = now or datetime.datetime.now(datetime.timezone.utc) self.dt = now + relativedelta(**data) - -# Monkey patch mins and secs into the units -units = pdt.pdtLocales["en_US"].units -units["minutes"].append("mins") -units["seconds"].append("secs") + @classmethod + async def convert(cls, ctx: Context, argument: str) -> Self: + return cls(argument, now=ctx.message.created_at) class HumanTime: calendar = pdt.Calendar(version=pdt.VERSION_CONTEXT_STYLE) - def __init__(self, argument): - now = discord.utils.utcnow() + def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None): + now = now or datetime.datetime.utcnow() dt, status = self.calendar.parseDT(argument, sourceTime=now) if not status.hasDateOrTime: - raise BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".') + raise commands.BadArgument('invalid time provided, try e.g. "tomorrow" or "3 days"') 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) - self.dt = dt - self._past = dt < now + self.dt: datetime.datetime = dt + self._past: bool = dt < now + + @classmethod + async def convert(cls, ctx: Context, argument: str) -> Self: + return cls(argument, now=ctx.message.created_at) class Time(HumanTime): - def __init__(self, argument): + def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None): try: - short_time = ShortTime(argument) - except Exception: + o = ShortTime(argument, now=now) + except Exception as e: super().__init__(argument) else: - self.dt = short_time.dt + self.dt = o.dt self._past = False class FutureTime(Time): - def __init__(self, argument): - super().__init__(argument) + def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None): + super().__init__(argument, now=now) if self._past: - raise BadArgument("The time is in the past.") + raise commands.BadArgument("this time is in the past") -class UserFriendlyTimeSync(Converter): - """That way quotes aren't absolutely necessary.""" +class BadTimeTransform(app_commands.AppCommandError): + pass + + +class TimeTransformer(app_commands.Transformer): + async def transform(self, interaction, value: str) -> datetime.datetime: + now = interaction.created_at + try: + short = ShortTime(value, now=now) + except commands.BadArgument: + try: + human = FutureTime(value, now=now) + except commands.BadArgument as e: + raise BadTimeTransform(str(e)) from None + else: + return human.dt + else: + return short.dt + + +class FriendlyTimeResult: + dt: datetime.datetime + arg: str - def __init__(self): - self.raw: str = None - self.dt: datetime = None - self.arg = None - self.now: datetime = None + __slots__ = ("dt", "arg") - def check_constraints(self, now, remaining): + def __init__(self, dt: datetime.datetime): + self.dt = dt + self.arg = "" + + async def ensure_constraints( + self, ctx: Context, uft: UserFriendlyTime, now: datetime.datetime, remaining: str + ) -> None: if self.dt < now: - raise BadArgument("This time is in the past.") + raise commands.BadArgument("This time is in the past.") - self.arg = remaining - return self + # if not remaining: + # if uft.default is None: + # raise commands.BadArgument("Missing argument after the time.") + # remaining = uft.default - def convert(self, ctx, argument): - self.raw = argument - remaining = "" - try: - calendar = HumanTime.calendar - regex = ShortTime.compiled - self.dt = self.now = discord.utils.utcnow() + if uft.converter is not None: + self.arg = await uft.converter.convert(ctx, remaining) + else: + self.arg = remaining - match = regex.match(argument) - if match is not None and match.group(0): - 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 self.check_constraints(self.now, remaining) - - # apparently nlp does not like "from now" - # it likes "from x" in other cases though - # so let me handle the 'now' case - if argument.endswith(" from now"): - argument = argument[:-9].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: - return self.check_constraints(self.now, argument) - - # handle the following cases: - # "date time" foo - # date time foo - # foo date time - - # first the first two cases: - dt, status, begin, end, _ = elements[0] - - if not status.hasDateOrTime: - return self.check_constraints(self.now, argument) - - if begin not in (0, 1) and end != len(argument): - raise BadArgument( - "Time is either in an inappropriate location, which must " - "be either at the end or beginning of your input, or I " - "just flat out did not understand what you meant. Sorry." - ) - if not status.hasTime: - # replace it with the current time - dt = dt.replace( - hour=self.now.hour, - minute=self.now.minute, - second=self.now.second, - microsecond=self.now.microsecond, - ) +class UserFriendlyTime(commands.Converter): + """That way quotes aren't absolutely necessary.""" - # if midnight is provided, just default to next day - if status.accuracy == pdt.pdtContext.ACU_HALFDAY: - dt = dt.replace(day=self.now.day + 1) + def __init__( + self, + converter: Optional[Union[type[commands.Converter], commands.Converter]] = None, + *, + default: Any = None, + ): + if isinstance(converter, type) and issubclass(converter, commands.Converter): + converter = converter() + + if converter is not None and not isinstance(converter, commands.Converter): + raise TypeError("commands.Converter subclass necessary.") + + self.converter: commands.Converter = converter # type: ignore # It doesn't understand this narrowing + self.default: Any = default + + async def convert(self, ctx: Context, argument: str) -> FriendlyTimeResult: + calendar = HumanTime.calendar + regex = ShortTime.compiled + now = ctx.message.created_at + + match = regex.match(argument) + if match is not None and match.group(0): + data = {k: int(v) for k, v in match.groupdict(default=0).items()} + remaining = argument[match.end() :].strip() + result = FriendlyTimeResult(now + relativedelta(**data)) + await result.ensure_constraints(ctx, self, now, remaining) + return result - self.dt = dt + if match is None or not match.group(0): + match = ShortTime.discord_fmt.match(argument) + if match is not None: + result = FriendlyTimeResult( + datetime.datetime.fromtimestamp(int(match.group("ts")), tz=datetime.timezone.utc) + ) + remaining = argument[match.end() :].strip() + await result.ensure_constraints(ctx, self, now, remaining) + return result - if begin in (0, 1): - if begin == 1: - # check if it's quoted: - if argument[0] != '"': - raise BadArgument("Expected quote before time input...") + # apparently nlp does not like "from now" + # it likes "from x" in other cases though so let me handle the 'now' case + if argument.endswith("from now"): + argument = argument[:-8].strip() - if not (end < len(argument) and argument[end] == '"'): - raise BadArgument("If the time is quoted, you must unquote it.") + 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:] - remaining = argument[end + 1 :].lstrip(" ,.!") - else: - remaining = argument[end:].lstrip(" ,.!") - elif len(argument) == end: - remaining = argument[:begin].strip() + elements = calendar.nlp(argument, sourceTime=now) + if elements is None or len(elements) == 0: + raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".') - return self.check_constraints(self.now, remaining) - except Exception: - logger.exception("Something went wrong while parsing the time.") - raise + # handle the following cases: + # "date time" foo + # date time foo + # foo date time + # first the first two cases: + dt, status, begin, end, dt_string = elements[0] -class UserFriendlyTime(UserFriendlyTimeSync): - async def convert(self, ctx, argument): - return super().convert(ctx, argument) + if not status.hasDateOrTime: + raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".') + if begin not in (0, 1) and end != len(argument): + raise commands.BadArgument( + "Time is either in an inappropriate location, which " + "must be either at the end or beginning of your input, " + "or I just flat out did not understand what you meant. Sorry." + ) -def human_timedelta(dt, *, spec="R"): - if spec == "manual": - now = discord.utils.utcnow() - if dt > now: - delta = relativedelta(dt, now) - suffix = "" - else: - delta = relativedelta(now, dt) - suffix = " ago" + 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) + + # if midnight is provided, just default to next day + if status.accuracy == pdt.pdtContext.ACU_HALFDAY: + dt = dt.replace(day=now.day + 1) - if delta.microseconds and delta.seconds: - delta = delta + relativedelta(seconds=+1) + result = FriendlyTimeResult(dt.replace(tzinfo=datetime.timezone.utc)) + remaining = "" - attrs = ["years", "months", "days", "hours", "minutes", "seconds"] + if begin in (0, 1): + if begin == 1: + # check if it's quoted: + if argument[0] != '"': + raise commands.BadArgument("Expected quote before time input...") - output = [] - for attr in attrs: - elem = getattr(delta, attr) - if not elem: - continue + if not (end < len(argument) and argument[end] == '"'): + raise commands.BadArgument("If the time is quoted, you must unquote it.") - if elem > 1: - output.append(f"{elem} {attr}") + remaining = argument[end + 1 :].lstrip(" ,.!") else: - output.append(f"{elem} {attr[:-1]}") - - if not output: - return "now" - if len(output) == 1: - return output[0] + suffix - if len(output) == 2: - return f"{output[0]} and {output[1]}{suffix}" - return f"{output[0]}, {output[1]} and {output[2]}{suffix}" + remaining = argument[end:].lstrip(" ,.!") + elif len(argument) == end: + remaining = argument[:begin].strip() + + await result.ensure_constraints(ctx, self, now, remaining) + return result + + +def human_timedelta( + dt: datetime.datetime, + *, + source: Optional[datetime.datetime] = None, + accuracy: Optional[int] = 3, + brief: bool = False, + suffix: bool = True, +) -> str: + now = source or datetime.datetime.now(datetime.timezone.utc) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=datetime.timezone.utc) + + if now.tzinfo is None: + now = now.replace(tzinfo=datetime.timezone.utc) + + # Microsecond free zone + now = now.replace(microsecond=0) + dt = dt.replace(microsecond=0) + + # This implementation uses relativedelta instead of the much more obvious + # divmod approach with seconds because the seconds approach is not entirely + # accurate once you go over 1 week in terms of accuracy since you have to + # hardcode a month as 30 or 31 days. + # A query like "11 months" can be interpreted as "!1 months and 6 days" + if dt > now: + delta = relativedelta(dt, now) + output_suffix = "" else: - unixtime = int(dt.timestamp()) - return f"<t:{unixtime}:{spec}>" + delta = relativedelta(now, dt) + output_suffix = " ago" if suffix else "" + + attrs = [ + ("year", "y"), + ("month", "mo"), + ("day", "d"), + ("hour", "h"), + ("minute", "m"), + ("second", "s"), + ] + + output = [] + for attr, brief_attr in attrs: + elem = getattr(delta, attr + "s") + if not elem: + continue + + if attr == "day": + weeks = delta.weeks + if weeks: + elem -= weeks * 7 + if not brief: + output.append(format(plural(weeks), "week")) + else: + output.append(f"{weeks}w") + + if elem <= 0: + continue + + if brief: + output.append(f"{elem}{brief_attr}") + else: + output.append(format(plural(elem), attr)) + + if accuracy is not None: + output = output[:accuracy] + + if len(output) == 0: + return "now" + else: + if not brief: + return human_join(output, final="and") + output_suffix + else: + return " ".join(output) + output_suffix + + +def format_relative(dt: datetime.datetime) -> str: + return format_dt(dt, "R") diff --git a/core/utils.py b/core/utils.py index 30efb05e75..ce28f243c1 100644 --- a/core/utils.py +++ b/core/utils.py @@ -172,10 +172,19 @@ def parse_image_url(url: str, *, convert_size=True) -> str: return "" -def human_join(strings): - if len(strings) <= 2: - return " or ".join(strings) - return ", ".join(strings[: len(strings) - 1]) + " or " + strings[-1] +def human_join(seq: typing.Sequence[str], delim: str = ", ", final: str = "or") -> str: + """https://github.com/Rapptz/RoboDanny/blob/bf7d4226350dff26df4981dd53134eeb2aceeb87/cogs/utils/formats.py#L21-L32""" + size = len(seq) + if size == 0: + return "" + + if size == 1: + return seq[0] + + if size == 2: + return f"{seq[0]} {final} {seq[1]}" + + return delim.join(seq[:-1]) + f" {final} {seq[-1]}" def days(day: typing.Union[str, int]) -> str: diff --git a/pyproject.toml b/pyproject.toml index b3dea8293f..b1826a921d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev17' +version = '4.0.0-dev18' description = "Modmail is similar to Reddit's 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 = [ diff --git a/requirements.txt b/requirements.txt index edc784029b..313426315e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, dnspython==2.2.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' emoji==1.7.0 frozenlist==1.3.0; python_version >= '3.7' -git+https://github.com/Rapptz/discord.py.git@e9c7c09ebfe780d8f9de1e9e4d3c2dddf85eccfd#egg=discord.py +discord.py==2.0.1 idna==3.3; python_version >= '3.5' isodate==0.6.1 lottie[pdf]==0.6.11 From 3ea3cf2f8cbc67e39e9a85beb7bfb88934aca22d Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 6 Sep 2022 22:40:54 +0800 Subject: [PATCH 1091/1221] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27165ebda8..0d32c078b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Delete channel auto close functionality now works. - Improved error handling for autoupdate. ([PR #3161](https://github.com/kyb3r/modmail/pull/3161)) - Skip loading of already-loaded cog. ([PR #3172](https://github.com/kyb3r/modmail/pull/3172)) +- Respect plugin's `cog_command_error`. ([GH #3170](https://github.com/kyb3r/modmail/issues/3170), [PR #3178](https://github.com/kyb3r/modmail/pull/3178)) ### Internal From ecffec2604e8e52b9248f1971751996dc383c461 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 6 Sep 2022 23:01:09 +0800 Subject: [PATCH 1092/1221] use silent as typing literal, resolve #3179 --- CHANGELOG.md | 1 + cogs/modmail.py | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d32c078b1..8789209eb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Improved error handling for autoupdate. ([PR #3161](https://github.com/kyb3r/modmail/pull/3161)) - Skip loading of already-loaded cog. ([PR #3172](https://github.com/kyb3r/modmail/pull/3172)) - Respect plugin's `cog_command_error`. ([GH #3170](https://github.com/kyb3r/modmail/issues/3170), [PR #3178](https://github.com/kyb3r/modmail/pull/3178)) +- Use silent as a typing literal for contacting. ([GH #3179](https://github.com/kyb3r/modmail/issues/3179)) ### Internal diff --git a/cogs/modmail.py b/cogs/modmail.py index a623ed3cdd..68c4c4715d 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone from itertools import zip_longest import time -from typing import Optional, Union, List, Tuple +from typing import Optional, Union, List, Tuple, Literal from types import SimpleNamespace import discord @@ -1500,9 +1500,9 @@ async def selfcontact(self, ctx): async def contact( self, ctx, - users: commands.Greedy[Union[discord.Member, discord.User, discord.Role]], + users: commands.Greedy[Union[Literal["silent", "silently"], discord.Member, discord.User, discord.Role]], *, - category: Union[SimilarCategoryConverter, str] = None, + category: SimilarCategoryConverter = None, manual_trigger=True, ): """ @@ -1516,16 +1516,22 @@ async def contact( A maximum of 5 users are allowed. `options` can be `silent` or `silently`. """ - silent = False + silent = any(x in users for x in ("silent", "silently")) + if silent: + try: + users.remove("silent") + except ValueError: + pass + + try: + users.remove("silently") + except ValueError: + pass + + print(users, silent) if isinstance(category, str): category = category.split() - # just check the last element in the list - if category[-1].lower() in ("silent", "silently"): - silent = True - # remove the last element as we no longer need it - category.pop() - category = " ".join(category) if category: try: From 7b0c360dd249f9dd6cc75b8f0c1cfc2f76ddc91e Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 6 Sep 2022 23:01:19 +0800 Subject: [PATCH 1093/1221] fix formatting --- cogs/modmail.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 68c4c4715d..a21dc205b4 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1500,7 +1500,9 @@ async def selfcontact(self, ctx): async def contact( self, ctx, - users: commands.Greedy[Union[Literal["silent", "silently"], discord.Member, discord.User, discord.Role]], + users: commands.Greedy[ + Union[Literal["silent", "silently"], discord.Member, discord.User, discord.Role] + ], *, category: SimilarCategoryConverter = None, manual_trigger=True, From 60e8444a46c7f94ab923e0cd0c2ca8351172985a Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 6 Sep 2022 23:01:50 +0800 Subject: [PATCH 1094/1221] bump ver --- bot.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 846eccd3ac..b05d1435c8 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev18" +__version__ = "4.0.0-dev19" import asyncio diff --git a/pyproject.toml b/pyproject.toml index b1826a921d..9a22625dba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev18' +version = '4.0.0-dev19' description = "Modmail is similar to Reddit's 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 ec399c3e579ff31492ad7223a9242809b4f9bb7d Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 6 Sep 2022 23:07:24 +0800 Subject: [PATCH 1095/1221] Autoupdate now automatically updates pipenv dependencies if possible --- CHANGELOG.md | 1 + bot.py | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8789209eb1..e584286230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Use discord relative timedeltas. ([GH #3046](https://github.com/kyb3r/modmail/issues/3046)) - Use discord native buttons for all paginator sessions. - `?help` and `?blocked` paginator sessions now have better multi-page UI. +- Autoupdate now automatically updates pipenv dependencies if possible. ### Fixed diff --git a/bot.py b/bot.py index b05d1435c8..418916b528 100644 --- a/bot.py +++ b/bot.py @@ -1674,17 +1674,28 @@ async def autoupdate(self): return elif res != "Already up to date.": + if os.getenv("PIPENV_ACTIVE"): + # Update pipenv if possible + await asyncio.create_subprocess_shell( + "pipenv sync", + stderr=PIPE, + stdout=PIPE, + ) + message = "" + else: + message = "\n\nDo manually update dependencies if your bot has crashed." + logger.info("Bot has been updated.") channel = self.update_channel if self.hosting_method in (HostingMethod.PM2, HostingMethod.SYSTEMD): embed = discord.Embed(title="Bot has been updated", color=self.main_color) - embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version} {message}") if self.config["update_notifications"]: await channel.send(embed=embed) else: embed = discord.Embed( title="Bot has been updated and is logging out.", - description="If you do not have an auto-restart setup, please manually start the bot.", + description=f"If you do not have an auto-restart setup, please manually start the bot. {message}", color=self.main_color, ) embed.set_footer(text=f"Updating Modmail v{self.version} -> v{latest.version}") From ddc34c0624e5b18c4f24f923813d79ccfd94424f Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 6 Sep 2022 23:07:40 +0800 Subject: [PATCH 1096/1221] formatting --- bot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 418916b528..a990ac772c 100644 --- a/bot.py +++ b/bot.py @@ -1689,7 +1689,9 @@ async def autoupdate(self): channel = self.update_channel if self.hosting_method in (HostingMethod.PM2, HostingMethod.SYSTEMD): embed = discord.Embed(title="Bot has been updated", color=self.main_color) - embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version} {message}") + embed.set_footer( + text=f"Updating Modmail v{self.version} " f"-> v{latest.version} {message}" + ) if self.config["update_notifications"]: await channel.send(embed=embed) else: From 1d3aac9bec0ea71f8f06720e33d5d04f7ac0b047 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 7 Sep 2022 00:09:51 +0800 Subject: [PATCH 1097/1221] Fix time args, and use literals for close --- cogs/modmail.py | 19 +++++++------------ core/time.py | 29 +++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index a21dc205b4..279df07dd6 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -464,7 +464,7 @@ async def send_scheduled_close_message(self, ctx, after, silent=False): @commands.command(usage="[after] [close message]") @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def close(self, ctx, *, after: UserFriendlyTime = None): + async def close(self, ctx, option: Optional[Literal["silent", "silently", "cancel"]]='', *, after: UserFriendlyTime = None): """ Close the current thread. @@ -486,15 +486,11 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): thread = ctx.thread - now = discord.utils.utcnow() - - close_after = (after.dt - now).total_seconds() if after else 0 - message = after.arg if after else None - silent = str(message).lower() in {"silent", "silently"} - cancel = str(message).lower() == "cancel" + close_after = (after.dt - after.now).total_seconds() if after else 0 + silent = any(x == option for x in {"silent", "silently"}) + cancel = option == "cancel" if cancel: - if thread.close_task is not None or thread.auto_close_task is not None: await thread.cancel_closure(all=True) embed = discord.Embed( @@ -508,10 +504,11 @@ async def close(self, ctx, *, after: UserFriendlyTime = None): return await ctx.send(embed=embed) + message = after.arg if after else None if self.bot.config["require_close_reason"] and message is None: raise commands.BadArgument("Provide a reason for closing the thread.") - if after and after.dt > now: + if after and after.dt > after.now: await self.send_scheduled_close_message(ctx, after, silent) await thread.close(closer=ctx.author, after=close_after, message=message, silent=silent) @@ -1530,7 +1527,6 @@ async def contact( except ValueError: pass - print(users, silent) if isinstance(category, str): category = category.split() @@ -1857,7 +1853,6 @@ async def block( return await ctx.send(embed=embed) reason = f"by {escape_markdown(ctx.author.name)}#{ctx.author.discriminator}" - now = discord.utils.utcnow() if after is not None: if "%" in reason: @@ -1866,7 +1861,7 @@ async def block( if after.arg: reason += f" until: <t:{unixtime}:R>" - if after.dt > now: + if after.dt > after.now: reason += f" until <t:{unixtime}:f>" reason += "." diff --git a/core/time.py b/core/time.py index b66f84757f..d0698d6381 100644 --- a/core/time.py +++ b/core/time.py @@ -20,8 +20,8 @@ units["seconds"].append("secs") if TYPE_CHECKING: + from discord.ext.commands import Context from typing_extensions import Self - from cogs.utils.context import Context class plural: @@ -82,7 +82,7 @@ def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None): self.dt = now + relativedelta(**data) @classmethod - async def convert(cls, ctx: Context, argument: str) -> Self: + async def convert(cls, ctx: Context, argument: str) -> Self: return cls(argument, now=ctx.message.created_at) @@ -146,14 +146,23 @@ async def transform(self, interaction, value: str) -> datetime.datetime: return short.dt +## CHANGE: Added now class FriendlyTimeResult: dt: datetime.datetime + now: datetime.datetime arg: str - __slots__ = ("dt", "arg") + __slots__ = ("dt", "arg", "now") - def __init__(self, dt: datetime.datetime): + def __init__(self, dt: datetime.datetime, now: datetime.datetime=None): self.dt = dt + self.now = now + + if now is None: + self.now = dt + else: + self.now = now + self.arg = "" async def ensure_constraints( @@ -162,6 +171,7 @@ async def ensure_constraints( if self.dt < now: raise commands.BadArgument("This time is in the past.") + # CHANGE # if not remaining: # if uft.default is None: # raise commands.BadArgument("Missing argument after the time.") @@ -200,7 +210,7 @@ async def convert(self, ctx: Context, argument: str) -> FriendlyTimeResult: if match is not None and match.group(0): data = {k: int(v) for k, v in match.groupdict(default=0).items()} remaining = argument[match.end() :].strip() - result = FriendlyTimeResult(now + relativedelta(**data)) + result = FriendlyTimeResult(now + relativedelta(**data), now) await result.ensure_constraints(ctx, self, now, remaining) return result @@ -208,7 +218,7 @@ async def convert(self, ctx: Context, argument: str) -> FriendlyTimeResult: match = ShortTime.discord_fmt.match(argument) if match is not None: result = FriendlyTimeResult( - datetime.datetime.fromtimestamp(int(match.group("ts")), tz=datetime.timezone.utc) + datetime.datetime.fromtimestamp(int(match.group("ts")), now, tz=datetime.timezone.utc) ) remaining = argument[match.end() :].strip() await result.ensure_constraints(ctx, self, now, remaining) @@ -226,7 +236,10 @@ async def convert(self, ctx: Context, argument: str) -> FriendlyTimeResult: elements = calendar.nlp(argument, sourceTime=now) if elements is None or len(elements) == 0: - raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".') + # CHANGE + result = FriendlyTimeResult(now) + await result.ensure_constraints(ctx, self, now, argument) + return result # handle the following cases: # "date time" foo @@ -254,7 +267,7 @@ async def convert(self, ctx: Context, argument: str) -> FriendlyTimeResult: if status.accuracy == pdt.pdtContext.ACU_HALFDAY: dt = dt.replace(day=now.day + 1) - result = FriendlyTimeResult(dt.replace(tzinfo=datetime.timezone.utc)) + result = FriendlyTimeResult(dt.replace(tzinfo=datetime.timezone.utc), now) remaining = "" if begin in (0, 1): From 30430a6c255407729472445961c11dd9c6a4bf06 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 7 Sep 2022 00:10:19 +0800 Subject: [PATCH 1098/1221] bump ver --- bot.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index a990ac772c..7c9f19f920 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev19" +__version__ = "4.0.0-dev20" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 9a22625dba..f4faae86fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev19' +version = '4.0.0-dev20' description = "Modmail is similar to Reddit's 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 420d4dc577c1ac5d68360dac81a9f9a023d473b0 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 7 Sep 2022 00:10:56 +0800 Subject: [PATCH 1099/1221] formatting --- cogs/modmail.py | 8 +++++++- core/time.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 279df07dd6..d1528f9b46 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -464,7 +464,13 @@ async def send_scheduled_close_message(self, ctx, after, silent=False): @commands.command(usage="[after] [close message]") @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() - async def close(self, ctx, option: Optional[Literal["silent", "silently", "cancel"]]='', *, after: UserFriendlyTime = None): + async def close( + self, + ctx, + option: Optional[Literal["silent", "silently", "cancel"]] = "", + *, + after: UserFriendlyTime = None, + ): """ Close the current thread. diff --git a/core/time.py b/core/time.py index d0698d6381..90aca9c021 100644 --- a/core/time.py +++ b/core/time.py @@ -82,7 +82,7 @@ def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None): self.dt = now + relativedelta(**data) @classmethod - async def convert(cls, ctx: Context, argument: str) -> Self: + async def convert(cls, ctx: Context, argument: str) -> Self: return cls(argument, now=ctx.message.created_at) @@ -154,7 +154,7 @@ class FriendlyTimeResult: __slots__ = ("dt", "arg", "now") - def __init__(self, dt: datetime.datetime, now: datetime.datetime=None): + def __init__(self, dt: datetime.datetime, now: datetime.datetime = None): self.dt = dt self.now = now From 711fb68c4d0378b3f79a83ce18a1778ffc1d2f64 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 7 Sep 2022 00:13:18 +0800 Subject: [PATCH 1100/1221] formatting --- bot.py | 1 - cogs/modmail.py | 1 - cogs/utility.py | 2 +- core/time.py | 4 ++-- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index 7c9f19f920..5eb3378d45 100644 --- a/bot.py +++ b/bot.py @@ -18,7 +18,6 @@ import discord import isodate from aiohttp import ClientSession, ClientResponseError -from discord.utils import MISSING from discord.ext import commands, tasks from discord.ext.commands.view import StringView from emoji import UNICODE_EMOJI diff --git a/cogs/modmail.py b/cogs/modmail.py index d1528f9b46..ecdf7144e6 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -2,7 +2,6 @@ import re from datetime import datetime, timezone from itertools import zip_longest -import time from typing import Optional, Union, List, Tuple, Literal from types import SimpleNamespace diff --git a/cogs/utility.py b/cogs/utility.py index 7f623141b7..af0e70beec 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -197,7 +197,7 @@ async def send_error_message(self, error): if snippet_aliases: embed.add_field( - name=f"Aliases to this snippet:", value=",".join(snippet_aliases), inline=False + name="Aliases to this snippet:", value=",".join(snippet_aliases), inline=False ) return await self.get_destination().send(embed=embed) diff --git a/core/time.py b/core/time.py index 90aca9c021..99c1ed7713 100644 --- a/core/time.py +++ b/core/time.py @@ -111,7 +111,7 @@ class Time(HumanTime): def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None): try: o = ShortTime(argument, now=now) - except Exception as e: + except Exception: super().__init__(argument) else: self.dt = o.dt @@ -146,7 +146,7 @@ async def transform(self, interaction, value: str) -> datetime.datetime: return short.dt -## CHANGE: Added now +# CHANGE: Added now class FriendlyTimeResult: dt: datetime.datetime now: datetime.datetime From 3d7b6261ea5c7e7eb36da0cc9c2962621e475b39 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 7 Sep 2022 00:25:22 +0800 Subject: [PATCH 1101/1221] add comnts to lint file --- .github/workflows/lints.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index a7418930be..21bf39368e 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -23,6 +23,7 @@ jobs: run: | python -m pip install --upgrade pip pipenv pipenv install --dev --system + # to refresh: bandit -f json -o .bandit_baseline.json -r . # - name: Bandit syntax check # run: bandit -r . -b .bandit_baseline.json - name: Pylint From 039347fb05b38ab39b4ea0acad7be3ee1cab27e1 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 20 Sep 2022 22:10:47 +0800 Subject: [PATCH 1102/1221] Use new timestamps for userinfo and fix potential blocked parsing --- bot.py | 2 +- cogs/modmail.py | 48 ++++++++++++++++++++++++++++++++++++++---------- core/thread.py | 9 ++++----- core/time.py | 13 ++----------- pyproject.toml | 2 +- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/bot.py b/bot.py index a96a58fd6e..23c911b8ea 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0-dev20" +__version__ = "4.0.0" import asyncio diff --git a/cogs/modmail.py b/cogs/modmail.py index ecdf7144e6..85b4af62ac 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1654,21 +1654,48 @@ async def blocked(self, ctx): blocked_users = list(self.bot.blocked_users.items()) for id_, reason in blocked_users: # parse "reason" and check if block is expired - # etc "blah blah blah... until 2019-10-14T21:12:45.559948." - end_time = re.search(r"until ([^`]+?)\.$", reason) - if end_time is None: + # etc "blah blah blah... until <t:XX:f>." + end_time = re.search(r"until <t:(\d+):(?:R|f)>.$", reason) + attempts = [ # backwards compat - end_time = re.search(r"%([^%]+?)%", reason) + re.search(r"until ([^`]+?)\.$", reason), + re.search(r"%([^%]+?)%", reason), + ] + if end_time is None: + for i in attempts: + if i is not None: + end_time = i + break + if end_time is not None: + # found a deprecated version + try: + after = ( + datetime.fromisoformat(end_time.group(1)).replace(tzinfo=timezone.utc) - now + ).total_seconds() + except ValueError: + logger.warning( + r"Broken block message for user %s, block and unblock again with a different message to prevent further issues", + id_, + ) + continue logger.warning( r"Deprecated time message for user %s, block and unblock again to update.", id_, ) + else: + try: + after = ( + datetime.fromtimestamp(int(end_time.group(1))).replace(tzinfo=timezone.utc) - now + ).total_seconds() + except ValueError: + logger.warning( + r"Broken block message for user %s, block and unblock again with a different message to prevent further issues", + id_, + ) + continue if end_time is not None: - after = ( - datetime.fromisoformat(end_time.group(1)).replace(tzinfo=timezone.utc) - now - ).total_seconds() if after <= 0: # No longer blocked self.bot.blocked_users.pop(str(id_)) @@ -1862,12 +1889,13 @@ async def block( if after is not None: if "%" in reason: raise commands.BadArgument('The reason contains illegal character "%".') - unixtime = int(after.dt.replace(tzinfo=timezone.utc).timestamp()) if after.arg: - reason += f" until: <t:{unixtime}:R>" + fmt_dt = discord.utils.format_dt(after.dt, "R") if after.dt > after.now: - reason += f" until <t:{unixtime}:f>" + fmt_dt = discord.utils.format_dt(after.dt, "f") + + reason += f" until {fmt_dt}" reason += "." diff --git a/core/thread.py b/core/thread.py index 606a204b37..99a805fef5 100644 --- a/core/thread.py +++ b/core/thread.py @@ -322,10 +322,10 @@ def _format_info_embed(self, user, log_url, log_count, color): role_names = separator.join(roles) - created = str((time - user.created_at).days) user_info = [] if self.bot.config["thread_show_account_age"]: - user_info.append(f" was created {days(created)}") + created = discord.utils.format_dt(user.created_at, "R") + user_info.append(f" was created {created}") embed = discord.Embed(color=color, description=user.mention, timestamp=time) @@ -337,10 +337,9 @@ def _format_info_embed(self, user, log_url, log_count, color): if member is not None: embed.set_author(name=str(user), icon_url=member.display_avatar.url, url=log_url) - joined = str((time - member.joined_at).days) - # embed.add_field(name='Joined', value=joined + days(joined)) if self.bot.config["thread_show_join_age"]: - user_info.append(f"joined {days(joined)}") + joined = discord.utils.format_dt(member.joined_at, "R") + user_info.append(f"joined {joined}") if member.nick: embed.add_field(name="Nickname", value=member.nick, inline=True) diff --git a/core/time.py b/core/time.py index 99c1ed7713..64dcd88507 100644 --- a/core/time.py +++ b/core/time.py @@ -6,6 +6,7 @@ from __future__ import annotations import datetime +import discord from typing import TYPE_CHECKING, Any, Optional, Union import parsedatetime as pdt from dateutil.relativedelta import relativedelta @@ -39,16 +40,6 @@ def __format__(self, format_spec: str) -> str: return f"{v} {singular}" -def format_dt(dt: datetime.datetime, style: Optional[str] = None) -> str: - """https://github.com/Rapptz/RoboDanny/blob/bf7d4226350dff26df4981dd53134eeb2aceeb87/cogs/utils/formats.py#L89-L95""" - if dt.tzinfo is None: - dt = dt.replace(tzinfo=datetime.timezone.utc) - - if style is None: - return f"<t:{int(dt.timestamp())}>" - return f"<t:{int(dt.timestamp())}:{style}>" - - class ShortTime: compiled = re.compile( """ @@ -365,4 +356,4 @@ def human_timedelta( def format_relative(dt: datetime.datetime) -> str: - return format_dt(dt, "R") + return discord.utils.format_dt(dt, "R") diff --git a/pyproject.toml b/pyproject.toml index f4faae86fd..a19e436399 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0-dev20' +version = '4.0.0' description = "Modmail is similar to Reddit's 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 ba4d6fb81fe61628fda805d56d5d9a0a2f560cdf Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 20 Sep 2022 22:11:47 +0800 Subject: [PATCH 1103/1221] Fix removeprefix issue by @Jerrie-Aries https://github.com/kyb3r/modmail/commit/ecc92e4bbf44d0f44d2df902e16a30734320070e#r83768323 --- bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 23c911b8ea..52ba65b960 100644 --- a/bot.py +++ b/bot.py @@ -1000,7 +1000,8 @@ async def get_contexts(self, message, *, cls=commands.Context): # This needs to be done before checking for aliases since # snippets can have multiple words. try: - snippet_text = self.snippets[message.content.strip(invoked_prefix)] + # Use removeprefix once PY3.9+ + snippet_text = self.snippets[message.content[len(invoked_prefix) :]] except KeyError: snippet_text = None From c6b4f530de7765839278546ed7351433450d99f9 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 20 Sep 2022 22:31:04 +0800 Subject: [PATCH 1104/1221] Fix block logic and rewrite into helper function --- bot.py | 41 +++++++++++------------------------ cogs/modmail.py | 57 +++++++------------------------------------------ core/thread.py | 1 - core/time.py | 4 ++-- core/utils.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 80 deletions(-) diff --git a/bot.py b/bot.py index 52ba65b960..9f13e3e04e 100644 --- a/bot.py +++ b/bot.py @@ -47,7 +47,7 @@ ) from core.thread import ThreadManager from core.time import human_timedelta -from core.utils import normalize_alias, parse_alias, truncate, tryint +from core.utils import extract_block_timestamp, normalize_alias, parse_alias, truncate, tryint logger = getLogger(__name__) @@ -735,21 +735,13 @@ def check_manual_blocked_roles(self, author: discord.Member) -> bool: if str(r.id) in self.blocked_roles: blocked_reason = self.blocked_roles.get(str(r.id)) or "" - now = discord.utils.utcnow() - - # 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 role %s, block and unblock again to update.", - r.name, - ) + + try: + end_time, after = extract_block_timestamp(blocked_reason, author.id) + except ValueError: + return False if end_time is not None: - after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() if after <= 0: # No longer blocked self.blocked_roles.pop(str(r.id)) @@ -765,26 +757,19 @@ def check_manual_blocked(self, author: discord.Member) -> bool: return True blocked_reason = self.blocked_users.get(str(author.id)) or "" - now = discord.utils.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, - ) + + try: + end_time, after = extract_block_timestamp(blocked_reason, author.id) + except ValueError: + return False 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)) @@ -891,7 +876,7 @@ async def add_reaction( if reaction != "disable": try: await msg.add_reaction(reaction) - except (discord.HTTPException, discord.InvalidArgument) as e: + except (discord.HTTPException, discord.BadArgument) as e: logger.warning("Failed to add reaction %s: %s.", reaction, e) return False return True @@ -1316,7 +1301,7 @@ async def handle_reaction_events(self, payload): for msg in linked_messages: await msg.remove_reaction(reaction, self.user) await message.remove_reaction(reaction, self.user) - except (discord.HTTPException, discord.InvalidArgument) as e: + except (discord.HTTPException, discord.BadArgument) as e: logger.warning("Failed to remove reaction: %s", e) async def handle_react_to_contact(self, payload): diff --git a/cogs/modmail.py b/cogs/modmail.py index 85b4af62ac..b6ebe81baf 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1654,46 +1654,10 @@ async def blocked(self, ctx): blocked_users = list(self.bot.blocked_users.items()) for id_, reason in blocked_users: # parse "reason" and check if block is expired - # etc "blah blah blah... until <t:XX:f>." - end_time = re.search(r"until <t:(\d+):(?:R|f)>.$", reason) - attempts = [ - # backwards compat - re.search(r"until ([^`]+?)\.$", reason), - re.search(r"%([^%]+?)%", reason), - ] - if end_time is None: - for i in attempts: - if i is not None: - end_time = i - break - - if end_time is not None: - # found a deprecated version - try: - after = ( - datetime.fromisoformat(end_time.group(1)).replace(tzinfo=timezone.utc) - now - ).total_seconds() - except ValueError: - logger.warning( - r"Broken block message for user %s, block and unblock again with a different message to prevent further issues", - id_, - ) - continue - logger.warning( - r"Deprecated time message for user %s, block and unblock again to update.", - id_, - ) - else: - try: - after = ( - datetime.fromtimestamp(int(end_time.group(1))).replace(tzinfo=timezone.utc) - now - ).total_seconds() - except ValueError: - logger.warning( - r"Broken block message for user %s, block and unblock again with a different message to prevent further issues", - id_, - ) - continue + try: + end_time, after = extract_block_timestamp(reason, id_) + except ValueError: + continue if end_time is not None: if after <= 0: @@ -1713,15 +1677,10 @@ async def blocked(self, ctx): for id_, reason in blocked_roles: # parse "reason" and check if block is expired # 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 role %s, block and unblock again to update.", - id_, - ) + try: + end_time, after = extract_block_timestamp(reason, id_) + except ValueError: + continue if end_time is not None: after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() diff --git a/core/thread.py b/core/thread.py index 99a805fef5..c9d8ae5bcb 100644 --- a/core/thread.py +++ b/core/thread.py @@ -22,7 +22,6 @@ from core.time import human_timedelta from core.utils import ( is_image_url, - days, parse_channel_topic, match_title, match_user_id, diff --git a/core/time.py b/core/time.py index 64dcd88507..963eac5a6e 100644 --- a/core/time.py +++ b/core/time.py @@ -63,7 +63,7 @@ def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None): if match is None or not match.group(0): match = self.discord_fmt.fullmatch(argument) if match is not None: - self.dt = datetime.datetime.fromtimestamp(int(match.group("ts")), tz=datetime.timezone.utc) + self.dt = datetime.datetime.utcfromtimestamp(int(match.group("ts")), tz=datetime.timezone.utc) return else: raise commands.BadArgument("invalid time provided") @@ -209,7 +209,7 @@ async def convert(self, ctx: Context, argument: str) -> FriendlyTimeResult: match = ShortTime.discord_fmt.match(argument) if match is not None: result = FriendlyTimeResult( - datetime.datetime.fromtimestamp(int(match.group("ts")), now, tz=datetime.timezone.utc) + datetime.datetime.utcfromtimestamp(int(match.group("ts")), now, tz=datetime.timezone.utc) ) remaining = argument[match.end() :].strip() await result.ensure_constraints(ctx, self, now, remaining) diff --git a/core/utils.py b/core/utils.py index ce28f243c1..30130eb740 100644 --- a/core/utils.py +++ b/core/utils.py @@ -2,6 +2,7 @@ import functools import re import typing +from datetime import datetime, timezone from difflib import get_close_matches from distutils.util import strtobool as _stb # pylint: disable=import-error from itertools import takewhile, zip_longest @@ -10,6 +11,9 @@ import discord from discord.ext import commands +from core.models import getLogger + + __all__ = [ "strtobool", "User", @@ -34,9 +38,13 @@ "tryint", "get_top_role", "get_joint_id", + "extract_block_timestamp", ] +logger = getLogger(__name__) + + def strtobool(val): if isinstance(val, bool): return val @@ -504,3 +512,50 @@ def get_joint_id(message: discord.Message) -> typing.Optional[int]: except ValueError: pass return None + + +def extract_block_timestamp(reason, id_): + # etc "blah blah blah... until <t:XX:f>." + now = discord.utils.utcnow() + end_time = re.search(r"until <t:(\d+):(?:R|f)>.$", reason) + attempts = [ + # backwards compat + re.search(r"until ([^`]+?)\.$", reason), + re.search(r"%([^%]+?)%", reason), + ] + after = None + if end_time is None: + for i in attempts: + if i is not None: + end_time = i + break + + if end_time is not None: + # found a deprecated version + try: + after = ( + datetime.fromisoformat(end_time.group(1)).replace(tzinfo=timezone.utc) - now + ).total_seconds() + except ValueError: + logger.warning( + r"Broken block message for user %s, block and unblock again with a different message to prevent further issues", + id_, + ) + raise + logger.warning( + r"Deprecated time message for user %s, block and unblock again to update.", + id_, + ) + else: + try: + after = ( + datetime.utcfromtimestamp(int(end_time.group(1))).replace(tzinfo=timezone.utc) - now + ).total_seconds() + except ValueError: + logger.warning( + r"Broken block message for user %s, block and unblock again with a different message to prevent further issues", + id_, + ) + raise + + return end_time, after From ebfd37baccfacf3aa9f34100ae1a3b91e6e7ec7f Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 20 Sep 2022 22:33:23 +0800 Subject: [PATCH 1105/1221] Purge Registry --- plugins/registry.json | 260 +----------------------------------------- 1 file changed, 1 insertion(+), 259 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index ad8dbebaf9..9e26dfeeb6 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1,259 +1 @@ -{ - "close_message": { - "repository": "python-discord/modmail-plugins", - "branch": "main", - "description": "Add a ?closemessage command that will close the thread after 15 minutes with a default message.", - "bot_version": "2.20.1", - "title": "Close message", - "icon_url": "https://i.imgur.com/ev7BFMz.png", - "thumbnail_url": "https://i.imgur.com/ev7BFMz.png" - }, - "mdlink": { - "repository": "python-discord/modmail-plugins", - "branch": "main", - "description": "Generate a ready to paste link to the thread logs.", - "bot_version": "2.20.1", - "title": "MDLink", - "icon_url": "https://i.imgur.com/JA2E63R.png", - "thumbnail_url": "https://i.imgur.com/JA2E63R.png" - }, - "reply_cooldown": { - "repository": "python-discord/modmail-plugins", - "branch": "main", - "description": "Forbid you from sending the same message twice in ten seconds.", - "bot_version": "2.20.1", - "title": "Reply cooldown", - "icon_url": "https://i.imgur.com/FtRQveT.png", - "thumbnail_url": "https://i.imgur.com/FtRQveT.png" - }, - "dragory-migrate": { - "repository": "kyb3r/modmail-plugins", - "branch": "master", - "description": "Migrate your logs from Dragory's modmail bot to this one with a simple command. Added at the request of users.", - "bot_version": "2.20.1", - "title": "Dragory Logs Migration", - "icon_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png", - "thumbnail_url": "https://cdn1.iconfinder.com/data/icons/web-hosting-2-4/52/200-512.png" - }, - "music": { - "repository": "Taaku18/modmail-plugins", - "branch": "master", - "description": "Play wonderfull jams through your modmail!", - "bot_version": "2.20.1", - "title": "music", - "icon_url": "https://i.imgur.com/JmJPX5W.gif", - "thumbnail_url": "https://i.imgur.com/jrYL7F8.gif" - }, - "media-only": { - "repository": "lorenzo132/modmail-plugins", - "branch": "master", - "description": "Make a channel mediaonly, only the following mediatypes will be accepted `.png` / `.gif` / `.jpg` / `.mp4`/ `.jpeg`", - "bot_version": "2.20.1", - "title": "Media-only", - "icon_url": "https://i.imgur.com/ussAoIi.png", - "thumbnail_url": "https://i.imgur.com/ussAoIi.png" - }, - "anti-steal-close": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Don't let anyone steal ya close.", - "title": "Anti Steal Close", - "icon_url": "https://i.imgur.com/LovxyV3.png", - "thumbnail_url": "https://i.imgur.com/LovxyV3.png" - }, - "announcement": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Easily make announcements in your server!", - "bot_version": "2.20.1", - "title": "Announcement Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, - "dm-on-join": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "DM New Users when they join", - "bot_version": "2.20.1", - "title": "DM-on-join Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, - "giveaway": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Host giveaways on your server", - "bot_version": "2.20.1", - "title": "\uD83C\uDF89 Giveaway Plugin \uD83C\uDF89", - "icon_url": "https://i.imgur.com/qk85xdi.png", - "thumbnail_url": "https://i.imgur.com/gUHB91v.png" - }, - "hastebin": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Easily Upload Text To hastebin!", - "bot_version": "2.20.1", - "title": "Hastebin Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, - "leave-server": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Don't want your bot in a server? Did someone invite it without your permission? If so, this plugin is useful for you!", - "bot_version": "2.20.1", - "title": "Leave-server Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, - "welcomer": { - "repository": "fourjr/modmail-plugins", - "branch": "master", - "description": "Add messages to welcome new members! Allows for embedded messages as well. [Read more](https://github.com/fourjr/modmail-plugins/blob/master/welcomer/README.md)", - "bot_version": "2.20.1", - "title": "New member messages plugin", - "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", - "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" - }, - "tags": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Tag Management For Your Server", - "bot_version": "2.20.1", - "title": "Tags Plugin", - "icon_url": "https://images.ionadev.ml/b/ZIDUUsl.png", - "thumbnail_url": "https://images.ionadev.ml/b/ZIDUUsl.png" - }, - "backupdb": { - "repository": "officialpiyush/modmail-plugins", - "branch": "master", - "description": "Backup you're current Modmail DB with a single command!\n\n**Requires `BACKUP_MONGO_URI` in either config.json or environment variables**", - "bot_version": "2.20.1", - "title": "Backup Database (backupdb)", - "icon_url": "https://images.ionadev.ml/b/nKAlOC4.jpg", - "thumbnail_url": "https://images.ionadev.ml/b/nKAlOC4.jpg" - }, - "colors": { - "repository": "Taaku18/modmail-plugins", - "branch": "master", - "description": "Conversions between hex, RGB, and color names.", - "bot_version": "2.20.1", - "title": "Colors!!", - "icon_url": "https://cdn1.iconfinder.com/data/icons/weather-19/32/rainbow-512.png", - "thumbnail_url": "https://i.imgur.com/fSxnc9W.jpg" - }, - "fun": { - "repository": "TheKinG2149/modmail-plugins", - "branch": "master", - "description": "Some fun commands like 8ball, dadjokes", - "bot_version": "2.24.1", - "title": "Fun", - "icon_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png", - "thumbnail_url": "https://cdn.discordapp.com/attachments/584692239893135362/591588754142265354/43880032.png" - }, - "stats": { - "repository": "KarateWumpus/modmail-plugins", - "branch": "master", - "description": "Get useful stats directly in an embed about either the Modmail bot, a user or the server.", - "bot_version": "2.24.1", - "title": "Get Stats", - "icon_url": "https://image.flaticon.com/icons/png/512/117/117761.png", - "thumbnail_url": "http://www.pngmart.com/files/7/Statistics-PNG-Clipart.png" - }, - "moderation": { - "repository": "Vincysuper07/modmail-plugins", - "branch": "main", - "description": "Moderate your server with Modmail, bring the Mod to Modmail!", - "bot_version": "3.6.2", - "title": "Moderate your server", - "icon_url": "https://cdn.discordapp.com/attachments/759829573654544454/773535811143598110/ad2e4d6e7b90ca6005a5038e22b099cc.png", - "thumbnail_url": "https://cdn.discordapp.com/attachments/759829573654544454/773535811143598110/ad2e4d6e7b90ca6005a5038e22b099cc.png" - }, - "serverstats": { - "repository": "dazvise/modmail-plugins", - "branch": "master", - "description": "Voice channels containing interesting and accurate statistics about your server such as Member Count.", - "bot_version": "2.20.1", - "title": "Server Stats", - "icon_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png", - "thumbnail_url": "https://i.gyazo.com/fadb70740e83f2448b23ffe192a1f32d.png" - }, - "suggest": { - "repository": "realcyguy/modmail-plugins", - "branch": "master", - "description": "Send suggestions to a selected server! It has accepting, denying, and moderation-ing.", - "bot_version": "3.4.1", - "title": "Suggest stuff.", - "icon_url": "https://i.imgur.com/qtE7AH8.png", - "thumbnail_url": "https://i.imgur.com/qtE7AH8.png" - }, - "githubstats": { - "repository": "mischievousdev/modmail-plugins", - "branch": "master", - "description": "Github statistics in discord", - "bot_version": "2.20.1", - "title": "Github Stats", - "icon_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg", - "thumbnail_url": "https://raw.githubusercontent.com/mischievousdev/modmail-plugins/master/download%20(9).jpeg" - }, - "slowmode": { - "repository": "teen1/modmail-plugins", - "branch": "master", - "description": "Configure slow mode for your channels with Modmail!", - "bot_version": "2.20.1", - "title": "Slow Mode", - "icon_url": "https://cdn.discordapp.com/attachments/717029057635549274/717033838966210601/Slow_mode_-_icon.png", - "thumbnail_url": "https://cdn.discordapp.com/attachments/717029057635549274/717029110907666482/Slow_mode_plugin_-_thumbnail.png" - }, - "translate": { - "repository": "WebKide/modmail-plugins", - "branch": "master", - "description": "(∩`-´)⊃━☆゚.*・。゚ translate text from one language to another (defaults to English)\n\nGet full list of available languages at: https://github.com/WebKide/modmail-plugins/blob/master/translate/langs.json\n\nThis command conflicts with Translator-plugin", - "bot_version": "3.5.0", - "title": "Translate", - "icon_url": "https://i.imgur.com/yeHFKgl.png", - "thumbnail_url": "https://i.imgur.com/yeHFKgl.png" - }, - "countdowns": { - "repository": "fourjr/modmail-plugins", - "branch": "master", - "description": "Setup a countdown voice channel in your server!", - "bot_version": "3.6.2", - "title": "Countdowns", - "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", - "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" - }, - "action": { - "repository": "6days9weeks/modmail-plugins", - "branch": "master", - "description": "Have fun with others by hugging them or giving them pats~!!", - "title": "Action", - "icon_url": "https://media.discordapp.net/attachments/720733784970100776/820933433579798528/689105042212388965.png", - "thumbnail_url": "https://data.whicdn.com/images/58526601/original.gif" - }, - "menu": { - "repository": "fourjr/modmail-plugins", - "branch": "master", - "description": "Adds reaction-based menus into thread creates. Check out `?configmenu`", - "title": "Menus", - "bot_version": "3.9.0", - "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", - "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" - }, - "claim": { - "repository": "fourjr/modmail-plugins", - "branch": "master", - "description": "Allows supporters to claim thread by sending ?claim in the thread channel", - "title": "Claim Thread", - "icon_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png", - "thumbnail_url": "https://cdn.discordapp.com/avatars/180314310298304512/7552e0089004079304cc9912d13ac81d.png" - }, - "phishchecker": { - "repository": "TheDiscordHistorian/historian-cogs", - "branch": "main", - "description": "Deletes scam links from your server and optionally kick / ban the user.", - "title": "Scam Link Detector", - "icon_url": "https://cdn.discordapp.com/attachments/576521645540245505/895661244743299102/antifish.png", - "thumbnail_url": "https://cdn.discordapp.com/attachments/576521645540245505/895661244743299102/antifish.png" - } -} +{} \ No newline at end of file From 7c2875c9e1db45f4120da17cb5dff85c7134f043 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 20 Sep 2022 22:34:00 +0800 Subject: [PATCH 1106/1221] Lock registry version --- cogs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 4cdd0aba3f..dc4c89aa6b 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -131,7 +131,7 @@ def __init__(self, bot): logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") async def populate_registry(self): - url = "https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json" + url = "https://raw.githubusercontent.com/kyb3r/modmail/be14cca89742cd691f6b91f0d0931a074ad81e3b/plugins/registry.json" async with self.bot.session.get(url) as resp: self.registry = json.loads(await resp.text()) From b7e25a0d5eed17808911a8532338c6bd3ba061f2 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 20 Sep 2022 22:35:11 +0800 Subject: [PATCH 1107/1221] Upgrade version to 3.10.5 with locked registry --- CHANGELOG.md | 3 +++ bot.py | 2 +- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f9b461169..268ee82a1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.10.5 +Locked plugin registry version impending v4 release. + # v3.10.3 This is a hotfix for contact command. diff --git a/bot.py b/bot.py index 0ee38be340..5113f367f8 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.10.3" +__version__ = "3.10.5" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 341e506413..cad728fd2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '3.10.3' +version = '3.10.5' description = "Modmail is similar to Reddit's 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 1f9f633ae1111b04701c137da192dc1d3d63a493 Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 20 Sep 2022 22:36:58 +0800 Subject: [PATCH 1108/1221] Add suggest plugin #3189 --- plugins/registry.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 9e26dfeeb6..494feebee4 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1 +1,11 @@ -{} \ No newline at end of file +{ + "suggest": { + "repository": "realcyguy/modmail-plugins", + "branch": "v4", + "description": "Send suggestions to a selected server! It has accepting, denying, and moderation-ing.", + "bot_version": "4.0.0", + "title": "Suggest stuff.", + "icon_url": "https://i.imgur.com/qtE7AH8.png", + "thumbnail_url": "https://i.imgur.com/qtE7AH8.png" + } +} \ No newline at end of file From cc4b4ff0aa345fc93dafa9556eacb4ec56d3a15f Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Tue, 20 Sep 2022 22:37:54 +0800 Subject: [PATCH 1109/1221] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e90aeb637..4c1c3327ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Upgraded to discord.py v2.0 ([internal changes](https://discordpy.readthedocs.io/en/latest/migrating.html), [GH #2990](https://github.com/kyb3r/modmail/issues/2990)). - Python 3.8 or higher is required. - Asyncio changes ([gist](https://gist.github.com/Rapptz/6706e1c8f23ac27c98cee4dd985c8120)) +- Plugin registry is purged and all developers have to re-apply due to breaking changes. ### Added From b04ac7836abae923794cc6ca69e80a434c855fbb Mon Sep 17 00:00:00 2001 From: Yee Jia Rong <28086837+fourjr@users.noreply.github.com> Date: Wed, 21 Sep 2022 00:12:09 +0800 Subject: [PATCH 1110/1221] Hotfix v4.0.1 for thread cooldown --- CHANGELOG.md | 13 +++++++ Pipfile | 1 + Pipfile.lock | 98 ++++++++++++++++++++++++++++++++++++------------- README.md | 2 +- bot.py | 20 +++++++--- cogs/modmail.py | 1 - cogs/utility.py | 2 +- core/config.py | 4 +- core/thread.py | 2 +- core/time.py | 5 ++- core/utils.py | 2 +- pyproject.toml | 2 +- 12 files changed, 111 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20403c41a2..c46714146c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v4.0.1 + +This is a hotfix release. + +### Improved + +- Error Messages + +### Fixed + +- Thread cooldown + + # v4.0.0 ### Breaking diff --git a/Pipfile b/Pipfile index 573f06cb76..e61e3b9288 100644 --- a/Pipfile +++ b/Pipfile @@ -23,6 +23,7 @@ python-dateutil = "~=2.8.1" python-dotenv = "==0.20.0" uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} lottie = {version = "==0.6.11", extras = ["pdf"]} +requests = "==2.28.1" [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 9657e2b273..e1c531856e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c074bd9564d93aac78141ccfec1138ffeb561dfa0956f60ee5b10322d156d20f" + "sha256": "9d1171901e036b1f78a2e89855d564e9f559736ca076128fa27eb14ee8f917a4" }, "pipfile-spec": 6, "requires": {}, @@ -130,6 +130,14 @@ ], "version": "==2.5.2" }, + "certifi": { + "hashes": [ + "sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5", + "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516" + ], + "markers": "python_version >= '3.6'", + "version": "==2022.9.14" + }, "cffi": { "hashes": [ "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", @@ -217,11 +225,11 @@ }, "cssselect2": { "hashes": [ - "sha256:3a83b2a68370c69c9cd3fcb88bbfaebe9d22edeef2c22d1ff3e1ed9c7fa45ed8", - "sha256:5b5d6dea81a5eb0c9ca39f116c8578dd413778060c94c1f51196371618909325" + "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a", + "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969" ], "markers": "python_version >= '3.7'", - "version": "==0.6.0" + "version": "==0.7.0" }, "defusedxml": { "hashes": [ @@ -321,11 +329,11 @@ }, "idna": { "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], "markers": "python_version >= '3.5'", - "version": "==3.3" + "version": "==3.4" }, "isodate": { "hashes": [ @@ -507,7 +515,9 @@ "version": "==2.21" }, "pymongo": { - "extras": [], + "extras": [ + "srv" + ], "hashes": [ "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", @@ -570,6 +580,7 @@ "sha256:81a3ebc33b1367f301d1c8eda57eec4868e951504986d5d3fe437479dcdac5b2", "sha256:8455176fd1b86de97d859fed4ae0ef867bf998581f584c7a1a591246dfec330f", "sha256:845b178bd127bb074835d2eac635b980c58ec5e700ebadc8355062df708d5a71", + "sha256:858af7c2ab98f21ed06b642578b769ecfcabe4754648b033168a91536f7beef9", "sha256:87e18f29bac4a6be76a30e74de9c9005475e27100acf0830679420ce1fd9a6fd", "sha256:89d7baa847383b9814de640c6f1a8553d125ec65e2761ad146ea2e75a7ad197c", "sha256:8c7ad5cab282f53b9d78d51504330d1c88c83fbe187e472c07e6908a0293142e", @@ -636,6 +647,14 @@ "index": "pypi", "version": "==0.20.0" }, + "requests": { + "hashes": [ + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + ], + "index": "pypi", + "version": "==2.28.1" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -652,27 +671,49 @@ "markers": "python_version >= '3.6'", "version": "==1.1.1" }, + "urllib3": { + "hashes": [ + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "version": "==1.26.12" + }, "uvloop": { "hashes": [ - "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450", - "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897", - "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861", - "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c", - "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805", - "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d", - "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464", - "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f", - "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9", - "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab", - "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f", - "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638", - "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64", - "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee", - "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382", - "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228" + "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d", + "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1", + "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595", + "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b", + "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05", + "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8", + "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20", + "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded", + "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c", + "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8", + "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474", + "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f", + "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62", + "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376", + "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c", + "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e", + "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b", + "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4", + "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578", + "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811", + "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d", + "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738", + "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa", + "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9", + "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539", + "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c", + "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718", + "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667", + "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c", + "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024" ], "markers": "sys_platform != 'win32'", - "version": "==0.16.0" + "version": "==0.17.0" }, "webencodings": { "hashes": [ @@ -924,6 +965,7 @@ }, "pyyaml": { "hashes": [ + "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", @@ -935,26 +977,32 @@ "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], diff --git a/README.md b/README.md index ace61fb903..559d49ba65 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v4.0.0-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v4.0.1-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> diff --git a/bot.py b/bot.py index 9f13e3e04e..1f62bfcac7 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.0" +__version__ = "4.0.1" import asyncio @@ -10,6 +10,7 @@ import string import struct import sys +import platform import typing from datetime import datetime, timezone from subprocess import PIPE @@ -857,10 +858,12 @@ async def get_thread_cooldown(self, author: discord.Member): return try: - cooldown = datetime.fromisoformat(last_log_closed_at) + thread_cooldown + cooldown = datetime.fromisoformat(last_log_closed_at).astimezone(timezone.utc) + 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") + cooldown = datetime.fromisoformat(last_log_closed_at).astimezone( + timezone.utc + ) + self.config.remove("thread_cooldown") if cooldown > now: # User messaged before thread cooldown ended @@ -1774,9 +1777,14 @@ def main(): "Unable to import cairosvg, install GTK Installer for Windows and restart your system (https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases/latest)" ) else: - logger.error( - "Unable to import cairosvg, report on our support server with your OS details: https://discord.gg/etJNHCQ" - ) + if "ubuntu" in platform.version().lower() or "debian" in platform.version().lower(): + logger.error( + "Unable to import cairosvg, try running `sudo apt-get install libpangocairo-1.0-0` or report on our support server with your OS details: https://discord.gg/etJNHCQ" + ) + else: + logger.error( + "Unable to import cairosvg, report on our support server with your OS details: https://discord.gg/etJNHCQ" + ) sys.exit(0) # check discord version diff --git a/cogs/modmail.py b/cogs/modmail.py index b6ebe81baf..75130fdb6e 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1683,7 +1683,6 @@ async def blocked(self, ctx): continue if end_time is not None: - after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() if after <= 0: # No longer blocked self.bot.blocked_roles.pop(str(id_)) diff --git a/cogs/utility.py b/cogs/utility.py index af0e70beec..16c84af29d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -810,7 +810,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): if key in keys: try: - self.bot.config.set(key, value) + await self.bot.config.set(key, value) await self.bot.config.update() embed = discord.Embed( title="Success", diff --git a/core/config.py b/core/config.py index dd76776092..c25eef3921 100644 --- a/core/config.py +++ b/core/config.py @@ -366,7 +366,7 @@ def get(self, key: str, convert=True) -> typing.Any: return value - def set(self, key: str, item: typing.Any, convert=True) -> None: + async def set(self, key: str, item: typing.Any, convert=True) -> None: if not convert: return self.__setitem__(key, item) @@ -401,7 +401,7 @@ def set(self, key: str, item: typing.Any, convert=True) -> None: except isodate.ISO8601Error: try: converter = UserFriendlyTime() - time = converter.convert(None, item) + time = await converter.convert(None, item, now=discord.utils.utcnow()) if time.arg: raise ValueError except BadArgument as exc: diff --git a/core/thread.py b/core/thread.py index c9d8ae5bcb..30f0542c6d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -568,7 +568,7 @@ async def _restart_close_timer(self): seconds = timeout.total_seconds() # seconds = 20 # Uncomment to debug with just 20 seconds reset_time = discord.utils.utcnow() + timedelta(seconds=seconds) - human_time = human_timedelta(dt=reset_time, spec="manual") + human_time = discord.utils.format_dt(reset_time) if self.bot.config.get("thread_auto_close_silently"): return await self.close(closer=self.bot.user, silent=True, after=int(seconds), auto_close=True) diff --git a/core/time.py b/core/time.py index 963eac5a6e..c56c7264e2 100644 --- a/core/time.py +++ b/core/time.py @@ -192,10 +192,11 @@ def __init__( self.converter: commands.Converter = converter # type: ignore # It doesn't understand this narrowing self.default: Any = default - async def convert(self, ctx: Context, argument: str) -> FriendlyTimeResult: + async def convert(self, ctx: Context, argument: str, *, now=None) -> FriendlyTimeResult: calendar = HumanTime.calendar regex = ShortTime.compiled - now = ctx.message.created_at + if now is None: + now = ctx.message.created_at match = regex.match(argument) if match is not None and match.group(0): diff --git a/core/utils.py b/core/utils.py index 30130eb740..d8046ade5f 100644 --- a/core/utils.py +++ b/core/utils.py @@ -469,7 +469,7 @@ async def create_thread_channel(bot, recipient, category, overwrites, *, name=No if not fallback: fallback = await category.clone(name="Fallback Modmail") - bot.config.set("fallback_category_id", str(fallback.id)) + await bot.config.set("fallback_category_id", str(fallback.id)) await bot.config.update() return await create_thread_channel( diff --git a/pyproject.toml b/pyproject.toml index a19e436399..79a3e74f0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.0' +version = '4.0.1' description = "Modmail is similar to Reddit's 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 d63575281f23286a103ed340968aa50b880ecb1d Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Wed, 21 Sep 2022 12:52:32 +0200 Subject: [PATCH 1111/1221] Fixed runtime.txt to use a valid python version --- runtime.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime.txt b/runtime.txt index 30e81e3130..119ff10234 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.10.3 +python-3.10.7 From ec52cc2001f487ab74f5b649f2a085828e9b8b22 Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Thu, 22 Sep 2022 17:39:18 +0530 Subject: [PATCH 1112/1221] Update modmail.py --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 75130fdb6e..57d4e4e844 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -749,7 +749,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']}>") - if entry["title"]: + if entry.get("title"): embed.add_field(name="Title", value=entry["title"], inline=False) embed.add_field(name="Preview", value=format_preview(entry["messages"]), inline=False) From 7960761ea740d2e5ed7f43ab03b0c11f6215afb6 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <hidzrie@gmail.com> Date: Fri, 23 Sep 2022 11:42:44 +0000 Subject: [PATCH 1113/1221] Fix RuntimeError when presence intent disabled. --- bot.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 1f62bfcac7..06841e90b9 100644 --- a/bot.py +++ b/bot.py @@ -225,7 +225,6 @@ async def runner(): except discord.PrivilegedIntentsRequired: retry_intents = True if retry_intents: - await self.http.close() if self.ws is not None and self.ws.open: await self.ws.close(code=1000) self._ready.clear() @@ -236,9 +235,9 @@ async def runner(): # Try again with members intent self._connection._intents = intents logger.warning( - "Attempting to login with only the server members and message content privileged intent. Some plugins might not work correctly." + "Attempting to reconnect with only the server members and message content privileged intent. Some plugins might not work correctly." ) - await self.start(self.token) + await self.connect(reconnect=True) except discord.PrivilegedIntentsRequired: logger.critical( "Privileged intents are not explicitly granted in the discord developers dashboard." From ea0c5fce3241ef12aa8753c269b12c6498e0413b Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Sat, 24 Sep 2022 13:53:19 +0200 Subject: [PATCH 1114/1221] Added advanced menu to registry --- plugins/registry.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 494feebee4..325ee2fac0 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1,4 +1,13 @@ { + "advanced-menu": { + "repository": "sebkuip/mm-plugins", + "branch": "master", + "description": "Advanced menu plugin using dropdown selectors. Supports submenus (and sub-submenus infinitely).", + "bot_version": "v4.0.0", + "title": "Advanced menu", + "icon_url": "https://raw.githubusercontent.com/sebkuip/mm-plugins/master/advanced-menu/logo.png", + "thumbnail_url": "https://raw.githubusercontent.com/sebkuip/mm-plugins/master/advanced-menu/logo.png" + }, "suggest": { "repository": "realcyguy/modmail-plugins", "branch": "v4", @@ -8,4 +17,4 @@ "icon_url": "https://i.imgur.com/qtE7AH8.png", "thumbnail_url": "https://i.imgur.com/qtE7AH8.png" } -} \ No newline at end of file +} From 9e86f224fcf4f18a5c73fe3592098640964e0920 Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sat, 1 Oct 2022 09:37:10 +0800 Subject: [PATCH 1115/1221] Add giveaway plugin to registry. (#3208) * Add giveaway plugin to registry. * Update `thumbnail_url` for giveaway plugin. --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 325ee2fac0..27f304c983 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -8,6 +8,15 @@ "icon_url": "https://raw.githubusercontent.com/sebkuip/mm-plugins/master/advanced-menu/logo.png", "thumbnail_url": "https://raw.githubusercontent.com/sebkuip/mm-plugins/master/advanced-menu/logo.png" }, + "giveaway": { + "repository": "Jerrie-Aries/modmail-plugins", + "branch": "master", + "description": "Host giveaways on your server with this plugin.", + "bot_version": "4.0.0", + "title": "Giveaway", + "icon_url": "https://github.com/Jerrie-Aries.png", + "thumbnail_url": "https://raw.githubusercontent.com/Jerrie-Aries/modmail-plugins/master/.static/giveaway.jpg" + }, "suggest": { "repository": "realcyguy/modmail-plugins", "branch": "v4", From a5156621a85da28938b078c73153bb840d3e7a06 Mon Sep 17 00:00:00 2001 From: Ralph <blackbirdralph@aol.com> Date: Fri, 30 Sep 2022 23:03:19 -0400 Subject: [PATCH 1116/1221] Re-add/Expand FourJR Plugins --- plugins/registry.json | 74 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 27f304c983..7f09fbe2cc 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -25,5 +25,79 @@ "title": "Suggest stuff.", "icon_url": "https://i.imgur.com/qtE7AH8.png", "thumbnail_url": "https://i.imgur.com/qtE7AH8.png" + }, + "welcomer": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Add messages to welcome new members! Allows for embedded messages as well. [Read more](https://github.com/fourjr/modmail-plugins/blob/master/welcomer/README.md)", + "bot_version": "2.20.1", + "title": "New member messages plugin", + "icon_url": "https://i.imgur.com/Mo60CdK.png", + "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" + }, + "countdowns": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Setup a countdown voice channel in your server!", + "bot_version": "3.6.2", + "title": "Countdowns", + "icon_url": "https://i.imgur.com/Mo60CdK.png", + "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" + }, + "claim": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Allows supporters to claim thread by sending ?claim in the thread channel", + "title": "Claim Thread", + "icon_url": "https://i.imgur.com/Mo60CdK.png", + "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" + }, + "emote-manager": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Allows managing server emotes via ?emoji", + "title": "Emote Manager", + "icon_url": "https://i.imgur.com/Mo60CdK.png", + "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" + }, + "claim": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Allows supporters to claim thread by sending ?claim in the thread channel", + "title": "Claim Thread", + "icon_url": "https://i.imgur.com/Mo60CdK.png", + "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" + }, + "gen-log": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Outputs a text log of a thread in a specified channel", + "title": "Log Generator", + "icon_url": "https://i.imgur.com/Mo60CdK.png", + "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" + }, + "media-logger": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Re-posts detected media from all visible channels into a specified logging channel", + "title": "Media Logger", + "icon_url": "https://i.imgur.com/Mo60CdK.png", + "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" + }, + "report": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Specify an emoji to react with on messages. Generates a "report" in specified logging channel upon react.", + "title": "Report", + "icon_url": "https://i.imgur.com/Mo60CdK.png", + "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" + }, + "top-supporters": { + "repository": "fourjr/modmail-plugins", + "branch": "master", + "description": "Gathers and prints the top supporters of handling threads.", + "title": "Top Supporters", + "icon_url": "https://i.imgur.com/Mo60CdK.png", + "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" } } From e978457f2ae61c5c9ee6c58fb64742376a62c1d5 Mon Sep 17 00:00:00 2001 From: Ralph <blackbirdralph@aol.com> Date: Fri, 30 Sep 2022 23:04:47 -0400 Subject: [PATCH 1117/1221] remove extra quotes --- plugins/registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/registry.json b/plugins/registry.json index 7f09fbe2cc..23feb063f9 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -87,7 +87,7 @@ "report": { "repository": "fourjr/modmail-plugins", "branch": "master", - "description": "Specify an emoji to react with on messages. Generates a "report" in specified logging channel upon react.", + "description": "Specify an emoji to react with on messages. Generates a 'report' in specified logging channel upon react.", "title": "Report", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" From 964c809dd880f4aebba3a458c16f116757d29b93 Mon Sep 17 00:00:00 2001 From: Ralph <blackbirdralph@aol.com> Date: Sat, 1 Oct 2022 03:05:34 -0400 Subject: [PATCH 1118/1221] Update plugins/registry.json Co-authored-by: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> --- plugins/registry.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 23feb063f9..57d735198d 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -59,14 +59,6 @@ "title": "Emote Manager", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" - }, - "claim": { - "repository": "fourjr/modmail-plugins", - "branch": "master", - "description": "Allows supporters to claim thread by sending ?claim in the thread channel", - "title": "Claim Thread", - "icon_url": "https://i.imgur.com/Mo60CdK.png", - "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, "gen-log": { "repository": "fourjr/modmail-plugins", From a139de8432b0abe7acb6018aff80e7ee4def84f6 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <hidzrie@gmail.com> Date: Sat, 1 Oct 2022 07:55:02 +0000 Subject: [PATCH 1119/1221] Add `announcement` plugin to registry. --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index 27f304c983..141ff174c2 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -8,6 +8,15 @@ "icon_url": "https://raw.githubusercontent.com/sebkuip/mm-plugins/master/advanced-menu/logo.png", "thumbnail_url": "https://raw.githubusercontent.com/sebkuip/mm-plugins/master/advanced-menu/logo.png" }, + "announcement": { + "repository": "Jerrie-Aries/modmail-plugins", + "branch": "master", + "description": "Create and post announcements. Supports both plain and embed. Also customisable using buttons and dropdown menus.", + "bot_version": "4.0.0", + "title": "Announcement", + "icon_url": "https://github.com/Jerrie-Aries.png", + "thumbnail_url": "https://raw.githubusercontent.com/Jerrie-Aries/modmail-plugins/master/.static/announcement.jpg" + }, "giveaway": { "repository": "Jerrie-Aries/modmail-plugins", "branch": "master", From 766bbbbd0e494c17dc2cffefb5c906216a7fcb2d Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 2 Oct 2022 22:49:22 -0700 Subject: [PATCH 1120/1221] Update CHANGELOG.md --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c46714146c..3f19259a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. + +# [Unreleased] + +### Fixed + +- Bots without presence intent encountering RuntimeError on start. + + # v4.0.1 This is a hotfix release. From 4aadf63590175213ca918e748893311913fd4814 Mon Sep 17 00:00:00 2001 From: Ralph <blackbirdralph@aol.com> Date: Mon, 3 Oct 2022 10:08:39 -0400 Subject: [PATCH 1121/1221] Update Branch and Bot_Version --- plugins/registry.json | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 57d735198d..558d725814 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -28,66 +28,72 @@ }, "welcomer": { "repository": "fourjr/modmail-plugins", - "branch": "master", + "branch": "v4", "description": "Add messages to welcome new members! Allows for embedded messages as well. [Read more](https://github.com/fourjr/modmail-plugins/blob/master/welcomer/README.md)", - "bot_version": "2.20.1", + "bot_version": "4.0.0", "title": "New member messages plugin", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, "countdowns": { "repository": "fourjr/modmail-plugins", - "branch": "master", + "branch": "v4", "description": "Setup a countdown voice channel in your server!", - "bot_version": "3.6.2", + "bot_version": "4.0.0", "title": "Countdowns", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, "claim": { "repository": "fourjr/modmail-plugins", - "branch": "master", + "branch": "v4", "description": "Allows supporters to claim thread by sending ?claim in the thread channel", + "bot_version": "4.0.0", "title": "Claim Thread", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, "emote-manager": { "repository": "fourjr/modmail-plugins", - "branch": "master", + "branch": "v4", "description": "Allows managing server emotes via ?emoji", + "bot_version": "4.0.0", "title": "Emote Manager", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, "gen-log": { "repository": "fourjr/modmail-plugins", - "branch": "master", + "branch": "v4", "description": "Outputs a text log of a thread in a specified channel", + "bot_version": "4.0.0", "title": "Log Generator", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, "media-logger": { "repository": "fourjr/modmail-plugins", - "branch": "master", + "branch": "v4", "description": "Re-posts detected media from all visible channels into a specified logging channel", + "bot_version": "4.0.0", "title": "Media Logger", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, "report": { "repository": "fourjr/modmail-plugins", - "branch": "master", + "branch": "v4", "description": "Specify an emoji to react with on messages. Generates a 'report' in specified logging channel upon react.", + "bot_version": "4.0.0", "title": "Report", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, "top-supporters": { "repository": "fourjr/modmail-plugins", - "branch": "master", + "branch": "v4", "description": "Gathers and prints the top supporters of handling threads.", + "bot_version": "4.0.0", "title": "Top Supporters", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" From df63dec4b02d449c39cc79616dfff0ef00ce37a8 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sat, 15 Oct 2022 04:34:33 -0700 Subject: [PATCH 1122/1221] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c46714146c..db997890ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# Unreleased + +### Fixed + +- Resolved an issue where `?logs` doesn't work when the thread has no title. ([PR #3201](https://github.com/kyb3r/modmail/pull/3201)) + # v4.0.1 This is a hotfix release. From 03558f742bc0df5aff38eb67fa6e0370bba8f8c9 Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Sun, 4 Dec 2022 11:47:32 +0530 Subject: [PATCH 1123/1221] Remove hard coded "( )" from plain messages --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 30f0542c6d..4d883ae687 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1150,7 +1150,7 @@ def lottie_to_png(data): additional_images = [] if embed.footer.text: - plain_message = f"**({embed.footer.text}) " + plain_message = f"**{embed.footer.text} " else: plain_message = "**" plain_message += f"{embed.author.name}:** {embed.description}" From cde98e90b4119fca97bd80880d3e33cc044cc9ef Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Sun, 4 Dec 2022 13:35:03 +0530 Subject: [PATCH 1124/1221] Fix guild icon not set issue --- bot.py | 16 +++++++++++++--- cogs/modmail.py | 14 +++++++------- cogs/utility.py | 8 +++++--- core/thread.py | 6 +++--- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/bot.py b/bot.py index 1f62bfcac7..01d8a52c11 100644 --- a/bot.py +++ b/bot.py @@ -87,6 +87,13 @@ def __init__(self): self.plugin_db = PluginDatabaseClient(self) # Deprecated self.startup() + def get_guild_icon(self, guild: typing.Optional[discord.Guild]) -> str: + if guild is None: + guild = self.guild + if guild.icon is None: + return self.user.display_avatar.url + return guild.icon_url + def _resolve_snippet(self, name: str) -> typing.Optional[str]: """ Get actual snippet names from direct aliases to snippets. @@ -913,7 +920,10 @@ async def process_dm_modmail(self, message: discord.Message) -> None: color=self.error_color, description=self.config["disabled_new_thread_response"], ) - embed.set_footer(text=self.config["disabled_new_thread_footer"], icon_url=self.guild.icon.url) + embed.set_footer( + text=self.config["disabled_new_thread_footer"], + icon_url=self.get_guild_icon(guild=message.guild), + ) logger.info("A new thread was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) return await message.channel.send(embed=embed) @@ -928,7 +938,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: ) embed.set_footer( text=self.config["disabled_current_thread_footer"], - icon_url=self.guild.icon.url, + icon_url=self.get_guild_icon(guild=message.guild), ) logger.info("A message was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) @@ -1335,7 +1345,7 @@ async def handle_react_to_contact(self, payload): ) embed.set_footer( text=self.config["disabled_new_thread_footer"], - icon_url=self.guild.icon.url, + icon_url=self.get_guild_icon(guild=channel.guild), ) logger.info( "A new thread using react to contact was blocked from %s due to disabled Modmail.", diff --git a/cogs/modmail.py b/cogs/modmail.py index 75130fdb6e..1e4e00a8d0 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -160,7 +160,7 @@ async def snippet(self, ctx, *, name: str.lower = None): 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 add" to add a snippet.') - embed.set_author(name="Snippets", icon_url=ctx.guild.icon.url) + embed.set_author(name="Snippets", icon_url=self.bot.get_guild_icon(guild=ctx.guild)) return await ctx.send(embed=embed) embeds = [] @@ -168,7 +168,7 @@ async def snippet(self, ctx, *, name: str.lower = None): 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) + embed.set_author(name="Snippets", icon_url=self.bot.get_guild_icon(guild=ctx.guild)) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -1031,7 +1031,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, name = tag avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.guild.icon.url + avatar_url = self.bot.get_guild_icon(guild=ctx.guild) em.set_footer(text=name, icon_url=avatar_url) for u in users: @@ -1120,7 +1120,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro name = tag avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.guild.icon.url + avatar_url = self.bot.get_guild_icon(guild=ctx.guild) em.set_footer(text=name, icon_url=avatar_url) for u in users: @@ -1200,7 +1200,7 @@ async def logs_closed_by(self, ctx, *, user: User = None): user = user if user is not None else ctx.author entries = await self.bot.api.search_closed_by(user.id) - embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon.url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.get_guild_icon(guild=ctx.guild)) if not embeds: embed = discord.Embed( @@ -1250,7 +1250,7 @@ async def logs_responded(self, ctx, *, user: User = None): entries = await self.bot.api.get_responded_logs(user.id) - embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon.url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.get_guild_icon(guild=ctx.guild)) if not embeds: embed = discord.Embed( @@ -1275,7 +1275,7 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): entries = await self.bot.api.search_by_text(query, limit) - embeds = self.format_log_embeds(entries, avatar_url=self.bot.guild.icon.url) + embeds = self.format_log_embeds(entries, avatar_url=self.bot.get_guild_icon(guild=ctx.guild)) if not embeds: embed = discord.Embed( diff --git a/cogs/utility.py b/cogs/utility.py index 16c84af29d..5c0ea09eb3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1020,7 +1020,7 @@ async def alias(self, ctx, *, name: str.lower = None): 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) + embed.set_author(name="Aliases", icon_url=self.bot.get_guild_icon(guild=ctx.guild)) return await ctx.send(embed=embed) embeds = [] @@ -1028,7 +1028,7 @@ async def alias(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = utils.format_description(i, names) embed = discord.Embed(color=self.bot.main_color, description=description) - embed.set_author(name="Command Aliases", icon_url=ctx.guild.icon.url) + embed.set_author(name="Command Aliases", icon_url=self.bot.get_guild_icon(guild=ctx.guild)) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -1611,7 +1611,9 @@ async def permissions_get( for name, level in takewhile(lambda x: x is not None, items) ) embed = discord.Embed(color=self.bot.main_color, description=description) - embed.set_author(name="Permission Overrides", icon_url=ctx.guild.icon.url) + embed.set_author( + name="Permission Overrides", icon_url=self.bot.get_guild_icon(guild=ctx.guild) + ) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) diff --git a/core/thread.py b/core/thread.py index 30f0542c6d..07f3108914 100644 --- a/core/thread.py +++ b/core/thread.py @@ -228,7 +228,7 @@ async def send_recipient_genesis_message(): else: footer = self.bot.config["thread_creation_footer"] - embed.set_footer(text=footer, icon_url=self.bot.guild.icon.url) + embed.set_footer(text=footer, icon_url=self.bot.get_guild_icon(guild=self.bot.modmail_guild)) embed.title = self.bot.config["thread_creation_title"] if creator is None or creator == recipient: @@ -521,7 +521,7 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, embed.description = message footer = self.bot.config["thread_close_footer"] - embed.set_footer(text=footer, icon_url=self.bot.guild.icon.url) + embed.set_footer(text=footer, icon_url=self.bot.get_guild_icon(guild=self.bot.guild)) if not silent: for user in self.recipients: @@ -957,7 +957,7 @@ async def send( name = tag avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.guild.icon.url + avatar_url = self.bot.get_guild_icon(guild=self.bot.guild) embed.set_author( name=name, icon_url=avatar_url, From ccc53ea5795ce3affcee9526d4f80a9f17b87c81 Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Mon, 5 Dec 2022 16:11:49 +0530 Subject: [PATCH 1125/1221] Update bot.py Co-authored-by: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 01d8a52c11..f060c5e246 100644 --- a/bot.py +++ b/bot.py @@ -92,7 +92,7 @@ def get_guild_icon(self, guild: typing.Optional[discord.Guild]) -> str: guild = self.guild if guild.icon is None: return self.user.display_avatar.url - return guild.icon_url + return guild.icon.url def _resolve_snippet(self, name: str) -> typing.Optional[str]: """ From f0e313c4917d6627e1e0001441a7ec38669fa016 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Dec 2022 14:24:59 -0800 Subject: [PATCH 1126/1221] Update changelog, and use default user icon instead of user avatar, so to not leak the user in anon commands --- CHANGELOG.md | 6 ++++++ bot.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cfeec9f8e..74d4ed5115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,14 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed +- Not having a guild icon no longer raises an exception. ([PR #3235](https://github.com/kyb3r/modmail/pull/3235)) + - When no icon is set, use the default user icon. - Resolved an issue where `?logs` doesn't work when the thread has no title. ([PR #3201](https://github.com/kyb3r/modmail/pull/3201)) +### Changed + +- Plain messages no longer forces `()` around the respondent text. ([PR #3234](https://github.com/kyb3r/modmail/pull/3234)) + # v4.0.1 This is a hotfix release. diff --git a/bot.py b/bot.py index 3602d37468..77aeb4142b 100644 --- a/bot.py +++ b/bot.py @@ -94,7 +94,7 @@ def get_guild_icon(self, guild: typing.Optional[discord.Guild]) -> str: if guild is None: guild = self.guild if guild.icon is None: - return self.user.display_avatar.url + return "https://cdn.discordapp.com/embed/avatars/0.png" return guild.icon.url def _resolve_snippet(self, name: str) -> typing.Optional[str]: From bb41861cf1e96c6bf8c4d50de99b23493cfc0d06 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 8 Dec 2022 14:42:08 -0800 Subject: [PATCH 1127/1221] Replaced discord.BadArgument with TypeError --- CHANGELOG.md | 1 + bot.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74d4ed5115..5bbc6910b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Not having a guild icon no longer raises an exception. ([PR #3235](https://github.com/kyb3r/modmail/pull/3235)) - When no icon is set, use the default user icon. - Resolved an issue where `?logs` doesn't work when the thread has no title. ([PR #3201](https://github.com/kyb3r/modmail/pull/3201)) +- AttributeError raised when failing to forward a reaction. ([GH #3218](https://github.com/kyb3r/modmail/issues/3218)) ### Changed diff --git a/bot.py b/bot.py index 77aeb4142b..2c8b8da73c 100644 --- a/bot.py +++ b/bot.py @@ -876,7 +876,7 @@ async def add_reaction( if reaction != "disable": try: await msg.add_reaction(reaction) - except (discord.HTTPException, discord.BadArgument) as e: + except (discord.HTTPException, TypeError) as e: logger.warning("Failed to add reaction %s: %s.", reaction, e) return False return True @@ -1304,7 +1304,7 @@ async def handle_reaction_events(self, payload): for msg in linked_messages: await msg.remove_reaction(reaction, self.user) await message.remove_reaction(reaction, self.user) - except (discord.HTTPException, discord.BadArgument) as e: + except (discord.HTTPException, TypeError) as e: logger.warning("Failed to remove reaction: %s", e) async def handle_react_to_contact(self, payload): From aed97d5b717e44698d4eb628f791c76dd59e7f1e Mon Sep 17 00:00:00 2001 From: Sattelite <90934664+Satellite-Galaxy@users.noreply.github.com> Date: Mon, 19 Dec 2022 05:15:52 -0500 Subject: [PATCH 1128/1221] Build and Push docker image to Githubs container registry (#3228) --- .github/workflows/docker-image.yml | 42 ++++++++++++++++++++++++++++++ Dockerfile | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000000..6af8630b38 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,42 @@ + +name: Create and publish a Docker image + +on: + push: + branches: ['master'] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index 2906f4508f..3c88a0e7ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9 as py +FROM python:3.10 as py FROM py as build From 88676e585af54a58e382e86543b5afed6093f3b2 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 19 Dec 2022 03:42:12 -0800 Subject: [PATCH 1129/1221] Update docker compose, changelog, and installation guide --- CHANGELOG.md | 2 + README.md | 131 ++++++++++++++++++++++++--------------------- docker-compose.yml | 4 +- 3 files changed, 74 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bbc6910b8..83cc9dd697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Changed - Plain messages no longer forces `()` around the respondent text. ([PR #3234](https://github.com/kyb3r/modmail/pull/3234)) +- Added workflow to automatically build Docker image ([PR #3232](https://github.com/kyb3r/modmail/pull/3228)) +- Updated installation guide to reflect new preferred hosting methods # v4.0.1 diff --git a/README.md b/README.md index 559d49ba65..a6ded0badd 100644 --- a/README.md +++ b/README.md @@ -84,80 +84,89 @@ This list is ever-growing thanks to active development and our exceptional contr ## Installation -Where can I find the Modmail bot invite link? +Q: Where can I find the Modmail bot invite link? + +A: Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time). + +There are a few options for hosting your very own dedicated Modmail bot. + +1. Patreon hosting +2. Local hosting (VPS, Dedicated Server, RPi, your computer, etc.) +3. PaaS (we provide a guide for Heroku) + +### Patreon Hosting + +If you don't want the trouble of renting and configuring your server to host Modmail, we got a solution for you! We offer hosting and maintenance of your own, private Modmail bot (including a Logviewer) through [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! + +### Local hosting (General Guide) + +Modmail can be hosted on any modern hardware, including your PC. For stability and reliability, we suggest purchasing a cloud server (VPS) for under $10/mo. If you need recommendations on choosing a VPS, join our [Discord server](https://discord.gg/j5e9p8w), and we'll send you a list of non-affiliated hosting providers. Alternatively, we can host Modmail for you when you're subscribed to our [Patreon](https://patreon.com/kyber). + +This guide assumes you've downloaded [`Python 3.10`](https://www.python.org/downloads/release/python-376/) and added python and pip to PATH. + +1. Clone this repo + ```console + $ git clone https://github.com/kyb3r/modmail + $ cd modmail + ``` +2. Create a Discord bot account, grant the necessary intents, and invite the bot ([guide](https://github.com/kyb3r/modmail/wiki/Installation#2-discord-bot-account)) +3. Create a free MongoDB database ([guide](https://github.com/kyb3r/modmail/wiki/Installation-(cont.)#3-create-a-database), follow it carefully!) +4. Rename the file `.env.example` to `.env` and fill it with appropriate values + - If you can't find `.env.example` because it's hidden, create a new text file named `.env`, then copy the contents of [this file](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) and replace the placeholders with their values + - If you're on Windows and cannot save the file as `.env`, save it as `.env.` instead (this only applies to Windows!) + - If you do not have a Logviewer yet, leave the `LOG_URL` field as-is +5. Update pip, install pipenv, and install dependencies using pipenv + ```console + $ pip install -U pip + $ pip install pipenv + $ pipenv install + ``` +6. Start the bot + ```console + $ pipenv run bot + ``` +7. Set up the Logviewer, see the [Logviewer installation guide](https://github.com/kyb3r/logviewer) + +### Local Hosting (Docker) + +We provide support for Docker to simplify the deployment of Modmail and Logviewer. +We assume you already have Docker and Docker Compose Plugin installed, if not, see [here](https://docs.docker.com/get-docker/). + +1. Create a Discord bot account, grant the necessary intents, and invite the bot ([guide](https://github.com/kyb3r/modmail/wiki/Installation#2-discord-bot-account)) +2. Create a file named `.env`, then copy the contents of [this file](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) and replace the placeholders with their values +3. Create a file named `docker-compose.yml`, then copy the contents of [this file](https://raw.githubusercontent.com/kyb3r/modmail/master/docker-compose.yml), do not change anything! +4. Start the bot + ```console + $ docker compose up -d + ``` + - For older Docker versions, you may need to run `docker-compose up -d` instead +5. View the status of your bot, using `docker ps` and `docker logs [container-id]` + +Our Docker images are hosted on [GitHub Container Registry](ghcr.io), you can build your own image if you wish: +```console +$ docker build --tag=modmail:master . +``` + +Then simply remove `ghcr.io/kyb3r/` from the `docker-compose.yml` file. -Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time). +### Local Hosting (OS-Specific) -### Heroku +This guide is a WIP. Join our [Discord server](https://discord.gg/j5e9p8w) for more info. -You can host this bot on Heroku. +### Platform as a Service (PaaS) + +You can host this bot on Heroku (no longer free). Installation via Heroku is possible with your web browser alone. The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. -To configure automatic updates: +When using Heroku, you can configure automatic updates: - Login to [GitHub](https://github.com/) and verify your account. - [Fork the repo](https://github.com/kyb3r/modmail/fork). - Install the [Pull app](https://github.com/apps/pull) for your fork. - Then go to the Deploy tab in your [Heroku account](https://dashboard.heroku.com/apps) of your bot app, select GitHub and connect your fork (usually by typing "Modmail"). - Turn on auto-deploy for the `master` branch. -### Hosting for Patreons - -If you don't want to go through the trouble of setting up your very own Modmail bot or wish to support this project, we got a solution for you! We offer the complete installation, hosting, and maintenance of your Modmail with [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! - -### Locally - -Local hosting of Modmail is also possible. First, you will need at least [`Python 3.8`](https://www.python.org/downloads/release/python-376/). - -Follow the [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) and disregard deploying the Heroku bot application. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. - -Clone the repo: - -```console -$ git clone https://github.com/kyb3r/modmail -$ cd modmail -``` - -Install dependencies: - -```console -$ pipenv install -``` - -Rename the `.env.example` to `.env` and fill out the fields. If `.env.example` is nonexistent (hidden), create a text file named `.env` and copy the contents of [`.env.example`](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) then modify the values. - -Finally, start Modmail. - -```console -$ pipenv run bot -``` - -#### Docker - -This repo supplies a Dockerfile for simplified deployment. - -You can build your own Docker image: - -```console -$ docker build . --tag=modmail -``` - -Or run directly from a pre-built version from https://hub.docker.com/. - -- Kyber's: - -```console -$ docker pull kyb3rr/modmail -``` - -And to run your docker image: - -```console -$ docker run --env-file .env kyb3rr/modmail -``` -- `.env` should be the path to your env file; you can also supply a path: `/path/to/.env`. - ## Sponsors Special thanks to our sponsors for supporting the project. diff --git a/docker-compose.yml b/docker-compose.yml index d3745c9b4c..0bdd3808bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.7" services: bot: - image: kyb3rr/modmail + image: ghcr.io/kyb3r/modmail:master restart: always env_file: - .env @@ -10,7 +10,7 @@ services: depends_on: - mongo logviewer: - image: kyb3rr/logviewer + image: ghcr.io/kyb3r/logviewer:master restart: always depends_on: - mongo From 6c690fe73403becbd0c388ebfbc19dfd77b244d9 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 19 Dec 2022 03:45:14 -0800 Subject: [PATCH 1130/1221] Fix black formatting --- bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 2c8b8da73c..a79981df5e 100644 --- a/bot.py +++ b/bot.py @@ -70,7 +70,7 @@ def __init__(self): self.config.populate_cache() intents = discord.Intents.all() - if not self.config['enable_presence_intent']: + if not self.config["enable_presence_intent"]: intents.presences = False super().__init__(command_prefix=None, intents=intents) # implemented in `get_prefix` @@ -229,7 +229,7 @@ async def runner(): self._connected = asyncio.Event() self.session = ClientSession(loop=self.loop) - if self.config['enable_presence_intent']: + if self.config["enable_presence_intent"]: logger.info("Starting bot with presence intent.") else: logger.info("Starting bot without presence intent.") From b693b27828437e771ec8496558b8992e7bdb6ee0 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 19 Dec 2022 03:47:39 -0800 Subject: [PATCH 1131/1221] Bump version to v4.0.2 --- CHANGELOG.md | 2 +- README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83cc9dd697..87d32b7f60 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 do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# [Unreleased] +# v4.0.2 ### Breaking diff --git a/README.md b/README.md index a6ded0badd..0a71061fd0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v4.0.1-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v4.0.2-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> diff --git a/bot.py b/bot.py index a79981df5e..b23b2449b2 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.1" +__version__ = "4.0.2" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 79a3e74f0b..468dc29112 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.1' +version = '4.0.2' description = "Modmail is similar to Reddit's 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 11eef8b8e25d3b0600b3bdef06a6ad8a98cb85bd Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Mon, 19 Dec 2022 18:14:45 -0500 Subject: [PATCH 1132/1221] Add alias make/create as an alias to alias add & add to changelog (#3195) --- CHANGELOG.md | 4 ++++ cogs/utility.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d32b7f60..0450b47c9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# [UNRELEASED] + +### Fixed +- `?alias make/create` as aliases to `?alias add`. This improves continuity between the bot and its command structure. # v4.0.2 diff --git a/cogs/utility.py b/cogs/utility.py index 5c0ea09eb3..ac642d9eb3 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1115,7 +1115,7 @@ async def make_alias(self, name, value, action): await self.bot.config.update() return embed - @alias.command(name="add") + @alias.command(name="add", aliases=["create", "make"]) @checks.has_permissions(PermissionLevel.MODERATOR) async def alias_add(self, ctx, name: str.lower, *, value): """ From c8b2522173b9923e8bfb009b7972ca7c40f72f43 Mon Sep 17 00:00:00 2001 From: Antonis Tatmichalis <44528553+lidistat67@users.noreply.github.com> Date: Tue, 20 Dec 2022 01:20:09 +0200 Subject: [PATCH 1133/1221] Fix silently order in help embed (#3215) --- cogs/modmail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 376f954cbe..5094e81890 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -483,7 +483,7 @@ async def close( Silently close a thread (no message) - `{prefix}close silently` - - `{prefix}close in 10m silently` + - `{prefix}close silently in 10m` Stop a thread from closing: - `{prefix}close cancel` From 485ab5ab936cb98748ad1f9d8825437dd15fc895 Mon Sep 17 00:00:00 2001 From: khakers <22665282+khakers@users.noreply.github.com> Date: Fri, 20 Jan 2023 16:30:30 -0800 Subject: [PATCH 1134/1221] Remove user fetching from blocked command (#3242) We already have all the information we need to mention a user (their id) and fetching the information from discord will lead to rate limiting and extremely poor performance on large block lists for no gain. --- cogs/modmail.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 5094e81890..484eb7aac8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1665,13 +1665,7 @@ async def blocked(self, ctx): self.bot.blocked_users.pop(str(id_)) logger.debug("No longer blocked, user %s.", id_) continue - - try: - user = await self.bot.get_or_fetch_user(int(id_)) - except discord.NotFound: - users.append((id_, reason)) - else: - users.append((user.mention, reason)) + users.append((f"<@{id_}>", reason)) blocked_roles = list(self.bot.blocked_roles.items()) for id_, reason in blocked_roles: From d6eba12937d4e0024e1c9208d0ac22b365829397 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Fri, 20 Jan 2023 16:34:17 -0800 Subject: [PATCH 1135/1221] Update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0450b47c9a..dfb6e3f27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s # [UNRELEASED] ### Fixed -- `?alias make/create` as aliases to `?alias add`. This improves continuity between the bot and its command structure. +- `?alias make/create` as aliases to `?alias add`. This improves continuity between the bot and its command structure. ([PR #3195](https://github.com/kyb3r/modmail/pull/3195)) +- Loading the blocked list with the `?blocked` command takes a long time when the list is large. ([PR #3242](https://github.com/kyb3r/modmail/pull/3242)) # v4.0.2 From 8d41c1d4b25cdede91f2f18954e15ced9cbf7bb9 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Wed, 8 Mar 2023 23:45:11 +0100 Subject: [PATCH 1136/1221] Added the option for registry_plugins_only (#3247) * Added the option for secure_plugins_only * Change config name * Forgot to update app.json * Forgot a period. Thanks taku :( --- app.json | 4 ++++ cogs/plugins.py | 8 ++++++++ core/config.py | 2 ++ 3 files changed, 14 insertions(+) diff --git a/app.json b/app.json index 66b5c77752..1cf5d107e4 100644 --- a/app.json +++ b/app.json @@ -34,6 +34,10 @@ "GITHUB_TOKEN": { "description": "A github personal access token with the repo scope.", "required": false + }, + "REGISTRY_PLUGINS_ONLY": { + "description": "If set to true, only plugins that are in the registry can be loaded.", + "required": false } } } \ No newline at end of file diff --git a/cogs/plugins.py b/cogs/plugins.py index 2bfac509af..c99f74ef40 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -302,6 +302,14 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): plugin = Plugin(user, repo, plugin_name, branch) else: + if not self.bot.config.get("registry_plugins_only", False): + embed = discord.Embed( + description="This plugin is not in the registry. " + "To install it, you must set `REGISTRY_PLUGINS_ONLY=false` in your .env file or config settings.", + color=self.bot.error_color, + ) + await ctx.send(embed=embed) + return try: plugin = Plugin.from_string(plugin_name) except InvalidPluginError: diff --git a/core/config.py b/core/config.py index 56db40c7b0..9a033167b7 100644 --- a/core/config.py +++ b/core/config.py @@ -167,6 +167,7 @@ class ConfigManager: "connection_uri": None, # replace mongo uri in the future "owners": None, "enable_presence_intent": False, + "registry_plugins_only": False, # bot "token": None, "enable_plugins": True, @@ -223,6 +224,7 @@ class ConfigManager: "thread_show_join_age", "use_hoisted_top_role", "enable_presence_intent", + "registry_plugins_only", } enums = { From 1f3bd0e9fcecb0be1feb217f174b47bb1d5e2251 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 12 Mar 2023 19:17:37 -0700 Subject: [PATCH 1137/1221] Update changelog and updated all the links to new repo (#3251) --- .github/CONTRIBUTING.md | 6 +- .github/pull.yml | 4 +- CHANGELOG.md | 236 +++++++++++++++++++++------------------- README.md | 48 ++++---- app.json | 2 +- cogs/modmail.py | 2 +- cogs/plugins.py | 4 +- cogs/utility.py | 6 +- core/changelog.py | 4 +- core/clients.py | 4 +- docker-compose.yml | 4 +- pyproject.toml | 4 +- 12 files changed, 166 insertions(+), 158 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b1309b6402..6c6694b809 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -30,11 +30,11 @@ Pull requests are the best way to propose changes to the codebase. We actively w ## Any contributions you make will be under the GNU Affero General Public License v3.0 In short, when you submit code changes, your submissions are understood to be under the same [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project. Feel free to contact the maintainers if that's a concern. -## Report bugs using [Github Issues](https://github.com/kyb3r/modmail/issues) -We use GitHub issues to track public bugs. Report a bug by [opening a new Issue](https://github.com/kyb3r/modmail/issues/new); it's that easy! +## Report bugs using [Github Issues](https://github.com/modmail-dev/modmail/issues) +We use GitHub issues to track public bugs. Report a bug by [opening a new Issue](https://github.com/modmail-dev/modmail/issues/new); it's that easy! ## Find pre-existing issues to tackle -Check out our [unstaged issue tracker](https://github.com/kyb3r/modmail/issues?q=is%3Aissue+is%3Aopen+-label%3Astaged) and start helping out! +Check out our [unstaged issue tracker](https://github.com/modmail-dev/modmail/issues?q=is%3Aissue+is%3Aopen+-label%3Astaged) and start helping out! Ways to help out: - Help out new members diff --git a/.github/pull.yml b/.github/pull.yml index 2fec8bbda0..8a0898a6b7 100644 --- a/.github/pull.yml +++ b/.github/pull.yml @@ -1,8 +1,8 @@ version: "1" rules: - base: master - upstream: kyb3r:master + upstream: modmail-dev:master mergeMethod: hardreset - base: development - upstream: kyb3r:development + upstream: modmail-dev:development mergeMethod: hardreset \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d32b7f60..46cf14177c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); -however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/modmail-dev/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# [UNRELEASED] + +### Added +- New .env config option: `REGISTRY_PLUGINS_ONLY`, restricts to only allow adding registry plugins. ([PR #3247](https://github.com/modmail-dev/modmail/pull/3247)) + +### Changed +- Repo moved to https://github.com/modmail-dev/modmail. + # v4.0.2 ### Breaking @@ -15,15 +23,15 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed -- Not having a guild icon no longer raises an exception. ([PR #3235](https://github.com/kyb3r/modmail/pull/3235)) +- Not having a guild icon no longer raises an exception. ([PR #3235](https://github.com/modmail-dev/modmail/pull/3235)) - When no icon is set, use the default user icon. -- Resolved an issue where `?logs` doesn't work when the thread has no title. ([PR #3201](https://github.com/kyb3r/modmail/pull/3201)) -- AttributeError raised when failing to forward a reaction. ([GH #3218](https://github.com/kyb3r/modmail/issues/3218)) +- Resolved an issue where `?logs` doesn't work when the thread has no title. ([PR #3201](https://github.com/modmail-dev/modmail/pull/3201)) +- AttributeError raised when failing to forward a reaction. ([GH #3218](https://github.com/modmail-dev/modmail/issues/3218)) ### Changed -- Plain messages no longer forces `()` around the respondent text. ([PR #3234](https://github.com/kyb3r/modmail/pull/3234)) -- Added workflow to automatically build Docker image ([PR #3232](https://github.com/kyb3r/modmail/pull/3228)) +- Plain messages no longer forces `()` around the respondent text. ([PR #3234](https://github.com/modmail-dev/modmail/pull/3234)) +- Added workflow to automatically build Docker image ([PR #3232](https://github.com/modmail-dev/modmail/pull/3228)) - Updated installation guide to reflect new preferred hosting methods # v4.0.1 @@ -43,59 +51,59 @@ This is a hotfix release. ### Breaking - Modmail now requires [`Message Content` privileged intent](https://support-dev.discord.com/hc/en-us/articles/4404772028055-Message-Content-Privileged-Intent-for-Verified-Bots). -- Upgraded to discord.py v2.0 ([internal changes](https://discordpy.readthedocs.io/en/latest/migrating.html), [GH #2990](https://github.com/kyb3r/modmail/issues/2990)). +- Upgraded to discord.py v2.0 ([internal changes](https://discordpy.readthedocs.io/en/latest/migrating.html), [GH #2990](https://github.com/modmail-dev/modmail/issues/2990)). - Python 3.8 or higher is required. - Asyncio changes ([gist](https://gist.github.com/Rapptz/6706e1c8f23ac27c98cee4dd985c8120)) - Plugin registry is purged and all developers have to re-apply due to breaking changes. ### Added -- `use_hoisted_top_role` config to use change how default mod tags work, see `v3.10.0#Added` for details. ([PR #3093](https://github.com/kyb3r/modmail/pull/3093)) -- `require_close_reason` config to require a reason to close a thread. ([GH #3107](https://github.com/kyb3r/modmail/issues/3107)) -- `plain_snippets` config to force all snippets to be plain. ([GH #3083](https://github.com/kyb3r/modmail/issues/3083)) +- `use_hoisted_top_role` config to use change how default mod tags work, see `v3.10.0#Added` for details. ([PR #3093](https://github.com/modmail-dev/modmail/pull/3093)) +- `require_close_reason` config to require a reason to close a thread. ([GH #3107](https://github.com/modmail-dev/modmail/issues/3107)) +- `plain_snippets` config to force all snippets to be plain. ([GH #3083](https://github.com/modmail-dev/modmail/issues/3083)) - `?fpareply` and `?fpreply` to reply to messages with variables plainly. -- `use_nickname_channel_name` config to use nicknames instead of usernames for channel names. ([GH #3112](https://github.com/kyb3r/modmail/issues/3112)) -- `use_random_channel_name` config to use random nicknames vaguely tied to user ID. It is unable to be computed in reverse. ([GH #3143](https://github.com/kyb3r/modmail/issues/3143)) -- `show_log_url_button` config to show Log URL button. ([GH #3122](https://github.com/kyb3r/modmail/issues/3122)) +- `use_nickname_channel_name` config to use nicknames instead of usernames for channel names. ([GH #3112](https://github.com/modmail-dev/modmail/issues/3112)) +- `use_random_channel_name` config to use random nicknames vaguely tied to user ID. It is unable to be computed in reverse. ([GH #3143](https://github.com/modmail-dev/modmail/issues/3143)) +- `show_log_url_button` config to show Log URL button. ([GH #3122](https://github.com/modmail-dev/modmail/issues/3122)) - Select menus for certain paginators. -- `Title` field in `?logs`. ([GH #3142](https://github.com/kyb3r/modmail/issues/3142)) -- Snippets can be used in aliases. ([GH #3108](https://github.com/kyb3r/modmail/issues/3108), [PR #3124](https://github.com/kyb3r/modmail/pull/3124)) -- `?snippet make/create` as aliases to `?snippet add`. ([GH #3172](https://github.com/kyb3r/modmail/issues/3173), [PR #3174](https://github.com/kyb3r/modmail/pull/3174)) +- `Title` field in `?logs`. ([GH #3142](https://github.com/modmail-dev/modmail/issues/3142)) +- Snippets can be used in aliases. ([GH #3108](https://github.com/modmail-dev/modmail/issues/3108), [PR #3124](https://github.com/modmail-dev/modmail/pull/3124)) +- `?snippet make/create` as aliases to `?snippet add`. ([GH #3172](https://github.com/modmail-dev/modmail/issues/3173), [PR #3174](https://github.com/modmail-dev/modmail/pull/3174)) ### Improved -- Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/kyb3r/modmail/issues/3048)) -- Use discord relative timedeltas. ([GH #3046](https://github.com/kyb3r/modmail/issues/3046)) +- Modmail now uses per-server avatars if applicable. ([GH #3048](https://github.com/modmail-dev/modmail/issues/3048)) +- Use discord relative timedeltas. ([GH #3046](https://github.com/modmail-dev/modmail/issues/3046)) - Use discord native buttons for all paginator sessions. - `?help` and `?blocked` paginator sessions now have better multi-page UI. - Autoupdate now automatically updates pipenv dependencies if possible. ### Fixed -- Several minor typos. ([PR #3095](https://github.com/kyb3r/modmail/pull/3095), [PR #3116](https://github.com/kyb3r/modmail/pull/3116)) -- Certain cases where fallback categories were not working as intended. ([PR #3109](https://github.com/kyb3r/modmail/pull/3109)) -- `?contact` would create in a random category in silent mode. ([GH #3091](https://github.com/kyb3r/modmail/issues/3091), [PR #3092](https://github.com/kyb3r/modmail/pull/3092)) -- Certain cases where `?close` would fail if closer isn't in cache. ([GH #3104](https://github.com/kyb3r/modmail/issues/3104), [PR #3105](https://github.com/kyb3r/modmail/pull/3105)) +- Several minor typos. ([PR #3095](https://github.com/modmail-dev/modmail/pull/3095), [PR #3116](https://github.com/modmail-dev/modmail/pull/3116)) +- Certain cases where fallback categories were not working as intended. ([PR #3109](https://github.com/modmail-dev/modmail/pull/3109)) +- `?contact` would create in a random category in silent mode. ([GH #3091](https://github.com/modmail-dev/modmail/issues/3091), [PR #3092](https://github.com/modmail-dev/modmail/pull/3092)) +- Certain cases where `?close` would fail if closer isn't in cache. ([GH #3104](https://github.com/modmail-dev/modmail/issues/3104), [PR #3105](https://github.com/modmail-dev/modmail/pull/3105)) - Stickers now work in Modmail. -- Large server sizes results in Guild.name == None. ([GH #3088](https://github.com/kyb3r/modmail/issues/3088)) -- Attachments now work on plain replies. ([GH #3102](https://github.com/kyb3r/modmail/issues/3102)) -- Support LOTTIE stickers. ([GH #3119](https://github.com/kyb3r/modmail/issues/3119)) -- Editing notes now work. ([GH #3094](https://github.com/kyb3r/modmail/issues/3094)) +- Large server sizes results in Guild.name == None. ([GH #3088](https://github.com/modmail-dev/modmail/issues/3088)) +- Attachments now work on plain replies. ([GH #3102](https://github.com/modmail-dev/modmail/issues/3102)) +- Support LOTTIE stickers. ([GH #3119](https://github.com/modmail-dev/modmail/issues/3119)) +- Editing notes now work. ([GH #3094](https://github.com/modmail-dev/modmail/issues/3094)) - Commands now work in threads. - Audit log searching now properly works. -- Old data causing `?blocked` to fail. ([GH #3131](https://github.com/kyb3r/modmail/issues/3131)) +- Old data causing `?blocked` to fail. ([GH #3131](https://github.com/modmail-dev/modmail/issues/3131)) - Delete channel auto close functionality now works. -- Improved error handling for autoupdate. ([PR #3161](https://github.com/kyb3r/modmail/pull/3161)) -- Skip loading of already-loaded cog. ([PR #3172](https://github.com/kyb3r/modmail/pull/3172)) -- Respect plugin's `cog_command_error`. ([GH #3170](https://github.com/kyb3r/modmail/issues/3170), [PR #3178](https://github.com/kyb3r/modmail/pull/3178)) -- Use silent as a typing literal for contacting. ([GH #3179](https://github.com/kyb3r/modmail/issues/3179)) +- Improved error handling for autoupdate. ([PR #3161](https://github.com/modmail-dev/modmail/pull/3161)) +- Skip loading of already-loaded cog. ([PR #3172](https://github.com/modmail-dev/modmail/pull/3172)) +- Respect plugin's `cog_command_error`. ([GH #3170](https://github.com/modmail-dev/modmail/issues/3170), [PR #3178](https://github.com/modmail-dev/modmail/pull/3178)) +- Use silent as a typing literal for contacting. ([GH #3179](https://github.com/modmail-dev/modmail/issues/3179)) ### Internal -- Improve regex parsing of channel topics. ([GH #3114](https://github.com/kyb3r/modmail/issues/3114), [PR #3111](https://github.com/kyb3r/modmail/pull/3111)) +- Improve regex parsing of channel topics. ([GH #3114](https://github.com/modmail-dev/modmail/issues/3114), [PR #3111](https://github.com/modmail-dev/modmail/pull/3111)) - Add warning if deploying on a developmental version. - Extensions are now loaded `on_connect`. -- MongoDB v5.0 clients are now supported. ([GH #3126](https://github.com/kyb3r/modmail/issues/3126)) +- MongoDB v5.0 clients are now supported. ([GH #3126](https://github.com/modmail-dev/modmail/issues/3126)) - Bump python-dotenv to v0.20.0, support for python 3.10 - Bump emoji to v1.7.0 - Bump aiohttp to v3.8.1 @@ -155,37 +163,37 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Added -- Ability to have group conversations with up to 5 users. ([GH #143](https://github.com/kyb3r/modmail/issues/143)) -- Snippets are invoked case insensitively. ([GH #3077](https://github.com/kyb3r/modmail/issues/3077), [PR #3080](https://github.com/kyb3r/modmail/pull/3080)) -- Default tags now use top hoisted role. ([GH #3014](https://github.com/kyb3r/modmail/issues/3014)) -- New thread-related config - `thread_show_roles`, `thread_show_account_age`, `thread_show_join_age`, `thread_cancelled`, `thread_creation_contact_title`, `thread_creation_self_contact_response`, `thread_creation_contact_response`. ([GH #3072](https://github.com/kyb3r/modmail/issues/3072)) +- Ability to have group conversations with up to 5 users. ([GH #143](https://github.com/modmail-dev/modmail/issues/143)) +- Snippets are invoked case insensitively. ([GH #3077](https://github.com/modmail-dev/modmail/issues/3077), [PR #3080](https://github.com/modmail-dev/modmail/pull/3080)) +- Default tags now use top hoisted role. ([GH #3014](https://github.com/modmail-dev/modmail/issues/3014)) +- New thread-related config - `thread_show_roles`, `thread_show_account_age`, `thread_show_join_age`, `thread_cancelled`, `thread_creation_contact_title`, `thread_creation_self_contact_response`, `thread_creation_contact_response`. ([GH #3072](https://github.com/modmail-dev/modmail/issues/3072)) - `use_timestamp_channel_name` config to create thread channels by timestamp. ### Improved -- `?contact` now accepts a role or multiple users (creates a group conversation). ([GH #3082](https://github.com/kyb3r/modmail/issues/3082)) -- Aliases are now supported in autotrigger. ([GH #3081](https://github.com/kyb3r/modmail/pull/3081)) +- `?contact` now accepts a role or multiple users (creates a group conversation). ([GH #3082](https://github.com/modmail-dev/modmail/issues/3082)) +- Aliases are now supported in autotrigger. ([GH #3081](https://github.com/modmail-dev/modmail/pull/3081)) ### Fixed -- Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/kyb3r/modmail/issues/3022), [PR #3028](https://github.com/kyb3r/modmail/pull/3028)) -- Blocked users are now no longer allowed to use `?contact` and react to contact. ([COMMENT #819004157](https://github.com/kyb3r/modmail/issues/2969#issuecomment-819004157), [PR #3027](https://github.com/kyb3r/modmail/pull/3027)) -- UnicodeEncodeError will no longer be raised on Windows. ([PR #3043](https://github.com/kyb3r/modmail/pull/3043)) -- Notifications are no longer duplicated when using both `?notify` and `subscribe`. ([PR #3015](https://github.com/kyb3r/modmail/pull/3015)) -- `?contact` now works properly with both category and silent. ([GH #3076](https://github.com/kyb3r/modmail/issues/3076)) -- `close_on_leave_reason` now works properly when `close_on_leave` is enabled. ([GH #3075](https://github.com/kyb3r/modmail/issues/3075)) +- Certain situations where the internal thread cache breaks and spams new channels. ([GH #3022](https://github.com/modmail-dev/modmail/issues/3022), [PR #3028](https://github.com/modmail-dev/modmail/pull/3028)) +- Blocked users are now no longer allowed to use `?contact` and react to contact. ([COMMENT #819004157](https://github.com/modmail-dev/modmail/issues/2969#issuecomment-819004157), [PR #3027](https://github.com/modmail-dev/modmail/pull/3027)) +- UnicodeEncodeError will no longer be raised on Windows. ([PR #3043](https://github.com/modmail-dev/modmail/pull/3043)) +- Notifications are no longer duplicated when using both `?notify` and `subscribe`. ([PR #3015](https://github.com/modmail-dev/modmail/pull/3015)) +- `?contact` now works properly with both category and silent. ([GH #3076](https://github.com/modmail-dev/modmail/issues/3076)) +- `close_on_leave_reason` now works properly when `close_on_leave` is enabled. ([GH #3075](https://github.com/modmail-dev/modmail/issues/3075)) - Invalid arguments are now properly catched and a proper error message is sent. -- Update database after resetting/purging all plugins. ([GH #3011](https://github.com/kyb3r/modmail/pull/3011)) -- `thread_auto_close` timer now only resets on non-note and replies from mods. ([GH #3030](https://github.com/kyb3r/modmail/issues/3030)) -- Deleted messages are now deleted on both ends. ([GH #3041](https://github.com/kyb3r/modmail/issues/3041), [@JerrieAries](https://github.com/kyb3r/modmail/commit/20b31f8e8b5497943513997fef788d72ae668438)) -- Persistent notes are now properly deleted from the database. ([GH #3013](https://github.com/kyb3r/modmail/issues/3013)) +- Update database after resetting/purging all plugins. ([GH #3011](https://github.com/modmail-dev/modmail/pull/3011)) +- `thread_auto_close` timer now only resets on non-note and replies from mods. ([GH #3030](https://github.com/modmail-dev/modmail/issues/3030)) +- Deleted messages are now deleted on both ends. ([GH #3041](https://github.com/modmail-dev/modmail/issues/3041), [@JerrieAries](https://github.com/modmail-dev/modmail/commit/20b31f8e8b5497943513997fef788d72ae668438)) +- Persistent notes are now properly deleted from the database. ([GH #3013](https://github.com/modmail-dev/modmail/issues/3013)) - Modmail Bot is now recognized to have `OWNER` permission level. This affects what can be run in autotriggers. ### Internal -- Fix return types, type hints and unresolved references ([PR #3009](https://github.com/kyb3r/modmail/pull/3009)) -- Reload thread cache only when it's the first on_ready trigger. ([GH #3037](https://github.com/kyb3r/modmail/issues/3037)) -- `format_channel_name` is now extendable to plugins. Modify `Bot.format_channel_name(bot, author, exclude_channel=None, force_null=False):`. ([GH #2982](https://github.com/kyb3r/modmail/issues/2982)) +- Fix return types, type hints and unresolved references ([PR #3009](https://github.com/modmail-dev/modmail/pull/3009)) +- Reload thread cache only when it's the first on_ready trigger. ([GH #3037](https://github.com/modmail-dev/modmail/issues/3037)) +- `format_channel_name` is now extendable to plugins. Modify `Bot.format_channel_name(bot, author, exclude_channel=None, force_null=False):`. ([GH #2982](https://github.com/modmail-dev/modmail/issues/2982)) # v3.9.5 @@ -199,17 +207,17 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Fixed -- Certain cases where fallback categories were not working as intended. ([GH #3002](https://github.com/kyb3r/modmail/issues/3002), [PR #3003](https://github.com/kyb3r/modmail/pull/3003)) +- Certain cases where fallback categories were not working as intended. ([GH #3002](https://github.com/modmail-dev/modmail/issues/3002), [PR #3003](https://github.com/modmail-dev/modmail/pull/3003)) - There is now a proper message when trying to contact a bot. ### Improved -- `?mention` can now be disabled with `?mention disable`. ([PR #2993](https://github.com/kyb3r/modmail/pull/2993/files)) -- `?mention` now allows vague entries such as `everyone` or `all`. ([PR #2993](https://github.com/kyb3r/modmail/pull/2993/files)) +- `?mention` can now be disabled with `?mention disable`. ([PR #2993](https://github.com/modmail-dev/modmail/pull/2993/files)) +- `?mention` now allows vague entries such as `everyone` or `all`. ([PR #2993](https://github.com/modmail-dev/modmail/pull/2993/files)) ### Internal -- Change heroku python version to 3.9.4 ([PR #3001](https://github.com/kyb3r/modmail/pull/3001)) +- Change heroku python version to 3.9.4 ([PR #3001](https://github.com/modmail-dev/modmail/pull/3001)) # v3.9.3 @@ -228,7 +236,7 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Improved -- Additional HostingMethods (i.e. DOCKER, PM2, SCREEN). Autoupdates are now disabled on all docker instances. ([GH #2977](https://github.com/kyb3r/modmail/issues/2977), [PR #2988](https://github.com/kyb3r/modmail/pull/2988)) +- Additional HostingMethods (i.e. DOCKER, PM2, SCREEN). Autoupdates are now disabled on all docker instances. ([GH #2977](https://github.com/modmail-dev/modmail/issues/2977), [PR #2988](https://github.com/modmail-dev/modmail/pull/2988)) ### Fixed @@ -249,8 +257,8 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Fixed -- `confirm_thread_creation` now properly works when a user opens a thread using react to contact. ([GH #2930](https://github.com/kyb3r/modmail/issues/2930), [PR #2971](https://github.com/kyb3r/modmail/pull/2971)) -- `?disable all/new` now disables react to contact threads. ([GH #2969](https://github.com/kyb3r/modmail/issues/2969), [PR #2971](https://github.com/kyb3r/modmail/pull/2971)) +- `confirm_thread_creation` now properly works when a user opens a thread using react to contact. ([GH #2930](https://github.com/modmail-dev/modmail/issues/2930), [PR #2971](https://github.com/modmail-dev/modmail/pull/2971)) +- `?disable all/new` now disables react to contact threads. ([GH #2969](https://github.com/modmail-dev/modmail/issues/2969), [PR #2971](https://github.com/modmail-dev/modmail/pull/2971)) - Ghost errors are no longer raised when threads are created using non-organic methods. ### Internal @@ -269,13 +277,13 @@ v3.10 adds group conversations while resolving other bugs and QOL changes. It is ### Added -- `?msglink <message id>`, allows you to obtain channel + message ID for T&S reports. ([GH #2963](https://github.com/kyb3r/modmail/issues/2963), [PR #2964](https://github.com/kyb3r/modmail/pull/2964)) -- `?mention disable/reset`, disables or resets mention on thread creation. ([PR #2951](https://github.com/kyb3r/modmail/pull/2951)) +- `?msglink <message id>`, allows you to obtain channel + message ID for T&S reports. ([GH #2963](https://github.com/modmail-dev/modmail/issues/2963), [PR #2964](https://github.com/modmail-dev/modmail/pull/2964)) +- `?mention disable/reset`, disables or resets mention on thread creation. ([PR #2951](https://github.com/modmail-dev/modmail/pull/2951)) ### Fixed - Non-master/development branch deployments no longer cause errors to be raised. -- Autotriggers now can search for roles/channels in guild context. ([GH #2961](https://github.com/kyb3r/modmail/issues/2961)) +- Autotriggers now can search for roles/channels in guild context. ([GH #2961](https://github.com/modmail-dev/modmail/issues/2961)) # v3.8.4 @@ -289,28 +297,28 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Fixed -- Retry with `null-discrim` if channel could not be created. ([GH #2934](https://github.com/kyb3r/modmail/issues/2934)) +- Retry with `null-discrim` if channel could not be created. ([GH #2934](https://github.com/modmail-dev/modmail/issues/2934)) - Fix update notifications. -- Retrieve user from Discord API if user has left the server, resolving issues in `?block`. ([GH #2935](https://github.com/kyb3r/modmail/issues/2935), [PR #2936](https://github.com/kyb3r/modmail/pull/2936)) +- Retrieve user from Discord API if user has left the server, resolving issues in `?block`. ([GH #2935](https://github.com/modmail-dev/modmail/issues/2935), [PR #2936](https://github.com/modmail-dev/modmail/pull/2936)) - IDs in `<member>` commands work now. # v3.8.1 ### Fixed -- Additional image uploads now render properly. ([PR #2933](https://github.com/kyb3r/modmail/pull/2933)) -- `confirm_thread_creation` no longer raises unnecessary errors. ([GH #2931](https://github.com/kyb3r/modmail/issues/2931), [PR #2933](https://github.com/kyb3r/modmail/pull/2933)) -- Autotriggers no longer sends attachments back. ([GH #2932](https://github.com/kyb3r/modmail/issues/2932)) +- Additional image uploads now render properly. ([PR #2933](https://github.com/modmail-dev/modmail/pull/2933)) +- `confirm_thread_creation` no longer raises unnecessary errors. ([GH #2931](https://github.com/modmail-dev/modmail/issues/2931), [PR #2933](https://github.com/modmail-dev/modmail/pull/2933)) +- Autotriggers no longer sends attachments back. ([GH #2932](https://github.com/modmail-dev/modmail/issues/2932)) # v3.8.0 ### Added -- `update_notifications` configuration option to toggle bot autoupdate notifications. ([GH #2896](https://github.com/kyb3r/modmail/issues/2896)) +- `update_notifications` configuration option to toggle bot autoupdate notifications. ([GH #2896](https://github.com/modmail-dev/modmail/issues/2896)) - `?fareply`, anonymously reply with variables. -- `anonymous_snippets` config variable to toggle if snippets should be anonymous. ([GH #2905](https://github.com/kyb3r/modmail/issues/2905)) +- `anonymous_snippets` config variable to toggle if snippets should be anonymous. ([GH #2905](https://github.com/modmail-dev/modmail/issues/2905)) - `disable_updates` config variable to control if the update command should be disabled or not. -- `silent_alert_on_mention` to alert mods silently. ([GH #2907](https://github.com/kyb3r/modmail/issues/2907)) +- `silent_alert_on_mention` to alert mods silently. ([GH #2907](https://github.com/modmail-dev/modmail/issues/2907)) - Support for only the "Server Members" intent. ### Improved @@ -321,17 +329,17 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Fixed -- Mentioned `competing` as an activity type. ([PR #2902](https://github.com/kyb3r/modmail/pull/2902)) +- Mentioned `competing` as an activity type. ([PR #2902](https://github.com/modmail-dev/modmail/pull/2902)) - Level permissions were not checked if command permissions were set. - Regex autotriggers were not working if term was in the middle of strings. - `?blocked` now no longers show blocks that have expired. - Blocked roles will no longer trigger an error during unblock. -- Custom emojis are now supported in `confirm_thread_creation_deny`. ([GH #2916](https://github.com/kyb3r/modmail/issues/2916)) -- Finding linked messages in replies work now. ([GH #2920](https://github.com/kyb3r/modmail/issues/2920), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2920#issuecomment-751530495)) -- Sending files in threads (non-images) now work. ([GH #2926](https://github.com/kyb3r/modmail/issues/2926)) -- Deleting messages no longer shows a false error. ([GH #2910](https://github.com/kyb3r/modmail/issues/2910), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2910#issuecomment-753557313)) +- Custom emojis are now supported in `confirm_thread_creation_deny`. ([GH #2916](https://github.com/modmail-dev/modmail/issues/2916)) +- Finding linked messages in replies work now. ([GH #2920](https://github.com/modmail-dev/modmail/issues/2920), [Jerrie-Aries](https://github.com/modmail-dev/modmail/issues/2920#issuecomment-751530495)) +- Sending files in threads (non-images) now work. ([GH #2926](https://github.com/modmail-dev/modmail/issues/2926)) +- Deleting messages no longer shows a false error. ([GH #2910](https://github.com/modmail-dev/modmail/issues/2910), [Jerrie-Aries](https://github.com/modmail-dev/modmail/issues/2910#issuecomment-753557313)) - Display an error on [Lottie](https://airbnb.io/lottie/#/) stickers, instead of failing the send. -- `?perms get` now shows role/user names. ([PR #2927](https://github.com/kyb3r/modmail/pull/2927)) +- `?perms get` now shows role/user names. ([PR #2927](https://github.com/modmail-dev/modmail/pull/2927)) ### Internal @@ -370,13 +378,13 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Added - Added `update_channel_id` to specify which channel autoupdate notifications were being sent to. -- Added `show_timestamp` to specify if timestamps should be displayed in message embeds. ([GH #2885](https://github.com/kyb3r/modmail/issues/2885)) +- Added `show_timestamp` to specify if timestamps should be displayed in message embeds. ([GH #2885](https://github.com/modmail-dev/modmail/issues/2885)) # v3.7.9 ### Fixed -- `perms add/remove` with permission levels should now work again. ([GH #2892](https://github.com/kyb3r/modmail/issues/2892), [PR #2893](https://github.com/kyb3r/modmail/pull/2893)) +- `perms add/remove` with permission levels should now work again. ([GH #2892](https://github.com/modmail-dev/modmail/issues/2892), [PR #2893](https://github.com/modmail-dev/modmail/pull/2893)) ### Improved @@ -386,7 +394,7 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Added -- Added `thread_contact_silently` to allow opening threads silently by default. ([PR #2887](https://github.com/kyb3r/modmail/pull/2887)) +- Added `thread_contact_silently` to allow opening threads silently by default. ([PR #2887](https://github.com/modmail-dev/modmail/pull/2887)) ### Fixed - Permission levels were not respected. @@ -412,7 +420,7 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Fixed - Autoupdate persists despite errors. -- Mention when normal thread created was not working. ([GH #2883](https://github.com/kyb3r/modmail/issues/2883)) +- Mention when normal thread created was not working. ([GH #2883](https://github.com/modmail-dev/modmail/issues/2883)) # v3.7.5 @@ -424,13 +432,13 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Fixed -- React to contact threads were treated like normal contact threads. ([GH #2881](https://github.com/kyb3r/modmail/issues/2881)) +- React to contact threads were treated like normal contact threads. ([GH #2881](https://github.com/modmail-dev/modmail/issues/2881)) # v3.7.2 ### Added -- Added `mention_channel_id` to specify which channel `alert_on_mention` was being sent to. ([GH #2880](https://github.com/kyb3r/modmail/issues/2880)) +- Added `mention_channel_id` to specify which channel `alert_on_mention` was being sent to. ([GH #2880](https://github.com/modmail-dev/modmail/issues/2880)) ### Fixed @@ -447,31 +455,31 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Added -- Plain replies functionality. Added commands `preply`, `pareply` and config `plain_reply_without_command`. ([GH #2872](https://github.com/kyb3r/modmail/issues/2872)) +- Plain replies functionality. Added commands `preply`, `pareply` and config `plain_reply_without_command`. ([GH #2872](https://github.com/modmail-dev/modmail/issues/2872)) - Added `react_to_contact_message`, `react_to_contact_emoji` to allow users to create threads by reacting to a message. -- Added `thread_move_notify_mods` to mention all mods again after moving thread. ([GH #215](https://github.com/kyb3r/modmail/issues/215)) -- Added `transfer_reactions` to link reactions between mods and users. ([GH #2763](https://github.com/kyb3r/modmail/issues/2763)) -- Added `close_on_leave`, `close_on_leave_reason` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/kyb3r/modmail/issues/2757)) -- Added `alert_on_mention` to mention mods upon a bot mention. ([GH #2833](https://github.com/kyb3r/modmail/issues/2833)) -- Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/kyb3r/modmail/issues/2773)) -- Support Gyazo image links in message embeds. ([GH #282](https://github.com/kyb3r/modmail/issues/282)) +- Added `thread_move_notify_mods` to mention all mods again after moving thread. ([GH #215](https://github.com/modmail-dev/modmail/issues/215)) +- Added `transfer_reactions` to link reactions between mods and users. ([GH #2763](https://github.com/modmail-dev/modmail/issues/2763)) +- Added `close_on_leave`, `close_on_leave_reason` to automatically close threads upon recipient leaving the server. ([GH #2757](https://github.com/modmail-dev/modmail/issues/2757)) +- Added `alert_on_mention` to mention mods upon a bot mention. ([GH #2833](https://github.com/modmail-dev/modmail/issues/2833)) +- Added `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny` to allow users to confirm that they indeed want to create a new thread. ([GH #2773](https://github.com/modmail-dev/modmail/issues/2773)) +- Support Gyazo image links in message embeds. ([GH #282](https://github.com/modmail-dev/modmail/issues/282)) - Added `silent` argument to `?contact` to restore old behaviour. -- Added new functionality: If `?help` is sent, bot does checks on every command, `?help all` restores old behaviour. ([GH #2847](https://github.com/kyb3r/modmail/issues/2847)) -- Added a way to block roles. ([GH #2753](https://github.com/kyb3r/modmail/issues/2753)) -- Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/kyb3r/modmail/issues/2865)) -- Added `?selfcontact` to allow users to open a thread. ([GH #2762](https://github.com/kyb3r/modmail/issues/2762)) +- Added new functionality: If `?help` is sent, bot does checks on every command, `?help all` restores old behaviour. ([GH #2847](https://github.com/modmail-dev/modmail/issues/2847)) +- Added a way to block roles. ([GH #2753](https://github.com/modmail-dev/modmail/issues/2753)) +- Added `cooldown_thread_title`, `cooldown_thread_response` to customise message sent when user is on a creating thread cooldown. ([GH #2865](https://github.com/modmail-dev/modmail/issues/2865)) +- Added `?selfcontact` to allow users to open a thread. ([GH #2762](https://github.com/modmail-dev/modmail/issues/2762)) - Support stickers and reject non-messages. (i.e. pin_add) -- Added support for thread titles, `?title`. ([GH #2838](https://github.com/kyb3r/modmail/issues/2838)) +- Added support for thread titles, `?title`. ([GH #2838](https://github.com/modmail-dev/modmail/issues/2838)) - Added `data_collection` to specify if bot metadata should be collected by Modmail developers. -- Added `?autotrigger`, `use_regex_autotrigger` config to specify keywords to trigger commands. ([GH #130](https://github.com/kyb3r/modmail/issues/130), [GH #649](https://github.com/kyb3r/modmail/issues/649)) -- Added `?note persistent` that creates notes that are persistent for a user. ([GH #2842](https://github.com/kyb3r/modmail/issues/2842), [PR #2878](https://github.com/kyb3r/modmail/pull/2878)) +- Added `?autotrigger`, `use_regex_autotrigger` config to specify keywords to trigger commands. ([GH #130](https://github.com/modmail-dev/modmail/issues/130), [GH #649](https://github.com/modmail-dev/modmail/issues/649)) +- Added `?note persistent` that creates notes that are persistent for a user. ([GH #2842](https://github.com/modmail-dev/modmail/issues/2842), [PR #2878](https://github.com/modmail-dev/modmail/pull/2878)) - Autoupdates and `?update` which was removed in v3.0.0 ### Fixed - `?contact` now sends members a DM. -- `level_permissions` and `command_permissions` would sometimes be reset. ([GH #2856](https://github.com/kyb3r/modmail/issues/2856)) -- Command truncated after && in alias. ([GH #2870](https://github.com/kyb3r/modmail/issues/2870)) +- `level_permissions` and `command_permissions` would sometimes be reset. ([GH #2856](https://github.com/modmail-dev/modmail/issues/2856)) +- Command truncated after && in alias. ([GH #2870](https://github.com/modmail-dev/modmail/issues/2870)) - `on_plugins_ready` event for plugins works now. ### Improved @@ -480,7 +488,7 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh - `?move` now does not require exact category names, accepts case-insensitive and startswith names. ### Internal -- Use enums in config. ([GH #2821](https://github.com/kyb3r/modmail/issues/2821)) +- Use enums in config. ([GH #2821](https://github.com/modmail-dev/modmail/issues/2821)) - `on_thread_close` event for plugins. - `on_thread_reply` event for plugins. @@ -503,14 +511,14 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh ### Added - Added `thread_move_title` to specify title of thread moved embed. -- Mark NSFW logs in log message. ([GH #2792](https://github.com/kyb3r/modmail/issues/2792)) -- Icon for moderator that closed the thread in log message. ([GH #2828](https://github.com/kyb3r/modmail/issues/2828)) -- Ability to set mentions via user/role ID. ([GH #2796](https://github.com/kyb3r/modmail/issues/2796)) +- Mark NSFW logs in log message. ([GH #2792](https://github.com/modmail-dev/modmail/issues/2792)) +- Icon for moderator that closed the thread in log message. ([GH #2828](https://github.com/modmail-dev/modmail/issues/2828)) +- Ability to set mentions via user/role ID. ([GH #2796](https://github.com/modmail-dev/modmail/issues/2796)) ### Changed - `?move` now consumes rest in category name, which means `?move Long Category Name` works without quotes! -- `?help` shows "No command description" if no description provided. ([PR #2845](https://github.com/kyb3r/modmail/pull/2845)) +- `?help` shows "No command description" if no description provided. ([PR #2845](https://github.com/modmail-dev/modmail/pull/2845)) ### Fixed - Unicode errors raised during windows selfhosting @@ -519,7 +527,7 @@ This update is a quick hotfix for a weird behaviour experienced on 1 Feb 2021 wh - Bump discord.py version to 1.5.1 - Explicitly state intents used for connection -- Use `--diff` for black CI instead of `--check` ([GH #2816](https://github.com/kyb3r/modmail/issues/2816)) +- Use `--diff` for black CI instead of `--check` ([GH #2816](https://github.com/modmail-dev/modmail/issues/2816)) # v3.5.0 @@ -529,7 +537,7 @@ Fixed discord.py issue. ### Added - A confirmation when you manually delete a thread message embed. -- Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. ([GH #2803](https://github.com/kyb3r/modmail/issues/2803)) +- Config var `enable_eval` defaults true, set `enable_eval=no` to disable the eval command. ([GH #2803](https://github.com/modmail-dev/modmail/issues/2803)) - Added `?plugins reset` command to completely reset everything related to plugins. This will fix some problems caused by broken plugins in the file system. - Support private GitHub repos for plugins (thanks to @officialpiyush pr#2767) @@ -838,7 +846,7 @@ Security update! - Removed auto-update functionality and the `?update` command in favor of the [Pull app](https://github.com/apps/pull). -Read more about updating your bot [here](https://github.com/kyb3r/modmail/wiki/updating) +Read more about updating your bot [here](https://github.com/modmail-dev/modmail/wiki/updating) ### Changed - Channel names now can contain Unicode characters. @@ -883,7 +891,7 @@ Added a 🛑 reaction to the paginators to delete the embed. ### Fixed -`?blocked` is now paginated using reactions. This fixes [#249](https://github.com/kyb3r/modmail/issues/249) +`?blocked` is now paginated using reactions. This fixes [#249](https://github.com/modmail-dev/modmail/issues/249) # v2.21.0 @@ -922,7 +930,7 @@ This update contains mostly internal changes. ### What's new? -New `?oauth whitelist` command, which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app, check the logviewer [repo](https://github.com/kyb3r/logviewer). +New `?oauth whitelist` command, which allows you to whitelist users so they can log in via discord to view logs. To set up oauth login for your logviewer app, check the logviewer [repo](https://github.com/modmail-dev/logviewer). # v2.19.1 @@ -1207,7 +1215,7 @@ Added image link in title in case discord fails to embed an image. ### What's new? - Plugins: - Think of it like addons! Anyone (with the skills) can create a plugin, make it public and distribute it. Add a welcome message to Modmail, or moderation commands? It's all up to your imagination! Have a niche feature request that you think only your server would benefit? Plugins are your go-to! - - [Creating Plugins Documentation](https://github.com/kyb3r/modmail/wiki/Plugins). + - [Creating Plugins Documentation](https://github.com/modmail-dev/modmail/wiki/Plugins). # v2.12.5 @@ -1218,7 +1226,7 @@ Added image link in title in case discord fails to embed an image. # v2.12.4 ### What's new? -- Named colors are now supported! Over 900 different common color names are recognized. A list of color names can be found in [core/_color_data.py](https://github.com/kyb3r/modmail/blob/master/core/_color_data.py). +- Named colors are now supported! Over 900 different common color names are recognized. A list of color names can be found in [core/_color_data.py](https://github.com/modmail-dev/modmail/blob/master/core/_color_data.py). - Named colors can be set the same way as hex. But this can only be done through `config set`, which means database modifications will not work. - For example: `config set main_color yellowish green`. - New config var `main_color` allows you to customize the main Modmail color (as requested by many). Defaults to Discord `blurple`. @@ -1412,7 +1420,7 @@ Thread channels will now default to being private (`@everyone`'s read message pe ### Background - Bots hosted by Heroku restart at least once every 27 hours. - During this period, local caches will be deleted, which results in the inability to set the scheduled close time to longer than 24 hours. This update resolves this issue. -- [PR #135](https://github.com/kyb3r/modmail/pull/135) +- [PR #135](https://github.com/modmail-dev/modmail/pull/135) ### Changed - Created a new internal config var: `closures`. @@ -1450,7 +1458,7 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o ### What's new? - Added the `?activity` command for setting the activity -- [PR #131](https://github.com/kyb3r/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening`, and `streaming`). +- [PR #131](https://github.com/modmail-dev/modmail/pull/131#issue-244686818) this supports multiple activity types (`playing`, `watching`, `listening`, and `streaming`). ### Removed - Removed the deprecated `status` command. @@ -1583,9 +1591,9 @@ Fixed a bug in the `?activity` command where it would fail to set the activity o # v2.0.0 -This release introduces the use of our centralized [API service](https://github.com/kyb3r/webserver) to enable dynamic configuration, auto-updates, and thread logs. +This release introduces the use of our centralized [API service](https://github.com/modmail-dev/webserver) to enable dynamic configuration, auto-updates, and thread logs. To use this release, you must acquire an API token from https://modmail.tk. -Read the updated installation guide [here](https://github.com/kyb3r/modmail/wiki/installation). +Read the updated installation guide [here](https://github.com/modmail-dev/modmail/wiki/installation). ### Changed - Stability improvements through synchronization primitives. diff --git a/README.md b/README.md index 0a71061fd0..062bf6d8a4 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,16 @@ <br> - <a href="https://heroku.com/deploy?template=https://github.com/kyb3r/modmail"> + <a href="https://heroku.com/deploy?template=https://github.com/modmail-dev/modmail"> <img src="https://img.shields.io/badge/deploy_to-heroku-997FBC.svg?style=for-the-badge&logo=Heroku"> </a> - <a href="https://github.com/kyb3r/modmail/"> + <a href="https://github.com/modmail-dev/modmail/"> <img src="https://api.modmail.dev/badges/instances.svg" alt="Bot instances"> </a> - <a href="https://discord.gg/j5e9p8w"> - <img src="https://img.shields.io/discord/515071617815019520.svg?label=Discord&logo=Discord&colorB=7289da&style=for-the-badge" alt="Support"> + <a href="https://discord.gg/cnUpwrnpYb"> + <img src="https://img.shields.io/discord/1079074933008781362.svg?label=Discord&logo=Discord&colorB=7289da&style=for-the-badge" alt="Support"> </a> <a href="https://patreon.com/kyber"> @@ -35,7 +35,7 @@ <img src="https://img.shields.io/badge/Code%20Style-Black-black?style=for-the-badge"> </a> - <a href="https://github.com/kyb3r/modmail/blob/master/LICENSE"> + <a href="https://github.com/modmail-dev/modmail/blob/master/LICENSE"> <img src="https://img.shields.io/badge/license-agpl-e74c3c.svg?style=for-the-badge" alt="MIT License"> </a> @@ -96,23 +96,23 @@ There are a few options for hosting your very own dedicated Modmail bot. ### Patreon Hosting -If you don't want the trouble of renting and configuring your server to host Modmail, we got a solution for you! We offer hosting and maintenance of your own, private Modmail bot (including a Logviewer) through [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for more info! +If you don't want the trouble of renting and configuring your server to host Modmail, we got a solution for you! We offer hosting and maintenance of your own, private Modmail bot (including a Logviewer) through [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/cnUpwrnpYb) for more info! ### Local hosting (General Guide) -Modmail can be hosted on any modern hardware, including your PC. For stability and reliability, we suggest purchasing a cloud server (VPS) for under $10/mo. If you need recommendations on choosing a VPS, join our [Discord server](https://discord.gg/j5e9p8w), and we'll send you a list of non-affiliated hosting providers. Alternatively, we can host Modmail for you when you're subscribed to our [Patreon](https://patreon.com/kyber). +Modmail can be hosted on any modern hardware, including your PC. For stability and reliability, we suggest purchasing a cloud server (VPS) for under $10/mo. If you need recommendations on choosing a VPS, join our [Discord server](https://discord.gg/cnUpwrnpYb), and we'll send you a list of non-affiliated hosting providers. Alternatively, we can host Modmail for you when you're subscribed to our [Patreon](https://patreon.com/kyber). This guide assumes you've downloaded [`Python 3.10`](https://www.python.org/downloads/release/python-376/) and added python and pip to PATH. 1. Clone this repo ```console - $ git clone https://github.com/kyb3r/modmail + $ git clone https://github.com/modmail-dev/modmail $ cd modmail ``` -2. Create a Discord bot account, grant the necessary intents, and invite the bot ([guide](https://github.com/kyb3r/modmail/wiki/Installation#2-discord-bot-account)) -3. Create a free MongoDB database ([guide](https://github.com/kyb3r/modmail/wiki/Installation-(cont.)#3-create-a-database), follow it carefully!) +2. Create a Discord bot account, grant the necessary intents, and invite the bot ([guide](https://github.com/modmail-dev/modmail/wiki/Installation#2-discord-bot-account)) +3. Create a free MongoDB database ([guide](https://github.com/modmail-dev/modmail/wiki/Installation-(cont.)#3-create-a-database), follow it carefully!) 4. Rename the file `.env.example` to `.env` and fill it with appropriate values - - If you can't find `.env.example` because it's hidden, create a new text file named `.env`, then copy the contents of [this file](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) and replace the placeholders with their values + - If you can't find `.env.example` because it's hidden, create a new text file named `.env`, then copy the contents of [this file](https://raw.githubusercontent.com/modmail-dev/modmail/master/.env.example) and replace the placeholders with their values - If you're on Windows and cannot save the file as `.env`, save it as `.env.` instead (this only applies to Windows!) - If you do not have a Logviewer yet, leave the `LOG_URL` field as-is 5. Update pip, install pipenv, and install dependencies using pipenv @@ -125,16 +125,16 @@ This guide assumes you've downloaded [`Python 3.10`](https://www.python.org/down ```console $ pipenv run bot ``` -7. Set up the Logviewer, see the [Logviewer installation guide](https://github.com/kyb3r/logviewer) +7. Set up the Logviewer, see the [Logviewer installation guide](https://github.com/modmail-dev/logviewer) ### Local Hosting (Docker) We provide support for Docker to simplify the deployment of Modmail and Logviewer. We assume you already have Docker and Docker Compose Plugin installed, if not, see [here](https://docs.docker.com/get-docker/). -1. Create a Discord bot account, grant the necessary intents, and invite the bot ([guide](https://github.com/kyb3r/modmail/wiki/Installation#2-discord-bot-account)) -2. Create a file named `.env`, then copy the contents of [this file](https://raw.githubusercontent.com/kyb3r/modmail/master/.env.example) and replace the placeholders with their values -3. Create a file named `docker-compose.yml`, then copy the contents of [this file](https://raw.githubusercontent.com/kyb3r/modmail/master/docker-compose.yml), do not change anything! +1. Create a Discord bot account, grant the necessary intents, and invite the bot ([guide](https://github.com/modmail-dev/modmail/wiki/Installation#2-discord-bot-account)) +2. Create a file named `.env`, then copy the contents of [this file](https://raw.githubusercontent.com/modmail-dev/modmail/master/.env.example) and replace the placeholders with their values +3. Create a file named `docker-compose.yml`, then copy the contents of [this file](https://raw.githubusercontent.com/modmail-dev/modmail/master/docker-compose.yml), do not change anything! 4. Start the bot ```console $ docker compose up -d @@ -147,22 +147,22 @@ Our Docker images are hosted on [GitHub Container Registry](ghcr.io), you can bu $ docker build --tag=modmail:master . ``` -Then simply remove `ghcr.io/kyb3r/` from the `docker-compose.yml` file. +Then simply remove `ghcr.io/modmail-dev/` from the `docker-compose.yml` file. ### Local Hosting (OS-Specific) -This guide is a WIP. Join our [Discord server](https://discord.gg/j5e9p8w) for more info. +This guide is a WIP. Join our [Discord server](https://discord.gg/cnUpwrnpYb) for more info. ### Platform as a Service (PaaS) You can host this bot on Heroku (no longer free). Installation via Heroku is possible with your web browser alone. -The [**installation guide**](https://github.com/kyb3r/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/etJNHCQ) for help and support. +The [**installation guide**](https://github.com/modmail-dev/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/cnUpwrnpYb) for help and support. When using Heroku, you can configure automatic updates: - Login to [GitHub](https://github.com/) and verify your account. - - [Fork the repo](https://github.com/kyb3r/modmail/fork). + - [Fork the repo](https://github.com/modmail-dev/modmail/fork). - Install the [Pull app](https://github.com/apps/pull) for your fork. - Then go to the Deploy tab in your [Heroku account](https://dashboard.heroku.com/apps) of your bot app, select GitHub and connect your fork (usually by typing "Modmail"). - Turn on auto-deploy for the `master` branch. @@ -213,20 +213,20 @@ Become a sponsor on [Patreon](https://patreon.com/kyber). Modmail supports the use of third-party plugins to extend or add functionalities to the bot. Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail. -You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/kyb3r/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. +You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/modmail-dev/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. -To develop your own, check out the [plugins documentation](https://github.com/kyb3r/modmail/wiki/Plugins). +To develop your own, check out the [plugins documentation](https://github.com/modmail-dev/modmail/wiki/Plugins). -Plugins requests and support are available in our [Modmail Support Server](https://discord.gg/j5e9p8w). +Plugins requests and support are available in our [Modmail Support Server](https://discord.gg/cnUpwrnpYb). ## Contributing -Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/kyb3r/modmail/blob/master/.github/CONTRIBUTING.md) before you get started. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/modmail-dev/modmail/blob/master/.github/CONTRIBUTING.md) before you get started. If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! ## Beta Testing -Our [development](https://github.com/kyb3r/modmail/tree/development) branch is where most of our features are tested before public release. Be warned that there could be bugs in various commands so keep it away from any large servers you manage. +Our [development](https://github.com/modmail-dev/modmail/tree/development) branch is where most of our features are tested before public release. Be warned that there could be bugs in various commands so keep it away from any large servers you manage. If you wish to test the new features and play around with them, feel free to join our [Public Test Server](https://discord.gg/v5hTjKC). Bugs can be raised within that server or in our Github issues (state that you are using the development branch though). diff --git a/app.json b/app.json index 1cf5d107e4..eee99b54bf 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { "name": "Modmail", "description": "An easy to install Modmail bot for Discord - DM to contact mods!", - "repository": "https://github.com/kyb3r/modmail", + "repository": "https://github.com/modmail-dev/modmail", "env": { "TOKEN": { "description": "Your discord bot's token.", diff --git a/cogs/modmail.py b/cogs/modmail.py index 376f954cbe..d620520487 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -94,7 +94,7 @@ async def setup(self, ctx): embed.add_field( 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: and if you are " + "[repo a star](https://github.com/modmail-dev/modmail) :star: and if you are " "feeling extra generous, buy us coffee on [Patreon](https://patreon.com/kyber) :heart:!", ) diff --git a/cogs/plugins.py b/cogs/plugins.py index c99f74ef40..f225d90caa 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -114,7 +114,7 @@ class Plugins(commands.Cog): These addons could have a range of features from moderation to simply making your life as a moderator easier! Learn how to create a plugin yourself here: - https://github.com/kyb3r/modmail/wiki/Plugins + https://github.com/modmail-dev/modmail/wiki/Plugins """ def __init__(self, bot): @@ -131,7 +131,7 @@ async def cog_load(self): logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") async def populate_registry(self): - url = "https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json" + url = "https://raw.githubusercontent.com/modmail-dev/modmail/master/plugins/registry.json" async with self.bot.session.get(url) as resp: self.registry = json.loads(await resp.text()) diff --git a/cogs/utility.py b/cogs/utility.py index 5c0ea09eb3..bcc1ea951a 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -351,7 +351,7 @@ async def about(self, ctx): embed.add_field( name="Want Modmail in Your Server?", - value="Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " + value="Follow the installation guide on [GitHub](https://github.com/modmail-dev/modmail/) " "and join our [Discord server](https://discord.gg/F34cRU8)!", inline=False, ) @@ -380,7 +380,7 @@ async def sponsors(self, ctx): """Shows the sponsors of this project.""" async with self.bot.session.get( - "https://raw.githubusercontent.com/kyb3r/modmail/master/SPONSORS.json" + "https://raw.githubusercontent.com/modmail-dev/modmail/master/SPONSORS.json" ) as resp: data = loads(await resp.text()) @@ -1941,7 +1941,7 @@ async def update(self, ctx, *, flag: str = ""): desc = ( f"The latest version is [`{self.bot.version}`]" - "(https://github.com/kyb3r/modmail/blob/master/bot.py#L1)" + "(https://github.com/modmail-dev/modmail/blob/master/bot.py#L1)" ) if self.bot.version >= parse_version(latest.version) and flag.lower() != "force": diff --git a/core/changelog.py b/core/changelog.py index a4f88ed323..06f141fce1 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -53,7 +53,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/modmail-dev/modmail/blob/{branch}/CHANGELOG.md" self.description = "" self.parse() @@ -186,7 +186,7 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": if branch not in ("master", "development"): branch = "master" - url = url or f"https://raw.githubusercontent.com/kyb3r/modmail/{branch}/CHANGELOG.md" + url = url or f"https://raw.githubusercontent.com/modmail-dev/modmail/{branch}/CHANGELOG.md" async with await bot.session.get(url) as resp: return cls(bot, branch, await resp.text()) diff --git a/core/clients.py b/core/clients.py index eebe3bcff6..f504b7051d 100644 --- a/core/clients.py +++ b/core/clients.py @@ -63,10 +63,10 @@ class GitHub: """ BASE = "https://api.github.com" - REPO = BASE + "/repos/kyb3r/modmail" + REPO = BASE + "/repos/modmail-dev/modmail" MERGE_URL = BASE + "/repos/{username}/modmail/merges" FORK_URL = REPO + "/forks" - STAR_URL = BASE + "/user/starred/kyb3r/modmail" + STAR_URL = BASE + "/user/starred/modmail-dev/modmail" def __init__(self, bot, access_token: str = "", username: str = "", **kwargs): self.bot = bot diff --git a/docker-compose.yml b/docker-compose.yml index 0bdd3808bd..fcb0e1b32f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.7" services: bot: - image: ghcr.io/kyb3r/modmail:master + image: ghcr.io/modmail-dev/modmail:master restart: always env_file: - .env @@ -10,7 +10,7 @@ services: depends_on: - mongo logviewer: - image: ghcr.io/kyb3r/logviewer:master + image: ghcr.io/modmail-dev/logviewer:master restart: always depends_on: - mongo diff --git a/pyproject.toml b/pyproject.toml index 468dc29112..751cc5a75a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,8 @@ authors = [ 'Taki <noemail@example.com>' ] readme = 'README.md' -repository = 'https://github.com/kyb3r/modmail' -homepage = 'https://github.com/kyb3r/modmail' +repository = 'https://github.com/modmail-dev/modmail' +homepage = 'https://github.com/modmail-dev/modmail' keywords = ['discord', 'modmail'] [tool.pylint.format] From 960a36ea04774ab449f1b2c51a24f8fb8822ae7e Mon Sep 17 00:00:00 2001 From: Spencer C <109806759+SpencerIsGiddy@users.noreply.github.com> Date: Tue, 14 Mar 2023 07:00:23 -0400 Subject: [PATCH 1138/1221] Lints: update to setup Python v4 (#3243) * Update lints.yml * Update lints.yml --------- Co-authored-by: Spencer Comfort <109806759+GiddyGoatGaming@users.noreply.github.com> Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- .github/workflows/lints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 21bf39368e..0f254d2980 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: x64 From e91930440aebfedc425b067791b5e92f2533bdfe Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 14 Mar 2023 04:00:54 -0700 Subject: [PATCH 1139/1221] Remove python 3.8 and windows/macos lint check. (#3258) Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> --- .github/workflows/lints.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 0f254d2980..eaad52221f 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -4,13 +4,12 @@ on: [push, pull_request] jobs: code-style: - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10'] - name: Python ${{ matrix.python-version }} on ${{ matrix.os }} + name: Python ${{ matrix.python-version }} on ubuntu-latest steps: - uses: actions/checkout@v3 From f2a434b0f0c32f7f61cdde0e16c2cd3b7b1b9d07 Mon Sep 17 00:00:00 2001 From: Cyrus Yip <54488650+RealCyGuy@users.noreply.github.com> Date: Tue, 14 Mar 2023 04:21:00 -0700 Subject: [PATCH 1140/1221] Fix replies in dms not being sent (#3239) * Fix replies not being sent * Use a list instead of a set --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index b23b2449b2..46eb00cb5c 100644 --- a/bot.py +++ b/bot.py @@ -888,7 +888,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: return sent_emoji, blocked_emoji = await self.retrieve_emoji() - if message.type != discord.MessageType.default: + if message.type not in [discord.MessageType.default, discord.MessageType.reply]: return thread = await self.threads.find(recipient=message.author) From 726fb0cedb930cf77f6b31a3dae1e854303c1268 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 14 Mar 2023 04:22:12 -0700 Subject: [PATCH 1141/1221] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb6e3f27d..0c1631dec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed - `?alias make/create` as aliases to `?alias add`. This improves continuity between the bot and its command structure. ([PR #3195](https://github.com/kyb3r/modmail/pull/3195)) - Loading the blocked list with the `?blocked` command takes a long time when the list is large. ([PR #3242](https://github.com/kyb3r/modmail/pull/3242)) +- Reply not being forwarded from DM. (PR [#3239](https://github.com/modmail-dev/modmail/pull/3239)) # v4.0.2 From b21037cbd794a962569de6240c1d95c13009a64e Mon Sep 17 00:00:00 2001 From: Martin <box152535@gmail.com> Date: Tue, 14 Mar 2023 13:26:18 +0100 Subject: [PATCH 1142/1221] Reminder Plugin (#3254) * Reminder Plugin Created a reminder plugin * Fix indentations --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- plugins/registry.json | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 8b6d26bf5f..fe175ea58a 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -35,6 +35,15 @@ "icon_url": "https://i.imgur.com/qtE7AH8.png", "thumbnail_url": "https://i.imgur.com/qtE7AH8.png" }, + "reminder": { + "repository": "martinbndr/kyb3r-modmail-plugins", + "branch": "master", + "description": "Let´s you create reminders.", + "bot_version": "4.0.0", + "title": "Reminder", + "icon_url": "https://raw.githubusercontent.com/martinbndr/kyb3r-modmail-plugins/master/reminder/logo.png", + "thumbnail_url": "https://raw.githubusercontent.com/martinbndr/kyb3r-modmail-plugins/master/reminder/logo.png" + }, "welcomer": { "repository": "fourjr/modmail-plugins", "branch": "v4", @@ -66,43 +75,43 @@ "repository": "fourjr/modmail-plugins", "branch": "v4", "description": "Allows managing server emotes via ?emoji", - "bot_version": "4.0.0", + "bot_version": "4.0.0", "title": "Emote Manager", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, - "gen-log": { + "gen-log": { "repository": "fourjr/modmail-plugins", "branch": "v4", "description": "Outputs a text log of a thread in a specified channel", - "bot_version": "4.0.0", + "bot_version": "4.0.0", "title": "Log Generator", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, - "media-logger": { + "media-logger": { "repository": "fourjr/modmail-plugins", "branch": "v4", "description": "Re-posts detected media from all visible channels into a specified logging channel", - "bot_version": "4.0.0", + "bot_version": "4.0.0", "title": "Media Logger", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, - "report": { + "report": { "repository": "fourjr/modmail-plugins", "branch": "v4", "description": "Specify an emoji to react with on messages. Generates a 'report' in specified logging channel upon react.", - "bot_version": "4.0.0", + "bot_version": "4.0.0", "title": "Report", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, - "top-supporters": { + "top-supporters": { "repository": "fourjr/modmail-plugins", "branch": "v4", "description": "Gathers and prints the top supporters of handling threads.", - "bot_version": "4.0.0", + "bot_version": "4.0.0", "title": "Top Supporters", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" From a7a7ce1390e6717bf41d82f8febc35a8a7c4147a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Serba?= <radoslaw@serba.ovh> Date: Thu, 18 May 2023 08:50:02 +0200 Subject: [PATCH 1143/1221] feat(build): drop root privileges, update .dockerignore --- .dockerignore | 5 +++++ Dockerfile | 3 +++ 2 files changed, 8 insertions(+) diff --git a/.dockerignore b/.dockerignore index 74003e7e30..4b3d7b8274 100644 --- a/.dockerignore +++ b/.dockerignore @@ -138,13 +138,18 @@ temp/ test.py # Other stuff +.dockerignore .env.example .gitignore .github/ app.json CHANGELOG.md +docker-compose.yml +LICENSE +PRIVACY.md Procfile pyproject.toml README.md Pipfile Pipfile.lock +SPONSORS.json diff --git a/Dockerfile b/Dockerfile index 3c88a0e7ca..2729a24826 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,3 +14,6 @@ COPY --from=build /inst /usr/local WORKDIR /modmailbot CMD ["python", "bot.py"] COPY . /modmailbot +RUN adduser --disabled-password --gecos '' app && \ + chown -R app /modmailbot +USER app From eb7995d90652345fd5521603ca25fbddb3e57549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Serba?= <radoslaw@serba.ovh> Date: Thu, 18 May 2023 08:57:55 +0200 Subject: [PATCH 1144/1221] feat(build): add Dockerfile to .dockerignore --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 4b3d7b8274..27f3de980f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -144,6 +144,7 @@ test.py .github/ app.json CHANGELOG.md +Dockerfile docker-compose.yml LICENSE PRIVACY.md From fa1a9494e8f51542eb55b7ad986239a9f8c36cf0 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sun, 9 Jul 2023 06:35:01 +0200 Subject: [PATCH 1145/1221] Remove modmail_guild_id from env examples (#3281) * Update .env.example The removal of `modmail_guild_id` since its causing more confusion to people then it helps them. Features relying on this never were made. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update app.json --------- Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> --- .env.example | 1 - app.json | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 44c91c59c7..14bdf060bf 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,5 @@ TOKEN=MyBotToken LOG_URL=https://logviewername.herokuapp.com/ GUILD_ID=1234567890 -MODMAIL_GUILD_ID=1234567890 OWNERS=Owner1ID,Owner2ID,Owner3ID CONNECTION_URI=mongodb+srv://mongodburi diff --git a/app.json b/app.json index eee99b54bf..decd58695c 100644 --- a/app.json +++ b/app.json @@ -11,10 +11,6 @@ "description": "The id for the server you are hosting this bot for.", "required": true }, - "MODMAIL_GUILD_ID": { - "description": "The ID of the discord server where the threads channels should be created (receiving server). Default to GUILD_ID.", - "required": false - }, "OWNERS": { "description": "Comma separated user IDs of people that are allowed to use owner only commands. (eval).", "required": true @@ -40,4 +36,4 @@ "required": false } } -} \ No newline at end of file +} From 319000b6b4c5e4523cb0de4e2a57c1cbd6f383f8 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 9 Jul 2023 10:34:13 +0000 Subject: [PATCH 1146/1221] Renamed user from app to modmail - Rename the user from 'app' to 'modmail', and made it a system user instead. - Moved user creation to a higher layer in the Dockerfile. Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> --- Dockerfile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2729a24826..96a398fad4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,17 +3,18 @@ FROM python:3.10 as py FROM py as build RUN apt update && apt install -y g++ git + COPY requirements.txt / RUN pip install --prefix=/inst -U -r /requirements.txt FROM py -ENV USING_DOCKER yes COPY --from=build /inst /usr/local +ENV USING_DOCKER yes +RUN useradd --system --no-create-home modmail +USER modmail + WORKDIR /modmailbot CMD ["python", "bot.py"] -COPY . /modmailbot -RUN adduser --disabled-password --gecos '' app && \ - chown -R app /modmailbot -USER app +COPY --chown=modmail:modmail . /modmailbot From 8a68f42ea0c7c3df051d8e9f68b232f6c466abcc Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 9 Jul 2023 10:38:09 +0000 Subject: [PATCH 1147/1221] Added .git to .dockerignore and unignored some files Removed LICENSE, PRIVACY.md, and SPONSORS.json from .dockerignore. Although they don't affect the functionalities of the bot, we prefer to include them due to their respective purposes. Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> --- .dockerignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.dockerignore b/.dockerignore index 27f3de980f..a3de147db4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -140,17 +140,15 @@ test.py # Other stuff .dockerignore .env.example +.git/ .gitignore .github/ app.json CHANGELOG.md Dockerfile docker-compose.yml -LICENSE -PRIVACY.md Procfile pyproject.toml README.md Pipfile Pipfile.lock -SPONSORS.json From fa572eed29fa9551c52ffc0897b8df5f4f33c394 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 13 Jul 2023 20:16:40 -0700 Subject: [PATCH 1148/1221] Fixed unset registry_plugins_only causing non-registry plugins to fail to install --- cogs/plugins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index f225d90caa..4fb8341c70 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -302,10 +302,10 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): plugin = Plugin(user, repo, plugin_name, branch) else: - if not self.bot.config.get("registry_plugins_only", False): + if self.bot.config.get("registry_plugins_only"): embed = discord.Embed( - description="This plugin is not in the registry. " - "To install it, you must set `REGISTRY_PLUGINS_ONLY=false` in your .env file or config settings.", + description="This plugin is not in the registry. To install this plugin, " + "you must set `REGISTRY_PLUGINS_ONLY=no` or remove this key in your .env file.", color=self.bot.error_color, ) await ctx.send(embed=embed) From 389dfb68648c1616aafe611c0e2bde5321042070 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 13 Jul 2023 20:19:28 -0700 Subject: [PATCH 1149/1221] To avoid confusion, config.get() only accepts one pos argument now --- CHANGELOG.md | 3 +++ core/config.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46cf14177c..e191fafb5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Changed - Repo moved to https://github.com/modmail-dev/modmail. +### Internal +- `ConfigManager.get` no longer accepts two positional arguments: the `convert` argument is now keyword-only. + # v4.0.2 ### Breaking diff --git a/core/config.py b/core/config.py index 9a033167b7..9476352573 100644 --- a/core/config.py +++ b/core/config.py @@ -301,7 +301,7 @@ def __getitem__(self, key: str) -> typing.Any: def __delitem__(self, key: str) -> None: return self.remove(key) - def get(self, key: str, convert=True) -> typing.Any: + def get(self, key: str, *, convert: bool = True) -> typing.Any: key = key.lower() if key not in self.all_keys: raise InvalidConfigError(f'Configuration "{key}" is invalid.') From 5ddb4e0444c1195e730924f8b9471e87ac330198 Mon Sep 17 00:00:00 2001 From: Baptiste Girardeau <contact@bgirardeau.me> Date: Sat, 15 Jul 2023 13:36:18 +0200 Subject: [PATCH 1150/1221] Set smaller size for guild icons urls on embed icons (#3261) * Set size for guild icons on embeds * Update changelog * Format with black * Fixed incorrect function return type --------- Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- CHANGELOG.md | 3 +-- bot.py | 14 +++++++++----- cogs/modmail.py | 8 ++++---- cogs/utility.py | 9 ++++++--- core/thread.py | 8 +++++--- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adee789203..bf75ef37d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,11 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Loading the blocked list with the `?blocked` command takes a long time when the list is large. ([PR #3242](https://github.com/kyb3r/modmail/pull/3242)) - Reply not being forwarded from DM. (PR [#3239](https://github.com/modmail-dev/modmail/pull/3239)) -# [UNRELEASED] - ### Added - New .env config option: `REGISTRY_PLUGINS_ONLY`, restricts to only allow adding registry plugins. ([PR #3247](https://github.com/modmail-dev/modmail/pull/3247)) ### Changed +- Guild icons in embed footers and author urls now have a fixed size of 128. ([PR #3261](https://github.com/modmail-dev/modmail/pull/3261)) - Repo moved to https://github.com/modmail-dev/modmail. # v4.0.2 diff --git a/bot.py b/bot.py index 46eb00cb5c..8f2f74e00c 100644 --- a/bot.py +++ b/bot.py @@ -90,12 +90,16 @@ def __init__(self): self.plugin_db = PluginDatabaseClient(self) # Deprecated self.startup() - def get_guild_icon(self, guild: typing.Optional[discord.Guild]) -> str: + def get_guild_icon( + self, guild: typing.Optional[discord.Guild], *, size: typing.Optional[int] = None + ) -> str: if guild is None: guild = self.guild if guild.icon is None: return "https://cdn.discordapp.com/embed/avatars/0.png" - return guild.icon.url + if size is None: + return guild.icon.url + return guild.icon.with_size(size).url def _resolve_snippet(self, name: str) -> typing.Optional[str]: """ @@ -912,7 +916,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: ) embed.set_footer( text=self.config["disabled_new_thread_footer"], - icon_url=self.get_guild_icon(guild=message.guild), + icon_url=self.get_guild_icon(guild=message.guild, size=128), ) logger.info("A new thread was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) @@ -928,7 +932,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: ) embed.set_footer( text=self.config["disabled_current_thread_footer"], - icon_url=self.get_guild_icon(guild=message.guild), + icon_url=self.get_guild_icon(guild=message.guild, size=128), ) logger.info("A message was blocked from %s due to disabled Modmail.", message.author) await self.add_reaction(message, blocked_emoji) @@ -1335,7 +1339,7 @@ async def handle_react_to_contact(self, payload): ) embed.set_footer( text=self.config["disabled_new_thread_footer"], - icon_url=self.get_guild_icon(guild=channel.guild), + icon_url=self.get_guild_icon(guild=channel.guild, size=128), ) logger.info( "A new thread using react to contact was blocked from %s due to disabled Modmail.", diff --git a/cogs/modmail.py b/cogs/modmail.py index d95b74d465..21ee536dab 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -160,7 +160,7 @@ async def snippet(self, ctx, *, name: str.lower = None): 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 add" to add a snippet.') - embed.set_author(name="Snippets", icon_url=self.bot.get_guild_icon(guild=ctx.guild)) + embed.set_author(name="Snippets", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128)) return await ctx.send(embed=embed) embeds = [] @@ -168,7 +168,7 @@ async def snippet(self, ctx, *, name: str.lower = None): 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=self.bot.get_guild_icon(guild=ctx.guild)) + embed.set_author(name="Snippets", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128)) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -1031,7 +1031,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, name = tag avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.get_guild_icon(guild=ctx.guild) + avatar_url = self.bot.get_guild_icon(guild=ctx.guild, size=128) em.set_footer(text=name, icon_url=avatar_url) for u in users: @@ -1120,7 +1120,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro name = tag avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.get_guild_icon(guild=ctx.guild) + avatar_url = self.bot.get_guild_icon(guild=ctx.guild, size=128) em.set_footer(text=name, icon_url=avatar_url) for u in users: diff --git a/cogs/utility.py b/cogs/utility.py index 245060c22b..b3a6d49aeb 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1020,7 +1020,7 @@ async def alias(self, ctx, *, name: str.lower = None): 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=self.bot.get_guild_icon(guild=ctx.guild)) + embed.set_author(name="Aliases", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128)) return await ctx.send(embed=embed) embeds = [] @@ -1028,7 +1028,9 @@ async def alias(self, ctx, *, name: str.lower = None): for i, names in enumerate(zip_longest(*(iter(sorted(self.bot.aliases)),) * 15)): description = utils.format_description(i, names) embed = discord.Embed(color=self.bot.main_color, description=description) - embed.set_author(name="Command Aliases", icon_url=self.bot.get_guild_icon(guild=ctx.guild)) + embed.set_author( + name="Command Aliases", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128) + ) embeds.append(embed) session = EmbedPaginatorSession(ctx, *embeds) @@ -1612,7 +1614,8 @@ async def permissions_get( ) embed = discord.Embed(color=self.bot.main_color, description=description) embed.set_author( - name="Permission Overrides", icon_url=self.bot.get_guild_icon(guild=ctx.guild) + name="Permission Overrides", + icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128), ) embeds.append(embed) diff --git a/core/thread.py b/core/thread.py index 53cdd1d202..0d2fb6408c 100644 --- a/core/thread.py +++ b/core/thread.py @@ -228,7 +228,9 @@ async def send_recipient_genesis_message(): else: footer = self.bot.config["thread_creation_footer"] - embed.set_footer(text=footer, icon_url=self.bot.get_guild_icon(guild=self.bot.modmail_guild)) + embed.set_footer( + text=footer, icon_url=self.bot.get_guild_icon(guild=self.bot.modmail_guild, size=128) + ) embed.title = self.bot.config["thread_creation_title"] if creator is None or creator == recipient: @@ -521,7 +523,7 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, embed.description = message footer = self.bot.config["thread_close_footer"] - embed.set_footer(text=footer, icon_url=self.bot.get_guild_icon(guild=self.bot.guild)) + embed.set_footer(text=footer, icon_url=self.bot.get_guild_icon(guild=self.bot.guild, size=128)) if not silent: for user in self.recipients: @@ -957,7 +959,7 @@ async def send( name = tag avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: - avatar_url = self.bot.get_guild_icon(guild=self.bot.guild) + avatar_url = self.bot.get_guild_icon(guild=self.bot.guild, size=128) embed.set_author( name=name, icon_url=avatar_url, From b1f3645a830dd914158315af1daea47f6479b9f5 Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Sat, 15 Jul 2023 17:10:51 +0530 Subject: [PATCH 1151/1221] `logs id` command (#3196) * Update modmail.py * Update clients.py * Formatting * Change log id to log key, added id as an alias * Print the log even if it is closed and fix bug * Update modmail.py * Added a missing period * Updated changelog --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- CHANGELOG.md | 1 + cogs/modmail.py | 22 ++++++++++++++++++++++ core/clients.py | 10 ++++++++++ 3 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf75ef37d3..fad0639ae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Added - New .env config option: `REGISTRY_PLUGINS_ONLY`, restricts to only allow adding registry plugins. ([PR #3247](https://github.com/modmail-dev/modmail/pull/3247)) +- `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) ### Changed - Guild icons in embed footers and author urls now have a fixed size of 128. ([PR #3261](https://github.com/modmail-dev/modmail/pull/3261)) diff --git a/cogs/modmail.py b/cogs/modmail.py index 21ee536dab..d72ea78cd5 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1212,6 +1212,28 @@ async def logs_closed_by(self, ctx, *, user: User = None): session = EmbedPaginatorSession(ctx, *embeds) await session.run() + @logs.command(name="key", aliases=["id"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def logs_key(self, ctx, key: str): + """ + Get the log link for the specified log key. + """ + icon_url = ctx.author.avatar.url + + logs = await self.bot.api.find_log_entry(key) + + if not logs: + embed = discord.Embed( + color=self.bot.error_color, + description=f"Log entry `{key}` not found.", + ) + return await ctx.send(embed=embed) + + embeds = self.format_log_embeds(logs, avatar_url=icon_url) + + session = EmbedPaginatorSession(ctx, *embeds) + await session.run() + @logs.command(name="delete", aliases=["wipe"]) @checks.has_permissions(PermissionLevel.OWNER) async def logs_delete(self, ctx, key_or_link: str): diff --git a/core/clients.py b/core/clients.py index f504b7051d..61c39fdd4b 100644 --- a/core/clients.py +++ b/core/clients.py @@ -356,6 +356,9 @@ async def validate_database_connection(self): async def get_user_logs(self, user_id: Union[str, int]) -> list: return NotImplemented + async def find_log_entry(self, key: str) -> list: + return NotImplemented + async def get_latest_user_logs(self, user_id: Union[str, int]): return NotImplemented @@ -529,6 +532,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 find_log_entry(self, key: str) -> list: + query = {"key": key} + projection = {"messages": {"$slice": 5}} + logger.debug(f"Retrieving log ID {key}.") + + 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}} From 43fbc312cfe9f1f8d8222f3e80a39ea5394038ea Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sat, 15 Jul 2023 19:45:36 +0800 Subject: [PATCH 1152/1221] Enable discord.py logger by default. (#3216) * Enable `discord.py` logger by default. * Revert: - Restore import orders - Logging stuff is now completely handled in `core.models.configure_logging` * Update logging configurations * Updated changelog * Fix overflow characters in logs when using `?debug` command. * Update changelog --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- CHANGELOG.md | 9 +- bot.py | 41 +------- cogs/utility.py | 23 +---- core/config.py | 1 + core/config_help.json | 9 ++ core/models.py | 214 +++++++++++++++++++++++++++++++----------- 6 files changed, 185 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fad0639ae3..d90473d40d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,17 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Reply not being forwarded from DM. (PR [#3239](https://github.com/modmail-dev/modmail/pull/3239)) ### Added -- New .env config option: `REGISTRY_PLUGINS_ONLY`, restricts to only allow adding registry plugins. ([PR #3247](https://github.com/modmail-dev/modmail/pull/3247)) - `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) +- `REGISTRY_PLUGINS_ONLY`, environment variable, when set, restricts to only allow adding registry plugins. ([PR #3247](https://github.com/modmail-dev/modmail/pull/3247)) +- `DISCORD_LOG_LEVEL` environment variable to set the log level of discord.py. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) ### Changed -- Guild icons in embed footers and author urls now have a fixed size of 128. ([PR #3261](https://github.com/modmail-dev/modmail/pull/3261)) - Repo moved to https://github.com/modmail-dev/modmail. +- Guild icons in embed footers and author urls now have a fixed size of 128. ([PR #3261](https://github.com/modmail-dev/modmail/pull/3261)) +- Discord.py internal logging is now enabled by default. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) + +### Internal +- Renamed `Bot.log_file_name` to `Bot.log_file_path`. Log files are now created at `temp/logs/modmail.log`. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) # v4.0.2 diff --git a/bot.py b/bot.py index 8f2f74e00c..45530a908f 100644 --- a/bot.py +++ b/bot.py @@ -52,7 +52,6 @@ logger = getLogger(__name__) - temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp") if not os.path.exists(temp_dir): os.mkdir(temp_dir) @@ -84,8 +83,11 @@ def __init__(self): self.threads = ThreadManager(self) - self.log_file_name = os.path.join(temp_dir, f"{self.token.split('.')[0]}.log") - self._configure_logging() + log_dir = os.path.join(temp_dir, "logs") + if not os.path.exists(log_dir): + os.mkdir(log_dir) + self.log_file_path = os.path.join(log_dir, "modmail.log") + configure_logging(self) self.plugin_db = PluginDatabaseClient(self) # Deprecated self.startup() @@ -182,29 +184,6 @@ async def load_extensions(self): logger.exception("Failed to load %s.", cog) logger.line("debug") - def _configure_logging(self): - level_text = self.config["log_level"].upper() - logging_levels = { - "CRITICAL": logging.CRITICAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, - } - logger.line() - - log_level = logging_levels.get(level_text) - if log_level is None: - log_level = self.config.remove("log_level") - logger.warning("Invalid logging level set: %s.", level_text) - logger.warning("Using default logging level: INFO.") - else: - logger.info("Logging level: %s", level_text) - - logger.info("Log file: %s", self.log_file_name) - configure_logging(self.log_file_name, log_level) - logger.debug("Successfully configured logging.") - @property def version(self): return parse_version(__version__) @@ -1801,16 +1780,6 @@ def main(): ) sys.exit(0) - # Set up discord.py internal logging - if os.environ.get("LOG_DISCORD"): - logger.debug(f"Discord logging enabled: {os.environ['LOG_DISCORD'].upper()}") - d_logger = logging.getLogger("discord") - - d_logger.setLevel(os.environ["LOG_DISCORD"].upper()) - handler = logging.FileHandler(filename="discord.log", encoding="utf-8", mode="w") - handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s")) - d_logger.addHandler(handler) - bot = ModmailBot() bot.run() diff --git a/cogs/utility.py b/cogs/utility.py index b3a6d49aeb..47889c2c93 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -401,13 +401,7 @@ async def sponsors(self, ctx): async def debug(self, ctx): """Shows the recent application logs of the bot.""" - log_file_name = self.bot.token.split(".")[0] - - with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), - "r+", - encoding="utf-8", - ) as f: + with open(self.bot.log_file_path, "r+", encoding="utf-8") as f: logs = f.read().strip() if not logs: @@ -433,7 +427,7 @@ async def debug(self, ctx): msg = "```Haskell\n" msg += line if len(msg) + 3 > 2000: - msg = msg[:1993] + "[...]```" + msg = msg[:1992] + "[...]```" messages.append(msg) msg = "```Haskell\n" @@ -455,12 +449,8 @@ async def debug_hastebin(self, ctx): """Posts application-logs to Hastebin.""" haste_url = os.environ.get("HASTE_URL", "https://hastebin.cc") - log_file_name = self.bot.token.split(".")[0] - with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), - "rb+", - ) as f: + with open(self.bot.log_file_path, "rb+") as f: logs = BytesIO(f.read().strip()) try: @@ -491,12 +481,7 @@ async def debug_hastebin(self, ctx): async def debug_clear(self, ctx): """Clears the locally cached logs.""" - log_file_name = self.bot.token.split(".")[0] - - with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), f"../temp/{log_file_name}.log"), - "w", - ): + with open(self.bot.log_file_path, "w"): pass await ctx.send( embed=discord.Embed(color=self.bot.main_color, description="Cached logs are now cleared.") diff --git a/core/config.py b/core/config.py index 9a033167b7..fa48864aed 100644 --- a/core/config.py +++ b/core/config.py @@ -178,6 +178,7 @@ class ConfigManager: "disable_updates": False, # Logging "log_level": "INFO", + "discord_log_level": "INFO", # data collection "data_collection": True, } diff --git a/core/config_help.json b/core/config_help.json index e7ebb9590d..f5b08d64ad 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -1129,6 +1129,15 @@ "This configuration can only to be set through `.env` file or environment (config) variables." ] }, + "discord_log_level": { + "default": "INFO", + "description": "The `discord.py` library logging level for logging to stdout.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] + }, "enable_plugins": { "default": "Yes", "description": "Whether plugins should be enabled and loaded into Modmail.", diff --git a/core/models.py b/core/models.py index 2eab1ceebb..29f6af71bb 100644 --- a/core/models.py +++ b/core/models.py @@ -1,16 +1,19 @@ import logging +import os import re import sys -import os +import _string + from difflib import get_close_matches from enum import IntEnum +from logging import FileHandler, StreamHandler, Handler from logging.handlers import RotatingFileHandler from string import Formatter +from typing import Optional import discord from discord.ext import commands -import _string try: from colorama import Fore, Style @@ -23,29 +26,6 @@ Fore = Style = type("Dummy", (object,), {"__getattr__": lambda self, item: ""})() -class PermissionLevel(IntEnum): - OWNER = 5 - ADMINISTRATOR = 4 - ADMIN = 4 - MODERATOR = 3 - MOD = 3 - SUPPORTER = 2 - RESPONDER = 2 - REGULAR = 1 - INVALID = -1 - - -class InvalidConfigError(commands.BadArgument): - def __init__(self, msg, *args): - super().__init__(msg, *args) - self.msg = msg - - @property - def embed(self): - # Single reference of Color.red() - return discord.Embed(title="Error", description=self.msg, color=discord.Color.red()) - - class ModmailLogger(logging.Logger): @staticmethod def _debug_(*msgs): @@ -94,18 +74,92 @@ def line(self, level="info"): ) -logging.setLoggerClass(ModmailLogger) -log_level = logging.INFO -loggers = set() +class FileFormatter(logging.Formatter): + ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") + + def format(self, record): + record.msg = self.ansi_escape.sub("", record.msg) + return super().format(record) + -ch = logging.StreamHandler(stream=sys.stdout) -ch.setLevel(log_level) -formatter = logging.Formatter( +log_stream_formatter = logging.Formatter( "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", datefmt="%m/%d/%y %H:%M:%S" ) -ch.setFormatter(formatter) +log_file_formatter = FileFormatter( + "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) -ch_debug = None + +def create_log_handler( + filename: Optional[str] = None, + *, + rotating: bool = False, + level: int = logging.DEBUG, + mode: str = "a+", + encoding: str = "utf-8", + maxBytes: int = 28000000, + backupCount: int = 1, + **kwargs, +) -> Handler: + """ + Creates a pre-configured log handler. This function is made for consistency's sake with + pre-defined default values for parameters and formatters to pass to handler class. + Additional keyword arguments also can be specified, just in case. + + Plugin developers should not use this and use `models.getLogger` instead. + + Parameters + ---------- + filename : Optional[Path] + Specifies that a `FileHandler` or `RotatingFileHandler` be created, using the specified filename, + rather than a `StreamHandler`. Defaults to `None`. + rotating : bool + Whether the file handler should be the `RotatingFileHandler`. Defaults to `False`. Note, this + argument only compatible if the `filename` is specified, otherwise `ValueError` will be raised. + level : int + The root logger level for the handler. Defaults to `logging.DEBUG`. + mode : str + If filename is specified, open the file in this mode. Defaults to 'a+'. + encoding : str + If this keyword argument is specified along with filename, its value is used when the `FileHandler` is created, + and thus used when opening the output file. Defaults to 'utf-8'. + maxBytes : int + The max file size before the rollover occurs. Defaults to 28000000 (28MB). Rollover occurs whenever the current + log file is nearly `maxBytes` in length; but if either of `maxBytes` or `backupCount` is zero, + rollover never occurs, so you generally want to set `backupCount` to at least 1. + backupCount : int + Max number of backup files. Defaults to 1. If this is set to zero, rollover will never occur. + + Returns + ------- + `StreamHandler` when `filename` is `None`, otherwise `FileHandler` or `RotatingFileHandler` + depending on the `rotating` value. + """ + if filename is None and rotating: + raise ValueError("`filename` must be set to instantiate a `RotatingFileHandler`.") + + if filename is None: + handler = StreamHandler(stream=sys.stdout, **kwargs) + handler.setFormatter(log_stream_formatter) + elif not rotating: + handler = FileHandler(filename, mode=mode, encoding=encoding, **kwargs) + handler.setFormatter(log_file_formatter) + else: + handler = RotatingFileHandler( + filename, mode=mode, encoding=encoding, maxBytes=maxBytes, backupCount=backupCount, **kwargs + ) + handler.setFormatter(log_file_formatter) + + handler.setLevel(level) + return handler + + +logging.setLoggerClass(ModmailLogger) +log_level = logging.INFO +loggers = set() +ch = create_log_handler(level=log_level) +ch_debug: Optional[RotatingFileHandler] = None def getLogger(name=None) -> ModmailLogger: @@ -118,33 +172,71 @@ def getLogger(name=None) -> ModmailLogger: return logger -class FileFormatter(logging.Formatter): - ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") - - def format(self, record): - record.msg = self.ansi_escape.sub("", record.msg) - return super().format(record) - - -def configure_logging(name, level=None): +def configure_logging(bot) -> None: global ch_debug, log_level - ch_debug = RotatingFileHandler(name, mode="a+", maxBytes=48000, backupCount=1, encoding="utf-8") - formatter_debug = FileFormatter( - "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - ch_debug.setFormatter(formatter_debug) - ch_debug.setLevel(logging.DEBUG) + logger = getLogger(__name__) + level_text = bot.config["log_level"].upper() + logging_levels = { + "CRITICAL": logging.CRITICAL, + "ERROR": logging.ERROR, + "WARNING": logging.WARNING, + "INFO": logging.INFO, + "DEBUG": logging.DEBUG, + } + logger.line() + + level = logging_levels.get(level_text) + if level is None: + level = bot.config.remove("log_level") + logger.warning("Invalid logging level set: %s.", level_text) + logger.warning("Using default logging level: %s.", level) + level = logging_levels[level] + else: + logger.info("Logging level: %s", level_text) + log_level = level + + logger.info("Log file: %s", bot.log_file_path) + ch_debug = create_log_handler(bot.log_file_path, rotating=True) + ch.setLevel(log_level) - if level is not None: - log_level = level + for log in loggers: + log.setLevel(log_level) + log.addHandler(ch_debug) + + # Set up discord.py logging + d_level_text = bot.config["discord_log_level"].upper() + d_level = logging_levels.get(d_level_text) + if d_level is None: + d_level = bot.config.remove("discord_log_level") + logger.warning("Invalid discord logging level set: %s.", d_level_text) + logger.warning("Using default discord logging level: %s.", d_level) + d_level = logging_levels[d_level] + d_logger = logging.getLogger("discord") + d_logger.setLevel(d_level) + + non_verbose_log_level = max(d_level, logging.INFO) + stream_handler = create_log_handler(level=non_verbose_log_level) + if non_verbose_log_level != d_level: + logger.info("Discord logging level (stdout): %s.", logging.getLevelName(non_verbose_log_level)) + logger.info("Discord logging level (logfile): %s.", logging.getLevelName(d_level)) + else: + logger.info("Discord logging level: %s.", logging.getLevelName(d_level)) + d_logger.addHandler(stream_handler) + d_logger.addHandler(ch_debug) + + logger.debug("Successfully configured logging.") - ch.setLevel(log_level) - for logger in loggers: - logger.setLevel(log_level) - logger.addHandler(ch_debug) +class InvalidConfigError(commands.BadArgument): + def __init__(self, msg, *args): + super().__init__(msg, *args) + self.msg = msg + + @property + def embed(self): + # Single reference of Color.red() + return discord.Embed(title="Error", description=self.msg, color=discord.Color.red()) class _Default: @@ -271,6 +363,18 @@ async def ack(self): return +class PermissionLevel(IntEnum): + OWNER = 5 + ADMINISTRATOR = 4 + ADMIN = 4 + MODERATOR = 3 + MOD = 3 + SUPPORTER = 2 + RESPONDER = 2 + REGULAR = 1 + INVALID = -1 + + class DMDisabled(IntEnum): NONE = 0 NEW_THREADS = 1 From a784f8299d023428e9a9d5a6fde0e06aab4d4feb Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sat, 15 Jul 2023 19:48:33 +0800 Subject: [PATCH 1153/1221] Cleanup after unloading extension. (#3226) * Cleanup after unloading extension, resolve #3223. * Remove leftover modules loaded from `plugins` path when purging. --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- CHANGELOG.md | 1 + cogs/plugins.py | 37 ++++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d90473d40d..68502a05a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?alias make/create` as aliases to `?alias add`. This improves continuity between the bot and its command structure. ([PR #3195](https://github.com/kyb3r/modmail/pull/3195)) - Loading the blocked list with the `?blocked` command takes a long time when the list is large. ([PR #3242](https://github.com/kyb3r/modmail/pull/3242)) - Reply not being forwarded from DM. (PR [#3239](https://github.com/modmail-dev/modmail/pull/3239)) +- Cleanup imports after removing/unloading a plugin. ([PR #3226](https://github.com/modmail-dev/Modmail/pull/3226)) ### Added - `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) diff --git a/cogs/plugins.py b/cogs/plugins.py index f225d90caa..b4d7ca415c 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -264,6 +264,17 @@ async def load_plugin(self, plugin): logger.error("Plugin load failure: %s", plugin.ext_string, exc_info=True) raise InvalidPluginError("Cannot load extension, plugin invalid.") from exc + async def unload_plugin(self, plugin: Plugin) -> None: + try: + await self.bot.unload_extension(plugin.ext_string) + except commands.ExtensionError as exc: + raise exc + + ext_parent = ".".join(plugin.ext_string.split(".")[:-1]) + for module in list(sys.modules.keys()): + if module == ext_parent or module.startswith(ext_parent + "."): + del sys.modules[module] + async def parse_user_input(self, ctx, plugin_name, check_version=False): if not self.bot.config["enable_plugins"]: @@ -378,7 +389,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): logger.warning("Unable to download plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.\n{type(e)}: {e}", + description=f"Failed to download plugin, check logs for error.\n{type(e).__name__}: {e}", color=self.bot.error_color, ) @@ -397,7 +408,7 @@ async def plugins_add(self, ctx, *, plugin_name: str): logger.warning("Unable to load plugin %s.", plugin, exc_info=True) embed = discord.Embed( - description=f"Failed to download plugin, check logs for error.\n{type(e)}: {e}", + description=f"Failed to load plugin, check logs for error.\n{type(e).__name__}: {e}", color=self.bot.error_color, ) @@ -438,7 +449,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str): if self.bot.config.get("enable_plugins"): try: - await self.bot.unload_extension(plugin.ext_string) + await self.unload_plugin(plugin) self.loaded_plugins.remove(plugin) except (commands.ExtensionNotLoaded, KeyError): logger.warning("Plugin was never loaded.") @@ -480,9 +491,10 @@ async def update_plugin(self, ctx, plugin_name): await self.download_plugin(plugin, force=True) if self.bot.config.get("enable_plugins"): try: - await self.bot.unload_extension(plugin.ext_string) + await self.unload_plugin(plugin) except commands.ExtensionError: logger.warning("Plugin unload fail.", exc_info=True) + try: await self.load_plugin(plugin) except Exception: @@ -529,17 +541,20 @@ async def plugins_reset(self, ctx): for ext in list(self.bot.extensions): if not ext.startswith("plugins."): continue + logger.error("Unloading plugin: %s.", ext) try: - logger.error("Unloading plugin: %s.", ext) - await self.bot.unload_extension(ext) - except Exception: - logger.error("Failed to unload plugin: %s.", ext) - else: - if not self.loaded_plugins: - continue plugin = next((p for p in self.loaded_plugins if p.ext_string == ext), None) if plugin: + await self.unload_plugin(plugin) self.loaded_plugins.remove(plugin) + else: + await self.bot.unload_extension(ext) + except Exception: + logger.error("Failed to unload plugin: %s.", ext) + + for module in list(sys.modules.keys()): + if module.startswith("plugins."): + del sys.modules[module] self.bot.config["plugins"].clear() await self.bot.config.update() From cc21725f2ff31a0566c7efe691a6bf87e4174bbd Mon Sep 17 00:00:00 2001 From: Cordila <49218334+Cordila@users.noreply.github.com> Date: Sat, 15 Jul 2023 17:23:46 +0530 Subject: [PATCH 1154/1221] Fix typo #3210 (#3233) * Fix typo * Rephrase the silently close messsage * Update changelog --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- CHANGELOG.md | 1 + cogs/modmail.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68502a05a0..e69f0f1849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Loading the blocked list with the `?blocked` command takes a long time when the list is large. ([PR #3242](https://github.com/kyb3r/modmail/pull/3242)) - Reply not being forwarded from DM. (PR [#3239](https://github.com/modmail-dev/modmail/pull/3239)) - Cleanup imports after removing/unloading a plugin. ([PR #3226](https://github.com/modmail-dev/Modmail/pull/3226)) +- Fixed a syntactic error in the close message when a thread is closed after a certain duration. ([PR #3233](https://github.com/modmail-dev/Modmail/pull/3233)) ### Added - `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) diff --git a/cogs/modmail.py b/cogs/modmail.py index d72ea78cd5..33086116c5 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -444,11 +444,9 @@ async def move(self, ctx, *, arguments): async def send_scheduled_close_message(self, ctx, after, silent=False): human_delta = human_timedelta(after.dt) - silent = "*silently* " if silent else "" - embed = discord.Embed( title="Scheduled close", - description=f"This thread will close {silent}{human_delta}.", + description=f"This thread will{' silently' if silent else ''} close in {human_delta}.", color=self.bot.error_color, ) From 1adbacfad21f12691d9b170d1c52ad61fad2ec9f Mon Sep 17 00:00:00 2001 From: Cyrus Yip <cyruscmyip1@gmail.com> Date: Sat, 15 Jul 2023 04:56:33 -0700 Subject: [PATCH 1155/1221] Strip whitespace in help command titles (#3271) * strip help command title * Update changelog --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- CHANGELOG.md | 1 + cogs/utility.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69f0f1849..3feb557449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Reply not being forwarded from DM. (PR [#3239](https://github.com/modmail-dev/modmail/pull/3239)) - Cleanup imports after removing/unloading a plugin. ([PR #3226](https://github.com/modmail-dev/Modmail/pull/3226)) - Fixed a syntactic error in the close message when a thread is closed after a certain duration. ([PR #3233](https://github.com/modmail-dev/Modmail/pull/3233)) +- Removed an extra space in the help command title when the command has no parameters. ([PR #3271](https://github.com/modmail-dev/Modmail/pull/3271)) ### Added - `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) diff --git a/cogs/utility.py b/cogs/utility.py index 47889c2c93..5386a86770 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -142,7 +142,7 @@ async def _get_help_embed(self, topic): perm_level = "NONE" embed = discord.Embed( - title=f"`{self.get_command_signature(topic)}`", + title=f"`{self.get_command_signature(topic).strip()}`", color=self.context.bot.main_color, description=self.process_help_msg(topic.help), ) From 77fbb6937b09d8994fbafea74dd34535ba767ed0 Mon Sep 17 00:00:00 2001 From: Martin <box152535@gmail.com> Date: Sat, 15 Jul 2023 13:59:26 +0200 Subject: [PATCH 1156/1221] confirm_thread_creation Buttons instead of reactions (#3273) * Reminder Plugin Created a reminder plugin * Fix indentations * confirm_thread_creation Buttons instead of reactions * Changelog+Small fixes * Updated the react to confirm message, and removed changelog entry * Code linting with black * Update changelog --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- CHANGELOG.md | 1 + core/config.py | 2 +- core/config_help.json | 4 ++-- core/thread.py | 51 +++++++++++++++---------------------------- core/utils.py | 29 ++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3feb557449..35cabb1e58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Repo moved to https://github.com/modmail-dev/modmail. - Guild icons in embed footers and author urls now have a fixed size of 128. ([PR #3261](https://github.com/modmail-dev/modmail/pull/3261)) - Discord.py internal logging is now enabled by default. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) +- The confirm-thread-creation dialog now uses buttons instead of reactions. ([PR #3273](https://github.com/modmail-dev/Modmail/pull/3273)) ### Internal - Renamed `Bot.log_file_name` to `Bot.log_file_path`. Log files are now created at `temp/logs/modmail.log`. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) diff --git a/core/config.py b/core/config.py index fa48864aed..9e4cf949de 100644 --- a/core/config.py +++ b/core/config.py @@ -123,7 +123,7 @@ class ConfigManager: # confirm thread creation "confirm_thread_creation": False, "confirm_thread_creation_title": "Confirm thread creation", - "confirm_thread_response": "React to confirm thread creation which will directly contact the moderators", + "confirm_thread_response": "Click the button to confirm thread creation which will directly contact the moderators.", "confirm_thread_creation_accept": "\N{WHITE HEAVY CHECK MARK}", "confirm_thread_creation_deny": "\N{NO ENTRY SIGN}", # regex diff --git a/core/config_help.json b/core/config_help.json index f5b08d64ad..4fafd7a3a6 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -1014,10 +1014,10 @@ ] }, "confirm_thread_response": { - "default": "React to confirm thread creation which will directly contact the moderators", + "default": "Click the button to confirm thread creation which will directly contact the moderators.", "description": "Description for the embed message sent to users to confirm a thread creation", "examples":[ - "`{prefix}config set confirm_thread_response React to confirm`" + "`{prefix}config set confirm_thread_response Click to confirm`" ], "notes": [ "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny`" diff --git a/core/thread.py b/core/thread.py index 0d2fb6408c..46be932c1a 100644 --- a/core/thread.py +++ b/core/thread.py @@ -29,6 +29,9 @@ get_top_role, create_thread_channel, get_joint_id, + AcceptButton, + DenyButton, + ConfirmThreadCreationView, ) logger = getLogger(__name__) @@ -1418,30 +1421,19 @@ async def create( destination = recipient else: destination = message.channel + view = ConfirmThreadCreationView() + view.add_item(AcceptButton(self.bot.config["confirm_thread_creation_accept"])) + view.add_item(DenyButton(self.bot.config["confirm_thread_creation_deny"])) confirm = await destination.send( embed=discord.Embed( title=self.bot.config["confirm_thread_creation_title"], description=self.bot.config["confirm_thread_response"], color=self.bot.main_color, - ) + ), + view=view, ) - accept_emoji = self.bot.config["confirm_thread_creation_accept"] - deny_emoji = self.bot.config["confirm_thread_creation_deny"] - emojis = [accept_emoji, deny_emoji] - for emoji in emojis: - await confirm.add_reaction(emoji) - await asyncio.sleep(0.2) - - try: - r, _ = await self.bot.wait_for( - "reaction_add", - check=lambda r, u: u.id == recipient.id - and r.message.id == confirm.id - and r.message.channel.id == confirm.channel.id - and str(r.emoji) in (accept_emoji, deny_emoji), - timeout=20, - ) - except asyncio.TimeoutError: + await view.wait() + if view.value is None: thread.cancelled = True self.bot.loop.create_task( destination.send( @@ -1452,23 +1444,16 @@ async def create( ) ) ) - else: - if str(r.emoji) == deny_emoji: - thread.cancelled = True - self.bot.loop.create_task( - destination.send( - embed=discord.Embed( - title=self.bot.config["thread_cancelled"], color=self.bot.error_color - ) + await confirm.edit(view=None) + if view.value is False: + thread.cancelled = True + self.bot.loop.create_task( + destination.send( + embed=discord.Embed( + title=self.bot.config["thread_cancelled"], color=self.bot.error_color ) ) - - async def remove_reactions(): - for emoji in emojis: - await confirm.remove_reaction(emoji, self.bot.user) - await asyncio.sleep(0.2) - - self.bot.loop.create_task(remove_reactions()) + ) if thread.cancelled: del self.cache[recipient.id] return thread diff --git a/core/utils.py b/core/utils.py index d8046ade5f..0e9f090d12 100644 --- a/core/utils.py +++ b/core/utils.py @@ -39,6 +39,9 @@ "get_top_role", "get_joint_id", "extract_block_timestamp", + "AcceptButton", + "DenyButton", + "ConfirmThreadCreationView", ] @@ -559,3 +562,29 @@ def extract_block_timestamp(reason, id_): raise return end_time, after + + +class AcceptButton(discord.ui.Button): + def __init__(self, emoji): + super().__init__(style=discord.ButtonStyle.gray, emoji=emoji) + + async def callback(self, interaction: discord.Interaction): + self.view.value = True + await interaction.response.edit_message(view=None) + self.view.stop() + + +class DenyButton(discord.ui.Button): + def __init__(self, emoji): + super().__init__(style=discord.ButtonStyle.gray, emoji=emoji) + + async def callback(self, interaction: discord.Interaction): + self.view.value = False + await interaction.response.edit_message(view=None) + self.view.stop() + + +class ConfirmThreadCreationView(discord.ui.View): + def __init__(self): + super().__init__(timeout=20) + self.value = None From 48bf245c24833c36a986ab670b50b1d999dc05cb Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Sat, 15 Jul 2023 14:01:48 +0200 Subject: [PATCH 1157/1221] Fixed the description of some config help descriptions having copy-paste errors (#3277) * Added the option for secure_plugins_only * Change config name * Forgot to update app.json * Forgot a period. Thanks taku :( * Fixed some copy-paste errors in descriptions * Update changelog --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- CHANGELOG.md | 1 + core/config_help.json | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35cabb1e58..2f27da4d1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Cleanup imports after removing/unloading a plugin. ([PR #3226](https://github.com/modmail-dev/Modmail/pull/3226)) - Fixed a syntactic error in the close message when a thread is closed after a certain duration. ([PR #3233](https://github.com/modmail-dev/Modmail/pull/3233)) - Removed an extra space in the help command title when the command has no parameters. ([PR #3271](https://github.com/modmail-dev/Modmail/pull/3271)) +- Corrected some incorrect config help descriptions. ([PR #3277](https://github.com/modmail-dev/Modmail/pull/3277)) ### Added - `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) diff --git a/core/config_help.json b/core/config_help.json index 4fafd7a3a6..f909d27821 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -131,7 +131,7 @@ "`{prefix}config set use_nickname_channel_name no`" ], "notes": [ - "This config is suitable for servers in Server Discovery to comply with channel name restrictions.", + "This config is NOT suitable for servers in Server Discovery to comply with channel name restrictions.", "This cannot be applied with `use_timestamp_channel_name`, `use_random_channel_name` or `use_user_id_channel_name`.", "See also: `use_timestamp_channel_name`, `use_user_id_channel_name`, `use_random_channel_name`." ] @@ -864,7 +864,7 @@ "default": "\"{{moderator.name}} has added you to a Modmail thread.\"", "description": "This is the message embed content sent to the recipient that is just added to a thread.", "examples": [ - "`{prefix}config set private_added_to_group_description Any message sent here will be sent to all otherthread recipients.`" + "`{prefix}config set private_added_to_group_response Any message sent here will be sent to all other thread recipients.`" ], "notes": [ "You may use the `{{moderator}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that added the user.", @@ -936,7 +936,7 @@ "default": "\"{{moderator.name}} has removed you from the Modmail thread.\"", "description": "This is the message embed content sent to the recipient that is just removed from a thread.", "examples": [ - "`{prefix}config set private_removed_from_group_description Bye`" + "`{prefix}config set private_removed_from_group_response Bye`" ], "notes": [ "You may use the `{{moderator}}` variable for access to the [Member](https://discordpy.readthedocs.io/en/latest/api.html#discord.Member) that added the user.", From d4ec13ed058dddd6c4a84c7bf47f4c516618f526 Mon Sep 17 00:00:00 2001 From: Martin <box152535@gmail.com> Date: Sat, 15 Jul 2023 14:04:53 +0200 Subject: [PATCH 1158/1221] [Fix] disable new command not updating db config (#3278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Fix] disable new command not updating ``disable new`` command didn´t update db config if modmail was already disabled for all threads. * Update changelog --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- CHANGELOG.md | 1 + cogs/modmail.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f27da4d1c..c13d8a757d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Guild icons in embed footers and author urls now have a fixed size of 128. ([PR #3261](https://github.com/modmail-dev/modmail/pull/3261)) - Discord.py internal logging is now enabled by default. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) - The confirm-thread-creation dialog now uses buttons instead of reactions. ([PR #3273](https://github.com/modmail-dev/Modmail/pull/3273)) +- `?disable all` no longer overrides `?disable new`. ([PR #3278](https://github.com/modmail-dev/Modmail/pull/3278)) ### Internal - Renamed `Bot.log_file_name` to `Bot.log_file_path`. Log files are now created at `temp/logs/modmail.log`. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) diff --git a/cogs/modmail.py b/cogs/modmail.py index 33086116c5..445015e4de 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -2152,7 +2152,7 @@ async def disable_new(self, ctx): description="Modmail will not create any new threads.", color=self.bot.main_color, ) - if self.bot.config["dm_disabled"] < DMDisabled.NEW_THREADS: + if self.bot.config["dm_disabled"] != DMDisabled.NEW_THREADS: self.bot.config["dm_disabled"] = DMDisabled.NEW_THREADS await self.bot.config.update() From f405aedf4af009ffa9f5502aa13c99d86e48e506 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sat, 15 Jul 2023 05:11:21 -0700 Subject: [PATCH 1159/1221] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c13d8a757d..158eb63a91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Discord.py internal logging is now enabled by default. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) - The confirm-thread-creation dialog now uses buttons instead of reactions. ([PR #3273](https://github.com/modmail-dev/Modmail/pull/3273)) - `?disable all` no longer overrides `?disable new`. ([PR #3278](https://github.com/modmail-dev/Modmail/pull/3278)) +- Dropped root privileges for Modmail running under Docker. ([PR #3284](https://github.com/modmail-dev/Modmail/pull/3284)) ### Internal - Renamed `Bot.log_file_name` to `Bot.log_file_path`. Log files are now created at `temp/logs/modmail.log`. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) From d891ad18ca0983f344a8c7da083c559e9bd96112 Mon Sep 17 00:00:00 2001 From: Stephen <48072084+StephenDaDev@users.noreply.github.com> Date: Sat, 15 Jul 2023 08:12:30 -0400 Subject: [PATCH 1160/1221] Remove Heroku Reference from Debug Command (#3292) * Very few users are still using Heroku for hosting, so this comment could cause confusion. * Fix mistake --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- cogs/utility.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 5386a86770..d0ecbeda22 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -410,7 +410,7 @@ async def debug(self, ctx): title="Debug Logs:", description="You don't have any logs at the moment.", ) - embed.set_footer(text="Go to Heroku to see your logs.") + embed.set_footer(text="Go to your console to see your logs.") return await ctx.send(embed=embed) messages = [] @@ -472,7 +472,7 @@ async def debug_hastebin(self, ctx): color=self.bot.main_color, description="Something's wrong. We're unable to upload your logs to hastebin.", ) - embed.set_footer(text="Go to Heroku to see your logs.") + embed.set_footer(text="Go to your console to see your logs.") await ctx.send(embed=embed) @debug.command(name="clear", aliases=["wipe"]) From 5170035df47e5f5571d94f1cb3f2175f1e70db62 Mon Sep 17 00:00:00 2001 From: Martin <box152535@gmail.com> Date: Sat, 15 Jul 2023 14:22:21 +0200 Subject: [PATCH 1161/1221] Added Autoreact plugin (#3285) * Added Autoreact plugin * Update changelog * black format (from a different pr's mistake) --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- CHANGELOG.md | 3 +-- cogs/plugins.py | 2 +- plugins/registry.json | 9 +++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75553acc79..9830415b10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) - `REGISTRY_PLUGINS_ONLY`, environment variable, when set, restricts to only allow adding registry plugins. ([PR #3247](https://github.com/modmail-dev/modmail/pull/3247)) - `DISCORD_LOG_LEVEL` environment variable to set the log level of discord.py. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) +- New registry plugin: [`autoreact`](https://github.com/martinbndr/kyb3r-modmail-plugins/tree/master/autoreact). ### Changed - Repo moved to https://github.com/modmail-dev/modmail. @@ -32,8 +33,6 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Internal - Renamed `Bot.log_file_name` to `Bot.log_file_path`. Log files are now created at `temp/logs/modmail.log`. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) - -### Internal - `ConfigManager.get` no longer accepts two positional arguments: the `convert` argument is now keyword-only. # v4.0.2 diff --git a/cogs/plugins.py b/cogs/plugins.py index 6922a1c79c..6bae7738c6 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -316,7 +316,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False): if self.bot.config.get("registry_plugins_only"): embed = discord.Embed( description="This plugin is not in the registry. To install this plugin, " - "you must set `REGISTRY_PLUGINS_ONLY=no` or remove this key in your .env file.", + "you must set `REGISTRY_PLUGINS_ONLY=no` or remove this key in your .env file.", color=self.bot.error_color, ) await ctx.send(embed=embed) diff --git a/plugins/registry.json b/plugins/registry.json index fe175ea58a..be138a30b2 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -17,6 +17,15 @@ "icon_url": "https://github.com/Jerrie-Aries.png", "thumbnail_url": "https://raw.githubusercontent.com/Jerrie-Aries/modmail-plugins/master/.static/announcement.jpg" }, + "autoreact": { + "repository": "martinbndr/kyb3r-modmail-plugins", + "branch": "master", + "description": "Automatically reacts with emojis in certain channels.", + "bot_version": "4.0.0", + "title": "Autoreact", + "icon_url": "https://raw.githubusercontent.com/martinbndr/kyb3r-modmail-plugins/master/autoreact/logo.png", + "thumbnail_url": "https://raw.githubusercontent.com/martinbndr/kyb3r-modmail-plugins/master/autoreact/logo.png" + }, "giveaway": { "repository": "Jerrie-Aries/modmail-plugins", "branch": "master", From 2b667102e95354fbdf07f88c3c88e95d802be0b7 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 17 Jul 2023 15:55:37 -0700 Subject: [PATCH 1162/1221] Fix #3291: Resolve code scanning alert for URL sanitization --- core/utils.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/core/utils.py b/core/utils.py index 0e9f090d12..60a1d654fd 100644 --- a/core/utils.py +++ b/core/utils.py @@ -147,13 +147,17 @@ def is_image_url(url: str, **kwargs) -> str: bool Whether the URL is a valid image URL. """ - if url.startswith("https://gyazo.com") or url.startswith("http://gyazo.com"): - # gyazo support - url = re.sub( - r"(http[s]?:\/\/)((?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)", - r"\1i.\2.png", - url, - ) + try: + result = parse.urlparse(url) + if result.netloc == 'gyazo.com' and result.scheme in ['http', 'https']: + # gyazo support + url = re.sub( + r"(https?://)((?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|%[0-9a-fA-F][0-9a-fA-F])+)", + r"\1i.\2.png", + url, + ) + except ValueError: + pass return parse_image_url(url, **kwargs) From f0c469ed1905ade9492f31dcd80e344de04f023b Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 17 Jul 2023 16:24:02 -0700 Subject: [PATCH 1163/1221] Implement #3187: Enhance bot join/leave logs across servers --- bot.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/bot.py b/bot.py index 45530a908f..5c8edd0843 100644 --- a/bot.py +++ b/bot.py @@ -48,7 +48,7 @@ ) from core.thread import ThreadManager from core.time import human_timedelta -from core.utils import extract_block_timestamp, normalize_alias, parse_alias, truncate, tryint +from core.utils import extract_block_timestamp, normalize_alias, parse_alias, truncate, tryint, human_join logger = getLogger(__name__) @@ -1380,28 +1380,44 @@ async def on_guild_channel_delete(self, channel): await thread.close(closer=mod, silent=True, delete_channel=False) async def on_member_remove(self, member): - if member.guild != self.guild: - return thread = await self.threads.find(recipient=member) if thread: - if self.config["close_on_leave"]: + if member.guild == self.guild and self.config["close_on_leave"]: await thread.close( closer=member.guild.me, message=self.config["close_on_leave_reason"], silent=True, ) else: - embed = discord.Embed( - description=self.config["close_on_leave_reason"], color=self.error_color - ) + if len(self.guilds) > 1: + guild_left = member.guild + remaining_guilds = member.mutual_guilds + + if remaining_guilds: + remaining_guild_names = [guild.name for guild in remaining_guilds] + leave_message = ( + f"The recipient has left {guild_left}. " + f"They are still in {human_join(remaining_guild_names, final='and')}." + ) + else: + leave_message = ( + f"The recipient has left {guild_left}. We no longer share any mutual servers." + ) + else: + leave_message = "The recipient has left the server." + + embed = discord.Embed(description=leave_message, color=self.error_color) await thread.channel.send(embed=embed) async def on_member_join(self, member): - if member.guild != self.guild: - return thread = await self.threads.find(recipient=member) if thread: - embed = discord.Embed(description="The recipient has joined the server.", color=self.mod_color) + if len(self.guilds) > 1: + guild_joined = member.guild + join_message = f"The recipient has joined {guild_joined}." + else: + join_message = "The recipient has joined the server." + embed = discord.Embed(description=join_message, color=self.mod_color) await thread.channel.send(embed=embed) async def on_message_delete(self, message): From 7508d524a5e3c6d2df3eabad5ec7d551a5edd4e6 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:23:32 -0700 Subject: [PATCH 1164/1221] Update readme with new documentation links, python version, and removed obsolete installion steps --- README.md | 119 +++++++++++++----------------------------------------- 1 file changed, 28 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 062bf6d8a4..319a9742cf 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ </a> <a href="https://www.python.org/downloads/"> - <img src="https://img.shields.io/badge/Made%20With-Python%203.8-blue.svg?style=for-the-badge&logo=Python" alt="Made with Python 3.8"> + <img src="https://img.shields.io/badge/Made%20With-Python%203.10-blue.svg?style=for-the-badge&logo=Python" alt="Made with Python 3.10"> </a> <a href="https://github.com/ambv/black"> @@ -50,11 +50,13 @@ Modmail is similar to Reddit's Modmail, both in functionality and purpose. It se This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included! +For up-to-date setup instructions, please visit our [**documentation**](https://docs.modmail.dev/installation) page. + ## How does it work? When a member sends a direct message to the bot, Modmail will create a channel or "thread" into a designated category. All further DM messages will automatically relay to that channel; any available staff can respond within the channel. -Our Logviewer will save the threads so you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.modmail.dev/example). +Our Logviewer will save the threads so you can view previous threads through their corresponding log link. ~~Here is an [**example**](https://logs.modmail.dev/example)~~ (demo not available at the moment). ## Features @@ -67,7 +69,7 @@ Our Logviewer will save the threads so you can view previous threads through the * Minimum length for members to be in the guild before allowed to contact Modmail (`guild_age`). * **Advanced Logging Functionality:** - * When you close a thread, Modmail will generate a [log link](https://logs.modmail.dev/example) and post it to your log channel. + * When you close a thread, Modmail will generate a log link and post it to your log channel. * Native Discord dark-mode feel. * Markdown/formatting support. * Login via Discord to protect your logs ([premium Patreon feature](https://patreon.com/kyber)). @@ -84,88 +86,34 @@ This list is ever-growing thanks to active development and our exceptional contr ## Installation -Q: Where can I find the Modmail bot invite link? +There are a number of options for hosting your very own dedicated Modmail bot. + +Visit our [**documentation**](https://docs.modmail.dev/installation) page for detailed guidance on how to deploy your Modmail bot. -A: Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following one of the methods listed below (roughly takes 15 minutes of your time). +### Patreon Hosting -There are a few options for hosting your very own dedicated Modmail bot. +If you don't want the trouble of renting and configuring your server to host Modmail, we got a solution for you! We offer hosting and maintenance of your own, private Modmail bot (including a Logviewer) through [**Patreon**](https://patreon.com/kyber). -1. Patreon hosting -2. Local hosting (VPS, Dedicated Server, RPi, your computer, etc.) -3. PaaS (we provide a guide for Heroku) +## FAQ -### Patreon Hosting +**Q: Where can I find the Modmail bot invite link?** + +**A:** Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following our [**documentation**](https://docs.modmail.dev/installation) steps or subscribe to [**Patreon**](https://patreon.com/kyber). + +**Q: Where can I find out more info about Modmail?** + +**A:** You can find more info about Modmail on our [**documentation**](https://docs.modmail.dev) page. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/cnUpwrnpYb) for help and support. + +## Plugins + +Modmail supports the use of third-party plugins to extend or add functionalities to the bot. +Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail. + +You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/modmail-dev/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. -If you don't want the trouble of renting and configuring your server to host Modmail, we got a solution for you! We offer hosting and maintenance of your own, private Modmail bot (including a Logviewer) through [**Patreon**](https://patreon.com/kyber). Join our [Modmail Discord Server](https://discord.gg/cnUpwrnpYb) for more info! - -### Local hosting (General Guide) - -Modmail can be hosted on any modern hardware, including your PC. For stability and reliability, we suggest purchasing a cloud server (VPS) for under $10/mo. If you need recommendations on choosing a VPS, join our [Discord server](https://discord.gg/cnUpwrnpYb), and we'll send you a list of non-affiliated hosting providers. Alternatively, we can host Modmail for you when you're subscribed to our [Patreon](https://patreon.com/kyber). - -This guide assumes you've downloaded [`Python 3.10`](https://www.python.org/downloads/release/python-376/) and added python and pip to PATH. - -1. Clone this repo - ```console - $ git clone https://github.com/modmail-dev/modmail - $ cd modmail - ``` -2. Create a Discord bot account, grant the necessary intents, and invite the bot ([guide](https://github.com/modmail-dev/modmail/wiki/Installation#2-discord-bot-account)) -3. Create a free MongoDB database ([guide](https://github.com/modmail-dev/modmail/wiki/Installation-(cont.)#3-create-a-database), follow it carefully!) -4. Rename the file `.env.example` to `.env` and fill it with appropriate values - - If you can't find `.env.example` because it's hidden, create a new text file named `.env`, then copy the contents of [this file](https://raw.githubusercontent.com/modmail-dev/modmail/master/.env.example) and replace the placeholders with their values - - If you're on Windows and cannot save the file as `.env`, save it as `.env.` instead (this only applies to Windows!) - - If you do not have a Logviewer yet, leave the `LOG_URL` field as-is -5. Update pip, install pipenv, and install dependencies using pipenv - ```console - $ pip install -U pip - $ pip install pipenv - $ pipenv install - ``` -6. Start the bot - ```console - $ pipenv run bot - ``` -7. Set up the Logviewer, see the [Logviewer installation guide](https://github.com/modmail-dev/logviewer) - -### Local Hosting (Docker) - -We provide support for Docker to simplify the deployment of Modmail and Logviewer. -We assume you already have Docker and Docker Compose Plugin installed, if not, see [here](https://docs.docker.com/get-docker/). - -1. Create a Discord bot account, grant the necessary intents, and invite the bot ([guide](https://github.com/modmail-dev/modmail/wiki/Installation#2-discord-bot-account)) -2. Create a file named `.env`, then copy the contents of [this file](https://raw.githubusercontent.com/modmail-dev/modmail/master/.env.example) and replace the placeholders with their values -3. Create a file named `docker-compose.yml`, then copy the contents of [this file](https://raw.githubusercontent.com/modmail-dev/modmail/master/docker-compose.yml), do not change anything! -4. Start the bot - ```console - $ docker compose up -d - ``` - - For older Docker versions, you may need to run `docker-compose up -d` instead -5. View the status of your bot, using `docker ps` and `docker logs [container-id]` - -Our Docker images are hosted on [GitHub Container Registry](ghcr.io), you can build your own image if you wish: -```console -$ docker build --tag=modmail:master . -``` - -Then simply remove `ghcr.io/modmail-dev/` from the `docker-compose.yml` file. - -### Local Hosting (OS-Specific) - -This guide is a WIP. Join our [Discord server](https://discord.gg/cnUpwrnpYb) for more info. - -### Platform as a Service (PaaS) - -You can host this bot on Heroku (no longer free). - -Installation via Heroku is possible with your web browser alone. -The [**installation guide**](https://github.com/modmail-dev/modmail/wiki/Installation) (which includes a video tutorial!) will guide you through the entire installation process. If you run into any problems, join our [Modmail Discord Server](https://discord.gg/cnUpwrnpYb) for help and support. - -When using Heroku, you can configure automatic updates: - - Login to [GitHub](https://github.com/) and verify your account. - - [Fork the repo](https://github.com/modmail-dev/modmail/fork). - - Install the [Pull app](https://github.com/apps/pull) for your fork. - - Then go to the Deploy tab in your [Heroku account](https://dashboard.heroku.com/apps) of your bot app, select GitHub and connect your fork (usually by typing "Modmail"). - - Turn on auto-deploy for the `master` branch. +To develop your own, check out the [plugins documentation](https://github.com/modmail-dev/modmail/wiki/Plugins). + +Plugins requests and support are available in our [Modmail Support Server](https://discord.gg/cnUpwrnpYb). ## Sponsors @@ -208,17 +156,6 @@ Discord Advice Center: Become a sponsor on [Patreon](https://patreon.com/kyber). -## Plugins - -Modmail supports the use of third-party plugins to extend or add functionalities to the bot. -Plugins allow niche features as well as anything else outside of the scope of the core functionality of Modmail. - -You can find a list of third-party plugins using the `?plugins registry` command or visit the [Unofficial List of Plugins](https://github.com/modmail-dev/modmail/wiki/Unofficial-List-of-Plugins) for a list of plugins contributed by the community. - -To develop your own, check out the [plugins documentation](https://github.com/modmail-dev/modmail/wiki/Plugins). - -Plugins requests and support are available in our [Modmail Support Server](https://discord.gg/cnUpwrnpYb). - ## Contributing Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/modmail-dev/modmail/blob/master/.github/CONTRIBUTING.md) before you get started. From 6d61cf29ff0d9e8a3b984eb44a6337402246bff0 Mon Sep 17 00:00:00 2001 From: Amy <git@amyerskine.me> Date: Sun, 19 Nov 2023 05:45:13 +0000 Subject: [PATCH 1165/1221] Add JSON logging support (#3305) * Add JSON logging support This adds support for JSON logging, along with the relevant options required. This does not change the default log behaviour, so should be backwards compatible. It is opt in via the LOG_FORMAT option, which can be 'json' to use the new logger, or anything else to fallback to the old behaviour. This is implemented in terms of a custom formatter, which is optionally applied to the stdout stream. The debug stream is unaffected by this. * Allow JSON to be selected when creating handlers * Allow different formats to be selected for streams/files * Remove old / unused code * Add new config opts to helpfile * Formatting, basic typing and reorder for consistency in project. --------- Co-authored-by: Jerrie-Aries <hidzrie@gmail.com> Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- core/config.py | 2 + core/config_help.json | 18 +++++++ core/models.py | 110 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 125 insertions(+), 5 deletions(-) diff --git a/core/config.py b/core/config.py index 527b2dc19f..a9e16a0df1 100644 --- a/core/config.py +++ b/core/config.py @@ -178,6 +178,8 @@ class ConfigManager: "disable_updates": False, # Logging "log_level": "INFO", + "stream_log_format": "plain", + "file_log_format": "plain", "discord_log_level": "INFO", # data collection "data_collection": True, diff --git a/core/config_help.json b/core/config_help.json index f909d27821..501c265827 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -1129,6 +1129,24 @@ "This configuration can only to be set through `.env` file or environment (config) variables." ] }, + "stream_log_format": { + "default": "plain", + "description": "The logging format when through a stream, can be 'plain' or 'json'", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] + }, + "file_log_format": { + "default": "plain", + "description": "The logging format when logging to a file, can be 'plain' or 'json'", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] + }, "discord_log_level": { "default": "INFO", "description": "The `discord.py` library logging level for logging to stdout.", diff --git a/core/models.py b/core/models.py index 29f6af71bb..2a397541c1 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,4 @@ +import json import logging import os import re @@ -9,7 +10,7 @@ from logging import FileHandler, StreamHandler, Handler from logging.handlers import RotatingFileHandler from string import Formatter -from typing import Optional +from typing import Dict, Optional import discord from discord.ext import commands @@ -74,6 +75,71 @@ def line(self, level="info"): ) +class JsonFormatter(logging.Formatter): + """ + Formatter that outputs JSON strings after parsing the LogRecord. + + Parameters + ---------- + fmt_dict : Optional[Dict[str, str]] + {key: logging format attribute} pairs. Defaults to {"message": "message"}. + time_format: str + time.strftime() format string. Default: "%Y-%m-%dT%H:%M:%S" + msec_format: str + Microsecond formatting. Appended at the end. Default: "%s.%03dZ" + """ + + def __init__( + self, + fmt_dict: Optional[Dict[str, str]] = None, + time_format: str = "%Y-%m-%dT%H:%M:%S", + msec_format: str = "%s.%03dZ", + ): + self.fmt_dict: Dict[str, str] = fmt_dict if fmt_dict is not None else {"message": "message"} + self.default_time_format: str = time_format + self.default_msec_format: str = msec_format + self.datefmt: Optional[str] = None + + def usesTime(self) -> bool: + """ + Overwritten to look for the attribute in the format dict values instead of the fmt string. + """ + return "asctime" in self.fmt_dict.values() + + def formatMessage(self, record) -> Dict[str, str]: + """ + Overwritten to return a dictionary of the relevant LogRecord attributes instead of a string. + KeyError is raised if an unknown attribute is provided in the fmt_dict. + """ + return {fmt_key: record.__dict__[fmt_val] for fmt_key, fmt_val in self.fmt_dict.items()} + + def format(self, record) -> str: + """ + Mostly the same as the parent's class method, the difference being that a dict is manipulated and dumped as JSON + instead of a string. + """ + record.message = record.getMessage() + + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + + message_dict = self.formatMessage(record) + + if record.exc_info: + # Cache the traceback text to avoid converting it multiple times + # (it's constant anyway) + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + + if record.exc_text: + message_dict["exc_info"] = record.exc_text + + if record.stack_info: + message_dict["stack_info"] = self.formatStack(record.stack_info) + + return json.dumps(message_dict, default=str) + + class FileFormatter(logging.Formatter): ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") @@ -85,11 +151,25 @@ def format(self, record): log_stream_formatter = logging.Formatter( "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", datefmt="%m/%d/%y %H:%M:%S" ) + log_file_formatter = FileFormatter( "%(asctime)s %(name)s[%(lineno)d] - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) +json_formatter = JsonFormatter( + { + "level": "levelname", + "message": "message", + "loggerName": "name", + "processName": "processName", + "processID": "process", + "threadName": "threadName", + "threadID": "thread", + "timestamp": "asctime", + } +) + def create_log_handler( filename: Optional[str] = None, @@ -98,6 +178,7 @@ def create_log_handler( level: int = logging.DEBUG, mode: str = "a+", encoding: str = "utf-8", + format: str = "plain", maxBytes: int = 28000000, backupCount: int = 1, **kwargs, @@ -124,6 +205,9 @@ def create_log_handler( encoding : str If this keyword argument is specified along with filename, its value is used when the `FileHandler` is created, and thus used when opening the output file. Defaults to 'utf-8'. + format : str + The format to output with, can either be 'json' or 'plain'. Will apply to whichever handler is created, + based on other conditional logic. maxBytes : int The max file size before the rollover occurs. Defaults to 28000000 (28MB). Rollover occurs whenever the current log file is nearly `maxBytes` in length; but if either of `maxBytes` or `backupCount` is zero, @@ -141,23 +225,28 @@ def create_log_handler( if filename is None: handler = StreamHandler(stream=sys.stdout, **kwargs) - handler.setFormatter(log_stream_formatter) + formatter = log_stream_formatter elif not rotating: handler = FileHandler(filename, mode=mode, encoding=encoding, **kwargs) - handler.setFormatter(log_file_formatter) + formatter = log_file_formatter else: handler = RotatingFileHandler( filename, mode=mode, encoding=encoding, maxBytes=maxBytes, backupCount=backupCount, **kwargs ) - handler.setFormatter(log_file_formatter) + formatter = log_file_formatter + + if format == "json": + formatter = json_formatter handler.setLevel(level) + handler.setFormatter(formatter) return handler logging.setLoggerClass(ModmailLogger) log_level = logging.INFO loggers = set() + ch = create_log_handler(level=log_level) ch_debug: Optional[RotatingFileHandler] = None @@ -173,7 +262,11 @@ def getLogger(name=None) -> ModmailLogger: def configure_logging(bot) -> None: - global ch_debug, log_level + global ch_debug, log_level, ch + + stream_log_format, file_log_format = bot.config["stream_log_format"], bot.config["file_log_format"] + if stream_log_format == "json": + ch.setFormatter(json_formatter) logger = getLogger(__name__) level_text = bot.config["log_level"].upper() @@ -198,8 +291,15 @@ def configure_logging(bot) -> None: logger.info("Log file: %s", bot.log_file_path) ch_debug = create_log_handler(bot.log_file_path, rotating=True) + + if file_log_format == "json": + ch_debug.setFormatter(json_formatter) + ch.setLevel(log_level) + logger.info("Stream log format: %s", stream_log_format) + logger.info("File log format: %s", file_log_format) + for log in loggers: log.setLevel(log_level) log.addHandler(ch_debug) From 5c710596c870121d2df864ceec8d86f0333fbb49 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sat, 18 Nov 2023 21:47:34 -0800 Subject: [PATCH 1166/1221] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9830415b10..0b3a61943a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `REGISTRY_PLUGINS_ONLY`, environment variable, when set, restricts to only allow adding registry plugins. ([PR #3247](https://github.com/modmail-dev/modmail/pull/3247)) - `DISCORD_LOG_LEVEL` environment variable to set the log level of discord.py. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) - New registry plugin: [`autoreact`](https://github.com/martinbndr/kyb3r-modmail-plugins/tree/master/autoreact). +- `STREAM_LOG_FORMAT` and `FILE_LOG_FORMAT` for settings the log format of the stream and file handlers respectively. Possible options are `json` and `plain` (default). ([PR #3305](https://github.com/modmail-dev/Modmail/pull/3305)) ### Changed - Repo moved to https://github.com/modmail-dev/modmail. From ae99060a3fe4a0afcd2d2fa35af85e8fce5b8aaa Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Sun, 19 Nov 2023 16:38:42 +0800 Subject: [PATCH 1167/1221] Fix rate limit issue on raw reaction add/remove events. (#3306) * Fix rate limit issue on raw reaction add/remove events. * Pasd message object to `find_linked_messages` since it is already fetched. --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- bot.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/bot.py b/bot.py index 45530a908f..3701d826ea 100644 --- a/bot.py +++ b/bot.py @@ -1222,25 +1222,36 @@ async def handle_reaction_events(self, payload): return channel = self.get_channel(payload.channel_id) - if not channel: # dm channel not in internal cache - _thread = await self.threads.find(recipient=user) - if not _thread: + thread = None + # dm channel not in internal cache + if not channel: + thread = await self.threads.find(recipient=user) + if not thread: + return + channel = await thread.recipient.create_dm() + if channel.id != payload.channel_id: + return + + from_dm = isinstance(channel, discord.DMChannel) + from_txt = isinstance(channel, discord.TextChannel) + if not from_dm and not from_txt: + return + + if not thread: + params = {"recipient": user} if from_dm else {"channel": channel} + thread = await self.threads.find(**params) + if not thread: return - channel = await _thread.recipient.create_dm() + # thread must exist before doing this API call try: message = await channel.fetch_message(payload.message_id) except (discord.NotFound, discord.Forbidden): return reaction = payload.emoji - close_emoji = await self.convert_emoji(self.config["close_emoji"]) - - if isinstance(channel, discord.DMChannel): - thread = await self.threads.find(recipient=user) - if not thread: - return + if from_dm: if ( payload.event_type == "REACTION_ADD" and message.embeds @@ -1248,7 +1259,7 @@ async def handle_reaction_events(self, payload): and self.config.get("recipient_thread_close") ): ts = message.embeds[0].timestamp - if thread and ts == thread.channel.created_at: + if ts == thread.channel.created_at: # the reacted message is the corresponding thread creation embed # closing thread return await thread.close(closer=user) @@ -1268,11 +1279,10 @@ async def handle_reaction_events(self, payload): logger.warning("Failed to find linked message for reactions: %s", e) return else: - thread = await self.threads.find(channel=channel) - if not thread: - return try: - _, *linked_messages = await thread.find_linked_messages(message.id, either_direction=True) + _, *linked_messages = await thread.find_linked_messages( + message1=message, either_direction=True + ) except ValueError as e: logger.warning("Failed to find linked message for reactions: %s", e) return From a8d7c26d8cfd863c13b4862ffae1cb998a13fda8 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 19 Nov 2023 02:21:42 -0800 Subject: [PATCH 1168/1221] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b3a61943a..76a929b178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Fixed a syntactic error in the close message when a thread is closed after a certain duration. ([PR #3233](https://github.com/modmail-dev/Modmail/pull/3233)) - Removed an extra space in the help command title when the command has no parameters. ([PR #3271](https://github.com/modmail-dev/Modmail/pull/3271)) - Corrected some incorrect config help descriptions. ([PR #3277](https://github.com/modmail-dev/Modmail/pull/3277)) +- Rate limit issue when fetch the messages due to reaction linking. ([PR #3306](https://github.com/modmail-dev/Modmail/pull/3306)) ### Added - `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) From 54f7b1cd916db6a8490544748318e007def52699 Mon Sep 17 00:00:00 2001 From: Atharv Agarwal <49218334+Atharv-Agarwal@users.noreply.github.com> Date: Mon, 20 Nov 2023 02:20:42 +0530 Subject: [PATCH 1169/1221] Add config to manage how long logs are stored (#3257) * Add config to change how long logs are stored * Optimize deleting logs * Update core/config_help.json Co-authored-by: khakers <22665282+khakers@users.noreply.github.com> Signed-off-by: Cordila <49218334+Cordila@users.noreply.github.com> * Testing changes * Update core/config_help.json (Fix copy paste) Co-authored-by: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Signed-off-by: Cordila <49218334+Cordila@users.noreply.github.com> --------- Signed-off-by: Cordila <49218334+Cordila@users.noreply.github.com> Co-authored-by: Cordila <49218334+Cordila@users.noreply.github.com> Co-authored-by: khakers <22665282+khakers@users.noreply.github.com> Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> Co-authored-by: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> --- bot.py | 18 ++++++++++++++---- cogs/plugins.py | 2 -- cogs/utility.py | 1 - core/config.py | 4 ++-- core/config_help.json | 11 +++++++++++ core/models.py | 1 - core/thread.py | 2 -- 7 files changed, 27 insertions(+), 12 deletions(-) diff --git a/bot.py b/bot.py index 3701d826ea..5e8854faa8 100644 --- a/bot.py +++ b/bot.py @@ -12,7 +12,7 @@ import sys import platform import typing -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta from subprocess import PIPE from types import SimpleNamespace @@ -616,6 +616,7 @@ async def on_ready(self): self.post_metadata.start() self.autoupdate.start() + self.log_expiry.start() self._started = True async def convert_emoji(self, name: str) -> str: @@ -640,7 +641,6 @@ async def get_or_fetch_user(self, id: int) -> discord.User: return self.get_user(id) or await self.fetch_user(id) async def retrieve_emoji(self) -> typing.Tuple[str, str]: - sent_emoji = self.config["sent_emoji"] blocked_emoji = self.config["blocked_emoji"] @@ -714,7 +714,6 @@ def check_manual_blocked_roles(self, author: discord.Member) -> bool: if isinstance(author, discord.Member): for r in author.roles: if str(r.id) in self.blocked_roles: - blocked_reason = self.blocked_roles.get(str(r.id)) or "" try: @@ -773,7 +772,6 @@ async def is_blocked( channel: discord.TextChannel = None, send_message: bool = False, ) -> bool: - member = self.guild.get_member(author.id) if member is None: # try to find in other guilds @@ -1707,6 +1705,18 @@ async def before_autoupdate(self): self.autoupdate.cancel() return + @tasks.loop(hours=1, reconnect=False) + async def log_expiry(self): + log_expire_after = self.config.get("log_expiration") + if log_expire_after == isodate.Duration(): + return self.log_expiry.stop() + + now = discord.utils.utcnow() + expiration_datetime = now - log_expire_after + expired_logs = await self.db.logs.delete_many({"closed_at": {"$lte": str(expiration_datetime)}}) + + logger.info(f"Deleted {expired_logs.deleted_count} expired logs.") + def format_channel_name(self, author, exclude_channel=None, force_null=False): """Sanitises a username for use with text channel names diff --git a/cogs/plugins.py b/cogs/plugins.py index 6bae7738c6..59a2c9d24d 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -276,7 +276,6 @@ async def unload_plugin(self, plugin: Plugin) -> None: del sys.modules[module] async def parse_user_input(self, ctx, plugin_name, check_version=False): - if not self.bot.config["enable_plugins"]: embed = discord.Embed( description="Plugins are disabled, enable them by setting `ENABLE_PLUGINS=true`", @@ -399,7 +398,6 @@ async def plugins_add(self, ctx, *, plugin_name: str): await self.bot.config.update() if self.bot.config.get("enable_plugins"): - invalidate_caches() try: diff --git a/cogs/utility.py b/cogs/utility.py index d0ecbeda22..749cf3e160 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -584,7 +584,6 @@ async def status(self, ctx, *, status_type: str.lower): return await ctx.send(embed=embed) async def set_presence(self, *, status=None, activity_type=None, activity_message=None): - if status is None: status = self.bot.config.get("status") diff --git a/core/config.py b/core/config.py index a9e16a0df1..5c6b0dd09d 100644 --- a/core/config.py +++ b/core/config.py @@ -21,7 +21,6 @@ class ConfigManager: - public_keys = { # activity "twitch_url": "https://www.twitch.tv/discordmodmail/", @@ -37,6 +36,7 @@ class ConfigManager: "account_age": isodate.Duration(), "guild_age": isodate.Duration(), "thread_cooldown": isodate.Duration(), + "log_expiration": isodate.Duration(), "reply_without_command": False, "anon_reply_without_command": False, "plain_reply_without_command": False, @@ -187,7 +187,7 @@ class ConfigManager: colors = {"mod_color", "recipient_color", "main_color", "error_color"} - time_deltas = {"account_age", "guild_age", "thread_auto_close", "thread_cooldown"} + time_deltas = {"account_age", "guild_age", "thread_auto_close", "thread_cooldown", "log_expiration"} booleans = { "use_user_id_channel_name", diff --git a/core/config_help.json b/core/config_help.json index 501c265827..d301763fe4 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -373,6 +373,17 @@ "To disable thread cooldown, do `{prefix}config del thread_cooldown`." ] }, + "log_expiration": { + "default": "Never", + "description": "The duration closed threads will be stored within the database before deletion. Logs that have been closed for longer than this duration will be deleted automatically.", + "examples": [ + "`{prefix}config set log_expiration P12DT3H` (stands for 12 days and 3 hours in [ISO-8601 Duration Format](https://en.wikipedia.org/wiki/ISO_8601#Durations))", + "`{prefix}config set log_expiration 3 days and 5 hours` (accepted readable time)" + ], + "notes": [ + "To disable log expiration, do `{prefix}config del log_expiration`." + ] + }, "thread_cancelled": { "default": "\"Cancelled\"", "description": "This is the message to display when a thread times out and creation is cancelled.", diff --git a/core/models.py b/core/models.py index 2a397541c1..611db375f0 100644 --- a/core/models.py +++ b/core/models.py @@ -394,7 +394,6 @@ async def convert(self, ctx, argument): try: return await super().convert(ctx, argument) except commands.ChannelNotFound: - if guild: categories = {c.name.casefold(): c for c in guild.categories} else: diff --git a/core/thread.py b/core/thread.py index 46be932c1a..09c5df46ee 100644 --- a/core/thread.py +++ b/core/thread.py @@ -721,7 +721,6 @@ async def delete_message( async def find_linked_message_from_dm( self, message, either_direction=False, get_thread_channel=False ) -> typing.List[discord.Message]: - joint_id = None if either_direction: joint_id = get_joint_id(message) @@ -914,7 +913,6 @@ async def send( persistent_note: bool = False, thread_creation: bool = False, ) -> None: - if not note and from_mod: self.bot.loop.create_task(self._restart_close_timer()) # Start or restart thread auto close From 2bc51e8f9eb34758e7c997d7eb6d23338867a83a Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 19 Nov 2023 12:54:53 -0800 Subject: [PATCH 1170/1221] Update changelog and added a note for comparing dates by string --- CHANGELOG.md | 3 ++- bot.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76a929b178..3e5d335e44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `REGISTRY_PLUGINS_ONLY`, environment variable, when set, restricts to only allow adding registry plugins. ([PR #3247](https://github.com/modmail-dev/modmail/pull/3247)) - `DISCORD_LOG_LEVEL` environment variable to set the log level of discord.py. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) - New registry plugin: [`autoreact`](https://github.com/martinbndr/kyb3r-modmail-plugins/tree/master/autoreact). -- `STREAM_LOG_FORMAT` and `FILE_LOG_FORMAT` for settings the log format of the stream and file handlers respectively. Possible options are `json` and `plain` (default). ([PR #3305](https://github.com/modmail-dev/Modmail/pull/3305)) +- `STREAM_LOG_FORMAT` and `FILE_LOG_FORMAT` environment variable to set the log format of the stream and file handlers respectively. Possible options are `json` and `plain` (default). ([PR #3305](https://github.com/modmail-dev/Modmail/pull/3305)) +- `LOG_EXPIRATION` environment variable to set the expiration time of logs. ([PR #3257](https://github.com/modmail-dev/Modmail/pull/3257)) ### Changed - Repo moved to https://github.com/modmail-dev/modmail. diff --git a/bot.py b/bot.py index 5e8854faa8..f36ecc8351 100644 --- a/bot.py +++ b/bot.py @@ -1713,6 +1713,8 @@ async def log_expiry(self): now = discord.utils.utcnow() expiration_datetime = now - log_expire_after + # WARNING: comparison is done lexicographically, not by date. + # This is fine as long as the date is in zero-padded ISO format, which it should be. expired_logs = await self.db.logs.delete_many({"closed_at": {"$lte": str(expiration_datetime)}}) logger.info(f"Deleted {expired_logs.deleted_count} expired logs.") From c6f87cdb0cffec3d88a0c75f003f95f30bd7980d Mon Sep 17 00:00:00 2001 From: Nicklaus <100449899+Nicklaus-s@users.noreply.github.com> Date: Sun, 19 Nov 2023 14:58:52 -0600 Subject: [PATCH 1171/1221] Add `rename` to `registry.json` (#3276) * Add `rename` to `registry.json` Signed-off-by: Nicklaus <100449899+Nicklaus-s@users.noreply.github.com> * Change icon Signed-off-by: Nicklaus <100449899+Nicklaus-s@users.noreply.github.com> --------- Signed-off-by: Nicklaus <100449899+Nicklaus-s@users.noreply.github.com> --- plugins/registry.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/registry.json b/plugins/registry.json index be138a30b2..4079001a50 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -124,5 +124,14 @@ "title": "Top Supporters", "icon_url": "https://i.imgur.com/Mo60CdK.png", "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" + }, + "rename": { + "repository": "Nicklaus-s/modmail-plugins", + "branch": "master", + "description": "Set a thread channel name.", + "bot_version": "4.0.0", + "title": "Rename", + "icon_url": "https://i.imgur.com/A1auJ95.png", + "thumbnail_url": "https://i.imgur.com/A1auJ95.png" } } From 5b2770caf2c8277f1de50d70db1407e1b5fdbb8b Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 19 Nov 2023 13:00:23 -0800 Subject: [PATCH 1172/1221] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5d335e44..1b7910bfff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,9 +22,9 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) - `REGISTRY_PLUGINS_ONLY`, environment variable, when set, restricts to only allow adding registry plugins. ([PR #3247](https://github.com/modmail-dev/modmail/pull/3247)) - `DISCORD_LOG_LEVEL` environment variable to set the log level of discord.py. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) -- New registry plugin: [`autoreact`](https://github.com/martinbndr/kyb3r-modmail-plugins/tree/master/autoreact). - `STREAM_LOG_FORMAT` and `FILE_LOG_FORMAT` environment variable to set the log format of the stream and file handlers respectively. Possible options are `json` and `plain` (default). ([PR #3305](https://github.com/modmail-dev/Modmail/pull/3305)) - `LOG_EXPIRATION` environment variable to set the expiration time of logs. ([PR #3257](https://github.com/modmail-dev/Modmail/pull/3257)) +- New registry plugins: [`autoreact`](https://github.com/martinbndr/kyb3r-modmail-plugins/tree/master/autoreact) and [`rename`](https://github.com/Nicklaus-s/modmail-plugins/tree/main/rename). ### Changed - Repo moved to https://github.com/modmail-dev/modmail. From a94e7a984172801c03e9a316e93ea1a5b47df9fd Mon Sep 17 00:00:00 2001 From: Jerrie <70805800+Jerrie-Aries@users.noreply.github.com> Date: Mon, 20 Nov 2023 05:26:12 +0800 Subject: [PATCH 1173/1221] Fix bug with `?plugin update`. (#3295) --- cogs/plugins.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index 59a2c9d24d..d5787956f1 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -500,12 +500,12 @@ async def update_plugin(self, ctx, plugin_name): description=f"Failed to update {plugin.name}. This plugin will now be removed from your bot.", color=self.bot.error_color, ) - self.bot.config["plugins"].remove(plugin_name) - logger.debug("Failed to update %s. Removed plugin from config.", plugin_name) + self.bot.config["plugins"].remove(str(plugin)) + logger.debug("Failed to update %s. Removed plugin from config.", plugin) else: - logger.debug("Updated %s.", plugin_name) + logger.debug("Updated %s.", plugin) else: - logger.debug("Updated %s.", plugin_name) + logger.debug("Updated %s.", plugin) return await ctx.send(embed=embed) @plugins.command(name="update") From 8bd30dc90760b5e5a6581abae0a72126b092da44 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 19 Nov 2023 13:27:48 -0800 Subject: [PATCH 1174/1221] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b7910bfff..f79f46c491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Removed an extra space in the help command title when the command has no parameters. ([PR #3271](https://github.com/modmail-dev/Modmail/pull/3271)) - Corrected some incorrect config help descriptions. ([PR #3277](https://github.com/modmail-dev/Modmail/pull/3277)) - Rate limit issue when fetch the messages due to reaction linking. ([PR #3306](https://github.com/modmail-dev/Modmail/pull/3306)) +- Update command fails when the plugin is invalid. ([PR #3295](https://github.com/modmail-dev/Modmail/pull/3295)) ### Added - `?log key <key>` to retrieve the log link and view a preview using a log key. ([PR #3196](https://github.com/modmail-dev/Modmail/pull/3196)) From 53d40e0585919eadba97e7d0abcb822d1ebcbb34 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Sun, 19 Nov 2023 22:41:29 +0100 Subject: [PATCH 1175/1221] fix: discord invite ( new server ) (#3307) * fix: discord invite ( new server ) Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update link Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> --------- Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 749cf3e160..579c4a7e4e 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -352,7 +352,7 @@ async def about(self, ctx): embed.add_field( name="Want Modmail in Your Server?", value="Follow the installation guide on [GitHub](https://github.com/modmail-dev/modmail/) " - "and join our [Discord server](https://discord.gg/F34cRU8)!", + "and join our [Discord server](https://discord.gg/cnUpwrnpYb)!", inline=False, ) From ec95eb7f6b6abfb1a89c48eb8e0915313a699116 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:16:04 -0800 Subject: [PATCH 1176/1221] Update deps: aiohttp, colorama, emoji --- Pipfile | 6 +- Pipfile.lock | 1435 ++++++++++++++++++++++++++------------------------ bot.py | 6 +- 3 files changed, 752 insertions(+), 695 deletions(-) diff --git a/Pipfile b/Pipfile index e61e3b9288..f2590a1e2b 100644 --- a/Pipfile +++ b/Pipfile @@ -10,10 +10,10 @@ pylint = "~=2.9.3" typing-extensions = "==4.2.0" [packages] -aiohttp = "==3.8.1" -colorama = "~=0.4.5" +aiohttp = "==3.9.0" +colorama = "==0.4.6" "discord.py" = "==2.0.1" -emoji = "==1.7.0" +emoji = "==2.8.0" isodate = "~=0.6.0" motor = "==2.5.1" natural = "~=0.2.0" diff --git a/Pipfile.lock b/Pipfile.lock index e1c531856e..35808d048b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9d1171901e036b1f78a2e89855d564e9f559736ca076128fa27eb14ee8f917a4" + "sha256": "f28fd3a11b8f1550ba6731a0bc17740966b1fd98d10c1b004d416ca5b1dd3b84" }, "pipfile-spec": 6, "requires": {}, @@ -16,196 +16,190 @@ "default": { "aiohttp": { "hashes": [ - "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3", - "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782", - "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75", - "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf", - "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7", - "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675", - "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1", - "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785", - "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4", - "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf", - "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5", - "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15", - "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca", - "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8", - "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac", - "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8", - "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef", - "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516", - "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700", - "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2", - "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8", - "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0", - "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676", - "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad", - "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155", - "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db", - "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd", - "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091", - "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602", - "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411", - "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93", - "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd", - "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec", - "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51", - "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7", - "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17", - "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d", - "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00", - "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923", - "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440", - "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32", - "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e", - "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1", - "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724", - "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a", - "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8", - "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2", - "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33", - "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b", - "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2", - "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632", - "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b", - "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2", - "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316", - "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74", - "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96", - "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866", - "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44", - "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950", - "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa", - "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c", - "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a", - "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd", - "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd", - "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9", - "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421", - "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2", - "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922", - "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4", - "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237", - "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642", - "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578" + "sha256:05857848da443c8c12110d99285d499b4e84d59918a21132e45c3f0804876994", + "sha256:05a183f1978802588711aed0dea31e697d760ce9055292db9dc1604daa9a8ded", + "sha256:09f23292d29135025e19e8ff4f0a68df078fe4ee013bca0105b2e803989de92d", + "sha256:11ca808f9a6b63485059f5f6e164ef7ec826483c1212a44f268b3653c91237d8", + "sha256:1736d87dad8ef46a8ec9cddd349fa9f7bd3a064c47dd6469c0d6763d3d49a4fc", + "sha256:1df43596b826022b14998f0460926ce261544fedefe0d2f653e1b20f49e96454", + "sha256:23170247ef89ffa842a02bbfdc425028574d9e010611659abeb24d890bc53bb8", + "sha256:2779f5e7c70f7b421915fd47db332c81de365678180a9f3ab404088f87ba5ff9", + "sha256:28185e36a78d247c55e9fbea2332d16aefa14c5276a582ce7a896231c6b1c208", + "sha256:2cbc14a13fb6b42d344e4f27746a4b03a2cb0c1c3c5b932b0d6ad8881aa390e3", + "sha256:2d71abc15ff7047412ef26bf812dfc8d0d1020d664617f4913df2df469f26b76", + "sha256:2d820162c8c2bdbe97d328cd4f417c955ca370027dce593345e437b2e9ffdc4d", + "sha256:317719d7f824eba55857fe0729363af58e27c066c731bc62cd97bc9c3d9c7ea4", + "sha256:35a68cd63ca6aaef5707888f17a70c36efe62b099a4e853d33dc2e9872125be8", + "sha256:3607375053df58ed6f23903aa10cf3112b1240e8c799d243bbad0f7be0666986", + "sha256:366bc870d7ac61726f32a489fbe3d1d8876e87506870be66b01aeb84389e967e", + "sha256:3abf0551874fecf95f93b58f25ef4fc9a250669a2257753f38f8f592db85ddea", + "sha256:3d7f6235c7475658acfc1769d968e07ab585c79f6ca438ddfecaa9a08006aee2", + "sha256:3dd8119752dd30dd7bca7d4bc2a92a59be6a003e4e5c2cf7e248b89751b8f4b7", + "sha256:42fe4fd9f0dfcc7be4248c162d8056f1d51a04c60e53366b0098d1267c4c9da8", + "sha256:45820ddbb276113ead8d4907a7802adb77548087ff5465d5c554f9aa3928ae7d", + "sha256:4790e44f46a4aa07b64504089def5744d3b6780468c4ec3a1a36eb7f2cae9814", + "sha256:4afa8f71dba3a5a2e1e1282a51cba7341ae76585345c43d8f0e624882b622218", + "sha256:4b777c9286b6c6a94f50ddb3a6e730deec327e9e2256cb08b5530db0f7d40fd8", + "sha256:4ee1b4152bc3190cc40ddd6a14715e3004944263ea208229ab4c297712aa3075", + "sha256:51a4cd44788ea0b5e6bb8fa704597af3a30be75503a7ed1098bc5b8ffdf6c982", + "sha256:536b01513d67d10baf6f71c72decdf492fb7433c5f2f133e9a9087379d4b6f31", + "sha256:571760ad7736b34d05597a1fd38cbc7d47f7b65deb722cb8e86fd827404d1f6b", + "sha256:5a2eb5311a37fe105aa35f62f75a078537e1a9e4e1d78c86ec9893a3c97d7a30", + "sha256:5ab16c254e2312efeb799bc3c06897f65a133b38b69682bf75d1f1ee1a9c43a9", + "sha256:65b0a70a25456d329a5e1426702dde67be0fb7a4ead718005ba2ca582d023a94", + "sha256:673343fbc0c1ac44d0d2640addc56e97a052504beacd7ade0dc5e76d3a4c16e8", + "sha256:6777a390e41e78e7c45dab43a4a0196c55c3b8c30eebe017b152939372a83253", + "sha256:6896b8416be9ada4d22cd359d7cb98955576ce863eadad5596b7cdfbf3e17c6c", + "sha256:694df243f394629bcae2d8ed94c589a181e8ba8604159e6e45e7b22e58291113", + "sha256:70e851f596c00f40a2f00a46126c95c2e04e146015af05a9da3e4867cfc55911", + "sha256:7276fe0017664414fdc3618fca411630405f1aaf0cc3be69def650eb50441787", + "sha256:76a86a9989ebf82ee61e06e2bab408aec4ea367dc6da35145c3352b60a112d11", + "sha256:7a94bde005a8f926d0fa38b88092a03dea4b4875a61fbcd9ac6f4351df1b57cd", + "sha256:7ae5f99a32c53731c93ac3075abd3e1e5cfbe72fc3eaac4c27c9dd64ba3b19fe", + "sha256:7e8a3b79b6d186a9c99761fd4a5e8dd575a48d96021f220ac5b5fa856e5dd029", + "sha256:816f4db40555026e4cdda604a1088577c1fb957d02f3f1292e0221353403f192", + "sha256:8303531e2c17b1a494ffaeba48f2da655fe932c4e9a2626c8718403c83e5dd2b", + "sha256:8488519aa05e636c5997719fe543c8daf19f538f4fa044f3ce94bee608817cff", + "sha256:87c8b0a6487e8109427ccf638580865b54e2e3db4a6e0e11c02639231b41fc0f", + "sha256:8c9e5f4d7208cda1a2bb600e29069eecf857e6980d0ccc922ccf9d1372c16f4b", + "sha256:94697c7293199c2a2551e3e3e18438b4cba293e79c6bc2319f5fd652fccb7456", + "sha256:9623cfd9e85b76b83ef88519d98326d4731f8d71869867e47a0b979ffec61c73", + "sha256:98d21092bf2637c5fa724a428a69e8f5955f2182bff61f8036827cf6ce1157bf", + "sha256:99ae01fb13a618b9942376df77a1f50c20a281390dad3c56a6ec2942e266220d", + "sha256:9c196b30f1b1aa3363a69dd69079ae9bec96c2965c4707eaa6914ba099fb7d4f", + "sha256:a00ce44c21612d185c5275c5cba4bab8d7c1590f248638b667ed8a782fa8cd6f", + "sha256:a1b66dbb8a7d5f50e9e2ea3804b01e766308331d0cac76eb30c563ac89c95985", + "sha256:a1d7edf74a36de0e5ca50787e83a77cf352f5504eb0ffa3f07000a911ba353fb", + "sha256:a1e3b3c107ccb0e537f309f719994a55621acd2c8fdf6d5ce5152aed788fb940", + "sha256:a486ddf57ab98b6d19ad36458b9f09e6022de0381674fe00228ca7b741aacb2f", + "sha256:ac9669990e2016d644ba8ae4758688534aabde8dbbc81f9af129c3f5f01ca9cd", + "sha256:b1a2ea8252cacc7fd51df5a56d7a2bb1986ed39be9397b51a08015727dfb69bd", + "sha256:c5b7bf8fe4d39886adc34311a233a2e01bc10eb4e842220235ed1de57541a896", + "sha256:c67a51ea415192c2e53e4e048c78bab82d21955b4281d297f517707dc836bf3d", + "sha256:ca4fddf84ac7d8a7d0866664936f93318ff01ee33e32381a115b19fb5a4d1202", + "sha256:d5b9345ab92ebe6003ae11d8092ce822a0242146e6fa270889b9ba965457ca40", + "sha256:d97c3e286d0ac9af6223bc132dc4bad6540b37c8d6c0a15fe1e70fb34f9ec411", + "sha256:db04d1de548f7a62d1dd7e7cdf7c22893ee168e22701895067a28a8ed51b3735", + "sha256:dcf71c55ec853826cd70eadb2b6ac62ec577416442ca1e0a97ad875a1b3a0305", + "sha256:de3cc86f4ea8b4c34a6e43a7306c40c1275e52bfa9748d869c6b7d54aa6dad80", + "sha256:deac0a32aec29608eb25d730f4bc5a261a65b6c48ded1ed861d2a1852577c932", + "sha256:e18d92c3e9e22553a73e33784fcb0ed484c9874e9a3e96c16a8d6a1e74a0217b", + "sha256:eb6dfd52063186ac97b4caa25764cdbcdb4b10d97f5c5f66b0fa95052e744eb7", + "sha256:f09960b5bb1017d16c0f9e9f7fc42160a5a49fa1e87a175fd4a2b1a1833ea0af", + "sha256:f1e4f254e9c35d8965d377e065c4a8a55d396fe87c8e7e8429bcfdeeb229bfb3", + "sha256:f32c86dc967ab8c719fd229ce71917caad13cc1e8356ee997bf02c5b368799bf", + "sha256:f50b4663c3e0262c3a361faf440761fbef60ccdde5fe8545689a4b3a3c149fb4", + "sha256:f8e05f5163528962ce1d1806fce763ab893b1c5b7ace0a3538cd81a90622f844", + "sha256:f929f4c9b9a00f3e6cc0587abb95ab9c05681f8b14e0fe1daecfa83ea90f8318", + "sha256:f9e09a1c83521d770d170b3801eea19b89f41ccaa61d53026ed111cb6f088887" ], "index": "pypi", - "version": "==3.8.1" + "version": "==3.9.0" }, "aiosignal": { "hashes": [ - "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a", - "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2" + "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", + "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" ], - "markers": "python_version >= '3.6'", - "version": "==1.2.0" + "markers": "python_version >= '3.7'", + "version": "==1.3.1" }, "async-timeout": { "hashes": [ - "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", - "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" + "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", + "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" ], - "markers": "python_version >= '3.6'", - "version": "==4.0.2" + "markers": "python_version < '3.11'", + "version": "==4.0.3" }, "attrs": { "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" + "markers": "python_version >= '3.7'", + "version": "==23.1.0" }, "cairocffi": { "hashes": [ - "sha256:108a3a7cb09e203bdd8501d9baad91d786d204561bd71e9364e8b34897c47b91" + "sha256:78e6bbe47357640c453d0be929fa49cd05cce2e1286f3d2a1ca9cbda7efdb8b7", + "sha256:aa78ee52b9069d7475eeac457389b6275aa92111895d78fbaa2202a52dac112e" ], "markers": "python_version >= '3.7'", - "version": "==1.3.0" + "version": "==1.6.1" }, "cairosvg": { "hashes": [ - "sha256:98c276b7e4f0caf01e5c7176765c104ffa1aa1461d63b2053b04ab663cf7052b", - "sha256:b0b9929cf5dba005178d746a8036fcf0025550f498ca54db61873322384783bc" + "sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0", + "sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b" ], - "version": "==2.5.2" + "version": "==2.7.1" }, "certifi": { "hashes": [ - "sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5", - "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516" + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" ], "markers": "python_version >= '3.6'", - "version": "==2022.9.14" + "version": "==2023.11.17" }, "cffi": { "hashes": [ - "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", - "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", - "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", - "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", - "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", - "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", - "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", - "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", - "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", - "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", - "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", - "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", - "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", - "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", - "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", - "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", - "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", - "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", - "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", - "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", - "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", - "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", - "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", - "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", - "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", - "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", - "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", - "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", - "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", - "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", - "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", - "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", - "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", - "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", - "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", - "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", - "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", - "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", - "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", - "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", - "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", - "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", - "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", - "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", - "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", - "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", - "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", - "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", - "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", - "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", - "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", - "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", - "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", - "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", - "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", - "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", - "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", - "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", - "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", - "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", - "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", - "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", - "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", - "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" ], - "version": "==1.15.1" + "markers": "python_version >= '3.8'", + "version": "==1.16.0" }, "charset-normalizer": { "hashes": [ @@ -217,11 +211,11 @@ }, "colorama": { "hashes": [ - "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", - "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], "index": "pypi", - "version": "==0.4.5" + "version": "==0.4.6" }, "cssselect2": { "hashes": [ @@ -249,83 +243,86 @@ }, "dnspython": { "hashes": [ - "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", - "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" + "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8", + "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.2.1" + "version": "==2.4.2" }, "emoji": { "hashes": [ - "sha256:65c54533ea3c78f30d0729288998715f418d7467de89ec258a31c0ce8660a1d1" + "sha256:8d8b5dec3c507444b58890e598fc895fcec022b3f5acb49497c6ccc5208b8b00", + "sha256:a8468fd836b7ecb6d1eac054c9a591701ce0ccd6c6f7779ad71b66f76664df90" ], "index": "pypi", - "version": "==1.7.0" + "version": "==2.8.0" }, "frozenlist": { "hashes": [ - "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e", - "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04", - "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944", - "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845", - "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f", - "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f", - "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb", - "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2", - "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3", - "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f", - "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a", - "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131", - "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1", - "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa", - "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8", - "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b", - "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2", - "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e", - "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b", - "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b", - "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10", - "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170", - "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8", - "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b", - "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989", - "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd", - "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03", - "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519", - "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189", - "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b", - "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca", - "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff", - "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96", - "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d", - "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5", - "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2", - "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204", - "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a", - "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8", - "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141", - "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792", - "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9", - "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c", - "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c", - "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221", - "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d", - "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc", - "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f", - "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9", - "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef", - "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3", - "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab", - "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b", - "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db", - "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988", - "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6", - "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc", - "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83", - "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be" + "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6", + "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01", + "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251", + "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9", + "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b", + "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87", + "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf", + "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f", + "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0", + "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2", + "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b", + "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc", + "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c", + "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467", + "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9", + "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1", + "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a", + "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79", + "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167", + "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300", + "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf", + "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea", + "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2", + "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab", + "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3", + "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb", + "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087", + "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc", + "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8", + "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62", + "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f", + "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326", + "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c", + "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431", + "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963", + "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7", + "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef", + "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3", + "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956", + "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781", + "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472", + "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc", + "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839", + "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672", + "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3", + "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503", + "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d", + "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8", + "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b", + "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc", + "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f", + "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559", + "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b", + "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95", + "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb", + "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963", + "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919", + "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f", + "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3", + "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1", + "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e" ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" + "markers": "python_version >= '3.8'", + "version": "==1.4.0" }, "idna": { "hashes": [ @@ -363,68 +360,83 @@ }, "multidict": { "hashes": [ - "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60", - "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c", - "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672", - "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51", - "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032", - "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2", - "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b", - "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80", - "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88", - "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a", - "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d", - "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389", - "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c", - "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9", - "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c", - "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516", - "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b", - "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43", - "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee", - "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227", - "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d", - "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae", - "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7", - "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4", - "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9", - "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f", - "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013", - "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9", - "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e", - "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693", - "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a", - "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15", - "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb", - "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96", - "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87", - "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376", - "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658", - "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0", - "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071", - "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360", - "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc", - "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3", - "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba", - "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8", - "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9", - "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2", - "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3", - "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68", - "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8", - "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d", - "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49", - "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608", - "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57", - "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86", - "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20", - "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293", - "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849", - "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937", - "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d" + "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9", + "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8", + "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03", + "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710", + "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161", + "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664", + "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569", + "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067", + "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313", + "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706", + "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2", + "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636", + "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49", + "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93", + "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603", + "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0", + "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60", + "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4", + "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e", + "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1", + "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60", + "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951", + "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc", + "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe", + "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95", + "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d", + "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8", + "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed", + "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2", + "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775", + "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87", + "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c", + "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2", + "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98", + "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3", + "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe", + "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78", + "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660", + "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176", + "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e", + "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988", + "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c", + "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c", + "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0", + "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449", + "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f", + "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde", + "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5", + "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d", + "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac", + "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a", + "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9", + "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca", + "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11", + "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35", + "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063", + "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b", + "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982", + "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258", + "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1", + "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52", + "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480", + "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7", + "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461", + "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d", + "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc", + "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779", + "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a", + "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547", + "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0", + "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171", + "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf", + "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d", + "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba" ], "markers": "python_version >= '3.7'", - "version": "==6.0.2" + "version": "==6.0.4" }, "natural": { "hashes": [ @@ -443,69 +455,63 @@ }, "pillow": { "hashes": [ - "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927", - "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14", - "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc", - "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58", - "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60", - "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76", - "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c", - "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac", - "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490", - "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1", - "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f", - "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d", - "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f", - "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069", - "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402", - "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437", - "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885", - "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e", - "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be", - "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8", - "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff", - "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da", - "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004", - "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f", - "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20", - "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d", - "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c", - "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544", - "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9", - "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3", - "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04", - "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c", - "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5", - "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4", - "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb", - "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4", - "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c", - "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467", - "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e", - "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421", - "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b", - "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8", - "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb", - "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3", - "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc", - "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf", - "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1", - "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a", - "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28", - "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0", - "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1", - "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8", - "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd", - "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4", - "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8", - "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f", - "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013", - "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59", - "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc", - "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4" + "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d", + "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de", + "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616", + "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839", + "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099", + "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a", + "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219", + "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106", + "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b", + "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412", + "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b", + "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7", + "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2", + "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7", + "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14", + "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f", + "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27", + "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57", + "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262", + "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28", + "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610", + "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172", + "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273", + "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e", + "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d", + "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818", + "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f", + "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9", + "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01", + "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7", + "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651", + "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312", + "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80", + "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666", + "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061", + "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b", + "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992", + "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593", + "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4", + "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db", + "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba", + "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd", + "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e", + "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212", + "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb", + "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2", + "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34", + "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256", + "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f", + "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2", + "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38", + "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996", + "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a", + "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793" ], - "markers": "python_version >= '3.7'", - "version": "==9.2.0" + "markers": "python_version >= '3.8'", + "version": "==10.1.0" }, "pycparser": { "hashes": [ @@ -515,121 +521,121 @@ "version": "==2.21" }, "pymongo": { - "extras": [ - "srv" - ], + "extras": [], "hashes": [ - "sha256:06b64cdf5121f86b78a84e61b8f899b6988732a8d304b503ea1f94a676221c06", - "sha256:07398d8a03545b98282f459f2603a6bb271f4448d484ed7f411121a519a7ea48", - "sha256:0a02313e71b7c370c43056f6b16c45effbb2d29a44d24403a3d5ba6ed322fa3f", - "sha256:0a89cadc0062a5e53664dde043f6c097172b8c1c5f0094490095282ff9995a5f", - "sha256:0be605bfb8461384a4cb81e80f51eb5ca1b89851f2d0e69a75458c788a7263a4", - "sha256:0d52a70350ec3dfc39b513df12b03b7f4c8f8ec6873bbf958299999db7b05eb1", - "sha256:0e7a5d0b9077e8c3e57727f797ee8adf12e1d5e7534642230d98980d160d1320", - "sha256:145d78c345a38011497e55aff22c0f8edd40ee676a6810f7e69563d68a125e83", - "sha256:14dee106a10b77224bba5efeeb6aee025aabe88eb87a2b850c46d3ee55bdab4a", - "sha256:176fdca18391e1206c32fb1d8265628a84d28333c20ad19468d91e3e98312cd1", - "sha256:1b4c535f524c9d8c86c3afd71d199025daa070859a2bdaf94a298120b0de16db", - "sha256:1b5cb75d2642ff7db823f509641f143f752c0d1ab03166cafea1e42e50469834", - "sha256:1c6c71e198b36f0f0dfe354f06d3655ecfa30d69493a1da125a9a54668aad652", - "sha256:1c771f1a8b3cd2d697baaf57e9cfa4ae42371cacfbea42ea01d9577c06d92f96", - "sha256:208a61db8b8b647fb5b1ff3b52b4ed6dbced01eac3b61009958adb203596ee99", - "sha256:2157d68f85c28688e8b723bbe70c8013e0aba5570e08c48b3562f74d33fc05c4", - "sha256:2301051701b27aff2cbdf83fae22b7ca883c9563dfd088033267291b46196643", - "sha256:2567885ff0c8c7c0887ba6cefe4ae4af96364a66a7069f924ce0cd12eb971d04", - "sha256:2577b8161eeae4dd376d13100b2137d883c10bb457dd08935f60c9f9d4b5c5f6", - "sha256:27e5ea64332385385b75414888ce9d1a9806be8616d7cef4ef409f4f256c6d06", - "sha256:28bfd5244d32faf3e49b5a8d1fab0631e922c26e8add089312e4be19fb05af50", - "sha256:295a5beaecb7bf054c1c6a28749ed72b19f4d4b61edcd8a0815d892424baf780", - "sha256:2c46a0afef69d61938a6fe32c3afd75b91dec3ab3056085dc72abbeedcc94166", - "sha256:3100a2352bdded6232b385ceda0c0a4624598c517d52c2d8cf014b7abbebd84d", - "sha256:320a1fe403dd83a35709fcf01083d14bc1462e9789b711201349a9158db3a87e", - "sha256:320f8734553c50cffe8a8e1ae36dfc7d7be1941c047489db20a814d2a170d7b5", - "sha256:33ab8c031f788609924e329003088831045f683931932a52a361d4a955b7dce2", - "sha256:3492ae1f97209c66af70e863e6420e6301cecb0a51a5efa701058aa73a8ca29e", - "sha256:351a2efe1c9566c348ad0076f4bf541f4905a0ebe2d271f112f60852575f3c16", - "sha256:3f0ac6e0203bd88863649e6ed9c7cfe53afab304bc8225f2597c4c0a74e4d1f0", - "sha256:3fedad05147b40ff8a93fcd016c421e6c159f149a2a481cfa0b94bfa3e473bab", - "sha256:4294f2c1cd069b793e31c2e6d7ac44b121cf7cedccd03ebcc30f3fc3417b314a", - "sha256:463b974b7f49d65a16ca1435bc1c25a681bb7d630509dd23b2e819ed36da0b7f", - "sha256:4e0a3ea7fd01cf0a36509f320226bd8491e0f448f00b8cb89f601c109f6874e1", - "sha256:514e78d20d8382d5b97f32b20c83d1d0452c302c9a135f0a9022236eb9940fda", - "sha256:517b09b1dd842390a965a896d1327c55dfe78199c9f5840595d40facbcd81854", - "sha256:51d1d061df3995c2332ae78f036492cc188cb3da8ef122caeab3631a67bb477e", - "sha256:5296669bff390135528001b4e48d33a7acaffcd361d98659628ece7f282f11aa", - "sha256:5296e5e69243ffd76bd919854c4da6630ae52e46175c804bc4c0e050d937b705", - "sha256:58db209da08a502ce6948841d522dcec80921d714024354153d00b054571993c", - "sha256:5b779e87300635b8075e8d5cfd4fdf7f46078cd7610c381d956bca5556bb8f97", - "sha256:5cf113a46d81cff0559d57aa66ffa473d57d1a9496f97426318b6b5b14fdec1c", - "sha256:5d20072d81cbfdd8e15e6a0c91fc7e3a4948c71e0adebfc67d3b4bcbe8602711", - "sha256:5d67dbc8da2dac1644d71c1839d12d12aa333e266a9964d5b1a49feed036bc94", - "sha256:5f530f35e1a57d4360eddcbed6945aecdaee2a491cd3f17025e7b5f2eea88ee7", - "sha256:5fdffb0cfeb4dc8646a5381d32ec981ae8472f29c695bf09e8f7a8edb2db12ca", - "sha256:602284e652bb56ca8760f8e88a5280636c5b63d7946fca1c2fe0f83c37dffc64", - "sha256:648fcfd8e019b122b7be0e26830a3a2224d57c3e934f19c1e53a77b8380e6675", - "sha256:64b9122be1c404ce4eb367ad609b590394587a676d84bfed8e03c3ce76d70560", - "sha256:6526933760ee1e6090db808f1690a111ec409699c1990efc96f134d26925c37f", - "sha256:6632b1c63d58cddc72f43ab9f17267354ddce563dd5e11eadabd222dcc808808", - "sha256:6f93dbfa5a461107bc3f5026e0d5180499e13379e9404f07a9f79eb5e9e1303d", - "sha256:71c0db2c313ea8a80825fb61b7826b8015874aec29ee6364ade5cb774fe4511b", - "sha256:71c5c200fd37a5322706080b09c3ec8907cf01c377a7187f354fc9e9e13abc73", - "sha256:7738147cd9dbd6d18d5593b3491b4620e13b61de975fd737283e4ad6c255c273", - "sha256:7a6e4dccae8ef5dd76052647d78f02d5d0ffaff1856277d951666c54aeba3ad2", - "sha256:7b4a9fcd95e978cd3c96cdc2096aa54705266551422cf0883c12a4044def31c6", - "sha256:80710d7591d579442c67a3bc7ae9dcba9ff95ea8414ac98001198d894fc4ff46", - "sha256:81a3ebc33b1367f301d1c8eda57eec4868e951504986d5d3fe437479dcdac5b2", - "sha256:8455176fd1b86de97d859fed4ae0ef867bf998581f584c7a1a591246dfec330f", - "sha256:845b178bd127bb074835d2eac635b980c58ec5e700ebadc8355062df708d5a71", - "sha256:858af7c2ab98f21ed06b642578b769ecfcabe4754648b033168a91536f7beef9", - "sha256:87e18f29bac4a6be76a30e74de9c9005475e27100acf0830679420ce1fd9a6fd", - "sha256:89d7baa847383b9814de640c6f1a8553d125ec65e2761ad146ea2e75a7ad197c", - "sha256:8c7ad5cab282f53b9d78d51504330d1c88c83fbe187e472c07e6908a0293142e", - "sha256:8d92c6bb9174d47c2257528f64645a00bbc6324a9ff45a626192797aff01dc14", - "sha256:9252c991e8176b5a2fa574c5ab9a841679e315f6e576eb7cf0bd958f3e39b0ad", - "sha256:93111fd4e08fa889c126aa8baf5c009a941880a539c87672e04583286517450a", - "sha256:95d15cf81cd2fb926f2a6151a9f94c7aacc102b415e72bc0e040e29332b6731c", - "sha256:9d5b66d457d2c5739c184a777455c8fde7ab3600a56d8bbebecf64f7c55169e1", - "sha256:a055d29f1302892a9389a382bed10a3f77708bcf3e49bfb76f7712fa5f391cc6", - "sha256:a1ba93be779a9b8e5e44f5c133dc1db4313661cead8a2fd27661e6cb8d942ee9", - "sha256:a283425e6a474facd73072d8968812d1d9058490a5781e022ccf8895500b83ce", - "sha256:a351986d6c9006308f163c359ced40f80b6cffb42069f3e569b979829951038d", - "sha256:a766157b195a897c64945d4ff87b050bb0e763bb78f3964e996378621c703b00", - "sha256:a8a3540e21213cb8ce232e68a7d0ee49cdd35194856c50b8bd87eeb572fadd42", - "sha256:a8e0a086dbbee406cc6f603931dfe54d1cb2fba585758e06a2de01037784b737", - "sha256:ab23b0545ec71ea346bf50a5d376d674f56205b729980eaa62cdb7871805014b", - "sha256:b0db9a4691074c347f5d7ee830ab3529bc5ad860939de21c1f9c403daf1eda9a", - "sha256:b1b5be40ebf52c3c67ee547e2c4435ed5bc6352f38d23e394520b686641a6be4", - "sha256:b3e08aef4ea05afbc0a70cd23c13684e7f5e074f02450964ec5cfa1c759d33d2", - "sha256:b7df0d99e189b7027d417d4bfd9b8c53c9c7ed5a0a1495d26a6f547d820eca88", - "sha256:be1f10145f7ea76e3e836fdc5c8429c605675bdcddb0bca9725ee6e26874c00c", - "sha256:bf254a1a95e95fdf4eaa25faa1ea450a6533ed7a997f9f8e49ab971b61ea514d", - "sha256:bfc2d763d05ec7211313a06e8571236017d3e61d5fef97fcf34ec4b36c0b6556", - "sha256:c164eda0be9048f83c24b9b2656900041e069ddf72de81c17d874d0c32f6079f", - "sha256:c22591cff80188dd8543be0b559d0c807f7288bd353dc0bcfe539b4588b3a5cd", - "sha256:c5f83bb59d0ff60c6fdb1f8a7b0288fbc4640b1f0fd56f5ae2387749c35d34e3", - "sha256:c7e8221278e5f9e2b6d3893cfc3a3e46c017161a57bb0e6f244826e4cee97916", - "sha256:c8d6bf6fcd42cde2f02efb8126812a010c297eacefcd090a609639d2aeda6185", - "sha256:c8f7dd025cb0bf19e2f60a64dfc24b513c8330e0cfe4a34ccf941eafd6194d9e", - "sha256:c9d212e2af72d5c8d082775a43eb726520e95bf1c84826440f74225843975136", - "sha256:cebb3d8bcac4a6b48be65ebbc5c9881ed4a738e27bb96c86d9d7580a1fb09e05", - "sha256:d3082e5c4d7b388792124f5e805b469109e58f1ab1eb1fbd8b998e8ab766ffb7", - "sha256:d81047341ab56061aa4b6823c54d4632579c3b16e675089e8f520e9b918a133b", - "sha256:d81299f63dc33cc172c26faf59cc54dd795fc6dd5821a7676cca112a5ee8bbd6", - "sha256:dfa217bf8cf3ff6b30c8e6a89014e0c0e7b50941af787b970060ae5ba04a4ce5", - "sha256:dfec57f15f53d677b8e4535695ff3f37df7f8fe431f2efa8c3c8c4025b53d1eb", - "sha256:e099b79ccf7c40f18b149a64d3d10639980035f9ceb223169dd806ff1bb0d9cc", - "sha256:e1fc4d3985868860b6585376e511bb32403c5ffb58b0ed913496c27fd791deea", - "sha256:e2b4c95c47fb81b19ea77dc1c50d23af3eba87c9628fcc2e03d44124a3d336ea", - "sha256:e4e5d163e6644c2bc84dd9f67bfa89288c23af26983d08fefcc2cbc22f6e57e6", - "sha256:e66b3c9f8b89d4fd58a59c04fdbf10602a17c914fbaaa5e6ea593f1d54b06362", - "sha256:ed7d11330e443aeecab23866055e08a5a536c95d2c25333aeb441af2dbac38d2", - "sha256:f340a2a908644ea6cccd399be0fb308c66e05d2800107345f9f0f0d59e1731c4", - "sha256:f38b35ecd2628bf0267761ed659e48af7e620a7fcccfccf5774e7308fb18325c", - "sha256:f6d5443104f89a840250087863c91484a72f254574848e951d1bdd7d8b2ce7c9", - "sha256:fc2048d13ff427605fea328cbe5369dce549b8c7657b0e22051a5b8831170af6" + "sha256:028175dd8d2979a889153a2308e8e500b3df7d9e3fd1c33ca7fdeadf61cc87a2", + "sha256:02f0e1a75d3bc0e16c7e15daf9c56185642be055e425f3b34888fc6eb1b22401", + "sha256:0665412dce26b2318092a33bd2d2327d487c4490cfcde158d6946d39b1e28d78", + "sha256:09b9d0f5a445c7e0ddcc021b09835aa6556f0166afc498f57dfdd72cdf6f02ad", + "sha256:09de3bfc995ae8cb955abb0c9ae963c134dba1b5622be3bcc527b89b0fd4091c", + "sha256:0e5536994cf2d8488c6fd9dea71df3c4dbb3e0d2ba5e695da06d9142a29a0969", + "sha256:0f2c5a5984599a88d087a15859860579b825098b473d8c843f1979a83d159f2e", + "sha256:1037097708498bdc85f23c8798a5c46c7bce432d77d23608ff14e0d831f1a971", + "sha256:10f0fddc1d63ba3d4a4bffcc7720184c1b7efd570726ad5e2f55818da320239f", + "sha256:12721d926d43d33dd3318e58dce9b0250e8a9c6e1093fa8e09f4805193ff4b43", + "sha256:1410faa51ce835cc1234c99ec42e98ab4f3c6f50d92d86a2d4f6e11c97ee7a4e", + "sha256:16e74b9c2aca2734c7f49f00fe68d6830a30d26df60e2ace7fe40ccb92087b94", + "sha256:172db03182a22e9002157b262c1ea3b0045c73d4ff465adc152ce5b4b0e7b8d4", + "sha256:174fd1000e896d0dfbc7f6d7e6a1992a4868796c7dec31679e38218c78d6a942", + "sha256:1c2c5e2b00e2fadcd590c0b2e293d71215e98ed1cb635cfca2be4998d197e534", + "sha256:1c9d23f62a3fa7523d849c4942acc0d9ff7081ebc00c808ee7cfdc070df0687f", + "sha256:21e61a536ffed84d10376c21c13a6ed1ebefb61989a844952547c229d6aeedf3", + "sha256:222591b828de10ac90064047b5d4916953f38c38b155009c4b8b5e0d33117c2b", + "sha256:2406df90b2335371706c59b7d79e9633b81ed2a7ecd48c1faf8584552bdf2d90", + "sha256:24e954be35ad4537840f20bbc8d75320ae647d3cb4fab12cb8fcd2d55f408e76", + "sha256:26f9cc42a162faa241c82e117ac85734ae9f14343dc2df1c90c6b2181f791b22", + "sha256:28565e3dbd69fe5fe35a210067064dbb6ed5abe997079f653c19c873c3896fe6", + "sha256:2943d739715f265a2983ac43747595b6af3312d0a370614040959fd293763adf", + "sha256:2bfc39276c0e6d07c95bd1088b5003f049e986e089509f7dbd68bb7a4b1e65ac", + "sha256:2dae3b353a10c3767e0aa1c1492f2af388f1012b08117695ab3fd1f219e5814e", + "sha256:2e0854170813238f0c3131050c67cb1fb1ade75c93bf6cd156c1bd9a16095528", + "sha256:30245a8747dc90019a3c9ad9df987e0280a3ea632ad36227cde7d1d8dcba0830", + "sha256:30ed2788a6ec68743e2040ab1d16573d7d9f6e7333e45070ce9268cbc93d148c", + "sha256:32eac95bbb030b2376ffd897376c6f870222a3457f01a9ce466b9057876132f8", + "sha256:34cd48df7e1fc69222f296d8f69e3957eb7c6b5aa0709d3467184880ed7538c0", + "sha256:34dbf5fecf653c152edb75a35a8b15dfdc4549473484ee768aeb12c97983cead", + "sha256:398fb86d374dc351a4abc2e24cd15e5e14b2127f6d90ce0df3fdf2adcc55ac1b", + "sha256:3ad3a3df830f7df7e0856c2bdb54d19f5bf188bd7420985e18643b8e4d2a075f", + "sha256:3b261d593f2563299062733ae003a925420a86ff4ddda68a69097d67204e43f3", + "sha256:3c5cb6c93c94df76a879bad4b89db0104b01806d17c2b803c1316ba50962b6d6", + "sha256:3cfc9bc1e8b5667bc1f3dbe46d2f85b3f24ff7533893bdc1203058012db2c046", + "sha256:4092b660ec720d44d3ca81074280dc25c7a3718df1b6c0fe9fe36ac6ed2833e4", + "sha256:42ba8606492d76e6f9e4c7a458ed4bc712603be393259a52450345f0945da2cf", + "sha256:4a32f3dfcca4a4816373bdb6256c18c78974ebb3430e7da988516cd95b2bd6e4", + "sha256:4a82a1c10f5608e6494913faa169e213d703194bfca0aa710901f303be212414", + "sha256:4bbc0d27dfef7689285e54f2e0a224f0c7cd9d5c46d2638fabad5500b951c92f", + "sha256:4d9ed67c987bf9ac2ac684590ba3d2599cdfb0f331ee3db607f9684469b3b59d", + "sha256:4f6dd55dab77adf60b445c11f426ee5cdfa1b86f6d54cb937bfcbf09572333ab", + "sha256:50a81b2d9f188c7909e0a1084fa969bb92a788076809c437ac1ae80393f46df9", + "sha256:50b99f4d3eee6f03778fe841d6f470e6c18e744dc665156da6da3bc6e65b398d", + "sha256:5136ebe8da6a1604998a8eb96be55935aa5f7129c41cc7bddc400d48e8df43be", + "sha256:570ae3365b23d4fd8c669cb57613b1a90b2757e993588d3370ef90945dbeec4b", + "sha256:5831a377d15a626fbec10890ffebc4c6abcd37e4126737932cd780a171eabdc1", + "sha256:59c98e86c5e861032b71e6e5b65f23e6afaacea6e82483b66f1191a5021a7b4f", + "sha256:5bdeb71a610a7b801416268e500e716d0fe693fb10d809e17f0fb3dac5be5a34", + "sha256:5c1db7d366004d6c699eb08c716a63ae0a3e946d061cbebea65d7ce361950265", + "sha256:61660710b054ae52c8fc10368e91d74719eb05554b631d7f8ca93d21d2bff2e6", + "sha256:644470442beaf969df99c4e00367a817eee05f0bba5d888f1ba6fe97b5e1c102", + "sha256:64ed1a5ce5e5926727eb0f87c698c4d9a7a9f7b0953683a65e9ce2b7cc5f8e91", + "sha256:65a063970e15a4f338f14b820561cf6cdaf2839691ac0adb2474ddff9d0b8b0b", + "sha256:65b6fddf6a7b91da044f202771a38e71bbb9bf42720a406b26b25fe2256e7102", + "sha256:6af0a4b17faf26779d5caee8542a4f2cba040cea27d3bffc476cbc6ccbd4c8ee", + "sha256:70b67390e27e58876853efbb87e43c85252de2515e2887f7dd901b4fa3d21973", + "sha256:7219b1a726ced3bacecabef9bd114529bbb69477901373e800d7d0140baadc95", + "sha256:7593cb1214185a0c5b43b96effc51ce82ddc933298ee36db7dc2bd45d61b4adc", + "sha256:776f90bf2252f90a4ae838e7917638894c6356bef7265f424592e2fd1f577d05", + "sha256:79f777eaf3f5b2c6d81f9ef00d87837001d7063302503bbcbfdbf3e9bc27c96f", + "sha256:7c7cab8155f430ca460a6fc7ae8a705b34f3e279a57adb5f900eb81943ec777c", + "sha256:7cb987b199fa223ad78eebaa9fbc183d5a5944bfe568a9d6f617316ca1c1f32f", + "sha256:7ec2bb598847569ae34292f580842d37619eea3e546005042f485e15710180d5", + "sha256:80d8576b04d0824f63bf803190359c0d3bcb6e7fa63fefbd4bc0ceaa7faae38c", + "sha256:851f2bb52b5cb2f4711171ca925e0e05344a8452972a748a8a8ffdda1e1d72a7", + "sha256:8927f22ef6a16229da7f18944deac8605bdc2c0858be5184259f2f7ce7fd4459", + "sha256:8ad0515abb132f52ce9d8abd1a29681a1e65dba7b7fe13ea01e1a8db5715bf80", + "sha256:8cc37b437cba909bef06499dadd91a39c15c14225e8d8c7870020049f8a549fe", + "sha256:93d4e9a02c17813b34e4bd9f6fbf07310c140c8f74341537c24d07c1cdeb24d1", + "sha256:944249aa83dee314420c37d0f40c30a8f6dc4a3877566017b87062e53af449f4", + "sha256:9b2ed9c3b30f11cd4a3fbfc22167af7987b01b444215c2463265153fe7cf66d6", + "sha256:9c3d07ea19cd2856d9943dce37e75d69ecbb5baf93c3e4c82f73b6075c481292", + "sha256:9f592b202d77923498b32ddc5b376e5fa9ba280d3e16ed56cb8c932fe6d6a478", + "sha256:a149377d1ff766fd618500798d0d94637f66d0ae222bb6d28f41f3e15c626297", + "sha256:a17b81f22398e3e0f72bdf938e98c810286994b2bcc0a125cd5ad8fd4ea54ad7", + "sha256:a424bdedfd84454d2905a861e0d4bb947cc5bd024fdeb3600c1a97d2be0f4255", + "sha256:a6cbb73d9fc2282677e2b7a137d13da987bd0b13abd88ed27bba5534c226db06", + "sha256:a796ef39dadf9d73af05d24937644d386495e43a7d13617aa3651d836da542c8", + "sha256:aa3bca8e76f5c00ed2bb4325e0e383a547d71595926d5275d7c88175aaf7435e", + "sha256:b01ce58eec5edeededf1992d2dce63fb8565e437be12d6f139d75b15614c4d08", + "sha256:b0746d0d4535f56bbaa63a8f6da362f330804d578e66e126b226eebe76c2bf00", + "sha256:b1223b826acbef07a7f5eb9bf37247b0b580119916dca9eae19d92b1290f5855", + "sha256:b5b733694e7df22d5c049581acfc487695a6ff813322318bed8dd66f79978636", + "sha256:b6793baf4639c72a500698a49e9250b293e17ae1faf11ac1699d8141194786fe", + "sha256:b96e0e9d2d48948240b510bac81614458fc10adcd3a93240c2fd96448b4efd35", + "sha256:bc04c92d05c142889c26810a4842273deb42e66411273cab4ad09268fe69ba69", + "sha256:bdd34c57b4da51a7961beb33645646d197e41f8517801dc76b37c1441e7a4e10", + "sha256:c0379447587ee4b8f983ba183202496e86c0358f47c45612619d634d1fcd82bd", + "sha256:c3b70ed82f20d18d22eafc9bda0ea656605071762f7d31f3c5afc35c59d3393b", + "sha256:c7c45a8a1a752002b0a7c81ab3a4c5e3b6f67f9826b16fbe3943f5329f565f24", + "sha256:c8f755ff1f4ab4ca790d1d6d3229006100b301475948021b6b2757822e0d6c97", + "sha256:d1a19d6c5098f1f4e11430cd74621699453cbc534dd7ade9167e582f50814b19", + "sha256:d1ee773fb72ba024e7e3bb6ea8907fe52bccafcb5184aaced6bad995bd30ea20", + "sha256:d42eb29ba314adfd9c11234b4b646f61b0448bf9b00f14db4b317e6e4b947e77", + "sha256:d593d50815771f517d3ac4367ff716e3f3c78edae51d98e1e25791459f8848ff", + "sha256:d7910135f5de1c5c3578e61d6f4b087715b15e365f11d4fa51a9cee92988b2bd", + "sha256:d7c91747ec8dde51440dd594603158cc98abb3f7df84b2ed8a836f138285e4fb", + "sha256:db2e11507fe9cc2a722be21ccc62c1b1295398fe9724c1f14900cdc7166fc0d7", + "sha256:db5b4f8ad8607a3d612da1d4c89a84e4cf5c88f98b46365820d9babe5884ba45", + "sha256:e1956f3338c10308e2f99c2c9ff46ae412035cbcd7aaa76c39ccdb806854a247", + "sha256:e22d6cf5802cd09b674c307cc9e03870b8c37c503ebec3d25b86f2ce8c535dc7", + "sha256:e5161167b3840e9c84c80f2534ea6a099f51749d5673b662a3dd248be17c3208", + "sha256:e5e87c0eb774561c546f979342a8ff36ebee153c60a0b6c6b03ba989ceb9538c", + "sha256:e6f8191a282ef77e526f8f8f63753a437e4aa4bc78f5edd8b6b6ed0eaebd5363", + "sha256:e8f6979664ff477cd61b06bf8aba206df7b2334209815ab3b1019931dab643d6", + "sha256:ea8824ebc9a1a5c8269e8f1e3989b5a6bec876726e2f3c33ebd036cb488277f0", + "sha256:f4175fcdddf764d371ee52ec4505a40facee2533e84abf2953cda86d050cfa1f", + "sha256:fe8194f107f0fa3cabd14e9e809f174eca335993c1db72d1e74e0f496e7afe1f" ], "index": "pypi", - "version": "==3.12.3" + "version": "==3.13.0" }, "python-dateutil": { "hashes": [ @@ -665,55 +671,57 @@ }, "tinycss2": { "hashes": [ - "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf", - "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8" + "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847", + "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627" ], - "markers": "python_version >= '3.6'", - "version": "==1.1.1" + "markers": "python_version >= '3.7'", + "version": "==1.2.1" }, "urllib3": { "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.12" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.18" }, "uvloop": { "hashes": [ - "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d", - "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1", - "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595", - "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b", - "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05", - "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8", - "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20", - "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded", - "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c", - "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8", - "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474", - "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f", - "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62", - "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376", - "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c", - "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e", - "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b", - "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4", - "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578", - "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811", - "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d", - "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738", - "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa", - "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9", - "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539", - "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c", - "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718", - "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667", - "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c", - "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024" + "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd", + "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec", + "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b", + "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc", + "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797", + "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5", + "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2", + "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d", + "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be", + "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd", + "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12", + "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17", + "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef", + "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24", + "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428", + "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1", + "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849", + "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593", + "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd", + "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67", + "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6", + "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3", + "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd", + "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8", + "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7", + "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533", + "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957", + "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650", + "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e", + "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7", + "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256" ], + "index": "pypi", "markers": "sys_platform != 'win32'", - "version": "==0.17.0" + "version": "==0.19.0" }, "webencodings": { "hashes": [ @@ -724,68 +732,83 @@ }, "yarl": { "hashes": [ - "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb", - "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3", - "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035", - "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453", - "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d", - "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a", - "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231", - "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f", - "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae", - "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b", - "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3", - "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507", - "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd", - "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae", - "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe", - "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c", - "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4", - "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64", - "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357", - "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54", - "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461", - "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4", - "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497", - "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0", - "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1", - "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957", - "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350", - "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780", - "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843", - "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548", - "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6", - "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40", - "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee", - "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b", - "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6", - "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0", - "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e", - "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880", - "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc", - "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e", - "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead", - "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28", - "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf", - "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd", - "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae", - "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0", - "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0", - "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae", - "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda", - "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546", - "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802", - "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be", - "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07", - "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936", - "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272", - "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc", - "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a", - "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28", - "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b" + "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571", + "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3", + "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3", + "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c", + "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7", + "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04", + "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191", + "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea", + "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4", + "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4", + "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095", + "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e", + "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74", + "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef", + "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33", + "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde", + "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45", + "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf", + "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b", + "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac", + "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0", + "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528", + "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716", + "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb", + "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18", + "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72", + "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6", + "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582", + "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5", + "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368", + "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc", + "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9", + "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be", + "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a", + "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80", + "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8", + "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6", + "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417", + "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574", + "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59", + "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608", + "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82", + "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1", + "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3", + "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d", + "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8", + "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc", + "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac", + "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8", + "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955", + "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0", + "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367", + "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb", + "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a", + "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623", + "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2", + "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6", + "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7", + "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4", + "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051", + "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938", + "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8", + "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9", + "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3", + "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5", + "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9", + "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333", + "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185", + "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3", + "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560", + "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b", + "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7", + "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78", + "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7" ], "markers": "python_version >= '3.7'", - "version": "==1.8.1" + "version": "==1.9.2" } }, "develop": { @@ -799,11 +822,11 @@ }, "bandit": { "hashes": [ - "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2", - "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a" + "sha256:75665181dc1e0096369112541a056c59d1c5f66f9bb74a8d686c3c362b83f549", + "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e" ], "index": "pypi", - "version": "==1.7.4" + "version": "==1.7.5" }, "black": { "hashes": [ @@ -836,86 +859,85 @@ }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "markers": "python_version >= '3.7'", - "version": "==8.1.3" - }, - "colorama": { - "hashes": [ - "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da", - "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4" - ], - "index": "pypi", - "version": "==0.4.5" + "version": "==8.1.7" }, "gitdb": { "hashes": [ - "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd", - "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa" + "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", + "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b" ], - "markers": "python_version >= '3.6'", - "version": "==4.0.9" + "markers": "python_version >= '3.7'", + "version": "==4.0.11" }, "gitpython": { "hashes": [ - "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704", - "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d" + "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4", + "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a" ], "markers": "python_version >= '3.7'", - "version": "==3.1.27" + "version": "==3.1.40" }, "isort": { "hashes": [ - "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", - "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" + "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", + "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" ], - "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", - "version": "==5.10.1" + "markers": "python_version >= '3.8'", + "version": "==5.12.0" }, "lazy-object-proxy": { "hashes": [ - "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7", - "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a", - "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c", - "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc", - "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f", - "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09", - "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442", - "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e", - "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029", - "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61", - "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb", - "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0", - "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35", - "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42", - "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1", - "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad", - "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443", - "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd", - "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9", - "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148", - "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38", - "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55", - "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36", - "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a", - "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b", - "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44", - "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6", - "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69", - "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4", - "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84", - "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de", - "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28", - "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c", - "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1", - "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8", - "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b", - "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb" + "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382", + "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82", + "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9", + "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494", + "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46", + "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30", + "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63", + "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4", + "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae", + "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be", + "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701", + "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd", + "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006", + "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a", + "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586", + "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8", + "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821", + "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07", + "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b", + "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171", + "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b", + "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2", + "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7", + "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4", + "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8", + "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e", + "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f", + "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda", + "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4", + "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e", + "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671", + "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11", + "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455", + "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734", + "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb", + "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59" ], - "markers": "python_version >= '3.6'", - "version": "==1.7.1" + "markers": "python_version >= '3.7'", + "version": "==1.9.0" + }, + "markdown-it-py": { + "hashes": [ + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.0" }, "mccabe": { "hashes": [ @@ -924,36 +946,53 @@ ], "version": "==0.6.1" }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, "mypy-extensions": { "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" ], - "version": "==0.4.3" + "markers": "python_version >= '3.5'", + "version": "==1.0.0" }, "pathspec": { "hashes": [ - "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93", - "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d" + "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", + "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" ], "markers": "python_version >= '3.7'", - "version": "==0.10.1" + "version": "==0.11.2" }, "pbr": { "hashes": [ - "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a", - "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf" + "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda", + "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9" ], "markers": "python_version >= '2.6'", - "version": "==5.10.0" + "version": "==6.0.0" }, "platformdirs": { "hashes": [ - "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", - "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" + "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", + "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" + ], + "markers": "python_version >= '3.7'", + "version": "==4.0.0" + }, + "pygments": { + "hashes": [ + "sha256:1b37f1b1e1bff2af52ecaf28cc601e2ef7077000b227a0675da25aef85784bc4", + "sha256:e45a0e74bf9c530f564ca81b8952343be986a29f6afe7f5ad95c5f06b7bdf5e8" ], "markers": "python_version >= '3.7'", - "version": "==2.5.2" + "version": "==2.17.1" }, "pylint": { "hashes": [ @@ -965,73 +1004,91 @@ }, "pyyaml": { "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" ], "markers": "python_version >= '3.6'", - "version": "==6.0" + "version": "==6.0.1" }, - "setuptools": { + "rich": { "hashes": [ - "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", - "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57" + "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", + "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" ], "markers": "python_version >= '3.7'", - "version": "==65.3.0" + "version": "==13.7.0" + }, + "setuptools": { + "hashes": [ + "sha256:4c65d4f7891e5b046e9146913b87098144de2ca2128fbc10135b8556a6ddd946", + "sha256:eb03b43f23910c5fd0909cb677ad017cd9531f493d27f8b3f5316ff1fb07390e" + ], + "markers": "python_version >= '3.8'", + "version": "==69.0.0" }, "smmap": { "hashes": [ - "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94", - "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936" + "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", + "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da" ], - "markers": "python_version >= '3.6'", - "version": "==5.0.0" + "markers": "python_version >= '3.7'", + "version": "==5.0.1" }, "stevedore": { "hashes": [ - "sha256:87e4d27fe96d0d7e4fc24f0cbe3463baae4ec51e81d95fbe60d2474636e0c7d8", - "sha256:f82cc99a1ff552310d19c379827c2c64dd9f85a38bcd5559db2470161867b786" + "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d", + "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c" ], "markers": "python_version >= '3.8'", - "version": "==4.0.0" + "version": "==5.1.0" }, "toml": { "hashes": [ diff --git a/bot.py b/bot.py index f36ecc8351..4245ce408b 100644 --- a/bot.py +++ b/bot.py @@ -21,7 +21,7 @@ from aiohttp import ClientSession, ClientResponseError from discord.ext import commands, tasks from discord.ext.commands.view import StringView -from emoji import UNICODE_EMOJI +from emoji import is_emoji from pkg_resources import parse_version @@ -623,11 +623,11 @@ async def convert_emoji(self, name: str) -> str: ctx = SimpleNamespace(bot=self, guild=self.modmail_guild) converter = commands.EmojiConverter() - if name not in UNICODE_EMOJI["en"]: + if not is_emoji(name): try: name = await converter.convert(ctx, name.strip(":")) except commands.BadArgument as e: - logger.warning("%s is not a valid emoji. %s.", name, e) + logger.warning("%s is not a valid emoji: %s", name, e) raise return name From 6fea4b61f1bfc20e2e9fcd9b235a5b39b14bb814 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:38:57 -0800 Subject: [PATCH 1177/1221] Updated the rest of the dependencies --- .github/workflows/lints.yml | 2 +- Pipfile | 26 +- Pipfile.lock | 490 +++++++++++++++++++----------------- pyproject.toml | 2 +- 4 files changed, 269 insertions(+), 251 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index eaad52221f..b283dff078 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10'] + python-version: ['3.10', '3.11'] name: Python ${{ matrix.python-version }} on ubuntu-latest diff --git a/Pipfile b/Pipfile index f2590a1e2b..95c75285b5 100644 --- a/Pipfile +++ b/Pipfile @@ -4,26 +4,26 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -bandit = "~=1.7.0" -black = "==22.3.0" -pylint = "~=2.9.3" -typing-extensions = "==4.2.0" +bandit = ">=1.7.5" +black = "==23.11.0" +pylint = "==3.0.2" +typing-extensions = "==4.8.0" [packages] aiohttp = "==3.9.0" colorama = "==0.4.6" "discord.py" = "==2.0.1" emoji = "==2.8.0" -isodate = "~=0.6.0" -motor = "==2.5.1" -natural = "~=0.2.0" -parsedatetime = "~=2.6" +isodate = "==0.6.1" +motor = "==3.3.2" +natural = "==0.2.0" # Why is this needed? +parsedatetime = "==2.6" pymongo = {extras = ["srv"], version = "*"} # Required by motor -python-dateutil = "~=2.8.1" -python-dotenv = "==0.20.0" -uvloop = {version = ">=0.15.2", markers = "sys_platform != 'win32'"} -lottie = {version = "==0.6.11", extras = ["pdf"]} -requests = "==2.28.1" +python-dateutil = "==2.8.2" +python-dotenv = "==1.0.0" +uvloop = {version = ">=0.19.0", markers = "sys_platform != 'win32'"} +lottie = {version = "==0.7.0", extras = ["pdf"]} +requests = "==2.31.0" [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 35808d048b..640ea61ff6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f28fd3a11b8f1550ba6731a0bc17740966b1fd98d10c1b004d416ca5b1dd3b84" + "sha256": "e883a9f5e7c62e52df23b5a710713897b06264d3a35fd4af2251ef78e7ddc82e" }, "pipfile-spec": 6, "requires": {}, @@ -133,6 +133,7 @@ "sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0", "sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b" ], + "markers": "python_version >= '3.5'", "version": "==2.7.1" }, "certifi": { @@ -198,16 +199,104 @@ "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" ], - "markers": "python_version >= '3.8'", + "markers": "python_full_version >= '3.8.0'", "version": "==1.16.0" }, "charset-normalizer": { "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], - "markers": "python_version >= '3.6'", - "version": "==2.1.1" + "markers": "python_version >= '3.7'", + "version": "==3.3.2" }, "colorama": { "hashes": [ @@ -246,7 +335,7 @@ "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8", "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version < '4.0' and python_full_version >= '3.8.0'", "version": "==2.4.2" }, "emoji": { @@ -321,7 +410,7 @@ "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1", "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e" ], - "markers": "python_version >= '3.8'", + "markers": "python_full_version >= '3.8.0'", "version": "==1.4.0" }, "idna": { @@ -345,18 +434,18 @@ "pdf" ], "hashes": [ - "sha256:d53e96265887aa9187c7c707fd612b3d52f38da64c81ea82297783efb47f7e3f" + "sha256:a3242f8ba37051fbdd7503ecd168203a08e4af26f17be2ecca08a64af1e7d3c1" ], "index": "pypi", - "version": "==0.6.11" + "version": "==0.7.0" }, "motor": { "hashes": [ - "sha256:663473f4498f955d35db7b6f25651cb165514c247136f368b84419cb7635f6b8", - "sha256:961fdceacaae2c7236c939166f66415be81be8bbb762da528386738de3a0f509" + "sha256:6fe7e6f0c4f430b9e030b9d22549b732f7c2226af3ab71ecc309e4a1b7d19953", + "sha256:d2fc38de15f1c8058f389c1a44a4d4105c0405c48c061cd492a654496f7bc26a" ], "index": "pypi", - "version": "==2.5.1" + "version": "==3.3.2" }, "multidict": { "hashes": [ @@ -510,7 +599,7 @@ "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a", "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793" ], - "markers": "python_version >= '3.8'", + "markers": "python_full_version >= '3.8.0'", "version": "==10.1.0" }, "pycparser": { @@ -521,121 +610,94 @@ "version": "==2.21" }, "pymongo": { - "extras": [], + "extras": [ + "srv" + ], "hashes": [ - "sha256:028175dd8d2979a889153a2308e8e500b3df7d9e3fd1c33ca7fdeadf61cc87a2", - "sha256:02f0e1a75d3bc0e16c7e15daf9c56185642be055e425f3b34888fc6eb1b22401", - "sha256:0665412dce26b2318092a33bd2d2327d487c4490cfcde158d6946d39b1e28d78", - "sha256:09b9d0f5a445c7e0ddcc021b09835aa6556f0166afc498f57dfdd72cdf6f02ad", - "sha256:09de3bfc995ae8cb955abb0c9ae963c134dba1b5622be3bcc527b89b0fd4091c", - "sha256:0e5536994cf2d8488c6fd9dea71df3c4dbb3e0d2ba5e695da06d9142a29a0969", - "sha256:0f2c5a5984599a88d087a15859860579b825098b473d8c843f1979a83d159f2e", - "sha256:1037097708498bdc85f23c8798a5c46c7bce432d77d23608ff14e0d831f1a971", - "sha256:10f0fddc1d63ba3d4a4bffcc7720184c1b7efd570726ad5e2f55818da320239f", - "sha256:12721d926d43d33dd3318e58dce9b0250e8a9c6e1093fa8e09f4805193ff4b43", - "sha256:1410faa51ce835cc1234c99ec42e98ab4f3c6f50d92d86a2d4f6e11c97ee7a4e", - "sha256:16e74b9c2aca2734c7f49f00fe68d6830a30d26df60e2ace7fe40ccb92087b94", - "sha256:172db03182a22e9002157b262c1ea3b0045c73d4ff465adc152ce5b4b0e7b8d4", - "sha256:174fd1000e896d0dfbc7f6d7e6a1992a4868796c7dec31679e38218c78d6a942", - "sha256:1c2c5e2b00e2fadcd590c0b2e293d71215e98ed1cb635cfca2be4998d197e534", - "sha256:1c9d23f62a3fa7523d849c4942acc0d9ff7081ebc00c808ee7cfdc070df0687f", - "sha256:21e61a536ffed84d10376c21c13a6ed1ebefb61989a844952547c229d6aeedf3", - "sha256:222591b828de10ac90064047b5d4916953f38c38b155009c4b8b5e0d33117c2b", - "sha256:2406df90b2335371706c59b7d79e9633b81ed2a7ecd48c1faf8584552bdf2d90", - "sha256:24e954be35ad4537840f20bbc8d75320ae647d3cb4fab12cb8fcd2d55f408e76", - "sha256:26f9cc42a162faa241c82e117ac85734ae9f14343dc2df1c90c6b2181f791b22", - "sha256:28565e3dbd69fe5fe35a210067064dbb6ed5abe997079f653c19c873c3896fe6", - "sha256:2943d739715f265a2983ac43747595b6af3312d0a370614040959fd293763adf", - "sha256:2bfc39276c0e6d07c95bd1088b5003f049e986e089509f7dbd68bb7a4b1e65ac", - "sha256:2dae3b353a10c3767e0aa1c1492f2af388f1012b08117695ab3fd1f219e5814e", - "sha256:2e0854170813238f0c3131050c67cb1fb1ade75c93bf6cd156c1bd9a16095528", - "sha256:30245a8747dc90019a3c9ad9df987e0280a3ea632ad36227cde7d1d8dcba0830", - "sha256:30ed2788a6ec68743e2040ab1d16573d7d9f6e7333e45070ce9268cbc93d148c", - "sha256:32eac95bbb030b2376ffd897376c6f870222a3457f01a9ce466b9057876132f8", - "sha256:34cd48df7e1fc69222f296d8f69e3957eb7c6b5aa0709d3467184880ed7538c0", - "sha256:34dbf5fecf653c152edb75a35a8b15dfdc4549473484ee768aeb12c97983cead", - "sha256:398fb86d374dc351a4abc2e24cd15e5e14b2127f6d90ce0df3fdf2adcc55ac1b", - "sha256:3ad3a3df830f7df7e0856c2bdb54d19f5bf188bd7420985e18643b8e4d2a075f", - "sha256:3b261d593f2563299062733ae003a925420a86ff4ddda68a69097d67204e43f3", - "sha256:3c5cb6c93c94df76a879bad4b89db0104b01806d17c2b803c1316ba50962b6d6", - "sha256:3cfc9bc1e8b5667bc1f3dbe46d2f85b3f24ff7533893bdc1203058012db2c046", - "sha256:4092b660ec720d44d3ca81074280dc25c7a3718df1b6c0fe9fe36ac6ed2833e4", - "sha256:42ba8606492d76e6f9e4c7a458ed4bc712603be393259a52450345f0945da2cf", - "sha256:4a32f3dfcca4a4816373bdb6256c18c78974ebb3430e7da988516cd95b2bd6e4", - "sha256:4a82a1c10f5608e6494913faa169e213d703194bfca0aa710901f303be212414", - "sha256:4bbc0d27dfef7689285e54f2e0a224f0c7cd9d5c46d2638fabad5500b951c92f", - "sha256:4d9ed67c987bf9ac2ac684590ba3d2599cdfb0f331ee3db607f9684469b3b59d", - "sha256:4f6dd55dab77adf60b445c11f426ee5cdfa1b86f6d54cb937bfcbf09572333ab", - "sha256:50a81b2d9f188c7909e0a1084fa969bb92a788076809c437ac1ae80393f46df9", - "sha256:50b99f4d3eee6f03778fe841d6f470e6c18e744dc665156da6da3bc6e65b398d", - "sha256:5136ebe8da6a1604998a8eb96be55935aa5f7129c41cc7bddc400d48e8df43be", - "sha256:570ae3365b23d4fd8c669cb57613b1a90b2757e993588d3370ef90945dbeec4b", - "sha256:5831a377d15a626fbec10890ffebc4c6abcd37e4126737932cd780a171eabdc1", - "sha256:59c98e86c5e861032b71e6e5b65f23e6afaacea6e82483b66f1191a5021a7b4f", - "sha256:5bdeb71a610a7b801416268e500e716d0fe693fb10d809e17f0fb3dac5be5a34", - "sha256:5c1db7d366004d6c699eb08c716a63ae0a3e946d061cbebea65d7ce361950265", - "sha256:61660710b054ae52c8fc10368e91d74719eb05554b631d7f8ca93d21d2bff2e6", - "sha256:644470442beaf969df99c4e00367a817eee05f0bba5d888f1ba6fe97b5e1c102", - "sha256:64ed1a5ce5e5926727eb0f87c698c4d9a7a9f7b0953683a65e9ce2b7cc5f8e91", - "sha256:65a063970e15a4f338f14b820561cf6cdaf2839691ac0adb2474ddff9d0b8b0b", - "sha256:65b6fddf6a7b91da044f202771a38e71bbb9bf42720a406b26b25fe2256e7102", - "sha256:6af0a4b17faf26779d5caee8542a4f2cba040cea27d3bffc476cbc6ccbd4c8ee", - "sha256:70b67390e27e58876853efbb87e43c85252de2515e2887f7dd901b4fa3d21973", - "sha256:7219b1a726ced3bacecabef9bd114529bbb69477901373e800d7d0140baadc95", - "sha256:7593cb1214185a0c5b43b96effc51ce82ddc933298ee36db7dc2bd45d61b4adc", - "sha256:776f90bf2252f90a4ae838e7917638894c6356bef7265f424592e2fd1f577d05", - "sha256:79f777eaf3f5b2c6d81f9ef00d87837001d7063302503bbcbfdbf3e9bc27c96f", - "sha256:7c7cab8155f430ca460a6fc7ae8a705b34f3e279a57adb5f900eb81943ec777c", - "sha256:7cb987b199fa223ad78eebaa9fbc183d5a5944bfe568a9d6f617316ca1c1f32f", - "sha256:7ec2bb598847569ae34292f580842d37619eea3e546005042f485e15710180d5", - "sha256:80d8576b04d0824f63bf803190359c0d3bcb6e7fa63fefbd4bc0ceaa7faae38c", - "sha256:851f2bb52b5cb2f4711171ca925e0e05344a8452972a748a8a8ffdda1e1d72a7", - "sha256:8927f22ef6a16229da7f18944deac8605bdc2c0858be5184259f2f7ce7fd4459", - "sha256:8ad0515abb132f52ce9d8abd1a29681a1e65dba7b7fe13ea01e1a8db5715bf80", - "sha256:8cc37b437cba909bef06499dadd91a39c15c14225e8d8c7870020049f8a549fe", - "sha256:93d4e9a02c17813b34e4bd9f6fbf07310c140c8f74341537c24d07c1cdeb24d1", - "sha256:944249aa83dee314420c37d0f40c30a8f6dc4a3877566017b87062e53af449f4", - "sha256:9b2ed9c3b30f11cd4a3fbfc22167af7987b01b444215c2463265153fe7cf66d6", - "sha256:9c3d07ea19cd2856d9943dce37e75d69ecbb5baf93c3e4c82f73b6075c481292", - "sha256:9f592b202d77923498b32ddc5b376e5fa9ba280d3e16ed56cb8c932fe6d6a478", - "sha256:a149377d1ff766fd618500798d0d94637f66d0ae222bb6d28f41f3e15c626297", - "sha256:a17b81f22398e3e0f72bdf938e98c810286994b2bcc0a125cd5ad8fd4ea54ad7", - "sha256:a424bdedfd84454d2905a861e0d4bb947cc5bd024fdeb3600c1a97d2be0f4255", - "sha256:a6cbb73d9fc2282677e2b7a137d13da987bd0b13abd88ed27bba5534c226db06", - "sha256:a796ef39dadf9d73af05d24937644d386495e43a7d13617aa3651d836da542c8", - "sha256:aa3bca8e76f5c00ed2bb4325e0e383a547d71595926d5275d7c88175aaf7435e", - "sha256:b01ce58eec5edeededf1992d2dce63fb8565e437be12d6f139d75b15614c4d08", - "sha256:b0746d0d4535f56bbaa63a8f6da362f330804d578e66e126b226eebe76c2bf00", - "sha256:b1223b826acbef07a7f5eb9bf37247b0b580119916dca9eae19d92b1290f5855", - "sha256:b5b733694e7df22d5c049581acfc487695a6ff813322318bed8dd66f79978636", - "sha256:b6793baf4639c72a500698a49e9250b293e17ae1faf11ac1699d8141194786fe", - "sha256:b96e0e9d2d48948240b510bac81614458fc10adcd3a93240c2fd96448b4efd35", - "sha256:bc04c92d05c142889c26810a4842273deb42e66411273cab4ad09268fe69ba69", - "sha256:bdd34c57b4da51a7961beb33645646d197e41f8517801dc76b37c1441e7a4e10", - "sha256:c0379447587ee4b8f983ba183202496e86c0358f47c45612619d634d1fcd82bd", - "sha256:c3b70ed82f20d18d22eafc9bda0ea656605071762f7d31f3c5afc35c59d3393b", - "sha256:c7c45a8a1a752002b0a7c81ab3a4c5e3b6f67f9826b16fbe3943f5329f565f24", - "sha256:c8f755ff1f4ab4ca790d1d6d3229006100b301475948021b6b2757822e0d6c97", - "sha256:d1a19d6c5098f1f4e11430cd74621699453cbc534dd7ade9167e582f50814b19", - "sha256:d1ee773fb72ba024e7e3bb6ea8907fe52bccafcb5184aaced6bad995bd30ea20", - "sha256:d42eb29ba314adfd9c11234b4b646f61b0448bf9b00f14db4b317e6e4b947e77", - "sha256:d593d50815771f517d3ac4367ff716e3f3c78edae51d98e1e25791459f8848ff", - "sha256:d7910135f5de1c5c3578e61d6f4b087715b15e365f11d4fa51a9cee92988b2bd", - "sha256:d7c91747ec8dde51440dd594603158cc98abb3f7df84b2ed8a836f138285e4fb", - "sha256:db2e11507fe9cc2a722be21ccc62c1b1295398fe9724c1f14900cdc7166fc0d7", - "sha256:db5b4f8ad8607a3d612da1d4c89a84e4cf5c88f98b46365820d9babe5884ba45", - "sha256:e1956f3338c10308e2f99c2c9ff46ae412035cbcd7aaa76c39ccdb806854a247", - "sha256:e22d6cf5802cd09b674c307cc9e03870b8c37c503ebec3d25b86f2ce8c535dc7", - "sha256:e5161167b3840e9c84c80f2534ea6a099f51749d5673b662a3dd248be17c3208", - "sha256:e5e87c0eb774561c546f979342a8ff36ebee153c60a0b6c6b03ba989ceb9538c", - "sha256:e6f8191a282ef77e526f8f8f63753a437e4aa4bc78f5edd8b6b6ed0eaebd5363", - "sha256:e8f6979664ff477cd61b06bf8aba206df7b2334209815ab3b1019931dab643d6", - "sha256:ea8824ebc9a1a5c8269e8f1e3989b5a6bec876726e2f3c33ebd036cb488277f0", - "sha256:f4175fcdddf764d371ee52ec4505a40facee2533e84abf2953cda86d050cfa1f", - "sha256:fe8194f107f0fa3cabd14e9e809f174eca335993c1db72d1e74e0f496e7afe1f" + "sha256:014e7049dd019a6663747ca7dae328943e14f7261f7c1381045dfc26a04fa330", + "sha256:055f5c266e2767a88bb585d01137d9c7f778b0195d3dbf4a487ef0638be9b651", + "sha256:05c30fd35cc97f14f354916b45feea535d59060ef867446b5c3c7f9b609dd5dc", + "sha256:0634994b026336195778e5693583c060418d4ab453eff21530422690a97e1ee8", + "sha256:09c7de516b08c57647176b9fc21d929d628e35bcebc7422220c89ae40b62126a", + "sha256:107a234dc55affc5802acb3b6d83cbb8c87355b38a9457fcd8806bdeb8bce161", + "sha256:10a379fb60f1b2406ae57b8899bacfe20567918c8e9d2d545e1b93628fcf2050", + "sha256:128b1485753106c54af481789cdfea12b90a228afca0b11fb3828309a907e10e", + "sha256:1394c4737b325166a65ae7c145af1ebdb9fb153ebedd37cf91d676313e4a67b8", + "sha256:1c63e3a2e8fb815c4b1f738c284a4579897e37c3cfd95fdb199229a1ccfb638a", + "sha256:1e4ed21029d80c4f62605ab16398fe1ce093fff4b5f22d114055e7d9fbc4adb0", + "sha256:1ec71ac633b126c0775ed4604ca8f56c3540f5c21a1220639f299e7a544b55f9", + "sha256:21812453354b151200034750cd30b0140e82ec2a01fd4357390f67714a1bfbde", + "sha256:256c503a75bd71cf7fb9ebf889e7e222d49c6036a48aad5a619f98a0adf0e0d7", + "sha256:2703a9f8f5767986b4f51c259ff452cc837c5a83c8ed5f5361f6e49933743b2f", + "sha256:288c21ab9531b037f7efa4e467b33176bc73a0c27223c141b822ab4a0e66ff2a", + "sha256:2972dd1f1285866aba027eff2f4a2bbf8aa98563c2ced14cb34ee5602b36afdf", + "sha256:2973f113e079fb98515722cd728e1820282721ec9fd52830e4b73cabdbf1eb28", + "sha256:2ca0ba501898b2ec31e6c3acf90c31910944f01d454ad8e489213a156ccf1bda", + "sha256:2d2be5c9c3488fa8a70f83ed925940f488eac2837a996708d98a0e54a861f212", + "sha256:2f8c04277d879146eacda920476e93d520eff8bec6c022ac108cfa6280d84348", + "sha256:325701ae7b56daa5b0692305b7cb505ca50f80a1288abb32ff420a8a209b01ca", + "sha256:3729b8db02063da50eeb3db88a27670d85953afb9a7f14c213ac9e3dca93034b", + "sha256:3919708594b86d0f5cdc713eb6fccd3f9b9532af09ea7a5d843c933825ef56c4", + "sha256:39a1cd5d383b37285641d5a7a86be85274466ae336a61b51117155936529f9b3", + "sha256:3ec6c20385c5a58e16b1ea60c5e4993ea060540671d7d12664f385f2fb32fe79", + "sha256:47aa128be2e66abd9d1a9b0437c62499d812d291f17b55185cb4aa33a5f710a4", + "sha256:49f2af6cf82509b15093ce3569229e0d53c90ad8ae2eef940652d4cf1f81e045", + "sha256:4a0269811661ba93c472c8a60ea82640e838c2eb148d252720a09b5123f2c2fe", + "sha256:518c90bdd6e842c446d01a766b9136fec5ec6cc94f3b8c3f8b4a332786ee6b64", + "sha256:5717a308a703dda2886a5796a07489c698b442f5e409cf7dc2ac93de8d61d764", + "sha256:5802acc012bbb4bce4dff92973dff76482f30ef35dd4cb8ab5b0e06aa8f08c80", + "sha256:5e63146dbdb1eac207464f6e0cfcdb640c9c5ff0f57b754fa96fe252314a1dc6", + "sha256:6695d7136a435c1305b261a9ddb9b3ecec9863e05aab3935b96038145fd3a977", + "sha256:680fa0fc719e1a3dcb81130858368f51d83667d431924d0bcf249644bce8f303", + "sha256:6b18276f14b4b6d92e707ab6db19b938e112bd2f1dc3f9f1a628df58e4fd3f0d", + "sha256:6bafea6061d63059d8bc2ffc545e2f049221c8a4457d236c5cd6a66678673eab", + "sha256:6d6a1b1361f118e7fefa17ae3114e77f10ee1b228b20d50c47c9f351346180c8", + "sha256:747c84f4e690fbe6999c90ac97246c95d31460d890510e4a3fa61b7d2b87aa34", + "sha256:79f41576b3022c2fe9780ae3e44202b2438128a25284a8ddfa038f0785d87019", + "sha256:7b0e6361754ac596cd16bfc6ed49f69ffcd9b60b7bc4bcd3ea65c6a83475e4ff", + "sha256:7e3b0127b260d4abae7b62203c4c7ef0874c901b55155692353db19de4b18bc4", + "sha256:7fc2bb8a74dcfcdd32f89528e38dcbf70a3a6594963d60dc9595e3b35b66e414", + "sha256:806e094e9e85d8badc978af8c95b69c556077f11844655cb8cd2d1758769e521", + "sha256:81dd1308bd5630d2bb5980f00aa163b986b133f1e9ed66c66ce2a5bc3572e891", + "sha256:82e620842e12e8cb4050d2643a81c8149361cd82c0a920fa5a15dc4ca8a4000f", + "sha256:85f2cdc400ee87f5952ebf2a117488f2525a3fb2e23863a8efe3e4ee9e54e4d1", + "sha256:8ab6bcc8e424e07c1d4ba6df96f7fb963bcb48f590b9456de9ebd03b88084fe8", + "sha256:8adf014f2779992eba3b513e060d06f075f0ab2fb3ad956f413a102312f65cdf", + "sha256:9b0f98481ad5dc4cb430a60bbb8869f05505283b9ae1c62bdb65eb5e020ee8e3", + "sha256:9bea9138b0fc6e2218147e9c6ce1ff76ff8e29dc00bb1b64842bd1ca107aee9f", + "sha256:a09bfb51953930e7e838972ddf646c5d5f984992a66d79da6ba7f6a8d8a890cd", + "sha256:a0be99b599da95b7a90a918dd927b20c434bea5e1c9b3efc6a3c6cd67c23f813", + "sha256:a49aca4d961823b2846b739380c847e8964ff7ae0f0a683992b9d926054f0d6d", + "sha256:a4dc1319d0c162919ee7f4ee6face076becae2abbd351cc14f1fe70af5fb20d9", + "sha256:a8273e1abbcff1d7d29cbbb1ea7e57d38be72f1af3c597c854168508b91516c2", + "sha256:a8f7f9feecae53fa18d6a3ea7c75f9e9a1d4d20e5c3f9ce3fba83f07bcc4eee2", + "sha256:ad4f66fbb893b55f96f03020e67dcab49ffde0177c6565ccf9dec4fdf974eb61", + "sha256:af425f323fce1b07755edd783581e7283557296946212f5b1a934441718e7528", + "sha256:b14dd73f595199f4275bed4fb509277470d9b9059310537e3b3daba12b30c157", + "sha256:b4ad70d7cac4ca0c7b31444a0148bd3af01a2662fa12b1ad6f57cd4a04e21766", + "sha256:b80a4ee19b3442c57c38afa978adca546521a8822d663310b63ae2a7d7b13f3a", + "sha256:ba51129fcc510824b6ca6e2ce1c27e3e4d048b6e35d3ae6f7e517bed1b8b25ce", + "sha256:c011bd5ad03cc096f99ffcfdd18a1817354132c1331bed7a837a25226659845f", + "sha256:cc94f9fea17a5af8cf1a343597711a26b0117c0b812550d99934acb89d526ed2", + "sha256:ccd785fafa1c931deff6a7116e9a0d402d59fabe51644b0d0c268295ff847b25", + "sha256:d16a534da0e39785687b7295e2fcf9a339f4a20689024983d11afaa4657f8507", + "sha256:d3077a31633beef77d057c6523f5de7271ddef7bde5e019285b00c0cc9cac1e3", + "sha256:d603edea1ff7408638b2504905c032193b7dcee7af269802dbb35bc8c3310ed5", + "sha256:db082f728160369d9a6ed2e722438291558fc15ce06d0a7d696a8dad735c236b", + "sha256:ddef295aaf80cefb0c1606f1995899efcb17edc6b327eb6589e234e614b87756", + "sha256:e16ade71c93f6814d095d25cd6d28a90d63511ea396bd96e9ffcb886b278baaa", + "sha256:e3db7d833a7c38c317dc95b54e27f1d27012e031b45a7c24e360b53197d5f6e7", + "sha256:e5e193f89f4f8c1fe273f9a6e6df915092c9f2af6db2d1afb8bd53855025c11f", + "sha256:eb438a8bf6b695bf50d57e6a059ff09652a07968b2041178b3744ea785fcef9b", + "sha256:ebf02c32afa6b67e5861a27183dd98ed88419a94a2ab843cc145fb0bafcc5b28", + "sha256:ecd9e1fa97aa11bf67472220285775fa15e896da108f425e55d23d7540a712ce", + "sha256:ef67fedd863ffffd4adfd46d9d992b0f929c7f61a8307366d664d93517f2c78e", + "sha256:f28ae33dc5a0b9cee06e95fd420e42155d83271ab75964baf747ce959cac5f52", + "sha256:fb1c56d891f9e34303c451998ef62ba52659648bb0d75b03c5e4ac223a3342c2", + "sha256:fe03bf25fae4b95d8afe40004a321df644400fdcba4c8e5e1a19c1085b740888" ], "index": "pypi", - "version": "==3.13.0" + "version": "==4.6.0" }, "python-dateutil": { "hashes": [ @@ -647,19 +709,19 @@ }, "python-dotenv": { "hashes": [ - "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f", - "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938" + "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", + "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" ], "index": "pypi", - "version": "==0.20.0" + "version": "==1.0.0" }, "requests": { "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "version": "==2.28.1" + "version": "==2.31.0" }, "six": { "hashes": [ @@ -679,11 +741,11 @@ }, "urllib3": { "hashes": [ - "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", - "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.18" + "markers": "python_full_version >= '3.8.0'", + "version": "==2.1.0" }, "uvloop": { "hashes": [ @@ -814,11 +876,11 @@ "develop": { "astroid": { "hashes": [ - "sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334", - "sha256:ab7f36e8a78b8e54a62028ba6beef7561db4cdb6f2a5009ecc44a6f42b5697ef" + "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca", + "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e" ], - "markers": "python_version ~= '3.6'", - "version": "==2.6.6" + "markers": "python_version >= '3.8'", + "version": "==3.0.1" }, "bandit": { "hashes": [ @@ -830,32 +892,27 @@ }, "black": { "hashes": [ - "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", - "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", - "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", - "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", - "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", - "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", - "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", - "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", - "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", - "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", - "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", - "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", - "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", - "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", - "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", - "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", - "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", - "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", - "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", - "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", - "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", - "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", - "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" + "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", + "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", + "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", + "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", + "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", + "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", + "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", + "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", + "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", + "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", + "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", + "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", + "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", + "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", + "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", + "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", + "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", + "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" ], "index": "pypi", - "version": "==22.3.0" + "version": "==23.11.0" }, "click": { "hashes": [ @@ -865,6 +922,14 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "dill": { + "hashes": [ + "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", + "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + ], + "markers": "python_version < '3.11'", + "version": "==0.3.7" + }, "gitdb": { "hashes": [ "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", @@ -889,48 +954,6 @@ "markers": "python_version >= '3.8'", "version": "==5.12.0" }, - "lazy-object-proxy": { - "hashes": [ - "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382", - "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82", - "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9", - "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494", - "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46", - "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30", - "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63", - "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4", - "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae", - "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be", - "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701", - "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd", - "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006", - "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a", - "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586", - "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8", - "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821", - "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07", - "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b", - "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171", - "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b", - "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2", - "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7", - "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4", - "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8", - "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e", - "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f", - "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda", - "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4", - "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e", - "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671", - "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11", - "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455", - "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734", - "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb", - "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59" - ], - "markers": "python_version >= '3.7'", - "version": "==1.9.0" - }, "markdown-it-py": { "hashes": [ "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", @@ -941,10 +964,11 @@ }, "mccabe": { "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" ], - "version": "==0.6.1" + "markers": "python_version >= '3.6'", + "version": "==0.7.0" }, "mdurl": { "hashes": [ @@ -962,6 +986,14 @@ "markers": "python_version >= '3.5'", "version": "==1.0.0" }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, "pathspec": { "hashes": [ "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", @@ -996,11 +1028,11 @@ }, "pylint": { "hashes": [ - "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594", - "sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e" + "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496", + "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" ], "index": "pypi", - "version": "==2.9.6" + "version": "==3.0.2" }, "pyyaml": { "hashes": [ @@ -1066,14 +1098,6 @@ "markers": "python_version >= '3.7'", "version": "==13.7.0" }, - "setuptools": { - "hashes": [ - "sha256:4c65d4f7891e5b046e9146913b87098144de2ca2128fbc10135b8556a6ddd946", - "sha256:eb03b43f23910c5fd0909cb677ad017cd9531f493d27f8b3f5316ff1fb07390e" - ], - "markers": "python_version >= '3.8'", - "version": "==69.0.0" - }, "smmap": { "hashes": [ "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", @@ -1090,14 +1114,6 @@ "markers": "python_version >= '3.8'", "version": "==5.1.0" }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, "tomli": { "hashes": [ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", @@ -1106,19 +1122,21 @@ "markers": "python_version < '3.11'", "version": "==2.0.1" }, - "typing-extensions": { + "tomlkit": { "hashes": [ - "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", - "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" + "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", + "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" ], - "index": "pypi", - "version": "==4.2.0" + "markers": "python_version >= '3.7'", + "version": "==0.12.3" }, - "wrapt": { + "typing-extensions": { "hashes": [ - "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" ], - "version": "==1.12.1" + "index": "pypi", + "version": "==4.8.0" } } } diff --git a/pyproject.toml b/pyproject.toml index 751cc5a75a..389c24e3d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = "110" -target-version = ['py39'] +target-version = ['py310'] include = '\.pyi?$' extend-exclude = ''' ( From 27a16fe3ca3826ed402f64950e2d81148dac9f18 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:45:09 -0800 Subject: [PATCH 1178/1221] Bump dpy version to 2.3.2 --- Pipfile | 2 +- Pipfile.lock | 40 ++++++++++++++++++++-------------------- bot.py | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Pipfile b/Pipfile index 95c75285b5..a0c9daa37e 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,7 @@ typing-extensions = "==4.8.0" [packages] aiohttp = "==3.9.0" colorama = "==0.4.6" -"discord.py" = "==2.0.1" +"discord.py" = "==2.3.2" emoji = "==2.8.0" isodate = "==0.6.1" motor = "==3.3.2" diff --git a/Pipfile.lock b/Pipfile.lock index 640ea61ff6..d420515b19 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e883a9f5e7c62e52df23b5a710713897b06264d3a35fd4af2251ef78e7ddc82e" + "sha256": "c1d663437fd62a76081e36be69c204bef95a30908d9eac1f3df8863ccbd4d383" }, "pipfile-spec": 6, "requires": {}, @@ -199,7 +199,7 @@ "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" ], - "markers": "python_full_version >= '3.8.0'", + "markers": "python_version >= '3.8'", "version": "==1.16.0" }, "charset-normalizer": { @@ -324,18 +324,18 @@ }, "discord.py": { "hashes": [ - "sha256:309146476e986cb8faf038cd5d604d4b3834ef15c2d34df697ce5064bf5cd779", - "sha256:aeb186348bf011708b085b2715cf92bbb72c692eb4f59c4c0b488130cc4c4b7e" + "sha256:4560f70f2eddba7e83370ecebd237ac09fbb4980dc66507482b0c0e5b8f76b9c", + "sha256:9da4679fc3cb10c64b388284700dc998663e0e57328283bbfcfc2525ec5960a6" ], "index": "pypi", - "version": "==2.0.1" + "version": "==2.3.2" }, "dnspython": { "hashes": [ "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8", "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984" ], - "markers": "python_version < '4.0' and python_full_version >= '3.8.0'", + "markers": "python_version >= '3.8' and python_version < '4.0'", "version": "==2.4.2" }, "emoji": { @@ -410,7 +410,7 @@ "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1", "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e" ], - "markers": "python_full_version >= '3.8.0'", + "markers": "python_version >= '3.8'", "version": "==1.4.0" }, "idna": { @@ -599,7 +599,7 @@ "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a", "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793" ], - "markers": "python_full_version >= '3.8.0'", + "markers": "python_version >= '3.8'", "version": "==10.1.0" }, "pycparser": { @@ -744,7 +744,7 @@ "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" ], - "markers": "python_full_version >= '3.8.0'", + "markers": "python_version >= '3.8'", "version": "==2.1.0" }, "uvloop": { @@ -919,7 +919,7 @@ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==8.1.7" }, "dill": { @@ -935,7 +935,7 @@ "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==4.0.11" }, "gitpython": { @@ -943,7 +943,7 @@ "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4", "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.1.40" }, "isort": { @@ -975,7 +975,7 @@ "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==0.1.2" }, "mypy-extensions": { @@ -991,7 +991,7 @@ "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==23.2" }, "pathspec": { @@ -999,7 +999,7 @@ "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==0.11.2" }, "pbr": { @@ -1015,7 +1015,7 @@ "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==4.0.0" }, "pygments": { @@ -1023,7 +1023,7 @@ "sha256:1b37f1b1e1bff2af52ecaf28cc601e2ef7077000b227a0675da25aef85784bc4", "sha256:e45a0e74bf9c530f564ca81b8952343be986a29f6afe7f5ad95c5f06b7bdf5e8" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==2.17.1" }, "pylint": { @@ -1095,7 +1095,7 @@ "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==13.7.0" }, "smmap": { @@ -1103,7 +1103,7 @@ "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==5.0.1" }, "stevedore": { @@ -1127,7 +1127,7 @@ "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==0.12.3" }, "typing-extensions": { diff --git a/bot.py b/bot.py index 4245ce408b..49787f5005 100644 --- a/bot.py +++ b/bot.py @@ -1793,7 +1793,7 @@ def main(): sys.exit(0) # check discord version - discord_version = "2.0.1" + discord_version = "2.3.2" if discord.__version__ != discord_version: logger.error( "Dependencies are not updated, run pipenv install. discord.py version expected %s, received %s", From 9464c5db97c5ce134a86333879d31ac06fe255c6 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:02:21 -0800 Subject: [PATCH 1179/1221] Fixed compat with MissingRequiredArgument requiring additional param --- bot.py | 7 ++++--- cogs/modmail.py | 7 +++---- cogs/utility.py | 9 ++++----- core/thread.py | 5 +++-- core/utils.py | 10 ++++++++++ 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/bot.py b/bot.py index 49787f5005..3a4cc8a3d8 100644 --- a/bot.py +++ b/bot.py @@ -1746,9 +1746,10 @@ def format_channel_name(self, author, exclude_channel=None, force_null=False): if force_null: name = "null" - name = new_name = ( - "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" - ) + f"-{author.discriminator}" + name = "".join(l for l in name if l not in string.punctuation and l.isprintable()) or "null" + if author.discriminator != "0": + name += f"-{author.discriminator}" + new_name = name counter = 1 existed = set(c.name for c in guild.text_channels if c != exclude_channel) diff --git a/cogs/modmail.py b/cogs/modmail.py index 445015e4de..ac9ab65a58 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -3,7 +3,6 @@ from datetime import datetime, timezone from itertools import zip_longest from typing import Optional, Union, List, Tuple, Literal -from types import SimpleNamespace import discord from discord.ext import commands @@ -1164,7 +1163,7 @@ async def logs(self, ctx, *, user: User = None): if not user: thread = ctx.thread if not thread: - raise commands.MissingRequiredArgument(SimpleNamespace(name="member")) + raise commands.MissingRequiredArgument(DummyParam("user")) user = thread.recipient or await self.bot.get_or_fetch_user(thread.id) default_avatar = "https://cdn.discordapp.com/embed/avatars/0.png" @@ -1839,7 +1838,7 @@ async def block( if thread: user_or_role = thread.recipient elif after is None: - raise commands.MissingRequiredArgument(SimpleNamespace(name="user or role")) + raise commands.MissingRequiredArgument(DummyParam("user or role")) else: raise commands.BadArgument(f'User or role "{after.arg}" not found.') @@ -1919,7 +1918,7 @@ async def unblock(self, ctx, *, user_or_role: Union[User, Role] = None): if thread: user_or_role = thread.recipient else: - raise commands.MissingRequiredArgument(SimpleNamespace(name="user")) + raise commands.MissingRequiredArgument(DummyParam("user or role")) mention = getattr(user_or_role, "mention", f"`{user_or_role.id}`") name = getattr(user_or_role, "name", f"`{user_or_role.id}`") diff --git a/cogs/utility.py b/cogs/utility.py index 579c4a7e4e..126b6f785f 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -11,7 +11,6 @@ from json import JSONDecodeError, loads from subprocess import PIPE from textwrap import indent -from types import SimpleNamespace from typing import Union import discord @@ -30,7 +29,7 @@ UnseenFormatter, getLogger, ) -from core.utils import trigger_typing, truncate +from core.utils import trigger_typing, truncate, DummyParam from core.paginator import EmbedPaginatorSession, MessagePaginatorSession @@ -522,12 +521,12 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): return await ctx.send(embed=embed) if not message: - raise commands.MissingRequiredArgument(SimpleNamespace(name="message")) + raise commands.MissingRequiredArgument(DummyParam("message")) try: activity_type = ActivityType[activity_type] except KeyError: - raise commands.MissingRequiredArgument(SimpleNamespace(name="activity")) + raise commands.MissingRequiredArgument(DummyParam("activity")) activity, _ = await self.set_presence(activity_type=activity_type, activity_message=message) @@ -572,7 +571,7 @@ async def status(self, ctx, *, status_type: str.lower): try: status = Status[status_type] except KeyError: - raise commands.MissingRequiredArgument(SimpleNamespace(name="status")) + raise commands.MissingRequiredArgument(DummyParam("status")) _, status = await self.set_presence(status=status) diff --git a/core/thread.py b/core/thread.py index 09c5df46ee..0222eb0102 100644 --- a/core/thread.py +++ b/core/thread.py @@ -32,6 +32,7 @@ AcceptButton, DenyButton, ConfirmThreadCreationView, + DummyParam, ) logger = getLogger(__name__) @@ -800,7 +801,7 @@ async def note( self, message: discord.Message, persistent=False, thread_creation=False ) -> discord.Message: if not message.content and not message.attachments: - raise MissingRequiredArgument(SimpleNamespace(name="msg")) + raise MissingRequiredArgument(DummyParam("msg")) msg = await self.send( message, @@ -821,7 +822,7 @@ async def reply( ) -> typing.Tuple[typing.List[discord.Message], discord.Message]: """Returns List[user_dm_msg] and thread_channel_msg""" if not message.content and not message.attachments: - raise MissingRequiredArgument(SimpleNamespace(name="msg")) + raise MissingRequiredArgument(DummyParam("msg")) if not any(g.get_member(self.id) for g in self.bot.guilds): return await message.channel.send( embed=discord.Embed( diff --git a/core/utils.py b/core/utils.py index 0e9f090d12..d5940d1c2b 100644 --- a/core/utils.py +++ b/core/utils.py @@ -42,6 +42,7 @@ "AcceptButton", "DenyButton", "ConfirmThreadCreationView", + "DummyParam", ] @@ -588,3 +589,12 @@ class ConfirmThreadCreationView(discord.ui.View): def __init__(self): super().__init__(timeout=20) self.value = None + + +class DummyParam: + """ + A dummy parameter that can be used for MissingRequiredArgument. + """ + def __init__(self, name): + self.name = name + self.displayed_name = name From ef349e87e62851881a82fc652110f4ec4d7307ef Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:05:47 -0800 Subject: [PATCH 1180/1221] black format + update changelog --- CHANGELOG.md | 3 +++ core/utils.py | 1 + 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f79f46c491..03f3e09241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s # [UNRELEASED] +Drops support for Python 3.9. Python 3.10 and Python 3.11 are now the only supported versions. + ### Fixed - `?alias make/create` as aliases to `?alias add`. This improves continuity between the bot and its command structure. ([PR #3195](https://github.com/kyb3r/modmail/pull/3195)) - Loading the blocked list with the `?blocked` command takes a long time when the list is large. ([PR #3242](https://github.com/kyb3r/modmail/pull/3242)) @@ -29,6 +31,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Changed - Repo moved to https://github.com/modmail-dev/modmail. +- Channel name no longer shows `-0` if the user has migrated to the new username system. - Guild icons in embed footers and author urls now have a fixed size of 128. ([PR #3261](https://github.com/modmail-dev/modmail/pull/3261)) - Discord.py internal logging is now enabled by default. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) - The confirm-thread-creation dialog now uses buttons instead of reactions. ([PR #3273](https://github.com/modmail-dev/Modmail/pull/3273)) diff --git a/core/utils.py b/core/utils.py index d5940d1c2b..b50ed3d0fc 100644 --- a/core/utils.py +++ b/core/utils.py @@ -595,6 +595,7 @@ class DummyParam: """ A dummy parameter that can be used for MissingRequiredArgument. """ + def __init__(self, name): self.name = name self.displayed_name = name From 7231a5d48aa8273f063b50a1b2bd1bf8f4c547d1 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:06:25 -0800 Subject: [PATCH 1181/1221] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03f3e09241..d0ca324a29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Drops support for Python 3.9. Python 3.10 and Python 3.11 are now the only suppo ### Internal - Renamed `Bot.log_file_name` to `Bot.log_file_path`. Log files are now created at `temp/logs/modmail.log`. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) - `ConfigManager.get` no longer accepts two positional arguments: the `convert` argument is now keyword-only. +- Various dependencies have been updated to their latest versions. # v4.0.2 From 55f9ba89049506cf21abace145d869422234caea Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:13:35 -0800 Subject: [PATCH 1182/1221] Remove some more discriminator references --- cogs/modmail.py | 12 +++++++----- core/utils.py | 6 +++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index ac9ab65a58..ee27806177 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -729,8 +729,9 @@ def format_log_embeds(self, logs, avatar_url): f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{entry['key']}" ) - username = entry["recipient"]["name"] + "#" - username += entry["recipient"]["discriminator"] + username = entry["recipient"]["name"] + if entry["recipient"]["discriminator"] != "0": + 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) @@ -1855,7 +1856,7 @@ async def block( ) return await ctx.send(embed=embed) - reason = f"by {escape_markdown(ctx.author.name)}#{ctx.author.discriminator}" + reason = f"by {escape_markdown(str(ctx.author))}" if after is not None: if "%" in reason: @@ -2054,11 +2055,12 @@ async def repair(self, ctx): # match username from channel name # username-1234, username-1234_1, username-1234_2 - m = re.match(r"^(.+)-(\d{4})(?:_\d+)?$", ctx.channel.name) + 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), + lambda member: member.name == m.group(1) + and (member.discriminator == "0" or member.discriminator == m.group(2)), ctx.guild.members, ) ) diff --git a/core/utils.py b/core/utils.py index b50ed3d0fc..e25bbf2009 100644 --- a/core/utils.py +++ b/core/utils.py @@ -127,7 +127,11 @@ def format_preview(messages: typing.List[typing.Dict[str, typing.Any]]): continue author = message["author"] content = str(message["content"]).replace("\n", " ") - name = author["name"] + "#" + str(author["discriminator"]) + + name = author["name"] + discriminator = str(author["discriminator"]) + if discriminator != "0": + name += "#" + discriminator prefix = "[M]" if author["mod"] else "[R]" out += truncate(f"`{prefix} {name}:` {content}", max=75) + "\n" From 5482e94374da73161b3ba33df25de089f2950cc3 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:19:31 -0800 Subject: [PATCH 1183/1221] Remove pkg_resources and replaced with packaging, updated requirements.txt --- Pipfile | 1 + Pipfile.lock | 198 ++++++++++++++++++++++++++--------------------- bot.py | 6 +- cogs/plugins.py | 6 +- cogs/utility.py | 11 +-- requirements.txt | 59 +++++++------- 6 files changed, 152 insertions(+), 129 deletions(-) diff --git a/Pipfile b/Pipfile index a0c9daa37e..98e60935be 100644 --- a/Pipfile +++ b/Pipfile @@ -17,6 +17,7 @@ emoji = "==2.8.0" isodate = "==0.6.1" motor = "==3.3.2" natural = "==0.2.0" # Why is this needed? +packaging = "==23.2" parsedatetime = "==2.6" pymongo = {extras = ["srv"], version = "*"} # Required by motor python-dateutil = "==2.8.2" diff --git a/Pipfile.lock b/Pipfile.lock index d420515b19..2892217178 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c1d663437fd62a76081e36be69c204bef95a30908d9eac1f3df8863ccbd4d383" + "sha256": "2744146f249e9fd303c9cba7df1fff03a597cf8a141075c2d4d51ff80efbee7b" }, "pipfile-spec": 6, "requires": {}, @@ -534,6 +534,14 @@ "index": "pypi", "version": "==0.2.0" }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "index": "pypi", + "version": "==23.2" + }, "parsedatetime": { "hashes": [ "sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455", @@ -794,83 +802,99 @@ }, "yarl": { "hashes": [ - "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571", - "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3", - "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3", - "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c", - "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7", - "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04", - "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191", - "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea", - "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4", - "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4", - "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095", - "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e", - "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74", - "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef", - "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33", - "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde", - "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45", - "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf", - "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b", - "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac", - "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0", - "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528", - "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716", - "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb", - "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18", - "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72", - "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6", - "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582", - "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5", - "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368", - "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc", - "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9", - "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be", - "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a", - "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80", - "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8", - "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6", - "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417", - "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574", - "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59", - "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608", - "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82", - "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1", - "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3", - "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d", - "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8", - "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc", - "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac", - "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8", - "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955", - "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0", - "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367", - "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb", - "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a", - "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623", - "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2", - "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6", - "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7", - "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4", - "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051", - "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938", - "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8", - "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9", - "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3", - "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5", - "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9", - "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333", - "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185", - "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3", - "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560", - "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b", - "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7", - "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78", - "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7" + "sha256:09c19e5f4404574fcfb736efecf75844ffe8610606f3fccc35a1515b8b6712c4", + "sha256:0ab5baaea8450f4a3e241ef17e3d129b2143e38a685036b075976b9c415ea3eb", + "sha256:0d155a092bf0ebf4a9f6f3b7a650dc5d9a5bbb585ef83a52ed36ba46f55cc39d", + "sha256:126638ab961633f0940a06e1c9d59919003ef212a15869708dcb7305f91a6732", + "sha256:1a0a4f3aaa18580038cfa52a7183c8ffbbe7d727fe581300817efc1e96d1b0e9", + "sha256:1d93461e2cf76c4796355494f15ffcb50a3c198cc2d601ad8d6a96219a10c363", + "sha256:26a1a8443091c7fbc17b84a0d9f38de34b8423b459fb853e6c8cdfab0eacf613", + "sha256:271d63396460b6607b588555ea27a1a02b717ca2e3f2cf53bdde4013d7790929", + "sha256:28a108cb92ce6cf867690a962372996ca332d8cda0210c5ad487fe996e76b8bb", + "sha256:29beac86f33d6c7ab1d79bd0213aa7aed2d2f555386856bb3056d5fdd9dab279", + "sha256:2c757f64afe53a422e45e3e399e1e3cf82b7a2f244796ce80d8ca53e16a49b9f", + "sha256:2dad8166d41ebd1f76ce107cf6a31e39801aee3844a54a90af23278b072f1ccf", + "sha256:2dc72e891672343b99db6d497024bf8b985537ad6c393359dc5227ef653b2f17", + "sha256:2f3c8822bc8fb4a347a192dd6a28a25d7f0ea3262e826d7d4ef9cc99cd06d07e", + "sha256:32435d134414e01d937cd9d6cc56e8413a8d4741dea36af5840c7750f04d16ab", + "sha256:3cfa4dbe17b2e6fca1414e9c3bcc216f6930cb18ea7646e7d0d52792ac196808", + "sha256:3d5434b34100b504aabae75f0622ebb85defffe7b64ad8f52b8b30ec6ef6e4b9", + "sha256:4003f380dac50328c85e85416aca6985536812c082387255c35292cb4b41707e", + "sha256:44e91a669c43f03964f672c5a234ae0d7a4d49c9b85d1baa93dec28afa28ffbd", + "sha256:4a14907b597ec55740f63e52d7fee0e9ee09d5b9d57a4f399a7423268e457b57", + "sha256:4ce77d289f8d40905c054b63f29851ecbfd026ef4ba5c371a158cfe6f623663e", + "sha256:4d6d74a97e898c1c2df80339aa423234ad9ea2052f66366cef1e80448798c13d", + "sha256:51382c72dd5377861b573bd55dcf680df54cea84147c8648b15ac507fbef984d", + "sha256:525cd69eff44833b01f8ef39aa33a9cc53a99ff7f9d76a6ef6a9fb758f54d0ff", + "sha256:53ec65f7eee8655bebb1f6f1607760d123c3c115a324b443df4f916383482a67", + "sha256:5f74b015c99a5eac5ae589de27a1201418a5d9d460e89ccb3366015c6153e60a", + "sha256:6280353940f7e5e2efaaabd686193e61351e966cc02f401761c4d87f48c89ea4", + "sha256:632c7aeb99df718765adf58eacb9acb9cbc555e075da849c1378ef4d18bf536a", + "sha256:6465d36381af057d0fab4e0f24ef0e80ba61f03fe43e6eeccbe0056e74aadc70", + "sha256:66a6dbf6ca7d2db03cc61cafe1ee6be838ce0fbc97781881a22a58a7c5efef42", + "sha256:6d350388ba1129bc867c6af1cd17da2b197dff0d2801036d2d7d83c2d771a682", + "sha256:7217234b10c64b52cc39a8d82550342ae2e45be34f5bff02b890b8c452eb48d7", + "sha256:721ee3fc292f0d069a04016ef2c3a25595d48c5b8ddc6029be46f6158d129c92", + "sha256:72a57b41a0920b9a220125081c1e191b88a4cdec13bf9d0649e382a822705c65", + "sha256:73cc83f918b69110813a7d95024266072d987b903a623ecae673d1e71579d566", + "sha256:778df71c8d0c8c9f1b378624b26431ca80041660d7be7c3f724b2c7a6e65d0d6", + "sha256:79e1df60f7c2b148722fb6cafebffe1acd95fd8b5fd77795f56247edaf326752", + "sha256:7c86d0d0919952d05df880a1889a4f0aeb6868e98961c090e335671dea5c0361", + "sha256:7eaf13af79950142ab2bbb8362f8d8d935be9aaf8df1df89c86c3231e4ff238a", + "sha256:828235a2a169160ee73a2fcfb8a000709edf09d7511fccf203465c3d5acc59e4", + "sha256:8535e111a064f3bdd94c0ed443105934d6f005adad68dd13ce50a488a0ad1bf3", + "sha256:88d2c3cc4b2f46d1ba73d81c51ec0e486f59cc51165ea4f789677f91a303a9a7", + "sha256:8a2538806be846ea25e90c28786136932ec385c7ff3bc1148e45125984783dc6", + "sha256:8dab30b21bd6fb17c3f4684868c7e6a9e8468078db00f599fb1c14e324b10fca", + "sha256:8f18a7832ff85dfcd77871fe677b169b1bc60c021978c90c3bb14f727596e0ae", + "sha256:946db4511b2d815979d733ac6a961f47e20a29c297be0d55b6d4b77ee4b298f6", + "sha256:96758e56dceb8a70f8a5cff1e452daaeff07d1cc9f11e9b0c951330f0a2396a7", + "sha256:9a172c3d5447b7da1680a1a2d6ecdf6f87a319d21d52729f45ec938a7006d5d8", + "sha256:9a5211de242754b5e612557bca701f39f8b1a9408dff73c6db623f22d20f470e", + "sha256:9df9a0d4c5624790a0dea2e02e3b1b3c69aed14bcb8650e19606d9df3719e87d", + "sha256:aa4643635f26052401750bd54db911b6342eb1a9ac3e74f0f8b58a25d61dfe41", + "sha256:aed37db837ecb5962469fad448aaae0f0ee94ffce2062cf2eb9aed13328b5196", + "sha256:af52725c7c39b0ee655befbbab5b9a1b209e01bb39128dce0db226a10014aacc", + "sha256:b0b8c06afcf2bac5a50b37f64efbde978b7f9dc88842ce9729c020dc71fae4ce", + "sha256:b61e64b06c3640feab73fa4ff9cb64bd8182de52e5dc13038e01cfe674ebc321", + "sha256:b7831566595fe88ba17ea80e4b61c0eb599f84c85acaa14bf04dd90319a45b90", + "sha256:b8bc5b87a65a4e64bc83385c05145ea901b613d0d3a434d434b55511b6ab0067", + "sha256:b8d51817cf4b8d545963ec65ff06c1b92e5765aa98831678d0e2240b6e9fd281", + "sha256:b9f9cafaf031c34d95c1528c16b2fa07b710e6056b3c4e2e34e9317072da5d1a", + "sha256:bb72d2a94481e7dc7a0c522673db288f31849800d6ce2435317376a345728225", + "sha256:c25ec06e4241e162f5d1f57c370f4078797ade95c9208bd0c60f484834f09c96", + "sha256:c405d482c320a88ab53dcbd98d6d6f32ada074f2d965d6e9bf2d823158fa97de", + "sha256:c4472fe53ebf541113e533971bd8c32728debc4c6d8cc177f2bff31d011ec17e", + "sha256:c4b1efb11a8acd13246ffb0bee888dd0e8eb057f8bf30112e3e21e421eb82d4a", + "sha256:c5f3faeb8100a43adf3e7925d556801d14b5816a0ac9e75e22948e787feec642", + "sha256:c6f034386e5550b5dc8ded90b5e2ff7db21f0f5c7de37b6efc5dac046eb19c10", + "sha256:c99ddaddb2fbe04953b84d1651149a0d85214780e4d0ee824e610ab549d98d92", + "sha256:ca6b66f69e30f6e180d52f14d91ac854b8119553b524e0e28d5291a724f0f423", + "sha256:cccdc02e46d2bd7cb5f38f8cc3d9db0d24951abd082b2f242c9e9f59c0ab2af3", + "sha256:cd49a908cb6d387fc26acee8b7d9fcc9bbf8e1aca890c0b2fdfd706057546080", + "sha256:cf7a4e8de7f1092829caef66fd90eaf3710bc5efd322a816d5677b7664893c93", + "sha256:cfd77e8e5cafba3fb584e0f4b935a59216f352b73d4987be3af51f43a862c403", + "sha256:d34c4f80956227f2686ddea5b3585e109c2733e2d4ef12eb1b8b4e84f09a2ab6", + "sha256:d61a0ca95503867d4d627517bcfdc28a8468c3f1b0b06c626f30dd759d3999fd", + "sha256:d81657b23e0edb84b37167e98aefb04ae16cbc5352770057893bd222cdc6e45f", + "sha256:d92d897cb4b4bf915fbeb5e604c7911021a8456f0964f3b8ebbe7f9188b9eabb", + "sha256:dd318e6b75ca80bff0b22b302f83a8ee41c62b8ac662ddb49f67ec97e799885d", + "sha256:dd952b9c64f3b21aedd09b8fe958e4931864dba69926d8a90c90d36ac4e28c9a", + "sha256:e0e7e83f31e23c5d00ff618045ddc5e916f9e613d33c5a5823bc0b0a0feb522f", + "sha256:e0f17d1df951336a02afc8270c03c0c6e60d1f9996fcbd43a4ce6be81de0bd9d", + "sha256:e2a16ef5fa2382af83bef4a18c1b3bcb4284c4732906aa69422cf09df9c59f1f", + "sha256:e36021db54b8a0475805acc1d6c4bca5d9f52c3825ad29ae2d398a9d530ddb88", + "sha256:e73db54c967eb75037c178a54445c5a4e7461b5203b27c45ef656a81787c0c1b", + "sha256:e741bd48e6a417bdfbae02e088f60018286d6c141639359fb8df017a3b69415a", + "sha256:f7271d6bd8838c49ba8ae647fc06469137e1c161a7ef97d778b72904d9b68696", + "sha256:fc391e3941045fd0987c77484b2799adffd08e4b6735c4ee5f054366a2e1551d", + "sha256:fc94441bcf9cb8c59f51f23193316afefbf3ff858460cb47b5758bf66a14d130", + "sha256:fe34befb8c765b8ce562f0200afda3578f8abb159c76de3ab354c80b72244c41", + "sha256:fe8080b4f25dfc44a86bedd14bc4f9d469dfc6456e6f3c5d9077e81a5fedfba7", + "sha256:ff34cb09a332832d1cf38acd0f604c068665192c6107a439a92abfd8acf90fe2" ], "markers": "python_version >= '3.7'", - "version": "==1.9.2" + "version": "==1.9.3" } }, "develop": { @@ -919,7 +943,7 @@ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==8.1.7" }, "dill": { @@ -935,7 +959,7 @@ "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==4.0.11" }, "gitpython": { @@ -943,7 +967,7 @@ "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4", "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==3.1.40" }, "isort": { @@ -975,7 +999,7 @@ "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.1.2" }, "mypy-extensions": { @@ -991,7 +1015,7 @@ "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], - "markers": "python_full_version >= '3.7.0'", + "index": "pypi", "version": "==23.2" }, "pathspec": { @@ -999,7 +1023,7 @@ "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.11.2" }, "pbr": { @@ -1015,7 +1039,7 @@ "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==4.0.0" }, "pygments": { @@ -1023,7 +1047,7 @@ "sha256:1b37f1b1e1bff2af52ecaf28cc601e2ef7077000b227a0675da25aef85784bc4", "sha256:e45a0e74bf9c530f564ca81b8952343be986a29f6afe7f5ad95c5f06b7bdf5e8" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==2.17.1" }, "pylint": { @@ -1095,7 +1119,7 @@ "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==13.7.0" }, "smmap": { @@ -1103,7 +1127,7 @@ "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==5.0.1" }, "stevedore": { @@ -1127,7 +1151,7 @@ "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.12.3" }, "typing-extensions": { diff --git a/bot.py b/bot.py index 3a4cc8a3d8..84ddf653a2 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ from discord.ext import commands, tasks from discord.ext.commands.view import StringView from emoji import is_emoji -from pkg_resources import parse_version +from packaging.version import Version try: @@ -186,7 +186,7 @@ async def load_extensions(self): @property def version(self): - return parse_version(__version__) + return Version(__version__) @property def api(self) -> ApiClient: @@ -1586,7 +1586,7 @@ async def autoupdate(self): changelog = await Changelog.from_url(self) latest = changelog.latest_version - if self.version < parse_version(latest.version): + if self.version < Version(latest.version): error = None data = {} try: diff --git a/cogs/plugins.py b/cogs/plugins.py index d5787956f1..fc6e09f05d 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -16,7 +16,7 @@ import discord from discord.ext import commands -from pkg_resources import parse_version +from packaging.version import Version from core import checks from core.models import PermissionLevel, getLogger @@ -300,7 +300,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 < Version(required_version): embed = discord.Embed( description="Your bot's version is too low. " f"This plugin requires version `{required_version}`.", @@ -688,7 +688,7 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N embed.set_footer(text="This plugin is currently loaded.") else: required_version = details.get("bot_version", False) - if required_version and self.bot.version < parse_version(required_version): + if required_version and self.bot.version < Version(required_version): embed.set_footer( text="Your bot is unable to install this plugin, " f"minimum required version is v{required_version}." diff --git a/cogs/utility.py b/cogs/utility.py index 126b6f785f..b892ffc01b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -14,11 +14,12 @@ from typing import Union import discord -from aiohttp import ClientResponseError from discord.enums import ActivityType, Status from discord.ext import commands, tasks from discord.ext.commands.view import StringView -from pkg_resources import parse_version + +from aiohttp import ClientResponseError +from packaging.version import Version from core import checks, utils from core.changelog import Changelog @@ -341,9 +342,9 @@ async def about(self, ctx): latest = changelog.latest_version if self.bot.version.is_prerelease: - stable = next(filter(lambda v: not parse_version(v.version).is_prerelease, changelog.versions)) + stable = next(filter(lambda v: not Version(v.version).is_prerelease, changelog.versions)) footer = f"You are on the prerelease version • the latest version is v{stable.version}." - elif self.bot.version < parse_version(latest.version): + elif self.bot.version < Version(latest.version): footer = f"A newer version is available v{latest.version}." else: footer = "You are up to date with the latest version." @@ -1930,7 +1931,7 @@ async def update(self, ctx, *, flag: str = ""): "(https://github.com/modmail-dev/modmail/blob/master/bot.py#L1)" ) - if self.bot.version >= parse_version(latest.version) and flag.lower() != "force": + if self.bot.version >= Version(latest.version) and flag.lower() != "force": embed = discord.Embed(title="Already up to date", description=desc, color=self.bot.main_color) data = await self.bot.api.get_user_info() diff --git a/requirements.txt b/requirements.txt index 313426315e..3c712a43a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,40 +1,37 @@ -# -# These requirements were autogenerated by pipenv -# To regenerate from the project's Pipfile, run: -# -# pipenv lock --requirements -# - -i https://pypi.org/simple -aiohttp==3.8.1 -aiosignal==1.2.0; python_version >= '3.6' -async-timeout==4.0.2; python_version >= '3.6' -attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -cairocffi==1.3.0; python_version >= '3.7' -cairosvg==2.5.2 -cffi==1.15.0 -charset-normalizer==2.0.12; python_version >= '3.5' -colorama==0.4.4 -cssselect2==0.6.0; python_version >= '3.7' +aiohttp==3.9.0 +aiosignal==1.3.1; python_version >= '3.7' +async-timeout==4.0.3; python_version < '3.11' +attrs==23.1.0; python_version >= '3.7' +cairocffi==1.6.1; python_version >= '3.7' +cairosvg==2.7.1; python_version >= '3.5' +certifi==2023.11.17; python_version >= '3.6' +cffi==1.16.0; python_version >= '3.8' +charset-normalizer==3.3.2; python_version >= '3.7' +colorama==0.4.6 +cssselect2==0.7.0; python_version >= '3.7' defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -dnspython==2.2.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -emoji==1.7.0 -frozenlist==1.3.0; python_version >= '3.7' -discord.py==2.0.1 -idna==3.3; python_version >= '3.5' +discord.py==2.3.2 +dnspython==2.4.2; python_version >= '3.8' and python_version < '4.0' +emoji==2.8.0 +frozenlist==1.4.0; python_version >= '3.8' +idna==3.4; python_version >= '3.5' isodate==0.6.1 -lottie[pdf]==0.6.11 -motor==2.5.1 -multidict==6.0.2; python_version >= '3.7' +lottie[pdf]==0.7.0 +motor==3.3.2 +multidict==6.0.4; python_version >= '3.7' natural==0.2.0 +packaging==23.2 parsedatetime==2.6 -pillow==9.1.0; python_version >= '3.7' +pillow==10.1.0; python_version >= '3.8' pycparser==2.21 -pymongo==3.12.3 +pymongo[srv]==4.6.0 python-dateutil==2.8.2 -python-dotenv==0.20.0 +python-dotenv==1.0.0 +requests==2.31.0 six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -tinycss2==1.1.1; python_version >= '3.6' -uvloop==0.16.0; sys_platform != 'win32' +tinycss2==1.2.1; python_version >= '3.7' +urllib3==2.1.0; python_version >= '3.8' +uvloop==0.19.0; sys_platform != 'win32' webencodings==0.5.1 -yarl==1.7.2; python_version >= '3.6' +yarl==1.9.3; python_version >= '3.7' From 71e6e8737673b8c9d9aa45fa313a6b23e2c381cd Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:28:01 -0800 Subject: [PATCH 1184/1221] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0ca324a29..6512a29f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Drops support for Python 3.9. Python 3.10 and Python 3.11 are now the only suppo - `STREAM_LOG_FORMAT` and `FILE_LOG_FORMAT` environment variable to set the log format of the stream and file handlers respectively. Possible options are `json` and `plain` (default). ([PR #3305](https://github.com/modmail-dev/Modmail/pull/3305)) - `LOG_EXPIRATION` environment variable to set the expiration time of logs. ([PR #3257](https://github.com/modmail-dev/Modmail/pull/3257)) - New registry plugins: [`autoreact`](https://github.com/martinbndr/kyb3r-modmail-plugins/tree/master/autoreact) and [`rename`](https://github.com/Nicklaus-s/modmail-plugins/tree/main/rename). +- Improved join/leave message for multiple servers. ### Changed - Repo moved to https://github.com/modmail-dev/modmail. From 048a9d2b823591729ff4e1ed4e3fad9368ee9064 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:30:36 -0800 Subject: [PATCH 1185/1221] Black reformat code --- core/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/utils.py b/core/utils.py index 8cc91d8130..9f9f572f5a 100644 --- a/core/utils.py +++ b/core/utils.py @@ -154,7 +154,7 @@ def is_image_url(url: str, **kwargs) -> str: """ try: result = parse.urlparse(url) - if result.netloc == 'gyazo.com' and result.scheme in ['http', 'https']: + if result.netloc == "gyazo.com" and result.scheme in ["http", "https"]: # gyazo support url = re.sub( r"(https?://)((?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|%[0-9a-fA-F][0-9a-fA-F])+)", From 3af8dfa4722871b1a20b565fbd92d6669e7fc386 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 21 Nov 2023 07:16:16 -0800 Subject: [PATCH 1186/1221] Fixed #3315: gif stickers are now rendered correctly, allow bare sticker reply/note --- CHANGELOG.md | 2 ++ core/thread.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6512a29f5f..01a03b35b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s Drops support for Python 3.9. Python 3.10 and Python 3.11 are now the only supported versions. ### Fixed +- GIF stickers causes the bot to crash. - `?alias make/create` as aliases to `?alias add`. This improves continuity between the bot and its command structure. ([PR #3195](https://github.com/kyb3r/modmail/pull/3195)) - Loading the blocked list with the `?blocked` command takes a long time when the list is large. ([PR #3242](https://github.com/kyb3r/modmail/pull/3242)) - Reply not being forwarded from DM. (PR [#3239](https://github.com/modmail-dev/modmail/pull/3239)) @@ -33,6 +34,7 @@ Drops support for Python 3.9. Python 3.10 and Python 3.11 are now the only suppo ### Changed - Repo moved to https://github.com/modmail-dev/modmail. - Channel name no longer shows `-0` if the user has migrated to the new username system. +- `?note` and `?reply` now allows you to send a sticker without any message. - Guild icons in embed footers and author urls now have a fixed size of 128. ([PR #3261](https://github.com/modmail-dev/modmail/pull/3261)) - Discord.py internal logging is now enabled by default. ([PR #3216](https://github.com/modmail-dev/Modmail/pull/3216)) - The confirm-thread-creation dialog now uses buttons instead of reactions. ([PR #3273](https://github.com/modmail-dev/Modmail/pull/3273)) diff --git a/core/thread.py b/core/thread.py index 0222eb0102..646c98a604 100644 --- a/core/thread.py +++ b/core/thread.py @@ -800,7 +800,7 @@ async def edit_dm_message(self, message: discord.Message, content: str) -> None: async def note( self, message: discord.Message, persistent=False, thread_creation=False ) -> discord.Message: - if not message.content and not message.attachments: + if not message.content and not message.attachments and not message.stickers: raise MissingRequiredArgument(DummyParam("msg")) msg = await self.send( @@ -821,7 +821,7 @@ async def reply( self, message: discord.Message, anonymous: bool = False, plain: bool = False ) -> typing.Tuple[typing.List[discord.Message], discord.Message]: """Returns List[user_dm_msg] and thread_channel_msg""" - if not message.content and not message.attachments: + if not message.content and not message.attachments and not message.stickers: raise MissingRequiredArgument(DummyParam("msg")) if not any(g.get_member(self.id) for g in self.bot.guilds): return await message.channel.send( @@ -1020,8 +1020,14 @@ def lottie_to_png(data): return stream.read() for i in message.stickers: - if i.format in (discord.StickerFormatType.png, discord.StickerFormatType.apng): - images.append((i.url, i.name, True)) + if i.format in ( + discord.StickerFormatType.png, + discord.StickerFormatType.apng, + discord.StickerFormatType.gif, + ): + images.append( + (f"https://media.discordapp.net/stickers/{i.id}.{i.format.file_extension}", i.name, True) + ) elif i.format == discord.StickerFormatType.lottie: # save the json lottie representation try: From 35194d81b18883f5d03a8a6098a6aac1e592bba9 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 21 Nov 2023 07:18:31 -0800 Subject: [PATCH 1187/1221] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a03b35b5..0ef7e87d6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s Drops support for Python 3.9. Python 3.10 and Python 3.11 are now the only supported versions. ### Fixed -- GIF stickers causes the bot to crash. +- GIF stickers no longer cause the bot to crash. - `?alias make/create` as aliases to `?alias add`. This improves continuity between the bot and its command structure. ([PR #3195](https://github.com/kyb3r/modmail/pull/3195)) - Loading the blocked list with the `?blocked` command takes a long time when the list is large. ([PR #3242](https://github.com/kyb3r/modmail/pull/3242)) - Reply not being forwarded from DM. (PR [#3239](https://github.com/modmail-dev/modmail/pull/3239)) From 62ae63761f492f59300d6694b5ea202bdc71edc3 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 21 Nov 2023 18:09:03 -0800 Subject: [PATCH 1188/1221] Use discord[speed] extra --- Pipfile | 2 +- Pipfile.lock | 246 ++++++++++++++++++++++++++++++++++++++++++++--- requirements.txt | 28 +++--- 3 files changed, 251 insertions(+), 25 deletions(-) diff --git a/Pipfile b/Pipfile index 98e60935be..21205b36f5 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,7 @@ typing-extensions = "==4.8.0" [packages] aiohttp = "==3.9.0" colorama = "==0.4.6" -"discord.py" = "==2.3.2" +"discord.py" = {version = "==2.3.2", extras = ["speed"]} emoji = "==2.8.0" isodate = "==0.6.1" motor = "==3.3.2" diff --git a/Pipfile.lock b/Pipfile.lock index 2892217178..5f07c7b131 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2744146f249e9fd303c9cba7df1fff03a597cf8a141075c2d4d51ff80efbee7b" + "sha256": "7fee393ea9ea4c0b923033f0da0fdc590ba3f75c6072812062cdc458b84bf9ae" }, "pipfile-spec": 6, "requires": {}, @@ -14,6 +14,13 @@ ] }, "default": { + "aiodns": { + "hashes": [ + "sha256:1073eac48185f7a4150cad7f96a5192d6911f12b4fb894de80a088508c9b3a99", + "sha256:a387b63da4ced6aad35b1dda2d09620ad608a1c7c0fb71efa07ebb4cd511928d" + ], + "version": "==3.1.1" + }, "aiohttp": { "hashes": [ "sha256:05857848da443c8c12110d99285d499b4e84d59918a21132e45c3f0804876994", @@ -94,6 +101,7 @@ "sha256:f9e09a1c83521d770d170b3801eea19b89f41ccaa61d53026ed111cb6f088887" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.9.0" }, "aiosignal": { @@ -120,6 +128,94 @@ "markers": "python_version >= '3.7'", "version": "==23.1.0" }, + "brotli": { + "hashes": [ + "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", + "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", + "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354", + "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a", + "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", + "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c", + "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", + "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", + "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", + "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", + "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438", + "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", + "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b", + "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b", + "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68", + "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d", + "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", + "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", + "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", + "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", + "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", + "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", + "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", + "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112", + "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", + "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", + "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", + "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95", + "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", + "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914", + "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", + "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", + "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", + "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", + "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", + "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", + "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", + "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", + "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", + "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", + "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", + "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", + "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", + "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", + "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", + "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", + "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74", + "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60", + "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", + "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1", + "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", + "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", + "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", + "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", + "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460", + "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", + "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", + "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", + "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", + "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", + "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", + "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", + "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", + "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", + "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", + "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", + "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", + "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579", + "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84", + "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", + "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", + "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", + "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", + "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", + "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2", + "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", + "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", + "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643", + "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", + "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985", + "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", + "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2", + "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064" + ], + "version": "==1.1.0" + }, "cairocffi": { "hashes": [ "sha256:78e6bbe47357640c453d0be929fa49cd05cce2e1286f3d2a1ca9cbda7efdb8b7", @@ -295,7 +391,7 @@ "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.3.2" }, "colorama": { @@ -304,6 +400,7 @@ "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", "version": "==0.4.6" }, "cssselect2": { @@ -323,11 +420,14 @@ "version": "==0.7.1" }, "discord.py": { + "extras": [ + "speed" + ], "hashes": [ "sha256:4560f70f2eddba7e83370ecebd237ac09fbb4980dc66507482b0c0e5b8f76b9c", "sha256:9da4679fc3cb10c64b388284700dc998663e0e57328283bbfcfc2525ec5960a6" ], - "index": "pypi", + "markers": "python_full_version >= '3.8.0'", "version": "==2.3.2" }, "dnspython": { @@ -344,6 +444,7 @@ "sha256:a8468fd836b7ecb6d1eac054c9a591701ce0ccd6c6f7779ad71b66f76664df90" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.0" }, "frozenlist": { @@ -436,7 +537,7 @@ "hashes": [ "sha256:a3242f8ba37051fbdd7503ecd168203a08e4af26f17be2ecca08a64af1e7d3c1" ], - "index": "pypi", + "markers": "python_version >= '3'", "version": "==0.7.0" }, "motor": { @@ -445,6 +546,7 @@ "sha256:d2fc38de15f1c8058f389c1a44a4d4105c0405c48c061cd492a654496f7bc26a" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.3.2" }, "multidict": { @@ -534,12 +636,68 @@ "index": "pypi", "version": "==0.2.0" }, + "orjson": { + "hashes": [ + "sha256:06ad5543217e0e46fd7ab7ea45d506c76f878b87b1b4e369006bdb01acc05a83", + "sha256:0a73160e823151f33cdc05fe2cea557c5ef12fdf276ce29bb4f1c571c8368a60", + "sha256:1234dc92d011d3554d929b6cf058ac4a24d188d97be5e04355f1b9223e98bbe9", + "sha256:1d0dc4310da8b5f6415949bd5ef937e60aeb0eb6b16f95041b5e43e6200821fb", + "sha256:2a11b4b1a8415f105d989876a19b173f6cdc89ca13855ccc67c18efbd7cbd1f8", + "sha256:2e2ecd1d349e62e3960695214f40939bbfdcaeaaa62ccc638f8e651cf0970e5f", + "sha256:3a2ce5ea4f71681623f04e2b7dadede3c7435dfb5e5e2d1d0ec25b35530e277b", + "sha256:3e892621434392199efb54e69edfff9f699f6cc36dd9553c5bf796058b14b20d", + "sha256:3fb205ab52a2e30354640780ce4587157a9563a68c9beaf52153e1cea9aa0921", + "sha256:4689270c35d4bb3102e103ac43c3f0b76b169760aff8bcf2d401a3e0e58cdb7f", + "sha256:49f8ad582da6e8d2cf663c4ba5bf9f83cc052570a3a767487fec6af839b0e777", + "sha256:4bd176f528a8151a6efc5359b853ba3cc0e82d4cd1fab9c1300c5d957dc8f48c", + "sha256:4cf7837c3b11a2dfb589f8530b3cff2bd0307ace4c301e8997e95c7468c1378e", + "sha256:4fd72fab7bddce46c6826994ce1e7de145ae1e9e106ebb8eb9ce1393ca01444d", + "sha256:5148bab4d71f58948c7c39d12b14a9005b6ab35a0bdf317a8ade9a9e4d9d0bd5", + "sha256:5869e8e130e99687d9e4be835116c4ebd83ca92e52e55810962446d841aba8de", + "sha256:602a8001bdf60e1a7d544be29c82560a7b49319a0b31d62586548835bbe2c862", + "sha256:61804231099214e2f84998316f3238c4c2c4aaec302df12b21a64d72e2a135c7", + "sha256:666c6fdcaac1f13eb982b649e1c311c08d7097cbda24f32612dae43648d8db8d", + "sha256:674eb520f02422546c40401f4efaf8207b5e29e420c17051cddf6c02783ff5ca", + "sha256:7ec960b1b942ee3c69323b8721df2a3ce28ff40e7ca47873ae35bfafeb4555ca", + "sha256:7f433be3b3f4c66016d5a20e5b4444ef833a1f802ced13a2d852c637f69729c1", + "sha256:7f8fb7f5ecf4f6355683ac6881fd64b5bb2b8a60e3ccde6ff799e48791d8f864", + "sha256:81a3a3a72c9811b56adf8bcc829b010163bb2fc308877e50e9910c9357e78521", + "sha256:858379cbb08d84fe7583231077d9a36a1a20eb72f8c9076a45df8b083724ad1d", + "sha256:8b9ba0ccd5a7f4219e67fbbe25e6b4a46ceef783c42af7dbc1da548eb28b6531", + "sha256:92af0d00091e744587221e79f68d617b432425a7e59328ca4c496f774a356071", + "sha256:9ebbdbd6a046c304b1845e96fbcc5559cd296b4dfd3ad2509e33c4d9ce07d6a1", + "sha256:9edd2856611e5050004f4722922b7b1cd6268da34102667bd49d2a2b18bafb81", + "sha256:a353bf1f565ed27ba71a419b2cd3db9d6151da426b61b289b6ba1422a702e643", + "sha256:b5b7d4a44cc0e6ff98da5d56cde794385bdd212a86563ac321ca64d7f80c80d1", + "sha256:b90f340cb6397ec7a854157fac03f0c82b744abdd1c0941a024c3c29d1340aff", + "sha256:c18a4da2f50050a03d1da5317388ef84a16013302a5281d6f64e4a3f406aabc4", + "sha256:c338ed69ad0b8f8f8920c13f529889fe0771abbb46550013e3c3d01e5174deef", + "sha256:c5a02360e73e7208a872bf65a7554c9f15df5fe063dc047f79738998b0506a14", + "sha256:c62b6fa2961a1dcc51ebe88771be5319a93fd89bd247c9ddf732bc250507bc2b", + "sha256:c812312847867b6335cfb264772f2a7e85b3b502d3a6b0586aa35e1858528ab1", + "sha256:c943b35ecdf7123b2d81d225397efddf0bce2e81db2f3ae633ead38e85cd5ade", + "sha256:ce0a29c28dfb8eccd0f16219360530bc3cfdf6bf70ca384dacd36e6c650ef8e8", + "sha256:cf80b550092cc480a0cbd0750e8189247ff45457e5a023305f7ef1bcec811616", + "sha256:cff7570d492bcf4b64cc862a6e2fb77edd5e5748ad715f487628f102815165e9", + "sha256:d2c1e559d96a7f94a4f581e2a32d6d610df5840881a8cba8f25e446f4d792df3", + "sha256:deeb3922a7a804755bbe6b5be9b312e746137a03600f488290318936c1a2d4dc", + "sha256:e28a50b5be854e18d54f75ef1bb13e1abf4bc650ab9d635e4258c58e71eb6ad5", + "sha256:e99c625b8c95d7741fe057585176b1b8783d46ed4b8932cf98ee145c4facf499", + "sha256:ec6f18f96b47299c11203edfbdc34e1b69085070d9a3d1f302810cc23ad36bf3", + "sha256:ed8bc367f725dfc5cabeed1ae079d00369900231fbb5a5280cf0736c30e2adf7", + "sha256:ee5926746232f627a3be1cc175b2cfad24d0170d520361f4ce3fa2fd83f09e1d", + "sha256:f295efcd47b6124b01255d1491f9e46f17ef40d3d7eabf7364099e463fb45f0f", + "sha256:fb0b361d73f6b8eeceba47cd37070b5e6c9de5beaeaa63a1cb35c7e1a73ef088" + ], + "version": "==3.9.10" + }, "packaging": { "hashes": [ "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==23.2" }, "parsedatetime": { @@ -610,6 +768,63 @@ "markers": "python_version >= '3.8'", "version": "==10.1.0" }, + "pycares": { + "hashes": [ + "sha256:112a4979c695b1c86f6782163d7dec58d57a3b9510536dcf4826550f9053dd9a", + "sha256:1168a48a834813aa80f412be2df4abaf630528a58d15c704857448b20b1675c0", + "sha256:21a5a0468861ec7df7befa69050f952da13db5427ae41ffe4713bc96291d1d95", + "sha256:229a1675eb33bc9afb1fc463e73ee334950ccc485bc83a43f6ae5839fb4d5fa3", + "sha256:22c00bf659a9fa44d7b405cf1cd69b68b9d37537899898d8cbe5dffa4016b273", + "sha256:23aa3993a352491a47fcf17867f61472f32f874df4adcbb486294bd9fbe8abee", + "sha256:24da119850841d16996713d9c3374ca28a21deee056d609fbbed29065d17e1f6", + "sha256:2eeec144bcf6a7b6f2d74d6e70cbba7886a84dd373c886f06cb137a07de4954c", + "sha256:34736a2ffaa9c08ca9c707011a2d7b69074bbf82d645d8138bba771479b2362f", + "sha256:3aebc73e5ad70464f998f77f2da2063aa617cbd8d3e8174dd7c5b4518f967153", + "sha256:3eaa6681c0a3e3f3868c77aca14b7760fed35fdfda2fe587e15c701950e7bc69", + "sha256:4afc2644423f4eef97857a9fd61be9758ce5e336b4b0bd3d591238bb4b8b03e0", + "sha256:52084961262232ec04bd75f5043aed7e5d8d9695e542ff691dfef0110209f2d4", + "sha256:56cf3349fa3a2e67ed387a7974c11d233734636fe19facfcda261b411af14d80", + "sha256:5ed4e04af4012f875b78219d34434a6d08a67175150ac1b79eb70ab585d4ba8c", + "sha256:64965dc19c578a683ea73487a215a8897276224e004d50eeb21f0bc7a0b63c88", + "sha256:6ef64649eba56448f65e26546d85c860709844d2fc22ef14d324fe0b27f761a9", + "sha256:77cf5a2fd5583c670de41a7f4a7b46e5cbabe7180d8029f728571f4d2e864084", + "sha256:7bddc6adba8f699728f7fc1c9ce8cef359817ad78e2ed52b9502cb5f8dc7f741", + "sha256:813d661cbe2e37d87da2d16b7110a6860e93ddb11735c6919c8a3545c7b9c8d8", + "sha256:82bba2ab77eb5addbf9758d514d9bdef3c1bfe7d1649a47bd9a0d55a23ef478b", + "sha256:8bf2eaa83a5987e48fa63302f0fe7ce3275cfda87b34d40fef9ce703fb3ac002", + "sha256:8d186dafccdaa3409194c0f94db93c1a5d191145a275f19da6591f9499b8e7b8", + "sha256:8f64cb58729689d4d0e78f0bfb4c25ce2f851d0274c0273ac751795c04b8798a", + "sha256:902461a92b6a80fd5041a2ec5235680c7cc35e43615639ec2a40e63fca2dfb51", + "sha256:917f08f0b5d9324e9a34211e68d27447c552b50ab967044776bbab7e42a553a2", + "sha256:94d6962db81541eb0396d2f0dfcbb18cdb8c8b251d165efc2d974ae652c547d4", + "sha256:97892cced5794d721fb4ff8765764aa4ea48fe8b2c3820677505b96b83d4ef47", + "sha256:9a0303428d013ccf5c51de59c83f9127aba6200adb7fd4be57eddb432a1edd2a", + "sha256:9dc04c54c6ea615210c1b9e803d0e2d2255f87a3d5d119b6482c8f0dfa15b26b", + "sha256:a0c5368206057884cde18602580083aeaad9b860e2eac14fd253543158ce1e93", + "sha256:ad58e284a658a8a6a84af2e0b62f2f961f303cedfe551854d7bd40c3cbb61912", + "sha256:afb91792f1556f97be7f7acb57dc7756d89c5a87bd8b90363a77dbf9ea653817", + "sha256:b61579cecf1f4d616e5ea31a6e423a16680ab0d3a24a2ffe7bb1d4ee162477ff", + "sha256:b7af06968cbf6851566e806bf3e72825b0e6671832a2cbe840be1d2d65350710", + "sha256:bce8db2fc6f3174bd39b81405210b9b88d7b607d33e56a970c34a0c190da0490", + "sha256:bfb89ca9e3d0a9b5332deeb666b2ede9d3469107742158f4aeda5ce032d003f4", + "sha256:c680fef1b502ee680f8f0b95a41af4ec2c234e50e16c0af5bbda31999d3584bd", + "sha256:c6a8bde63106f162fca736e842a916853cad3c8d9d137e11c9ffa37efa818b02", + "sha256:cb49d5805cd347c404f928c5ae7c35e86ba0c58ffa701dbe905365e77ce7d641", + "sha256:ceb12974367b0a68a05d52f4162b29f575d241bd53de155efe632bf2c943c7f6", + "sha256:d33e2a1120887e89075f7f814ec144f66a6ce06a54f5722ccefc62fbeda83cff", + "sha256:db24c4e7fea4a052c6e869cbf387dd85d53b9736cfe1ef5d8d568d1ca925e977", + "sha256:e3a6f7cfdfd11eb5493d6d632e582408c8f3b429f295f8799c584c108b28db6f", + "sha256:eb66c30eb11e877976b7ead13632082a8621df648c408b8e15cdb91a452dd502", + "sha256:ed2a38e34bec6f2586435f6ff0bc5fe11d14bebd7ed492cf739a424e81681540", + "sha256:f36bdc1562142e3695555d2f4ac0cb69af165eddcefa98efc1c79495b533481f", + "sha256:f47579d508f2f56eddd16ce72045782ad3b1b3b678098699e2b6a1b30733e1c2", + "sha256:f5f646eec041db6ffdbcaf3e0756fb92018f7af3266138c756bb09d2b5baadec", + "sha256:fd644505a8cfd7f6584d33a9066d4e3d47700f050ef1490230c962de5dfb28c6", + "sha256:fff16b09042ba077f7b8aa5868d1d22456f0002574d0ba43462b10a009331677" + ], + "markers": "python_version >= '3.8'", + "version": "==4.4.0" + }, "pycparser": { "hashes": [ "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", @@ -704,7 +919,7 @@ "sha256:fb1c56d891f9e34303c451998ef62ba52659648bb0d75b03c5e4ac223a3342c2", "sha256:fe03bf25fae4b95d8afe40004a321df644400fdcba4c8e5e1a19c1085b740888" ], - "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.6.0" }, "python-dateutil": { @@ -713,6 +928,7 @@ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "python-dotenv": { @@ -721,6 +937,7 @@ "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==1.0.0" }, "requests": { @@ -729,6 +946,7 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "six": { @@ -789,7 +1007,6 @@ "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7", "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256" ], - "index": "pypi", "markers": "sys_platform != 'win32'", "version": "==0.19.0" }, @@ -903,7 +1120,7 @@ "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca", "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e" ], - "markers": "python_version >= '3.8'", + "markers": "python_full_version >= '3.8.0'", "version": "==3.0.1" }, "bandit": { @@ -912,6 +1129,7 @@ "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.7.5" }, "black": { @@ -936,6 +1154,7 @@ "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==23.11.0" }, "click": { @@ -975,7 +1194,7 @@ "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" ], - "markers": "python_version >= '3.8'", + "markers": "python_full_version >= '3.8.0'", "version": "==5.12.0" }, "markdown-it-py": { @@ -1016,6 +1235,7 @@ "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==23.2" }, "pathspec": { @@ -1044,11 +1264,11 @@ }, "pygments": { "hashes": [ - "sha256:1b37f1b1e1bff2af52ecaf28cc601e2ef7077000b227a0675da25aef85784bc4", - "sha256:e45a0e74bf9c530f564ca81b8952343be986a29f6afe7f5ad95c5f06b7bdf5e8" + "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", + "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" ], "markers": "python_version >= '3.7'", - "version": "==2.17.1" + "version": "==2.17.2" }, "pylint": { "hashes": [ @@ -1056,6 +1276,7 @@ "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" ], "index": "pypi", + "markers": "python_full_version >= '3.8.0'", "version": "==3.0.2" }, "pyyaml": { @@ -1119,7 +1340,7 @@ "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==13.7.0" }, "smmap": { @@ -1160,6 +1381,7 @@ "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==4.8.0" } } diff --git a/requirements.txt b/requirements.txt index 3c712a43a0..2c7bdb7880 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,34 +1,38 @@ -i https://pypi.org/simple -aiohttp==3.9.0 +aiodns==3.1.1 +aiohttp==3.9.0; python_version >= '3.8' aiosignal==1.3.1; python_version >= '3.7' async-timeout==4.0.3; python_version < '3.11' attrs==23.1.0; python_version >= '3.7' +brotli==1.1.0 cairocffi==1.6.1; python_version >= '3.7' cairosvg==2.7.1; python_version >= '3.5' certifi==2023.11.17; python_version >= '3.6' cffi==1.16.0; python_version >= '3.8' -charset-normalizer==3.3.2; python_version >= '3.7' -colorama==0.4.6 +charset-normalizer==3.3.2; python_full_version >= '3.7.0' +colorama==0.4.6; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' cssselect2==0.7.0; python_version >= '3.7' defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -discord.py==2.3.2 +discord.py[speed]==2.3.2; python_full_version >= '3.8.0' dnspython==2.4.2; python_version >= '3.8' and python_version < '4.0' -emoji==2.8.0 +emoji==2.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' frozenlist==1.4.0; python_version >= '3.8' idna==3.4; python_version >= '3.5' isodate==0.6.1 -lottie[pdf]==0.7.0 -motor==3.3.2 +lottie[pdf]==0.7.0; python_version >= '3' +motor==3.3.2; python_version >= '3.7' multidict==6.0.4; python_version >= '3.7' natural==0.2.0 -packaging==23.2 +orjson==3.9.10 +packaging==23.2; python_version >= '3.7' parsedatetime==2.6 pillow==10.1.0; python_version >= '3.8' +pycares==4.4.0; python_version >= '3.8' pycparser==2.21 -pymongo[srv]==4.6.0 -python-dateutil==2.8.2 -python-dotenv==1.0.0 -requests==2.31.0 +pymongo[srv]==4.6.0; python_version >= '3.7' +python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +python-dotenv==1.0.0; python_version >= '3.8' +requests==2.31.0; python_version >= '3.7' six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' tinycss2==1.2.1; python_version >= '3.7' urllib3==2.1.0; python_version >= '3.8' From 768d4daf81f826015420f2d5d3da770e4e7924e7 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 26 Nov 2023 13:51:47 -0800 Subject: [PATCH 1189/1221] Bump version to 4.1.0 --- CHANGELOG.md | 2 +- README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef7e87d6d..cb334e4e8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/modmail-dev/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# [UNRELEASED] +# v4.1.0 Drops support for Python 3.9. Python 3.10 and Python 3.11 are now the only supported versions. diff --git a/README.md b/README.md index 319a9742cf..ea6cb3eae0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v4.0.2-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v4.1.0-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> diff --git a/bot.py b/bot.py index 3c24681d12..bb47c75283 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.0.2" +__version__ = "4.1.0" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 389c24e3d5..cc43b4a54c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.0.2' +version = '4.1.0' description = "Modmail is similar to Reddit's 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 285e336cece0c87f43cb19959b6da28b0ea91ac7 Mon Sep 17 00:00:00 2001 From: Raiden <raidensakurajima@gmail.com> Date: Wed, 15 May 2024 19:48:26 +0800 Subject: [PATCH 1190/1221] Fix Docker file permission issue (#3323) * Update Dockerfile * Use the slim-bookworm image, refactored some steps * Disable user login * Capitalize comment --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- Dockerfile | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 96a398fad4..246d3cf7a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,38 @@ -FROM python:3.10 as py +FROM python:3.11-slim-bookworm as base -FROM py as build +RUN apt-get update && \ + apt-get install --no-install-recommends -y \ + # Install CairoSVG dependencies. + libcairo2 && \ + # Cleanup APT. + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + # Create a non-root user. + useradd --shell /usr/sbin/nologin --create-home -d /opt/modmail modmail -RUN apt update && apt install -y g++ git +FROM base as builder -COPY requirements.txt / -RUN pip install --prefix=/inst -U -r /requirements.txt +COPY requirements.txt . -FROM py +RUN pip install --root-user-action=ignore --no-cache-dir --upgrade pip wheel && \ + python -m venv /opt/modmail/.venv && \ + . /opt/modmail/.venv/bin/activate && \ + pip install --no-cache-dir --upgrade -r requirements.txt -COPY --from=build /inst /usr/local +FROM base -ENV USING_DOCKER yes -RUN useradd --system --no-create-home modmail -USER modmail +# Copy the entire venv. +COPY --from=builder --chown=modmail:modmail /opt/modmail/.venv /opt/modmail/.venv + +# Copy repository files. +WORKDIR /opt/modmail +USER modmail:modmail +COPY --chown=modmail:modmail . . + +# This sets some Python runtime variables and disables the internal auto-update. +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PATH=/opt/modmail/.venv/bin:$PATH \ + USING_DOCKER=yes -WORKDIR /modmailbot CMD ["python", "bot.py"] -COPY --chown=modmail:modmail . /modmailbot From 30bd95843ff5950790645141883f6f194f2a3c74 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 26 May 2024 03:18:21 -0700 Subject: [PATCH 1191/1221] Updated sponsors --- README.md | 7 +++++++ SPONSORS.json | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/README.md b/README.md index ea6cb3eae0..980066ca7e 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,13 @@ Advertise Your Server: </a> <br> <br> +Help Us • Help Other's: +<br> +<a href='https://discord.gg/5yQCFzY6HU'> + <img height=100 src='https://i.imgur.com/Gi3jxeH.gif' style='margin:5px'> +</a> +<br> +<br> Discord Advice Center: <br> <a href='https://discord.gg/zmwZy5fd9v'> diff --git a/SPONSORS.json b/SPONSORS.json index ce34dc3f9d..3a964dc3cf 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -128,5 +128,32 @@ "url": "https://discord.gg/uncommon", "title": "uncommon community" } + }, + { + "embed": { + "author": { + "name": "Help us • Help Others" + }, + "title": "Join Today", + "url": "https://discord.gg/5yQCFzY6HU", + "description": "At Help Us • Help Others, we accept as true with inside the transformative electricity of cooperation and kindness. Each one people has the capability to make a meaningful impact by means of helping and caring for others. Whether you want assistance or want to offer it, this is the right region for you!", + "fields": [ + { + "name": "What we offer:", + "value": "`🎬` - Active community\n`👮` - Active staff around the globe! \n`🛜` - 40+ Advertising channels to grow your socials!\n`💎` - Boosting Perks\n`🎉` - Event's monthly especially bank holiday roles!!\n`🔢` - Unique levelling systems\n`📞` - Multiple voice channels including gaming!\n`🎁` - Exclusive giveaways!" + }, + { + "name": "We Are Hiring", + "value": "`🔵` - Moderators\n`🔵` - Human Resources\n`🔵` - Community Team\n`🔵` - Partnership Manager\n`🔵` - Growth Manager\n`🚀` Much more to come!\n\n\nJoin Today!" + } + ], + "image": { + "url": "https://cdn.discordapp.com/attachments/1218338794416246874/1243635366326567002/AD_animated.gif" + }, + "color": 45300, + "footer": { + "text": "Help Us • Help Others" + } + } } ] From 8c04d25c917916f65a536687a6b8dc9398904c4e Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 23 Jun 2024 09:14:20 -0700 Subject: [PATCH 1192/1221] Update sponsors --- SPONSORS.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index 3a964dc3cf..80e3539f87 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -81,7 +81,7 @@ "icon_url": "https://i.imgur.com/cjVtRw5.jpg" }, "image": { - "url": "https://i.imgur.com/1hrjcHd.png" + "url": "https://i.imgur.com/1hrjcHd.png" }, "fields": [ { @@ -155,5 +155,18 @@ "text": "Help Us • Help Others" } } + }, + { + "embed": { + "description": "> Be apart of our community as we start to grow! and embark on a long journey.\n——————————————————-\n**What we offer?**\n\n➺〚🖌️〛Custom Liveries \n➺〚❤️〛Friendly and Growing community.\n➺〚🤝〛Partnerships.\n➺〚🎮〛Daily SSUs. \n➺〚🚨〛Great roleplays.\n➺〚💬〛Kind and Professional staff\n➺〚🎉〛Giveaways!!! \n——————————————————-\n**Emergency Services**\n\n➺〚🚔〛NY Police Force\n➺〚🚒〛Fire & Emergency NY\n➺〚🚧〛NY department of transportation \n\n——————————————————-\n**Whitelisted**\nComing soon!\n——————————————————-\n**What are we looking for!**\n\n➺〚💬〛More members\n➺〚⭐〛Staff Members - **WE'RE HIRING!**\n➺〚🤝〛Partnerships\n➺〚💎〛Boosters\n——————————————————\n\n**[Join now](https://discord.com/invite/qt62qSnKVa)**", + "author": { + "name": "New York Roleplay", + "icon_url": "https://cdn.discordapp.com/icons/1172553254882775111/648d5bc50393a21216527a1aaa61286d.webp" + }, + "color": 431075, + "thumbnail": { + "url": "https://cdn.discordapp.com/icons/1172553254882775111/648d5bc50393a21216527a1aaa61286d.webp" + } + } } ] From 19ff5a035cd05cc639aae91036581ee485649ed6 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 26 Aug 2024 00:07:16 +0200 Subject: [PATCH 1193/1221] Update sponsorship info --- SPONSORS.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SPONSORS.json b/SPONSORS.json index 80e3539f87..eb43227f6b 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -168,5 +168,15 @@ "url": "https://cdn.discordapp.com/icons/1172553254882775111/648d5bc50393a21216527a1aaa61286d.webp" } } + }, + { + "embeds": { + "title": "Pixelmark TM PLC", + "description": "Hi there! Welcome to PixelMark PLC! \nI'm so glad you're here. I started PixelMark PLC on May 24, 2023. Our team is dedicated to providing top-quality products and a great shopping experience. But more than that, PixelMark PLC is a community. We're here to share, create, and have fun together. Thanks for joining us on this exciting journey! Our current Goal is to reach 100 Human Members, you can help us achieve that Goal by joining or/and inviting your Friends!\n\nBest regards, \n*Felixpro202110 / Chief Executive Officer (Founder)* \n-----------------------------------------------------------------------\n> https://discord.gg/RVzNVRaFeE\n> https://www.roblox.com/groups/16031525/PixelMark-PLC", + "color": 10634504, + "image": { + "url": "https://imgur.com/iTl1dXm.png" + } + } } ] From 041c4fdc78b2391d481ddeee50d0883b645557e7 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 26 Aug 2024 00:07:51 +0200 Subject: [PATCH 1194/1221] Update sponsorship info --- SPONSORS.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index eb43227f6b..22cf0d44f1 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -170,7 +170,7 @@ } }, { - "embeds": { + "embed": { "title": "Pixelmark TM PLC", "description": "Hi there! Welcome to PixelMark PLC! \nI'm so glad you're here. I started PixelMark PLC on May 24, 2023. Our team is dedicated to providing top-quality products and a great shopping experience. But more than that, PixelMark PLC is a community. We're here to share, create, and have fun together. Thanks for joining us on this exciting journey! Our current Goal is to reach 100 Human Members, you can help us achieve that Goal by joining or/and inviting your Friends!\n\nBest regards, \n*Felixpro202110 / Chief Executive Officer (Founder)* \n-----------------------------------------------------------------------\n> https://discord.gg/RVzNVRaFeE\n> https://www.roblox.com/groups/16031525/PixelMark-PLC", "color": 10634504, From 26d73f5fa8a2c13ec9a8793658357f088a42da55 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:24:20 +0100 Subject: [PATCH 1195/1221] Add support for custom activities (#3352) --- cogs/utility.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index b892ffc01b..31cb065a28 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -499,6 +499,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): - `listening` - `watching` - `competing` + - `custom` When activity type is set to `listening`, it must be followed by a "to": "listening to..." @@ -510,6 +511,9 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): the linked twitch page: - `{prefix}config set twitch_url https://www.twitch.tv/somechannel/` + When activity type is set to `custom`, you can set + any custom text as the activity message. + To remove the current activity status: - `{prefix}activity clear` """ @@ -609,7 +613,9 @@ async def set_presence(self, *, status=None, activity_type=None, activity_messag elif activity_type == ActivityType.streaming: url = self.bot.config["twitch_url"] - if activity_type is not None: + if activity_type == ActivityType.custom: + activity = discord.CustomActivity(name=activity_message) + elif activity_type is not None: activity = discord.Activity(type=activity_type, name=activity_message, url=url) else: activity = None From fa6ad6388af2bb399615de3ede2c7569e7bcd8e5 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 27 Oct 2024 06:26:54 -0700 Subject: [PATCH 1196/1221] Updated changelog for custom activities #3352 --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb334e4e8e..d32485205c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/modmail-dev/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v4.1.1 + +### Added +- Support for custom activities with `?activity custom <text>` ([PR #3352](https://github.com/modmail-dev/Modmail/pull/3352)) + # v4.1.0 Drops support for Python 3.9. Python 3.10 and Python 3.11 are now the only supported versions. @@ -14,7 +19,7 @@ Drops support for Python 3.9. Python 3.10 and Python 3.11 are now the only suppo - GIF stickers no longer cause the bot to crash. - `?alias make/create` as aliases to `?alias add`. This improves continuity between the bot and its command structure. ([PR #3195](https://github.com/kyb3r/modmail/pull/3195)) - Loading the blocked list with the `?blocked` command takes a long time when the list is large. ([PR #3242](https://github.com/kyb3r/modmail/pull/3242)) -- Reply not being forwarded from DM. (PR [#3239](https://github.com/modmail-dev/modmail/pull/3239)) +- Reply not being forwarded from DM. ([PR #3239](https://github.com/modmail-dev/modmail/pull/3239)) - Cleanup imports after removing/unloading a plugin. ([PR #3226](https://github.com/modmail-dev/Modmail/pull/3226)) - Fixed a syntactic error in the close message when a thread is closed after a certain duration. ([PR #3233](https://github.com/modmail-dev/Modmail/pull/3233)) - Removed an extra space in the help command title when the command has no parameters. ([PR #3271](https://github.com/modmail-dev/Modmail/pull/3271)) From 2400ceb74ba968665ac555fb657214ec690e9f49 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:30:40 +0100 Subject: [PATCH 1197/1221] msglink command now scans all recipients (#3341) * msglink command now scans all recipients * Run black formatting --- cogs/modmail.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index ee27806177..e2a0039384 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -696,9 +696,15 @@ async def sfw(self, ctx): @checks.thread_only() async def msglink(self, ctx, message_id: int): """Retrieves the link to a message in the current thread.""" - try: - message = await ctx.thread.recipient.fetch_message(message_id) - except discord.NotFound: + found = False + for recipient in ctx.thread.recipients: + try: + message = await recipient.fetch_message(message_id) + found = True + break + except discord.NotFound: + continue + if not found: embed = discord.Embed( color=self.bot.error_color, description="Message not found or no longer exists." ) From 5de513ca18a9f36091ad8ee75364e74f0352df72 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 27 Oct 2024 06:36:05 -0700 Subject: [PATCH 1198/1221] Updated changelog for msglink multiple recipients fix #3341 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d32485205c..e2a5a52241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ however, insignificant breaking changes do not guarantee a major version bump, s # v4.1.1 +### Fixed +- `?msglink` now supports threads with multiple recipients. ([PR #3341](https://github.com/modmail-dev/Modmail/pull/3341)) + ### Added - Support for custom activities with `?activity custom <text>` ([PR #3352](https://github.com/modmail-dev/Modmail/pull/3352)) From 3c422c8f550b1eac6eb88f9b6f754cdc1db12af5 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:56:13 +0100 Subject: [PATCH 1199/1221] Error-handle a timeout error when loading plugins (#3330) * Fix registry loading errors * Fix black formatting * Repo consistency * Change code cleanness as requested by taku --------- Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- cogs/plugins.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/cogs/plugins.py b/cogs/plugins.py index fc6e09f05d..78bc0aa544 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -6,8 +6,8 @@ import sys import typing import zipfile -from importlib import invalidate_caches from difflib import get_close_matches +from importlib import invalidate_caches from pathlib import Path, PurePath from re import match from site import USER_SITE @@ -15,13 +15,12 @@ import discord from discord.ext import commands - from packaging.version import Version from core import checks from core.models import PermissionLevel, getLogger from core.paginator import EmbedPaginatorSession -from core.utils import truncate, trigger_typing +from core.utils import trigger_typing, truncate logger = getLogger(__name__) @@ -132,8 +131,11 @@ async def cog_load(self): async def populate_registry(self): url = "https://raw.githubusercontent.com/modmail-dev/modmail/master/plugins/registry.json" - async with self.bot.session.get(url) as resp: - self.registry = json.loads(await resp.text()) + try: + async with self.bot.session.get(url) as resp: + self.registry = json.loads(await resp.text()) + except asyncio.TimeoutError: + logger.warning("Failed to fetch registry. Loading with empty registry") async def initial_load_plugins(self): for plugin_name in list(self.bot.config["plugins"]): @@ -638,6 +640,14 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N registry = sorted(self.registry.items(), key=lambda elem: elem[0]) + if not registry: + embed = discord.Embed( + color=self.bot.error_color, + description="Registry is empty. This could be because it failed to load.", + ) + await ctx.send(embed=embed) + return + if isinstance(plugin_name, int): index = plugin_name - 1 if index < 0: From 7c1574ff81887f2907ab53b968083c97f797e862 Mon Sep 17 00:00:00 2001 From: Martin <box152535@gmail.com> Date: Sun, 27 Oct 2024 14:59:18 +0100 Subject: [PATCH 1200/1221] fixes persistent note (#3324) This fixes persistent note not sending on thread creation. Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 646c98a604..81dc03f44d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -250,7 +250,7 @@ async def send_persistent_notes(): ids = {} class State: - def store_user(self, user): + def store_user(self, user, cache): return user for note in notes: From eee764ebc368b2a165aff1204aa272e8f2a72a99 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 27 Oct 2024 07:00:48 -0700 Subject: [PATCH 1201/1221] Updated changelog for persistent notes fix #3324 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a5a52241..688462f439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed - `?msglink` now supports threads with multiple recipients. ([PR #3341](https://github.com/modmail-dev/Modmail/pull/3341)) +- Fixed persistent notes not working due to discord.py internal change. ([PR #3324](https://github.com/modmail-dev/Modmail/pull/3324)) ### Added - Support for custom activities with `?activity custom <text>` ([PR #3352](https://github.com/modmail-dev/Modmail/pull/3352)) From da97bdc0c78daaf6a3b052346c5a5eb4a4062744 Mon Sep 17 00:00:00 2001 From: andy <andy@ondrya.com> Date: Sun, 27 Oct 2024 10:02:56 -0400 Subject: [PATCH 1202/1221] Update SPONSORS.json (#3348) Removal of Advertise Your Server. Company has been closed. Signed-off-by: andy <andy@ondrya.com> --- SPONSORS.json | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index 22cf0d44f1..491b30eb9f 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -91,23 +91,6 @@ ] } }, - { - "embed": { - "title": "Advertise Your Server", - "description": "Advertise Your Server is the leading advertising and growth Discord Server. With over 60,000 members we can help grow your community with our range of services.\n\n__**Advertise Your Server offers everything you need to grow and find servers:**__\n\n:chart_with_upwards_trend: **Discord Growth Experts** to give you advice on how to __grow your server.__ (server/advert reviews, growth tips)\n:dividers: Over 40 different channels for **different server categories.**\n:robot: Our own __custom__ **bump bot.** (Liam)\n:bar_chart: Currently the __BIGGEST__ advertising server on Discord.\n:computer: Our own server __Listing Site__!\n:ticket: Small Servers Program for servers with less than 300 members.\n:dvd: Weekly Podcast, Blog, Email Newsletter and YouTube Tutorials. \n\nhttps://discord.gg/zP8KcF4VQz\nhttps://aysdiscord.com", - "author": { - "name": "Advertise Your Server", - "icon_url": "https://cdn.discordapp.com/attachments/563522692418895872/907067815486427176/logo4.png" - }, - "color": 431075, - "footer": { - "text": "Grow Your Discord Server" - }, - "image": { - "url": "https://cdn.discordapp.com/attachments/472811257913933834/907068966311166043/unknown_2.png" - } - } - }, { "embed": { "footer": { From 464b64c36abbef91316bf017b3542ae64540297f Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 14 Jan 2025 05:08:34 +0000 Subject: [PATCH 1203/1221] Update SPONSORS.json Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> --- SPONSORS.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/SPONSORS.json b/SPONSORS.json index 491b30eb9f..feb822ee21 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -154,12 +154,8 @@ }, { "embed": { - "title": "Pixelmark TM PLC", - "description": "Hi there! Welcome to PixelMark PLC! \nI'm so glad you're here. I started PixelMark PLC on May 24, 2023. Our team is dedicated to providing top-quality products and a great shopping experience. But more than that, PixelMark PLC is a community. We're here to share, create, and have fun together. Thanks for joining us on this exciting journey! Our current Goal is to reach 100 Human Members, you can help us achieve that Goal by joining or/and inviting your Friends!\n\nBest regards, \n*Felixpro202110 / Chief Executive Officer (Founder)* \n-----------------------------------------------------------------------\n> https://discord.gg/RVzNVRaFeE\n> https://www.roblox.com/groups/16031525/PixelMark-PLC", - "color": 10634504, - "image": { - "url": "https://imgur.com/iTl1dXm.png" - } + "description": "**__CityStore PLC__**\n*Your Retail Journey*\n*\"Better choice and better value in food, fashion & homewares.\"*\n\n\n**------------------------------------------**\n*__About us__*\nSupermarket, CityStore PLC! Attend a training to become staff!\n\nThis game is currently in V3\n\nWe have a training Centre and applications center!\n\n**------------------------------------------**\n\n> *❤️ Don't hesitate! Dive into the excitement today by joining our vibrant community on Discord. Experience our unique perspective and become an integral part of our group. Your **journey** with us promises to be unforgettable no regrets, only great memories await! ❤️*\n\n*We hope to see you. *\n\n*Signed,*\n**CityStore PLC**\n> Discord: https://discord.gg/yjFQb5mrSk\n> Roblox Group: https://www.roblox.com/groups/32819373/CityStore-PLC#!/about\n\nJoin us now and become apart of Citystore PLC community! 🎉", + "color": 15523550 } } ] From bb2892f5aebf1d335d612cf03d25e34553276fdc Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:13:09 -0800 Subject: [PATCH 1204/1221] Update sponsors --- SPONSORS.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SPONSORS.json b/SPONSORS.json index feb822ee21..b6212b8ed7 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -154,7 +154,8 @@ }, { "embed": { - "description": "**__CityStore PLC__**\n*Your Retail Journey*\n*\"Better choice and better value in food, fashion & homewares.\"*\n\n\n**------------------------------------------**\n*__About us__*\nSupermarket, CityStore PLC! Attend a training to become staff!\n\nThis game is currently in V3\n\nWe have a training Centre and applications center!\n\n**------------------------------------------**\n\n> *❤️ Don't hesitate! Dive into the excitement today by joining our vibrant community on Discord. Experience our unique perspective and become an integral part of our group. Your **journey** with us promises to be unforgettable no regrets, only great memories await! ❤️*\n\n*We hope to see you. *\n\n*Signed,*\n**CityStore PLC**\n> Discord: https://discord.gg/yjFQb5mrSk\n> Roblox Group: https://www.roblox.com/groups/32819373/CityStore-PLC#!/about\n\nJoin us now and become apart of Citystore PLC community! 🎉", + "title": "CityStore PLC", + "description": "*Your Retail Journey*\n*\"Better choice and better value in food, fashion & homewares.\"*\n\n\n**------------------------------------------**\n*__About us__*\nSupermarket, CityStore PLC! Attend a training to become staff!\n\nThis game is currently in V3\n\nWe have a training Centre and applications center!\n\n**------------------------------------------**\n\n> *❤️ Don't hesitate! Dive into the excitement today by joining our vibrant community on Discord. Experience our unique perspective and become an integral part of our group. Your **journey** with us promises to be unforgettable no regrets, only great memories await! ❤️*\n\n*We hope to see you. *\n\n*Signed,*\n**CityStore PLC**\n> Discord: https://discord.gg/yjFQb5mrSk\n> Roblox Group: https://www.roblox.com/groups/32819373/CityStore-PLC#!/about\n\nJoin us now and become apart of Citystore PLC community! 🎉", "color": 15523550 } } From 792a3727ce1317e0dfad46e243959856c4bc0d9f Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:16:18 -0800 Subject: [PATCH 1205/1221] Bump version to v4.1.1 --- bot.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index bb47c75283..3c6ebe7911 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.1.0" +__version__ = "4.1.1" import asyncio diff --git a/pyproject.toml b/pyproject.toml index cc43b4a54c..1a5ed16a3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.1.0' +version = '4.1.1' description = "Modmail is similar to Reddit's 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 89afed5793221d8d55792c50f8dd30b28db4885d Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:28:36 +0000 Subject: [PATCH 1206/1221] Update README.md Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 980066ca7e..500978a1f9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v4.1.0-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v4.1.1-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> @@ -24,7 +24,7 @@ </a> <a href="https://patreon.com/kyber"> - <img src="https://img.shields.io/badge/patreon-donate-orange.svg?style=for-the-badge&logo=Patreon" alt="Python 3.8"> + <img src="https://img.shields.io/badge/patreon-donate-orange.svg?style=for-the-badge&logo=Patreon" alt="Patreon"> </a> <a href="https://www.python.org/downloads/"> From 6c820bfeca3f42d9e9e0bc4fd5d6ddfb8a1e18ad Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:59:47 +0000 Subject: [PATCH 1207/1221] Update README.md Update sponsors Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 500978a1f9..eef9e3f940 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,13 @@ Discord Advice Center: <a href='https://discord.gg/zmwZy5fd9v'> <img height=100 src='https://i.imgur.com/1hrjcHd.png' style='margin:5px'> </a> +<br> +<br> +Blacklight Promotions: +<br> +<a href='https://blacklightpromotions.online'> + <img height=100 src='https://i.imgur.com/yLgE6h6.png' style='margin:5px'> +</a> Become a sponsor on [Patreon](https://patreon.com/kyber). From 1a3bda800d500c2c8be42f44403bbfe73b4ab2a8 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Wed, 12 Mar 2025 04:37:06 +0100 Subject: [PATCH 1208/1221] Merge the pydis changes to the bot (#3365) * Update README.md Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> * Update README.md Update sponsors Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> * Escape hyphen in regex string Unescaped this was permitting any character between $ (index 36) and _ (index 95), which aren't all valid in urls. * Get or fetch member when trying to reply This fixes an issue where the member may not be in the cache --------- Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> Co-authored-by: Chris Lovering <chris.lovering.95@gmail.com> --- README.md | 11 +++++++++-- bot.py | 10 ++++++++++ core/thread.py | 10 ++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 980066ca7e..eef9e3f940 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v4.1.0-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v4.1.1-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> @@ -24,7 +24,7 @@ </a> <a href="https://patreon.com/kyber"> - <img src="https://img.shields.io/badge/patreon-donate-orange.svg?style=for-the-badge&logo=Patreon" alt="Python 3.8"> + <img src="https://img.shields.io/badge/patreon-donate-orange.svg?style=for-the-badge&logo=Patreon" alt="Patreon"> </a> <a href="https://www.python.org/downloads/"> @@ -159,6 +159,13 @@ Discord Advice Center: <a href='https://discord.gg/zmwZy5fd9v'> <img height=100 src='https://i.imgur.com/1hrjcHd.png' style='margin:5px'> </a> +<br> +<br> +Blacklight Promotions: +<br> +<a href='https://blacklightpromotions.online'> + <img height=100 src='https://i.imgur.com/yLgE6h6.png' style='margin:5px'> +</a> Become a sponsor on [Patreon](https://patreon.com/kyber). diff --git a/bot.py b/bot.py index 3c6ebe7911..bcf67e5f9a 100644 --- a/bot.py +++ b/bot.py @@ -640,6 +640,16 @@ async def get_or_fetch_user(self, id: int) -> discord.User: """ return self.get_user(id) or await self.fetch_user(id) + @staticmethod + async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> typing.Optional[discord.Member]: + """ + Attempt to get a member from cache; on failure fetch from the API. + + Returns: + The :obj:`discord.Member` or :obj:`None` to indicate the member could not be found. + """ + return guild.get_member(member_id) or await guild.fetch_member(member_id) + async def retrieve_emoji(self) -> typing.Tuple[str, str]: sent_emoji = self.config["sent_emoji"] blocked_emoji = self.config["blocked_emoji"] diff --git a/core/thread.py b/core/thread.py index 81dc03f44d..00060ab7f5 100644 --- a/core/thread.py +++ b/core/thread.py @@ -823,7 +823,13 @@ async def reply( """Returns List[user_dm_msg] and thread_channel_msg""" if not message.content and not message.attachments and not message.stickers: raise MissingRequiredArgument(DummyParam("msg")) - if not any(g.get_member(self.id) for g in self.bot.guilds): + for guild in self.bot.guilds: + try: + if await self.bot.get_or_fetch_member(guild, self.id): + break + except discord.NotFound: + pass + else: return await message.channel.send( embed=discord.Embed( color=self.bot.error_color, @@ -995,7 +1001,7 @@ async def send( attachments.append(attachment) image_urls = re.findall( - r"http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + r"http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$\-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", message.content, ) From 297aa0c7ee292af29fbcecb55e65d8f8e2f62edc Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Fri, 14 Mar 2025 04:14:26 -0700 Subject: [PATCH 1209/1221] Bump version to 4.1.2 --- CHANGELOG.md | 5 +++++ README.md | 2 +- bot.py | 2 +- pyproject.toml | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 688462f439..4ec54e0f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/modmail-dev/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v4.1.2 + +### Fixed +- Members not caching correctly for large servers. ([PR #3365](https://github.com/modmail-dev/Modmail/pull/3365)) + # v4.1.1 ### Fixed diff --git a/README.md b/README.md index eef9e3f940..2e3d32e344 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v4.1.1-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v4.1.2-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> diff --git a/bot.py b/bot.py index bcf67e5f9a..3f13ef7ced 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.1.1" +__version__ = "4.1.2" import asyncio diff --git a/pyproject.toml b/pyproject.toml index 1a5ed16a3b..7e29a4d4ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.1.1' +version = '4.1.2' description = "Modmail is similar to Reddit's 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 21f4766b010d06b6d0afe78e1da7c59a8209dc90 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 11 Nov 2025 06:30:52 +0000 Subject: [PATCH 1210/1221] Merge development v4.2.0 (#3390) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add (truncated) preview to snippets command (#3342) * Add (truncated) preview to snippets command * Add old view as option with "compact" * Fix black formatting * Fix: Image url regex in thread send method (#3378) * feat minimum character requirement for thread creation. (#3380) * update: dpy, snoozing. This pull request updated discord.py to 2.5.2. This also brings a new few features. - snooze - snoozed - unsnooze - clearsnoozed Aswell as a few new config options. - max_snooze_time - snooze_title - snooze_text - unsnooze_tex - unsnooze_notify_channel Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * remove: unneeded import Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Formatting black Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix?: internal messages on restoration Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * formatting Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix: internal messages. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix: internals Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update thread.py Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix: use same logkey after restoration Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Add files via upload Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update thread.py Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update thread.py Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update thread.py Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix: show who send which internal message. * Black formatting. * Update Pipfile Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update Pipfile.lock Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix: unsnooze bug * feat: CV2 * update: black * fix: duplicates in logs, notes. * feat: dpy 2.6.3, forwarded messages, bug fixes. * Fix jump_url not being displayed * Update pipfile for new dpy version * fix: bug in note title/color * Update snooze arg * Update Pipfile to include tomli package Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * auto detect dpy version Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Remove crlf terminators * fix: ignore typing failures (#3389) * fix: ignore typing failures Make Modmail keep working when typing is disabled/outage * fix: only surpress failures * chore: sync local edits before push * Lock pipenv * Fix: closing with timed words/ command in reply. (#3391) * fix: ignore typing failures Make Modmail keep working when typing is disabled/outage * fix: only surpress failures * chore: sync local edits before push * Fix: closing with timed words/ command in reply. * Fix: typing in changelog command. * Fix: closing with timed words (additional)) * Fix changelog entry for command reply issue Corrected wording in the changelog entry regarding command inclusion in replies. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update CHANGELOG for v4.2.0 enhancements Forwarded messages now display correctly in threads. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> --------- Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Remove disutil, undo lowercasing escape seq * Add back uvloop * Add config help for snooze configs Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> * Update sponsors and bmac links --------- Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> Co-authored-by: Sebastian <61157793+sebkuip@users.noreply.github.com> Co-authored-by: Zallom <Yg75nWkHX7jBHcqRPUbb783c54xSa9+github63384893@gmail.com> Co-authored-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Co-authored-by: “lorenzo132” <lhoorn4@gmail.com> Co-authored-by: Martin <box152535@gmail.com> --- .env.example | 1 + .gitattributes | 1 + .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- CHANGELOG.md | 33 + Pipfile | 3 +- Pipfile.lock | 2353 ++++++++++++++++--------- README.md | 32 +- SPONSORS.json | 27 - bot.py | 216 ++- cogs/modmail.py | 345 +++- cogs/plugins.py | 9 +- cogs/utility.py | 101 +- core/_color_data.py | 1 - core/changelog.py | 4 +- core/clients.py | 10 +- core/config.py | 11 + core/config_help.json | 102 +- core/paginator.py | 29 +- core/thread.py | 415 ++++- core/time.py | 64 +- core/utils.py | 165 +- pyproject.toml | 2 +- requirements.txt | 54 +- 24 files changed, 2914 insertions(+), 1068 deletions(-) create mode 100644 .gitattributes diff --git a/.env.example b/.env.example index 14bdf060bf..972eca4517 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,4 @@ LOG_URL=https://logviewername.herokuapp.com/ GUILD_ID=1234567890 OWNERS=Owner1ID,Owner2ID,Owner3ID CONNECTION_URI=mongodb+srv://mongodburi +DISABLE_AUTOUPDATES=true \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..176a458f94 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index fbcb038e69..8125c1756a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -patreon: kyber +buy_me_a_coffee: modmaildev diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 99779ab4f3..35fc2bedb1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -20,7 +20,7 @@ body: - Heroku - Systemd - PM2 - - Patreon + - Buy Me A Coffee / Patreon - Other validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ec54e0f8a..83c4f1bdd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/modmail-dev/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v4.2.0 + +Upgraded discord.py to version 2.6.3, added support for CV2. +Forwarded messages now properly show in threads, rather than showing as an empty embed. + +### Fixed +- Make Modmail keep working when typing is disabled due to an outage caused by Discord. +- Resolved an issue where forwarded messages appeared as empty embeds. +- Fixed internal message handling and restoration processes. +- Eliminated duplicate logs and notes. +- Addressed inconsistent use of `logkey` after ticket restoration. +- Fixed issues with identifying the user who sent internal messages. +- Solved an ancient bug where closing with words like `evening` wouldn't work. +- Fixed the command from being included in the reply in rare conditions. + +### Added +Commands: +* `snooze`: Initiates a snooze action. +* `snoozed`: Displays snoozed items. +* `unsnooze`: Reverses the snooze action. +* `clearsnoozed`: Clears all snoozed items. + +Configuration Options: +* `max_snooze_time`: Sets the maximum duration for snooze. +* `snooze_title`: Customizes the title for snooze notifications. +* `snooze_text`: Customizes the text for snooze notifications. +* `unsnooze_text`: Customizes the text for unsnooze notifications. +* `unsnooze_notify_channel`: Specifies the channel for unsnooze notifications. +* `thread_min_characters`: Minimum number of characters required. +* `thread_min_characters_title`: Title shown when the message is too short. +* `thread_min_characters_response`: Response shown to the user if their message is too short. +* `thread_min_characters_footer`: Footer displaying the minimum required characters. + # v4.1.2 ### Fixed diff --git a/Pipfile b/Pipfile index 21205b36f5..8fe5bd40c8 100644 --- a/Pipfile +++ b/Pipfile @@ -8,11 +8,12 @@ bandit = ">=1.7.5" black = "==23.11.0" pylint = "==3.0.2" typing-extensions = "==4.8.0" +tomli = "==2.2.1" # Needed for black on Python < 3.11 [packages] aiohttp = "==3.9.0" colorama = "==0.4.6" -"discord.py" = {version = "==2.3.2", extras = ["speed"]} +"discord.py" = {version = "==2.6.3", extras = ["speed"]} emoji = "==2.8.0" isodate = "==0.6.1" motor = "==3.3.2" diff --git a/Pipfile.lock b/Pipfile.lock index 5f07c7b131..ae2da5ae76 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7fee393ea9ea4c0b923033f0da0fdc590ba3f75c6072812062cdc458b84bf9ae" + "sha256": "93bdedec9b82ac210253d3424e48f3af9e79dca6c44b5b941ec446709de27409" }, "pipfile-spec": 6, "requires": {}, @@ -16,10 +16,11 @@ "default": { "aiodns": { "hashes": [ - "sha256:1073eac48185f7a4150cad7f96a5192d6911f12b4fb894de80a088508c9b3a99", - "sha256:a387b63da4ced6aad35b1dda2d09620ad608a1c7c0fb71efa07ebb4cd511928d" + "sha256:11264edbab51896ecf546c18eb0dd56dff0428c6aa6d2cd87e643e07300eb310", + "sha256:6d0404f7d5215849233f6ee44854f2bb2481adf71b336b2279016ea5990ca5c5" ], - "version": "==3.1.1" + "markers": "python_version >= '3.9'", + "version": "==3.5.0" }, "aiohttp": { "hashes": [ @@ -106,33 +107,81 @@ }, "aiosignal": { "hashes": [ - "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", - "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" + "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", + "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7" ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "async-timeout": { - "hashes": [ - "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", - "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" - ], - "markers": "python_version < '3.11'", - "version": "==4.0.3" + "markers": "python_version >= '3.9'", + "version": "==1.4.0" }, "attrs": { "hashes": [ - "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", - "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", + "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" ], - "markers": "python_version >= '3.7'", - "version": "==23.1.0" + "markers": "python_version >= '3.8'", + "version": "==25.3.0" + }, + "audioop-lts": { + "hashes": [ + "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", + "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", + "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", + "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", + "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", + "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", + "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", + "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", + "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", + "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", + "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", + "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", + "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", + "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", + "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", + "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", + "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", + "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", + "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", + "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", + "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", + "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", + "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", + "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", + "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", + "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", + "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", + "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", + "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", + "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", + "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", + "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", + "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", + "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", + "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", + "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", + "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", + "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", + "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", + "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", + "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", + "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", + "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", + "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", + "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", + "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", + "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", + "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", + "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800" + ], + "markers": "python_version >= '3.13'", + "version": "==0.2.2" }, "brotli": { "hashes": [ "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354", + "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419", "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a", "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c", @@ -140,43 +189,67 @@ "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", + "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", + "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438", "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b", "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b", "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68", + "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d", + "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943", "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", + "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", + "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", + "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547", "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", + "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", + "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a", + "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb", "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112", "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", + "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2", "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95", + "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", + "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", + "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38", "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914", "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", + "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", + "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c", "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", + "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f", "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", + "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", + "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", + "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", + "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", + "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", + "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74", + "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60", "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1", @@ -187,27 +260,44 @@ "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460", "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", + "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2", + "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", + "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75", + "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5", + "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", + "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", + "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", + "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", + "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01", "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579", "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84", + "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7", + "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c", + "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", + "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52", "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", + "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", + "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2", "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", + "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643", + "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985", "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", @@ -218,181 +308,202 @@ }, "cairocffi": { "hashes": [ - "sha256:78e6bbe47357640c453d0be929fa49cd05cce2e1286f3d2a1ca9cbda7efdb8b7", - "sha256:aa78ee52b9069d7475eeac457389b6275aa92111895d78fbaa2202a52dac112e" + "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", + "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f" ], - "markers": "python_version >= '3.7'", - "version": "==1.6.1" + "markers": "python_version >= '3.8'", + "version": "==1.7.1" }, "cairosvg": { "hashes": [ - "sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0", - "sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b" + "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", + "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5" ], - "markers": "python_version >= '3.5'", - "version": "==2.7.1" + "markers": "python_version >= '3.9'", + "version": "==2.8.2" }, "certifi": { "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", + "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" ], - "markers": "python_version >= '3.6'", - "version": "==2023.11.17" + "markers": "python_version >= '3.7'", + "version": "==2025.10.5" }, "cffi": { "hashes": [ - "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", - "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", - "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", - "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", - "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", - "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", - "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", - "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", - "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", - "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", - "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", - "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", - "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", - "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", - "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", - "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", - "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", - "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", - "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", - "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", - "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", - "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", - "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", - "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", - "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", - "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", - "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", - "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", - "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", - "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", - "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", - "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", - "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", - "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", - "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", - "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", - "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", - "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", - "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", - "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", - "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", - "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", - "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", - "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", - "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", - "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", - "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", - "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", - "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", - "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", - "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", - "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" - ], - "markers": "python_version >= '3.8'", - "version": "==1.16.0" + "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", + "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", + "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", + "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", + "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", + "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", + "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", + "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", + "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", + "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", + "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", + "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", + "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", + "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", + "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", + "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", + "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", + "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", + "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", + "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", + "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", + "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", + "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", + "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", + "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", + "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", + "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", + "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", + "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", + "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", + "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", + "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", + "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", + "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", + "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", + "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", + "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", + "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", + "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", + "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", + "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", + "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", + "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", + "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", + "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", + "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", + "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", + "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", + "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", + "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", + "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", + "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", + "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", + "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", + "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", + "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", + "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", + "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", + "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", + "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", + "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", + "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", + "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", + "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", + "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", + "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", + "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", + "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", + "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", + "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", + "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", + "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", + "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", + "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", + "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", + "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", + "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", + "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", + "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", + "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", + "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", + "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", + "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", + "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf" + ], + "markers": "python_version >= '3.9'", + "version": "==2.0.0" }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", + "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", + "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", + "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", + "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", + "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", + "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c", + "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", + "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", + "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", + "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", + "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", + "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", + "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", + "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", + "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", + "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", + "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", + "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4", + "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", + "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", + "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", + "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", + "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", + "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", + "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", + "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b", + "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", + "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", + "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", + "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", + "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", + "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", + "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", + "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", + "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", + "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a", + "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40", + "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", + "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", + "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", + "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", + "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", + "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", + "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", + "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", + "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", + "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", + "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9", + "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", + "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", + "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", + "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b", + "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", + "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942", + "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", + "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", + "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b", + "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", + "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", + "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", + "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", + "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", + "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", + "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", + "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", + "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", + "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", + "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", + "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", + "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", + "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb", + "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", + "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557", + "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", + "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", + "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", + "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", + "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.3" }, "colorama": { "hashes": [ @@ -405,11 +516,11 @@ }, "cssselect2": { "hashes": [ - "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a", - "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969" + "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", + "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a" ], - "markers": "python_version >= '3.7'", - "version": "==0.7.0" + "markers": "python_version >= '3.9'", + "version": "==0.8.0" }, "defusedxml": { "hashes": [ @@ -424,19 +535,19 @@ "speed" ], "hashes": [ - "sha256:4560f70f2eddba7e83370ecebd237ac09fbb4980dc66507482b0c0e5b8f76b9c", - "sha256:9da4679fc3cb10c64b388284700dc998663e0e57328283bbfcfc2525ec5960a6" + "sha256:69835269d73d9889a2f0efff4c91264a18998db0fdc4295a3c886fe9196dea4e", + "sha256:92bb3ef9dbe08525803be1e357bc0191f59ae16956690fc96c34f40bcd02c649" ], - "markers": "python_full_version >= '3.8.0'", - "version": "==2.3.2" + "markers": "python_version >= '3.8'", + "version": "==2.6.3" }, "dnspython": { "hashes": [ - "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8", - "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984" + "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", + "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f" ], - "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==2.4.2" + "markers": "python_version >= '3.10'", + "version": "==2.8.0" }, "emoji": { "hashes": [ @@ -449,78 +560,147 @@ }, "frozenlist": { "hashes": [ - "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6", - "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01", - "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251", - "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9", - "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b", - "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87", - "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf", - "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f", - "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0", - "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2", - "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b", - "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc", - "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c", - "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467", - "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9", - "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1", - "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a", - "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79", - "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167", - "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300", - "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf", - "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea", - "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2", - "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab", - "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3", - "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb", - "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087", - "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc", - "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8", - "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62", - "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f", - "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326", - "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c", - "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431", - "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963", - "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7", - "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef", - "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3", - "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956", - "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781", - "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472", - "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc", - "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839", - "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672", - "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3", - "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503", - "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d", - "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8", - "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b", - "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc", - "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f", - "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559", - "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b", - "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95", - "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb", - "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963", - "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919", - "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f", - "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3", - "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1", - "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e" - ], - "markers": "python_version >= '3.8'", - "version": "==1.4.0" + "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", + "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", + "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", + "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", + "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", + "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", + "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", + "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", + "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", + "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", + "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", + "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", + "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", + "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", + "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", + "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", + "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", + "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", + "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", + "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", + "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", + "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", + "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", + "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", + "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", + "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", + "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", + "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", + "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", + "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", + "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", + "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", + "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", + "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", + "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", + "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", + "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", + "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", + "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", + "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", + "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", + "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", + "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", + "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", + "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", + "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", + "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", + "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", + "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", + "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", + "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", + "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", + "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", + "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", + "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", + "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", + "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", + "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", + "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", + "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", + "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", + "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", + "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", + "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", + "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", + "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", + "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", + "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", + "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", + "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", + "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", + "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", + "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", + "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", + "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", + "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", + "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", + "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", + "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", + "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", + "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", + "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", + "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", + "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", + "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", + "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", + "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", + "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", + "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", + "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", + "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", + "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", + "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", + "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", + "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", + "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", + "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", + "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", + "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", + "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", + "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", + "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", + "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", + "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", + "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", + "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", + "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", + "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", + "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", + "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", + "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", + "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", + "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", + "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", + "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", + "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", + "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", + "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", + "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", + "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", + "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", + "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", + "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", + "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", + "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", + "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", + "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", + "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", + "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", + "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd" + ], + "markers": "python_version >= '3.9'", + "version": "==1.8.0" }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], - "markers": "python_version >= '3.5'", - "version": "==3.4" + "markers": "python_version >= '3.6'", + "version": "==3.10" }, "isodate": { "hashes": [ @@ -551,83 +731,119 @@ }, "multidict": { "hashes": [ - "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9", - "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8", - "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03", - "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710", - "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161", - "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664", - "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569", - "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067", - "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313", - "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706", - "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2", - "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636", - "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49", - "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93", - "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603", - "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0", - "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60", - "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4", - "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e", - "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1", - "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60", - "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951", - "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc", - "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe", - "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95", - "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d", - "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8", - "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed", - "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2", - "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775", - "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87", - "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c", - "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2", - "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98", - "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3", - "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe", - "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78", - "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660", - "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176", - "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e", - "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988", - "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c", - "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c", - "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0", - "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449", - "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f", - "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde", - "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5", - "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d", - "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac", - "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a", - "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9", - "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca", - "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11", - "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35", - "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063", - "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b", - "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982", - "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258", - "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1", - "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52", - "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480", - "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7", - "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461", - "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d", - "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc", - "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779", - "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a", - "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547", - "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0", - "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171", - "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf", - "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d", - "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0.4" + "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", + "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", + "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", + "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", + "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", + "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", + "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f", + "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1", + "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", + "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", + "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", + "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", + "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae", + "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", + "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", + "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", + "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", + "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", + "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", + "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0", + "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", + "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", + "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", + "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", + "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0", + "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", + "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", + "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", + "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", + "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", + "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", + "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", + "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", + "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", + "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", + "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", + "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", + "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", + "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb", + "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", + "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", + "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", + "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", + "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", + "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", + "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", + "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb", + "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", + "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", + "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", + "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", + "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", + "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", + "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", + "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", + "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", + "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", + "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", + "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", + "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", + "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b", + "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", + "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", + "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", + "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", + "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", + "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978", + "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", + "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", + "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", + "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", + "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", + "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", + "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4", + "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665", + "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", + "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", + "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", + "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", + "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", + "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17", + "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", + "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", + "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", + "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", + "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", + "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", + "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", + "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", + "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", + "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", + "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd", + "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", + "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", + "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", + "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", + "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", + "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", + "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", + "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", + "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", + "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb", + "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210", + "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53", + "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", + "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", + "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9", + "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", + "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a", + "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773" + ], + "markers": "python_version >= '3.9'", + "version": "==6.6.4" }, "natural": { "hashes": [ @@ -638,58 +854,92 @@ }, "orjson": { "hashes": [ - "sha256:06ad5543217e0e46fd7ab7ea45d506c76f878b87b1b4e369006bdb01acc05a83", - "sha256:0a73160e823151f33cdc05fe2cea557c5ef12fdf276ce29bb4f1c571c8368a60", - "sha256:1234dc92d011d3554d929b6cf058ac4a24d188d97be5e04355f1b9223e98bbe9", - "sha256:1d0dc4310da8b5f6415949bd5ef937e60aeb0eb6b16f95041b5e43e6200821fb", - "sha256:2a11b4b1a8415f105d989876a19b173f6cdc89ca13855ccc67c18efbd7cbd1f8", - "sha256:2e2ecd1d349e62e3960695214f40939bbfdcaeaaa62ccc638f8e651cf0970e5f", - "sha256:3a2ce5ea4f71681623f04e2b7dadede3c7435dfb5e5e2d1d0ec25b35530e277b", - "sha256:3e892621434392199efb54e69edfff9f699f6cc36dd9553c5bf796058b14b20d", - "sha256:3fb205ab52a2e30354640780ce4587157a9563a68c9beaf52153e1cea9aa0921", - "sha256:4689270c35d4bb3102e103ac43c3f0b76b169760aff8bcf2d401a3e0e58cdb7f", - "sha256:49f8ad582da6e8d2cf663c4ba5bf9f83cc052570a3a767487fec6af839b0e777", - "sha256:4bd176f528a8151a6efc5359b853ba3cc0e82d4cd1fab9c1300c5d957dc8f48c", - "sha256:4cf7837c3b11a2dfb589f8530b3cff2bd0307ace4c301e8997e95c7468c1378e", - "sha256:4fd72fab7bddce46c6826994ce1e7de145ae1e9e106ebb8eb9ce1393ca01444d", - "sha256:5148bab4d71f58948c7c39d12b14a9005b6ab35a0bdf317a8ade9a9e4d9d0bd5", - "sha256:5869e8e130e99687d9e4be835116c4ebd83ca92e52e55810962446d841aba8de", - "sha256:602a8001bdf60e1a7d544be29c82560a7b49319a0b31d62586548835bbe2c862", - "sha256:61804231099214e2f84998316f3238c4c2c4aaec302df12b21a64d72e2a135c7", - "sha256:666c6fdcaac1f13eb982b649e1c311c08d7097cbda24f32612dae43648d8db8d", - "sha256:674eb520f02422546c40401f4efaf8207b5e29e420c17051cddf6c02783ff5ca", - "sha256:7ec960b1b942ee3c69323b8721df2a3ce28ff40e7ca47873ae35bfafeb4555ca", - "sha256:7f433be3b3f4c66016d5a20e5b4444ef833a1f802ced13a2d852c637f69729c1", - "sha256:7f8fb7f5ecf4f6355683ac6881fd64b5bb2b8a60e3ccde6ff799e48791d8f864", - "sha256:81a3a3a72c9811b56adf8bcc829b010163bb2fc308877e50e9910c9357e78521", - "sha256:858379cbb08d84fe7583231077d9a36a1a20eb72f8c9076a45df8b083724ad1d", - "sha256:8b9ba0ccd5a7f4219e67fbbe25e6b4a46ceef783c42af7dbc1da548eb28b6531", - "sha256:92af0d00091e744587221e79f68d617b432425a7e59328ca4c496f774a356071", - "sha256:9ebbdbd6a046c304b1845e96fbcc5559cd296b4dfd3ad2509e33c4d9ce07d6a1", - "sha256:9edd2856611e5050004f4722922b7b1cd6268da34102667bd49d2a2b18bafb81", - "sha256:a353bf1f565ed27ba71a419b2cd3db9d6151da426b61b289b6ba1422a702e643", - "sha256:b5b7d4a44cc0e6ff98da5d56cde794385bdd212a86563ac321ca64d7f80c80d1", - "sha256:b90f340cb6397ec7a854157fac03f0c82b744abdd1c0941a024c3c29d1340aff", - "sha256:c18a4da2f50050a03d1da5317388ef84a16013302a5281d6f64e4a3f406aabc4", - "sha256:c338ed69ad0b8f8f8920c13f529889fe0771abbb46550013e3c3d01e5174deef", - "sha256:c5a02360e73e7208a872bf65a7554c9f15df5fe063dc047f79738998b0506a14", - "sha256:c62b6fa2961a1dcc51ebe88771be5319a93fd89bd247c9ddf732bc250507bc2b", - "sha256:c812312847867b6335cfb264772f2a7e85b3b502d3a6b0586aa35e1858528ab1", - "sha256:c943b35ecdf7123b2d81d225397efddf0bce2e81db2f3ae633ead38e85cd5ade", - "sha256:ce0a29c28dfb8eccd0f16219360530bc3cfdf6bf70ca384dacd36e6c650ef8e8", - "sha256:cf80b550092cc480a0cbd0750e8189247ff45457e5a023305f7ef1bcec811616", - "sha256:cff7570d492bcf4b64cc862a6e2fb77edd5e5748ad715f487628f102815165e9", - "sha256:d2c1e559d96a7f94a4f581e2a32d6d610df5840881a8cba8f25e446f4d792df3", - "sha256:deeb3922a7a804755bbe6b5be9b312e746137a03600f488290318936c1a2d4dc", - "sha256:e28a50b5be854e18d54f75ef1bb13e1abf4bc650ab9d635e4258c58e71eb6ad5", - "sha256:e99c625b8c95d7741fe057585176b1b8783d46ed4b8932cf98ee145c4facf499", - "sha256:ec6f18f96b47299c11203edfbdc34e1b69085070d9a3d1f302810cc23ad36bf3", - "sha256:ed8bc367f725dfc5cabeed1ae079d00369900231fbb5a5280cf0736c30e2adf7", - "sha256:ee5926746232f627a3be1cc175b2cfad24d0170d520361f4ce3fa2fd83f09e1d", - "sha256:f295efcd47b6124b01255d1491f9e46f17ef40d3d7eabf7364099e463fb45f0f", - "sha256:fb0b361d73f6b8eeceba47cd37070b5e6c9de5beaeaa63a1cb35c7e1a73ef088" - ], - "version": "==3.9.10" + "sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904", + "sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873", + "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d", + "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710", + "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", + "sha256:124d5ba71fee9c9902c4a7baa9425e663f7f0aecf73d31d54fe3dd357d62c1a7", + "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce", + "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", + "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077", + "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b", + "sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f", + "sha256:215c595c792a87d4407cb72dd5e0f6ee8e694ceeb7f9102b533c5a9bf2a916bb", + "sha256:22724d80ee5a815a44fc76274bb7ba2e7464f5564aacb6ecddaa9970a83e3225", + "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae", + "sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7", + "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451", + "sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55", + "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804", + "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca", + "sha256:3d721fee37380a44f9d9ce6c701b3960239f4fb3d5ceea7f31cbd43882edaa2f", + "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", + "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667", + "sha256:56afaf1e9b02302ba636151cfc49929c1bb66b98794291afd0e5f20fecaf757c", + "sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467", + "sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a", + "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", + "sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb", + "sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b", + "sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204", + "sha256:73b92a5b69f31b1a58c0c7e31080aeaec49c6e01b9522e71ff38d08f15aa56de", + "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167", + "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1", + "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", + "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f", + "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d", + "sha256:8ab962931015f170b97a3dd7bd933399c1bae8ed8ad0fb2a7151a5654b6941c7", + "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a", + "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b", + "sha256:8e531abd745f51f8035e207e75e049553a86823d189a51809c078412cefb399a", + "sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc", + "sha256:913f629adef31d2d350d41c051ce7e33cf0fd06a5d1cb28d49b1899b23b903aa", + "sha256:976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770", + "sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120", + "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2", + "sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f", + "sha256:9d8787bdfbb65a85ea76d0e96a3b1bed7bf0fbcb16d40408dc1172ad784a49d2", + "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc", + "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", + "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872", + "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", + "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", + "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", + "sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6", + "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", + "sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91", + "sha256:bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc", + "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424", + "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e", + "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23", + "sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1", + "sha256:c5189a5dab8b0312eadaf9d58d3049b6a52c454256493a557405e77a3d67ab7f", + "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", + "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4", + "sha256:d2489b241c19582b3f1430cc5d732caefc1aaf378d97e7fb95b9e56bed11725f", + "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229", + "sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d", + "sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf", + "sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4", + "sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038", + "sha256:e0a23b41f8f98b4e61150a03f83e4f0d566880fe53519d445a962929a4d21045", + "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", + "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064", + "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc", + "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049", + "sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b", + "sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824", + "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c", + "sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6", + "sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2", + "sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e", + "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1", + "sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569", + "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c" + ], + "markers": "python_version >= '3.9'", + "version": "==3.11.3" }, "packaging": { "hashes": [ @@ -710,217 +960,429 @@ }, "pillow": { "hashes": [ - "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d", - "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de", - "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616", - "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839", - "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099", - "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a", - "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219", - "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106", - "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b", - "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412", - "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b", - "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7", - "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2", - "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7", - "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14", - "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f", - "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27", - "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57", - "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262", - "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28", - "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610", - "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172", - "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273", - "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e", - "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d", - "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818", - "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f", - "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9", - "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01", - "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7", - "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651", - "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312", - "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80", - "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666", - "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061", - "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b", - "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992", - "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593", - "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4", - "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db", - "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba", - "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd", - "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e", - "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212", - "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb", - "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2", - "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34", - "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256", - "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f", - "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2", - "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38", - "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996", - "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a", - "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793" - ], - "markers": "python_version >= '3.8'", - "version": "==10.1.0" + "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", + "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", + "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", + "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", + "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", + "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", + "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", + "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", + "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", + "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", + "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", + "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", + "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", + "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", + "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", + "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", + "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", + "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", + "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", + "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", + "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", + "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", + "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", + "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", + "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", + "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", + "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", + "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", + "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", + "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", + "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", + "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", + "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", + "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", + "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", + "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", + "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", + "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", + "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", + "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", + "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", + "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", + "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", + "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", + "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", + "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", + "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", + "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", + "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", + "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", + "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", + "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", + "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", + "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", + "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", + "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", + "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", + "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", + "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", + "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", + "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", + "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", + "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", + "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", + "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", + "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", + "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", + "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", + "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", + "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", + "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", + "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", + "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", + "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", + "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", + "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", + "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", + "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", + "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", + "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", + "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", + "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", + "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", + "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", + "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", + "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", + "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", + "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", + "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", + "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", + "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", + "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", + "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", + "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", + "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", + "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", + "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", + "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", + "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", + "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", + "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", + "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", + "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", + "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", + "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", + "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3" + ], + "markers": "python_version >= '3.9'", + "version": "==11.3.0" + }, + "propcache": { + "hashes": [ + "sha256:009093c9b5dbae114a5958e6a649f8a5d94dd6866b0f82b60395eb92c58002d4", + "sha256:015b2ca2f98ea9e08ac06eecc409d5d988f78c5fd5821b2ad42bc9afcd6b1557", + "sha256:01c0ebc172ca28e9d62876832befbf7f36080eee6ed9c9e00243de2a8089ad57", + "sha256:02e071548b6a376e173b0102c3f55dc16e7d055b5307d487e844c320e38cacf2", + "sha256:0363a696a9f24b37a04ed5e34c2e07ccbe92798c998d37729551120a1bb744c4", + "sha256:0596d2ae99d74ca436553eb9ce11fe4163dc742fcf8724ebe07d7cb0db679bb1", + "sha256:075ca32384294434344760fdcb95f7833e1d7cf7c4e55f0e726358140179da35", + "sha256:077a32977399dc05299b16e793210341a0b511eb0a86d1796873e83ce47334cc", + "sha256:082a643479f49a6778dcd68a80262fc324b14fd8e9b1a5380331fe41adde1738", + "sha256:087e2d3d7613e1b59b2ffca0daabd500c1a032d189c65625ee05ea114afcad0b", + "sha256:0964c55c95625193defeb4fd85f8f28a9a754ed012cab71127d10e3dc66b1373", + "sha256:0b04ac2120c161416c866d0b6a4259e47e92231ff166b518cc0efb95777367c3", + "sha256:0b12df77eb19266efd146627a65b8ad414f9d15672d253699a50c8205661a820", + "sha256:0cd30341142c68377cf3c4e2d9f0581e6e528694b2d57c62c786be441053d2fc", + "sha256:0ea11fceb31fa95b0fa2007037f19e922e2caceb7dc6c6cac4cb56e2d291f1a2", + "sha256:184c779363740d6664982ad05699f378f7694220e2041996f12b7c2a4acdcad0", + "sha256:1927b78dd75fc31a7fdc76cc7039e39f3170cb1d0d9a271e60f0566ecb25211a", + "sha256:1cdabd60e109506462e6a7b37008e57979e737dc6e7dfbe1437adcfe354d1a0a", + "sha256:1e7fa29c71ffa8d6a37324258737d09475f84715a6e8c350f67f0bc8e5e44993", + "sha256:1e7fd82d4a5b7583588f103b0771e43948532f1292105f13ee6f3b300933c4ca", + "sha256:2015218812ee8f13bbaebc9f52b1e424cc130b68d4857bef018e65e3834e1c4d", + "sha256:213eb0d3bc695a70cffffe11a1c2e1c2698d89ffd8dba35a49bc44a035d45c93", + "sha256:2166466a666a5bebc332cd209cad77d996fad925ca7e8a2a6310ba9e851ae641", + "sha256:227892597953611fce2601d49f1d1f39786a6aebc2f253c2de775407f725a3f6", + "sha256:22f589652ee38de96aa58dd219335604e09666092bc250c1d9c26a55bcef9932", + "sha256:236c8da353ea7c22a8e963ab78cddb1126f700ae9538e2c4c6ef471e5545494b", + "sha256:24403152e41abf09488d3ae9c0c3bf7ff93e2fb12b435390718f21810353db28", + "sha256:26692850120241a99bb4a4eec675cd7b4fdc431144f0d15ef69f7f8599f6165f", + "sha256:2a4bf309d057327f1f227a22ac6baf34a66f9af75e08c613e47c4d775b06d6c7", + "sha256:2af6de831a26f42a3f94592964becd8d7f238551786d7525807f02e53defbd13", + "sha256:2c46d37955820dd883cf9156ceb7825b8903e910bdd869902e20a5ac4ecd2c8b", + "sha256:33ad7d37b9a386f97582f5d042cc7b8d4b3591bb384cf50866b749a17e4dba90", + "sha256:34000e31795bdcda9826e0e70e783847a42e3dcd0d6416c5d3cb717905ebaec0", + "sha256:381c84a445efb8c9168f1393a5a7c566de22edc42bfe207a142fff919b37f5d9", + "sha256:399c73201d88c856a994916200d7cba41d7687096f8eb5139eb68f02785dc3f7", + "sha256:39f0f6a3b56e82dc91d84c763b783c5c33720a33c70ee48a1c13ba800ac1fa69", + "sha256:4596c12aa7e3bb2abf158ea8f79eb0fb4851606695d04ab846b2bb386f5690a1", + "sha256:4a52c25a51d5894ba60c567b0dbcf73de2f3cd642cf5343679e07ca3a768b085", + "sha256:4bf95be277fbb51513895c2cecc81ab12a421cdbd8837f159828a919a0167f96", + "sha256:4c2735d3305e6cecab6e53546909edf407ad3da5b9eeaf483f4cf80142bb21be", + "sha256:4c491462e1dc80f9deb93f428aad8d83bb286de212837f58eb48e75606e7726c", + "sha256:515b610a364c8cdd2b72c734cc97dece85c416892ea8d5c305624ac8734e81db", + "sha256:545987971b2aded25ba4698135ea0ae128836e7deb6e18c29a581076aaef44aa", + "sha256:55a54de5266bc44aa274915cdf388584fa052db8748a869e5500ab5993bac3f4", + "sha256:566552ed9b003030745e5bc7b402b83cf3cecae1bade95262d78543741786db5", + "sha256:5710b1c01472542bb024366803812ca13e8774d21381bcfc1f7ae738eeb38acc", + "sha256:5a531d29d7b873b12730972237c48b1a4e5980b98cf21b3f09fa4710abd3a8c3", + "sha256:5b113feeda47f908562d9a6d0e05798ad2f83d4473c0777dafa2bc7756473218", + "sha256:5e0a5bc019014531308fb67d86066d235daa7551baf2e00e1ea7b00531f6ea85", + "sha256:626ec13592928b677f48ff5861040b604b635e93d8e2162fb638397ea83d07e8", + "sha256:659a0ea6d9017558ed7af00fb4028186f64d0ba9adfc70a4d2c85fcd3d026321", + "sha256:65ff56a31f25925ef030b494fe63289bf07ef0febe6da181b8219146c590e185", + "sha256:681a168d06284602d56e97f09978057aa88bcc4177352b875b3d781df4efd4cb", + "sha256:6a6a36b94c09711d6397d79006ca47901539fbc602c853d794c39abd6a326549", + "sha256:6d1f67dad8cc36e8abc2207a77f3f952ac80be7404177830a7af4635a34cbc16", + "sha256:6ebc6e2e65c31356310ddb6519420eaa6bb8c30fbd809d0919129c89dcd70f4c", + "sha256:71a400b2f0b079438cc24f9a27f02eff24d8ef78f2943f949abc518b844ade3d", + "sha256:71c45f02ffbb8a21040ae816ceff7f6cd749ffac29fc0f9daa42dc1a9652d577", + "sha256:728d98179e92d77096937fdfecd2c555a3d613abe56c9909165c24196a3b5012", + "sha256:72b51340047ac43b3cf388eebd362d052632260c9f73a50882edbb66e589fd44", + "sha256:779aaae64089e2f4992e993faea801925395d26bb5de4a47df7ef7f942c14f80", + "sha256:783e91595cf9b66c2deda17f2e8748ae8591aa9f7c65dcab038872bfe83c5bb1", + "sha256:790286d3d542c0ef9f6d0280d1049378e5e776dcba780d169298f664c39394db", + "sha256:7aa8cc5c94e682dce91cb4d12d7b81c01641f4ef5b3b3dc53325d43f0e3b9f2e", + "sha256:7d51f70f77950f8efafed4383865d3533eeee52d8a0dd1c35b65f24de41de4e0", + "sha256:7da5c4c72ae40fd3ce87213ab057db66df53e55600d0b9e72e2b7f5a470a2cc4", + "sha256:7dfa60953169d2531dd8ae306e9c27c5d4e5efe7a2ba77049e8afdaece062937", + "sha256:7ea86eb32e74f9902df57e8608e8ac66f1e1e1d24d1ed2ddeb849888413b924d", + "sha256:7f088e21d15b3abdb9047e4b7b7a0acd79bf166893ac2b34a72ab1062feb219e", + "sha256:83ae2f5343f6f06f4c91ae530d95f56b415f768f9c401a5ee2a10459cf74370b", + "sha256:84f847e64f4d1a232e50460eebc1196642ee9b4c983612f41cd2d44fd2fe7c71", + "sha256:858eaabd2191dd0da5272993ad08a748b5d3ae1aefabea8aee619b45c2af4a64", + "sha256:8659f995b19185179474b18de8755689e1f71e1334d05c14e1895caa4e409cf7", + "sha256:88d50d662c917ec2c9d3858920aa7b9d5bfb74ab9c51424b775ccbe683cb1b4e", + "sha256:892a072e5b19c3f324a4f8543c9f7e8fc2b0aa08579e46f69bdf0cfc1b440454", + "sha256:8d18d796ffecdc8253742fd53a94ceee2e77ad149eb9ed5960c2856b5f692f71", + "sha256:92bc43a1ab852310721ce856f40a3a352254aa6f5e26f0fad870b31be45bba2e", + "sha256:944de70384c62d16d4a00c686b422aa75efbc67c4addaebefbb56475d1c16034", + "sha256:94a278c45e6463031b5a8278e40a07edf2bcc3b5379510e22b6c1a6e6498c194", + "sha256:94b0f7407d18001dbdcbb239512e753b1b36725a6e08a4983be1c948f5435f79", + "sha256:96153e037ae065bb71cae889f23c933190d81ae183f3696a030b47352fd8655d", + "sha256:9ba68c57cde9c667f6b65b98bc342dfa7240b1272ffb2c24b32172ee61b6d281", + "sha256:a1d5e474d43c238035b74ecf997f655afa67f979bae591ac838bb3fbe3076392", + "sha256:a4efbaf10793fd574c76a5732c75452f19d93df6e0f758c67dd60552ebd8614b", + "sha256:a60634a9de41f363923c6adfb83105d39e49f7a3058511563ed3de6748661af6", + "sha256:a7f06f077fc4ef37e8a37ca6bbb491b29e29db9fb28e29cf3896aad10dbd4137", + "sha256:a8ef2ea819549ae2e8698d2ec229ae948d7272feea1cb2878289f767b6c585a4", + "sha256:a9725d96a81e17e48a0fe82d0c3de2f5e623d7163fec70a6c7df90753edd1bec", + "sha256:ab9c1bd95ebd1689f0e24f2946c495808777e9e8df7bb3c1dfe3e9eb7f47fe0d", + "sha256:abe04e7aa5ab2e4056fcf3255ebee2071e4a427681f76d4729519e292c46ecc1", + "sha256:ae3adf88a66f5863cf79394bc359da523bb27a2ed6ba9898525a6a02b723bfc5", + "sha256:b2f29697d1110e8cdf7a39cc630498df0082d7898b79b731c1c863f77c6e8cfc", + "sha256:b730048ae8b875e2c0af1a09ca31b303fc7b5ed27652beec03fa22b29545aec9", + "sha256:bcb5bfac5b9635e6fc520c8af6efc7a0a56f12a1fe9e9d3eb4328537e316dd6a", + "sha256:bd6c6dba1a3b8949e08c4280071c86e38cb602f02e0ed6659234108c7a7cd710", + "sha256:c0e1c218fff95a66ad9f2f83ad41a67cf4d0a3f527efe820f57bde5fda616de4", + "sha256:c1443fa4bb306461a3a8a52b7de0932a2515b100ecb0ebc630cc3f87d451e0a9", + "sha256:c1ad731253eb738f9cadd9fa1844e019576c70bca6a534252e97cf33a57da529", + "sha256:c20d796210720455086ef3f85adc413d1e41d374742f9b439354f122bbc3b528", + "sha256:c2e274f3d1cbb2ddcc7a55ce3739af0f8510edc68a7f37981b2258fa1eedc833", + "sha256:c3f4b125285d354a627eb37f3ea7c13b8842c7c0d47783581d0df0e272dbf5f0", + "sha256:c9b8119244d122241a9c4566bce49bb20408a6827044155856735cf14189a7da", + "sha256:cd6e22255ed73efeaaeb1765505a66a48a9ec9ebc919fce5ad490fe5e33b1555", + "sha256:cd8684f628fe285ea5c86f88e1c30716239dc9d6ac55e7851a4b7f555b628da3", + "sha256:cdb0cecafb528ab15ed89cdfed183074d15912d046d3e304955513b50a34b907", + "sha256:d74aa60b1ec076d4d5dcde27c9a535fc0ebb12613f599681c438ca3daa68acac", + "sha256:d7f008799682e8826ce98f25e8bc43532d2cd26c187a1462499fa8d123ae054f", + "sha256:d9a8d277dc218ddf04ec243a53ac309b1afcebe297c0526a8f82320139b56289", + "sha256:da47070e1340a1639aca6b1c18fe1f1f3d8d64d3a1f9ddc67b94475f44cd40f3", + "sha256:da584d917a1a17f690fc726617fd2c3f3006ea959dae5bb07a5630f7b16f9f5f", + "sha256:de536cf796abc5b58d11c0ad56580215d231d9554ea4bb6b8b1b3bed80aa3234", + "sha256:de8e310d24b5a61de08812dd70d5234da1458d41b059038ee7895a9e4c8cae79", + "sha256:df7107a91126a495880576610ae989f19106e1900dd5218d08498391fa43b31d", + "sha256:e0ce7f3d1faf7ad58652ed758cc9753049af5308b38f89948aa71793282419c5", + "sha256:e2d01fd53e89cb3d71d20b8c225a8c70d84660f2d223afc7ed7851a4086afe6d", + "sha256:e5227da556b2939da6125cda1d5eecf9e412e58bc97b41e2f192605c3ccbb7c2", + "sha256:e6229ad15366cd8b6d6b4185c55dd48debf9ca546f91416ba2e5921ad6e210a6", + "sha256:e878553543ece1f8006d0ba4d096b40290580db173bfb18e16158045b9371335", + "sha256:eb77a85253174bf73e52c968b689d64be62d71e8ac33cabef4ca77b03fb4ef92", + "sha256:f114a3e1f8034e2957d34043b7a317a8a05d97dfe8fddb36d9a2252c0117dbbc", + "sha256:f495007ada16a4e16312b502636fafff42a9003adf1d4fb7541e0a0870bc056f", + "sha256:f5c82af8e329c3cdc3e717dd3c7b2ff1a218b6de611f6ce76ee34967570a9de9" + ], + "markers": "python_version >= '3.9'", + "version": "==0.4.0" }, "pycares": { "hashes": [ - "sha256:112a4979c695b1c86f6782163d7dec58d57a3b9510536dcf4826550f9053dd9a", - "sha256:1168a48a834813aa80f412be2df4abaf630528a58d15c704857448b20b1675c0", - "sha256:21a5a0468861ec7df7befa69050f952da13db5427ae41ffe4713bc96291d1d95", - "sha256:229a1675eb33bc9afb1fc463e73ee334950ccc485bc83a43f6ae5839fb4d5fa3", - "sha256:22c00bf659a9fa44d7b405cf1cd69b68b9d37537899898d8cbe5dffa4016b273", - "sha256:23aa3993a352491a47fcf17867f61472f32f874df4adcbb486294bd9fbe8abee", - "sha256:24da119850841d16996713d9c3374ca28a21deee056d609fbbed29065d17e1f6", - "sha256:2eeec144bcf6a7b6f2d74d6e70cbba7886a84dd373c886f06cb137a07de4954c", - "sha256:34736a2ffaa9c08ca9c707011a2d7b69074bbf82d645d8138bba771479b2362f", - "sha256:3aebc73e5ad70464f998f77f2da2063aa617cbd8d3e8174dd7c5b4518f967153", - "sha256:3eaa6681c0a3e3f3868c77aca14b7760fed35fdfda2fe587e15c701950e7bc69", - "sha256:4afc2644423f4eef97857a9fd61be9758ce5e336b4b0bd3d591238bb4b8b03e0", - "sha256:52084961262232ec04bd75f5043aed7e5d8d9695e542ff691dfef0110209f2d4", - "sha256:56cf3349fa3a2e67ed387a7974c11d233734636fe19facfcda261b411af14d80", - "sha256:5ed4e04af4012f875b78219d34434a6d08a67175150ac1b79eb70ab585d4ba8c", - "sha256:64965dc19c578a683ea73487a215a8897276224e004d50eeb21f0bc7a0b63c88", - "sha256:6ef64649eba56448f65e26546d85c860709844d2fc22ef14d324fe0b27f761a9", - "sha256:77cf5a2fd5583c670de41a7f4a7b46e5cbabe7180d8029f728571f4d2e864084", - "sha256:7bddc6adba8f699728f7fc1c9ce8cef359817ad78e2ed52b9502cb5f8dc7f741", - "sha256:813d661cbe2e37d87da2d16b7110a6860e93ddb11735c6919c8a3545c7b9c8d8", - "sha256:82bba2ab77eb5addbf9758d514d9bdef3c1bfe7d1649a47bd9a0d55a23ef478b", - "sha256:8bf2eaa83a5987e48fa63302f0fe7ce3275cfda87b34d40fef9ce703fb3ac002", - "sha256:8d186dafccdaa3409194c0f94db93c1a5d191145a275f19da6591f9499b8e7b8", - "sha256:8f64cb58729689d4d0e78f0bfb4c25ce2f851d0274c0273ac751795c04b8798a", - "sha256:902461a92b6a80fd5041a2ec5235680c7cc35e43615639ec2a40e63fca2dfb51", - "sha256:917f08f0b5d9324e9a34211e68d27447c552b50ab967044776bbab7e42a553a2", - "sha256:94d6962db81541eb0396d2f0dfcbb18cdb8c8b251d165efc2d974ae652c547d4", - "sha256:97892cced5794d721fb4ff8765764aa4ea48fe8b2c3820677505b96b83d4ef47", - "sha256:9a0303428d013ccf5c51de59c83f9127aba6200adb7fd4be57eddb432a1edd2a", - "sha256:9dc04c54c6ea615210c1b9e803d0e2d2255f87a3d5d119b6482c8f0dfa15b26b", - "sha256:a0c5368206057884cde18602580083aeaad9b860e2eac14fd253543158ce1e93", - "sha256:ad58e284a658a8a6a84af2e0b62f2f961f303cedfe551854d7bd40c3cbb61912", - "sha256:afb91792f1556f97be7f7acb57dc7756d89c5a87bd8b90363a77dbf9ea653817", - "sha256:b61579cecf1f4d616e5ea31a6e423a16680ab0d3a24a2ffe7bb1d4ee162477ff", - "sha256:b7af06968cbf6851566e806bf3e72825b0e6671832a2cbe840be1d2d65350710", - "sha256:bce8db2fc6f3174bd39b81405210b9b88d7b607d33e56a970c34a0c190da0490", - "sha256:bfb89ca9e3d0a9b5332deeb666b2ede9d3469107742158f4aeda5ce032d003f4", - "sha256:c680fef1b502ee680f8f0b95a41af4ec2c234e50e16c0af5bbda31999d3584bd", - "sha256:c6a8bde63106f162fca736e842a916853cad3c8d9d137e11c9ffa37efa818b02", - "sha256:cb49d5805cd347c404f928c5ae7c35e86ba0c58ffa701dbe905365e77ce7d641", - "sha256:ceb12974367b0a68a05d52f4162b29f575d241bd53de155efe632bf2c943c7f6", - "sha256:d33e2a1120887e89075f7f814ec144f66a6ce06a54f5722ccefc62fbeda83cff", - "sha256:db24c4e7fea4a052c6e869cbf387dd85d53b9736cfe1ef5d8d568d1ca925e977", - "sha256:e3a6f7cfdfd11eb5493d6d632e582408c8f3b429f295f8799c584c108b28db6f", - "sha256:eb66c30eb11e877976b7ead13632082a8621df648c408b8e15cdb91a452dd502", - "sha256:ed2a38e34bec6f2586435f6ff0bc5fe11d14bebd7ed492cf739a424e81681540", - "sha256:f36bdc1562142e3695555d2f4ac0cb69af165eddcefa98efc1c79495b533481f", - "sha256:f47579d508f2f56eddd16ce72045782ad3b1b3b678098699e2b6a1b30733e1c2", - "sha256:f5f646eec041db6ffdbcaf3e0756fb92018f7af3266138c756bb09d2b5baadec", - "sha256:fd644505a8cfd7f6584d33a9066d4e3d47700f050ef1490230c962de5dfb28c6", - "sha256:fff16b09042ba077f7b8aa5868d1d22456f0002574d0ba43462b10a009331677" - ], - "markers": "python_version >= '3.8'", - "version": "==4.4.0" + "sha256:00538826d2eaf4a0e4becb0753b0ac8d652334603c445c9566c9eb273657eb4c", + "sha256:066f3caa07c85e1a094aebd9e7a7bb3f3b2d97cff2276665693dd5c0cc81cf84", + "sha256:0aed0974eab3131d832e7e84a73ddb0dddbc57393cd8c0788d68a759a78c4a7b", + "sha256:1571a7055c03a95d5270c914034eac7f8bfa1b432fc1de53d871b821752191a4", + "sha256:1732db81e348bfce19c9bf9448ba660aea03042eeeea282824da1604a5bd4dcf", + "sha256:1dbbf0cfb39be63598b4cdc2522960627bf2f523e49c4349fb64b0499902ec7c", + "sha256:218619b912cef7c64a339ab0e231daea10c994a05699740714dff8c428b9694a", + "sha256:23d50a0842e8dbdddf870a7218a7ab5053b68892706b3a391ecb3d657424d266", + "sha256:29daa36548c04cdcd1a78ae187a4b7b003f0b357a2f4f1f98f9863373eedc759", + "sha256:2c296ab94d1974f8d2f76c499755a9ce31ffd4986e8898ef19b90e32525f7d84", + "sha256:2d5cac829da91ade70ce1af97dad448c6cd4778b48facbce1b015e16ced93642", + "sha256:30ceed06f3bf5eff865a34d21562c25a7f3dad0ed336b9dd415330e03a6c50c4", + "sha256:30d197180af626bb56f17e1fa54640838d7d12ed0f74665a3014f7155435b199", + "sha256:30feeab492ac609f38a0d30fab3dc1789bd19c48f725b2955bcaaef516e32a21", + "sha256:3139ec1f4450a4b253386035c5ecd2722582ae3320a456df5021ffe3f174260a", + "sha256:31b85ad00422b38f426e5733a71dfb7ee7eb65a99ea328c508d4f552b1760dc8", + "sha256:35ff1ec260372c97ed688efd5b3c6e5481f2274dea08f6c4ea864c195a9673c6", + "sha256:3784b80d797bcc2ff2bf3d4b27f46d8516fe1707ff3b82c2580dc977537387f9", + "sha256:386da2581db4ea2832629e275c061103b0be32f9391c5dfaea7f6040951950ad", + "sha256:3b44e54cad31d3c3be5e8149ac36bc1c163ec86e0664293402f6f846fb22ad00", + "sha256:3bd81ad69f607803f531ff5cfa1262391fa06e78488c13495cee0f70d02e0287", + "sha256:3d5300a598ad48bbf169fba1f2b2e4cf7ab229e7c1a48d8c1166f9ccf1755cb3", + "sha256:3db6b6439e378115572fa317053f3ee6eecb39097baafe9292320ff1a9df73e3", + "sha256:3ef1ab7abbd238bb2dbbe871c3ea39f5a7fc63547c015820c1e24d0d494a1689", + "sha256:45d3254a694459fdb0640ef08724ca9d4b4f6ff6d7161c9b526d7d2e2111379e", + "sha256:4b6f7581793d8bb3014028b8397f6f80b99db8842da58f4409839c29b16397ad", + "sha256:4da2e805ed8c789b9444ef4053f6ef8040cd13b0c1ca6d3c4fe6f9369c458cb4", + "sha256:5344d52efa37df74728505a81dd52c15df639adffd166f7ddca7a6318ecdb605", + "sha256:5d69e2034160e1219665decb8140e439afc7a7afcfd4adff08eb0f6142405c3e", + "sha256:5d70324ca1d82c6c4b00aa678347f7560d1ef2ce1d181978903459a97751543a", + "sha256:5e1ab899bb0763dea5d6569300aab3a205572e6e2d0ef1a33b8cf2b86d1312a4", + "sha256:6195208b16cce1a7b121727710a6f78e8403878c1017ab5a3f92158b048cec34", + "sha256:66c310773abe42479302abf064832f4a37c8d7f788f4d5ee0d43cbad35cf5ff4", + "sha256:6f74b1d944a50fa12c5006fd10b45e1a45da0c5d15570919ce48be88e428264c", + "sha256:6f751f5a0e4913b2787f237c2c69c11a53f599269012feaa9fb86d7cef3aec26", + "sha256:702d21823996f139874aba5aa9bb786d69e93bde6e3915b99832eb4e335d31ae", + "sha256:719f7ddff024fdacde97b926b4b26d0cc25901d5ef68bb994a581c420069936d", + "sha256:742fbaa44b418237dbd6bf8cdab205c98b3edb334436a972ad341b0ea296fb47", + "sha256:7570e0b50db619b2ee370461c462617225dc3a3f63f975c6f117e2f0c94f82ca", + "sha256:775d99966e28c8abd9910ddef2de0f1e173afc5a11cea9f184613c747373ab80", + "sha256:77bf82dc0beb81262bf1c7f546e1c1fde4992e5c8a2343b867ca201b85f9e1aa", + "sha256:7830709c23bbc43fbaefbb3dde57bdd295dc86732504b9d2e65044df8fd5e9fb", + "sha256:7aba9a312a620052133437f2363aae90ae4695ee61cb2ee07cbb9951d4c69ddd", + "sha256:80752133442dc7e6dd9410cec227c49f69283c038c316a8585cca05ec32c2766", + "sha256:836725754c32363d2c5d15b931b3ebd46b20185c02e850672cb6c5f0452c1e80", + "sha256:83a7401d7520fa14b00d85d68bcca47a0676c69996e8515d53733972286f9739", + "sha256:84b0b402dd333403fdce0e204aef1ef834d839c439c0c1aa143dc7d1237bb197", + "sha256:84fde689557361764f052850a2d68916050adbfd9321f6105aca1d8f1a9bd49b", + "sha256:87dab618fe116f1936f8461df5970fcf0befeba7531a36b0a86321332ff9c20b", + "sha256:8a75a406432ce39ce0ca41edff7486df6c970eb0fe5cfbe292f195a6b8654461", + "sha256:910ce19a549f493fb55cfd1d7d70960706a03de6bfc896c1429fc5d6216df77e", + "sha256:9518514e3e85646bac798d94d34bf5b8741ee0cb580512e8450ce884f526b7cf", + "sha256:95bc81f83fadb67f7f87914f216a0e141555ee17fd7f56e25aa0cc165e99e53b", + "sha256:96e07d5a8b733d753e37d1f7138e7321d2316bb3f0f663ab4e3d500fabc82807", + "sha256:97d971b3a88a803bb95ff8a40ea4d68da59319eb8b59e924e318e2560af8c16d", + "sha256:9a00408105901ede92e318eecb46d0e661d7d093d0a9b1224c71b5dd94f79e83", + "sha256:9d0c543bdeefa4794582ef48f3c59e5e7a43d672a4bfad9cbbd531e897911690", + "sha256:a4060d8556c908660512d42df1f4a874e4e91b81f79e3a9090afedc7690ea5ba", + "sha256:a98fac4a3d4f780817016b6f00a8a2c2f41df5d25dfa8e5b1aa0d783645a6566", + "sha256:aa160dc9e785212c49c12bb891e242c949758b99542946cc8e2098ef391f93b0", + "sha256:aca981fc00c8af8d5b9254ea5c2f276df8ece089b081af1ef4856fbcfc7c698a", + "sha256:afc6503adf8b35c21183b9387be64ca6810644ef54c9ef6c99d1d5635c01601b", + "sha256:b50ca218a3e2e23cbda395fd002d030385202fbb8182aa87e11bea0a568bd0b8", + "sha256:b93d624560ba52287873bacff70b42c99943821ecbc810b959b0953560f53c36", + "sha256:bac55842047567ddae177fb8189b89a60633ac956d5d37260f7f71b517fd8b87", + "sha256:c0eec184df42fc82e43197e073f9cc8f93b25ad2f11f230c64c2dc1c80dbc078", + "sha256:c2971af3a4094280f7c24293ff4d361689c175c1ebcbea6b3c1560eaff7cb240", + "sha256:c2af7a9d3afb63da31df1456d38b91555a6c147710a116d5cc70ab1e9f457a4f", + "sha256:c863d9003ca0ce7df26429007859afd2a621d3276ed9fef154a9123db9252557", + "sha256:c9d839b5700542b27c1a0d359cbfad6496341e7c819c7fea63db9588857065ed", + "sha256:cb711a66246561f1cae51244deef700eef75481a70d99611fd3c8ab5bd69ab49", + "sha256:cdac992206756b024b371760c55719eb5cd9d6b2cb25a8d5a04ae1b0ff426232", + "sha256:cf306f3951740d7bed36149a6d8d656a7d5432dd4bbc6af3bb6554361fc87401", + "sha256:d2a3526dbf6cb01b355e8867079c9356a8df48706b4b099ac0bf59d4656e610d", + "sha256:d552fb2cb513ce910d1dc22dbba6420758a991a356f3cd1b7ec73a9e31f94d01", + "sha256:d5fe089be67bc5927f0c0bd60c082c79f22cf299635ee3ddd370ae2a6e8b4ae0", + "sha256:dc54a21586c096df73f06f9bdf594e8d86d7be84e5d4266358ce81c04c3cc88c", + "sha256:dcd4a7761fdfb5aaac88adad0a734dd065c038f5982a8c4b0dd28efa0bd9cc7c", + "sha256:dde02314eefb85dce3cfdd747e8b44c69a94d442c0d7221b7de151ee4c93f0f5", + "sha256:df0a17f4e677d57bca3624752bbb515316522ad1ce0de07ed9d920e6c4ee5d35", + "sha256:e0fcd3a8bac57a0987d9b09953ba0f8703eb9dca7c77f7051d8c2ed001185be8", + "sha256:e2f8d9cfe0eb3a2997fde5df99b1aaea5a46dabfcfcac97b2d05f027c2cd5e28", + "sha256:ea785d1f232b42b325578f0c8a2fa348192e182cc84a1e862896076a4a2ba2a7", + "sha256:eddf5e520bb88b23b04ac1f28f5e9a7c77c718b8b4af3a4a7a2cc4a600f34502", + "sha256:ee1ea367835eb441d246164c09d1f9703197af4425fc6865cefcde9e2ca81f85", + "sha256:ee751409322ff10709ee867d5aea1dc8431eec7f34835f0f67afd016178da134", + "sha256:f199702740f3b766ed8c70efb885538be76cb48cd0cb596b948626f0b825e07a", + "sha256:f4695153333607e63068580f2979b377b641a03bc36e02813659ffbea2b76fe2", + "sha256:f6c602c5e3615abbf43dbdf3c6c64c65e76e5aa23cb74e18466b55d4a2095468", + "sha256:faa8321bc2a366189dcf87b3823e030edf5ac97a6b9a7fc99f1926c4bf8ef28e", + "sha256:ff3d25883b7865ea34c00084dd22a7be7c58fd3131db6b25c35eafae84398f9d", + "sha256:ffb22cee640bc12ee0e654eba74ecfb59e2e0aebc5bccc3cc7ef92f487008af7" + ], + "markers": "python_version >= '3.9'", + "version": "==4.11.0" }, "pycparser": { "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", + "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934" ], - "version": "==2.21" + "markers": "python_version >= '3.8'", + "version": "==2.23" }, "pymongo": { "extras": [ "srv" ], "hashes": [ - "sha256:014e7049dd019a6663747ca7dae328943e14f7261f7c1381045dfc26a04fa330", - "sha256:055f5c266e2767a88bb585d01137d9c7f778b0195d3dbf4a487ef0638be9b651", - "sha256:05c30fd35cc97f14f354916b45feea535d59060ef867446b5c3c7f9b609dd5dc", - "sha256:0634994b026336195778e5693583c060418d4ab453eff21530422690a97e1ee8", - "sha256:09c7de516b08c57647176b9fc21d929d628e35bcebc7422220c89ae40b62126a", - "sha256:107a234dc55affc5802acb3b6d83cbb8c87355b38a9457fcd8806bdeb8bce161", - "sha256:10a379fb60f1b2406ae57b8899bacfe20567918c8e9d2d545e1b93628fcf2050", - "sha256:128b1485753106c54af481789cdfea12b90a228afca0b11fb3828309a907e10e", - "sha256:1394c4737b325166a65ae7c145af1ebdb9fb153ebedd37cf91d676313e4a67b8", - "sha256:1c63e3a2e8fb815c4b1f738c284a4579897e37c3cfd95fdb199229a1ccfb638a", - "sha256:1e4ed21029d80c4f62605ab16398fe1ce093fff4b5f22d114055e7d9fbc4adb0", - "sha256:1ec71ac633b126c0775ed4604ca8f56c3540f5c21a1220639f299e7a544b55f9", - "sha256:21812453354b151200034750cd30b0140e82ec2a01fd4357390f67714a1bfbde", - "sha256:256c503a75bd71cf7fb9ebf889e7e222d49c6036a48aad5a619f98a0adf0e0d7", - "sha256:2703a9f8f5767986b4f51c259ff452cc837c5a83c8ed5f5361f6e49933743b2f", - "sha256:288c21ab9531b037f7efa4e467b33176bc73a0c27223c141b822ab4a0e66ff2a", - "sha256:2972dd1f1285866aba027eff2f4a2bbf8aa98563c2ced14cb34ee5602b36afdf", - "sha256:2973f113e079fb98515722cd728e1820282721ec9fd52830e4b73cabdbf1eb28", - "sha256:2ca0ba501898b2ec31e6c3acf90c31910944f01d454ad8e489213a156ccf1bda", - "sha256:2d2be5c9c3488fa8a70f83ed925940f488eac2837a996708d98a0e54a861f212", - "sha256:2f8c04277d879146eacda920476e93d520eff8bec6c022ac108cfa6280d84348", - "sha256:325701ae7b56daa5b0692305b7cb505ca50f80a1288abb32ff420a8a209b01ca", - "sha256:3729b8db02063da50eeb3db88a27670d85953afb9a7f14c213ac9e3dca93034b", - "sha256:3919708594b86d0f5cdc713eb6fccd3f9b9532af09ea7a5d843c933825ef56c4", - "sha256:39a1cd5d383b37285641d5a7a86be85274466ae336a61b51117155936529f9b3", - "sha256:3ec6c20385c5a58e16b1ea60c5e4993ea060540671d7d12664f385f2fb32fe79", - "sha256:47aa128be2e66abd9d1a9b0437c62499d812d291f17b55185cb4aa33a5f710a4", - "sha256:49f2af6cf82509b15093ce3569229e0d53c90ad8ae2eef940652d4cf1f81e045", - "sha256:4a0269811661ba93c472c8a60ea82640e838c2eb148d252720a09b5123f2c2fe", - "sha256:518c90bdd6e842c446d01a766b9136fec5ec6cc94f3b8c3f8b4a332786ee6b64", - "sha256:5717a308a703dda2886a5796a07489c698b442f5e409cf7dc2ac93de8d61d764", - "sha256:5802acc012bbb4bce4dff92973dff76482f30ef35dd4cb8ab5b0e06aa8f08c80", - "sha256:5e63146dbdb1eac207464f6e0cfcdb640c9c5ff0f57b754fa96fe252314a1dc6", - "sha256:6695d7136a435c1305b261a9ddb9b3ecec9863e05aab3935b96038145fd3a977", - "sha256:680fa0fc719e1a3dcb81130858368f51d83667d431924d0bcf249644bce8f303", - "sha256:6b18276f14b4b6d92e707ab6db19b938e112bd2f1dc3f9f1a628df58e4fd3f0d", - "sha256:6bafea6061d63059d8bc2ffc545e2f049221c8a4457d236c5cd6a66678673eab", - "sha256:6d6a1b1361f118e7fefa17ae3114e77f10ee1b228b20d50c47c9f351346180c8", - "sha256:747c84f4e690fbe6999c90ac97246c95d31460d890510e4a3fa61b7d2b87aa34", - "sha256:79f41576b3022c2fe9780ae3e44202b2438128a25284a8ddfa038f0785d87019", - "sha256:7b0e6361754ac596cd16bfc6ed49f69ffcd9b60b7bc4bcd3ea65c6a83475e4ff", - "sha256:7e3b0127b260d4abae7b62203c4c7ef0874c901b55155692353db19de4b18bc4", - "sha256:7fc2bb8a74dcfcdd32f89528e38dcbf70a3a6594963d60dc9595e3b35b66e414", - "sha256:806e094e9e85d8badc978af8c95b69c556077f11844655cb8cd2d1758769e521", - "sha256:81dd1308bd5630d2bb5980f00aa163b986b133f1e9ed66c66ce2a5bc3572e891", - "sha256:82e620842e12e8cb4050d2643a81c8149361cd82c0a920fa5a15dc4ca8a4000f", - "sha256:85f2cdc400ee87f5952ebf2a117488f2525a3fb2e23863a8efe3e4ee9e54e4d1", - "sha256:8ab6bcc8e424e07c1d4ba6df96f7fb963bcb48f590b9456de9ebd03b88084fe8", - "sha256:8adf014f2779992eba3b513e060d06f075f0ab2fb3ad956f413a102312f65cdf", - "sha256:9b0f98481ad5dc4cb430a60bbb8869f05505283b9ae1c62bdb65eb5e020ee8e3", - "sha256:9bea9138b0fc6e2218147e9c6ce1ff76ff8e29dc00bb1b64842bd1ca107aee9f", - "sha256:a09bfb51953930e7e838972ddf646c5d5f984992a66d79da6ba7f6a8d8a890cd", - "sha256:a0be99b599da95b7a90a918dd927b20c434bea5e1c9b3efc6a3c6cd67c23f813", - "sha256:a49aca4d961823b2846b739380c847e8964ff7ae0f0a683992b9d926054f0d6d", - "sha256:a4dc1319d0c162919ee7f4ee6face076becae2abbd351cc14f1fe70af5fb20d9", - "sha256:a8273e1abbcff1d7d29cbbb1ea7e57d38be72f1af3c597c854168508b91516c2", - "sha256:a8f7f9feecae53fa18d6a3ea7c75f9e9a1d4d20e5c3f9ce3fba83f07bcc4eee2", - "sha256:ad4f66fbb893b55f96f03020e67dcab49ffde0177c6565ccf9dec4fdf974eb61", - "sha256:af425f323fce1b07755edd783581e7283557296946212f5b1a934441718e7528", - "sha256:b14dd73f595199f4275bed4fb509277470d9b9059310537e3b3daba12b30c157", - "sha256:b4ad70d7cac4ca0c7b31444a0148bd3af01a2662fa12b1ad6f57cd4a04e21766", - "sha256:b80a4ee19b3442c57c38afa978adca546521a8822d663310b63ae2a7d7b13f3a", - "sha256:ba51129fcc510824b6ca6e2ce1c27e3e4d048b6e35d3ae6f7e517bed1b8b25ce", - "sha256:c011bd5ad03cc096f99ffcfdd18a1817354132c1331bed7a837a25226659845f", - "sha256:cc94f9fea17a5af8cf1a343597711a26b0117c0b812550d99934acb89d526ed2", - "sha256:ccd785fafa1c931deff6a7116e9a0d402d59fabe51644b0d0c268295ff847b25", - "sha256:d16a534da0e39785687b7295e2fcf9a339f4a20689024983d11afaa4657f8507", - "sha256:d3077a31633beef77d057c6523f5de7271ddef7bde5e019285b00c0cc9cac1e3", - "sha256:d603edea1ff7408638b2504905c032193b7dcee7af269802dbb35bc8c3310ed5", - "sha256:db082f728160369d9a6ed2e722438291558fc15ce06d0a7d696a8dad735c236b", - "sha256:ddef295aaf80cefb0c1606f1995899efcb17edc6b327eb6589e234e614b87756", - "sha256:e16ade71c93f6814d095d25cd6d28a90d63511ea396bd96e9ffcb886b278baaa", - "sha256:e3db7d833a7c38c317dc95b54e27f1d27012e031b45a7c24e360b53197d5f6e7", - "sha256:e5e193f89f4f8c1fe273f9a6e6df915092c9f2af6db2d1afb8bd53855025c11f", - "sha256:eb438a8bf6b695bf50d57e6a059ff09652a07968b2041178b3744ea785fcef9b", - "sha256:ebf02c32afa6b67e5861a27183dd98ed88419a94a2ab843cc145fb0bafcc5b28", - "sha256:ecd9e1fa97aa11bf67472220285775fa15e896da108f425e55d23d7540a712ce", - "sha256:ef67fedd863ffffd4adfd46d9d992b0f929c7f61a8307366d664d93517f2c78e", - "sha256:f28ae33dc5a0b9cee06e95fd420e42155d83271ab75964baf747ce959cac5f52", - "sha256:fb1c56d891f9e34303c451998ef62ba52659648bb0d75b03c5e4ac223a3342c2", - "sha256:fe03bf25fae4b95d8afe40004a321df644400fdcba4c8e5e1a19c1085b740888" - ], - "markers": "python_version >= '3.7'", - "version": "==4.6.0" + "sha256:024e735127e7f39763eb3043c628a857cbf6cdfabd9bf5a1825f3597bab74352", + "sha256:03bd7284fd299c7df09cebeb53f3251e0b4850fba71bf63cbbff1ddf20cc569d", + "sha256:05b9eab3bc049f8fd150869375eff3a85ceab606531b6226b60f054daf7d1368", + "sha256:05e96219d1acca15643042478103241771e46a3f5331bae3e19d2ea2756968b1", + "sha256:0ad7ec297e8999b0f984c998e2c1c605f11e9b8c7682256bbbc53c24a195f76e", + "sha256:1393e7e9d2fc291e0d72f8583ef58e1999e84f7cd01d5a72db53b4ed1ae44686", + "sha256:18bc73e47d21cabfde719d0cc5aa6b556856993397c9433d934089c86732e3d3", + "sha256:1b5c61e32c6afbf35a9c6638db892f75dc53ebcd45a9a1bf992ffff0ec28aaaa", + "sha256:1db14e952ceb574cb8acacf063040e2a6e9570bd50671fa903fb47adb7cf49cc", + "sha256:1f2af4b98fc6d54489d187c0faa12bfbf0ef6c56c3e735eeb837ac8ff235b490", + "sha256:20ee2722ac45fba2c502edbc5281b6efcd8601d94ae1900a48c106459a1715d7", + "sha256:21b1d1d33bdbc87c1a082b747aa9ab40a30638c4e58e799d8fe9f5cb15feb38f", + "sha256:22ad78ac0222b8c5f5a28cdf6300cf19481fff193110506768c9915c8cd3396b", + "sha256:29645a9a8166f20b3fc6aa05095af6caf8ee9af9a4cf23cd857576084e29cc9c", + "sha256:2968cf01e2257f2f5193aba259116c1e9e56f739a16eceef36e85a55edc91604", + "sha256:2b6c8588b04e304bb4670e5409b3def2d9daedb8f719d47780a59de7227f1d3f", + "sha256:2c816a9e9d4aaaa0e4e9fb2534b72957666d262f3ce874a0408f8b925cfd4d99", + "sha256:2daa9434828a5e5638b9d78f0031c9e19b5bc84ce9f5e69cf6083f58aa3e3901", + "sha256:2fad596a092ab9cd821c98d75b48dd6a9c3fc52df8b1453d2f10d8219676269a", + "sha256:338b29d89f92c665a1038d53c7cc68869e2a04e171dd4fb2d416d7ad263dc50a", + "sha256:38785ba507a019edb742e333c6bf2fa3644043f1ce79ef4d20a4f7bb2180ee74", + "sha256:3ab5ba56b868c56a38cfeb3202ee78dcdd4152bc364d24b71aaf1ee3994c7f96", + "sha256:3dc0ec9d78d4f28a24bd965e06c0a77459086522005aa199a8e4fc652ed1ce8e", + "sha256:3fafe5ef96943ab9b837f89b6abe779951102bee44d21c743259d43cfc1d9f6e", + "sha256:42acd45f7030743eed3d5e66a03dd3e9c12c7869301d123bffa1f71dc0e3f882", + "sha256:45103766c3f1bf1f5fc2da43a48dbe03a343389a334eb1d02ef39024957cdc91", + "sha256:475a97d48be850140f2455ed4a2a9920f70538b0824c53ffa5deeb940f49fdb3", + "sha256:50307e2403f0cfdf4fd0f5c6c9a45edbb4c5fa63196e1671b7fed5bbcd884109", + "sha256:50325282876a263ece78371319e78518dd034e434c11e3ab12402547292b8fd5", + "sha256:557611de3fa33bd5b8e5d38a7056b15d5c38361af50378ff5cf8b9cbf371913b", + "sha256:587202db4a64d5c091bc39695095af461a6a08b2a52ddd881a6e5cb34244d672", + "sha256:597dce90bc607b3180735a6692abcf75c111d7f6169b2b1cca1db85086ee980c", + "sha256:5b72de1a2f8cc52561c65a5062ade72e06da76e0899d4f11ae1c1bced1534ff8", + "sha256:5fa558bc6320e1183965db06e069973c9642b971a37729a8ae23c37f1c13ce21", + "sha256:6462763611c5fb97b56fcd3a62c325bc8111c686c7768f50668882d21b4f28f4", + "sha256:68fbc920409bd96e6a63b651254baa45e27473c4b73232a3fb5662279383a622", + "sha256:69515e1042a2fb4fadf6384918be34703aa2c792b9a8d3d406ad43e07cb095a2", + "sha256:6d797730d07bff953d05693e75d9575e7357739a5eb2747521987d2f99b9899f", + "sha256:7089a1f2d5883f5137f1c2766691db904741985bb7e7a400ed50c3b370507b17", + "sha256:7466840413fbd23605e9f95b702374b061525e85ea7f47dca6a88981455490ec", + "sha256:75f6f8363a57ba8a6bb1076114dc9aa29336f525c8b621cc1e4cfccae4ff546a", + "sha256:76c1c013bc577c7fb2c9a69d52ee335672eac1bdbfb9c37a432bb155bc69ffdc", + "sha256:780447f9112f0e57d821ced8d593657b45616f3821becb0740e6c0fc38b0e91e", + "sha256:82ba58edb6f6112aac543214ac22fc8e3d569372a7b3a180511cf4a70bd4c0ef", + "sha256:9fa833908d94b5869e6c9a53b778dc8235caca6fcda03aac8410b8f067cd8a6f", + "sha256:a15ad3f11556a30e5dd86344567e85eb46550b09e0ea8d3297476788f0c76d77", + "sha256:a244e27c034707f48f979fdcebe0df47ea000fd52ee1b2b2d2d2cb5b7b0e24dd", + "sha256:a33e118e14bd350bef6127a000c8d08e6bade8b9045bcd70d09a665434035705", + "sha256:a42ad84dfab44218f264e2d68b79e0e684c03c66fe8180a7961d6eb670eec4a3", + "sha256:a4a0d3cf68f9bf84a7ee737ba0b29265cfeeaf586a856c6d7773491c545e5230", + "sha256:a775371086ff63da1ae97f676bcb5556c86e4e281ccac998d49d6e24efa50ca1", + "sha256:a98f67df7aae325c0476aa453877475f9a1160f84b7e6e24e4804498ef99178e", + "sha256:a994b40542ba44748af9e382fd54e69428f40c1728ae06bc649c87a1135d1cfb", + "sha256:b5fe426128a03393d2e7f10169e1f10cf6a6355f40876f52b51a03721c12e6e5", + "sha256:bf646006bfce5e153cc838adaee319ff8a3d625978d491208cc290e89f9c2a21", + "sha256:c434219d66bf20f46011cc11b28e0dbfeb965f7a7cfd1e4b8e733a5f642ae1c2", + "sha256:c95a8d0ca11d16e325749fbd9f7d9aeb9a90241245e419007a941f446ff94dd6", + "sha256:cedfd1be19c8f7b41a1f5fbaea299303087b5d40605e956ffbcfe2adc76de0ec", + "sha256:d54b8139979e6e2ee6fec91b189e948ee2d83f125957793cf191c5e33be567e7", + "sha256:d9410537204bb9e83f1c5e43f6e5df2c0d3fe092dbd8d30bd883736818a6d786", + "sha256:ddf9face1dadf4cce4578dda29547ca2af6df09d44e7dd1bd6fe185f7c18dfc9", + "sha256:de0c88d7229a96a5bfe2827170578bcd871ee16843c47e5cb3290edf1aaf62ca", + "sha256:dece75a28450fa813040b13f7fbe80a614d02e04f7ff84255a2600c440bf227a", + "sha256:e6a22f0349142c92bb2ccbd35a8a0b7dc5a2eeac14217fb28cfa9956bcfee139", + "sha256:eb1423432631994d965e92ee63e448627d57793fd780c56c49570f12d4be1ff4", + "sha256:ebd6f6eed7c6c92af3d3fa375ecd0e730da628c59a95aca5b2445fbb3d1eb874", + "sha256:ee0ab602d309c6f903c002f8705b92459d885349f1f9561040f34a2a06c84891", + "sha256:f03716cfd4c86e3a8537ab8e1169cec26d532cc70fcd02e30027820ac587d28b", + "sha256:f5bfbde2ccc88b96d56cdc6bd104b856d9b039aa209b491311f8012e611d9f58", + "sha256:f736f1a6d85f3b1c182018ae0e6c387bb342935e3b97637c258b9b46e0509af2", + "sha256:f9d3bbb4741a6ab81dbcd73b0754725f304b118c4c738449639fd060ee8b5da9" + ], + "markers": "python_version >= '3.9'", + "version": "==4.15.2" }, "python-dateutil": { "hashes": [ @@ -928,7 +1390,7 @@ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.8.2" }, "python-dotenv": { @@ -951,64 +1413,70 @@ }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.17.0" }, "tinycss2": { "hashes": [ - "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847", - "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627" + "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", + "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289" ], - "markers": "python_version >= '3.7'", - "version": "==1.2.1" + "markers": "python_version >= '3.8'", + "version": "==1.4.0" }, "urllib3": { "hashes": [ - "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", - "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" ], - "markers": "python_version >= '3.8'", - "version": "==2.1.0" + "markers": "python_version >= '3.9'", + "version": "==2.5.0" }, "uvloop": { "hashes": [ - "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd", - "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec", - "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b", - "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc", - "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797", - "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5", - "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2", - "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d", - "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be", - "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd", - "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12", - "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17", - "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef", - "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24", - "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428", - "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1", - "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849", - "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593", - "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd", - "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67", - "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6", - "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3", - "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd", - "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8", - "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7", - "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533", - "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957", - "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650", - "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e", - "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7", - "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256" + "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", + "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", + "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc", + "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414", + "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", + "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", + "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd", + "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", + "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", + "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", + "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", + "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a", + "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", + "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2", + "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0", + "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", + "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", + "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", + "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", + "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", + "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75", + "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", + "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", + "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", + "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", + "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", + "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206", + "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", + "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", + "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b", + "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", + "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79", + "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", + "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe", + "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", + "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", + "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2" ], "markers": "sys_platform != 'win32'", - "version": "==0.19.0" + "version": "==0.21.0" }, "webencodings": { "hashes": [ @@ -1019,118 +1487,255 @@ }, "yarl": { "hashes": [ - "sha256:09c19e5f4404574fcfb736efecf75844ffe8610606f3fccc35a1515b8b6712c4", - "sha256:0ab5baaea8450f4a3e241ef17e3d129b2143e38a685036b075976b9c415ea3eb", - "sha256:0d155a092bf0ebf4a9f6f3b7a650dc5d9a5bbb585ef83a52ed36ba46f55cc39d", - "sha256:126638ab961633f0940a06e1c9d59919003ef212a15869708dcb7305f91a6732", - "sha256:1a0a4f3aaa18580038cfa52a7183c8ffbbe7d727fe581300817efc1e96d1b0e9", - "sha256:1d93461e2cf76c4796355494f15ffcb50a3c198cc2d601ad8d6a96219a10c363", - "sha256:26a1a8443091c7fbc17b84a0d9f38de34b8423b459fb853e6c8cdfab0eacf613", - "sha256:271d63396460b6607b588555ea27a1a02b717ca2e3f2cf53bdde4013d7790929", - "sha256:28a108cb92ce6cf867690a962372996ca332d8cda0210c5ad487fe996e76b8bb", - "sha256:29beac86f33d6c7ab1d79bd0213aa7aed2d2f555386856bb3056d5fdd9dab279", - "sha256:2c757f64afe53a422e45e3e399e1e3cf82b7a2f244796ce80d8ca53e16a49b9f", - "sha256:2dad8166d41ebd1f76ce107cf6a31e39801aee3844a54a90af23278b072f1ccf", - "sha256:2dc72e891672343b99db6d497024bf8b985537ad6c393359dc5227ef653b2f17", - "sha256:2f3c8822bc8fb4a347a192dd6a28a25d7f0ea3262e826d7d4ef9cc99cd06d07e", - "sha256:32435d134414e01d937cd9d6cc56e8413a8d4741dea36af5840c7750f04d16ab", - "sha256:3cfa4dbe17b2e6fca1414e9c3bcc216f6930cb18ea7646e7d0d52792ac196808", - "sha256:3d5434b34100b504aabae75f0622ebb85defffe7b64ad8f52b8b30ec6ef6e4b9", - "sha256:4003f380dac50328c85e85416aca6985536812c082387255c35292cb4b41707e", - "sha256:44e91a669c43f03964f672c5a234ae0d7a4d49c9b85d1baa93dec28afa28ffbd", - "sha256:4a14907b597ec55740f63e52d7fee0e9ee09d5b9d57a4f399a7423268e457b57", - "sha256:4ce77d289f8d40905c054b63f29851ecbfd026ef4ba5c371a158cfe6f623663e", - "sha256:4d6d74a97e898c1c2df80339aa423234ad9ea2052f66366cef1e80448798c13d", - "sha256:51382c72dd5377861b573bd55dcf680df54cea84147c8648b15ac507fbef984d", - "sha256:525cd69eff44833b01f8ef39aa33a9cc53a99ff7f9d76a6ef6a9fb758f54d0ff", - "sha256:53ec65f7eee8655bebb1f6f1607760d123c3c115a324b443df4f916383482a67", - "sha256:5f74b015c99a5eac5ae589de27a1201418a5d9d460e89ccb3366015c6153e60a", - "sha256:6280353940f7e5e2efaaabd686193e61351e966cc02f401761c4d87f48c89ea4", - "sha256:632c7aeb99df718765adf58eacb9acb9cbc555e075da849c1378ef4d18bf536a", - "sha256:6465d36381af057d0fab4e0f24ef0e80ba61f03fe43e6eeccbe0056e74aadc70", - "sha256:66a6dbf6ca7d2db03cc61cafe1ee6be838ce0fbc97781881a22a58a7c5efef42", - "sha256:6d350388ba1129bc867c6af1cd17da2b197dff0d2801036d2d7d83c2d771a682", - "sha256:7217234b10c64b52cc39a8d82550342ae2e45be34f5bff02b890b8c452eb48d7", - "sha256:721ee3fc292f0d069a04016ef2c3a25595d48c5b8ddc6029be46f6158d129c92", - "sha256:72a57b41a0920b9a220125081c1e191b88a4cdec13bf9d0649e382a822705c65", - "sha256:73cc83f918b69110813a7d95024266072d987b903a623ecae673d1e71579d566", - "sha256:778df71c8d0c8c9f1b378624b26431ca80041660d7be7c3f724b2c7a6e65d0d6", - "sha256:79e1df60f7c2b148722fb6cafebffe1acd95fd8b5fd77795f56247edaf326752", - "sha256:7c86d0d0919952d05df880a1889a4f0aeb6868e98961c090e335671dea5c0361", - "sha256:7eaf13af79950142ab2bbb8362f8d8d935be9aaf8df1df89c86c3231e4ff238a", - "sha256:828235a2a169160ee73a2fcfb8a000709edf09d7511fccf203465c3d5acc59e4", - "sha256:8535e111a064f3bdd94c0ed443105934d6f005adad68dd13ce50a488a0ad1bf3", - "sha256:88d2c3cc4b2f46d1ba73d81c51ec0e486f59cc51165ea4f789677f91a303a9a7", - "sha256:8a2538806be846ea25e90c28786136932ec385c7ff3bc1148e45125984783dc6", - "sha256:8dab30b21bd6fb17c3f4684868c7e6a9e8468078db00f599fb1c14e324b10fca", - "sha256:8f18a7832ff85dfcd77871fe677b169b1bc60c021978c90c3bb14f727596e0ae", - "sha256:946db4511b2d815979d733ac6a961f47e20a29c297be0d55b6d4b77ee4b298f6", - "sha256:96758e56dceb8a70f8a5cff1e452daaeff07d1cc9f11e9b0c951330f0a2396a7", - "sha256:9a172c3d5447b7da1680a1a2d6ecdf6f87a319d21d52729f45ec938a7006d5d8", - "sha256:9a5211de242754b5e612557bca701f39f8b1a9408dff73c6db623f22d20f470e", - "sha256:9df9a0d4c5624790a0dea2e02e3b1b3c69aed14bcb8650e19606d9df3719e87d", - "sha256:aa4643635f26052401750bd54db911b6342eb1a9ac3e74f0f8b58a25d61dfe41", - "sha256:aed37db837ecb5962469fad448aaae0f0ee94ffce2062cf2eb9aed13328b5196", - "sha256:af52725c7c39b0ee655befbbab5b9a1b209e01bb39128dce0db226a10014aacc", - "sha256:b0b8c06afcf2bac5a50b37f64efbde978b7f9dc88842ce9729c020dc71fae4ce", - "sha256:b61e64b06c3640feab73fa4ff9cb64bd8182de52e5dc13038e01cfe674ebc321", - "sha256:b7831566595fe88ba17ea80e4b61c0eb599f84c85acaa14bf04dd90319a45b90", - "sha256:b8bc5b87a65a4e64bc83385c05145ea901b613d0d3a434d434b55511b6ab0067", - "sha256:b8d51817cf4b8d545963ec65ff06c1b92e5765aa98831678d0e2240b6e9fd281", - "sha256:b9f9cafaf031c34d95c1528c16b2fa07b710e6056b3c4e2e34e9317072da5d1a", - "sha256:bb72d2a94481e7dc7a0c522673db288f31849800d6ce2435317376a345728225", - "sha256:c25ec06e4241e162f5d1f57c370f4078797ade95c9208bd0c60f484834f09c96", - "sha256:c405d482c320a88ab53dcbd98d6d6f32ada074f2d965d6e9bf2d823158fa97de", - "sha256:c4472fe53ebf541113e533971bd8c32728debc4c6d8cc177f2bff31d011ec17e", - "sha256:c4b1efb11a8acd13246ffb0bee888dd0e8eb057f8bf30112e3e21e421eb82d4a", - "sha256:c5f3faeb8100a43adf3e7925d556801d14b5816a0ac9e75e22948e787feec642", - "sha256:c6f034386e5550b5dc8ded90b5e2ff7db21f0f5c7de37b6efc5dac046eb19c10", - "sha256:c99ddaddb2fbe04953b84d1651149a0d85214780e4d0ee824e610ab549d98d92", - "sha256:ca6b66f69e30f6e180d52f14d91ac854b8119553b524e0e28d5291a724f0f423", - "sha256:cccdc02e46d2bd7cb5f38f8cc3d9db0d24951abd082b2f242c9e9f59c0ab2af3", - "sha256:cd49a908cb6d387fc26acee8b7d9fcc9bbf8e1aca890c0b2fdfd706057546080", - "sha256:cf7a4e8de7f1092829caef66fd90eaf3710bc5efd322a816d5677b7664893c93", - "sha256:cfd77e8e5cafba3fb584e0f4b935a59216f352b73d4987be3af51f43a862c403", - "sha256:d34c4f80956227f2686ddea5b3585e109c2733e2d4ef12eb1b8b4e84f09a2ab6", - "sha256:d61a0ca95503867d4d627517bcfdc28a8468c3f1b0b06c626f30dd759d3999fd", - "sha256:d81657b23e0edb84b37167e98aefb04ae16cbc5352770057893bd222cdc6e45f", - "sha256:d92d897cb4b4bf915fbeb5e604c7911021a8456f0964f3b8ebbe7f9188b9eabb", - "sha256:dd318e6b75ca80bff0b22b302f83a8ee41c62b8ac662ddb49f67ec97e799885d", - "sha256:dd952b9c64f3b21aedd09b8fe958e4931864dba69926d8a90c90d36ac4e28c9a", - "sha256:e0e7e83f31e23c5d00ff618045ddc5e916f9e613d33c5a5823bc0b0a0feb522f", - "sha256:e0f17d1df951336a02afc8270c03c0c6e60d1f9996fcbd43a4ce6be81de0bd9d", - "sha256:e2a16ef5fa2382af83bef4a18c1b3bcb4284c4732906aa69422cf09df9c59f1f", - "sha256:e36021db54b8a0475805acc1d6c4bca5d9f52c3825ad29ae2d398a9d530ddb88", - "sha256:e73db54c967eb75037c178a54445c5a4e7461b5203b27c45ef656a81787c0c1b", - "sha256:e741bd48e6a417bdfbae02e088f60018286d6c141639359fb8df017a3b69415a", - "sha256:f7271d6bd8838c49ba8ae647fc06469137e1c161a7ef97d778b72904d9b68696", - "sha256:fc391e3941045fd0987c77484b2799adffd08e4b6735c4ee5f054366a2e1551d", - "sha256:fc94441bcf9cb8c59f51f23193316afefbf3ff858460cb47b5758bf66a14d130", - "sha256:fe34befb8c765b8ce562f0200afda3578f8abb159c76de3ab354c80b72244c41", - "sha256:fe8080b4f25dfc44a86bedd14bc4f9d469dfc6456e6f3c5d9077e81a5fedfba7", - "sha256:ff34cb09a332832d1cf38acd0f604c068665192c6107a439a92abfd8acf90fe2" - ], - "markers": "python_version >= '3.7'", - "version": "==1.9.3" + "sha256:019c2798df9d74fe8fb9cc916702966dad7e2e3eef66b4c19f8084ba5e0b6ecd", + "sha256:01ef0d7f1dd60d241529dc79a3fa647451056394f9a5ed05fbceeb5009de6122", + "sha256:028c59136b65fccfe5578520a3fb2a94e06601c545ca0125b7e07b3a39f238a5", + "sha256:0416fde6dc89866f4ff494a0ffcc4b2da984cf61aaa279c14a53495e8520c809", + "sha256:056fc431f10ae35aa2375c9de2b68176b34f54fb7de8bc2e830564e2a3d29efa", + "sha256:069cfc781f5d68389c8a4228f720cab453e1b6fa606bcd710a9cc01e38ffe2c1", + "sha256:06c71f698ac5b5bfdde1ce3d58d262235b3c1109f083286c96c01cee64ebf705", + "sha256:0a9454d4c513a3aa2fd87471126e0d32b01f1bf58d49309a84431521488b30c4", + "sha256:0a94664fe3c6dd44c36e875af0f338769dc9f80a1ccd58f53cf5f5b8341e8627", + "sha256:0aaa36261a1279b03fa0655a9bd879cc42e06406adaae0150fde25c778393fcb", + "sha256:0ab4e81b455dd8beb2537648be972eb63351bbe34fb55457054392fee759ac9c", + "sha256:0b16c889a7168ecf7242946dec013c9fb82ade70ab8e6b5d3290383390083a2b", + "sha256:0cc3eeea8f527119aac1b0c874bbb8092675da85fd6d9d91946cf7be7d59477b", + "sha256:0d37bf6f601c714b536159715d9ec6e69bf8a94dc593abe54c1b43ac339eb5e7", + "sha256:0e485c4f9f5b5b9fc10b4bb0ba5baf145ed0a702756da126c9f62f8a89b391a8", + "sha256:10580c7d9b50c883b93cc0ab5c91df5cc1e5b18713736471d622776b01c36810", + "sha256:1107b93c32cf7d7e2ece9bbb1b1820ecb923cfea24c8aa599a309434ed37d707", + "sha256:13c9b91e2e1224a8d33addc1bd58bb097396519c4c49524843947776b8dd45da", + "sha256:140402fef1f482840fcd4d2ee9bfd07f08bfb2c80dd215220bd47f6f3566b882", + "sha256:14872677213d96552268f927982d4c83f5d0674b0d54b623d8e909710460ab14", + "sha256:16957642c0594feba56a4bb430eea5f9132bb589ebb8b4740c0d47f022a5d976", + "sha256:1743d35529a8b9b2b6a9e5f00076c2c146726453051621b739b081dda382ee70", + "sha256:1754b3380ffef931b8eae3bbe6fc0b249db56294ffeb6e6124c2d031a82a3a92", + "sha256:178860382595f3b1fab2596b19570adc495c6211eee8b10a4112ce96342f6515", + "sha256:18e8272a4166d2bb68d51f86445f061aac21fcfc9c1d5b1187f9703f362d85dd", + "sha256:190356a39fed15109ab95600f8ff59c1a0665625f4cfe910388c82b965edaf87", + "sha256:19df967a905f2f9a09733dfb397baa6807772502931000f881943d7cfc6e9f47", + "sha256:1a0ba7cd4eabb7433e69737f33333d9e79d8ab6dbaa2f4d7313ad6611200cc65", + "sha256:1b5d29c1a86cc63e55f69253b8c817091884c4e1b79ee762a8643de834e70a64", + "sha256:1bff86850033508af0a7f9973ced23a16de7ba4ce30521080e2330475b8711b5", + "sha256:1e0b01fa225ec12e54c73be383326ae2a4a59a4a465a0e6cac679f314ed85d1f", + "sha256:20b2dca6588f65b5def8e8eae4a087d504eacf34b5b435c021cc233ce82f6c15", + "sha256:212a5c72d551f94b7799b5de1cc55ddcf3c69ac462f7c0df1beee7e47edb9fef", + "sha256:221aa7c16055e8b9f2eba718cbbf10f174e47f02e659156804d9679654c5cbb0", + "sha256:2227fcc88bebdc90ed87d924bdf8a76a730fc91796641e41ca747aabd13a5074", + "sha256:2584651c047718ec4a863ee81a5432f6f68974e6f0c58975f0aab408ff839798", + "sha256:26940710eece6b5b08a108e81d6325b47610990cd8bb28886e27d4a0d6d60930", + "sha256:2a05a5e018de23c4d2d75c8fbd8b58aba5199f752326f60a22aa37ef28d987bd", + "sha256:2b2f8e0bbdf49530ed09b2bc988082cab6ce24f4c49a0efd2ff5d9477cb29084", + "sha256:2b841c5529f9ca28cf23ed34c8040547ca6530b968a482c14de96a6ade470ee5", + "sha256:30b6a56388963ebe5428d835112439563bcaa9c8349742aeac68a8ad5a231221", + "sha256:37b5e7bba1f6df45058cff626c83a0e8a1259363095e768046a3da40b24e9c4f", + "sha256:3b539230fd64f283594a56633a9751d299cde5ab9c2791452ccb47a865842fa8", + "sha256:3cbae81bff4014ca7745fa11f7015f784198fadba8935cf5a71e139b0b124ff0", + "sha256:430e162d961af58f3dcac58aed038ba974ec7a73803ac6545db2338fbd0f4ed3", + "sha256:45aa7711e1933bac1679f9534f112767f1fe64c97a8576294b760015d0fb65e7", + "sha256:45f17adf1b8bc56becb1bc38f293b1714866786c9d79e245fb3d3731788622a6", + "sha256:4b449296e2ba009481385349138130f209bb502c4f890b3298bf3ea13d43a6d5", + "sha256:4d5af10c9f580002c0ea6c8f345c8cadb2e0c53dce77d3f2639b9e31e5f24d3d", + "sha256:4ee80f79c928ce7c18cf3ad18a5da7f3f0f1b08923e08d87143d628a6d5d2dba", + "sha256:4fcce63c1117ef0630a92a0bda3028a96dc17feed2c78c713de4c963d13d1881", + "sha256:5110ebfe3cbf892b41590fcf4aa70a17ac0a5e9a73b4a8945010bdb970ff1b93", + "sha256:52a8b7541c5d8240ae32d12014f8448e29e1ae794f9443ea020b926cff8691e1", + "sha256:56ead8d62b346c1ec67a6e8b2f66885180ea5bec05821d309ac1cb99ff4aacf5", + "sha256:5c0123db2d86d169554d5fb19421e8e455efcfe2e8e254328b85c77e712ab506", + "sha256:5e7d24e9c3b638f046fcd9a5374818257a8c6d1c3fc7542887521b81a970fbc2", + "sha256:6074904025bc462b0b3f7230b36d8942d9a611b783ce1431ade5ad6a8867b73d", + "sha256:60dcb45a3d762460ac5014755c190db36acf127f68d68643cde7d6d7ce0e5627", + "sha256:61bf6233d04ccba7906f5261ff3628fa97a68fc526cda3d9dd092d2f49926933", + "sha256:63157d66cf7682dec8b3117491cb87a5d8e1cd56df59156d5553ab9721895d19", + "sha256:6378871775e0feb225693cbdad3d997327af0ab4c7e39d93849008c73b867134", + "sha256:6614325ef69d8a53c731ed5e4bd55449ffc5fe86ad652789c0afc853099662ad", + "sha256:66248832212957d8bad28e8d9d307be1d987b94ffaf7e7cca658a349d52d3572", + "sha256:692603a8f82e7baa86bb3921d5002b711788cec547b626030f1f6cf017290ab7", + "sha256:701cd0ee20fe9087c21229db579f2222a75c229b44840a7df7b2d795522068c3", + "sha256:7331a7d2683e644b7830c924ac634fa3ec52257f5098f6415d8ad765d6bc29a8", + "sha256:74b2e94d3e410ed49c7a4cb2c3a5089a6632f7ab68e49bb612b972577e26e771", + "sha256:780313d2a1877adef0e3839ef9596ad53ab640715e7f453e7304c121cd7f262d", + "sha256:7a9d0efd6ff6f4f55ff7a37852e4fcdc24b1feb3b09e204df3dda990171fe725", + "sha256:7b7d46a6ca781a336c7317d9c1d381bebb3b0da5309c2293dd1c4fe3d62942bf", + "sha256:7d271fed8a4b46723db5001619c36192d94a3bd49d76ef186f13abb6897ff8e5", + "sha256:7d5d8eeb1051fac562d80aad7b6b496e2901f41fc2b0988c61016a1426996f66", + "sha256:7d8917677a64304db00ec46629aff335c935c788a10a164b29464b7e2d707463", + "sha256:7da21f0d9bebdc8ac1dde69b3c0951b339984883e2a751790f0f72cbfd1dd007", + "sha256:863d7401d3a109f75c7a5ca0e33e8fb7704a61007f4bda03e08e05f3bf1af40f", + "sha256:866c17223f7d734377a260a2800e14791cb5e55ec252de624e053a0b36b8568a", + "sha256:884d4f3509dfc810299d14faed24c0fbcac82ae2a9737b0cb1d8f7a5e8a291f8", + "sha256:88ff0c0bea02ce78af8a91b173fb43aad5f1945221182f77ba7816fd01bcbc4c", + "sha256:8910f022242c0a15f6d77d781c6ba16bb88d9fed3bff8964de652ee2580029ac", + "sha256:8bfdb95a85404a943197264461b904a2e9e228fd28cb86e4e57321f5b4d5be07", + "sha256:8d39e71705dccdcdf077752d4dc0fcc9554bf797f8af0c1db59f0025a72d4ed2", + "sha256:92a719bb1118f302f6fc3c7638e78e152de8bf279c0200325af831afa1b60f1a", + "sha256:9618070bb76a064c13020323b7fc23c332930604dfbc96b77e7ad7baca960c12", + "sha256:973d630c00bbaf07045870d331c8596bf4fa07aa8eb10d69a02c542af714f128", + "sha256:99febd7a9efab236d798d72ca878ae0d92fffadcc2e472636d6e093ce2677980", + "sha256:9eaf0f28ed19919bdeb02cfa541daaee8a05c070227eaab8d9732f1eebfe2869", + "sha256:9ee84156656d4a09010c280f41011f0a317c62e745f7a2cfafabd8035823fe2d", + "sha256:a999c5c50af0e564cab5bbbbbee97d494eb0e09f99481385108ddfd90049b3fe", + "sha256:ac210d628b9a50699189ec09f0f73630c78e60027d81d75c461deebf7aed752c", + "sha256:ac487adb2e838d03aed0c1a9df4ba348ca2c215bf2afa2f6e1d9449c7029971f", + "sha256:ad6775f8bd57e2c4068246e03c00e212e01b27ea0e96a4b4f17f9d45d80cd5d8", + "sha256:aef7e9b60b371f4d3c3ea80c0ef2d841623dd64aad7718ab815a3205bd4bdf08", + "sha256:b0e38cf49c17e35831ec38029854b772717d6071f0419b74b80be57571a83d0a", + "sha256:b68c0c9deb2fcd183376600df99e88032a9c192d352b0f781e130b09220ef1cf", + "sha256:b846a17f810708f1beff6ad088121fd35334729df3e520412163c74ef49433f7", + "sha256:bc8a06f7bc45219b2c191d68e779e6b3f62e32d09d2f8cf7b381ba1dcb7a68f9", + "sha256:bd6ca6e66b4fee5e879207854f125b94f6ad77e98ddae4d7778d2e96be94ede4", + "sha256:c115756cb1cad49862aa0c2687922ed10da6be7689cf35e3ab602c4a6da2d8fb", + "sha256:c2c4da0802f6897f7fb766c4f0e7f55c96b103981265fcf12b648d088bee3744", + "sha256:c464852c531e44abc5ba05d0c0c97a8fa63719106b3dca46fedae14daedf46ae", + "sha256:c48477c6ff32032624aa122323adc343055bb7e347e01146a86e652b06281731", + "sha256:c6dfa317e4b87052589253f50119211b801146ff7214b8684830e9084fa6eb0a", + "sha256:c763e42a29ac98e7240004e36b7ce231046054393182f1f630c897ce049579ed", + "sha256:c7fab0120e4ea5a2c170382bd27345b2b56e22b6270b40e4231a68f090ce17ed", + "sha256:cb56dcaf10bac9713fff133074d2460b0b217f27760a2b642efb2bc4179bfde6", + "sha256:cfcca979b72f240bac7c73564026eae4c97639151a415e6ced6392d120022d2d", + "sha256:d070756da822a538231d519ce290a1423ab108d6174ad1497cd020bee503d818", + "sha256:d5c35188fac7e448b52eb3916365fe5f59eb27fecec21ba757eea4f650584ca5", + "sha256:d8da09e318a2916da7110d1147355056ee89d61b4ded49ba3ada717517f2fc71", + "sha256:d957259a15e45e5fa5d51ce59ab7519cff8d3de0109d404627276ec68412c718", + "sha256:de1ab4f48fbcb4c2e578951338cc1c8245e510be061d2773a2d47616fb0d6470", + "sha256:de9f7a51f828f73ea0ca2e856a7cac8766752f336241abdb6c5f45f402dd59ea", + "sha256:e00aaf1574075439ccb0b827ca822c5a97c0103351ead292c42a9f17bd2eae0a", + "sha256:e6df05c2234786b15632cd154d60122c302fd860d89c3ee47c166ad92eb6ae55", + "sha256:e7a8f70c7c283d0b4af90314ff8d969c9ab2c7ee522bfb612f42c542935f6e11", + "sha256:ec1f6129c1175d15da7b7c13ae5d4226acf6b5fe362c5b01ac9787fa88c64781", + "sha256:ececd833be7fd8390371c082103916702170e81a1b22beb989452f934def78d6", + "sha256:ee77d3c82576baae66a3281c9a6431fc84281443a7e36a8490a45b3dbbb60446", + "sha256:f0a6cd797394761692cc6f33b10f2ea46789ac0b7fba82b6df737f51e1297122", + "sha256:f1b3930f0934057825227016a141ce16aad4b2a3805fb4e2de71064d042d72e9", + "sha256:f9dae6ef584d3241571674ed7bcd1a28b003a5f0c3a6ca561ab42e5ce0c482e3", + "sha256:fb09731156f54dfd8bb097ce80f9436c2a1a282061ba29e526c375c69086b764" + ], + "markers": "python_version >= '3.9'", + "version": "==1.21.0" + }, + "zstandard": { + "hashes": [ + "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", + "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", + "sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3", + "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", + "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", + "sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936", + "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", + "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", + "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", + "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", + "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", + "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", + "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", + "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", + "sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362", + "sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649", + "sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb", + "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", + "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", + "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", + "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", + "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", + "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", + "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", + "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", + "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", + "sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860", + "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", + "sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53", + "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", + "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", + "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", + "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", + "sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2", + "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", + "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", + "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", + "sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388", + "sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530", + "sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577", + "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", + "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", + "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", + "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", + "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", + "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", + "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", + "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", + "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", + "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", + "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", + "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", + "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", + "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", + "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", + "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", + "sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27", + "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", + "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", + "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", + "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", + "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", + "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", + "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", + "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", + "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", + "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", + "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", + "sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df", + "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", + "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", + "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", + "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", + "sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0", + "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", + "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", + "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", + "sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7", + "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", + "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", + "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", + "sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751", + "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", + "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", + "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", + "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", + "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", + "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", + "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", + "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", + "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", + "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", + "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", + "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", + "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", + "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", + "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", + "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", + "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01" + ], + "markers": "python_version >= '3.9'", + "version": "==0.25.0" } }, "develop": { "astroid": { "hashes": [ - "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca", - "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e" + "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93", + "sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17" ], "markers": "python_full_version >= '3.8.0'", - "version": "==3.0.1" + "version": "==3.0.3" }, "bandit": { "hashes": [ - "sha256:75665181dc1e0096369112541a056c59d1c5f66f9bb74a8d686c3c362b83f549", - "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e" + "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0", + "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.7.5" + "markers": "python_version >= '3.9'", + "version": "==1.8.6" }, "black": { "hashes": [ @@ -1159,51 +1764,35 @@ }, "click": { "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", + "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.7" + "markers": "python_version >= '3.10'", + "version": "==8.3.0" }, "dill": { "hashes": [ - "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", - "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", + "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049" ], - "markers": "python_version < '3.11'", - "version": "==0.3.7" - }, - "gitdb": { - "hashes": [ - "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", - "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b" - ], - "markers": "python_version >= '3.7'", - "version": "==4.0.11" - }, - "gitpython": { - "hashes": [ - "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4", - "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.40" + "markers": "python_version >= '3.8'", + "version": "==0.4.0" }, "isort": { "hashes": [ - "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", - "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" ], "markers": "python_full_version >= '3.8.0'", - "version": "==5.12.0" + "version": "==5.13.2" }, "markdown-it-py": { "hashes": [ - "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", - "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" + "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", + "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" ], - "markers": "python_version >= '3.8'", - "version": "==3.0.0" + "markers": "python_version >= '3.10'", + "version": "==4.0.0" }, "mccabe": { "hashes": [ @@ -1223,11 +1812,11 @@ }, "mypy-extensions": { "hashes": [ - "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", - "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", + "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" ], - "markers": "python_version >= '3.5'", - "version": "==1.0.0" + "markers": "python_version >= '3.8'", + "version": "==1.1.0" }, "packaging": { "hashes": [ @@ -1240,35 +1829,27 @@ }, "pathspec": { "hashes": [ - "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", - "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" ], - "markers": "python_version >= '3.7'", - "version": "==0.11.2" - }, - "pbr": { - "hashes": [ - "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda", - "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9" - ], - "markers": "python_version >= '2.6'", - "version": "==6.0.0" + "markers": "python_version >= '3.8'", + "version": "==0.12.1" }, "platformdirs": { "hashes": [ - "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", - "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" + "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", + "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf" ], - "markers": "python_version >= '3.7'", - "version": "==4.0.0" + "markers": "python_version >= '3.9'", + "version": "==4.4.0" }, "pygments": { "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" + "markers": "python_version >= '3.8'", + "version": "==2.19.2" }, "pylint": { "hashes": [ @@ -1281,99 +1862,145 @@ }, "pyyaml": { "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", + "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", + "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", + "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", + "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", + "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", + "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", + "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", + "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", + "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", + "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", + "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", + "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", + "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", + "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", + "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", + "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", + "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", + "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", + "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", + "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", + "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", + "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", + "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", + "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", + "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", + "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", + "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", + "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", + "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", + "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", + "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", + "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", + "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", + "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", + "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", + "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", + "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", + "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", + "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", + "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", + "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", + "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", + "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", + "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", + "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", + "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", + "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", + "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", + "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", + "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", + "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", + "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", + "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", + "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", + "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", + "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", + "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", + "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", + "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", + "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", + "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", + "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", + "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", + "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", + "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", + "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", + "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", + "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", + "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", + "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", + "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", + "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" + "markers": "python_version >= '3.8'", + "version": "==6.0.3" }, "rich": { "hashes": [ - "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", - "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" + "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", + "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8" ], - "markers": "python_full_version >= '3.7.0'", - "version": "==13.7.0" - }, - "smmap": { - "hashes": [ - "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", - "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0.1" + "markers": "python_full_version >= '3.8.0'", + "version": "==14.1.0" }, "stevedore": { "hashes": [ - "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d", - "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c" + "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf", + "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73" ], - "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "markers": "python_version >= '3.9'", + "version": "==5.5.0" }, "tomli": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", + "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", + "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", + "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", + "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", + "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", + "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", + "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", + "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", + "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", + "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", + "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", + "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", + "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", + "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", + "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", + "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", + "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", + "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", + "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", + "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", + "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", + "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", + "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", + "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", + "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", + "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", + "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", + "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", + "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", + "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", + "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7" ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.2.1" }, "tomlkit": { "hashes": [ - "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", - "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" + "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", + "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0" ], - "markers": "python_version >= '3.7'", - "version": "==0.12.3" + "markers": "python_version >= '3.8'", + "version": "==0.13.3" }, "typing-extensions": { "hashes": [ diff --git a/README.md b/README.md index 2e3d32e344..50f71890ca 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ <img src="https://img.shields.io/discord/1079074933008781362.svg?label=Discord&logo=Discord&colorB=7289da&style=for-the-badge" alt="Support"> </a> - <a href="https://patreon.com/kyber"> - <img src="https://img.shields.io/badge/patreon-donate-orange.svg?style=for-the-badge&logo=Patreon" alt="Patreon"> + <a href="https://buymeacoffee.com/modmaildev"> + <img src="https://img.shields.io/badge/buymeacoffee-donate-ff813f.svg?style=for-the-badge&logo=buy-me-a-coffee" alt="Buy Me A Coffee"> </a> <a href="https://www.python.org/downloads/"> @@ -48,7 +48,7 @@ Modmail is similar to Reddit's Modmail, both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way. -This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Patreon](https://www.patreon.com/kyber)**, cool benefits included! +This bot is free for everyone and always will be. If you like this project and would like to show your appreciation, you can support us on **[Buy Me A Coffee](https://buymeacoffee.com/modmaildev)**, cool benefits included! For up-to-date setup instructions, please visit our [**documentation**](https://docs.modmail.dev/installation) page. @@ -72,7 +72,7 @@ Our Logviewer will save the threads so you can view previous threads through the * When you close a thread, Modmail will generate a log link and post it to your log channel. * Native Discord dark-mode feel. * Markdown/formatting support. - * Login via Discord to protect your logs ([premium Patreon feature](https://patreon.com/kyber)). + * Login via Discord to protect your logs ([premium feature](https://buymeacoffee.com/modmaildev/membership)). * See past logs of a user with `?logs`. * Searchable by text queries using `?logs search`. @@ -90,15 +90,15 @@ There are a number of options for hosting your very own dedicated Modmail bot. Visit our [**documentation**](https://docs.modmail.dev/installation) page for detailed guidance on how to deploy your Modmail bot. -### Patreon Hosting +### Paid Hosting -If you don't want the trouble of renting and configuring your server to host Modmail, we got a solution for you! We offer hosting and maintenance of your own, private Modmail bot (including a Logviewer) through [**Patreon**](https://patreon.com/kyber). +If you don't want the trouble of renting and configuring your server to host Modmail, we got a solution for you! We offer hosting and maintenance of your own, private Modmail bot (including a Logviewer) through [**Buy Me A Coffee**](https://buymeacoffee.com/modmaildev/membership). ## FAQ **Q: Where can I find the Modmail bot invite link?** -**A:** Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following our [**documentation**](https://docs.modmail.dev/installation) steps or subscribe to [**Patreon**](https://patreon.com/kyber). +**A:** Unfortunately, due to how this bot functions, it cannot be invited. The lack of an invite link is to ensure an individuality to your server and grant you full control over your bot and data. Nonetheless, you can quickly obtain a free copy of Modmail for your server by following our [**documentation**](https://docs.modmail.dev/installation) steps or subscribe to [**Buy Me A Coffee**](https://buymeacoffee.com/modmaildev/membership). **Q: Where can I find out more info about Modmail?** @@ -147,13 +147,6 @@ Advertise Your Server: </a> <br> <br> -Help Us • Help Other's: -<br> -<a href='https://discord.gg/5yQCFzY6HU'> - <img height=100 src='https://i.imgur.com/Gi3jxeH.gif' style='margin:5px'> -</a> -<br> -<br> Discord Advice Center: <br> <a href='https://discord.gg/zmwZy5fd9v'> @@ -161,20 +154,19 @@ Discord Advice Center: </a> <br> <br> -Blacklight Promotions: +Kistó Bakery: <br> -<a href='https://blacklightpromotions.online'> - <img height=100 src='https://i.imgur.com/yLgE6h6.png' style='margin:5px'> +<a href='https://www.roblox.com/communities/9318596/Kist#!/about'> + <img height=100 src='https://i.imgur.com/ck81JiJ.jpeg' style='margin:5px'> </a> - -Become a sponsor on [Patreon](https://patreon.com/kyber). +Become a sponsor on [Buy Me A Coffee](https://buymeacoffee.com/modmaildev/membership). ## Contributing Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/modmail-dev/modmail/blob/master/.github/CONTRIBUTING.md) before you get started. -If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! +If you like this project and would like to show your appreciation, support us on **[Buy Me A Coffee](https://buymeacoffee.com/modmaildev)**! ## Beta Testing diff --git a/SPONSORS.json b/SPONSORS.json index b6212b8ed7..cdaf3621ac 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -112,33 +112,6 @@ "title": "uncommon community" } }, - { - "embed": { - "author": { - "name": "Help us • Help Others" - }, - "title": "Join Today", - "url": "https://discord.gg/5yQCFzY6HU", - "description": "At Help Us • Help Others, we accept as true with inside the transformative electricity of cooperation and kindness. Each one people has the capability to make a meaningful impact by means of helping and caring for others. Whether you want assistance or want to offer it, this is the right region for you!", - "fields": [ - { - "name": "What we offer:", - "value": "`🎬` - Active community\n`👮` - Active staff around the globe! \n`🛜` - 40+ Advertising channels to grow your socials!\n`💎` - Boosting Perks\n`🎉` - Event's monthly especially bank holiday roles!!\n`🔢` - Unique levelling systems\n`📞` - Multiple voice channels including gaming!\n`🎁` - Exclusive giveaways!" - }, - { - "name": "We Are Hiring", - "value": "`🔵` - Moderators\n`🔵` - Human Resources\n`🔵` - Community Team\n`🔵` - Partnership Manager\n`🔵` - Growth Manager\n`🚀` Much more to come!\n\n\nJoin Today!" - } - ], - "image": { - "url": "https://cdn.discordapp.com/attachments/1218338794416246874/1243635366326567002/AD_animated.gif" - }, - "color": 45300, - "footer": { - "text": "Help Us • Help Others" - } - } - }, { "embed": { "description": "> Be apart of our community as we start to grow! and embark on a long journey.\n——————————————————-\n**What we offer?**\n\n➺〚🖌️〛Custom Liveries \n➺〚❤️〛Friendly and Growing community.\n➺〚🤝〛Partnerships.\n➺〚🎮〛Daily SSUs. \n➺〚🚨〛Great roleplays.\n➺〚💬〛Kind and Professional staff\n➺〚🎉〛Giveaways!!! \n——————————————————-\n**Emergency Services**\n\n➺〚🚔〛NY Police Force\n➺〚🚒〛Fire & Emergency NY\n➺〚🚧〛NY department of transportation \n\n——————————————————-\n**Whitelisted**\nComing soon!\n——————————————————-\n**What are we looking for!**\n\n➺〚💬〛More members\n➺〚⭐〛Staff Members - **WE'RE HIRING!**\n➺〚🤝〛Partnerships\n➺〚💎〛Boosters\n——————————————————\n\n**[Join now](https://discord.com/invite/qt62qSnKVa)**", diff --git a/bot.py b/bot.py index 3f13ef7ced..671d9ab9c4 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "4.1.2" +__version__ = "4.2.0" import asyncio @@ -48,7 +48,15 @@ ) from core.thread import ThreadManager from core.time import human_timedelta -from core.utils import extract_block_timestamp, normalize_alias, parse_alias, truncate, tryint, human_join +from core.utils import ( + extract_block_timestamp, + normalize_alias, + parse_alias, + truncate, + tryint, + human_join, + extract_forwarded_content, +) logger = getLogger(__name__) @@ -879,10 +887,182 @@ async def process_dm_modmail(self, message: discord.Message) -> None: return sent_emoji, blocked_emoji = await self.retrieve_emoji() + # Handle forwarded messages (Discord forwards) + # See: https://discord.com/developers/docs/resources/message#message-reference-content-attribution-forwards + # 1. Multi-forward (message_snapshots) + if hasattr(message, "flags") and getattr(message.flags, "has_snapshot", False): + if hasattr(message, "message_snapshots") and message.message_snapshots: + 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=self.config["cooldown_thread_title"], + description=self.config["cooldown_thread_response"].format(delta=delta), + color=self.error_color, + ) + ) + return + if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + embed = discord.Embed( + title=self.config["disabled_new_thread_title"], + color=self.error_color, + description=self.config["disabled_new_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_new_thread_footer"], + icon_url=self.get_guild_icon(guild=message.guild, size=128), + ) + logger.info( + "A new thread was blocked from %s due to disabled Modmail.", message.author + ) + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + thread = await self.threads.create(message.author, message=message) + else: + if self.config["dm_disabled"] == DMDisabled.ALL_THREADS: + embed = discord.Embed( + title=self.config["disabled_current_thread_title"], + color=self.error_color, + description=self.config["disabled_current_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_current_thread_footer"], + icon_url=self.get_guild_icon(guild=message.guild, size=128), + ) + logger.info("A message was blocked from %s due to disabled Modmail.", message.author) + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + # Extract forwarded content using utility function + combined_content = extract_forwarded_content(message) or "[Forwarded message with no content]" + + class ForwardedMessage: + def __init__(self, original_message, forwarded_content): + self.author = original_message.author + self.content = forwarded_content + self.attachments = [] + self.stickers = [] + self.created_at = original_message.created_at + self.embeds = [] + self.id = original_message.id + self.flags = original_message.flags + self.message_snapshots = original_message.message_snapshots + self.type = getattr(original_message, "type", None) + + forwarded_msg = ForwardedMessage(message, combined_content) + await thread.send(forwarded_msg) + await self.add_reaction(message, sent_emoji) + self.dispatch("thread_reply", thread, False, message, False, False) + return + else: + message.content = "[Forwarded message with no content]" + # 2. Single-message forward (MessageType.forward) + elif getattr(message, "type", None) == getattr(discord.MessageType, "forward", None): + # Check for message.reference and its type + ref = getattr(message, "reference", None) + if ref and getattr(ref, "type", None) == getattr(discord, "MessageReferenceType", None).forward: + # Try to fetch the referenced message + ref_msg = None + try: + if ref.resolved: + ref_msg = ref.resolved + elif ref.message_id and ref.channel_id: + channel = self.get_channel(ref.channel_id) or ( + await self.fetch_channel(ref.channel_id) + ) + ref_msg = await channel.fetch_message(ref.message_id) + except Exception: + ref_msg = None + if ref_msg: + # Forward the referenced message as if it was sent + 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=self.config["cooldown_thread_title"], + description=self.config["cooldown_thread_response"].format(delta=delta), + color=self.error_color, + ) + ) + return + if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + embed = discord.Embed( + title=self.config["disabled_new_thread_title"], + color=self.error_color, + description=self.config["disabled_new_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_new_thread_footer"], + icon_url=self.get_guild_icon(guild=message.guild, size=128), + ) + logger.info( + "A new thread was blocked from %s due to disabled Modmail.", message.author + ) + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + thread = await self.threads.create(message.author, message=message) + else: + if self.config["dm_disabled"] == DMDisabled.ALL_THREADS: + embed = discord.Embed( + title=self.config["disabled_current_thread_title"], + color=self.error_color, + description=self.config["disabled_current_thread_response"], + ) + embed.set_footer( + text=self.config["disabled_current_thread_footer"], + icon_url=self.get_guild_icon(guild=message.guild, size=128), + ) + logger.info( + "A message was blocked from %s due to disabled Modmail.", message.author + ) + await self.add_reaction(message, blocked_emoji) + return await message.channel.send(embed=embed) + + # Create a forwarded message wrapper to preserve forward info + class ForwardedMessage: + def __init__(self, original_message, ref_message): + self.author = original_message.author + # Use the utility function to extract content or fallback to ref message content + extracted_content = extract_forwarded_content(original_message) + self.content = ( + extracted_content + or ref_message.content + or "[Forwarded message with no text content]" + ) + self.attachments = getattr(ref_message, "attachments", []) + self.stickers = getattr(ref_message, "stickers", []) + self.created_at = original_message.created_at + self.embeds = getattr(ref_message, "embeds", []) + self.id = original_message.id + self.type = getattr(original_message, "type", None) + self.reference = original_message.reference + + forwarded_msg = ForwardedMessage(message, ref_msg) + await thread.send(forwarded_msg) + await self.add_reaction(message, sent_emoji) + self.dispatch("thread_reply", thread, False, message, False, False) + return + else: + message.content = "[Forwarded message with no content]" + if message.type not in [discord.MessageType.default, discord.MessageType.reply]: return thread = await self.threads.find(recipient=message.author) + if thread and thread.snoozed: + await thread.restore_from_snooze() + self.threads.cache[thread.id] = thread + # Update the DB with the new channel_id after restoration + if thread.channel: + await self.api.logs.update_one( + {"recipient.id": str(thread.id)}, {"$set": {"channel_id": str(thread.channel.id)}} + ) + # Re-fetch the thread object to ensure channel is valid + thread = await self.threads.find(recipient=message.author) + if thread is None: delta = await self.get_thread_cooldown(message.author) if delta: @@ -1156,6 +1336,19 @@ async def on_message(self, message): content = "" await self.mention_channel.send(content=content, embed=em) + # --- MODERATOR-ONLY MESSAGE LOGGING --- + # If a moderator sends a message directly in a thread channel (not via modmail command), log it + if not message.author.bot and not isinstance(message.channel, discord.DMChannel): + thread = await self.threads.find(channel=message.channel) + if thread is not None: + ctxs = await self.get_contexts(message) + is_command = any(ctx.command for ctx in ctxs) + if not is_command: + # Only log if not a command + perms = message.channel.permissions_for(message.author) + if perms.manage_messages or perms.administrator: + await self.api.append_log(message, type_="internal") + await self.process_commands(message) async def process_commands(self, message): @@ -1193,8 +1386,6 @@ async def process_commands(self, message): or self.config.get("plain_reply_without_command") ): await thread.reply(message, anonymous=anonymous, plain=plain) - else: - await self.api.append_log(message, type_="internal") elif ctx.invoked_with: exc = commands.CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with)) self.dispatch("command_error", ctx, exc) @@ -1212,7 +1403,10 @@ async def on_typing(self, channel, user, _): thread = await self.threads.find(recipient=user) if thread: - await thread.channel.typing() + try: + await thread.channel.typing() + except Exception: + pass else: if not self.config.get("mod_typing"): return @@ -1222,7 +1416,10 @@ async def on_typing(self, channel, user, _): for user in thread.recipients: if await self.is_blocked(user): continue - await user.typing() + try: + await user.typing() + except Exception: + pass async def handle_reaction_events(self, payload): user = self.get_user(payload.user_id) @@ -1529,7 +1726,10 @@ async def on_command_error( return if isinstance(exception, (commands.BadArgument, commands.BadUnionArgument)): - await context.typing() + try: + await context.typing() + except Exception: + pass await context.send(embed=discord.Embed(color=self.error_color, description=str(exception))) elif isinstance(exception, commands.CommandNotFound): logger.warning("CommandNotFound: %s", exception) @@ -1820,7 +2020,7 @@ def main(): sys.exit(0) # check discord version - discord_version = "2.3.2" + discord_version = "2.6.3" if discord.__version__ != discord_version: logger.error( "Dependencies are not updated, run pipenv install. discord.py version expected %s, received %s", diff --git a/cogs/modmail.py b/cogs/modmail.py index e2a0039384..d7d9d7b010 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,8 +1,9 @@ import asyncio import re -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta from itertools import zip_longest from typing import Optional, Union, List, Tuple, Literal +import logging import discord from discord.ext import commands @@ -29,6 +30,19 @@ class Modmail(commands.Cog): def __init__(self, bot): self.bot = bot + def _resolve_user(self, user_str): + """Helper to resolve a user from mention, ID, or username.""" + import re + + if not user_str: + return None + if user_str.isdigit(): + return int(user_str) + match = re.match(r"<@!?(\d+)>", user_str) + if match: + return int(match.group(1)) + return None + @commands.command() @trigger_typing @checks.has_permissions(PermissionLevel.OWNER) @@ -94,7 +108,7 @@ async def setup(self, ctx): name="Thanks for using our bot!", value="If you like what you see, consider giving the " "[repo a star](https://github.com/modmail-dev/modmail) :star: and if you are " - "feeling extra generous, buy us coffee on [Patreon](https://patreon.com/kyber) :heart:!", + "feeling extra generous, buy us coffee on [Buy Me A Coffee](https://buymeacoffee.com/modmaildev) :heart:!", ) embed.set_footer(text=f'Type "{self.bot.prefix}help" for a complete list of commands.') @@ -143,6 +157,21 @@ async def snippet(self, ctx, *, name: str.lower = None): """ if name is not None: + if name == "compact": + embeds = [] + + 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=self.bot.get_guild_icon(guild=ctx.guild, size=128) + ) + embeds.append(embed) + + session = EmbedPaginatorSession(ctx, *embeds) + await session.run() + return + snippet_name = self.bot._resolve_snippet(name) if snippet_name is None: @@ -162,13 +191,14 @@ async def snippet(self, ctx, *, name: str.lower = None): embed.set_author(name="Snippets", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128)) return await ctx.send(embed=embed) - embeds = [] - - 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) + embeds = [discord.Embed(color=self.bot.main_color) for _ in range((len(self.bot.snippets) // 10) + 1)] + for embed in embeds: embed.set_author(name="Snippets", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128)) - embeds.append(embed) + + for i, snippet in enumerate(sorted(self.bot.snippets.items())): + embeds[i // 10].add_field( + name=snippet[0], value=return_or_truncate(snippet[1], 350), inline=False + ) session = EmbedPaginatorSession(ctx, *embeds) await session.run() @@ -849,7 +879,10 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str ) if self.bot.config["show_timestamp"]: em.timestamp = discord.utils.utcnow() - em.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar.url) + em.set_footer( + text=str(ctx.author), + icon_url=ctx.author.display_avatar.url if ctx.author.display_avatar else None, + ) for u in users: to_exec.append(u.send(embed=em)) @@ -865,7 +898,9 @@ async def adduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, str ) if self.bot.config["show_timestamp"]: em.timestamp = discord.utils.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].display_avatar.url) + em.set_footer( + text=f"{users[0]}", icon_url=users[0].display_avatar.url if users[0].display_avatar else None + ) for i in ctx.thread.recipients: if i not in users: @@ -942,7 +977,10 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, ) if self.bot.config["show_timestamp"]: em.timestamp = discord.utils.utcnow() - em.set_footer(text=str(ctx.author), icon_url=ctx.author.display_avatar.url) + em.set_footer( + text=str(ctx.author), + icon_url=ctx.author.display_avatar.url if ctx.author.display_avatar else None, + ) for u in users: to_exec.append(u.send(embed=em)) @@ -958,7 +996,9 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, ) if self.bot.config["show_timestamp"]: em.timestamp = discord.utils.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].display_avatar.url) + em.set_footer( + text=f"{users[0]}", icon_url=users[0].display_avatar.url if users[0].display_avatar else None + ) for i in ctx.thread.recipients: if i not in users: @@ -1036,7 +1076,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: avatar_url = self.bot.get_guild_icon(guild=ctx.guild, size=128) - em.set_footer(text=name, icon_url=avatar_url) + em.set_footer(text=name, icon_url=avatar_url if avatar_url else None) for u in users: to_exec.append(u.send(embed=em)) @@ -1052,7 +1092,9 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, ) if self.bot.config["show_timestamp"]: em.timestamp = discord.utils.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].display_avatar.url) + em.set_footer( + text=f"{users[0]}", icon_url=users[0].display_avatar.url if users[0].display_avatar else None + ) for i in ctx.thread.recipients: if i not in users: @@ -1125,7 +1167,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: avatar_url = self.bot.get_guild_icon(guild=ctx.guild, size=128) - em.set_footer(text=name, icon_url=avatar_url) + em.set_footer(text=name, icon_url=avatar_url if avatar_url else None) for u in users: to_exec.append(u.send(embed=em)) @@ -1141,7 +1183,9 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro ) if self.bot.config["show_timestamp"]: em.timestamp = discord.utils.utcnow() - em.set_footer(text=f"{users[0]}", icon_url=users[0].display_avatar.url) + em.set_footer( + text=f"{users[0]}", icon_url=users[0].display_avatar.url if users[0].display_avatar else None + ) for i in ctx.thread.recipients: if i not in users: @@ -1156,6 +1200,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() async def logs(self, ctx, *, user: User = None): """ Get previous Modmail thread logs of a member. @@ -1165,7 +1210,8 @@ async def logs(self, ctx, *, user: User = None): `user` may be a user ID, mention, or name. """ - await ctx.typing() + async with safe_typing(ctx): + pass if not user: thread = ctx.thread @@ -1297,7 +1343,8 @@ async def logs_search(self, ctx, limit: Optional[int] = None, *, query): Provide a `limit` to specify the maximum number of logs the bot should find. """ - await ctx.typing() + async with safe_typing(ctx): + pass entries = await self.bot.api.search_by_text(query, limit) @@ -1326,7 +1373,7 @@ async def reply(self, ctx, *, msg: str = ""): ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): await ctx.thread.reply(ctx.message) @commands.command(aliases=["formatreply"]) @@ -1348,7 +1395,7 @@ async def freply(self, ctx, *, msg: str = ""): msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author ) ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): await ctx.thread.reply(ctx.message) @commands.command(aliases=["formatanonreply"]) @@ -1370,7 +1417,7 @@ async def fareply(self, ctx, *, msg: str = ""): msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author ) ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): await ctx.thread.reply(ctx.message, anonymous=True) @commands.command(aliases=["formatplainreply"]) @@ -1392,7 +1439,7 @@ async def fpreply(self, ctx, *, msg: str = ""): msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author ) ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): await ctx.thread.reply(ctx.message, plain=True) @commands.command(aliases=["formatplainanonreply"]) @@ -1414,7 +1461,7 @@ async def fpareply(self, ctx, *, msg: str = ""): msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author ) ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): await ctx.thread.reply(ctx.message, anonymous=True, plain=True) @commands.command(aliases=["anonreply", "anonymousreply"]) @@ -1431,7 +1478,7 @@ async def areply(self, ctx, *, msg: str = ""): and `anon_tag` config variables to do so. """ ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): await ctx.thread.reply(ctx.message, anonymous=True) @commands.command(aliases=["plainreply"]) @@ -1445,7 +1492,7 @@ async def preply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): await ctx.thread.reply(ctx.message, plain=True) @commands.command(aliases=["plainanonreply", "plainanonymousreply"]) @@ -1459,7 +1506,7 @@ async def pareply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): await ctx.thread.reply(ctx.message, anonymous=True, plain=True) @commands.group(invoke_without_command=True) @@ -1472,7 +1519,7 @@ async def note(self, ctx, *, msg: str = ""): Useful for noting context. """ ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): msg = await ctx.thread.note(ctx.message) await msg.pin() @@ -1484,7 +1531,7 @@ async def note_persistent(self, ctx, *, msg: str = ""): Take a persistent note about the current user. """ ctx.message.content = msg - async with ctx.typing(): + async with safe_typing(ctx): msg = await ctx.thread.note(ctx.message, persistent=True) await msg.pin() await self.bot.api.create_note(recipient=ctx.thread.recipient, message=ctx.message, message_id=msg.id) @@ -1621,6 +1668,7 @@ async def contact( creator=creator, category=category, manual_trigger=manual_trigger, + # The minimum character check is enforced in ThreadManager.create ) if thread.cancelled: @@ -1644,7 +1692,9 @@ async def contact( ) if self.bot.config["show_timestamp"]: em.timestamp = discord.utils.utcnow() - em.set_footer(text=f"{creator}", icon_url=creator.display_avatar.url) + em.set_footer( + text=f"{creator}", icon_url=creator.display_avatar.url if creator.display_avatar else None + ) for u in users: await u.send(embed=em) @@ -2213,6 +2263,243 @@ async def isenable(self, ctx): return await ctx.send(embed=embed) + @commands.command(usage="[duration]") + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def snooze(self, ctx, *, duration: UserFriendlyTime = None): + """ + Snooze this thread: deletes the channel, keeps the ticket open in DM, and restores it when the user replies or a moderator unsnoozes it. + Optionally specify a duration, e.g. 'snooze 2d' for 2 days. + Uses config: max_snooze_time, snooze_title, snooze_text + """ + thread = ctx.thread + if thread.snoozed: + await ctx.send("This thread is already snoozed.") + logging.info(f"[SNOOZE] Thread for {getattr(thread.recipient, 'id', None)} already snoozed.") + return + max_snooze = self.bot.config.get("max_snooze_time") + if max_snooze is None: + max_snooze = 604800 + max_snooze = int(max_snooze) + if duration: + snooze_for = int((duration.dt - duration.now).total_seconds()) + if snooze_for > max_snooze: + snooze_for = max_snooze + else: + snooze_for = max_snooze + + # Storing snooze_start and snooze_for in the log entry + now = datetime.now(timezone.utc) + await self.bot.api.logs.update_one( + {"recipient.id": str(thread.id)}, + {"$set": {"snooze_start": now.isoformat(), "snooze_for": snooze_for}}, + ) + embed = discord.Embed( + title=self.bot.config.get("snooze_title") or "Thread Snoozed", + description=self.bot.config.get("snooze_text") or "This thread has been snoozed.", + color=self.bot.error_color, + ) + await ctx.send(embed=embed) + ok = await thread.snooze(moderator=ctx.author, snooze_for=snooze_for) + if ok: + logging.info( + f"[SNOOZE] Thread for {getattr(thread.recipient, 'id', None)} snoozed for {snooze_for}s." + ) + self.bot.threads.cache[thread.id] = thread + else: + await ctx.send("Failed to snooze this thread.") + logging.error(f"[SNOOZE] Failed to snooze thread for {getattr(thread.recipient, 'id', None)}.") + + @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def unsnooze(self, ctx, *, user: str = None): + """ + Unsnooze a thread: restores the channel and replays messages. + You can specify a user by mention or ID, or run in a thread channel to unsnooze that thread. + Uses config: unsnooze_text + """ + import discord + + thread = None + user_obj = None + if user is not None: + user_id = self._resolve_user(user) + if user_id: + try: + user_obj = await self.bot.get_or_fetch_user(user_id) + except Exception: + user_obj = discord.Object(user_id) + if user_obj: + thread = await self.bot.threads.find(recipient=user_obj) + if not thread: + await ctx.send(f"[DEBUG] No thread found for user {user} (obj: {user_obj}).") + logging.warning(f"[UNSNOOZE] No thread found for user {user} (obj: {user_obj})") + return + elif hasattr(ctx, "thread"): + thread = ctx.thread + else: + await ctx.send("This is not a Modmail thread.") + logging.warning("[UNSNOOZE] Not a Modmail thread context.") + return + if not thread.snoozed: + await ctx.send("This thread is not snoozed.") + logging.info(f"[UNSNOOZE] Thread for {getattr(thread.recipient, 'id', None)} is not snoozed.") + return + + # Manually fetch snooze_data if the thread object doesn't have it + if not thread.snooze_data: + log_entry = await self.bot.api.logs.find_one({"recipient.id": str(thread.id), "snoozed": True}) + if log_entry: + thread.snooze_data = log_entry.get("snooze_data") + + ok = await thread.restore_from_snooze() + if ok: + self.bot.threads.cache[thread.id] = thread + await ctx.send( + self.bot.config.get("unsnooze_text") or "This thread has been unsnoozed and restored." + ) + logging.info(f"[UNSNOOZE] Thread for {getattr(thread.recipient, 'id', None)} unsnoozed.") + else: + await ctx.send("Failed to unsnooze this thread.") + logging.error( + f"[UNSNOOZE] Failed to unsnooze thread for {getattr(thread.recipient, 'id', None)}." + ) + + @commands.command() + @checks.has_permissions(PermissionLevel.SUPPORTER) + async def snoozed(self, ctx): + """ + List all currently snoozed threads/users. + """ + snoozed_threads = [thread for thread in self.bot.threads.cache.values() if thread.snoozed] + if not snoozed_threads: + await ctx.send("No threads are currently snoozed.") + return + + lines = [] + now = datetime.now(timezone.utc) + for thread in snoozed_threads: + user = thread.recipient.name if thread.recipient else "Unknown" + user_id = thread.id + + since_str = "?" + until_str = "?" + + if thread.snooze_data: + since = thread.snooze_data.get("snooze_start") + duration = thread.snooze_data.get("snooze_for") + + if since: + try: + since_dt = datetime.fromisoformat(since) + since_str = f"<t:{int(since_dt.timestamp())}:R>" # Discord relative timestamp + except (ValueError, TypeError) as e: + logging.warning(f"[SNOOZED] Invalid snooze_start for {user_id}: {since} ({e})") + else: + logging.warning(f"[SNOOZED] Missing snooze_start for {user_id}") + + if duration and since_str != "?": + try: + until_dt = datetime.fromisoformat(since) + timedelta(seconds=int(duration)) + until_str = f"<t:{int(until_dt.timestamp())}:R>" + except (ValueError, TypeError) as e: + logging.warning( + f"[SNOOZED] Invalid until time for {user_id}: {since} + {duration} ({e})" + ) + + lines.append(f"- {user} (`{user_id}`) since {since_str}, until {until_str}") + + await ctx.send("Snoozed threads:\n" + "\n".join(lines)) + + async def cog_load(self): + self.bot.loop.create_task(self.snooze_auto_unsnooze_task()) + + async def snooze_auto_unsnooze_task(self): + await self.bot.wait_until_ready() + while True: + now = datetime.now(timezone.utc) + snoozed = await self.bot.api.logs.find({"snoozed": True}).to_list(None) + for entry in snoozed: + start = entry.get("snooze_start") + snooze_for = entry.get("snooze_for") + if not start: + continue + start_dt = datetime.fromisoformat(start) + if snooze_for is not None: + duration = int(snooze_for) + else: + max_snooze = self.bot.config.get("max_snooze_time") + if max_snooze is None: + max_snooze = 604800 + duration = int(max_snooze) + if (now - start_dt).total_seconds() > duration: + # Auto-unsnooze + thread = await self.bot.threads.find(recipient_id=int(entry["recipient"]["id"])) + if thread and thread.snoozed: + await thread.restore_from_snooze() + await asyncio.sleep(60) + + async def process_dm_modmail(self, message: discord.Message) -> None: + # ... existing code ... + # Before processing, check if thread is snoozed and auto-unsnooze + thread = await self.threads.find(recipient=message.author) + if thread and thread.snoozed: + await thread.restore_from_snooze() + # Ensure the thread object in the cache is updated with the new channel + self.threads.cache[thread.id] = thread + # ... rest of the method unchanged ... + + @commands.command() + @checks.has_permissions(PermissionLevel.OWNER) + async def clearsnoozed(self, ctx): + """ + List all snoozed threads and ask for confirmation before clearing (unsnoozing) all of them. + Only proceed if the user confirms. + """ + snoozed = await self.bot.api.logs.find({"snoozed": True}).to_list(None) + if not snoozed: + await ctx.send("No threads are currently snoozed.") + return + lines = [] + for entry in snoozed: + user = entry.get("recipient", {}).get("name", "Unknown") + user_id = entry.get("recipient", {}).get("id", "?") + lines.append(f"- {user} (`{user_id}`)") + msg = await ctx.send( + "The following threads are currently snoozed and will be unsnoozed if you confirm:\n" + + "\n".join(lines) + + "\n\nType `yes` to confirm, or anything else to cancel." + ) + + def check(m): + return m.author == ctx.author and m.channel == ctx.channel + + try: + reply = await self.bot.wait_for("message", check=check, timeout=30) + except asyncio.TimeoutError: + await ctx.send("Timed out. No threads were unsnoozed.") + return + if reply.content.strip().lower() != "yes": + await ctx.send("Cancelled. No threads were unsnoozed.") + return + count = 0 + for entry in snoozed: + user_id = entry.get("recipient", {}).get("id") + if not user_id: + continue + user_obj = None + try: + user_obj = await self.bot.get_or_fetch_user(int(user_id)) + except Exception: + user_obj = discord.Object(int(user_id)) + thread = await self.bot.threads.find(recipient=user_obj) + if thread and thread.snoozed: + ok = await thread.restore_from_snooze() + if ok: + self.bot.threads.cache[thread.id] = thread + count += 1 + await ctx.send(f"Unsnoozed {count} threads.") + async def setup(bot): await bot.add_cog(Modmail(bot)) diff --git a/cogs/plugins.py b/cogs/plugins.py index 78bc0aa544..c7dceb7283 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -20,7 +20,7 @@ from core import checks from core.models import PermissionLevel, getLogger from core.paginator import EmbedPaginatorSession -from core.utils import trigger_typing, truncate +from core.utils import trigger_typing, truncate, safe_typing logger = getLogger(__name__) @@ -484,7 +484,7 @@ async def update_plugin(self, ctx, plugin_name): embed = discord.Embed(description="Plugin is not installed.", color=self.bot.error_color) return await ctx.send(embed=embed) - async with ctx.typing(): + async with safe_typing(ctx): embed = discord.Embed( description=f"Successfully updated {plugin.name}.", color=self.bot.main_color ) @@ -759,7 +759,10 @@ async def plugins_registry_compact(self, ctx): for page in pages: embed = discord.Embed(color=self.bot.main_color, description=page) - embed.set_author(name="Plugin Registry", icon_url=self.bot.user.display_avatar.url) + embed.set_author( + name="Plugin Registry", + icon_url=self.bot.user.display_avatar.url if self.bot.user.display_avatar else None, + ) embeds.append(embed) paginator = EmbedPaginatorSession(ctx, *embeds) diff --git a/cogs/utility.py b/cogs/utility.py index 31cb065a28..16eeafc4d6 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1,3 +1,4 @@ +from core.utils import trigger_typing, truncate, safe_typing import asyncio import inspect import os @@ -90,7 +91,9 @@ async def format_cog_help(self, cog, *, no_cog=False): embed.add_field(name="Commands", value=format_ or "No commands.") name = cog.qualified_name + " - Help" if not no_cog else "Miscellaneous Commands" - embed.set_author(name=name, icon_url=bot.user.display_avatar.url) + embed.set_author( + name=name, icon_url=bot.user.display_avatar.url if bot.user.display_avatar else None + ) embed.set_footer( text=f'Type "{prefix}{self.command_attrs["name"]} command" ' @@ -322,10 +325,10 @@ async def about(self, ctx): embed = discord.Embed(color=self.bot.main_color, timestamp=discord.utils.utcnow()) embed.set_author( name="Modmail - About", - icon_url=self.bot.user.display_avatar.url, + icon_url=self.bot.user.display_avatar.url if self.bot.user.display_avatar else None, url="https://discord.gg/F34cRU8", ) - embed.set_thumbnail(url=self.bot.user.display_avatar.url) + embed.set_thumbnail(url=self.bot.user.display_avatar.url if self.bot.user.display_avatar else None) desc = "This is an open source Discord bot that serves as a means for " desc += "members to easily communicate with server administrators in " @@ -359,7 +362,7 @@ async def about(self, ctx): embed.add_field( name="Support the Developers", value="This bot is completely free for everyone. We rely on kind individuals " - "like you to support us on [`Patreon`](https://patreon.com/kyber) (perks included) " + "like you to support us on [`Buy Me A Coffee`](https://buymeacoffee.com/modmaildev) (perks included for memberships) " "to keep this bot free forever!", inline=False, ) @@ -854,7 +857,10 @@ 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.display_avatar.url) + embed.set_author( + name="Config variable", + icon_url=self.bot.user.display_avatar.url if self.bot.user.display_avatar else None, + ) else: embed = discord.Embed( @@ -871,7 +877,10 @@ async def config_get(self, ctx, *, key: str.lower = None): 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.display_avatar.url) + embed.set_author( + name="Current config(s):", + icon_url=self.bot.user.display_avatar.url if self.bot.user.display_avatar else None, + ) config = self.bot.config.filter_default(self.bot.config) for name, value in config.items(): @@ -1354,7 +1363,18 @@ 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) + try: + await self.bot.main_category.set_permissions(key, read_messages=True) + except discord.Forbidden: + warn = discord.Embed( + title="Missing Permissions", + color=self.bot.error_color, + description=( + "I couldn't update the Modmail category permissions. " + "Please grant me 'Manage Channels' and 'Manage Roles' for this category." + ), + ) + await ctx.send(embed=warn) embed = discord.Embed( title="Success", @@ -1445,17 +1465,50 @@ async def permissions_remove( if level > PermissionLevel.REGULAR: if value == -1: logger.info("Denying @everyone access to Modmail category.") - await self.bot.main_category.set_permissions( - self.bot.modmail_guild.default_role, read_messages=False - ) + try: + await self.bot.main_category.set_permissions( + self.bot.modmail_guild.default_role, read_messages=False + ) + except discord.Forbidden: + warn = discord.Embed( + title="Missing Permissions", + color=self.bot.error_color, + description=( + "I couldn't update the Modmail category permissions. " + "Please grant me 'Manage Channels' and 'Manage Roles' for this category." + ), + ) + await ctx.send(embed=warn) 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) + try: + await self.bot.main_category.set_permissions(user_or_role, overwrite=None) + except discord.Forbidden: + warn = discord.Embed( + title="Missing Permissions", + color=self.bot.error_color, + description=( + "I couldn't update the Modmail category permissions. " + "Please grant me 'Manage Channels' and 'Manage Roles' for this category." + ), + ) + await ctx.send(embed=warn) 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) + try: + await self.bot.main_category.set_permissions(member, overwrite=None) + except discord.Forbidden: + warn = discord.Embed( + title="Missing Permissions", + color=self.bot.error_color, + description=( + "I couldn't update the Modmail category permissions. " + "Please grant me 'Manage Channels' and 'Manage Roles' for this category." + ), + ) + await ctx.send(embed=warn) embed = discord.Embed( title="Success", @@ -1678,7 +1731,7 @@ async def oauth(self, ctx): """ Commands relating to logviewer oauth2 login authentication. - This functionality on your logviewer site is a [**Patron**](https://patreon.com/kyber) only feature. + This functionality on your logviewer site is a [**Buy Me A Coffee**](https://buymeacoffee.com/modmaildev/membership) only feature. """ await ctx.send_help(ctx.command) @@ -1912,8 +1965,12 @@ async def github(self, ctx): if data: embed = discord.Embed(title="GitHub", description="Current User", color=self.bot.main_color) user = data["user"] - embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) - embed.set_thumbnail(url=user["avatar_url"]) + embed.set_author( + name=user["username"], + icon_url=user["avatar_url"] if user["avatar_url"] else None, + url=user["url"], + ) + embed.set_thumbnail(url=user["avatar_url"] if user["avatar_url"] else None) await ctx.send(embed=embed) else: await ctx.send(embed=discord.Embed(title="Invalid Github Token", color=self.bot.error_color)) @@ -1943,7 +2000,11 @@ async def update(self, ctx, *, flag: str = ""): data = await self.bot.api.get_user_info() if data: user = data["user"] - embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) + embed.set_author( + name=user["username"], + icon_url=user["avatar_url"] if user["avatar_url"] else None, + url=user["url"], + ) await ctx.send(embed=embed) else: error = None @@ -1982,7 +2043,7 @@ async def update(self, ctx, *, flag: str = ""): embed.set_author( name=user["username"] + " - Updating bot", - icon_url=user["avatar_url"], + icon_url=user["avatar_url"] if user["avatar_url"] else None, url=user["url"], ) @@ -2000,7 +2061,11 @@ async def update(self, ctx, *, flag: str = ""): color=self.bot.main_color, ) embed.set_footer(text="Force update") - embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) + embed.set_author( + name=user["username"], + icon_url=user["avatar_url"] if user["avatar_url"] else None, + url=user["url"], + ) await ctx.send(embed=embed) else: command = "git pull" diff --git a/core/_color_data.py b/core/_color_data.py index 0ac42d5c1f..13ec45620e 100644 --- a/core/_color_data.py +++ b/core/_color_data.py @@ -3,7 +3,6 @@ Slightly modified to conform with usage. """ - BASE_COLORS = { "b": "0000ff", "g": "007f00", diff --git a/core/changelog.py b/core/changelog.py index 06f141fce1..465479bf17 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -90,7 +90,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.display_avatar.url, + icon_url=self.bot.user.display_avatar.url if self.bot.user.display_avatar else None, url=self.url, ) @@ -98,7 +98,7 @@ def embed(self) -> Embed: embed.add_field(name=name, value=truncate(value, 1024), inline=False) embed.set_footer(text=f"Current version: v{self.bot.version}") - embed.set_thumbnail(url=self.bot.user.display_avatar.url) + embed.set_thumbnail(url=self.bot.user.display_avatar.url if self.bot.user.display_avatar else None) return embed diff --git a/core/clients.py b/core/clients.py index 61c39fdd4b..459ee2ea6b 100644 --- a/core/clients.py +++ b/core/clients.py @@ -592,14 +592,14 @@ async def create_log_entry(self, recipient: Member, channel: TextChannel, creato "id": str(recipient.id), "name": recipient.name, "discriminator": recipient.discriminator, - "avatar_url": recipient.display_avatar.url, + "avatar_url": recipient.display_avatar.url if recipient.display_avatar else None, "mod": False, }, "creator": { "id": str(creator.id), "name": creator.name, "discriminator": creator.discriminator, - "avatar_url": creator.display_avatar.url, + "avatar_url": creator.display_avatar.url if creator.display_avatar else None, "mod": isinstance(creator, Member), }, "closer": None, @@ -661,7 +661,7 @@ async def append_log( "id": str(message.author.id), "name": message.author.name, "discriminator": message.author.discriminator, - "avatar_url": message.author.display_avatar.url, + "avatar_url": message.author.display_avatar.url if message.author.display_avatar else None, "mod": not isinstance(message.channel, DMChannel), }, "content": message.content, @@ -711,7 +711,9 @@ async def create_note(self, recipient: Member, message: Message, message_id: Uni "id": str(message.author.id), "name": message.author.name, "discriminator": message.author.discriminator, - "avatar_url": message.author.display_avatar.url, + "avatar_url": ( + message.author.display_avatar.url if message.author.display_avatar else None + ), }, "message": message.content, "message_id": str(message_id), diff --git a/core/config.py b/core/config.py index 5c6b0dd09d..a704443bb1 100644 --- a/core/config.py +++ b/core/config.py @@ -129,6 +129,17 @@ class ConfigManager: # regex "use_regex_autotrigger": False, "use_hoisted_top_role": True, + # Minimum characters for thread creation + "thread_min_characters": 0, + "thread_min_characters_title": "Message too short", + "thread_min_characters_response": "Your message is too short to create a thread. Please provide more details.", + "thread_min_characters_footer": "Minimum {min_characters} characters required.", + # --- SNOOZE FEATURE CONFIG --- + "max_snooze_time": 604800, # in seconds, default 7 days + "snooze_title": "Thread Snoozed", + "snooze_text": "This thread has been snoozed. The channel will be restored when the user replies or a moderator unsnoozes it.", + "unsnooze_text": "This thread has been unsnoozed and restored.", + "unsnooze_notify_channel": "thread", # Can be a channel ID or 'thread' for the thread's own channel } private_keys = { diff --git a/core/config_help.json b/core/config_help.json index d301763fe4..462e4e1c03 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -1222,5 +1222,105 @@ "If this configuration is enabled, only roles that are hoisted (displayed seperately in member list) will be used. If a user has no hoisted roles, it will return 'None'.", "If you would like to display the top role of a user regardless of if it's hoisted or not, disable `use_hoisted_top_role`." ] + }, + "thread_min_characters": { + "default": "0", + "description": "The minimum number of characters required in the initial message to create a thread. Set to 0 to disable.", + "examples": [ + "`{prefix}config set thread_min_characters 20`" + ], + "notes": [ + "If a user tries to create a thread with a message shorter than this, an error will be shown.", + "See also: `thread_min_characters_title`, `thread_min_characters_response`, `thread_min_characters_footer`." + ] + }, + "thread_min_characters_title": { + "default": "Message too short", + "description": "The title of the error embed when a user tries to create a thread with too few characters.", + "examples": [ + "`{prefix}config set thread_min_characters_title Too short!`" + ], + "notes": [ + "See also: `thread_min_characters`, `thread_min_characters_response`, `thread_min_characters_footer`." + ] + }, + "thread_min_characters_response": { + "default": "Your message is too short to create a thread. Please provide more details.", + "description": "The description of the error embed when a user tries to create a thread with too few characters.", + "examples": [ + "`{prefix}config set thread_min_characters_response Please write a longer message.`" + ], + "notes": [ + "You can use `{min_characters}` as a placeholder for the minimum required characters.", + "See also: `thread_min_characters`, `thread_min_characters_title`, `thread_min_characters_footer`." + ] + }, + "thread_min_characters_footer": { + "default": "Minimum {min_characters} characters required.", + "description": "The footer of the error embed when a user tries to create a thread with too few characters.", + "examples": [ + "`{prefix}config set thread_min_characters_footer At least {min_characters} characters needed.`" + ], + "notes": [ + "You can use `{min_characters}` as a placeholder for the minimum required characters.", + "See also: `thread_min_characters`, `thread_min_characters_title`, `thread_min_characters_response`." + ] + }, + "max_snooze_time": { + "default": "604800 (7 days in seconds)", + "description": "The maximum duration in seconds that a thread can be snoozed. When a thread is snoozed, it is temporarily hidden until the user replies or a moderator unsnoozes it.", + "examples": [ + "`{prefix}config set max_snooze_time 86400` (1 day)", + "`{prefix}config set max_snooze_time 1209600` (14 days)" + ], + "notes": [ + "The value must be specified in seconds.", + "See also: `snooze_title`, `snooze_text`, `unsnooze_text`, `unsnooze_notify_channel`." + ] + }, + "snooze_title": { + "default": "\"Thread Snoozed\"", + "description": "This is the message embed title sent when a thread is snoozed.", + "examples": [ + "`{prefix}config set snooze_title Thread Paused`" + ], + "notes": [ + "See also: `snooze_text`, `unsnooze_title`, `max_snooze_time`." + ] + }, + "snooze_text": { + "default": "\"This thread has been snoozed. The channel will be restored when the user replies or a moderator unsnoozes it.\"", + "description": "This is the message embed content sent when a thread is snoozed.", + "examples": [ + "`{prefix}config set snooze_text This conversation is on hold temporarily.`" + ], + "notes": [ + "Discord flavoured markdown is fully supported in `snooze_text`.", + "See also: `snooze_title`, `unsnooze_text`, `max_snooze_time`." + ] + }, + "unsnooze_text": { + "default": "\"This thread has been unsnoozed and restored.\"", + "description": "This is the message content sent when a thread is unsnoozed and restored.", + "examples": [ + "`{prefix}config set unsnooze_text Thread has been reactivated.`" + ], + "notes": [ + "Discord flavoured markdown is fully supported in `unsnooze_text`.", + "See also: `snooze_text`, `unsnooze_notify_channel`, `max_snooze_time`." + ] + }, + "unsnooze_notify_channel": { + "default": "\"thread\"", + "description": "This is the channel where the unsnooze notification will be sent. Set to \"thread\" to send in the thread's own channel, or provide a specific channel ID.", + "examples": [ + "`{prefix}config set unsnooze_notify_channel thread`", + "`{prefix}config set unsnooze_notify_channel 9234932582312` (9234932582312 is the channel ID)" + ], + "notes": [ + "If set to \"thread\", the notification will be sent in the thread channel itself.", + "If set to a channel ID, the notification will be sent to that specific channel.", + "See also: `unsnooze_text`, `max_snooze_time`." + ] } -} +} \ No newline at end of file diff --git a/core/paginator.py b/core/paginator.py index 5a6844f382..48222d283e 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -149,16 +149,27 @@ def last_page(self): """Returns the index of the last page""" return len(self.pages) - 1 - async def run(self) -> typing.Optional[Message]: + async def run(self) -> None: """ Starts the pagination session. """ if not self.running: await self.show_page(self.current) - if self.view is not None: - await self.view.wait() + # Don't block command execution while waiting for the View timeout. + # Schedule the wait-and-close sequence in the background so the command + # returns immediately (prevents typing indicator from hanging). + if self.view is not None: + + async def _wait_and_close(): + try: + await self.view.wait() + finally: + await self.close(delete=False) + # Fire and forget + self.ctx.bot.loop.create_task(_wait_and_close()) + else: await self.close(delete=False) async def close( @@ -223,8 +234,7 @@ def __init__(self, handler: PaginatorSession, *args, **kwargs): self.clear_items() # clear first so we can control the order self.fill_items() - @discord.ui.button(label="Stop", style=ButtonStyle.danger) - async def stop_button(self, interaction: Interaction, button: Button): + async def stop_callback(self, interaction: Interaction): await self.handler.close(interaction=interaction) def fill_items(self): @@ -244,7 +254,10 @@ def fill_items(self): self.handler._buttons_map[label] = button self.add_item(button) - self.add_item(self.stop_button) + + stop_button = Button(label="Stop", style=ButtonStyle.danger) + stop_button.callback = self.stop_callback + self.add_item(stop_button) async def interaction_check(self, interaction: Interaction): """Only allow the message author to interact""" @@ -314,7 +327,7 @@ def __init__(self, ctx: commands.Context, *embeds, **options): footer_text = footer_text + " • " + embed.footer.text if embed.footer.icon: - icon_url = embed.footer.icon.url + icon_url = embed.footer.icon.url if embed.footer.icon else None else: icon_url = None embed.set_footer(text=footer_text, icon_url=icon_url) @@ -375,7 +388,7 @@ def _set_footer(self): footer_text = footer_text + " • " + self.footer_text if self.embed.footer.icon: - icon_url = self.embed.footer.icon.url + icon_url = self.embed.footer.icon.url if self.embed.footer.icon else None else: icon_url = None diff --git a/core/thread.py b/core/thread.py index 00060ab7f5..d380552694 100644 --- a/core/thread.py +++ b/core/thread.py @@ -8,7 +8,7 @@ import traceback import typing import warnings -from datetime import timedelta +from datetime import timedelta, datetime, timezone from types import SimpleNamespace import isodate @@ -33,6 +33,7 @@ DenyButton, ConfirmThreadCreationView, DummyParam, + extract_forwarded_content, ) logger = getLogger(__name__) @@ -66,6 +67,10 @@ def __init__( self.close_task = None self.auto_close_task = None self._cancelled = False + # --- SNOOZE STATE --- + self.snoozed = False # True if thread is snoozed + self.snooze_data = None # Dict with channel/category/position/messages for restoration + self.log_key = None # Ensure log_key always exists def __repr__(self): return f'Thread(recipient="{self.recipient or self.id}", channel={self.channel.id}, other_recipients={len(self._other_recipients)})' @@ -126,6 +131,202 @@ def cancelled(self, flag: bool): for i in self.wait_tasks: i.cancel() + async def snooze(self, moderator=None, command_used=None, snooze_for=None): + """ + Save channel/category/position/messages to DB, mark as snoozed, delete channel. + """ + if self.snoozed: + return False # Already snoozed + channel = self.channel + if not isinstance(channel, discord.TextChannel): + return False + # Ensure self.log_key is set before snoozing + if not self.log_key: + # Try to fetch from DB using channel_id + log_entry = await self.bot.api.get_log(self.channel.id) + if log_entry and "key" in log_entry: + self.log_key = log_entry["key"] + # Fallback: try by recipient id + elif hasattr(self, "id"): + log_entry = await self.bot.api.get_log(str(self.id)) + if log_entry and "key" in log_entry: + self.log_key = log_entry["key"] + + now = datetime.now(timezone.utc) + self.snooze_data = { + "category_id": channel.category_id, + "position": channel.position, + "name": channel.name, + "topic": channel.topic, + "slowmode_delay": channel.slowmode_delay, + "nsfw": channel.nsfw, + "overwrites": [(role.id, perm._values) for role, perm in channel.overwrites.items()], + "messages": [ + { + "author_id": m.author.id, + "content": m.content, + "attachments": [a.url for a in m.attachments], + "embeds": [e.to_dict() for e in m.embeds], + "created_at": m.created_at.isoformat(), + "type": ( + "mod_only" + if ( + m.embeds + and getattr(m.embeds[0], "author", None) + and ( + getattr(m.embeds[0].author, "name", "").startswith("📝 Note") + or getattr(m.embeds[0].author, "name", "").startswith("📝 Persistent Note") + ) + ) + else None + ), + "author_name": getattr(m.author, "name", None), + } + async for m in channel.history(limit=None, oldest_first=True) + if not ( + m.embeds + and getattr(m.embeds[0], "author", None) + and ( + getattr(m.embeds[0].author, "name", "").startswith("📝 Note") + or getattr(m.embeds[0].author, "name", "").startswith("📝 Persistent Note") + ) + ) + and getattr(m, "type", None) not in ("internal", "note") + ], + "snoozed_by": getattr(moderator, "name", None) if moderator else None, + "snooze_command": command_used, + "log_key": self.log_key, + "snooze_start": now.isoformat(), + "snooze_for": snooze_for, + } + self.snoozed = True + # Save to DB (robust: try recipient.id, then channel_id) + result = await self.bot.api.logs.update_one( + {"recipient.id": str(self.id)}, + {"$set": {"snoozed": True, "snooze_data": self.snooze_data}}, + ) + if result.modified_count == 0 and self.channel: + result = await self.bot.api.logs.update_one( + {"channel_id": str(self.channel.id)}, + {"$set": {"snoozed": True, "snooze_data": self.snooze_data}}, + ) + import logging + + logging.info(f"[SNOOZE] DB update result: {result.modified_count}") + # Delete channel + await channel.delete(reason="Thread snoozed by moderator") + self._channel = None + return True + + async def restore_from_snooze(self): + """ + Recreate channel in original category/position, replay messages, mark as not snoozed. + """ + if not self.snooze_data or not isinstance(self.snooze_data, dict): + import logging + + logging.warning( + f"[UNSNOOZE] Tried to restore thread {self.id} but snooze_data is None or not a dict." + ) + return False + # Now safe to access self.snooze_data + snoozed_by = self.snooze_data.get("snoozed_by") + snooze_command = self.snooze_data.get("snooze_command") + guild = self.bot.modmail_guild + category = guild.get_channel(self.snooze_data["category_id"]) + overwrites = {} + for role_id, perm_values in self.snooze_data["overwrites"]: + role = guild.get_role(role_id) or guild.get_member(role_id) + if role: + overwrites[role] = discord.PermissionOverwrite(**perm_values) + channel = await guild.create_text_channel( + name=self.snooze_data["name"], + category=category, + topic=self.snooze_data["topic"], + slowmode_delay=self.snooze_data["slowmode_delay"], + overwrites=overwrites, + nsfw=self.snooze_data["nsfw"], + position=self.snooze_data["position"], + reason="Thread unsnoozed/restored", + ) + self._channel = channel + # Strictly restore the log_key from snooze_data (never create a new one) + self.log_key = self.snooze_data.get("log_key") + # Replay messages + for msg in self.snooze_data["messages"]: + author = self.bot.get_user(msg["author_id"]) or await self.bot.get_or_fetch_user(msg["author_id"]) + content = msg["content"] + embeds = [discord.Embed.from_dict(e) for e in msg.get("embeds", []) if e] + attachments = msg.get("attachments", []) + msg_type = msg.get("type") + # Only send if there is content, embeds, or attachments + if not content and not embeds and not attachments: + continue # Skip empty messages + author_is_mod = msg["author_id"] not in [r.id for r in self.recipients] + if author_is_mod: + username = msg.get("author_name") or (getattr(author, "name", None)) or "Unknown" + user_id = msg.get("author_id") + if embeds: + embeds[0].set_author( + name=f"{username} ({user_id})", + icon_url=( + author.display_avatar.url + if author and hasattr(author, "display_avatar") + else None + ), + ) + await channel.send(embeds=embeds) + else: + formatted = ( + f"**{username} ({user_id})**: {content}" if content else f"**{username} ({user_id})**" + ) + await channel.send(formatted) + else: + await channel.send(content=content or None, embeds=embeds or None) + self.snoozed = False + # Store snooze_data for notification before clearing + snooze_data_for_notify = self.snooze_data + self.snooze_data = None + # Update channel_id in DB and clear snooze_data (robust: try log_key first) + if self.log_key: + result = await self.bot.api.logs.update_one( + {"key": self.log_key}, + {"$set": {"channel_id": str(channel.id)}, "$unset": {"snoozed": "", "snooze_data": ""}}, + ) + else: + result = await self.bot.api.logs.update_one( + {"recipient.id": str(self.id)}, + {"$set": {"channel_id": str(channel.id)}, "$unset": {"snoozed": "", "snooze_data": ""}}, + ) + if result.modified_count == 0: + result = await self.bot.api.logs.update_one( + {"channel_id": str(channel.id)}, + { + "$set": {"channel_id": str(channel.id)}, + "$unset": {"snoozed": "", "snooze_data": ""}, + }, + ) + import logging + + logging.info(f"[UNSNOOZE] DB update result: {result.modified_count}") + # Notify in the configured channel + notify_channel = self.bot.config.get("unsnooze_notify_channel") or "thread" + notify_text = self.bot.config.get("unsnooze_text") or "This thread has been unsnoozed and restored." + if notify_channel == "thread": + await channel.send(notify_text) + else: + ch = self.bot.get_channel(int(notify_channel)) + if ch: + await ch.send(f"Thread for user <@{self.id}> has been unsnoozed and restored.") + # Show who ran the snooze command and the command used + # Use snooze_data_for_notify to avoid accessing self.snooze_data after it is set to None + snoozed_by = snooze_data_for_notify.get("snoozed_by") if snooze_data_for_notify else None + snooze_command = snooze_data_for_notify.get("snooze_command") if snooze_data_for_notify else None + if snoozed_by or snooze_command: + info = f"Snoozed by: {snoozed_by or 'Unknown'} | Command: {snooze_command or '?snooze'}" + await channel.send(info) + return True + @classmethod async def from_channel(cls, manager: "ThreadManager", channel: discord.TextChannel) -> "Thread": # there is a chance it grabs from another recipient's main thread @@ -415,6 +616,8 @@ async def close( await self._close(closer, silent, delete_channel, message) async def _close(self, closer, silent=False, delete_channel=True, message=None, scheduled=False): + if self.channel: + self.manager.closing.add(self.channel.id) try: self.manager.cache.pop(self.id) except KeyError as e: @@ -541,10 +744,15 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, if user is not None: tasks.append(user.send(embed=embed)) - if delete_channel: + if delete_channel and self.channel: tasks.append(self.channel.delete()) - await asyncio.gather(*tasks) + try: + await asyncio.gather(*tasks) + finally: + if self.channel: + self.manager.closing.discard(self.channel.id) + self.bot.dispatch("thread_close", self, closer, silent, delete_channel, message, scheduled) async def cancel_closure(self, auto_close: bool = False, all: bool = False) -> None: @@ -620,10 +828,7 @@ async def find_linked_messages( ): raise ValueError("Thread message not found.") - if message1.embeds[0].color.value == self.bot.main_color and ( - message1.embeds[0].author.name.startswith("Note") - or message1.embeds[0].author.name.startswith("Persistent Note") - ): + if message1.embeds[0].footer and "Internal Message" in message1.embeds[0].footer.text: if not note: raise ValueError("Thread message not found.") return message1, None @@ -686,7 +891,7 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> embed1.description = message tasks = [self.bot.api.edit_message(message1.id, message), message1.edit(embed=embed1)] - if message1.embeds[0].author.name.startswith("Persistent Note"): + if message1.embeds[0].footer and "Persistent Internal Message" in message1.embeds[0].footer.text: tasks += [self.bot.api.edit_note(message1.id, message)] else: for m2 in message2: @@ -713,7 +918,7 @@ async def delete_message( if m2 is not None: tasks += [m2.delete()] - if message1.embeds[0].author.name.startswith("Persistent Note"): + if message1.embeds[0].footer and "Persistent Internal Message" in message1.embeds[0].footer.text: tasks += [self.bot.api.delete_note(message1.id)] if tasks: @@ -811,8 +1016,9 @@ async def note( thread_creation=thread_creation, ) + # Log as 'note' type for logviewer self.bot.loop.create_task( - self.bot.api.append_log(message, message_id=msg.id, channel_id=self.channel.id, type_="system") + self.bot.api.append_log(message, message_id=msg.id, channel_id=self.channel.id, type_="note") ) return msg @@ -920,6 +1126,38 @@ async def send( persistent_note: bool = False, thread_creation: bool = False, ) -> None: + # Handle notes with Discord-like system message format - return early + if note: + destination = destination or self.channel + content = message.content or "[No content]" + + # Create embed for note with Discord system message style + embed = discord.Embed( + description=content, color=0x5865F2 # Discord blurple color for system messages + ) + + # Set author with note icon and username + if persistent_note: + note_type = "Persistent Note" + else: + note_type = "Note" + + embed.set_author( + name=f"📝 {note_type} ({message.author.name})", icon_url=message.author.display_avatar.url + ) + + # Add timestamp if enabled + if self.bot.config["show_timestamp"]: + embed.timestamp = message.created_at + + # Add a subtle footer to distinguish from replies + if persistent_note: + embed.set_footer(text="Persistent Internal Note") + else: + embed.set_footer(text="Internal Note") + + return await destination.send(embed=embed) + if not note and from_mod: self.bot.loop.create_task(self._restart_close_timer()) # Start or restart thread auto close @@ -943,6 +1181,15 @@ async def send( destination = destination or self.channel + if destination is None: + logger.error("Attempted to send a message to a thread with no channel (destination is None).") + return + try: + await destination.typing() + except discord.NotFound: + logger.warning("Channel not found when trying to send message.") + return + author = message.author member = self.bot.guild.get_member(author.id) if member: @@ -950,10 +1197,57 @@ async def send( else: avatar_url = author.display_avatar.url - embed = discord.Embed(description=message.content) + # Handle forwarded messages first + forwarded_jump_url = None + if hasattr(message, "message_snapshots") and len(message.message_snapshots) > 0: + snap = message.message_snapshots[0] + # Only show "No content" if there's truly no content (no text, attachments, embeds, or stickers) + if not snap.content and not message.attachments and not message.embeds and not message.stickers: + content = "No content" + else: + content = snap.content or "" + + # Get jump_url from cached_message, fetch if not cached + if hasattr(snap, "cached_message") and snap.cached_message is not None: + forwarded_jump_url = snap.cached_message.jump_url + else: + if ( + hasattr(message, "reference") + and message.reference + and message.reference.type == discord.MessageReferenceType.forward + ): + try: + original_msg_channel = self.bot.get_channel(message.reference.channel_id) + original_msg = await original_msg_channel.fetch_message(message.reference.message_id) + forwarded_jump_url = original_msg.jump_url + except (discord.NotFound, discord.Forbidden, AttributeError): + pass + + content = f"📨 **Forwarded message:**\n{content}" if content else "📨 **Forwarded message:**" + else: + # Only show "No content" if there's truly no content (no text, attachments, embeds, or stickers) + if ( + not message.content + and not message.attachments + and not message.embeds + and not message.stickers + ): + content = "No content" + else: + content = message.content or "" + + # Only set description if there's actual content to show + if content: + embed = discord.Embed(description=content) + else: + embed = discord.Embed() if self.bot.config["show_timestamp"]: embed.timestamp = message.created_at + # Add forwarded message context + if forwarded_jump_url: + embed.add_field(name="Context", value=f"- {forwarded_jump_url}", inline=True) + system_avatar_url = "https://discordapp.com/assets/f78426a064bc9dd24847519259bc42af.png" if not note: @@ -983,12 +1277,18 @@ async def send( url=f"https://discordapp.com/users/{author.id}#{message.id}", ) else: - # Special note messages + # Notes use system message style with note icon + if persistent_note: + note_type = "Persistent Note" + else: + note_type = "Note" + embed.set_author( - name=f"{'Persistent' if persistent_note else ''} Note ({author.name})", - icon_url=system_avatar_url, + name=f"📝 {note_type} ({str(author)})", + icon_url=avatar_url, url=f"https://discordapp.com/users/{author.id}#{message.id}", ) + embed.color = 0x5865F2 # Discord blurple for system messages ext = [(a.url, a.filename, False) for a in message.attachments] @@ -1001,7 +1301,7 @@ async def send( attachments.append(attachment) image_urls = re.findall( - r"http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$\-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + r"http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", message.content, ) @@ -1113,22 +1413,33 @@ def lottie_to_png(data): file_upload_count += 1 if from_mod: - embed.colour = self.bot.mod_color - # Anonymous reply sent in thread channel - if anonymous and isinstance(destination, discord.TextChannel): - embed.set_footer(text="Anonymous Reply") - # Normal messages - elif not anonymous: - mod_tag = self.bot.config["mod_tag"] - if mod_tag is None: - mod_tag = str(get_top_role(message.author, self.bot.config["use_hoisted_top_role"])) - embed.set_footer(text=mod_tag) # Normal messages + if note: + # Notes use Discord blurple and special footer + embed.colour = 0x5865F2 + if persistent_note: + embed.set_footer(text="Persistent Internal Note") + else: + embed.set_footer(text="Internal Note") else: - embed.set_footer(text=self.bot.config["anon_tag"]) - elif note: - embed.colour = self.bot.main_color + # Regular mod messages + embed.colour = self.bot.mod_color + # Anonymous reply sent in thread channel + if anonymous and isinstance(destination, discord.TextChannel): + embed.set_footer(text="Anonymous Reply") + # Normal messages + elif not anonymous: + mod_tag = self.bot.config["mod_tag"] + if mod_tag is None: + mod_tag = str(get_top_role(message.author, self.bot.config["use_hoisted_top_role"])) + embed.set_footer(text=mod_tag) # Normal messages + else: + embed.set_footer(text=self.bot.config["anon_tag"]) else: - embed.set_footer(text=f"Message ID: {message.id}") + # Add forwarded message indicator in footer for mods + footer_text = f"Message ID: {message.id}" + if hasattr(message, "message_snapshots") and len(message.message_snapshots) > 0: + footer_text += " • Forwarded" + embed.set_footer(text=footer_text) embed.colour = self.bot.recipient_color if (from_mod or note) and not thread_creation: @@ -1146,11 +1457,14 @@ def lottie_to_png(data): ): logger.info("Sending a message to %s when DM disabled is set.", self.recipient) + # Best-effort typing: never block message delivery if typing fails try: await destination.typing() except discord.NotFound: logger.warning("Channel not found.") raise + except (discord.Forbidden, discord.HTTPException, Exception) as e: + logger.warning("Unable to send typing to %s: %s. Continuing without typing.", destination, e) if not from_mod and not note: mentions = await self.get_notifications() @@ -1278,6 +1592,7 @@ class ThreadManager: def __init__(self, bot): self.bot = bot self.cache = {} + self.closing = set() async def populate_cache(self) -> None: for channel in self.bot.modmail_guild.text_channels: @@ -1301,6 +1616,8 @@ async def find( ) -> typing.Optional[Thread]: """Finds a thread from cache or from discord channel topics.""" if recipient is None and channel is not None and isinstance(channel, discord.TextChannel): + if channel.id in self.closing: + return None thread = await self._find_from_channel(channel) if thread is None: user_id, thread = next( @@ -1322,11 +1639,8 @@ async def find( logger.warning("Thread for %s cancelled.", recipient) return thread else: - if not thread.cancelled and ( - 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) - await thread.close(closer=self.bot.user, silent=True, delete_channel=False) + # If the thread is snoozed (channel is None), return it for restoration + if thread.cancelled: thread = None else: @@ -1406,6 +1720,33 @@ async def create( ) -> Thread: """Creates a Modmail thread""" + # Minimum character check + min_chars = self.bot.config.get("thread_min_characters") + if min_chars is None: + min_chars = 0 + try: + min_chars = int(min_chars) + except ValueError: + min_chars = 0 + if min_chars > 0 and message is not None and message.content is not None: + if len(message.content.strip()) < min_chars: + embed = discord.Embed( + title=self.bot.config["thread_min_characters_title"], + description=self.bot.config["thread_min_characters_response"].replace( + "{min_characters}", str(min_chars) + ), + color=self.bot.error_color, + ) + embed.set_footer( + text=self.bot.config["thread_min_characters_footer"].replace( + "{min_characters}", str(min_chars) + ) + ) + await message.channel.send(embed=embed) + thread = Thread(self, recipient) + thread.cancelled = True + return thread + # checks for existing thread in cache thread = self.cache.get(recipient.id) if thread: @@ -1433,8 +1774,10 @@ async def create( else: destination = message.channel view = ConfirmThreadCreationView() - view.add_item(AcceptButton(self.bot.config["confirm_thread_creation_accept"])) - view.add_item(DenyButton(self.bot.config["confirm_thread_creation_deny"])) + view.add_item( + AcceptButton("accept-thread-creation", self.bot.config["confirm_thread_creation_accept"]) + ) + view.add_item(DenyButton("deny-thread-creation", self.bot.config["confirm_thread_creation_deny"])) confirm = await destination.send( embed=discord.Embed( title=self.bot.config["confirm_thread_creation_title"], diff --git a/core/time.py b/core/time.py index c56c7264e2..b6ce91f419 100644 --- a/core/time.py +++ b/core/time.py @@ -3,6 +3,7 @@ Source: https://github.com/Rapptz/RoboDanny/blob/rewrite/cogs/utils/time.py """ + from __future__ import annotations import datetime @@ -159,6 +160,30 @@ def __init__(self, dt: datetime.datetime, now: datetime.datetime = None): async def ensure_constraints( self, ctx: Context, uft: UserFriendlyTime, now: datetime.datetime, remaining: str ) -> None: + # Strip stray connector words like "in", "to", or "at" that may + # remain when the natural language parser isolates the time token + # positioned at the end (e.g. "in 10m" leaves "in" before the token). + if isinstance(remaining, str): + cleaned = remaining.strip(" ,.!") + stray_tokens = { + "in", + "to", + "at", + "me", + # also treat vague times of day as stray tokens when they are the only leftover word + "evening", + "night", + "midnight", + "morning", + "afternoon", + "tonight", + "noon", + "today", + "tomorrow", + } + if cleaned.lower() in stray_tokens: + remaining = "" + if self.dt < now: raise commands.BadArgument("This time is in the past.") @@ -198,6 +223,26 @@ async def convert(self, ctx: Context, argument: str, *, now=None) -> FriendlyTim if now is None: now = ctx.message.created_at + # Heuristic: If the user provides only certain single words that are commonly + # used as salutations or vague times of day, interpret them as a message + # rather than a schedule. This avoids accidental scheduling when the intent + # is a short message (e.g. '?close evening'). Explicit scheduling still works + # via 'in 2h', '2m30s', 'at 8pm', etc. + if argument.strip().lower() in { + "evening", + "night", + "midnight", + "morning", + "afternoon", + "tonight", + "noon", + "today", + "tomorrow", + }: + result = FriendlyTimeResult(now) + await result.ensure_constraints(ctx, self, now, argument) + return result + match = regex.match(argument) if match is not None and match.group(0): data = {k: int(v) for k, v in match.groupdict(default=0).items()} @@ -244,7 +289,10 @@ async def convert(self, ctx: Context, argument: str, *, now=None) -> FriendlyTim if not status.hasDateOrTime: raise commands.BadArgument('Invalid time provided, try e.g. "tomorrow" or "3 days".') - if begin not in (0, 1) and end != len(argument): + # If the parsed time token is embedded in the text but only followed by + # trailing punctuation/whitespace, treat it as if it's positioned at the end. + trailing = argument[end:].strip(" ,.!") + if begin not in (0, 1) and trailing != "": raise commands.BadArgument( "Time is either in an inappropriate location, which " "must be either at the end or beginning of your input, " @@ -259,6 +307,20 @@ async def convert(self, ctx: Context, argument: str, *, now=None) -> FriendlyTim if status.accuracy == pdt.pdtContext.ACU_HALFDAY: dt = dt.replace(day=now.day + 1) + # Heuristic: If the matched time string is a vague time-of-day (e.g., + # 'evening', 'morning', 'afternoon', 'night') and there's additional + # non-punctuation text besides that token, assume the user intended a + # closing message rather than scheduling. This avoids cases like + # '?close Have a good evening!' being treated as a scheduled close. + vague_tod = {"evening", "morning", "afternoon", "night"} + matched_text = dt_string.strip().strip('"').rstrip(" ,.!").lower() + pre_text = argument[:begin].strip(" ,.!") + post_text = argument[end:].strip(" ,.!") + if matched_text in vague_tod and (pre_text or post_text): + result = FriendlyTimeResult(now) + await result.ensure_constraints(ctx, self, now, argument) + return result + result = FriendlyTimeResult(dt.replace(tzinfo=datetime.timezone.utc), now) remaining = "" diff --git a/core/utils.py b/core/utils.py index 9f9f572f5a..19ff4cd37b 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,10 +1,10 @@ import base64 import functools +import contextlib import re import typing from datetime import datetime, timezone from difflib import get_close_matches -from distutils.util import strtobool as _stb # pylint: disable=import-error from itertools import takewhile, zip_longest from urllib import parse @@ -34,15 +34,18 @@ "normalize_alias", "format_description", "trigger_typing", + "safe_typing", "escape_code_block", "tryint", "get_top_role", "get_joint_id", "extract_block_timestamp", + "return_or_truncate", "AcceptButton", "DenyButton", "ConfirmThreadCreationView", "DummyParam", + "extract_forwarded_content", ] @@ -52,15 +55,12 @@ def strtobool(val): if isinstance(val, bool): return val - try: - return _stb(str(val)) - except ValueError: - val = val.lower() - if val == "enable": - return 1 - if val == "disable": - return 0 - raise + val = str(val).lower() + if val in ("y", "yes", "on", "1", "true", "t", "enable"): + return 1 + if val in ("n", "no", "off", "0", "false", "f", "disable"): + return 0 + raise ValueError(f"invalid truth value {val}") class User(commands.MemberConverter): @@ -391,7 +391,7 @@ def decode_alias(m): iterate = [alias] for a in iterate: - a = re.sub("\x1AU(.+?)\x1AU", decode_alias, a) + a = re.sub(r"\x1AU(.+?)\x1AU", decode_alias, a) if a[0] == a[-1] == '"': a = a[1:-1] aliases.append(a) @@ -423,11 +423,42 @@ def format_description(i, names): ) +class _SafeTyping: + """Best-effort typing context manager. + + Suppresses errors from Discord's typing endpoint so core flows continue + when typing is disabled or experiencing outages. + """ + + def __init__(self, target): + # target can be a Context or any Messageable (channel/DM/user) + self._target = target + self._cm = None + + async def __aenter__(self): + try: + self._cm = self._target.typing() + return await self._cm.__aenter__() + except Exception: + # typing is best-effort; ignore any failure + self._cm = None + + async def __aexit__(self, exc_type, exc, tb): + if self._cm is not None: + with contextlib.suppress(Exception): + return await self._cm.__aexit__(exc_type, exc, tb) + + +def safe_typing(target): + return _SafeTyping(target) + + def trigger_typing(func): @functools.wraps(func) async def wrapper(self, ctx: commands.Context, *args, **kwargs): - await ctx.typing() - return await func(self, ctx, *args, **kwargs) + # Keep typing active for the duration of the command; suppress failures + async with safe_typing(ctx): + return await func(self, ctx, *args, **kwargs) return wrapper @@ -573,9 +604,15 @@ def extract_block_timestamp(reason, id_): return end_time, after +def return_or_truncate(text, max_length): + if len(text) <= max_length: + return text + return text[: max_length - 3] + "..." + + class AcceptButton(discord.ui.Button): - def __init__(self, emoji): - super().__init__(style=discord.ButtonStyle.gray, emoji=emoji) + def __init__(self, custom_id: str, emoji: str): + super().__init__(style=discord.ButtonStyle.gray, emoji=emoji, custom_id=custom_id) async def callback(self, interaction: discord.Interaction): self.view.value = True @@ -584,8 +621,8 @@ async def callback(self, interaction: discord.Interaction): class DenyButton(discord.ui.Button): - def __init__(self, emoji): - super().__init__(style=discord.ButtonStyle.gray, emoji=emoji) + def __init__(self, custom_id: str, emoji: str): + super().__init__(style=discord.ButtonStyle.gray, emoji=emoji, custom_id=custom_id) async def callback(self, interaction: discord.Interaction): self.view.value = False @@ -599,6 +636,100 @@ def __init__(self): self.value = None +def extract_forwarded_content(message) -> typing.Optional[str]: + """ + Extract forwarded message content from Discord forwarded messages. + + Parameters + ---------- + message : discord.Message + The message to extract forwarded content from. + + Returns + ------- + Optional[str] + The extracted forwarded content, or None if not a forwarded message. + """ + import discord + + try: + # Handle multi-forward (message_snapshots) + if hasattr(message, "flags") and getattr(message.flags, "has_snapshot", False): + if hasattr(message, "message_snapshots") and message.message_snapshots: + forwarded_parts = [] + for snap in message.message_snapshots: + author = getattr(snap, "author", None) + author_name = getattr(author, "name", "Unknown") if author else "Unknown" + snap_content = getattr(snap, "content", "") + + if snap_content: + # Truncate very long messages to prevent spam + if len(snap_content) > 500: + snap_content = snap_content[:497] + "..." + forwarded_parts.append(f"**{author_name}:** {snap_content}") + elif getattr(snap, "embeds", None): + for embed in snap.embeds: + if hasattr(embed, "description") and embed.description: + embed_desc = embed.description + if len(embed_desc) > 300: + embed_desc = embed_desc[:297] + "..." + forwarded_parts.append(f"**{author_name}:** {embed_desc}") + break + elif getattr(snap, "attachments", None): + attachment_info = ", ".join( + [getattr(a, "filename", "Unknown") for a in snap.attachments[:3]] + ) + if len(snap.attachments) > 3: + attachment_info += f" (+{len(snap.attachments) - 3} more)" + forwarded_parts.append(f"**{author_name}:** [Attachments: {attachment_info}]") + else: + forwarded_parts.append(f"**{author_name}:** [No content]") + + if forwarded_parts: + return "\n".join(forwarded_parts) + + # Handle single-message forward + elif getattr(message, "type", None) == getattr(discord.MessageType, "forward", None): + ref = getattr(message, "reference", None) + if ( + ref + and hasattr(discord, "MessageReferenceType") + and getattr(ref, "type", None) == getattr(discord.MessageReferenceType, "forward", None) + ): + try: + ref_msg = getattr(ref, "resolved", None) + if ref_msg: + ref_author = getattr(ref_msg, "author", None) + ref_author_name = getattr(ref_author, "name", "Unknown") if ref_author else "Unknown" + ref_content = getattr(ref_msg, "content", "") + + if ref_content: + if len(ref_content) > 500: + ref_content = ref_content[:497] + "..." + return f"**{ref_author_name}:** {ref_content}" + elif getattr(ref_msg, "embeds", None): + for embed in ref_msg.embeds: + if hasattr(embed, "description") and embed.description: + embed_desc = embed.description + if len(embed_desc) > 300: + embed_desc = embed_desc[:297] + "..." + return f"**{ref_author_name}:** {embed_desc}" + elif getattr(ref_msg, "attachments", None): + attachment_info = ", ".join( + [getattr(a, "filename", "Unknown") for a in ref_msg.attachments[:3]] + ) + if len(ref_msg.attachments) > 3: + attachment_info += f" (+{len(ref_msg.attachments) - 3} more)" + return f"**{ref_author_name}:** [Attachments: {attachment_info}]" + except Exception: + pass + except Exception: + # Silently handle any unexpected errors + pass + + return None + + class DummyParam: """ A dummy parameter that can be used for MissingRequiredArgument. diff --git a/pyproject.toml b/pyproject.toml index 7e29a4d4ef..0a6d6eaa6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.1.2' +version = '4.2.0' description = "Modmail is similar to Reddit's 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 = [ diff --git a/requirements.txt b/requirements.txt index 2c7bdb7880..8530e8e0d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,41 +1,43 @@ -i https://pypi.org/simple -aiodns==3.1.1 +aiodns==3.5.0; python_version >= '3.9' aiohttp==3.9.0; python_version >= '3.8' -aiosignal==1.3.1; python_version >= '3.7' -async-timeout==4.0.3; python_version < '3.11' -attrs==23.1.0; python_version >= '3.7' +aiosignal==1.4.0; python_version >= '3.9' +attrs==25.3.0; python_version >= '3.8' +audioop-lts==0.2.2; python_version >= '3.13' brotli==1.1.0 -cairocffi==1.6.1; python_version >= '3.7' -cairosvg==2.7.1; python_version >= '3.5' -certifi==2023.11.17; python_version >= '3.6' -cffi==1.16.0; python_version >= '3.8' -charset-normalizer==3.3.2; python_full_version >= '3.7.0' +cairocffi==1.7.1; python_version >= '3.8' +cairosvg==2.8.2; python_version >= '3.9' +certifi==2025.10.5; python_version >= '3.7' +cffi==2.0.0; python_version >= '3.9' +charset-normalizer==3.4.3; python_version >= '3.7' colorama==0.4.6; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' -cssselect2==0.7.0; python_version >= '3.7' +cssselect2==0.8.0; python_version >= '3.9' defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' -discord.py[speed]==2.3.2; python_full_version >= '3.8.0' -dnspython==2.4.2; python_version >= '3.8' and python_version < '4.0' +discord.py[speed]==2.6.3; python_version >= '3.8' +dnspython==2.8.0; python_version >= '3.10' emoji==2.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -frozenlist==1.4.0; python_version >= '3.8' -idna==3.4; python_version >= '3.5' +frozenlist==1.8.0; python_version >= '3.9' +idna==3.10; python_version >= '3.6' isodate==0.6.1 lottie[pdf]==0.7.0; python_version >= '3' motor==3.3.2; python_version >= '3.7' -multidict==6.0.4; python_version >= '3.7' +multidict==6.6.4; python_version >= '3.9' natural==0.2.0 -orjson==3.9.10 +orjson==3.11.3; python_version >= '3.9' packaging==23.2; python_version >= '3.7' parsedatetime==2.6 -pillow==10.1.0; python_version >= '3.8' -pycares==4.4.0; python_version >= '3.8' -pycparser==2.21 -pymongo[srv]==4.6.0; python_version >= '3.7' -python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +pillow==11.3.0; python_version >= '3.9' +propcache==0.4.0; python_version >= '3.9' +pycares==4.11.0; python_version >= '3.9' +pycparser==2.23; python_version >= '3.8' +pymongo[srv]==4.15.2; python_version >= '3.9' +python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' python-dotenv==1.0.0; python_version >= '3.8' requests==2.31.0; python_version >= '3.7' -six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -tinycss2==1.2.1; python_version >= '3.7' -urllib3==2.1.0; python_version >= '3.8' -uvloop==0.19.0; sys_platform != 'win32' +six==1.17.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' +tinycss2==1.4.0; python_version >= '3.8' +urllib3==2.5.0; python_version >= '3.9' +uvloop==0.21.0; sys_platform != 'win32' webencodings==0.5.1 -yarl==1.9.3; python_version >= '3.7' +yarl==1.21.0; python_version >= '3.9' +zstandard==0.25.0; python_version >= '3.9' From c11e32fc6de6e3004a72676e11af15aec216ef32 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:32:08 -0800 Subject: [PATCH 1211/1221] Update readme version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 50f71890ca..f7470c9900 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v4.1.2-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v4.2.0-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> From 151769381340d7999c3750ea4157541e4b615bf5 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 11 Nov 2025 02:46:09 -0800 Subject: [PATCH 1212/1221] Relock pipfile --- Pipfile.lock | 1888 +++++++++++++++++++++++----------------------- requirements.txt | 24 +- 2 files changed, 959 insertions(+), 953 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index ae2da5ae76..5c4310d52e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -115,196 +115,116 @@ }, "attrs": { "hashes": [ - "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", - "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" + "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", + "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373" ], - "markers": "python_version >= '3.8'", - "version": "==25.3.0" - }, - "audioop-lts": { - "hashes": [ - "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", - "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", - "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", - "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", - "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", - "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", - "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", - "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", - "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", - "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", - "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", - "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", - "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", - "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", - "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", - "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", - "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", - "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", - "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", - "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", - "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", - "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", - "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", - "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", - "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", - "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", - "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", - "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", - "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", - "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", - "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", - "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", - "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", - "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", - "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", - "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", - "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", - "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", - "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", - "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", - "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", - "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", - "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", - "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", - "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", - "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", - "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", - "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", - "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800" - ], - "markers": "python_version >= '3.13'", - "version": "==0.2.2" + "markers": "python_version >= '3.9'", + "version": "==25.4.0" }, "brotli": { "hashes": [ - "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", - "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", - "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354", - "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419", - "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a", - "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", - "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c", - "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", - "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", - "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", - "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", - "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", - "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", - "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438", - "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", - "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b", - "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b", - "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68", - "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", - "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d", - "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943", - "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", - "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", - "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", - "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", - "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", - "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", - "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", - "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547", - "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", - "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", - "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", - "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a", - "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb", - "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112", - "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", - "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2", - "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", - "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", - "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95", - "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", - "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", - "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", - "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38", - "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914", - "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", - "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", - "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", - "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", - "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c", - "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", - "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f", - "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", - "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", - "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", - "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", - "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", - "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", - "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", - "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", - "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", - "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", - "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", - "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", - "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", - "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", - "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", - "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", - "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", - "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", - "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74", - "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", - "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60", - "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", - "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1", - "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", - "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", - "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", - "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", - "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460", - "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", - "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", - "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2", - "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", - "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", - "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", - "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75", - "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5", - "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", - "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", - "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", - "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", - "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", - "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", - "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", - "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", - "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01", - "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", - "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", - "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", - "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", - "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579", - "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84", - "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7", - "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c", - "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", - "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52", - "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", - "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", - "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", - "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", - "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", - "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", - "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", - "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2", - "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", - "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", - "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", - "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643", - "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", - "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", - "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985", - "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", - "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2", - "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064" - ], - "version": "==1.1.0" + "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", + "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", + "sha256:09ac247501d1909e9ee47d309be760c89c990defbb2e0240845c892ea5ff0de4", + "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", + "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", + "sha256:14ef29fc5f310d34fc7696426071067462c9292ed98b5ff5a27ac70a200e5470", + "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", + "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", + "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", + "sha256:1b71754d5b6eda54d16fbbed7fce2d8bc6c052a1b91a35c320247946ee103502", + "sha256:1ce223652fd4ed3eb2b7f78fbea31c52314baecfac68db44037bb4167062a937", + "sha256:1e68cdf321ad05797ee41d1d09169e09d40fdf51a725bb148bff892ce04583d7", + "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", + "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", + "sha256:2881416badd2a88a7a14d981c103a52a23a276a553a8aacc1346c2ff47c8dc17", + "sha256:29b7e6716ee4ea0c59e3b241f682204105f7da084d6254ec61886508efeb43bc", + "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", + "sha256:2d39b54b968f4b49b5e845758e202b1035f948b0561ff5e6385e855c96625971", + "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", + "sha256:3173e1e57cebb6d1de186e46b5680afbd82fd4301d7b2465beebe83ed317066d", + "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", + "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", + "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", + "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", + "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", + "sha256:3ebe801e0f4e56d17cd386ca6600573e3706ce1845376307f5d2cbd32149b69a", + "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", + "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", + "sha256:465a0d012b3d3e4f1d6146ea019b5c11e3e87f03d1676da1cc3833462e672fb0", + "sha256:4735a10f738cb5516905a121f32b24ce196ab82cfc1e4ba2e3ad1b371085fd46", + "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", + "sha256:50b1b799f45da91292ffaa21a473ab3a3054fa78560e8ff67082a185274431c8", + "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", + "sha256:5732eff8973dd995549a18ecbd8acd692ac611c5c0bb3f59fa3541ae27b33be3", + "sha256:598e88c736f63a0efec8363f9eb34e5b5536b7b6b1821e401afcb501d881f59a", + "sha256:640fe199048f24c474ec6f3eae67c48d286de12911110437a36a87d7c89573a6", + "sha256:66c02c187ad250513c2f4fce973ef402d22f80e0adce734ee4e4efd657b6cb64", + "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", + "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", + "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", + "sha256:71a66c1c9be66595d628467401d5976158c97888c2c9379c034e1e2312c5b4f5", + "sha256:7274942e69b17f9cef76691bcf38f2b2d4c8a5f5dba6ec10958363dcb3308a0a", + "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", + "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", + "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", + "sha256:7ad8cec81f34edf44a1c6a7edf28e7b7806dfb8886e371d95dcf789ccd4e4982", + "sha256:7e9053f5fb4e0dfab89243079b3e217f2aea4085e4d58c5c06115fc34823707f", + "sha256:7fa18d65a213abcfbb2f6cafbb4c58863a8bd6f2103d65203c520ac117d1944b", + "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", + "sha256:82676c2781ecf0ab23833796062786db04648b7aae8be139f6b8065e5e7b1518", + "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", + "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", + "sha256:865cedc7c7c303df5fad14a57bc5db1d4f4f9b2b4d0a7523ddd206f00c121a16", + "sha256:88ef7d55b7bcf3331572634c3fd0ed327d237ceb9be6066810d39020a3ebac7a", + "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", + "sha256:8d4f47f284bdd28629481c97b5f29ad67544fa258d9091a6ed1fda47c7347cd1", + "sha256:92edab1e2fd6cd5ca605f57d4545b6599ced5dea0fd90b2bcdf8b247a12bd190", + "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", + "sha256:95db242754c21a88a79e01504912e537808504465974ebb92931cfca2510469e", + "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", + "sha256:96fbe82a58cdb2f872fa5d87dedc8477a12993626c446de794ea025bbda625ea", + "sha256:99cfa69813d79492f0e5d52a20fd18395bc82e671d5d40bd5a91d13e75e468e8", + "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", + "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", + "sha256:9fe11467c42c133f38d42289d0861b6b4f9da31e8087ca2c0d7ebb4543625526", + "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", + "sha256:a387225a67f619bf16bd504c37655930f910eb03675730fc2ad69d3d8b5e7e92", + "sha256:a56ef534b66a749759ebd091c19c03ef81eb8cd96f0d1d16b59127eaf1b97a12", + "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", + "sha256:ac27a70bda257ae3f380ec8310b0a06680236bea547756c277b5dfe55a2452a8", + "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", + "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", + "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", + "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997", + "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", + "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", + "sha256:b908d1a7b28bc72dfb743be0d4d3f8931f8309f810af66c906ae6cd4127c93cb", + "sha256:ba76177fd318ab7b3b9bf6522be5e84c2ae798754b6cc028665490f6e66b5533", + "sha256:bba6e7e6cfe1e6cb6eb0b7c2736a6059461de1fa2c0ad26cf845de6c078d16c8", + "sha256:c0d6770111d1879881432f81c369de5cde6e9467be7c682a983747ec800544e2", + "sha256:c16ab1ef7bb55651f5836e8e62db1f711d55b82ea08c3b8083ff037157171a69", + "sha256:c1702888c9f3383cc2f09eb3e88b8babf5965a54afb79649458ec7c3c7a63e96", + "sha256:c25332657dee6052ca470626f18349fc1fe8855a56218e19bd7a8c6ad4952c49", + "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", + "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", + "sha256:d206a36b4140fbb5373bf1eb73fb9de589bb06afd0d22376de23c5e91d0ab35f", + "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", + "sha256:d8c05b1dfb61af28ef37624385b0029df902ca896a639881f594060b30ffc9a7", + "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", + "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", + "sha256:e80a28f2b150774844c8b454dd288be90d76ba6109670fe33d7ff54d96eb5cb8", + "sha256:e813da3d2d865e9793ef681d3a6b66fa4b7c19244a45b817d0cceda67e615990", + "sha256:e85190da223337a6b7431d92c799fca3e2982abd44e7b8dec69938dcc81c8e9e", + "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", + "sha256:eda5a6d042c698e28bda2507a89b16555b9aa954ef1d750e1c20473481aff675", + "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196", + "sha256:f16dace5e4d3596eaeb8af334b4d2c820d34b8278da633ce4a00020b2eac981c", + "sha256:f8d635cafbbb0c61327f942df2e3f474dde1cff16c3cd0580564774eaba1ee13", + "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", + "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d" + ], + "version": "==1.2.0" }, "cairocffi": { "hashes": [ @@ -422,88 +342,122 @@ }, "charset-normalizer": { "hashes": [ - "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", - "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", - "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", - "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", - "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", - "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", - "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c", - "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", - "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", - "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", - "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", - "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", - "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", - "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", - "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", - "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", - "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", - "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", - "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4", - "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", - "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", - "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", - "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", - "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", - "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", - "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", - "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b", - "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", - "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", - "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", - "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", - "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", - "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", - "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", - "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", - "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", - "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a", - "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40", - "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", - "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", - "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", - "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", - "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", - "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", - "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", - "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", - "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", - "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", - "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9", - "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", - "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", - "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", - "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b", - "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", - "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942", - "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", - "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", - "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b", - "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", - "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", - "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", - "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", - "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", - "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", - "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", - "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", - "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", - "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", - "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", - "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", - "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", - "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb", - "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", - "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557", - "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", - "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", - "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", - "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", - "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9" + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" ], "markers": "python_version >= '3.7'", - "version": "==3.4.3" + "version": "==3.4.4" }, "colorama": { "hashes": [ @@ -696,11 +650,11 @@ }, "idna": { "hashes": [ - "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", - "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" ], - "markers": "python_version >= '3.6'", - "version": "==3.10" + "markers": "python_version >= '3.8'", + "version": "==3.11" }, "isodate": { "hashes": [ @@ -731,119 +685,155 @@ }, "multidict": { "hashes": [ - "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", - "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729", - "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", - "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", - "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", - "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495", - "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f", - "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1", - "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", - "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", - "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", - "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", - "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae", - "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", - "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", - "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f", - "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f", - "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", - "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", - "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0", - "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", - "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", - "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", - "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", - "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0", - "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", - "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", - "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", - "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", - "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", - "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", - "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", - "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", - "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", - "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", - "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", - "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", - "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0", - "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb", - "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", - "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", - "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", - "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", - "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", - "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", - "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", - "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb", - "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", - "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", - "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", - "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", - "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", - "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", - "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", - "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", - "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", - "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", - "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", - "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", - "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db", - "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b", - "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", - "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", - "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987", - "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796", - "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", - "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978", - "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", - "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", - "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", - "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", - "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace", - "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", - "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4", - "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665", - "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f", - "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", - "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", - "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb", - "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", - "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17", - "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb", - "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", - "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877", - "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", - "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", - "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0", - "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", - "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8", - "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", - "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", - "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd", - "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", - "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7", - "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", - "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", - "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", - "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", - "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb", - "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", - "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6", - "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb", - "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210", - "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53", - "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", - "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", - "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9", - "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", - "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a", - "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773" + "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", + "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", + "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", + "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", + "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", + "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", + "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", + "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", + "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", + "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", + "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", + "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", + "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", + "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", + "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", + "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", + "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", + "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", + "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", + "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", + "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", + "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", + "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", + "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", + "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", + "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", + "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", + "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", + "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", + "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", + "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", + "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c", + "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", + "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6", + "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", + "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", + "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84", + "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", + "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", + "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", + "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", + "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", + "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", + "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", + "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", + "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", + "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", + "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", + "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", + "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", + "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", + "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", + "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", + "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4", + "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", + "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", + "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", + "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", + "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", + "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", + "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c", + "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", + "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546", + "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", + "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", + "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", + "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", + "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", + "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", + "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", + "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f", + "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", + "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e", + "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", + "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", + "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", + "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", + "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851", + "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb", + "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32", + "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", + "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", + "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", + "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", + "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", + "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", + "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", + "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5", + "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", + "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", + "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", + "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", + "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", + "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", + "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", + "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", + "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", + "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", + "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", + "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", + "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", + "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", + "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", + "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", + "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", + "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", + "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", + "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", + "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", + "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329", + "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", + "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", + "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", + "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", + "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", + "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", + "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", + "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", + "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", + "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", + "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", + "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91", + "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", + "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", + "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", + "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40", + "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", + "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", + "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", + "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", + "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", + "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", + "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", + "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", + "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", + "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e", + "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", + "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", + "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", + "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", + "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", + "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", + "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687", + "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e", + "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", + "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7" ], "markers": "python_version >= '3.9'", - "version": "==6.6.4" + "version": "==6.7.0" }, "natural": { "hashes": [ @@ -854,92 +844,96 @@ }, "orjson": { "hashes": [ - "sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904", - "sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873", - "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d", - "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710", - "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", - "sha256:124d5ba71fee9c9902c4a7baa9425e663f7f0aecf73d31d54fe3dd357d62c1a7", - "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce", - "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", - "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077", - "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b", - "sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f", - "sha256:215c595c792a87d4407cb72dd5e0f6ee8e694ceeb7f9102b533c5a9bf2a916bb", - "sha256:22724d80ee5a815a44fc76274bb7ba2e7464f5564aacb6ecddaa9970a83e3225", - "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae", - "sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7", - "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451", - "sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55", - "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804", - "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca", - "sha256:3d721fee37380a44f9d9ce6c701b3960239f4fb3d5ceea7f31cbd43882edaa2f", - "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", - "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667", - "sha256:56afaf1e9b02302ba636151cfc49929c1bb66b98794291afd0e5f20fecaf757c", - "sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467", - "sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a", - "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", - "sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb", - "sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b", - "sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204", - "sha256:73b92a5b69f31b1a58c0c7e31080aeaec49c6e01b9522e71ff38d08f15aa56de", - "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167", - "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1", - "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", - "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f", - "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d", - "sha256:8ab962931015f170b97a3dd7bd933399c1bae8ed8ad0fb2a7151a5654b6941c7", - "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a", - "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b", - "sha256:8e531abd745f51f8035e207e75e049553a86823d189a51809c078412cefb399a", - "sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc", - "sha256:913f629adef31d2d350d41c051ce7e33cf0fd06a5d1cb28d49b1899b23b903aa", - "sha256:976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770", - "sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120", - "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2", - "sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f", - "sha256:9d8787bdfbb65a85ea76d0e96a3b1bed7bf0fbcb16d40408dc1172ad784a49d2", - "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc", - "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", - "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872", - "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", - "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", - "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", - "sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6", - "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", - "sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91", - "sha256:bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc", - "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424", - "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e", - "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23", - "sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1", - "sha256:c5189a5dab8b0312eadaf9d58d3049b6a52c454256493a557405e77a3d67ab7f", - "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", - "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4", - "sha256:d2489b241c19582b3f1430cc5d732caefc1aaf378d97e7fb95b9e56bed11725f", - "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229", - "sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d", - "sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf", - "sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4", - "sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038", - "sha256:e0a23b41f8f98b4e61150a03f83e4f0d566880fe53519d445a962929a4d21045", - "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", - "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064", - "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc", - "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049", - "sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b", - "sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824", - "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c", - "sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6", - "sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2", - "sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e", - "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1", - "sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569", - "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c" + "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9", + "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c", + "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", + "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", + "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", + "sha256:0b2eba969ea4203c177c7b38b36c69519e6067ee68c34dc37081fac74c796e10", + "sha256:0baa0ea43cfa5b008a28d3c07705cf3ada40e5d347f0f44994a64b1b7b4b5350", + "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", + "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241", + "sha256:1e3704d35e47d5bee811fb1cbd8599f0b4009b14d451c4c57be5a7e25eb89a13", + "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", + "sha256:23ef7abc7fca96632d8174ac115e668c1e931b8fe4dde586e92a500bf1914dcc", + "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", + "sha256:2c82e4f0b1c712477317434761fbc28b044c838b6b1240d895607441412371ac", + "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", + "sha256:3740bffd9816fc0326ddc406098a3a8f387e42223f5f455f2a02a9f834ead80c", + "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", + "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", + "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905", + "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907", + "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a", + "sha256:3e0a700c4b82144b72946b6629968df9762552ee1344bfdb767fecdd634fbd5a", + "sha256:405261b0a8c62bcbd8e2931c26fdc08714faf7025f45531541e2b29e544b545b", + "sha256:41bf25fb39a34cf8edb4398818523277ee7096689db352036a9e8437f2f3ee6b", + "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", + "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44", + "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9", + "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d", + "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", + "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63", + "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39", + "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938", + "sha256:622463ab81d19ef3e06868b576551587de8e4d518892d1afab71e0fbc1f9cffc", + "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b", + "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", + "sha256:65fd2f5730b1bf7f350c6dc896173d3460d235c4be007af73986d7cd9a2acd23", + "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", + "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045", + "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", + "sha256:6e18a5c15e764e5f3fc569b47872450b4bcea24f2a6354c0a0e95ad21045d5a9", + "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", + "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", + "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", + "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415", + "sha256:80fd082f5dcc0e94657c144f1b2a3a6479c44ad50be216cf0c244e567f5eae19", + "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549", + "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c", + "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040", + "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", + "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", + "sha256:94f206766bf1ea30e1382e4890f763bd1eefddc580e08fec1ccdc20ddd95c827", + "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be", + "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", + "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210", + "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", + "sha256:9fdc3ae730541086158d549c97852e2eea6820665d4faf0f41bf99df41bc11ea", + "sha256:a69ab657a4e6733133a3dca82768f2f8b884043714e8d2b9ba9f52b6efef5c44", + "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", + "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", + "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2", + "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7", + "sha256:af02ff34059ee9199a3546f123a6ab4c86caf1708c79042caf0820dc290a6d4f", + "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", + "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", + "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa", + "sha256:bba5118143373a86f91dadb8df41d9457498226698ebdf8e11cbb54d5b0e802d", + "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", + "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140", + "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708", + "sha256:caa447f2b5356779d914658519c874cf3b7629e99e63391ed519c28c8aea4919", + "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e", + "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50", + "sha256:d58c166a18f44cc9e2bad03a327dc2d1a3d2e85b847133cfbafd6bfc6719bd79", + "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", + "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", + "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175", + "sha256:e10b4d65901da88845516ce9f7f9736f9638d19a1d483b3883dc0182e6e5edba", + "sha256:e2985ce8b8c42d00492d0ed79f2bd2b6460d00f2fa671dfde4bf2e02f49bf5c6", + "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a", + "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", + "sha256:e3aa2118a3ece0d25489cbe48498de8a5d580e42e8d9979f65bf47900a15aba1", + "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853", + "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", + "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", + "sha256:fa9627eba4e82f99ca6d29bc967f09aba446ee2b5a1ea728949ede73d313f5d3", + "sha256:fb1c37c71cad991ef4d89c7a634b5ffb4447dbd7ae3ae13e8f5ee7f1775e7ab1", + "sha256:fb6a03a678085f64b97f9d4a9ae69376ce91a3a9e9b56a82b1580d8e1d501aff" ], "markers": "python_version >= '3.9'", - "version": "==3.11.3" + "version": "==3.11.4" }, "packaging": { "hashes": [ @@ -960,243 +954,228 @@ }, "pillow": { "hashes": [ - "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", - "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", - "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", - "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", - "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", - "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", - "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", - "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", - "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", - "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", - "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", - "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", - "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", - "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", - "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", - "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", - "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", - "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", - "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", - "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", - "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", - "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", - "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", - "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", - "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", - "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", - "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", - "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", - "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", - "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", - "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", - "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", - "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", - "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", - "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", - "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", - "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", - "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", - "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", - "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", - "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", - "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", - "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", - "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", - "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", - "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", - "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", - "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", - "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", - "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", - "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", - "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", - "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", - "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", - "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", - "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", - "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", - "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", - "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", - "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", - "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", - "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", - "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", - "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", - "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", - "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", - "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", - "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", - "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", - "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", - "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", - "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", - "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", - "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", - "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", - "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", - "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", - "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", - "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", - "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", - "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", - "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", - "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", - "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", - "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", - "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", - "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", - "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", - "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", - "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", - "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", - "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", - "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", - "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", - "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", - "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", - "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", - "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", - "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", - "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", - "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", - "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", - "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", - "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", - "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", - "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3" + "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", + "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", + "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", + "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", + "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", + "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", + "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1", + "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", + "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", + "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", + "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", + "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", + "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", + "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", + "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", + "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10", + "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", + "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", + "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b", + "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", + "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", + "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d", + "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", + "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", + "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", + "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", + "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", + "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", + "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", + "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", + "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", + "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7", + "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", + "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", + "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", + "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", + "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", + "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", + "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", + "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", + "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", + "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", + "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", + "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", + "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", + "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", + "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", + "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", + "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", + "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", + "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", + "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", + "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", + "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", + "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", + "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", + "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", + "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", + "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", + "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", + "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", + "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", + "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", + "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", + "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275", + "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", + "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", + "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", + "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", + "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", + "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", + "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca", + "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa", + "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", + "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", + "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", + "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", + "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", + "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", + "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363", + "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", + "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e", + "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782", + "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", + "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", + "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", + "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", + "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", + "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", + "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", + "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1" ], - "markers": "python_version >= '3.9'", - "version": "==11.3.0" + "markers": "python_version >= '3.10'", + "version": "==12.0.0" }, "propcache": { "hashes": [ - "sha256:009093c9b5dbae114a5958e6a649f8a5d94dd6866b0f82b60395eb92c58002d4", - "sha256:015b2ca2f98ea9e08ac06eecc409d5d988f78c5fd5821b2ad42bc9afcd6b1557", - "sha256:01c0ebc172ca28e9d62876832befbf7f36080eee6ed9c9e00243de2a8089ad57", - "sha256:02e071548b6a376e173b0102c3f55dc16e7d055b5307d487e844c320e38cacf2", - "sha256:0363a696a9f24b37a04ed5e34c2e07ccbe92798c998d37729551120a1bb744c4", - "sha256:0596d2ae99d74ca436553eb9ce11fe4163dc742fcf8724ebe07d7cb0db679bb1", - "sha256:075ca32384294434344760fdcb95f7833e1d7cf7c4e55f0e726358140179da35", - "sha256:077a32977399dc05299b16e793210341a0b511eb0a86d1796873e83ce47334cc", - "sha256:082a643479f49a6778dcd68a80262fc324b14fd8e9b1a5380331fe41adde1738", - "sha256:087e2d3d7613e1b59b2ffca0daabd500c1a032d189c65625ee05ea114afcad0b", - "sha256:0964c55c95625193defeb4fd85f8f28a9a754ed012cab71127d10e3dc66b1373", - "sha256:0b04ac2120c161416c866d0b6a4259e47e92231ff166b518cc0efb95777367c3", - "sha256:0b12df77eb19266efd146627a65b8ad414f9d15672d253699a50c8205661a820", - "sha256:0cd30341142c68377cf3c4e2d9f0581e6e528694b2d57c62c786be441053d2fc", - "sha256:0ea11fceb31fa95b0fa2007037f19e922e2caceb7dc6c6cac4cb56e2d291f1a2", - "sha256:184c779363740d6664982ad05699f378f7694220e2041996f12b7c2a4acdcad0", - "sha256:1927b78dd75fc31a7fdc76cc7039e39f3170cb1d0d9a271e60f0566ecb25211a", - "sha256:1cdabd60e109506462e6a7b37008e57979e737dc6e7dfbe1437adcfe354d1a0a", - "sha256:1e7fa29c71ffa8d6a37324258737d09475f84715a6e8c350f67f0bc8e5e44993", - "sha256:1e7fd82d4a5b7583588f103b0771e43948532f1292105f13ee6f3b300933c4ca", - "sha256:2015218812ee8f13bbaebc9f52b1e424cc130b68d4857bef018e65e3834e1c4d", - "sha256:213eb0d3bc695a70cffffe11a1c2e1c2698d89ffd8dba35a49bc44a035d45c93", - "sha256:2166466a666a5bebc332cd209cad77d996fad925ca7e8a2a6310ba9e851ae641", - "sha256:227892597953611fce2601d49f1d1f39786a6aebc2f253c2de775407f725a3f6", - "sha256:22f589652ee38de96aa58dd219335604e09666092bc250c1d9c26a55bcef9932", - "sha256:236c8da353ea7c22a8e963ab78cddb1126f700ae9538e2c4c6ef471e5545494b", - "sha256:24403152e41abf09488d3ae9c0c3bf7ff93e2fb12b435390718f21810353db28", - "sha256:26692850120241a99bb4a4eec675cd7b4fdc431144f0d15ef69f7f8599f6165f", - "sha256:2a4bf309d057327f1f227a22ac6baf34a66f9af75e08c613e47c4d775b06d6c7", - "sha256:2af6de831a26f42a3f94592964becd8d7f238551786d7525807f02e53defbd13", - "sha256:2c46d37955820dd883cf9156ceb7825b8903e910bdd869902e20a5ac4ecd2c8b", - "sha256:33ad7d37b9a386f97582f5d042cc7b8d4b3591bb384cf50866b749a17e4dba90", - "sha256:34000e31795bdcda9826e0e70e783847a42e3dcd0d6416c5d3cb717905ebaec0", - "sha256:381c84a445efb8c9168f1393a5a7c566de22edc42bfe207a142fff919b37f5d9", - "sha256:399c73201d88c856a994916200d7cba41d7687096f8eb5139eb68f02785dc3f7", - "sha256:39f0f6a3b56e82dc91d84c763b783c5c33720a33c70ee48a1c13ba800ac1fa69", - "sha256:4596c12aa7e3bb2abf158ea8f79eb0fb4851606695d04ab846b2bb386f5690a1", - "sha256:4a52c25a51d5894ba60c567b0dbcf73de2f3cd642cf5343679e07ca3a768b085", - "sha256:4bf95be277fbb51513895c2cecc81ab12a421cdbd8837f159828a919a0167f96", - "sha256:4c2735d3305e6cecab6e53546909edf407ad3da5b9eeaf483f4cf80142bb21be", - "sha256:4c491462e1dc80f9deb93f428aad8d83bb286de212837f58eb48e75606e7726c", - "sha256:515b610a364c8cdd2b72c734cc97dece85c416892ea8d5c305624ac8734e81db", - "sha256:545987971b2aded25ba4698135ea0ae128836e7deb6e18c29a581076aaef44aa", - "sha256:55a54de5266bc44aa274915cdf388584fa052db8748a869e5500ab5993bac3f4", - "sha256:566552ed9b003030745e5bc7b402b83cf3cecae1bade95262d78543741786db5", - "sha256:5710b1c01472542bb024366803812ca13e8774d21381bcfc1f7ae738eeb38acc", - "sha256:5a531d29d7b873b12730972237c48b1a4e5980b98cf21b3f09fa4710abd3a8c3", - "sha256:5b113feeda47f908562d9a6d0e05798ad2f83d4473c0777dafa2bc7756473218", - "sha256:5e0a5bc019014531308fb67d86066d235daa7551baf2e00e1ea7b00531f6ea85", - "sha256:626ec13592928b677f48ff5861040b604b635e93d8e2162fb638397ea83d07e8", - "sha256:659a0ea6d9017558ed7af00fb4028186f64d0ba9adfc70a4d2c85fcd3d026321", - "sha256:65ff56a31f25925ef030b494fe63289bf07ef0febe6da181b8219146c590e185", - "sha256:681a168d06284602d56e97f09978057aa88bcc4177352b875b3d781df4efd4cb", - "sha256:6a6a36b94c09711d6397d79006ca47901539fbc602c853d794c39abd6a326549", - "sha256:6d1f67dad8cc36e8abc2207a77f3f952ac80be7404177830a7af4635a34cbc16", - "sha256:6ebc6e2e65c31356310ddb6519420eaa6bb8c30fbd809d0919129c89dcd70f4c", - "sha256:71a400b2f0b079438cc24f9a27f02eff24d8ef78f2943f949abc518b844ade3d", - "sha256:71c45f02ffbb8a21040ae816ceff7f6cd749ffac29fc0f9daa42dc1a9652d577", - "sha256:728d98179e92d77096937fdfecd2c555a3d613abe56c9909165c24196a3b5012", - "sha256:72b51340047ac43b3cf388eebd362d052632260c9f73a50882edbb66e589fd44", - "sha256:779aaae64089e2f4992e993faea801925395d26bb5de4a47df7ef7f942c14f80", - "sha256:783e91595cf9b66c2deda17f2e8748ae8591aa9f7c65dcab038872bfe83c5bb1", - "sha256:790286d3d542c0ef9f6d0280d1049378e5e776dcba780d169298f664c39394db", - "sha256:7aa8cc5c94e682dce91cb4d12d7b81c01641f4ef5b3b3dc53325d43f0e3b9f2e", - "sha256:7d51f70f77950f8efafed4383865d3533eeee52d8a0dd1c35b65f24de41de4e0", - "sha256:7da5c4c72ae40fd3ce87213ab057db66df53e55600d0b9e72e2b7f5a470a2cc4", - "sha256:7dfa60953169d2531dd8ae306e9c27c5d4e5efe7a2ba77049e8afdaece062937", - "sha256:7ea86eb32e74f9902df57e8608e8ac66f1e1e1d24d1ed2ddeb849888413b924d", - "sha256:7f088e21d15b3abdb9047e4b7b7a0acd79bf166893ac2b34a72ab1062feb219e", - "sha256:83ae2f5343f6f06f4c91ae530d95f56b415f768f9c401a5ee2a10459cf74370b", - "sha256:84f847e64f4d1a232e50460eebc1196642ee9b4c983612f41cd2d44fd2fe7c71", - "sha256:858eaabd2191dd0da5272993ad08a748b5d3ae1aefabea8aee619b45c2af4a64", - "sha256:8659f995b19185179474b18de8755689e1f71e1334d05c14e1895caa4e409cf7", - "sha256:88d50d662c917ec2c9d3858920aa7b9d5bfb74ab9c51424b775ccbe683cb1b4e", - "sha256:892a072e5b19c3f324a4f8543c9f7e8fc2b0aa08579e46f69bdf0cfc1b440454", - "sha256:8d18d796ffecdc8253742fd53a94ceee2e77ad149eb9ed5960c2856b5f692f71", - "sha256:92bc43a1ab852310721ce856f40a3a352254aa6f5e26f0fad870b31be45bba2e", - "sha256:944de70384c62d16d4a00c686b422aa75efbc67c4addaebefbb56475d1c16034", - "sha256:94a278c45e6463031b5a8278e40a07edf2bcc3b5379510e22b6c1a6e6498c194", - "sha256:94b0f7407d18001dbdcbb239512e753b1b36725a6e08a4983be1c948f5435f79", - "sha256:96153e037ae065bb71cae889f23c933190d81ae183f3696a030b47352fd8655d", - "sha256:9ba68c57cde9c667f6b65b98bc342dfa7240b1272ffb2c24b32172ee61b6d281", - "sha256:a1d5e474d43c238035b74ecf997f655afa67f979bae591ac838bb3fbe3076392", - "sha256:a4efbaf10793fd574c76a5732c75452f19d93df6e0f758c67dd60552ebd8614b", - "sha256:a60634a9de41f363923c6adfb83105d39e49f7a3058511563ed3de6748661af6", - "sha256:a7f06f077fc4ef37e8a37ca6bbb491b29e29db9fb28e29cf3896aad10dbd4137", - "sha256:a8ef2ea819549ae2e8698d2ec229ae948d7272feea1cb2878289f767b6c585a4", - "sha256:a9725d96a81e17e48a0fe82d0c3de2f5e623d7163fec70a6c7df90753edd1bec", - "sha256:ab9c1bd95ebd1689f0e24f2946c495808777e9e8df7bb3c1dfe3e9eb7f47fe0d", - "sha256:abe04e7aa5ab2e4056fcf3255ebee2071e4a427681f76d4729519e292c46ecc1", - "sha256:ae3adf88a66f5863cf79394bc359da523bb27a2ed6ba9898525a6a02b723bfc5", - "sha256:b2f29697d1110e8cdf7a39cc630498df0082d7898b79b731c1c863f77c6e8cfc", - "sha256:b730048ae8b875e2c0af1a09ca31b303fc7b5ed27652beec03fa22b29545aec9", - "sha256:bcb5bfac5b9635e6fc520c8af6efc7a0a56f12a1fe9e9d3eb4328537e316dd6a", - "sha256:bd6c6dba1a3b8949e08c4280071c86e38cb602f02e0ed6659234108c7a7cd710", - "sha256:c0e1c218fff95a66ad9f2f83ad41a67cf4d0a3f527efe820f57bde5fda616de4", - "sha256:c1443fa4bb306461a3a8a52b7de0932a2515b100ecb0ebc630cc3f87d451e0a9", - "sha256:c1ad731253eb738f9cadd9fa1844e019576c70bca6a534252e97cf33a57da529", - "sha256:c20d796210720455086ef3f85adc413d1e41d374742f9b439354f122bbc3b528", - "sha256:c2e274f3d1cbb2ddcc7a55ce3739af0f8510edc68a7f37981b2258fa1eedc833", - "sha256:c3f4b125285d354a627eb37f3ea7c13b8842c7c0d47783581d0df0e272dbf5f0", - "sha256:c9b8119244d122241a9c4566bce49bb20408a6827044155856735cf14189a7da", - "sha256:cd6e22255ed73efeaaeb1765505a66a48a9ec9ebc919fce5ad490fe5e33b1555", - "sha256:cd8684f628fe285ea5c86f88e1c30716239dc9d6ac55e7851a4b7f555b628da3", - "sha256:cdb0cecafb528ab15ed89cdfed183074d15912d046d3e304955513b50a34b907", - "sha256:d74aa60b1ec076d4d5dcde27c9a535fc0ebb12613f599681c438ca3daa68acac", - "sha256:d7f008799682e8826ce98f25e8bc43532d2cd26c187a1462499fa8d123ae054f", - "sha256:d9a8d277dc218ddf04ec243a53ac309b1afcebe297c0526a8f82320139b56289", - "sha256:da47070e1340a1639aca6b1c18fe1f1f3d8d64d3a1f9ddc67b94475f44cd40f3", - "sha256:da584d917a1a17f690fc726617fd2c3f3006ea959dae5bb07a5630f7b16f9f5f", - "sha256:de536cf796abc5b58d11c0ad56580215d231d9554ea4bb6b8b1b3bed80aa3234", - "sha256:de8e310d24b5a61de08812dd70d5234da1458d41b059038ee7895a9e4c8cae79", - "sha256:df7107a91126a495880576610ae989f19106e1900dd5218d08498391fa43b31d", - "sha256:e0ce7f3d1faf7ad58652ed758cc9753049af5308b38f89948aa71793282419c5", - "sha256:e2d01fd53e89cb3d71d20b8c225a8c70d84660f2d223afc7ed7851a4086afe6d", - "sha256:e5227da556b2939da6125cda1d5eecf9e412e58bc97b41e2f192605c3ccbb7c2", - "sha256:e6229ad15366cd8b6d6b4185c55dd48debf9ca546f91416ba2e5921ad6e210a6", - "sha256:e878553543ece1f8006d0ba4d096b40290580db173bfb18e16158045b9371335", - "sha256:eb77a85253174bf73e52c968b689d64be62d71e8ac33cabef4ca77b03fb4ef92", - "sha256:f114a3e1f8034e2957d34043b7a317a8a05d97dfe8fddb36d9a2252c0117dbbc", - "sha256:f495007ada16a4e16312b502636fafff42a9003adf1d4fb7541e0a0870bc056f", - "sha256:f5c82af8e329c3cdc3e717dd3c7b2ff1a218b6de611f6ce76ee34967570a9de9" + "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", + "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", + "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", + "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", + "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", + "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", + "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", + "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", + "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", + "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", + "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", + "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", + "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", + "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", + "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", + "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", + "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", + "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", + "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", + "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", + "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", + "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", + "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", + "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", + "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", + "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", + "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", + "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", + "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", + "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", + "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", + "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", + "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", + "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", + "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", + "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", + "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", + "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", + "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", + "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", + "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", + "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", + "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", + "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", + "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", + "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", + "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", + "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", + "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", + "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", + "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", + "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", + "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", + "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", + "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", + "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", + "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", + "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", + "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", + "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", + "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", + "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", + "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", + "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", + "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", + "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", + "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", + "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", + "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", + "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", + "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", + "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", + "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", + "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", + "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", + "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", + "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", + "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", + "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", + "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", + "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", + "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", + "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", + "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", + "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", + "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", + "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", + "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", + "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", + "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", + "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", + "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", + "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", + "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", + "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", + "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", + "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", + "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", + "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", + "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", + "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", + "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", + "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", + "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", + "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", + "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", + "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", + "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", + "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", + "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", + "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", + "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", + "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", + "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", + "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", + "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", + "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", + "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", + "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", + "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", + "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", + "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781" ], "markers": "python_version >= '3.9'", - "version": "==0.4.0" + "version": "==0.4.1" }, "pycares": { "hashes": [ @@ -1309,80 +1288,80 @@ "srv" ], "hashes": [ - "sha256:024e735127e7f39763eb3043c628a857cbf6cdfabd9bf5a1825f3597bab74352", - "sha256:03bd7284fd299c7df09cebeb53f3251e0b4850fba71bf63cbbff1ddf20cc569d", - "sha256:05b9eab3bc049f8fd150869375eff3a85ceab606531b6226b60f054daf7d1368", - "sha256:05e96219d1acca15643042478103241771e46a3f5331bae3e19d2ea2756968b1", - "sha256:0ad7ec297e8999b0f984c998e2c1c605f11e9b8c7682256bbbc53c24a195f76e", - "sha256:1393e7e9d2fc291e0d72f8583ef58e1999e84f7cd01d5a72db53b4ed1ae44686", - "sha256:18bc73e47d21cabfde719d0cc5aa6b556856993397c9433d934089c86732e3d3", - "sha256:1b5c61e32c6afbf35a9c6638db892f75dc53ebcd45a9a1bf992ffff0ec28aaaa", - "sha256:1db14e952ceb574cb8acacf063040e2a6e9570bd50671fa903fb47adb7cf49cc", - "sha256:1f2af4b98fc6d54489d187c0faa12bfbf0ef6c56c3e735eeb837ac8ff235b490", - "sha256:20ee2722ac45fba2c502edbc5281b6efcd8601d94ae1900a48c106459a1715d7", - "sha256:21b1d1d33bdbc87c1a082b747aa9ab40a30638c4e58e799d8fe9f5cb15feb38f", - "sha256:22ad78ac0222b8c5f5a28cdf6300cf19481fff193110506768c9915c8cd3396b", - "sha256:29645a9a8166f20b3fc6aa05095af6caf8ee9af9a4cf23cd857576084e29cc9c", - "sha256:2968cf01e2257f2f5193aba259116c1e9e56f739a16eceef36e85a55edc91604", - "sha256:2b6c8588b04e304bb4670e5409b3def2d9daedb8f719d47780a59de7227f1d3f", - "sha256:2c816a9e9d4aaaa0e4e9fb2534b72957666d262f3ce874a0408f8b925cfd4d99", - "sha256:2daa9434828a5e5638b9d78f0031c9e19b5bc84ce9f5e69cf6083f58aa3e3901", - "sha256:2fad596a092ab9cd821c98d75b48dd6a9c3fc52df8b1453d2f10d8219676269a", - "sha256:338b29d89f92c665a1038d53c7cc68869e2a04e171dd4fb2d416d7ad263dc50a", - "sha256:38785ba507a019edb742e333c6bf2fa3644043f1ce79ef4d20a4f7bb2180ee74", - "sha256:3ab5ba56b868c56a38cfeb3202ee78dcdd4152bc364d24b71aaf1ee3994c7f96", - "sha256:3dc0ec9d78d4f28a24bd965e06c0a77459086522005aa199a8e4fc652ed1ce8e", - "sha256:3fafe5ef96943ab9b837f89b6abe779951102bee44d21c743259d43cfc1d9f6e", - "sha256:42acd45f7030743eed3d5e66a03dd3e9c12c7869301d123bffa1f71dc0e3f882", - "sha256:45103766c3f1bf1f5fc2da43a48dbe03a343389a334eb1d02ef39024957cdc91", - "sha256:475a97d48be850140f2455ed4a2a9920f70538b0824c53ffa5deeb940f49fdb3", - "sha256:50307e2403f0cfdf4fd0f5c6c9a45edbb4c5fa63196e1671b7fed5bbcd884109", - "sha256:50325282876a263ece78371319e78518dd034e434c11e3ab12402547292b8fd5", - "sha256:557611de3fa33bd5b8e5d38a7056b15d5c38361af50378ff5cf8b9cbf371913b", - "sha256:587202db4a64d5c091bc39695095af461a6a08b2a52ddd881a6e5cb34244d672", - "sha256:597dce90bc607b3180735a6692abcf75c111d7f6169b2b1cca1db85086ee980c", - "sha256:5b72de1a2f8cc52561c65a5062ade72e06da76e0899d4f11ae1c1bced1534ff8", - "sha256:5fa558bc6320e1183965db06e069973c9642b971a37729a8ae23c37f1c13ce21", - "sha256:6462763611c5fb97b56fcd3a62c325bc8111c686c7768f50668882d21b4f28f4", - "sha256:68fbc920409bd96e6a63b651254baa45e27473c4b73232a3fb5662279383a622", - "sha256:69515e1042a2fb4fadf6384918be34703aa2c792b9a8d3d406ad43e07cb095a2", - "sha256:6d797730d07bff953d05693e75d9575e7357739a5eb2747521987d2f99b9899f", - "sha256:7089a1f2d5883f5137f1c2766691db904741985bb7e7a400ed50c3b370507b17", - "sha256:7466840413fbd23605e9f95b702374b061525e85ea7f47dca6a88981455490ec", - "sha256:75f6f8363a57ba8a6bb1076114dc9aa29336f525c8b621cc1e4cfccae4ff546a", - "sha256:76c1c013bc577c7fb2c9a69d52ee335672eac1bdbfb9c37a432bb155bc69ffdc", - "sha256:780447f9112f0e57d821ced8d593657b45616f3821becb0740e6c0fc38b0e91e", - "sha256:82ba58edb6f6112aac543214ac22fc8e3d569372a7b3a180511cf4a70bd4c0ef", - "sha256:9fa833908d94b5869e6c9a53b778dc8235caca6fcda03aac8410b8f067cd8a6f", - "sha256:a15ad3f11556a30e5dd86344567e85eb46550b09e0ea8d3297476788f0c76d77", - "sha256:a244e27c034707f48f979fdcebe0df47ea000fd52ee1b2b2d2d2cb5b7b0e24dd", - "sha256:a33e118e14bd350bef6127a000c8d08e6bade8b9045bcd70d09a665434035705", - "sha256:a42ad84dfab44218f264e2d68b79e0e684c03c66fe8180a7961d6eb670eec4a3", - "sha256:a4a0d3cf68f9bf84a7ee737ba0b29265cfeeaf586a856c6d7773491c545e5230", - "sha256:a775371086ff63da1ae97f676bcb5556c86e4e281ccac998d49d6e24efa50ca1", - "sha256:a98f67df7aae325c0476aa453877475f9a1160f84b7e6e24e4804498ef99178e", - "sha256:a994b40542ba44748af9e382fd54e69428f40c1728ae06bc649c87a1135d1cfb", - "sha256:b5fe426128a03393d2e7f10169e1f10cf6a6355f40876f52b51a03721c12e6e5", - "sha256:bf646006bfce5e153cc838adaee319ff8a3d625978d491208cc290e89f9c2a21", - "sha256:c434219d66bf20f46011cc11b28e0dbfeb965f7a7cfd1e4b8e733a5f642ae1c2", - "sha256:c95a8d0ca11d16e325749fbd9f7d9aeb9a90241245e419007a941f446ff94dd6", - "sha256:cedfd1be19c8f7b41a1f5fbaea299303087b5d40605e956ffbcfe2adc76de0ec", - "sha256:d54b8139979e6e2ee6fec91b189e948ee2d83f125957793cf191c5e33be567e7", - "sha256:d9410537204bb9e83f1c5e43f6e5df2c0d3fe092dbd8d30bd883736818a6d786", - "sha256:ddf9face1dadf4cce4578dda29547ca2af6df09d44e7dd1bd6fe185f7c18dfc9", - "sha256:de0c88d7229a96a5bfe2827170578bcd871ee16843c47e5cb3290edf1aaf62ca", - "sha256:dece75a28450fa813040b13f7fbe80a614d02e04f7ff84255a2600c440bf227a", - "sha256:e6a22f0349142c92bb2ccbd35a8a0b7dc5a2eeac14217fb28cfa9956bcfee139", - "sha256:eb1423432631994d965e92ee63e448627d57793fd780c56c49570f12d4be1ff4", - "sha256:ebd6f6eed7c6c92af3d3fa375ecd0e730da628c59a95aca5b2445fbb3d1eb874", - "sha256:ee0ab602d309c6f903c002f8705b92459d885349f1f9561040f34a2a06c84891", - "sha256:f03716cfd4c86e3a8537ab8e1169cec26d532cc70fcd02e30027820ac587d28b", - "sha256:f5bfbde2ccc88b96d56cdc6bd104b856d9b039aa209b491311f8012e611d9f58", - "sha256:f736f1a6d85f3b1c182018ae0e6c387bb342935e3b97637c258b9b46e0509af2", - "sha256:f9d3bbb4741a6ab81dbcd73b0754725f304b118c4c738449639fd060ee8b5da9" + "sha256:07bcc36d11252f24fe671e7e64044d39a13d997b0502c6401161f28cc144f584", + "sha256:09440e78dff397b2f34a624f445ac8eb44c9756a2688b85b3bf344d351d198e1", + "sha256:1246a82fa6dd73ac2c63aa7e463752d5d1ca91e0c7a23396b78f21273befd3a7", + "sha256:17d13458baf4a6a9f2e787d95adf8ec50d412accb9926a044bd1c41029c323b2", + "sha256:17fc94d1e067556b122eeb09e25c003268e8c0ea1f2f78e745b33bb59a1209c4", + "sha256:1f681722c9f27e86c49c2e8a838e61b6ecf2285945fd1798bd01458134257834", + "sha256:21c0a95a4db72562fd0805e2f76496bf432ba2e27a5651f4b9c670466260c258", + "sha256:292fd5a3f045751a823a54cdea75809b2216a62cc5f74a1a96b337db613d46a8", + "sha256:2c96dde79bdccd167b930a709875b0cd4321ac32641a490aebfa10bdcd0aa99b", + "sha256:2f3d66f7c495efc3cfffa611b36075efe86da1860a7df75522a6fe499ee10383", + "sha256:2fd3b99520f2bb013960ac29dece1b43f2f1b6d94351ca33ba1b1211ecf79a09", + "sha256:300eaf83ad053e51966be1839324341b08eaf880d3dc63ada7942d5912e09c49", + "sha256:3561fa96c3123275ec5ccf919e595547e100c412ec0894e954aa0da93ecfdb9e", + "sha256:390c4954c774eda280898e73aea36482bf20cba3ecb958dbb86d6a68b9ecdd68", + "sha256:39a13d8f7141294404ce46dfbabb2f2d17e9b1192456651ae831fa351f86fbeb", + "sha256:446417a34ff6c2411ce3809e17ce9a67269c9f1cb4966b01e49e0c590cc3c6b3", + "sha256:45aebbd369ca79b7c46eaea5b04d2e4afca4eda117b68965a07a9da05d774e4d", + "sha256:47ffb068e16ae5e43580d5c4e3b9437f05414ea80c32a1e5cac44a835859c259", + "sha256:482ca9b775747562ce1589df10c97a0e62a604ce5addf933e5819dd967c5e23c", + "sha256:49fd6e158cf75771b2685a8a221a40ab96010ae34dd116abd06371dc6c38ab60", + "sha256:4a0a054e9937ec8fdb465835509b176f6b032851c8648f6a5d1b19932d0eacd6", + "sha256:52f40c4b8c00bc53d4e357fe0de13d031c4cddb5d201e1a027db437e8d2887f8", + "sha256:58d0f4123855f05c0649f9b8ee083acc5b26e7f4afde137cd7b8dc03e9107ff3", + "sha256:5bf879a6ed70264574d4d8fb5a467c2a64dc76ecd72c0cb467c4464f849c8c77", + "sha256:5c78237e878e0296130e398151b0d4aa6c9eaf82e38fb6e0aaae2029bc7ef0ce", + "sha256:5c85a4c72b7965033f95c94c42dac27d886c01dbc23fe337ccb14f052a0ccc29", + "sha256:5f6feb678f26171f2a6b2cbb340949889154c7067972bd4cc129b62161474f08", + "sha256:6a054d282dd922ac400b6f47ea3ef58d8b940968d76d855da831dc739b7a04de", + "sha256:71413cd8f091ae25b1fec3af7c2e531cf9bdb88ce4079470e64835f6a664282a", + "sha256:76a8d4de8dceb69f6e06736198ff6f7e1149515ef946f192ff2594d2cc98fc53", + "sha256:77353978be9fc9e5fe56369682efed0aac5f92a2a1570704d62b62a3c9e1a24f", + "sha256:7a981271347623b5319932796690c2d301668ac3a1965974ac9f5c3b8a22cea5", + "sha256:7c0fd3de3a12ff0a8113a3f64cedb01f87397ab8eaaffa88d7f18ca66cd39385", + "sha256:7dd2a49f088890ca08930bbf96121443b48e26b02b84ba0a3e1ae2bf2c5a9b48", + "sha256:82a490f1ade4ec6a72068e3676b04c126e3043e69b38ec474a87c6444cf79098", + "sha256:86b1b5b63f4355adffc329733733a9b71fdad88f37a9dc41e163aed2130f9abc", + "sha256:89e45d7fa987f4e246cdf43ff001e3f911f73eb19ba9dabc2a6d80df5c97883b", + "sha256:8bd6dd736f5d07a825caf52c38916d5452edc0fac7aee43ec67aba6f61c2dbb7", + "sha256:8d4b01a48369ea6d5bc83fea535f56279f806aa3e4991189f0477696dd736289", + "sha256:90ad56bd1d769d2f44af74f0fd0c276512361644a3c636350447994412cbc9a1", + "sha256:9483521c03f6017336f54445652ead3145154e8d3ea06418e52cea57fee43292", + "sha256:959ef69c5e687b6b749fbf2140c7062abdb4804df013ae0507caabf30cba6875", + "sha256:97f9babdb98c31676f97d468f7fe2dc49b8a66fb6900effddc4904c1450196c8", + "sha256:982107c667921e896292f4be09c057e2f1a40c645c9bfc724af5dd5fb8398094", + "sha256:9897a837677e3814873d0572f7e5d53c23ce18e274f3b5b87f05fb6eea22615b", + "sha256:9b03db2fe37c950aff94b29ded5c349b23729bccd90a0a5907bbf807d8c77298", + "sha256:9bc9f99e7702fdb0dcc3ff1dd490adc5d20b3941ad41e58f887d4998b9922a14", + "sha256:9df2db6bd91b07400879b6ec89827004c0c2b55fc606bb62db93cafb7677c340", + "sha256:a47a3218f7900f65bf0f36fcd1f2485af4945757360e7e143525db9d715d2010", + "sha256:b33d59bf6fa1ca1d7d96d4fccff51e41312358194190d53ef70a84c070f5287e", + "sha256:b3a0ec660d61efb91c16a5962ec937011fe3572c4338216831f102e53d294e5c", + "sha256:b63bac343b79bd209e830aac1f5d9d552ff415f23a924d3e51abbe3041265436", + "sha256:bd0497c564b0ae34fb816464ffc09986dd9ca29e2772a0f7af989e472fecc2ad", + "sha256:c4fdd8e6eab8ff77c1c8041792b5f760d48508623cd10b50d5639e73f1eec049", + "sha256:c57dad9f289d72af1d7c47a444c4d9fa401f951cedbbcc54c7dd0c2107d6d786", + "sha256:c7eb497519f42ac89c30919a51f80e68a070cfc2f3b0543cac74833cd45a6b9c", + "sha256:cfa4a0a0f024a0336640e1201994e780a17bda5e6a7c0b4d23841eb9152e868b", + "sha256:d09d895c7f08bcbed4d2e96a00e52e9e545ae5a37b32d2dc10099b205a21fc6d", + "sha256:d2d4ca446348d850ac4a5c3dc603485640ae2e7805dbb90765c3ba7d79129b37", + "sha256:d66da207ccb0d68c5792eaaac984a0d9c6c8ec609c6bcfa11193a35200dc5992", + "sha256:dc583a1130e2516440b93bb2ecb55cfdac6d5373615ae472a9d1f26801f58749", + "sha256:dcff15b9157c16bc796765d4d3d151df669322acfb0357e4c3ccd056153f0ff4", + "sha256:de3bc878c3be54ae41c2cabc9e9407549ed4fec41f4e279c04e840dddd7c630c", + "sha256:e7cde58ef6470c0da922b65e885fb1ffe04deef81e526bd5dea429290fa358ca", + "sha256:e84dec392cf5f72d365e0aac73f627b0a3170193ebb038c3f7e7df11b7983ee7", + "sha256:f6b0513e5765fdde39f36e6a29a36c67071122b5efa748940ae51075beb5e4bc", + "sha256:fae552767d8e5153ed498f1bca92d905d0d46311d831eefb0f06de38f7695c95", + "sha256:fb384623ece34db78d445dd578a52d28b74e8319f4d9535fbaff79d0eae82b3d", + "sha256:fe4bcb8acfb288e238190397d4a699aeb4adb70e8545a6f4e44f99d4e8096ab1", + "sha256:ff99864085d2c7f4bb672c7167680ceb7d273e9a93c1a8074c986a36dbb71cc6", + "sha256:ffe217d2502f3fba4e2b0dc015ce3b34f157b66dfe96835aa64432e909dd0d95" ], "markers": "python_version >= '3.9'", - "version": "==4.15.2" + "version": "==4.15.3" }, "python-dateutil": { "hashes": [ @@ -1427,6 +1406,14 @@ "markers": "python_version >= '3.8'", "version": "==1.4.0" }, + "typing-extensions": { + "hashes": [ + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" + ], + "markers": "python_version >= '3.9'", + "version": "==4.15.0" + }, "urllib3": { "hashes": [ "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", @@ -1437,46 +1424,58 @@ }, "uvloop": { "hashes": [ - "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", - "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", - "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc", - "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414", - "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", - "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", - "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd", - "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", - "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", - "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", - "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", - "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a", - "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", - "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2", - "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0", - "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", - "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", - "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", - "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", - "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", - "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75", - "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", - "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", - "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", - "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", - "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", - "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206", - "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", - "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", - "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b", - "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", - "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79", - "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", - "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe", - "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", - "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", - "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2" + "sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772", + "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", + "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743", + "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54", + "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", + "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659", + "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", + "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", + "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7", + "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", + "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", + "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", + "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", + "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", + "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", + "sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193", + "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", + "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", + "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", + "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", + "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", + "sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242", + "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", + "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", + "sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6", + "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", + "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", + "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", + "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", + "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", + "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", + "sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa", + "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", + "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", + "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", + "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", + "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4", + "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", + "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", + "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", + "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", + "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", + "sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820", + "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", + "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", + "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", + "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c", + "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", + "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42" ], "markers": "sys_platform != 'win32'", - "version": "==0.21.0" + "version": "==0.22.1" }, "webencodings": { "hashes": [ @@ -1487,131 +1486,139 @@ }, "yarl": { "hashes": [ - "sha256:019c2798df9d74fe8fb9cc916702966dad7e2e3eef66b4c19f8084ba5e0b6ecd", - "sha256:01ef0d7f1dd60d241529dc79a3fa647451056394f9a5ed05fbceeb5009de6122", - "sha256:028c59136b65fccfe5578520a3fb2a94e06601c545ca0125b7e07b3a39f238a5", - "sha256:0416fde6dc89866f4ff494a0ffcc4b2da984cf61aaa279c14a53495e8520c809", - "sha256:056fc431f10ae35aa2375c9de2b68176b34f54fb7de8bc2e830564e2a3d29efa", - "sha256:069cfc781f5d68389c8a4228f720cab453e1b6fa606bcd710a9cc01e38ffe2c1", - "sha256:06c71f698ac5b5bfdde1ce3d58d262235b3c1109f083286c96c01cee64ebf705", - "sha256:0a9454d4c513a3aa2fd87471126e0d32b01f1bf58d49309a84431521488b30c4", - "sha256:0a94664fe3c6dd44c36e875af0f338769dc9f80a1ccd58f53cf5f5b8341e8627", - "sha256:0aaa36261a1279b03fa0655a9bd879cc42e06406adaae0150fde25c778393fcb", - "sha256:0ab4e81b455dd8beb2537648be972eb63351bbe34fb55457054392fee759ac9c", - "sha256:0b16c889a7168ecf7242946dec013c9fb82ade70ab8e6b5d3290383390083a2b", - "sha256:0cc3eeea8f527119aac1b0c874bbb8092675da85fd6d9d91946cf7be7d59477b", - "sha256:0d37bf6f601c714b536159715d9ec6e69bf8a94dc593abe54c1b43ac339eb5e7", - "sha256:0e485c4f9f5b5b9fc10b4bb0ba5baf145ed0a702756da126c9f62f8a89b391a8", - "sha256:10580c7d9b50c883b93cc0ab5c91df5cc1e5b18713736471d622776b01c36810", - "sha256:1107b93c32cf7d7e2ece9bbb1b1820ecb923cfea24c8aa599a309434ed37d707", - "sha256:13c9b91e2e1224a8d33addc1bd58bb097396519c4c49524843947776b8dd45da", - "sha256:140402fef1f482840fcd4d2ee9bfd07f08bfb2c80dd215220bd47f6f3566b882", - "sha256:14872677213d96552268f927982d4c83f5d0674b0d54b623d8e909710460ab14", - "sha256:16957642c0594feba56a4bb430eea5f9132bb589ebb8b4740c0d47f022a5d976", - "sha256:1743d35529a8b9b2b6a9e5f00076c2c146726453051621b739b081dda382ee70", - "sha256:1754b3380ffef931b8eae3bbe6fc0b249db56294ffeb6e6124c2d031a82a3a92", - "sha256:178860382595f3b1fab2596b19570adc495c6211eee8b10a4112ce96342f6515", - "sha256:18e8272a4166d2bb68d51f86445f061aac21fcfc9c1d5b1187f9703f362d85dd", - "sha256:190356a39fed15109ab95600f8ff59c1a0665625f4cfe910388c82b965edaf87", - "sha256:19df967a905f2f9a09733dfb397baa6807772502931000f881943d7cfc6e9f47", - "sha256:1a0ba7cd4eabb7433e69737f33333d9e79d8ab6dbaa2f4d7313ad6611200cc65", - "sha256:1b5d29c1a86cc63e55f69253b8c817091884c4e1b79ee762a8643de834e70a64", - "sha256:1bff86850033508af0a7f9973ced23a16de7ba4ce30521080e2330475b8711b5", - "sha256:1e0b01fa225ec12e54c73be383326ae2a4a59a4a465a0e6cac679f314ed85d1f", - "sha256:20b2dca6588f65b5def8e8eae4a087d504eacf34b5b435c021cc233ce82f6c15", - "sha256:212a5c72d551f94b7799b5de1cc55ddcf3c69ac462f7c0df1beee7e47edb9fef", - "sha256:221aa7c16055e8b9f2eba718cbbf10f174e47f02e659156804d9679654c5cbb0", - "sha256:2227fcc88bebdc90ed87d924bdf8a76a730fc91796641e41ca747aabd13a5074", - "sha256:2584651c047718ec4a863ee81a5432f6f68974e6f0c58975f0aab408ff839798", - "sha256:26940710eece6b5b08a108e81d6325b47610990cd8bb28886e27d4a0d6d60930", - "sha256:2a05a5e018de23c4d2d75c8fbd8b58aba5199f752326f60a22aa37ef28d987bd", - "sha256:2b2f8e0bbdf49530ed09b2bc988082cab6ce24f4c49a0efd2ff5d9477cb29084", - "sha256:2b841c5529f9ca28cf23ed34c8040547ca6530b968a482c14de96a6ade470ee5", - "sha256:30b6a56388963ebe5428d835112439563bcaa9c8349742aeac68a8ad5a231221", - "sha256:37b5e7bba1f6df45058cff626c83a0e8a1259363095e768046a3da40b24e9c4f", - "sha256:3b539230fd64f283594a56633a9751d299cde5ab9c2791452ccb47a865842fa8", - "sha256:3cbae81bff4014ca7745fa11f7015f784198fadba8935cf5a71e139b0b124ff0", - "sha256:430e162d961af58f3dcac58aed038ba974ec7a73803ac6545db2338fbd0f4ed3", - "sha256:45aa7711e1933bac1679f9534f112767f1fe64c97a8576294b760015d0fb65e7", - "sha256:45f17adf1b8bc56becb1bc38f293b1714866786c9d79e245fb3d3731788622a6", - "sha256:4b449296e2ba009481385349138130f209bb502c4f890b3298bf3ea13d43a6d5", - "sha256:4d5af10c9f580002c0ea6c8f345c8cadb2e0c53dce77d3f2639b9e31e5f24d3d", - "sha256:4ee80f79c928ce7c18cf3ad18a5da7f3f0f1b08923e08d87143d628a6d5d2dba", - "sha256:4fcce63c1117ef0630a92a0bda3028a96dc17feed2c78c713de4c963d13d1881", - "sha256:5110ebfe3cbf892b41590fcf4aa70a17ac0a5e9a73b4a8945010bdb970ff1b93", - "sha256:52a8b7541c5d8240ae32d12014f8448e29e1ae794f9443ea020b926cff8691e1", - "sha256:56ead8d62b346c1ec67a6e8b2f66885180ea5bec05821d309ac1cb99ff4aacf5", - "sha256:5c0123db2d86d169554d5fb19421e8e455efcfe2e8e254328b85c77e712ab506", - "sha256:5e7d24e9c3b638f046fcd9a5374818257a8c6d1c3fc7542887521b81a970fbc2", - "sha256:6074904025bc462b0b3f7230b36d8942d9a611b783ce1431ade5ad6a8867b73d", - "sha256:60dcb45a3d762460ac5014755c190db36acf127f68d68643cde7d6d7ce0e5627", - "sha256:61bf6233d04ccba7906f5261ff3628fa97a68fc526cda3d9dd092d2f49926933", - "sha256:63157d66cf7682dec8b3117491cb87a5d8e1cd56df59156d5553ab9721895d19", - "sha256:6378871775e0feb225693cbdad3d997327af0ab4c7e39d93849008c73b867134", - "sha256:6614325ef69d8a53c731ed5e4bd55449ffc5fe86ad652789c0afc853099662ad", - "sha256:66248832212957d8bad28e8d9d307be1d987b94ffaf7e7cca658a349d52d3572", - "sha256:692603a8f82e7baa86bb3921d5002b711788cec547b626030f1f6cf017290ab7", - "sha256:701cd0ee20fe9087c21229db579f2222a75c229b44840a7df7b2d795522068c3", - "sha256:7331a7d2683e644b7830c924ac634fa3ec52257f5098f6415d8ad765d6bc29a8", - "sha256:74b2e94d3e410ed49c7a4cb2c3a5089a6632f7ab68e49bb612b972577e26e771", - "sha256:780313d2a1877adef0e3839ef9596ad53ab640715e7f453e7304c121cd7f262d", - "sha256:7a9d0efd6ff6f4f55ff7a37852e4fcdc24b1feb3b09e204df3dda990171fe725", - "sha256:7b7d46a6ca781a336c7317d9c1d381bebb3b0da5309c2293dd1c4fe3d62942bf", - "sha256:7d271fed8a4b46723db5001619c36192d94a3bd49d76ef186f13abb6897ff8e5", - "sha256:7d5d8eeb1051fac562d80aad7b6b496e2901f41fc2b0988c61016a1426996f66", - "sha256:7d8917677a64304db00ec46629aff335c935c788a10a164b29464b7e2d707463", - "sha256:7da21f0d9bebdc8ac1dde69b3c0951b339984883e2a751790f0f72cbfd1dd007", - "sha256:863d7401d3a109f75c7a5ca0e33e8fb7704a61007f4bda03e08e05f3bf1af40f", - "sha256:866c17223f7d734377a260a2800e14791cb5e55ec252de624e053a0b36b8568a", - "sha256:884d4f3509dfc810299d14faed24c0fbcac82ae2a9737b0cb1d8f7a5e8a291f8", - "sha256:88ff0c0bea02ce78af8a91b173fb43aad5f1945221182f77ba7816fd01bcbc4c", - "sha256:8910f022242c0a15f6d77d781c6ba16bb88d9fed3bff8964de652ee2580029ac", - "sha256:8bfdb95a85404a943197264461b904a2e9e228fd28cb86e4e57321f5b4d5be07", - "sha256:8d39e71705dccdcdf077752d4dc0fcc9554bf797f8af0c1db59f0025a72d4ed2", - "sha256:92a719bb1118f302f6fc3c7638e78e152de8bf279c0200325af831afa1b60f1a", - "sha256:9618070bb76a064c13020323b7fc23c332930604dfbc96b77e7ad7baca960c12", - "sha256:973d630c00bbaf07045870d331c8596bf4fa07aa8eb10d69a02c542af714f128", - "sha256:99febd7a9efab236d798d72ca878ae0d92fffadcc2e472636d6e093ce2677980", - "sha256:9eaf0f28ed19919bdeb02cfa541daaee8a05c070227eaab8d9732f1eebfe2869", - "sha256:9ee84156656d4a09010c280f41011f0a317c62e745f7a2cfafabd8035823fe2d", - "sha256:a999c5c50af0e564cab5bbbbbee97d494eb0e09f99481385108ddfd90049b3fe", - "sha256:ac210d628b9a50699189ec09f0f73630c78e60027d81d75c461deebf7aed752c", - "sha256:ac487adb2e838d03aed0c1a9df4ba348ca2c215bf2afa2f6e1d9449c7029971f", - "sha256:ad6775f8bd57e2c4068246e03c00e212e01b27ea0e96a4b4f17f9d45d80cd5d8", - "sha256:aef7e9b60b371f4d3c3ea80c0ef2d841623dd64aad7718ab815a3205bd4bdf08", - "sha256:b0e38cf49c17e35831ec38029854b772717d6071f0419b74b80be57571a83d0a", - "sha256:b68c0c9deb2fcd183376600df99e88032a9c192d352b0f781e130b09220ef1cf", - "sha256:b846a17f810708f1beff6ad088121fd35334729df3e520412163c74ef49433f7", - "sha256:bc8a06f7bc45219b2c191d68e779e6b3f62e32d09d2f8cf7b381ba1dcb7a68f9", - "sha256:bd6ca6e66b4fee5e879207854f125b94f6ad77e98ddae4d7778d2e96be94ede4", - "sha256:c115756cb1cad49862aa0c2687922ed10da6be7689cf35e3ab602c4a6da2d8fb", - "sha256:c2c4da0802f6897f7fb766c4f0e7f55c96b103981265fcf12b648d088bee3744", - "sha256:c464852c531e44abc5ba05d0c0c97a8fa63719106b3dca46fedae14daedf46ae", - "sha256:c48477c6ff32032624aa122323adc343055bb7e347e01146a86e652b06281731", - "sha256:c6dfa317e4b87052589253f50119211b801146ff7214b8684830e9084fa6eb0a", - "sha256:c763e42a29ac98e7240004e36b7ce231046054393182f1f630c897ce049579ed", - "sha256:c7fab0120e4ea5a2c170382bd27345b2b56e22b6270b40e4231a68f090ce17ed", - "sha256:cb56dcaf10bac9713fff133074d2460b0b217f27760a2b642efb2bc4179bfde6", - "sha256:cfcca979b72f240bac7c73564026eae4c97639151a415e6ced6392d120022d2d", - "sha256:d070756da822a538231d519ce290a1423ab108d6174ad1497cd020bee503d818", - "sha256:d5c35188fac7e448b52eb3916365fe5f59eb27fecec21ba757eea4f650584ca5", - "sha256:d8da09e318a2916da7110d1147355056ee89d61b4ded49ba3ada717517f2fc71", - "sha256:d957259a15e45e5fa5d51ce59ab7519cff8d3de0109d404627276ec68412c718", - "sha256:de1ab4f48fbcb4c2e578951338cc1c8245e510be061d2773a2d47616fb0d6470", - "sha256:de9f7a51f828f73ea0ca2e856a7cac8766752f336241abdb6c5f45f402dd59ea", - "sha256:e00aaf1574075439ccb0b827ca822c5a97c0103351ead292c42a9f17bd2eae0a", - "sha256:e6df05c2234786b15632cd154d60122c302fd860d89c3ee47c166ad92eb6ae55", - "sha256:e7a8f70c7c283d0b4af90314ff8d969c9ab2c7ee522bfb612f42c542935f6e11", - "sha256:ec1f6129c1175d15da7b7c13ae5d4226acf6b5fe362c5b01ac9787fa88c64781", - "sha256:ececd833be7fd8390371c082103916702170e81a1b22beb989452f934def78d6", - "sha256:ee77d3c82576baae66a3281c9a6431fc84281443a7e36a8490a45b3dbbb60446", - "sha256:f0a6cd797394761692cc6f33b10f2ea46789ac0b7fba82b6df737f51e1297122", - "sha256:f1b3930f0934057825227016a141ce16aad4b2a3805fb4e2de71064d042d72e9", - "sha256:f9dae6ef584d3241571674ed7bcd1a28b003a5f0c3a6ca561ab42e5ce0c482e3", - "sha256:fb09731156f54dfd8bb097ce80f9436c2a1a282061ba29e526c375c69086b764" + "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", + "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", + "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", + "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", + "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", + "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", + "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", + "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", + "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", + "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", + "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", + "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", + "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", + "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", + "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", + "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", + "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", + "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", + "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", + "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", + "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", + "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", + "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", + "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", + "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", + "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", + "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", + "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", + "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", + "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", + "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", + "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", + "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", + "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", + "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", + "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", + "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", + "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", + "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", + "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", + "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", + "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", + "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", + "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", + "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", + "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", + "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", + "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", + "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", + "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", + "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", + "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", + "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", + "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", + "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", + "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", + "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", + "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", + "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", + "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", + "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", + "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", + "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", + "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", + "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", + "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", + "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", + "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", + "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", + "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", + "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", + "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", + "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", + "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", + "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", + "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", + "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", + "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", + "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", + "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", + "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", + "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", + "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", + "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", + "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", + "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", + "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", + "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", + "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", + "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", + "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", + "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", + "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", + "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", + "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", + "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", + "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", + "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", + "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", + "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", + "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", + "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", + "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", + "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", + "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", + "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", + "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", + "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", + "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", + "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", + "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", + "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", + "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", + "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", + "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", + "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", + "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", + "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", + "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", + "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", + "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", + "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", + "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", + "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", + "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", + "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", + "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", + "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", + "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", + "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249" ], "markers": "python_version >= '3.9'", - "version": "==1.21.0" + "version": "==1.22.0" }, "zstandard": { "hashes": [ @@ -1837,11 +1844,11 @@ }, "platformdirs": { "hashes": [ - "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", - "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf" + "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", + "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3" ], - "markers": "python_version >= '3.9'", - "version": "==4.4.0" + "markers": "python_version >= '3.10'", + "version": "==4.5.0" }, "pygments": { "hashes": [ @@ -1941,11 +1948,11 @@ }, "rich": { "hashes": [ - "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", - "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8" + "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", + "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd" ], "markers": "python_full_version >= '3.8.0'", - "version": "==14.1.0" + "version": "==14.2.0" }, "stevedore": { "hashes": [ @@ -2004,12 +2011,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", + "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.8.0" + "markers": "python_version >= '3.9'", + "version": "==4.15.0" } } } diff --git a/requirements.txt b/requirements.txt index 8530e8e0d5..36d9778da2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,14 +2,13 @@ aiodns==3.5.0; python_version >= '3.9' aiohttp==3.9.0; python_version >= '3.8' aiosignal==1.4.0; python_version >= '3.9' -attrs==25.3.0; python_version >= '3.8' -audioop-lts==0.2.2; python_version >= '3.13' -brotli==1.1.0 +attrs==25.4.0; python_version >= '3.9' +brotli==1.2.0 cairocffi==1.7.1; python_version >= '3.8' cairosvg==2.8.2; python_version >= '3.9' certifi==2025.10.5; python_version >= '3.7' cffi==2.0.0; python_version >= '3.9' -charset-normalizer==3.4.3; python_version >= '3.7' +charset-normalizer==3.4.4; python_version >= '3.7' colorama==0.4.6; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' cssselect2==0.8.0; python_version >= '3.9' defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' @@ -17,27 +16,28 @@ discord.py[speed]==2.6.3; python_version >= '3.8' dnspython==2.8.0; python_version >= '3.10' emoji==2.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' frozenlist==1.8.0; python_version >= '3.9' -idna==3.10; python_version >= '3.6' +idna==3.11; python_version >= '3.8' isodate==0.6.1 lottie[pdf]==0.7.0; python_version >= '3' motor==3.3.2; python_version >= '3.7' -multidict==6.6.4; python_version >= '3.9' +multidict==6.7.0; python_version >= '3.9' natural==0.2.0 -orjson==3.11.3; python_version >= '3.9' +orjson==3.11.4; python_version >= '3.9' packaging==23.2; python_version >= '3.7' parsedatetime==2.6 -pillow==11.3.0; python_version >= '3.9' -propcache==0.4.0; python_version >= '3.9' +pillow==12.0.0; python_version >= '3.10' +propcache==0.4.1; python_version >= '3.9' pycares==4.11.0; python_version >= '3.9' pycparser==2.23; python_version >= '3.8' -pymongo[srv]==4.15.2; python_version >= '3.9' +pymongo[srv]==4.15.3; python_version >= '3.9' python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' python-dotenv==1.0.0; python_version >= '3.8' requests==2.31.0; python_version >= '3.7' six==1.17.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' tinycss2==1.4.0; python_version >= '3.8' +typing-extensions==4.15.0; python_version >= '3.9' urllib3==2.5.0; python_version >= '3.9' -uvloop==0.21.0; sys_platform != 'win32' +uvloop==0.22.1; sys_platform != 'win32' webencodings==0.5.1 -yarl==1.21.0; python_version >= '3.9' +yarl==1.22.0; python_version >= '3.9' zstandard==0.25.0; python_version >= '3.9' From 98cb126d1ee09965bad89a4b77176227591ec6be Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 11 Nov 2025 04:10:35 -0800 Subject: [PATCH 1213/1221] Bump aiohttp and motor deps, fixed pipenv dependency errors --- Pipfile | 13 +- Pipfile.lock | 308 ++++++++++++++++++++++++++++++++--------------- requirements.txt | 12 +- 3 files changed, 228 insertions(+), 105 deletions(-) diff --git a/Pipfile b/Pipfile index 8fe5bd40c8..daa0e60698 100644 --- a/Pipfile +++ b/Pipfile @@ -7,24 +7,27 @@ verify_ssl = true bandit = ">=1.7.5" black = "==23.11.0" pylint = "==3.0.2" -typing-extensions = "==4.8.0" tomli = "==2.2.1" # Needed for black on Python < 3.11 [packages] -aiohttp = "==3.9.0" +aiohttp = "==3.13.2" +async-timeout = {version = "==5.0.1", markers = "python_version < '3.11'"} # Required by aiohttp +typing-extensions = ">=4.12.2" # Required by aiohttp colorama = "==0.4.6" "discord.py" = {version = "==2.6.3", extras = ["speed"]} emoji = "==2.8.0" isodate = "==0.6.1" -motor = "==3.3.2" +motor = "==3.7.1" natural = "==0.2.0" # Why is this needed? packaging = "==23.2" parsedatetime = "==2.6" -pymongo = {extras = ["srv"], version = "*"} # Required by motor +dnspython = ">=2.8,<3" # Required by pymongo +pymongo = ">=4.9,<5" # Required by motor python-dateutil = "==2.8.2" python-dotenv = "==1.0.0" uvloop = {version = ">=0.19.0", markers = "sys_platform != 'win32'"} -lottie = {version = "==0.7.0", extras = ["pdf"]} +lottie = {version = "==0.7.2", extras = ["pdf"]} +setuptools = "*" # Needed for lottie requests = "==2.31.0" [scripts] diff --git a/Pipfile.lock b/Pipfile.lock index 5c4310d52e..39cd6c33e2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "93bdedec9b82ac210253d3424e48f3af9e79dca6c44b5b941ec446709de27409" + "sha256": "b9e47a4bb95c39f0d11eeffe03c9229ef1751eec0e412c1a9b4c1f6dc47ed754" }, "pipfile-spec": 6, "requires": {}, @@ -22,88 +22,140 @@ "markers": "python_version >= '3.9'", "version": "==3.5.0" }, + "aiohappyeyeballs": { + "hashes": [ + "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", + "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8" + ], + "markers": "python_version >= '3.9'", + "version": "==2.6.1" + }, "aiohttp": { "hashes": [ - "sha256:05857848da443c8c12110d99285d499b4e84d59918a21132e45c3f0804876994", - "sha256:05a183f1978802588711aed0dea31e697d760ce9055292db9dc1604daa9a8ded", - "sha256:09f23292d29135025e19e8ff4f0a68df078fe4ee013bca0105b2e803989de92d", - "sha256:11ca808f9a6b63485059f5f6e164ef7ec826483c1212a44f268b3653c91237d8", - "sha256:1736d87dad8ef46a8ec9cddd349fa9f7bd3a064c47dd6469c0d6763d3d49a4fc", - "sha256:1df43596b826022b14998f0460926ce261544fedefe0d2f653e1b20f49e96454", - "sha256:23170247ef89ffa842a02bbfdc425028574d9e010611659abeb24d890bc53bb8", - "sha256:2779f5e7c70f7b421915fd47db332c81de365678180a9f3ab404088f87ba5ff9", - "sha256:28185e36a78d247c55e9fbea2332d16aefa14c5276a582ce7a896231c6b1c208", - "sha256:2cbc14a13fb6b42d344e4f27746a4b03a2cb0c1c3c5b932b0d6ad8881aa390e3", - "sha256:2d71abc15ff7047412ef26bf812dfc8d0d1020d664617f4913df2df469f26b76", - "sha256:2d820162c8c2bdbe97d328cd4f417c955ca370027dce593345e437b2e9ffdc4d", - "sha256:317719d7f824eba55857fe0729363af58e27c066c731bc62cd97bc9c3d9c7ea4", - "sha256:35a68cd63ca6aaef5707888f17a70c36efe62b099a4e853d33dc2e9872125be8", - "sha256:3607375053df58ed6f23903aa10cf3112b1240e8c799d243bbad0f7be0666986", - "sha256:366bc870d7ac61726f32a489fbe3d1d8876e87506870be66b01aeb84389e967e", - "sha256:3abf0551874fecf95f93b58f25ef4fc9a250669a2257753f38f8f592db85ddea", - "sha256:3d7f6235c7475658acfc1769d968e07ab585c79f6ca438ddfecaa9a08006aee2", - "sha256:3dd8119752dd30dd7bca7d4bc2a92a59be6a003e4e5c2cf7e248b89751b8f4b7", - "sha256:42fe4fd9f0dfcc7be4248c162d8056f1d51a04c60e53366b0098d1267c4c9da8", - "sha256:45820ddbb276113ead8d4907a7802adb77548087ff5465d5c554f9aa3928ae7d", - "sha256:4790e44f46a4aa07b64504089def5744d3b6780468c4ec3a1a36eb7f2cae9814", - "sha256:4afa8f71dba3a5a2e1e1282a51cba7341ae76585345c43d8f0e624882b622218", - "sha256:4b777c9286b6c6a94f50ddb3a6e730deec327e9e2256cb08b5530db0f7d40fd8", - "sha256:4ee1b4152bc3190cc40ddd6a14715e3004944263ea208229ab4c297712aa3075", - "sha256:51a4cd44788ea0b5e6bb8fa704597af3a30be75503a7ed1098bc5b8ffdf6c982", - "sha256:536b01513d67d10baf6f71c72decdf492fb7433c5f2f133e9a9087379d4b6f31", - "sha256:571760ad7736b34d05597a1fd38cbc7d47f7b65deb722cb8e86fd827404d1f6b", - "sha256:5a2eb5311a37fe105aa35f62f75a078537e1a9e4e1d78c86ec9893a3c97d7a30", - "sha256:5ab16c254e2312efeb799bc3c06897f65a133b38b69682bf75d1f1ee1a9c43a9", - "sha256:65b0a70a25456d329a5e1426702dde67be0fb7a4ead718005ba2ca582d023a94", - "sha256:673343fbc0c1ac44d0d2640addc56e97a052504beacd7ade0dc5e76d3a4c16e8", - "sha256:6777a390e41e78e7c45dab43a4a0196c55c3b8c30eebe017b152939372a83253", - "sha256:6896b8416be9ada4d22cd359d7cb98955576ce863eadad5596b7cdfbf3e17c6c", - "sha256:694df243f394629bcae2d8ed94c589a181e8ba8604159e6e45e7b22e58291113", - "sha256:70e851f596c00f40a2f00a46126c95c2e04e146015af05a9da3e4867cfc55911", - "sha256:7276fe0017664414fdc3618fca411630405f1aaf0cc3be69def650eb50441787", - "sha256:76a86a9989ebf82ee61e06e2bab408aec4ea367dc6da35145c3352b60a112d11", - "sha256:7a94bde005a8f926d0fa38b88092a03dea4b4875a61fbcd9ac6f4351df1b57cd", - "sha256:7ae5f99a32c53731c93ac3075abd3e1e5cfbe72fc3eaac4c27c9dd64ba3b19fe", - "sha256:7e8a3b79b6d186a9c99761fd4a5e8dd575a48d96021f220ac5b5fa856e5dd029", - "sha256:816f4db40555026e4cdda604a1088577c1fb957d02f3f1292e0221353403f192", - "sha256:8303531e2c17b1a494ffaeba48f2da655fe932c4e9a2626c8718403c83e5dd2b", - "sha256:8488519aa05e636c5997719fe543c8daf19f538f4fa044f3ce94bee608817cff", - "sha256:87c8b0a6487e8109427ccf638580865b54e2e3db4a6e0e11c02639231b41fc0f", - "sha256:8c9e5f4d7208cda1a2bb600e29069eecf857e6980d0ccc922ccf9d1372c16f4b", - "sha256:94697c7293199c2a2551e3e3e18438b4cba293e79c6bc2319f5fd652fccb7456", - "sha256:9623cfd9e85b76b83ef88519d98326d4731f8d71869867e47a0b979ffec61c73", - "sha256:98d21092bf2637c5fa724a428a69e8f5955f2182bff61f8036827cf6ce1157bf", - "sha256:99ae01fb13a618b9942376df77a1f50c20a281390dad3c56a6ec2942e266220d", - "sha256:9c196b30f1b1aa3363a69dd69079ae9bec96c2965c4707eaa6914ba099fb7d4f", - "sha256:a00ce44c21612d185c5275c5cba4bab8d7c1590f248638b667ed8a782fa8cd6f", - "sha256:a1b66dbb8a7d5f50e9e2ea3804b01e766308331d0cac76eb30c563ac89c95985", - "sha256:a1d7edf74a36de0e5ca50787e83a77cf352f5504eb0ffa3f07000a911ba353fb", - "sha256:a1e3b3c107ccb0e537f309f719994a55621acd2c8fdf6d5ce5152aed788fb940", - "sha256:a486ddf57ab98b6d19ad36458b9f09e6022de0381674fe00228ca7b741aacb2f", - "sha256:ac9669990e2016d644ba8ae4758688534aabde8dbbc81f9af129c3f5f01ca9cd", - "sha256:b1a2ea8252cacc7fd51df5a56d7a2bb1986ed39be9397b51a08015727dfb69bd", - "sha256:c5b7bf8fe4d39886adc34311a233a2e01bc10eb4e842220235ed1de57541a896", - "sha256:c67a51ea415192c2e53e4e048c78bab82d21955b4281d297f517707dc836bf3d", - "sha256:ca4fddf84ac7d8a7d0866664936f93318ff01ee33e32381a115b19fb5a4d1202", - "sha256:d5b9345ab92ebe6003ae11d8092ce822a0242146e6fa270889b9ba965457ca40", - "sha256:d97c3e286d0ac9af6223bc132dc4bad6540b37c8d6c0a15fe1e70fb34f9ec411", - "sha256:db04d1de548f7a62d1dd7e7cdf7c22893ee168e22701895067a28a8ed51b3735", - "sha256:dcf71c55ec853826cd70eadb2b6ac62ec577416442ca1e0a97ad875a1b3a0305", - "sha256:de3cc86f4ea8b4c34a6e43a7306c40c1275e52bfa9748d869c6b7d54aa6dad80", - "sha256:deac0a32aec29608eb25d730f4bc5a261a65b6c48ded1ed861d2a1852577c932", - "sha256:e18d92c3e9e22553a73e33784fcb0ed484c9874e9a3e96c16a8d6a1e74a0217b", - "sha256:eb6dfd52063186ac97b4caa25764cdbcdb4b10d97f5c5f66b0fa95052e744eb7", - "sha256:f09960b5bb1017d16c0f9e9f7fc42160a5a49fa1e87a175fd4a2b1a1833ea0af", - "sha256:f1e4f254e9c35d8965d377e065c4a8a55d396fe87c8e7e8429bcfdeeb229bfb3", - "sha256:f32c86dc967ab8c719fd229ce71917caad13cc1e8356ee997bf02c5b368799bf", - "sha256:f50b4663c3e0262c3a361faf440761fbef60ccdde5fe8545689a4b3a3c149fb4", - "sha256:f8e05f5163528962ce1d1806fce763ab893b1c5b7ace0a3538cd81a90622f844", - "sha256:f929f4c9b9a00f3e6cc0587abb95ab9c05681f8b14e0fe1daecfa83ea90f8318", - "sha256:f9e09a1c83521d770d170b3801eea19b89f41ccaa61d53026ed111cb6f088887" + "sha256:04c3971421576ed24c191f610052bcb2f059e395bc2489dd99e397f9bc466329", + "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", + "sha256:070599407f4954021509193404c4ac53153525a19531051661440644728ba9a7", + "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", + "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", + "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", + "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", + "sha256:0e87dff73f46e969af38ab3f7cb75316a7c944e2e574ff7c933bc01b10def7f5", + "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", + "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", + "sha256:1f9b2c2d4b9d958b1f9ae0c984ec1dd6b6689e15c75045be8ccb4011426268ca", + "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", + "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", + "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", + "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", + "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", + "sha256:23ad365e30108c422d0b4428cf271156dd56790f6dd50d770b8e360e6c5ab2e6", + "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", + "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", + "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", + "sha256:29562998ec66f988d49fb83c9b01694fa927186b781463f376c5845c121e4e0b", + "sha256:2adebd4577724dcae085665f294cc57c8701ddd4d26140504db622b8d566d7aa", + "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", + "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", + "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", + "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", + "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", + "sha256:3a92cf4b9bea33e15ecbaa5c59921be0f23222608143d025c989924f7e3e0c07", + "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", + "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", + "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", + "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", + "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", + "sha256:4dd3db9d0f4ebca1d887d76f7cdbcd1116ac0d05a9221b9dad82c64a62578c4d", + "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", + "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", + "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", + "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", + "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", + "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", + "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", + "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", + "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", + "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", + "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", + "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", + "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", + "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", + "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", + "sha256:7c3a50345635a02db61792c85bb86daffac05330f6473d524f1a4e3ef9d0046d", + "sha256:7fbdf5ad6084f1940ce88933de34b62358d0f4a0b6ec097362dcd3e5a65a4989", + "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", + "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", + "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", + "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", + "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", + "sha256:8b2f1414f6a1e0683f212ec80e813f4abef94c739fd090b66c9adf9d2a05feac", + "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", + "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", + "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", + "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", + "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", + "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", + "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", + "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", + "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", + "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", + "sha256:9c705601e16c03466cb72011bd1af55d68fa65b045356d8f96c216e5f6db0fa5", + "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", + "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", + "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", + "sha256:9f377d0a924e5cc94dc620bc6366fc3e889586a7f18b748901cf016c916e2084", + "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", + "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", + "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", + "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", + "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", + "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", + "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", + "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", + "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", + "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", + "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", + "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", + "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", + "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", + "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", + "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", + "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", + "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", + "sha256:d7bc4b7f9c4921eba72677cd9fedd2308f4a4ca3e12fab58935295ad9ea98700", + "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", + "sha256:dacd50501cd017f8cccb328da0c90823511d70d24a323196826d923aad865901", + "sha256:e036a3a645fe92309ec34b918394bb377950cbb43039a97edae6c08db64b23e2", + "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", + "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", + "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", + "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", + "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", + "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", + "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", + "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", + "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", + "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", + "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", + "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", + "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", + "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", + "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", + "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", + "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", + "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", + "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", + "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", + "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", + "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", + "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", + "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", + "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", + "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==3.9.0" + "markers": "python_version >= '3.9'", + "version": "==3.13.2" }, "aiosignal": { "hashes": [ @@ -113,6 +165,14 @@ "markers": "python_version >= '3.9'", "version": "==1.4.0" }, + "async-timeout": { + "hashes": [ + "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", + "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3" + ], + "markers": "python_version < '3.11'", + "version": "==5.0.1" + }, "attrs": { "hashes": [ "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", @@ -121,6 +181,61 @@ "markers": "python_version >= '3.9'", "version": "==25.4.0" }, + "audioop-lts": { + "hashes": [ + "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", + "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", + "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", + "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", + "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", + "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", + "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", + "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", + "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", + "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", + "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", + "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", + "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", + "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", + "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", + "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", + "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", + "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", + "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", + "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", + "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", + "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", + "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", + "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", + "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", + "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", + "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", + "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", + "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", + "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", + "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", + "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", + "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", + "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", + "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", + "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", + "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", + "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", + "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", + "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", + "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", + "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", + "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", + "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", + "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", + "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", + "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", + "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", + "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800" + ], + "markers": "python_version >= '3.13'", + "version": "==0.2.2" + }, "brotli": { "hashes": [ "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", @@ -500,6 +615,7 @@ "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f" ], + "index": "pypi", "markers": "python_version >= '3.10'", "version": "==2.8.0" }, @@ -669,19 +785,19 @@ "pdf" ], "hashes": [ - "sha256:a3242f8ba37051fbdd7503ecd168203a08e4af26f17be2ecca08a64af1e7d3c1" + "sha256:85304ac152cd042c9fe98a527b5d136a31c6fe2aacd859ce1c58189bb946d127" ], "markers": "python_version >= '3'", - "version": "==0.7.0" + "version": "==0.7.2" }, "motor": { "hashes": [ - "sha256:6fe7e6f0c4f430b9e030b9d22549b732f7c2226af3ab71ecc309e4a1b7d19953", - "sha256:d2fc38de15f1c8058f389c1a44a4d4105c0405c48c061cd492a654496f7bc26a" + "sha256:27b4d46625c87928f331a6ca9d7c51c2f518ba0e270939d395bc1ddc89d64526", + "sha256:8a63b9049e38eeeb56b4fdd57c3312a6d1f25d01db717fe7d82222393c410298" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==3.3.2" + "markers": "python_version >= '3.9'", + "version": "==3.7.1" }, "multidict": { "hashes": [ @@ -1284,9 +1400,6 @@ "version": "==2.23" }, "pymongo": { - "extras": [ - "srv" - ], "hashes": [ "sha256:07bcc36d11252f24fe671e7e64044d39a13d997b0502c6401161f28cc144f584", "sha256:09440e78dff397b2f34a624f445ac8eb44c9756a2688b85b3bf344d351d198e1", @@ -1360,6 +1473,7 @@ "sha256:ff99864085d2c7f4bb672c7167680ceb7d273e9a93c1a8074c986a36dbb71cc6", "sha256:ffe217d2502f3fba4e2b0dc015ce3b34f157b66dfe96835aa64432e909dd0d95" ], + "index": "pypi", "markers": "python_version >= '3.9'", "version": "==4.15.3" }, @@ -1390,6 +1504,15 @@ "markers": "python_version >= '3.7'", "version": "==2.31.0" }, + "setuptools": { + "hashes": [ + "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", + "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==80.9.0" + }, "six": { "hashes": [ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", @@ -1411,6 +1534,7 @@ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" ], + "index": "pypi", "markers": "python_version >= '3.9'", "version": "==4.15.0" }, @@ -2008,14 +2132,6 @@ ], "markers": "python_version >= '3.8'", "version": "==0.13.3" - }, - "typing-extensions": { - "hashes": [ - "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", - "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" - ], - "markers": "python_version >= '3.9'", - "version": "==4.15.0" } } } diff --git a/requirements.txt b/requirements.txt index 36d9778da2..9c07172039 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,11 @@ -i https://pypi.org/simple aiodns==3.5.0; python_version >= '3.9' -aiohttp==3.9.0; python_version >= '3.8' +aiohappyeyeballs==2.6.1; python_version >= '3.9' +aiohttp==3.13.2; python_version >= '3.9' aiosignal==1.4.0; python_version >= '3.9' +async-timeout==5.0.1; python_version < '3.11' attrs==25.4.0; python_version >= '3.9' +audioop-lts==0.2.2; python_version >= '3.13' brotli==1.2.0 cairocffi==1.7.1; python_version >= '3.8' cairosvg==2.8.2; python_version >= '3.9' @@ -18,8 +21,8 @@ emoji==2.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, frozenlist==1.8.0; python_version >= '3.9' idna==3.11; python_version >= '3.8' isodate==0.6.1 -lottie[pdf]==0.7.0; python_version >= '3' -motor==3.3.2; python_version >= '3.7' +lottie[pdf]==0.7.2; python_version >= '3' +motor==3.7.1; python_version >= '3.9' multidict==6.7.0; python_version >= '3.9' natural==0.2.0 orjson==3.11.4; python_version >= '3.9' @@ -29,10 +32,11 @@ pillow==12.0.0; python_version >= '3.10' propcache==0.4.1; python_version >= '3.9' pycares==4.11.0; python_version >= '3.9' pycparser==2.23; python_version >= '3.8' -pymongo[srv]==4.15.3; python_version >= '3.9' +pymongo==4.15.3; python_version >= '3.9' python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' python-dotenv==1.0.0; python_version >= '3.8' requests==2.31.0; python_version >= '3.7' +setuptools==80.9.0; python_version >= '3.9' six==1.17.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2' tinycss2==1.4.0; python_version >= '3.8' typing-extensions==4.15.0; python_version >= '3.9' From 829a3642405e4c3536b42338f18fe9a6577a548c Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:32:37 +0100 Subject: [PATCH 1214/1221] fix: Remove thread-only restriction from logs command (#3397) The logs command can be used anywhere again. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> --- cogs/modmail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index d7d9d7b010..fa6c01666b 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1200,7 +1200,6 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) - @checks.thread_only() async def logs(self, ctx, *, user: User = None): """ Get previous Modmail thread logs of a member. From 124669d462db41e41216d71d164e00c3dbf36e4c Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 28 Nov 2025 09:31:52 +0100 Subject: [PATCH 1215/1221] modmail update QOL (#3392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add (truncated) preview to snippets command (#3342) * Add (truncated) preview to snippets command * Add old view as option with "compact" * Fix black formatting * Fix: Image url regex in thread send method (#3378) * feat minimum character requirement for thread creation. (#3380) * update: dpy, snoozing. This pull request updated discord.py to 2.5.2. This also brings a new few features. - snooze - snoozed - unsnooze - clearsnoozed Aswell as a few new config options. - max_snooze_time - snooze_title - snooze_text - unsnooze_tex - unsnooze_notify_channel Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * remove: unneeded import Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Formatting black Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix?: internal messages on restoration Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * formatting Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix: internal messages. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix: internals Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update thread.py Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix: use same logkey after restoration Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Add files via upload Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update thread.py Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update thread.py Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update thread.py Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix: show who send which internal message. * Black formatting. * Update Pipfile Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update Pipfile.lock Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * fix: unsnooze bug * feat: CV2 * update: black * fix: duplicates in logs, notes. * feat: dpy 2.6.3, forwarded messages, bug fixes. * Fix jump_url not being displayed * Update pipfile for new dpy version * fix: bug in note title/color * Update snooze arg * Update Pipfile to include tomli package Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * auto detect dpy version Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Remove crlf terminators * fix: ignore typing failures Make Modmail keep working when typing is disabled/outage * fix: only surpress failures * chore: sync local edits before push * fix: ignore typing failures (#3389) * fix: ignore typing failures Make Modmail keep working when typing is disabled/outage * fix: only surpress failures * chore: sync local edits before push * Lock pipenv * Fix: closing with timed words/ command in reply. * Fix: typing in changelog command. * Fix: closing with timed words (additional)) * Fix changelog entry for command reply issue Corrected wording in the changelog entry regarding command inclusion in replies. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update CHANGELOG for v4.2.0 enhancements Forwarded messages now display correctly in threads. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Fix: closing with timed words/ command in reply. (#3391) * fix: ignore typing failures Make Modmail keep working when typing is disabled/outage * fix: only surpress failures * chore: sync local edits before push * Fix: closing with timed words/ command in reply. * Fix: typing in changelog command. * Fix: closing with timed words (additional)) * Fix changelog entry for command reply issue Corrected wording in the changelog entry regarding command inclusion in replies. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Update CHANGELOG for v4.2.0 enhancements Forwarded messages now display correctly in threads. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> --------- Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * Remove disutil, undo lowercasing escape seq * Add back uvloop * Add config help for snooze configs Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> * fix; raceconditions, thread duplication on unsnooze, message queue for accurasy on high load * Update package versions in requirements.txt Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> * snooze(move): auto-unsnooze on reply/any mod message; enforce hidden permissions on auto-created Snoozed Threads and sync perms on move; restore original overwrites on unsnooze; add capacity guard and config docs * unsnooze: suppress mentions during restore (AllowedMentions.none on replay and notifications) * Remove base64 snooze/unsnooze logic, fix notification crash, clean up replay logic * fix: escape mentions on unsnooze * Fix: Only create log URL button if valid, and robust channel restore for snooze * black formatting * Unsnooze: prefix username (user_id) for plain-text replay messages * feat: command queue during unsnooze process. feat(config): `unsnooze_history_limit`: Limits the number of messages replayed when unsnoozing (genesis message and notes are always shown). * fix: contact while snooze returned as invalid channel * Update thread.py * fix: snooze timing * change: rename default snooze time config * fix: parsing * fix: cache for snooze timer * fix: Properly accessing nested data * rename: default_snooze_time -> snooze_default_duration * improve unsnooze notify * fix: id extraction for clean database. * improve: support for user-friendly time input for snooze_default_duration * reflect config help snooze_default_duration for userfriendly time * fix: anonreply showing None This fixes a bug where, if no `anon_username` is set and the moderator has no roles, the `anon_username` is not showing as `None` anymore, and will show as intended. The logic now works as follows: - If a config anon_username is set → use it - Else, if a mod_tag is set → use it - Else, if the moderator has a top role → use that - Else → use "Anonymous" * black formatting * feat: thread creation menu Credits to Sebkuip(https://github.com/sebkuip) for the original idea and populair plugin.(advanced-menu) This now is a core feature. * Update requirements.txt * fixes This solves: - `config get`: invalid form body - restores functionality after menu timeout. * feat: thread_creation_menu_precreate_channel adds a new: thread_creation_menu_precreate_channel to create the threads, even when nothing is selected yet. * core Fully merged `threadmenu config` into the main bot’s config system. Added threadmenu embed customization capability. * Change thread_creation_menu config Moves thread_creation_menu_options, thread_creation_menu_submenus and thread_creation_menu_enabled to the private config keys to avoid changes via ?config set as they can be changed via ?threadmenu * Change Option Description/Emoji Changes the description and emoji making it possible to be None (optional) * fix: disable menu after closure * feats/fixes configs added: thread_creation_send_dm_embed fix: snooze while no option was selected, after snoozing selecting option first errored. This is now solved * forgot to add help * rely on config for precreating. fix error with unknown channels * feat: large images in threadmenu embed * feat: threadmenu reset * cleanup * codereviews response. Formatting (ruff) * log instead of pass on exception * format * higher delays * black formatting, smoothen menu closure * smoothen which selecetion was made * solves the command from showing in the replies in rare cases * Update CHANGELOG.md * Update sponsors and bmac links * fix: typeerror * fix: Remove thread-only restriction from logs command The logs command can be used anywhere again. * Remove commands from being saved in DB. * fix: correct guildpfp with sepserver setup - made by martin This PR fixes displaying the wrong guild icon in the thread_creation_response embed. When servers having a seperate server setup, the guild icon from the main guild should be displayed instead of the inbox guild. * fix: robust channel deletion handling, skip audit log attribution if permission is missing * update: use current reqs.txt as masterbranch * sync: pip&pip lock with master * Patch Thread Menu Editing Crash * bump version * Missing a newline Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> * Missing a newline Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> --------- Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Signed-off-by: Taku <45324516+Taaku18@users.noreply.github.com> Co-authored-by: Sebastian <61157793+sebkuip@users.noreply.github.com> Co-authored-by: Zallom <Yg75nWkHX7jBHcqRPUbb783c54xSa9+github63384893@gmail.com> Co-authored-by: Martin <box152535@gmail.com> Co-authored-by: Taku <45324516+Taaku18@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 26 + bot.py | 307 ++++++- cogs/modmail.py | 423 ++++++++-- cogs/plugins.py | 28 +- cogs/threadmenu.py | 830 ++++++++++++++++++ cogs/utility.py | 150 +++- core/clients.py | 26 +- core/config.py | 90 +- core/config_help.json | 289 ++++++- core/models.py | 26 +- core/paginator.py | 2 +- core/thread.py | 1876 +++++++++++++++++++++++++++++++++++++---- core/time.py | 20 +- core/utils.py | 17 +- pyproject.toml | 2 +- 15 files changed, 3765 insertions(+), 347 deletions(-) create mode 100644 cogs/threadmenu.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c4f1bdd4..536680ad2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/modmail-dev/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v4.2.1 + +### Added +* `unsnooze_history_limit`: Limits the number of messages replayed when unsnoozing (genesis message and notes are always shown). +* `snooze_behavior`: Choose between `delete` (legacy) or `move` behavior for snoozing. +* `snoozed_category_id`: Target category for `move` snoozing; required when `snooze_behavior` is `move`. +* Thread-creation menu: Adds an interactive select step before a thread channel is created. + * Commands: + * `threadmenu toggle`: Enable/disable the menu. + * `threadmenu show`: List current top-level options. + * `threadmenu option add`: Interactive wizard to create an option. + * `threadmenu option edit/remove/show`: Manage or inspect an existing option. + * `threadmenu submenu create/delete/list/show`: Manage submenus. + * `threadmenu submenu option add/edit/remove`: Manage options inside a submenu. + * Configuration / Behavior: + * Per-option `category` targeting when creating a thread; falls back to `main_category_id` if invalid/missing. + * Optional selection logging (`thread_creation_menu_selection_log`) posts the chosen option in the new thread. + * Anonymous prompt support (`thread_creation_menu_anonymous_menu`). + +### Changed +- Renamed `max_snooze_time` to `snooze_default_duration`. The old config will be invalidated. +- When `snooze_behavior` is set to `move`, the snoozed category now has a hard limit of 49 channels. New snoozes are blocked once it’s full until space is freed. +- When switching `snooze_behavior` to `move` via `?config set`, the bot reminds admins to set `snoozed_category_id` if it’s missing. +- Thread-creation menu options & submenu options now support an optional per-option `category` target. The interactive wizards (`threadmenu option add` / `threadmenu submenu option add`) and edit commands allow specifying or updating a category. If the stored category is missing or invalid at selection time, channel creation automatically falls back to `main_category_id`. + + # v4.2.0 Upgraded discord.py to version 2.6.3, added support for CV2. diff --git a/bot.py b/bot.py index 671d9ab9c4..6176ac5824 100644 --- a/bot.py +++ b/bot.py @@ -1,10 +1,9 @@ -__version__ = "4.2.0" +__version__ = "4.2.1" import asyncio import copy import hashlib -import logging import os import re import string @@ -12,7 +11,7 @@ import sys import platform import typing -from datetime import datetime, timezone, timedelta +from datetime import datetime, timezone from subprocess import PIPE from types import SimpleNamespace @@ -84,12 +83,18 @@ def __init__(self): self.session = None self._api = None self.formatter = SafeFormatter() - self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] + self.loaded_cogs = [ + "cogs.modmail", + "cogs.plugins", + "cogs.utility", + "cogs.threadmenu", + ] self._connected = None self.start_time = discord.utils.utcnow() self._started = False self.threads = ThreadManager(self) + self._message_queues = {} # User ID -> asyncio.Queue for message ordering log_dir = os.path.join(temp_dir, "logs") if not os.path.exists(log_dir): @@ -101,7 +106,10 @@ def __init__(self): self.startup() def get_guild_icon( - self, guild: typing.Optional[discord.Guild], *, size: typing.Optional[int] = None + self, + guild: typing.Optional[discord.Guild], + *, + size: typing.Optional[int] = None, ) -> str: if guild is None: guild = self.guild @@ -316,7 +324,10 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: try: 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) + logger.warning( + "No log channel set, setting #%s to be the log channel.", + channel.name, + ) return channel except IndexError: pass @@ -569,7 +580,11 @@ async def on_ready(self): 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) + 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)) @@ -590,7 +605,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: + if log.get("channel_id") is None or self.get_channel(int(log["channel_id"])) is None: logger.debug("Unable to resolve thread with channel %s.", log["channel_id"]) log_data = await self.api.post_log( log["channel_id"], @@ -611,7 +626,10 @@ async def on_ready(self): if log_data: logger.debug("Successfully closed thread with channel %s.", log["channel_id"]) else: - logger.debug("Failed to close thread with channel %s, skipping.", log["channel_id"]) + logger.debug( + "Failed to close thread with channel %s, skipping.", + log["channel_id"], + ) other_guilds = [guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild}] if any(other_guilds): @@ -870,7 +888,8 @@ async def get_thread_cooldown(self, author: discord.Member): @staticmethod async def add_reaction( - msg, reaction: typing.Union[discord.Emoji, discord.Reaction, discord.PartialEmoji, str] + msg, + reaction: typing.Union[discord.Emoji, discord.Reaction, discord.PartialEmoji, str], ) -> bool: if reaction != "disable": try: @@ -880,6 +899,36 @@ async def add_reaction( return False return True + async def _queue_dm_message(self, message: discord.Message) -> None: + """Queue DM messages to ensure they're processed in order per user.""" + user_id = message.author.id + + if user_id not in self._message_queues: + self._message_queues[user_id] = asyncio.Queue() + # Start processing task for this user + self.loop.create_task(self._process_user_messages(user_id)) + + await self._message_queues[user_id].put(message) + + async def _process_user_messages(self, user_id: int) -> None: + """Process messages for a specific user in order.""" + queue = self._message_queues[user_id] + + while True: + try: + # Wait for a message with timeout to clean up inactive queues + message = await asyncio.wait_for(queue.get(), timeout=300) # 5 minutes + await self.process_dm_modmail(message) + queue.task_done() + except asyncio.TimeoutError: + # Clean up inactive queue + if queue.empty(): + self._message_queues.pop(user_id, None) + break + except Exception as e: + logger.error(f"Error processing message for user {user_id}: {e}", exc_info=True) + queue.task_done() + async def process_dm_modmail(self, message: discord.Message) -> None: """Processes messages sent to the bot.""" blocked = await self._process_blocked(message) @@ -1055,13 +1104,33 @@ def __init__(self, original_message, ref_message): if thread and thread.snoozed: await thread.restore_from_snooze() self.threads.cache[thread.id] = thread - # Update the DB with the new channel_id after restoration - if thread.channel: - await self.api.logs.update_one( - {"recipient.id": str(thread.id)}, {"$set": {"channel_id": str(thread.channel.id)}} + # No need to re-fetch the thread - it's already restored and cached properly + + # If the previous thread was closed with delete_channel=True the channel object + # stored on the thread will now be invalid (deleted). In some rare race cases + # the thread can still be returned from the cache (or reconstructed) while the + # channel lookup returns None, causing downstream relay attempts to raise + # discord.NotFound ("Channel not found when trying to send message."). Treat + # this situation as "no active thread" so the user's new DM starts a fresh + # thread instead of silently failing. + try: + if ( + thread + and thread.channel + and isinstance(thread.channel, discord.TextChannel) + and self.get_channel(getattr(thread.channel, "id", None)) is None + ): + logger.info( + "Stale thread detected for %s (channel deleted). Purging cache entry and creating new thread.", + message.author, ) - # Re-fetch the thread object to ensure channel is valid - thread = await self.threads.find(recipient=message.author) + # Best-effort removal; ignore if already gone. + self.threads.cache.pop(thread.id, None) + thread = None + except Exception: + # If any attribute access fails, fall back to treating it as closed. + self.threads.cache.pop(getattr(thread, "id", None), None) + thread = None if thread is None: delta = await self.get_thread_cooldown(message.author) @@ -1075,7 +1144,10 @@ def __init__(self, original_message, ref_message): ) return - if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + if self.config["dm_disabled"] in ( + DMDisabled.NEW_THREADS, + DMDisabled.ALL_THREADS, + ): embed = discord.Embed( title=self.config["disabled_new_thread_title"], color=self.error_color, @@ -1085,11 +1157,17 @@ def __init__(self, original_message, ref_message): text=self.config["disabled_new_thread_footer"], icon_url=self.get_guild_icon(guild=message.guild, size=128), ) - logger.info("A new thread was blocked from %s due to disabled Modmail.", message.author) + logger.info( + "A new thread was blocked from %s due to disabled Modmail.", + message.author, + ) await self.add_reaction(message, blocked_emoji) return await message.channel.send(embed=embed) thread = await self.threads.create(message.author, message=message) + # If thread menu is enabled, thread creation is deferred until user selects an option. + if getattr(thread, "_pending_menu", False): + return else: if self.config["dm_disabled"] == DMDisabled.ALL_THREADS: embed = discord.Embed( @@ -1101,7 +1179,10 @@ def __init__(self, original_message, ref_message): text=self.config["disabled_current_thread_footer"], icon_url=self.get_guild_icon(guild=message.guild, size=128), ) - logger.info("A message was blocked from %s due to disabled Modmail.", message.author) + logger.info( + "A message was blocked from %s due to disabled Modmail.", + message.author, + ) await self.add_reaction(message, blocked_emoji) return await message.channel.send(embed=embed) @@ -1111,6 +1192,49 @@ def __init__(self, original_message, ref_message): except Exception: logger.error("Failed to send message:", exc_info=True) await self.add_reaction(message, blocked_emoji) + + try: + # Re-check channel existence + if thread and thread.channel and isinstance(thread.channel, discord.TextChannel): + if self.get_channel(thread.channel.id) is None: + logger.info( + "Relay failed due to deleted channel for %s; creating new thread.", + message.author, + ) + self.threads.cache.pop(thread.id, None) + new_thread = await self.threads.create(message.author, message=message) + if not getattr(new_thread, "_pending_menu", False) and not new_thread.cancelled: + try: + await new_thread.send(message) + except Exception: + logger.error( + "Failed to relay message after creating new thread:", + exc_info=True, + ) + else: + for user in new_thread.recipients: + if user != message.author: + try: + await new_thread.send(message, user) + except Exception: + logger.error( + "Failed to send message to additional recipient:", + exc_info=True, + ) + await self.add_reaction(message, sent_emoji) + self.dispatch( + "thread_reply", + new_thread, + False, + message, + False, + False, + ) + except Exception: + logger.warning( + "Unexpected failure in DM relay/new-thread follow-up block.", + exc_info=True, + ) else: for user in thread.recipients: # send to all other recipients @@ -1223,7 +1347,12 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) if trigger: invoker = re.search(trigger, message.content).group(0) else: - trigger = next(filter(lambda x: x.lower() in message.content.lower(), self.auto_triggers.keys())) + trigger = next( + filter( + lambda x: x.lower() in message.content.lower(), + self.auto_triggers.keys(), + ) + ) if trigger: invoker = trigger.lower() @@ -1356,7 +1485,7 @@ async def process_commands(self, message): return if isinstance(message.channel, discord.DMChannel): - return await self.process_dm_modmail(message) + return await self._queue_dm_message(message) ctxs = await self.get_contexts(message) for ctx in ctxs: @@ -1368,11 +1497,44 @@ async def process_commands(self, message): ) checks.has_permissions(PermissionLevel.INVALID)(ctx.command) + # Check if thread is unsnoozing and queue command if so + thread = await self.threads.find(channel=ctx.channel) + if thread and thread._unsnoozing: + queued = await thread.queue_command(ctx, ctx.command) + if queued: + # Send a brief acknowledgment that command is queued + try: + await ctx.message.add_reaction("⏳") + except Exception as e: + logger.warning("Failed to add queued-reaction: %s", e) + continue + await self.invoke(ctx) continue thread = await self.threads.find(channel=ctx.channel) if thread is not None: + # If thread is snoozed (moved), auto-unsnooze when a mod sends a message directly in channel + behavior = (self.config.get("snooze_behavior") or "delete").lower() + if thread.snoozed and behavior == "move": + if not thread.snooze_data: + try: + log_entry = await self.api.logs.find_one( + {"recipient.id": str(thread.id), "snoozed": True} + ) + if log_entry: + thread.snooze_data = log_entry.get("snooze_data") + except Exception: + logger.debug( + "Failed to add queued command reaction (⏳).", + exc_info=True, + ) + try: + await thread.restore_from_snooze() + # refresh local cache + self.threads.cache[thread.id] = thread + except Exception as e: + logger.warning("Auto-unsnooze on direct message failed: %s", e) anonymous = False plain = False if self.config.get("anon_reply_without_command"): @@ -1385,7 +1547,9 @@ async def process_commands(self, message): or self.config.get("anon_reply_without_command") or self.config.get("plain_reply_without_command") ): - await thread.reply(message, anonymous=anonymous, plain=plain) + # When replying without a command in a thread channel, use the raw content + # from the sent message as reply text while still preserving attachments. + await thread.reply(message, message.content, anonymous=anonymous, plain=plain) elif ctx.invoked_with: exc = commands.CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with)) self.dispatch("command_error", ctx, exc) @@ -1406,7 +1570,10 @@ async def on_typing(self, channel, user, _): try: await thread.channel.typing() except Exception: - pass + logger.debug( + "Failed to trigger typing indicator in recipient DM.", + exc_info=True, + ) else: if not self.config.get("mod_typing"): return @@ -1419,7 +1586,11 @@ async def on_typing(self, channel, user, _): try: await user.typing() except Exception: - pass + logger.debug( + "Failed to trigger typing for recipient %s.", + getattr(user, "id", "?"), + exc_info=True, + ) async def handle_reaction_events(self, payload): user = self.get_user(payload.user_id) @@ -1492,7 +1663,7 @@ async def handle_reaction_events(self, payload): logger.warning("Failed to find linked message for reactions: %s", e) return - if self.config["transfer_reactions"] and linked_messages is not [None]: + if self.config["transfer_reactions"] and linked_messages != [None]: if payload.event_type == "REACTION_ADD": for msg in linked_messages: await self.add_reaction(msg, reaction) @@ -1525,7 +1696,10 @@ async def handle_react_to_contact(self, payload): await message.remove_reaction(payload.emoji, member) await message.add_reaction(emoji_fmt) # bot adds as well - if self.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + if self.config["dm_disabled"] in ( + DMDisabled.NEW_THREADS, + DMDisabled.ALL_THREADS, + ): embed = discord.Embed( title=self.config["disabled_new_thread_title"], color=self.error_color, @@ -1541,6 +1715,19 @@ async def handle_react_to_contact(self, payload): ) return await member.send(embed=embed) + # Check if user has a snoozed thread + existing_thread = await self.threads.find(recipient=member) + if existing_thread and existing_thread.snoozed: + # Unsnooze the thread + await existing_thread.restore_from_snooze() + self.threads.cache[existing_thread.id] = existing_thread + # Send notification to the thread channel + if existing_thread.channel: + await existing_thread.channel.send( + f"ℹ️ {member.mention} reacted to contact and their snoozed thread has been unsnoozed." + ) + return + ctx = await self.get_context(message) await ctx.invoke(self.get_command("contact"), users=[member], manual_trigger=False) @@ -1574,12 +1761,30 @@ async def on_guild_channel_delete(self, channel): await self.config.update() return - audit_logs = self.modmail_guild.audit_logs(limit=10, action=discord.AuditLogAction.channel_delete) - found_entry = False - async for entry in audit_logs: - if int(entry.target.id) == channel.id: - found_entry = True - break + # Attempt to attribute channel deletion to a moderator via audit logs. + # This requires the "View Audit Log" permission; if missing, skip silently. + if not self.modmail_guild.me.guild_permissions.view_audit_log: + logger.debug( + "Skipping audit log lookup for deleted channel %d: missing view_audit_log permission.", + channel.id, + ) + return + + try: + audit_logs = self.modmail_guild.audit_logs(limit=10, action=discord.AuditLogAction.channel_delete) + found_entry = False + async for entry in audit_logs: + if int(entry.target.id) == channel.id: + found_entry = True + break + except discord.Forbidden: + logger.debug( + "Forbidden when fetching audit logs for deleted channel %d (missing permission).", channel.id + ) + return + except discord.HTTPException as e: + logger.debug("HTTPException when fetching audit logs for deleted channel %d: %s", channel.id, e) + return if not found_entry: logger.debug("Cannot find the audit log entry for channel delete of %d.", channel.id) @@ -1676,7 +1881,12 @@ async def on_message_delete(self, message): await thread.delete_message(message, note=False) embed = discord.Embed(description="Successfully deleted message.", color=self.main_color) except ValueError as e: - if str(e) not in {"DM message not found.", "Malformed thread message."}: + # Treat common non-fatal cases as benign: relay counterpart not present, note embeds, etc. + if str(e) not in { + "DM message not found.", + "Malformed thread message.", + "Thread message not found.", + }: logger.debug("Failed to find linked message to delete: %s", e) embed = discord.Embed(description="Failed to delete message.", color=self.error_color) else: @@ -1715,7 +1925,11 @@ async def on_error(self, event_method, *args, **kwargs): logger.error("Unexpected exception:", exc_info=sys.exc_info()) async def on_command_error( - self, context: commands.Context, exception: Exception, *, unhandled_by_cog: bool = False + self, + context: commands.Context, + exception: Exception, + *, + unhandled_by_cog: bool = False, ) -> None: if not unhandled_by_cog: command = context.command @@ -1729,7 +1943,10 @@ async def on_command_error( try: await context.typing() except Exception: - pass + logger.debug( + "Failed to start typing context for command error feedback.", + exc_info=True, + ) await context.send(embed=discord.Embed(color=self.error_color, description=str(exception))) elif isinstance(exception, commands.CommandNotFound): logger.warning("CommandNotFound: %s", exception) @@ -1760,7 +1977,10 @@ async def on_command_error( ) logger.warning("CheckFailure: %s", exception) elif isinstance(exception, commands.DisabledCommand): - logger.info("DisabledCommand: %s is trying to run eval but it's disabled", context.author.name) + logger.info( + "DisabledCommand: %s is trying to run eval but it's disabled", + context.author.name, + ) else: logger.error("Unexpected exception:", exc_info=exception) @@ -1792,7 +2012,13 @@ async def post_metadata(self): } ) else: - data.update({"owner_name": info.owner.name, "owner_id": info.owner.id, "team": False}) + data.update( + { + "owner_name": info.owner.name, + "owner_id": info.owner.id, + "team": False, + } + ) async with self.session.post("https://api.modmail.dev/metadata", json=data): logger.debug("Uploading metadata to Modmail server.") @@ -1845,7 +2071,7 @@ async def autoupdate(self): user = data["user"] embed.add_field( name="Merge Commit", - value=f"[`{short_sha}`]({html_url}) " f"{message} - {user['username']}", + value=f"[`{short_sha}`]({html_url}) {message} - {user['username']}", ) embed.set_author( name=user["username"] + " - Updating Bot", @@ -1892,7 +2118,10 @@ async def autoupdate(self): logger.info("Bot has been updated.") channel = self.update_channel - if self.hosting_method in (HostingMethod.PM2, HostingMethod.SYSTEMD): + if self.hosting_method in ( + HostingMethod.PM2, + HostingMethod.SYSTEMD, + ): embed = discord.Embed(title="Bot has been updated", color=self.main_color) embed.set_footer( text=f"Updating Modmail v{self.version} " f"-> v{latest.version} {message}" diff --git a/cogs/modmail.py b/cogs/modmail.py index fa6c01666b..0a97888646 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -7,6 +7,7 @@ import discord from discord.ext import commands +from discord.ext import tasks from discord.ext.commands.view import StringView from discord.ext.commands.cooldowns import BucketType from discord.role import Role @@ -29,6 +30,74 @@ class Modmail(commands.Cog): def __init__(self, bot): self.bot = bot + self._snoozed_cache = [] + self._auto_unsnooze_task = self.bot.loop.create_task(self.auto_unsnooze_task()) + + async def auto_unsnooze_task(self): + await self.bot.wait_until_ready() + last_db_query = 0 + while not self.bot.is_closed(): + now = datetime.now(timezone.utc) + try: + # Query DB every 2 minutes + if (now.timestamp() - last_db_query) > 120: + snoozed_threads = await self.bot.api.logs.find( + {"snooze_until": {"$gte": now.isoformat()}} + ).to_list(None) + self._snoozed_cache = snoozed_threads or [] + last_db_query = now.timestamp() + # Check cache every 10 seconds + to_unsnooze = [] + for thread_data in list(self._snoozed_cache): + snooze_until = thread_data.get("snooze_until") + recipient = thread_data.get("recipient") + if not recipient or not recipient.get("id"): + continue + thread_id = int(recipient.get("id")) + if snooze_until: + try: + dt = parser.isoparse(snooze_until) + except Exception: + continue + if now >= dt: + to_unsnooze.append(thread_data) + for thread_data in to_unsnooze: + recipient = thread_data.get("recipient") + if not recipient or not recipient.get("id"): + continue + thread_id = int(recipient.get("id")) + thread = self.bot.threads.cache.get(thread_id) or await self.bot.threads.find( + id=thread_id + ) + if thread and thread.snoozed: + await thread.restore_from_snooze() + logging.info(f"[AUTO-UNSNOOZE] Thread {thread_id} auto-unsnoozed.") + try: + channel = thread.channel + if channel: + await channel.send("⏰ This thread has been automatically unsnoozed.") + except Exception as e: + logger.info( + "Failed to notify channel after auto-unsnooze: %s", + e, + ) + self._snoozed_cache.remove(thread_data) + except Exception as e: + logging.error(f"Error in auto_unsnooze_task: {e}") + await asyncio.sleep(10) + + def _resolve_user(self, user_str): + """Helper to resolve a user from mention, ID, or username.""" + import re + + if not user_str: + return None + if user_str.isdigit(): + return int(user_str) + match = re.match(r"<@!?(\d+)>", user_str) + if match: + return int(match.group(1)) + return None def _resolve_user(self, user_str): """Helper to resolve a user from mention, ID, or username.""" @@ -179,16 +248,22 @@ async def snippet(self, ctx, *, name: str.lower = None): else: val = self.bot.snippets[snippet_name] embed = discord.Embed( - title=f'Snippet - "{snippet_name}":', description=val, color=self.bot.main_color + title=f'Snippet - "{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." + 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 add" to add a snippet.') - embed.set_author(name="Snippets", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128)) + embed.set_author( + name="Snippets", + icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128), + ) return await ctx.send(embed=embed) embeds = [discord.Embed(color=self.bot.main_color) for _ in range((len(self.bot.snippets) // 10) + 1)] @@ -448,7 +523,10 @@ async def move(self, ctx, *, arguments): silent = any(word in silent_words for word in options.split()) await thread.channel.move( - category=category, end=True, sync_permissions=True, reason=f"{ctx.author} moved this thread." + category=category, + end=True, + sync_permissions=True, + reason=f"{ctx.author} moved this thread.", ) if self.bot.config["thread_move_notify"] and not silent: @@ -471,21 +549,24 @@ async def move(self, ctx, *, arguments): 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) + """Send a scheduled close notice only to the staff thread channel. + Uses Discord relative timestamp formatting for better UX. + """ + ts = int((after.dt if after.dt.tzinfo else after.dt.replace(tzinfo=timezone.utc)).timestamp()) embed = discord.Embed( title="Scheduled close", - description=f"This thread will{' silently' if silent else ''} close in {human_delta}.", + description=f"This thread will{' silently' if silent else ''} close <t:{ts}:R>.", color=self.bot.error_color, ) - 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.timestamp = after.dt - await ctx.send(embed=embed) + thread = getattr(ctx, "thread", None) + if thread and ctx.channel == thread.channel: + await thread.channel.send(embed=embed) @commands.command(usage="[after] [close message]") @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -526,7 +607,8 @@ async def close( 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( @@ -625,7 +707,8 @@ async def unnotify(self, ctx, *, user_or_role: Union[discord.Role, User, str.low 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) @@ -736,7 +819,8 @@ async def msglink(self, ctx, message_id: int): continue if not found: embed = discord.Embed( - color=self.bot.error_color, description="Message not found or no longer exists." + color=self.bot.error_color, + description="Message not found or no longer exists.", ) else: embed = discord.Embed(color=self.bot.main_color, description=message.jump_url) @@ -968,7 +1052,8 @@ async def removeuser(self, ctx, *users_arg: Union[discord.Member, discord.Role, to_exec = [] if not silent: description = self.bot.formatter.format( - self.bot.config["private_removed_from_group_response"], moderator=ctx.author + self.bot.config["private_removed_from_group_response"], + moderator=ctx.author, ) em = discord.Embed( title=self.bot.config["private_removed_from_group_title"], @@ -1072,7 +1157,7 @@ async def anonadduser(self, ctx, *users_arg: Union[discord.Member, discord.Role, tag = str(get_top_role(ctx.author, self.bot.config["use_hoisted_top_role"])) name = self.bot.config["anon_username"] if name is None: - name = tag + name = "Anonymous" avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: avatar_url = self.bot.get_guild_icon(guild=ctx.guild, size=128) @@ -1163,7 +1248,7 @@ async def anonremoveuser(self, ctx, *users_arg: Union[discord.Member, discord.Ro tag = str(get_top_role(ctx.author, self.bot.config["use_hoisted_top_role"])) name = self.bot.config["anon_username"] if name is None: - name = tag + name = "Anonymous" avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: avatar_url = self.bot.get_guild_icon(guild=ctx.guild, size=128) @@ -1370,10 +1455,10 @@ async def reply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ + # Ensure logs record only the reply text, not the command. ctx.message.content = msg - async with safe_typing(ctx): - await ctx.thread.reply(ctx.message) + await ctx.thread.reply(ctx.message, msg) @commands.command(aliases=["formatreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1391,11 +1476,15 @@ async def freply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ msg = self.bot.formatter.format( - msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author + msg, + channel=ctx.channel, + recipient=ctx.thread.recipient, + author=ctx.message.author, ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): - await ctx.thread.reply(ctx.message) + await ctx.thread.reply(ctx.message, msg) @commands.command(aliases=["formatanonreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1413,11 +1502,15 @@ async def fareply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ msg = self.bot.formatter.format( - msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author + msg, + channel=ctx.channel, + recipient=ctx.thread.recipient, + author=ctx.message.author, ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): - await ctx.thread.reply(ctx.message, anonymous=True) + await ctx.thread.reply(ctx.message, msg, anonymous=True) @commands.command(aliases=["formatplainreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1435,11 +1528,15 @@ async def fpreply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ msg = self.bot.formatter.format( - msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author + msg, + channel=ctx.channel, + recipient=ctx.thread.recipient, + author=ctx.message.author, ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): - await ctx.thread.reply(ctx.message, plain=True) + await ctx.thread.reply(ctx.message, msg, plain=True) @commands.command(aliases=["formatplainanonreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1457,11 +1554,15 @@ async def fpareply(self, ctx, *, msg: str = ""): automatically embedding image URLs. """ msg = self.bot.formatter.format( - msg, channel=ctx.channel, recipient=ctx.thread.recipient, author=ctx.message.author + msg, + channel=ctx.channel, + recipient=ctx.thread.recipient, + author=ctx.message.author, ) + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): - await ctx.thread.reply(ctx.message, anonymous=True, plain=True) + await ctx.thread.reply(ctx.message, msg, anonymous=True, plain=True) @commands.command(aliases=["anonreply", "anonymousreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1476,9 +1577,10 @@ async def areply(self, ctx, *, msg: str = ""): Edit the `anon_username`, `anon_avatar_url` and `anon_tag` config variables to do so. """ + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): - await ctx.thread.reply(ctx.message, anonymous=True) + await ctx.thread.reply(ctx.message, msg, anonymous=True) @commands.command(aliases=["plainreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1490,9 +1592,10 @@ async def preply(self, ctx, *, msg: str = ""): Supports attachments and images as well as automatically embedding image URLs. """ + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): - await ctx.thread.reply(ctx.message, plain=True) + await ctx.thread.reply(ctx.message, msg, plain=True) @commands.command(aliases=["plainanonreply", "plainanonymousreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1504,9 +1607,10 @@ async def pareply(self, ctx, *, msg: str = ""): Supports attachments and images as well as automatically embedding image URLs. """ + # Ensure logs record only the reply text, not the command. ctx.message.content = msg async with safe_typing(ctx): - await ctx.thread.reply(ctx.message, anonymous=True, plain=True) + await ctx.thread.reply(ctx.message, msg, anonymous=True, plain=True) @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1521,6 +1625,13 @@ async def note(self, ctx, *, msg: str = ""): async with safe_typing(ctx): msg = await ctx.thread.note(ctx.message) await msg.pin() + # Acknowledge and clean up the invoking command message + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + try: + await ctx.message.delete(delay=3) + except (discord.Forbidden, discord.NotFound): + pass @note.command(name="persistent", aliases=["persist"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1534,6 +1645,13 @@ async def note_persistent(self, ctx, *, msg: str = ""): msg = await ctx.thread.note(ctx.message, persistent=True) await msg.pin() await self.bot.api.create_note(recipient=ctx.thread.recipient, message=ctx.message, message_id=msg.id) + # Acknowledge and clean up the invoking command message + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + try: + await ctx.message.delete(delay=3) + except (discord.Forbidden, discord.NotFound) as e: + logger.debug(f"Failed to delete note command message: {e}") @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) @@ -1567,6 +1685,29 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): @checks.has_permissions(PermissionLevel.REGULAR) async def selfcontact(self, ctx): """Creates a thread with yourself""" + # Check if user already has a thread + existing_thread = await self.bot.threads.find(recipient=ctx.author) + if existing_thread: + if existing_thread.snoozed: + # Unsnooze the thread + msg = await ctx.send("ℹ️ You had a snoozed thread. Unsnoozing now...") + await existing_thread.restore_from_snooze() + self.bot.threads.cache[existing_thread.id] = existing_thread + try: + await msg.delete(delay=10) + except (discord.Forbidden, discord.NotFound): + pass + return + else: + # Thread already exists and is active + embed = discord.Embed( + title="Thread not created", + description=f"A thread for you already exists in {existing_thread.channel.mention}.", + color=self.bot.error_color, + ) + await ctx.send(embed=embed, delete_after=10) + return + await ctx.invoke(self.contact, users=[ctx.author]) @commands.command(usage="<user> [category] [options]") @@ -1575,7 +1716,12 @@ async def contact( self, ctx, users: commands.Greedy[ - Union[Literal["silent", "silently"], discord.Member, discord.User, discord.Role] + Union[ + Literal["silent", "silently"], + discord.Member, + discord.User, + discord.Role, + ] ], *, category: SimilarCategoryConverter = None, @@ -1625,9 +1771,14 @@ async def contact( users += u.members users.remove(u) + snoozed_users = [] for u in list(users): exists = await self.bot.threads.find(recipient=u) if exists: + # Check if thread is snoozed + if exists.snoozed: + snoozed_users.append(u) + continue errors.append(f"A thread for {u} already exists.") if exists.channel: errors[-1] += f" in {exists.channel.mention}" @@ -1641,6 +1792,21 @@ async def contact( errors.append(f"{ref} currently blocked from contacting {self.bot.user.name}.") users.remove(u) + # Handle snoozed users - unsnooze them and return early + if snoozed_users: + for u in snoozed_users: + thread = await self.bot.threads.find(recipient=u) + if thread and thread.snoozed: + msg = await ctx.send(f"ℹ️ {u.mention} had a snoozed thread. Unsnoozing now...") + await thread.restore_from_snooze() + self.bot.threads.cache[thread.id] = thread + try: + await msg.delete(delay=10) + except (discord.Forbidden, discord.NotFound) as e: + logger.debug(f"Failed to delete message (likely already deleted or lacking permissions): {e}") + # Don't try to create a new thread - we just unsnoozed existing ones + return + if len(users) > 5: errors.append("Group conversations only support 5 users.") users = [] @@ -1653,11 +1819,14 @@ async def contact( title = None if manual_trigger: # not react to contact - embed = discord.Embed(title=title, color=self.bot.error_color, description="\n".join(errors)) + embed = discord.Embed( + title=title, + color=self.bot.error_color, + description="\n".join(errors), + ) await ctx.send(embed=embed, delete_after=10) if not users: - # end return creator = ctx.author if manual_trigger else users[0] @@ -1673,7 +1842,10 @@ async def contact( if thread.cancelled: return - if self.bot.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): + if self.bot.config["dm_disabled"] in ( + DMDisabled.NEW_THREADS, + DMDisabled.ALL_THREADS, + ): logger.info("Contacting user %s when Modmail DM is disabled.", users[0]) if not silent and not self.bot.config.get("thread_contact_silently"): @@ -1713,8 +1885,10 @@ async def contact( if manual_trigger: sent_emoji, _ = await self.bot.retrieve_emoji() await self.bot.add_reaction(ctx.message, sent_emoji) - await asyncio.sleep(5) - await ctx.message.delete() + try: + await ctx.message.delete(delay=5) + except (discord.Forbidden, discord.NotFound): + pass @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) @@ -2015,7 +2189,9 @@ async def unblock(self, ctx, *, user_or_role: Union[User, Role] = 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) @@ -2155,7 +2331,9 @@ async def repair(self, ctx): 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}" + reason="Fix broken Modmail thread", + name=name, + topic=f"User ID: {user.id}", ) return await self.bot.add_reaction(ctx.message, sent_emoji) @@ -2267,31 +2445,129 @@ async def isenable(self, ctx): @checks.thread_only() async def snooze(self, ctx, *, duration: UserFriendlyTime = None): """ - Snooze this thread: deletes the channel, keeps the ticket open in DM, and restores it when the user replies or a moderator unsnoozes it. - Optionally specify a duration, e.g. 'snooze 2d' for 2 days. - Uses config: max_snooze_time, snooze_title, snooze_text + Snooze this thread. Behavior depends on config: + - delete (default): deletes the channel and restores it later + - move: moves the channel to the configured snoozed category + Optionally specify a duration, e.g. 'snooze 2d' for 2 days. + Uses config: snooze_default_duration, snooze_title, snooze_text """ thread = ctx.thread if thread.snoozed: await ctx.send("This thread is already snoozed.") logging.info(f"[SNOOZE] Thread for {getattr(thread.recipient, 'id', None)} already snoozed.") return - max_snooze = self.bot.config.get("max_snooze_time") - if max_snooze is None: - max_snooze = 604800 - max_snooze = int(max_snooze) + # Default snooze duration with safe fallback + try: + default_snooze = int(self.bot.config.get("snooze_default_duration", 604800)) + except (ValueError, TypeError): + default_snooze = 604800 if duration: snooze_for = int((duration.dt - duration.now).total_seconds()) - if snooze_for > max_snooze: - snooze_for = max_snooze + snooze_for = min(snooze_for, default_snooze) else: - snooze_for = max_snooze + snooze_for = default_snooze + + # Capacity pre-check: if behavior is move, ensure snoozed category has room (<49 channels) + behavior = (self.bot.config.get("snooze_behavior") or "delete").lower() + if behavior == "move": + snoozed_cat_id = self.bot.config.get("snoozed_category_id") + target_category = None + if snoozed_cat_id: + try: + target_category = self.bot.modmail_guild.get_channel(int(snoozed_cat_id)) + except Exception: + target_category = None + # Auto-create snoozed category if missing + if not isinstance(target_category, discord.CategoryChannel): + try: + logging.info("Auto-creating snoozed category for move-based snoozing.") + # Hide category by default; only bot can view/manage + overwrites = { + self.bot.modmail_guild.default_role: discord.PermissionOverwrite(view_channel=False) + } + bot_member = self.bot.modmail_guild.me + if bot_member is not None: + overwrites[bot_member] = discord.PermissionOverwrite( + view_channel=True, + send_messages=True, + read_message_history=True, + manage_channels=True, + manage_messages=True, + attach_files=True, + embed_links=True, + add_reactions=True, + ) + target_category = await self.bot.modmail_guild.create_category( + name="Snoozed Threads", + overwrites=overwrites, + reason="Auto-created snoozed category for move-based snoozing", + ) + try: + await self.bot.config.set("snoozed_category_id", target_category.id) + await self.bot.config.update() + except Exception as e: + logging.warning("Failed to persist snoozed_category_id: %s", e) + try: + await ctx.send( + "⚠️ Created snoozed category but failed to save it to config. Please set `snoozed_category_id` manually." + ) + except Exception as e: + logging.info( + "Failed to notify about snoozed category persistence issue: %s", + e, + ) + await ctx.send( + embed=discord.Embed( + title="Snoozed category created", + description=( + f"Created category {target_category.mention if hasattr(target_category, 'mention') else target_category.name} " + "and set it as `snoozed_category_id`." + ), + color=self.bot.main_color, + ) + ) + except Exception as e: + await ctx.send( + embed=discord.Embed( + title="Could not create snoozed category", + description=( + "I couldn't create a category automatically. Please ensure I have Manage Channels " + "permission, or set `snoozed_category_id` manually." + ), + color=self.bot.error_color, + ) + ) + logging.warning("Failed to auto-create snoozed category: %s", e) + # Capacity check after ensuring category exists + if isinstance(target_category, discord.CategoryChannel): + try: + if len(target_category.channels) >= 49: + await ctx.send( + embed=discord.Embed( + title="Snooze unavailable", + description=( + "The configured snoozed category is full (49 channels). " + "Unsnooze or move some channels out before snoozing more." + ), + color=self.bot.error_color, + ) + ) + return + except Exception as e: + logging.debug("Failed to check snoozed category channel count: %s", e) - # Storing snooze_start and snooze_for in the log entry + # Store snooze_until timestamp for reliable auto-unsnooze now = datetime.now(timezone.utc) + snooze_until = now + timedelta(seconds=snooze_for) await self.bot.api.logs.update_one( {"recipient.id": str(thread.id)}, - {"$set": {"snooze_start": now.isoformat(), "snooze_for": snooze_for}}, + { + "$set": { + "snooze_start": now.isoformat(), + "snooze_for": snooze_for, + "snooze_until": snooze_until.isoformat(), + } + }, ) embed = discord.Embed( title=self.bot.config.get("snooze_title") or "Thread Snoozed", @@ -2327,6 +2603,11 @@ async def unsnooze(self, ctx, *, user: str = None): try: user_obj = await self.bot.get_or_fetch_user(user_id) except Exception: + logger.debug( + "Failed fetching user during unsnooze; falling back to partial object (%s).", + user_id, + exc_info=True, + ) user_obj = discord.Object(user_id) if user_obj: thread = await self.bot.threads.find(recipient=user_obj) @@ -2411,32 +2692,30 @@ async def snoozed(self, ctx): await ctx.send("Snoozed threads:\n" + "\n".join(lines)) async def cog_load(self): - self.bot.loop.create_task(self.snooze_auto_unsnooze_task()) + self.snooze_auto_unsnooze.start() - async def snooze_auto_unsnooze_task(self): + @tasks.loop(seconds=10) + async def snooze_auto_unsnooze(self): + now = datetime.now(timezone.utc) + snoozed = await self.bot.api.logs.find({"snoozed": True}).to_list(None) + for entry in snoozed: + snooze_until = entry.get("snooze_until") + if snooze_until: + try: + until_dt = datetime.fromisoformat(snooze_until) + if now >= until_dt: + thread = await self.bot.threads.find(recipient_id=int(entry["recipient"]["id"])) + if thread and thread.snoozed: + await thread.restore_from_snooze() + except (ValueError, TypeError) as e: + logger.debug( + "Failed parsing snooze_until timestamp for auto-unsnooze loop: %s", + e, + ) + + @snooze_auto_unsnooze.before_loop + async def _snooze_auto_unsnooze_before(self): await self.bot.wait_until_ready() - while True: - now = datetime.now(timezone.utc) - snoozed = await self.bot.api.logs.find({"snoozed": True}).to_list(None) - for entry in snoozed: - start = entry.get("snooze_start") - snooze_for = entry.get("snooze_for") - if not start: - continue - start_dt = datetime.fromisoformat(start) - if snooze_for is not None: - duration = int(snooze_for) - else: - max_snooze = self.bot.config.get("max_snooze_time") - if max_snooze is None: - max_snooze = 604800 - duration = int(max_snooze) - if (now - start_dt).total_seconds() > duration: - # Auto-unsnooze - thread = await self.bot.threads.find(recipient_id=int(entry["recipient"]["id"])) - if thread and thread.snoozed: - await thread.restore_from_snooze() - await asyncio.sleep(60) async def process_dm_modmail(self, message: discord.Message) -> None: # ... existing code ... diff --git a/cogs/plugins.py b/cogs/plugins.py index c7dceb7283..aa4ad5a65c 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -251,7 +251,11 @@ 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) + logger.error( + "Failed to download requirements for %s.", + plugin.ext_string, + exc_info=True, + ) raise InvalidPluginError(f"Unable to download requirements: ```\n{stderr.decode()}\n```") if os.path.exists(USER_SITE): @@ -361,7 +365,10 @@ async def plugins_add(self, ctx, *, plugin_name: str): return if str(plugin) in self.bot.config["plugins"]: - embed = discord.Embed(description="This plugin is already installed.", color=self.bot.error_color) + embed = discord.Embed( + description="This plugin is already installed.", + color=self.bot.error_color, + ) return await ctx.send(embed=embed) if plugin.name in self.bot.cogs: @@ -470,7 +477,8 @@ 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) @@ -486,7 +494,8 @@ async def update_plugin(self, ctx, plugin_name): async with safe_typing(ctx): 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, ) await self.download_plugin(plugin, force=True) if self.bot.config.get("enable_plugins"): @@ -570,7 +579,8 @@ async def plugins_reset(self, ctx): logger.warning("Removing %s.", entry.name) embed = discord.Embed( - description="Successfully purged all plugins from the bot.", color=self.bot.main_color + description="Successfully purged all plugins from the bot.", + color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -598,7 +608,8 @@ 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) @@ -666,7 +677,10 @@ async def plugins_registry(self, ctx, *, plugin_name: typing.Union[int, str] = N matches = get_close_matches(plugin_name, self.registry.keys()) if matches: - embed.add_field(name="Perhaps you meant:", value="\n".join(f"`{m}`" for m in matches)) + embed.add_field( + name="Perhaps you meant:", + value="\n".join(f"`{m}`" for m in matches), + ) return await ctx.send(embed=embed) diff --git a/cogs/threadmenu.py b/cogs/threadmenu.py new file mode 100644 index 0000000000..7f9e193844 --- /dev/null +++ b/cogs/threadmenu.py @@ -0,0 +1,830 @@ +import json +import asyncio +from copy import copy as _copy + +import discord +from discord.ext import commands + +from core import checks +from core.models import PermissionLevel + + +class ThreadCreationMenuCore(commands.Cog): + """Core-integrated thread menu configuration and management. + + This Cog exposes the same commands as the legacy plugin to manage menu options, + but stores settings in core config (no plugin DB). + """ + + def __init__(self, bot): + self.bot = bot + + # ----- helpers ----- + def _get_conf(self) -> dict: + return { + "enabled": bool(self.bot.config.get("thread_creation_menu_enabled")), + "options": self.bot.config.get("thread_creation_menu_options") or {}, + "submenus": self.bot.config.get("thread_creation_menu_submenus") or {}, + "timeout": int(self.bot.config.get("thread_creation_menu_timeout") or 20), + "close_on_timeout": bool(self.bot.config.get("thread_creation_menu_close_on_timeout")), + "anonymous_menu": bool(self.bot.config.get("thread_creation_menu_anonymous_menu")), + "embed_text": self.bot.config.get("thread_creation_menu_embed_text") + or "Please select an option.", + "dropdown_placeholder": self.bot.config.get("thread_creation_menu_dropdown_placeholder") + or "Select an option to contact the staff team.", + "embed_title": self.bot.config.get("thread_creation_menu_embed_title"), + "embed_footer": self.bot.config.get("thread_creation_menu_embed_footer"), + "embed_thumbnail_url": self.bot.config.get("thread_creation_menu_embed_thumbnail_url"), + "embed_footer_icon_url": self.bot.config.get("thread_creation_menu_embed_footer_icon_url"), + "embed_color": self.bot.config.get("thread_creation_menu_embed_color"), + } + + async def _save_conf(self, conf: dict): + await self.bot.config.set("thread_creation_menu_enabled", conf.get("enabled", False)) + await self.bot.config.set("thread_creation_menu_options", conf.get("options", {}), convert=False) + await self.bot.config.set("thread_creation_menu_submenus", conf.get("submenus", {}), convert=False) + await self.bot.config.set("thread_creation_menu_timeout", conf.get("timeout", 20)) + await self.bot.config.set( + "thread_creation_menu_close_on_timeout", conf.get("close_on_timeout", False) + ) + await self.bot.config.set("thread_creation_menu_anonymous_menu", conf.get("anonymous_menu", False)) + await self.bot.config.set( + "thread_creation_menu_embed_text", conf.get("embed_text", "Please select an option.") + ) + await self.bot.config.set( + "thread_creation_menu_dropdown_placeholder", + conf.get("dropdown_placeholder", "Select an option to contact the staff team."), + ) + await self.bot.config.set("thread_creation_menu_embed_title", conf.get("embed_title")) + await self.bot.config.set("thread_creation_menu_embed_footer", conf.get("embed_footer")) + await self.bot.config.set("thread_creation_menu_embed_thumbnail_url", conf.get("embed_thumbnail_url")) + await self.bot.config.set( + "thread_creation_menu_embed_footer_icon_url", conf.get("embed_footer_icon_url") + ) + if conf.get("embed_color"): + try: + await self.bot.config.set("thread_creation_menu_embed_color", conf.get("embed_color")) + except Exception: + pass + await self.bot.config.update() + + # ----- commands ----- + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @commands.group(invoke_without_command=True) + async def threadmenu(self, ctx): + """Thread-creation menu settings (core).""" + await ctx.send_help(ctx.command) + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu.command(name="toggle") + async def threadmenu_toggle(self, ctx): + """Enable or disable the thread-creation menu. + + Toggles the global on/off state. When disabled, users won't see + or be able to use the interactive thread creation select menu. + """ + conf = self._get_conf() + conf["enabled"] = not conf["enabled"] + await self._save_conf(conf) + await ctx.send(f"Thread-creation menu is now {'enabled' if conf['enabled'] else 'disabled'}.") + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu.command(name="show") + async def threadmenu_show(self, ctx): + """Show all current main-menu options. + + Lists every option (label + description) configured in the root + (non-submenu) select menu so you can review what users will see. + """ + conf = self._get_conf() + if not conf["options"]: + return await ctx.send("There are no options in the main menu.") + embed = discord.Embed(title="Main menu", color=discord.Color.blurple()) + for v in conf["options"].values(): + embed.add_field(name=v["label"], value=v["description"], inline=False) + await ctx.send(embed=embed) + + # ----- options ----- + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu.group(name="option", invoke_without_command=True) + async def threadmenu_option(self, ctx): + """Manage main-menu options (add/remove/edit/show). + + Use subcommands: + - add: interactive wizard to create an option + - remove <label>: delete an option + - edit <label>: interactively modify an option + - show <label>: display full details (type, command/submenu, emoji) + """ + await ctx.send_help(ctx.command) + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_option.command(name="show") + async def threadmenu_option_show(self, ctx, *, label: str): + """Show detailed information about a main-menu option.""" + conf = self._get_conf() + key = label.lower().replace(" ", "_") + if key not in conf["options"]: + return await ctx.send("That label does not exist.") + v = conf["options"][key] + embed = discord.Embed(title=v["label"], color=discord.Color.blurple()) + embed.add_field(name="Description", value=v["description"], inline=False) + embed.add_field(name="Emoji", value=v["emoji"], inline=False) + embed.add_field(name="Type", value=v["type"], inline=False) + embed.add_field( + name=("Command" if v["type"] == "command" else "Submenu"), value=v["callback"], inline=False + ) + # Show category if set + cat_id = v.get("category_id") + if cat_id: + guild = self.bot.modmail_guild or ctx.guild + category = guild and guild.get_channel(cat_id) + cat_name = getattr(category, "name", "Unknown/Deleted") + embed.add_field(name="Category", value=f"{cat_name} (ID: {cat_id})", inline=False) + else: + embed.add_field(name="Category", value="Default (main category)", inline=False) + await ctx.send(embed=embed) + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_option.command(name="add") + async def threadmenu_option_add(self, ctx): + """Interactive wizard to add a main-menu option.""" + conf = self._get_conf() + + def check(m): + return m.author == ctx.author and m.channel == ctx.channel + + def typecheck(m): + return ( + m.author == ctx.author + and m.channel == ctx.channel + and m.content.lower() + in [ + "command", + "submenu", + ] + ) + + if len(conf["options"]) >= 25: + return await ctx.send("You can only have a maximum of 25 options due to discord limitations.") + + await ctx.send( + "You can type `skip` for non-required steps or `cancel` to cancel the process at any time." + ) + await ctx.send("What is the label of the option?") + label = (await self.bot.wait_for("message", check=check)).content + sanitized_label = label.lower().replace(" ", "_") + + if label.lower() == "cancel": + return await ctx.send("Cancelled.") + + if sanitized_label in conf["options"]: + await ctx.send("That option already exists. Use `threadmenu edit` to edit it.") + return + + await ctx.send("What is the description of the option? (not required)") + description = (await self.bot.wait_for("message", check=check)).content + + if description.lower() == "cancel": + return await ctx.send("Cancelled.") + + if len(description) > 100: + return await ctx.send( + "The description must be less than 100 characters due to discord limitations." + ) + + if description.lower() == "skip": + description = None + + await ctx.send("What is the emoji of the option? (not required)") + emoji = (await self.bot.wait_for("message", check=check)).content + + if emoji.lower() == "cancel": + return await ctx.send("Cancelled.") + + if emoji.lower() == "skip": + emoji = None + + await ctx.send("What is the type of the option? (command/submenu)") + type_ = (await self.bot.wait_for("message", check=typecheck)).content.lower() + + if type_ == "cancel": + return await ctx.send("Cancelled.") + + if type_ == "command": + await ctx.send("What is the command to run for the option?") + else: + await ctx.send("What is the label of the submenu for the option?") + callback = (await self.bot.wait_for("message", check=check)).content + if type_ != "command": + callback = callback.lower().replace(" ", "_") + + if callback.lower() == "cancel": + return await ctx.send("Cancelled.") + + if type_ == "submenu" and callback not in conf["submenus"]: + return await ctx.send("That submenu does not exist. Use `threadmenu submenu create` to add it.") + + # Optional: category where the thread should be created when this option is chosen + await ctx.send( + "Optionally provide a category for threads created via this option (mention, ID, or name).\n" + "Type `default` to use the main category." + ) + category_msg = await self.bot.wait_for("message", check=check) + category_raw = category_msg.content.strip() + category_id: int | None = None + if category_raw.lower() == "cancel": + return await ctx.send("Cancelled.") + if category_raw.lower() not in {"", "default", "none", "skip"}: + guild = self.bot.modmail_guild or ctx.guild + resolved = None + try: + # Try ID + if category_raw.isdigit(): + resolved = guild.get_channel(int(category_raw)) if guild else None + # Try mention <#id> is not valid for categories; fall back to name search + if not resolved and guild: + resolved = discord.utils.find( + lambda c: isinstance(c, discord.CategoryChannel) + and c.name.lower() == category_raw.lower(), + guild.categories, + ) + except Exception: + resolved = None + if isinstance(resolved, discord.CategoryChannel): + category_id = resolved.id + else: + await ctx.send( + "Couldn't resolve that category. I'll default to the main category for this option." + ) + + conf["options"][sanitized_label] = { + "label": label, + "description": description, + "emoji": emoji, + "type": type_, + "callback": callback, + "category_id": category_id, + } + await self._save_conf(conf) + await ctx.send("Option added.") + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_option.command(name="remove") + async def threadmenu_option_remove(self, ctx, *, label: str): + """Remove a main-menu option by label.""" + conf = self._get_conf() + key = label.lower().replace(" ", "_") + if key not in conf["options"]: + return await ctx.send("That option does not exist.") + del conf["options"][key] + await self._save_conf(conf) + await ctx.send("Option removed.") + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_option.command(name="edit") + async def threadmenu_option_edit(self, ctx, *, label: str): + """Interactive wizard to edit a main-menu option.""" + conf = self._get_conf() + key = label.lower().replace(" ", "_") + if key not in conf["options"]: + return await ctx.send("That option does not exist.") + + def check(m): + return m.author == ctx.author and m.channel == ctx.channel + + def typecheck(m): + return ( + m.author == ctx.author + and m.channel == ctx.channel + and m.content.lower() + in [ + "command", + "submenu", + ] + ) + + await ctx.send( + "You can type `skip` for non-required steps (uses previous value) or `cancel` to cancel the process at any time." + "Use `none` to clear the value for non-required steps." + ) + await ctx.send("What is the new description of the option? (not required)") + description = (await self.bot.wait_for("message", check=check)).content + if description.lower() == "cancel": + return await ctx.send("Cancelled.") + + old_description = conf["options"][key]["description"] + + if description.lower() == "skip": + description = old_description + elif description.lower() == "none": + description = None + else: + if len(description) > 100: + return await ctx.send( + "The description must be less than 100 characters due to discord limitations." + ) + + await ctx.send("What is the new emoji of the option?") + emoji = (await self.bot.wait_for("message", check=check)).content + if emoji.lower() == "cancel": + return await ctx.send("Cancelled.") + + old_emoji = conf["options"][key].get("emoji") + + if emoji.lower() == "skip": + emoji = old_emoji + elif emoji.lower() == "none": + emoji = None + + await ctx.send("What is the new type of the option? (command/submenu)") + type_ = (await self.bot.wait_for("message", check=typecheck)).content.lower() + if type_ == "cancel": + return await ctx.send("Cancelled.") + + if type_ == "command": + await ctx.send("What is the new command to run for the option?") + else: + await ctx.send("What is the new label of the new submenu for the option?") + callback = (await self.bot.wait_for("message", check=check)).content + if type_ != "command": + callback = callback.lower().replace(" ", "_") + if callback.lower() == "cancel": + return await ctx.send("Cancelled.") + if type_ == "submenu" and callback not in conf["submenus"]: + return await ctx.send("That submenu does not exist. Use `threadmenu submenu create` to add it.") + + # Category edit (optional) + await ctx.send( + "Optionally provide a new category for this option (mention, ID, or name).\n" + "Send `skip` to keep current setting; send `default` or `none` to clear." + ) + cat_msg = await self.bot.wait_for("message", check=check) + cat_raw = cat_msg.content.strip() + if cat_raw.lower() == "cancel": + return await ctx.send("Cancelled.") + + current = conf["options"][key].get("category_id") + new_category_id = current + if cat_raw.lower() in {"default", "none"}: + new_category_id = None + elif cat_raw.lower() in {"", "skip"}: + new_category_id = current + else: + guild = self.bot.modmail_guild or ctx.guild + resolved = None + try: + if cat_raw.isdigit(): + resolved = guild.get_channel(int(cat_raw)) if guild else None + if not resolved and guild: + resolved = discord.utils.find( + lambda c: isinstance(c, discord.CategoryChannel) + and c.name.lower() == cat_raw.lower(), + guild.categories, + ) + except Exception: + resolved = None + if isinstance(resolved, discord.CategoryChannel): + new_category_id = resolved.id + else: + await ctx.send("Couldn't resolve that category. Keeping previous setting.") + + old_label = conf["options"][key]["label"] + conf["options"][key] = { + "label": old_label, + "description": description, + "emoji": emoji, + "type": type_, + "callback": callback, + "category_id": new_category_id, + } + await self._save_conf(conf) + await ctx.send("Option edited.") + + # ----- submenus ----- + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu.group(name="submenu", invoke_without_command=True) + async def threadmenu_submenu(self, ctx): + """Manage submenus (create/delete/list/show and options within). + + Submenus let you branch the initial select menu into additional + categorized option groups. Use `submenu option` subcommands to + manage the nested options. + """ + await ctx.send_help(ctx.command) + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_submenu.command(name="create") + async def threadmenu_submenu_create(self, ctx, *, label: str): + """Create an empty submenu that can hold nested options.""" + conf = self._get_conf() + key = label.lower().replace(" ", "_") + if key in conf["submenus"]: + return await ctx.send( + "That submenu already exists. Please use a unique label or use `threadmenu submenu delete` to delete it." + ) + conf["submenus"][key] = {} + await self._save_conf(conf) + await ctx.send("Submenu created.") + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_submenu.command(name="delete") + async def threadmenu_submenu_delete(self, ctx, *, label: str): + """Delete a submenu and all its options.""" + conf = self._get_conf() + key = label.lower().replace(" ", "_") + if key not in conf["submenus"]: + return await ctx.send("That submenu does not exist.") + del conf["submenus"][key] + await self._save_conf(conf) + await ctx.send("Submenu deleted.") + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_submenu.command(name="list") + async def threadmenu_submenu_list(self, ctx): + """List all submenu keys currently configured.""" + conf = self._get_conf() + if not conf["submenus"]: + return await ctx.send("There are no submenus.") + submenu_list = "Submenus:\n" + ("\n".join(conf["submenus"].keys())) + if len(submenu_list) > 2000: + submenu_list = submenu_list[:1997] + "..." + await ctx.send(submenu_list) + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_submenu.command(name="show") + async def threadmenu_submenu_show(self, ctx, *, label: str): + """Show the options configured inside a submenu.""" + conf = self._get_conf() + key = label.lower().replace(" ", "_") + if key not in conf["submenus"]: + return await ctx.send("That submenu does not exist. Use `threadmenu submenu create` to add it.") + if not conf["submenus"][key]: + return await ctx.send(f"There are no options in {key}") + embed = discord.Embed(title=key, color=discord.Color.blurple()) + for v in conf["submenus"][key].values(): + embed.add_field(name=v["label"], value=v["description"], inline=False) + await ctx.send(embed=embed) + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_submenu.group(name="option", invoke_without_command=True) + async def threadmenu_submenu_option(self, ctx): + """Manage options within a specific submenu (add/remove/edit).""" + await ctx.send_help(ctx.command) + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_submenu_option.command(name="add") + async def threadmenu_submenu_option_add(self, ctx, *, submenu: str): + """Interactive wizard to add an option inside a submenu.""" + conf = self._get_conf() + submenu = submenu.lower().replace(" ", "_") + if submenu not in conf["submenus"]: + return await ctx.send("That submenu does not exist.") + + def check(m): + return m.author == ctx.author and m.channel == ctx.channel + + def typecheck(m): + return ( + m.author == ctx.author + and m.channel == ctx.channel + and m.content.lower() + in [ + "command", + "submenu", + ] + ) + + if len(conf["submenus"][submenu]) >= 24: + return await ctx.send("You can only have a maximum of 24 options due to discord limitations.") + + await ctx.send( + "You can type `skip` for non-required steps or `cancel` to cancel the process at any time." + ) + await ctx.send("What is the label of the option?") + label = (await self.bot.wait_for("message", check=check)).content + sanitized_label = label.lower().replace(" ", "_") + + if label.lower() == "cancel": + return await ctx.send("Cancelled.") + if label.lower() == "main menu": + return await ctx.send("You cannot use that label.") + if sanitized_label in conf["submenus"][submenu]: + await ctx.send("That option already exists. Use `threadmenu submenu edit` to edit it.") + return + + await ctx.send("What is the description of the option? (not required)") + description = (await self.bot.wait_for("message", check=check)).content + if description.lower() == "cancel": + return await ctx.send("Cancelled.") + if len(description) > 100: + return await ctx.send( + "The description must be less than 100 characters due to discord limitations." + ) + + if description.lower() == "skip": + description = None + + await ctx.send("What is the emoji of the option? (not required)") + emoji = (await self.bot.wait_for("message", check=check)).content + if emoji.lower() == "cancel": + return await ctx.send("Cancelled.") + + if emoji.lower() == "skip": + emoji = None + + await ctx.send("What is the type for the option? (command/submenu)") + type_ = (await self.bot.wait_for("message", check=typecheck)).content.lower() + if type_ == "cancel": + return await ctx.send("Cancelled.") + + if type_ == "command": + await ctx.send("What is the command to run for the option?") + else: + await ctx.send("What is the label of the submenu for the option?") + callback = (await self.bot.wait_for("message", check=check)).content + if type_ != "command": + callback = callback.lower().replace(" ", "_") + if type_ == "submenu" and callback not in conf["submenus"]: + return await ctx.send("That submenu does not exist. Use `threadmenu submenu create` to add it.") + + # Optional category for submenu option + await ctx.send( + "Optionally provide a category for threads created via this submenu option (mention, ID, or name).\n" + "Type `default` to use the main category." + ) + category_msg = await self.bot.wait_for("message", check=check) + category_raw = category_msg.content.strip() + category_id: int | None = None + if category_raw.lower() == "cancel": + return await ctx.send("Cancelled.") + if category_raw.lower() not in {"", "default", "none", "skip"}: + guild = self.bot.modmail_guild or ctx.guild + resolved = None + try: + if category_raw.isdigit(): + resolved = guild.get_channel(int(category_raw)) if guild else None + if not resolved and guild: + resolved = discord.utils.find( + lambda c: isinstance(c, discord.CategoryChannel) + and c.name.lower() == category_raw.lower(), + guild.categories, + ) + except Exception: + resolved = None + if isinstance(resolved, discord.CategoryChannel): + category_id = resolved.id + else: + await ctx.send( + "Couldn't resolve that category. I'll default to the main category for this submenu option." + ) + + conf["submenus"][submenu][sanitized_label] = { + "label": label, + "description": description, + "emoji": emoji, + "type": type_, + "callback": callback, + "category_id": category_id, + } + await self._save_conf(conf) + await ctx.send("Option added.") + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_submenu_option.command(name="remove") + async def threadmenu_submenu_option_remove(self, ctx, *, submenu: str): + """Remove an option from a submenu via an interactive prompt.""" + conf = self._get_conf() + submenu = submenu.lower().replace(" ", "_") + if submenu not in conf["submenus"]: + return await ctx.send("That submenu does not exist.") + + def check(m): + return m.author == ctx.author and m.channel == ctx.channel + + await ctx.send("You can send `cancel` at any time to cancel the process.") + await ctx.send("What is the label of the option to remove?") + label = (await self.bot.wait_for("message", check=check)).content + key = label.lower().replace(" ", "_") + if label.lower() == "cancel": + return await ctx.send("Cancelled.") + if key not in conf["submenus"][submenu]: + return await ctx.send("That option does not exist.") + + del conf["submenus"][submenu][key] + await self._save_conf(conf) + await ctx.send("Option removed.") + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu_submenu_option.command(name="edit") + async def threadmenu_submenu_option_edit(self, ctx, *, submenu: str): + """Interactive wizard to edit a submenu option.""" + conf = self._get_conf() + submenu = submenu.lower().replace(" ", "_") + if submenu not in conf["submenus"]: + return await ctx.send("That submenu does not exist. Use `threadmenu submenu create` to add it.") + + def check(m): + return m.author == ctx.author and m.channel == ctx.channel + + def typecheck(m): + return ( + m.author == ctx.author + and m.channel == ctx.channel + and m.content.lower() + in [ + "command", + "submenu", + ] + ) + + await ctx.send( + "You can type `skip` for non-required steps (uses previous value) or `cancel` to cancel the process at any time." + "Use `none` to clear the value for non-required steps." + ) + await ctx.send("What is the label of the option to edit?") + label = (await self.bot.wait_for("message", check=check)).content + key = label.lower().replace(" ", "_") + if label.lower() == "cancel": + return await ctx.send("Cancelled.") + if key not in conf["submenus"][submenu]: + return await ctx.send("That label does not exist.") + + await ctx.send("What is the new description of the option? (not required)") + description = (await self.bot.wait_for("message", check=check)).content + if description.lower() == "cancel": + return await ctx.send("Cancelled.") + + option_data = conf["submenus"][submenu][key] + old_description = option_data.get("description") + + if description.lower() == "skip": + description = old_description + elif description.lower() == "none": + description = None + else: + if len(description) > 100: + return await ctx.send( + "The description must be less than 100 characters due to discord limitations." + ) + + await ctx.send("What is the new emoji of the option? (not required)") + emoji = (await self.bot.wait_for("message", check=check)).content + if emoji.lower() == "cancel": + return await ctx.send("Cancelled.") + + old_emoji = option_data.get("emoji") + + if emoji.lower() == "skip": + emoji = old_emoji + elif emoji.lower() == "none": + emoji = None + + await ctx.send("What is the new type for the option? (command/submenu)") + type_ = (await self.bot.wait_for("message", check=typecheck)).content.lower() + if type_ == "cancel": + return await ctx.send("Cancelled.") + + if type_ == "command": + await ctx.send("What is the command to run for the option?") + else: + await ctx.send("What is the label of the submenu for the option?") + callback = (await self.bot.wait_for("message", check=check)).content + if type_ != "command": + callback = callback.lower().replace(" ", "_") + if callback.lower() == "cancel": + return await ctx.send("Cancelled.") + if type_ == "submenu" and callback not in conf["submenus"]: + return await ctx.send("That submenu does not exist.") + + # Category edit (optional) + await ctx.send( + "Optionally provide a new category for this submenu option (mention, ID, or name).\n" + "Send `skip` to keep current setting; send `default` or `none` to clear." + ) + cat_msg = await self.bot.wait_for("message", check=check) + cat_raw = cat_msg.content.strip() + if cat_raw.lower() == "cancel": + return await ctx.send("Cancelled.") + current = conf["submenus"][submenu][key].get("category_id") + new_category_id = current + if cat_raw.lower() in {"default", "none"}: + new_category_id = None + elif cat_raw.lower() in {"", "skip"}: + new_category_id = current + else: + guild = self.bot.modmail_guild or ctx.guild + resolved = None + try: + if cat_raw.isdigit(): + resolved = guild.get_channel(int(cat_raw)) if guild else None + if not resolved and guild: + resolved = discord.utils.find( + lambda c: isinstance(c, discord.CategoryChannel) + and c.name.lower() == cat_raw.lower(), + guild.categories, + ) + except Exception: + resolved = None + if isinstance(resolved, discord.CategoryChannel): + new_category_id = resolved.id + else: + await ctx.send("Couldn't resolve that category. Keeping previous setting.") + + conf["submenus"][submenu][key]["description"] = description + conf["submenus"][submenu][key]["emoji"] = emoji + conf["submenus"][submenu][key]["type"] = type_ + conf["submenus"][submenu][key]["callback"] = callback + conf["submenus"][submenu][key]["category_id"] = new_category_id + await self._save_conf(conf) + await ctx.send("Option edited.") + + # ----- import/export ----- + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu.command(name="dump_config") + async def threadmenu_dump_config(self, ctx): + """Dump the current core thread menu config to a file.""" + conf = self._get_conf() + with open("thread_creation_menu_config.json", "w", encoding="utf-8") as f: + json.dump(conf, f, indent=4) + await ctx.send(file=discord.File("thread_creation_menu_config.json")) + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu.command(name="reset") + async def threadmenu_reset(self, ctx): + """Reset ALL thread-creation menu settings to their defaults. + + This clears options and submenus and restores every key starting with + `thread_creation_menu_` back to the default values. Confirmation required. + This action is irreversible. + """ + warning = ( + "This will clear ALL thread menu options, submenus, and related settings and restore defaults.\n" + "This action is irreversible. Type `confirm` within 30 seconds to proceed, or anything else to cancel." + ) + await ctx.send(warning) + + def check(m: discord.Message) -> bool: + return m.author == ctx.author and m.channel == ctx.channel + + try: + reply = await self.bot.wait_for("message", check=check, timeout=30) + except asyncio.TimeoutError: + return await ctx.send("Timed out — reset cancelled.") + + if reply.content.strip().lower() != "confirm": + return await ctx.send("Reset cancelled.") + + # Reset all `thread_creation_menu_` keys to defaults + defaults = getattr(self.bot.config, "defaults", {}) + keys = [k for k in defaults.keys() if k.startswith("thread_creation_menu_")] + + # Ensure we handle mappings without unwanted conversion + for k in keys: + v = defaults[k] + if k in {"thread_creation_menu_options", "thread_creation_menu_submenus"}: + await self.bot.config.set(k, v, convert=False) + else: + await self.bot.config.set(k, v) + + # Also disable the menu explicitly for clarity + await self.bot.config.set("thread_creation_menu_enabled", False) + await self.bot.config.update() + + await ctx.send( + f"Thread-creation menu configuration has been reset to defaults (reset {len(keys)} keys)." + ) + + @checks.has_permissions(PermissionLevel.ADMINISTRATOR) + @threadmenu.command(name="load_config") + async def threadmenu_load_config(self, ctx): + """Load the thread menu config from an attached file.""" + if not ctx.message.attachments: + return await ctx.send("You must attach a json file to load the config from.") + b = await ctx.message.attachments[0].read() + json_data = b.decode("utf-8") + try: + data = json.loads(json_data) + except json.decoder.JSONDecodeError: + return await ctx.send("Invalid json file.") + + # minimal validation + required = { + "enabled", + "options", + "submenus", + "timeout", + "close_on_timeout", + "anonymous_menu", + "embed_text", + "dropdown_placeholder", + } + if not required.issubset(set(data.keys())): + return await ctx.send("Config file missing required keys.") + + await self._save_conf(data) + await ctx.send("Successfully loaded config into core.") + + +async def setup(bot): + await bot.add_cog(ThreadCreationMenuCore(bot)) diff --git a/cogs/utility.py b/cogs/utility.py index 16eeafc4d6..c420ee7979 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1,4 +1,4 @@ -from core.utils import trigger_typing, truncate, safe_typing +from core.utils import trigger_typing, truncate import asyncio import inspect import os @@ -13,6 +13,7 @@ from subprocess import PIPE from textwrap import indent from typing import Union +import typing import discord from discord.enums import ActivityType, Status @@ -31,7 +32,7 @@ UnseenFormatter, getLogger, ) -from core.utils import trigger_typing, truncate, DummyParam +from core.utils import DummyParam from core.paginator import EmbedPaginatorSession, MessagePaginatorSession @@ -118,7 +119,11 @@ 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) @@ -200,7 +205,9 @@ async def send_error_message(self, error): if snippet_aliases: embed.add_field( - name="Aliases to this snippet:", value=",".join(snippet_aliases), inline=False + name="Aliases to this snippet:", + value=",".join(snippet_aliases), + inline=False, ) return await self.get_destination().send(embed=embed) @@ -221,7 +228,10 @@ async def send_error_message(self, error): await self.context.bot.config.update() else: if len(values) == 1: - embed = discord.Embed(title=f"{command} is an alias.", color=self.context.bot.main_color) + 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( @@ -693,7 +703,9 @@ async def mention(self, ctx, *user_or_role: Union[discord.Role, discord.Member, current = self.bot.config["mention"] if not user_or_role: 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), ) elif ( len(user_or_role) == 1 @@ -810,11 +822,38 @@ async def config_set(self, ctx, key: str.lower, *, value: str): color=self.bot.main_color, description=f"Set `{key}` to `{self.bot.config[key]}`.", ) + # If turning on move-based snoozing, remind to set snoozed_category_id + if key == "snooze_behavior": + behavior = ( + str(self.bot.config.get("snooze_behavior", convert=False)).strip().lower().strip('"') + ) + if behavior == "move": + cat_id = self.bot.config.get("snoozed_category_id", convert=False) + valid = False + if cat_id: + try: + cat_obj = self.bot.modmail_guild.get_channel(int(str(cat_id))) + valid = isinstance(cat_obj, discord.CategoryChannel) + except Exception: + valid = False + if not valid: + example = f"`{self.bot.prefix}config set snoozed_category_id <category_id>`" + embed.add_field( + name="Action required", + value=( + "You set `snooze_behavior` to `move`. Please set `snoozed_category_id` " + "to the category where snoozed threads should be moved.\n" + f"For example: {example}" + ), + inline=False, + ) except InvalidConfigError as exc: 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=truncate(", ".join(valid_keys), 1024)) @@ -836,7 +875,9 @@ 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)) @@ -873,21 +914,44 @@ 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.display_avatar.url if self.bot.user.display_avatar else None, - ) - config = self.bot.config.filter_default(self.bot.config) - - for name, value in config.items(): - if name in self.bot.config.public_keys: - embed.add_field(name=name, value=f"`{value}`", inline=False) + # Build one or more embeds, each with up to 25 fields + base_desc = "Here is a list of currently set configuration variable(s)." + author_name = "Current config(s):" + icon = self.bot.user.display_avatar.url if self.bot.user.display_avatar else None - return await ctx.send(embed=embed) + config = self.bot.config.filter_default(self.bot.config) + items = [(name, value) for name, value in config.items() if name in self.bot.config.public_keys] + + embeds: list[discord.Embed] = [] + chunk: list[tuple[str, typing.Any]] = [] + for pair in items: + chunk.append(pair) + if len(chunk) == 15: + e = discord.Embed(color=self.bot.main_color, description=base_desc) + e.set_author(name=author_name, icon_url=icon) + for name, value in chunk: + e.add_field(name=name, value=f"`{value}`", inline=False) + embeds.append(e) + chunk = [] + + if chunk: + e = discord.Embed(color=self.bot.main_color, description=base_desc) + e.set_author(name=author_name, icon_url=icon) + for name, value in chunk: + e.add_field(name=name, value=f"`{value}`", inline=False) + embeds.append(e) + + # Send one or many embeds depending on count. + # In the single-key branch above, variable 'embed' exists; in this multi branch, we only use 'embeds'. + if key: + return await ctx.send(embed=embed) + else: + if not embeds: + return await ctx.send("No public configuration keys are set.") + # Use the existing paginator consistently + paginator = EmbedPaginatorSession(ctx, *embeds) + await paginator.run() + return @config.command(name="help", aliases=["info"]) @checks.has_permissions(PermissionLevel.OWNER) @@ -907,7 +971,10 @@ async def config_help(self, ctx, key: str.lower = None): description=f"`{key}` is an invalid key.", ) if closest: - embed.add_field(name="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), + ) return await ctx.send(embed=embed) config_help = self.bot.config.config_help @@ -998,7 +1065,9 @@ 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) @@ -1016,10 +1085,14 @@ 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=self.bot.get_guild_icon(guild=ctx.guild, size=128)) + embed.set_author( + name="Aliases", + icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128), + ) return await ctx.send(embed=embed) embeds = [] @@ -1028,7 +1101,8 @@ async def alias(self, ctx, *, name: str.lower = None): description = utils.format_description(i, names) embed = discord.Embed(color=self.bot.main_color, description=description) embed.set_author( - name="Command Aliases", icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128) + name="Command Aliases", + icon_url=self.bot.get_guild_icon(guild=ctx.guild, size=128), ) embeds.append(embed) @@ -1048,7 +1122,9 @@ async def alias_raw(self, ctx, *, name: str.lower): 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 + title=f'Raw alias - "{name}":', + description=f"```\n{val}```", + color=self.bot.main_color, ) return await ctx.send(embed=embed) @@ -1066,7 +1142,9 @@ async def make_alias(self, name, value, action): if len(values) > 25: embed = discord.Embed( - title="Error", description="Too many steps, max=25.", color=self.bot.error_color + title="Error", + description="Too many steps, max=25.", + color=self.bot.error_color, ) return embed @@ -1558,7 +1636,11 @@ def _get_perm(self, ctx, name, type_): @permissions.command(name="get", usage="[@user] or [command/level/override] [name]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_get( - self, ctx, user_or_role: Union[discord.Role, utils.User, str], *, name: str = None + self, + ctx, + user_or_role: Union[discord.Role, utils.User, str], + *, + name: str = None, ): """ View the currently-set permissions. @@ -2080,7 +2162,11 @@ async def update(self, ctx, *, flag: str = ""): res = res.decode("utf-8").rstrip() if err and not res: - embed = discord.Embed(title="Update failed", description=err, color=self.bot.error_color) + embed = discord.Embed( + title="Update failed", + description=err, + color=self.bot.error_color, + ) await ctx.send(embed=embed) elif res != "Already up to date.": @@ -2134,7 +2220,7 @@ async def eval_(self, ctx, *, body: str): body = utils.cleanup_code(body) stdout = StringIO() - to_compile = f'async def func():\n{indent(body, " ")}' + to_compile = f"async def func():\n{indent(body, ' ')}" def paginate(text: str): """Simple generator that paginates text.""" diff --git a/core/clients.py b/core/clients.py index 459ee2ea6b..90f09b3b48 100644 --- a/core/clients.py +++ b/core/clients.py @@ -133,7 +133,9 @@ async def request( return await self._get_response_data(resp) @staticmethod - async def _get_response_data(response: ClientResponse) -> Union[Dict[str, Any], str]: + async def _get_response_data( + response: ClientResponse, + ) -> Union[Dict[str, Any], str]: """ Internal method to convert the response data to `dict` if the data is a json object, or to `str` (raw response) if the data is not a valid json. @@ -476,7 +478,11 @@ 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.") @@ -540,7 +546,11 @@ async def find_log_entry(self, key: str) -> 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} + 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) @@ -679,7 +689,9 @@ async def append_log( } return await self.logs.find_one_and_update( - {"channel_id": channel_id}, {"$push": {"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: @@ -689,7 +701,11 @@ async def post_log(self, channel_id: Union[int, str], data: dict) -> dict: async def search_closed_by(self, user_id: Union[int, str]): return await self.logs.find( - {"guild_id": str(self.bot.guild_id), "open": False, "closer.id": str(user_id)}, + { + "guild_id": str(self.bot.guild_id), + "open": False, + "closer.id": str(user_id), + }, {"messages": {"$slice": 5}}, ).to_list(None) diff --git a/core/config.py b/core/config.py index a704443bb1..0e45b00175 100644 --- a/core/config.py +++ b/core/config.py @@ -70,6 +70,7 @@ class ConfigManager: "thread_creation_self_contact_response": "You have opened a Modmail thread.", "thread_creation_contact_response": "{creator.name} has opened a Modmail thread.", "thread_creation_title": "Thread Created", + "thread_creation_send_dm_embed": True, "thread_close_footer": "Replying will create a new thread", "thread_close_title": "Thread Closed", "thread_close_response": "{closer.mention} has closed this Modmail thread.", @@ -135,11 +136,34 @@ class ConfigManager: "thread_min_characters_response": "Your message is too short to create a thread. Please provide more details.", "thread_min_characters_footer": "Minimum {min_characters} characters required.", # --- SNOOZE FEATURE CONFIG --- - "max_snooze_time": 604800, # in seconds, default 7 days + "snooze_default_duration": 604800, # in seconds, default 7 days "snooze_title": "Thread Snoozed", "snooze_text": "This thread has been snoozed. The channel will be restored when the user replies or a moderator unsnoozes it.", "unsnooze_text": "This thread has been unsnoozed and restored.", "unsnooze_notify_channel": "thread", # Can be a channel ID or 'thread' for the thread's own channel + # snooze behavior + "snooze_behavior": "delete", # 'delete' to delete channel, 'move' to move channel to snoozed_category_id + "snoozed_category_id": None, # Category ID to move snoozed channels into when snooze_behavior == 'move' + # attachments persistence for delete-behavior snooze + "snooze_store_attachments": False, # when True, store image attachments as base64 in snooze_data + "snooze_attachment_max_bytes": 4_194_304, # 4 MiB per attachment cap to avoid Mongo 16MB limit + "unsnooze_history_limit": None, # Limit number of messages replayed when unsnoozing (None = all messages) + # --- THREAD CREATION MENU --- + "thread_creation_menu_timeout": 30, # Default interaction timeout for the thread-creation menu (in seconds) + "thread_creation_menu_close_on_timeout": False, + "thread_creation_menu_anonymous_menu": False, + "thread_creation_menu_embed_text": "Please select an option.", + "thread_creation_menu_dropdown_placeholder": "Select an option to contact the staff team.", + "thread_creation_menu_selection_log": True, # log selected option in newly created thread channel + "thread_creation_menu_precreate_channel": False, + # thread-creation menu embed customization + "thread_creation_menu_embed_title": None, + "thread_creation_menu_embed_footer": None, + "thread_creation_menu_embed_thumbnail_url": None, + "thread_creation_menu_embed_image_url": None, + "thread_creation_menu_embed_large_image": False, + "thread_creation_menu_embed_footer_icon_url": None, + "thread_creation_menu_embed_color": str(discord.Color.green()), } private_keys = { @@ -161,6 +185,10 @@ class ConfigManager: "notification_squad": {}, "subscriptions": {}, "closures": {}, + # Thread creation menu + "thread_creation_menu_enabled": False, + "thread_creation_menu_options": {}, # main menu options mapping key -> {label, description, emoji, type, callback} + "thread_creation_menu_submenus": {}, # submenu name -> submenu options (same structure as options) # misc "plugins": [], "aliases": {}, @@ -196,9 +224,23 @@ class ConfigManager: "data_collection": True, } - colors = {"mod_color", "recipient_color", "main_color", "error_color"} + colors = { + "mod_color", + "recipient_color", + "main_color", + "error_color", + "thread_creation_menu_embed_color", + } + + time_deltas = { + "account_age", + "guild_age", + "thread_auto_close", + "thread_cooldown", + "log_expiration", + } - time_deltas = {"account_age", "guild_age", "thread_auto_close", "thread_cooldown", "log_expiration"} + duration_seconds = {"snooze_default_duration"} booleans = { "use_user_id_channel_name", @@ -239,6 +281,16 @@ class ConfigManager: "use_hoisted_top_role", "enable_presence_intent", "registry_plugins_only", + # snooze + "snooze_store_attachments", + # thread creation menu booleans + "thread_creation_send_dm_embed", + "thread_creation_menu_enabled", + "thread_creation_menu_close_on_timeout", + "thread_creation_menu_anonymous_menu", + "thread_creation_menu_selection_log", + "thread_creation_menu_precreate_channel", + "thread_creation_menu_embed_large_image", } enums = { @@ -275,6 +327,7 @@ def populate_cache(self) -> dict: data.update({k.lower(): v for k, v in json.load(f).items() if k.lower() in self.all_keys}) except json.JSONDecodeError: logger.critical("Failed to load config.json env values.", exc_info=True) + self._cache = data config_help_json = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config_help.json") @@ -360,6 +413,14 @@ def get(self, key: str, *, convert: bool = True) -> typing.Any: logger.warning("Invalid %s %s.", key, value) value = self.remove(key) + elif key in self.duration_seconds: + if not isinstance(value, int): + try: + value = int(value) + except (ValueError, TypeError): + logger.warning("Invalid %s %s.", key, value) + value = self.remove(key) + elif key in self.force_str: # Temporary: as we saved in int previously, leading to int32 overflow, # this is transitioning IDs to strings @@ -388,6 +449,10 @@ async def set(self, key: str, item: typing.Any, convert=True) -> None: if not convert: return self.__setitem__(key, item) + if "channel" in key or "category" in key: + if isinstance(item, str) and item not in {"thread", "NONE"}: + item = item.strip("<#>") + if key in self.colors: try: hex_ = str(item) @@ -440,6 +505,25 @@ async def set(self, key: str, item: typing.Any, convert=True) -> None: except ValueError: raise InvalidConfigError("Must be a yes/no value.") + elif key in self.duration_seconds: + if isinstance(item, int): + return self.__setitem__(key, item) + try: + converter = UserFriendlyTime() + time = await converter.convert(None, str(item), now=discord.utils.utcnow()) + if time.arg: + raise ValueError + except BadArgument as exc: + raise InvalidConfigError(*exc.args) + except Exception as e: + logger.debug(e) + raise InvalidConfigError( + "Unrecognized time, please use a duration like '5 days' or '2 hours'." + ) + now = discord.utils.utcnow() + duration_seconds = int((time.dt - now).total_seconds()) + return self.__setitem__(key, duration_seconds) + elif key in self.enums: if isinstance(item, self.enums[key]): # value is an enum type diff --git a/core/config_help.json b/core/config_help.json index 462e4e1c03..b5832935c7 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -492,6 +492,18 @@ "See also: `thread_creation_response`, `thread_creation_footer`, `thread_close_title`." ] }, + "thread_creation_send_dm_embed": { + "default": "Yes", + "description": "When set to `yes`, the bot will send an embed message to the recipient notifying them that a new thread was created. Set to `no` to suppress the initial DM embed. When suppressed and `recipient_thread_close` is enabled, the close emoji will be added to the user's original message instead so they may still close the thread.", + "examples": [ + "`{prefix}config set thread_creation_send_dm_embed no`", + "`{prefix}config set thread_creation_send_dm_embed yes`" + ], + "notes": [ + "Useful if you prefer not to send an extra DM to recipients when creating threads.", + "See also: `recipient_thread_close`, `thread_creation_response`, `thread_creation_title`." + ] + }, "thread_close_footer": { "default": "\"Replying will create a new thread\"", "description": "This is the message embed footer sent to the recipient upon the closure of a thread.", @@ -1266,15 +1278,16 @@ "See also: `thread_min_characters`, `thread_min_characters_title`, `thread_min_characters_response`." ] }, - "max_snooze_time": { + "snooze_default_duration": { "default": "604800 (7 days in seconds)", - "description": "The maximum duration in seconds that a thread can be snoozed. When a thread is snoozed, it is temporarily hidden until the user replies or a moderator unsnoozes it.", + "description": "The default duration that a thread will be snoozed when no duration is specified. When a thread is snoozed, it is temporarily hidden until the user replies or a moderator unsnoozes it.", "examples": [ - "`{prefix}config set max_snooze_time 86400` (1 day)", - "`{prefix}config set max_snooze_time 1209600` (14 days)" + "`{prefix}config set snooze_default_duration 7 days`", + "`{prefix}config set snooze_default_duration 2 hours`", + "`{prefix}config set snooze_default_duration 86400` (raw seconds also accepted)" ], "notes": [ - "The value must be specified in seconds.", + "Accepts user-friendly time like '5 days', '2 hours', or raw seconds.", "See also: `snooze_title`, `snooze_text`, `unsnooze_text`, `unsnooze_notify_channel`." ] }, @@ -1285,7 +1298,7 @@ "`{prefix}config set snooze_title Thread Paused`" ], "notes": [ - "See also: `snooze_text`, `unsnooze_title`, `max_snooze_time`." + "See also: `snooze_text`, `unsnooze_title`, `snooze_default_duration`." ] }, "snooze_text": { @@ -1296,7 +1309,7 @@ ], "notes": [ "Discord flavoured markdown is fully supported in `snooze_text`.", - "See also: `snooze_title`, `unsnooze_text`, `max_snooze_time`." + "See also: `snooze_title`, `unsnooze_text`, `snooze_default_duration`." ] }, "unsnooze_text": { @@ -1307,7 +1320,7 @@ ], "notes": [ "Discord flavoured markdown is fully supported in `unsnooze_text`.", - "See also: `snooze_text`, `unsnooze_notify_channel`, `max_snooze_time`." + "See also: `snooze_text`, `unsnooze_notify_channel`, `snooze_default_duration`." ] }, "unsnooze_notify_channel": { @@ -1320,7 +1333,265 @@ "notes": [ "If set to \"thread\", the notification will be sent in the thread channel itself.", "If set to a channel ID, the notification will be sent to that specific channel.", - "See also: `unsnooze_text`, `max_snooze_time`." + "See also: `unsnooze_text`, `snooze_default_duration`." + ] + }, + "snooze_behavior": { + "default": "\"delete\"", + "description": "Controls how snoozing behaves. 'delete' removes the thread channel and restores it later; 'move' moves the channel into the 'snoozed_category_id' without deleting it.", + "examples": [ + "`{prefix}config set snooze_behavior delete`", + "`{prefix}config set snooze_behavior move`" + ], + "notes": [ + "When set to 'move', set `snoozed_category_id` to a valid Category ID.", + "When unsnoozed, channels moved will return to their original category and position when possible; if original no longer exists they will be moved under `main_category_id`." + ] + }, + "snoozed_category_id": { + "default": "None", + "description": "The category ID where snoozed threads are moved when `snooze_behavior` is set to 'move'.", + "examples": [ + "`{prefix}config set snoozed_category_id 123456789012345678`" + ], + "notes": [ + "Only used when `snooze_behavior` is 'move'.", + "If not set or invalid, the channel will remain in its current category or the bot will fall back to deleting on failure." + ] + }, + "snooze_store_attachments": { + "default": "No", + "description": "When enabled and `snooze_behavior` is 'delete', image attachments are stored as base64 within the snooze data so they can be re-uploaded on unsnooze, preserving media even if the original channel was deleted.", + "examples": [ + "`{prefix}config set snooze_store_attachments yes`", + "`{prefix}config set snooze_store_attachments no`" + ], + "notes": [ + "Only applies to delete-behavior snoozes. In move-behavior, attachments remain in the channel history.", + "To avoid exceeding Mongo's 16MB document size, consider also adjusting `snooze_attachment_max_bytes`." + ] + }, + "snooze_attachment_max_bytes": { + "default": "4194304 (4 MiB)", + "description": "Maximum size per attachment to store as base64 when `snooze_store_attachments` is enabled.", + "examples": [ + "`{prefix}config set snooze_attachment_max_bytes 2097152` (2 MiB)", + "`{prefix}config set snooze_attachment_max_bytes 0` (disable size check; not recommended)" + ], + "notes": [ + "This cap helps prevent hitting MongoDB's 16MB per-document limit when storing large attachments.", + "Non-image files are not stored as base64 and will be preserved as their original URLs if available." + ] + }, + "unsnooze_history_limit": { + "default": "None (all messages replayed)", + "description": "Limits the number of messages replayed when a thread is unsnoozed. When set, only the last N messages will be displayed in the restored channel.", + "examples": [ + "`{prefix}config set unsnooze_history_limit 50`", + "`{prefix}config set unsnooze_history_limit 100`" + ], + "notes": [ + "All messages remain stored in the database regardless of this limit.", + "Set to None or delete this config to replay all messages when unsnoozing.", + "See also: `snooze_behavior`, `unsnooze_text`." + ] + }, + "thread_creation_menu_enabled": { + "default": "Disabled", + "description": "Enables the thread creation menu which asks users to pick an option before the Modmail thread channel is created.", + "examples": [ + "`{prefix}config set thread_creation_menu_enabled yes`" + ], + "notes": [ + "When enabled and at least one option is configured, incoming user DMs are paused until a selection is made.", + "If the menu times out without a selection and `thread_creation_menu_close_on_timeout` is enabled, the thread is aborted silently.", + "See also: `thread_creation_menu_options`, `thread_creation_menu_timeout`." + ] + }, + "thread_creation_menu_options": { + "default": "Empty (no options)", + "description": "Mapping of menu option keys to their data (label, description, emoji, type, callback). A non-empty mapping is required to display the menu.", + "examples": [ + "`{prefix}threadmenu option add` (interactive wizard)", + "`{prefix}threadmenu show`" + ], + "notes": [ + "Type can be 'command' (executes a command after creation) or 'submenu' (opens a submenu).", + "Submenu callbacks must reference keys stored in `thread_creation_menu_submenus`.", + "See also: `thread_creation_menu_submenus`." + ] + }, + "thread_creation_menu_submenus": { + "default": "Empty (no submenus)", + "description": "Mapping of submenu keys to submenu option mappings, identical structure to `thread_creation_menu_options` allowing nested selection.", + "examples": [ + "`{prefix}threadmenu submenu create Appeals`", + "`{prefix}threadmenu submenu option add Appeals`" + ], + "notes": [ + "Submenus allow categorizing options beyond the 25 main menu limit (each submenu limited by Discord select menu rules).", + "See also: `thread_creation_menu_options`." + ] + }, + "thread_creation_menu_timeout": { + "default": "30 (seconds)", + "description": "Number of seconds to wait for a user to pick a menu option before timing out.", + "examples": [ + "`{prefix}config set thread_creation_menu_timeout 30`" + ], + "notes": [ + "If the timeout is reached, `thread_creation_menu_close_on_timeout` controls whether the attempt is aborted or the user is asked to message again.", + "See also: `thread_creation_menu_close_on_timeout`." + ] + }, + "thread_creation_menu_close_on_timeout": { + "default": "No", + "description": "Silently aborts thread creation if the user does not select an option before the timeout expires.", + "examples": [ + "`{prefix}config set thread_creation_menu_close_on_timeout yes`" + ], + "notes": [ + "If disabled, the user receives a message telling them to send a new DM to start again.", + "See also: `thread_creation_menu_timeout`." + ] + }, + "thread_creation_menu_anonymous_menu": { + "default": "No", + "description": "If enabled, the initial menu prompt relayed to staff (after creation) is anonymized; only selection details are logged, not the original prompt message author context.", + "examples": [ + "`{prefix}config set thread_creation_menu_anonymous_menu yes`" + ], + "notes": [ + "Does not affect the DM the user sees, only how it's displayed/logged internally.", + "See also: `thread_creation_menu_selection_log`." + ] + }, + "thread_creation_menu_embed_text": { + "default": "\"Please select an option.\"", + "description": "Text shown in the embed above the selection dropdown in the user's DM.", + "examples": [ + "`{prefix}config set thread_creation_menu_embed_text Please choose a department`" + ], + "notes": [ + "Keep this concise; users cannot proceed until choosing.", + "See also: `thread_creation_menu_dropdown_placeholder`." + ] + }, + "thread_creation_menu_dropdown_placeholder": { + "default": "\"Select an option to contact the staff team.\"", + "description": "Placeholder text displayed in the dropdown before selection.", + "examples": [ + "`{prefix}config set thread_creation_menu_dropdown_placeholder Pick a topic...`" + ], + "notes": [ + "Appears grayed out inside the select component.", + "See also: `thread_creation_menu_embed_text`." + ] + }, + "thread_creation_menu_selection_log": { + "default": "Yes", + "description": "Logs the chosen menu option (label, type, callback) as a message in the newly created thread channel for moderator visibility.", + "examples": [ + "`{prefix}config set thread_creation_menu_selection_log no`" + ], + "notes": [ + "When disabled, the staff will not see an automatic message with the selection details though commands may still act.", + "See also: `thread_creation_menu_enabled`, `thread_creation_menu_options`." + ] + }, + "thread_creation_menu_precreate_channel": { + "default": "No", + "description": "When enabled, a thread channel is created immediately upon the user's first DM even if the thread creation menu is enabled. The menu is still shown but selection becomes optional and happens after channel creation.", + "examples": [ + "`{prefix}config set thread_creation_menu_precreate_channel yes`" + ], + "notes": [ + "If a user never selects an option the thread remains open in the default/main category.", + "Category-specific option selections will move the channel afterward if a category_id is configured on the option.", + "Confirmation (`confirm_thread_creation`) is only applied in the deferred mode; enabling precreate bypasses the confirm step for menu flows.", + "See also: `thread_creation_menu_enabled`, `thread_creation_menu_options`, `confirm_thread_creation`." + ] + }, + "thread_creation_menu_embed_title": { + "default": "Empty (no title)", + "description": "Optional title at the top of the thread-creation menu embed in the user's DM.", + "examples": [ + "`{prefix}config set thread_creation_menu_embed_title Contact the staff team`", + "`{prefix}config delete thread_creation_menu_embed_title` (clear)" + ], + "notes": [ + "See also: `thread_creation_menu_embed_text`." + ] + }, + "thread_creation_menu_embed_footer": { + "default": "Empty (no footer)", + "description": "Optional footer text at the bottom of the menu embed.", + "examples": [ + "`{prefix}config set thread_creation_menu_embed_footer Please choose the most relevant option`", + "`{prefix}config delete thread_creation_menu_embed_footer` (clear)" + ], + "notes": [ + "You can also configure an optional footer icon via `thread_creation_menu_embed_footer_icon_url`." + ] + }, + "thread_creation_menu_embed_footer_icon_url": { + "default": "Empty (no icon)", + "description": "Optional URL for the small footer icon displayed next to the footer text.", + "examples": [ + "`{prefix}config set thread_creation_menu_embed_footer_icon_url https://example.com/icon.png`", + "`{prefix}config delete thread_creation_menu_embed_footer_icon_url` (clear)" + ], + "notes": [ + "Use a direct image URL (PNG/JPEG/GIF)." + ] + }, + "thread_creation_menu_embed_thumbnail_url": { + "default": "Empty (no thumbnail)", + "description": "Optional thumbnail image shown in the top-right of the menu embed.", + "examples": [ + "`{prefix}config set thread_creation_menu_embed_thumbnail_url https://example.com/logo.png`", + "`{prefix}config delete thread_creation_menu_embed_thumbnail_url` (clear)" + ], + "notes": [ + "Use a direct image URL; recommended square image.", + "Consider file size/CDN reliability so it loads quickly for users." + ] + }, + "thread_creation_menu_embed_image_url": { + "default": "Empty (no image)", + "description": "Optional large hero image displayed in the body of the menu embed. If set, this image is shown prominently and takes precedence over the thumbnail.", + "examples": [ + "`{prefix}config set thread_creation_menu_embed_image_url https://example.com/banner.png`", + "`{prefix}config delete thread_creation_menu_embed_image_url` (clear)" + ], + "notes": [ + "Use a direct image URL (PNG/JPEG/GIF).", + "When both a thumbnail and image URL are set, the image URL is used as the large embed image while the thumbnail may still be shown in the top-right.", + "See also: `thread_creation_menu_embed_thumbnail_url`, `thread_creation_menu_embed_large_image`." + ] + }, + "thread_creation_menu_embed_large_image": { + "default": "No", + "description": "Promotes the thumbnail to a large hero image when no `thread_creation_menu_embed_image_url` is set. Useful if you want a big image without specifying a separate URL.", + "examples": [ + "`{prefix}config set thread_creation_menu_embed_large_image yes`", + "`{prefix}config set thread_creation_menu_embed_large_image no`" + ], + "notes": [ + "Only applies when `thread_creation_menu_embed_image_url` is not set.", + "If both are provided, the explicit image URL takes precedence.", + "See also: `thread_creation_menu_embed_thumbnail_url`, `thread_creation_menu_embed_image_url`." + ] + }, + "thread_creation_menu_embed_color": { + "default": "Green (hex for Discord Color.green)", + "description": "Color for the menu embed's side strip. Accepts hex (e.g. #5865F2) or one of the supported color names.", + "examples": [ + "`{prefix}config set thread_creation_menu_embed_color #5865F2`", + "`{prefix}config set thread_creation_menu_embed_color blurple`" + ], + "notes": [ + "Color names map to the built-in palette (e.g., 'red', 'green', 'blurple')." ] } } \ No newline at end of file diff --git a/core/models.py b/core/models.py index 611db375f0..5f36f12181 100644 --- a/core/models.py +++ b/core/models.py @@ -30,15 +30,15 @@ class ModmailLogger(logging.Logger): @staticmethod def _debug_(*msgs): - return f'{Fore.CYAN}{" ".join(msgs)}{Style.RESET_ALL}' + return f"{Fore.CYAN}{' '.join(msgs)}{Style.RESET_ALL}" @staticmethod def _info_(*msgs): - return f'{Fore.LIGHTMAGENTA_EX}{" ".join(msgs)}{Style.RESET_ALL}' + return f"{Fore.LIGHTMAGENTA_EX}{' '.join(msgs)}{Style.RESET_ALL}" @staticmethod def _error_(*msgs): - return f'{Fore.RED}{" ".join(msgs)}{Style.RESET_ALL}' + return f"{Fore.RED}{' '.join(msgs)}{Style.RESET_ALL}" def debug(self, msg, *args, **kwargs): if self.isEnabledFor(logging.DEBUG): @@ -149,7 +149,8 @@ def format(self, record): log_stream_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", ) log_file_formatter = FileFormatter( @@ -231,7 +232,12 @@ def create_log_handler( formatter = log_file_formatter else: handler = RotatingFileHandler( - filename, mode=mode, encoding=encoding, maxBytes=maxBytes, backupCount=backupCount, **kwargs + filename, + mode=mode, + encoding=encoding, + maxBytes=maxBytes, + backupCount=backupCount, + **kwargs, ) formatter = log_file_formatter @@ -264,7 +270,10 @@ def getLogger(name=None) -> ModmailLogger: def configure_logging(bot) -> None: global ch_debug, log_level, ch - stream_log_format, file_log_format = bot.config["stream_log_format"], bot.config["file_log_format"] + stream_log_format, file_log_format = ( + bot.config["stream_log_format"], + bot.config["file_log_format"], + ) if stream_log_format == "json": ch.setFormatter(json_formatter) @@ -318,7 +327,10 @@ def configure_logging(bot) -> None: non_verbose_log_level = max(d_level, logging.INFO) stream_handler = create_log_handler(level=non_verbose_log_level) if non_verbose_log_level != d_level: - logger.info("Discord logging level (stdout): %s.", logging.getLevelName(non_verbose_log_level)) + logger.info( + "Discord logging level (stdout): %s.", + logging.getLevelName(non_verbose_log_level), + ) logger.info("Discord logging level (logfile): %s.", logging.getLevelName(d_level)) else: logger.info("Discord logging level: %s.", logging.getLevelName(d_level)) diff --git a/core/paginator.py b/core/paginator.py index 48222d283e..bf76bdc2e2 100644 --- a/core/paginator.py +++ b/core/paginator.py @@ -383,7 +383,7 @@ def add_page(self, item: str) -> None: def _set_footer(self): if self.embed is not None: - footer_text = f"Page {self.current+1} of {len(self.pages)}" + footer_text = f"Page {self.current + 1} of {len(self.pages)}" if self.footer_text: footer_text = footer_text + " • " + self.footer_text diff --git a/core/thread.py b/core/thread.py index d380552694..39edb2b482 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1,6 +1,6 @@ import asyncio -import base64 import copy +import base64 import functools import io import re @@ -14,12 +14,13 @@ import isodate import discord +from discord.ext import commands from discord.ext.commands import MissingRequiredArgument, CommandError from lottie.importers import importers as l_importers from lottie.exporters import exporters as l_exporters -from core.models import DMDisabled, DummyMessage, getLogger -from core.time import human_timedelta +from core.models import DMDisabled, DummyMessage, PermissionLevel, getLogger +from core import checks from core.utils import ( is_image_url, parse_channel_topic, @@ -67,10 +68,15 @@ def __init__( self.close_task = None self.auto_close_task = None self._cancelled = False + self._dm_menu_msg_id = None + self._dm_menu_channel_id = None # --- SNOOZE STATE --- self.snoozed = False # True if thread is snoozed self.snooze_data = None # Dict with channel/category/position/messages for restoration self.log_key = None # Ensure log_key always exists + # --- UNSNOOZE COMMAND QUEUE --- + self._unsnoozing = False # True while restore_from_snooze is running + self._command_queue = [] # Queue of (ctx, command) tuples; close commands always last def __repr__(self): return f'Thread(recipient="{self.recipient or self.id}", channel={self.channel.id}, other_recipients={len(self._other_recipients)})' @@ -88,9 +94,10 @@ async def wait_until_ready(self) -> None: try: await task except asyncio.TimeoutError: - pass - - self.wait_tasks.remove(task) + logger.warning("Waiting for thread setup timed out.") + finally: + if task in self.wait_tasks: + self.wait_tasks.remove(task) @property def id(self) -> int: @@ -113,10 +120,19 @@ def ready(self) -> bool: return self._ready_event.is_set() @ready.setter - def ready(self, flag: bool): + def ready(self, flag: bool) -> None: + """Set the ready state and dispatch thread_create when transitioning to ready. + + Some legacy code paths set thread.ready = True/False. This setter preserves that API by + updating the internal event and emitting the creation event when entering the ready state. + """ if flag: - self._ready_event.set() - self.bot.dispatch("thread_create", self) + if not self._ready_event.is_set(): + self._ready_event.set() + try: + self.bot.dispatch("thread_create", self) + except Exception as e: + logger.warning("Error dispatching thread_create: %s", e) else: self._ready_event.clear() @@ -133,13 +149,38 @@ def cancelled(self, flag: bool): async def snooze(self, moderator=None, command_used=None, snooze_for=None): """ - Save channel/category/position/messages to DB, mark as snoozed, delete channel. + Save channel/category/position/messages to DB, mark as snoozed. + Behavior is configurable: + - delete (default): delete the channel and store all data for full restore later + - move: move channel to a configured snoozed category and hide it (keeps channel alive) """ if self.snoozed: return False # Already snoozed channel = self.channel if not isinstance(channel, discord.TextChannel): return False + # If using move-based snooze, hard-cap snoozed category to 49 channels + behavior_pre = (self.bot.config.get("snooze_behavior") or "delete").lower() + if behavior_pre == "move": + snoozed_cat_id = self.bot.config.get("snoozed_category_id") + target_category = None + if snoozed_cat_id: + try: + target_category = self.bot.modmail_guild.get_channel(int(snoozed_cat_id)) + except Exception: + target_category = None + if isinstance(target_category, discord.CategoryChannel): + try: + if len(target_category.channels) >= 49: + logger.warning( + "Snoozed category (%s) is full (>=49 channels). Blocking snooze for thread %s.", + target_category.id, + self.id, + ) + return False + except Exception: + # If we cannot determine channel count, proceed; downstream will handle errors + pass # Ensure self.log_key is set before snoozing if not self.log_key: # Try to fetch from DB using channel_id @@ -180,18 +221,18 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None): ) else None ), - "author_name": getattr(m.author, "name", None), + "author_name": ( + getattr(m.embeds[0].author, "name", "").split(" (")[0] + if m.embeds and m.embeds[0].author and m.author == self.bot.user + else getattr(m.author, "name", None) if m.author != self.bot.user else None + ), + "author_avatar": ( + getattr(m.embeds[0].author, "icon_url", None) + if m.embeds and m.embeds[0].author and m.author == self.bot.user + else m.author.display_avatar.url if m.author != self.bot.user else None + ), } async for m in channel.history(limit=None, oldest_first=True) - if not ( - m.embeds - and getattr(m.embeds[0], "author", None) - and ( - getattr(m.embeds[0].author, "name", "").startswith("📝 Note") - or getattr(m.embeds[0].author, "name", "").startswith("📝 Persistent Note") - ) - ) - and getattr(m, "type", None) not in ("internal", "note") ], "snoozed_by": getattr(moderator, "name", None) if moderator else None, "snooze_command": command_used, @@ -213,76 +254,445 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None): import logging logging.info(f"[SNOOZE] DB update result: {result.modified_count}") - # Delete channel - await channel.delete(reason="Thread snoozed by moderator") - self._channel = None + + behavior = behavior_pre + if behavior == "move": + # Move the channel to the snoozed category (if configured) and optionally apply a prefix + snoozed_cat_id = self.bot.config.get("snoozed_category_id") + target_category = None + guild = self.bot.modmail_guild + if snoozed_cat_id: + try: + target_category = guild.get_channel(int(snoozed_cat_id)) + except Exception: + target_category = None + # If no valid snooze category is configured, create one automatically + if not isinstance(target_category, discord.CategoryChannel): + try: + # By default, hide the snoozed category from everyone and allow only the bot to see it + overwrites = {guild.default_role: discord.PermissionOverwrite(view_channel=False)} + bot_member = guild.me + if bot_member is not None: + overwrites[bot_member] = discord.PermissionOverwrite( + view_channel=True, + send_messages=True, + read_message_history=True, + manage_channels=True, + manage_messages=True, + attach_files=True, + embed_links=True, + add_reactions=True, + ) + + target_category = await guild.create_category( + name="Snoozed Threads", + overwrites=overwrites, + reason="Auto-created snoozed category for move-based snoozing", + ) + # Persist the newly created category ID into config for future runs + try: + await self.bot.config.set("snoozed_category_id", target_category.id) + await self.bot.config.update() + except Exception: + logger.warning("Failed to persist snoozed_category_id after auto-creation.") + except Exception as e: + logger.warning( + "Failed to auto-create snoozed category (%s). Falling back to current category.", + e, + ) + target_category = channel.category + try: + # Move and sync permissions so the channel inherits the hidden snoozed-category perms + await channel.edit( + category=target_category, + reason="Thread snoozed (moved)", + sync_permissions=True, + ) + # Keep channel reference; just moved + self._channel = channel + # mark in snooze data that this was a move-based snooze + self.snooze_data["moved"] = True + except Exception as e: + logger.warning( + "Failed to move channel to snoozed category: %s. Falling back to delete.", + e, + ) + await channel.delete(reason="Thread snoozed by moderator (fallback delete)") + self._channel = None + else: + # Delete channel + await channel.delete(reason="Thread snoozed by moderator") + self._channel = None return True async def restore_from_snooze(self): """ - Recreate channel in original category/position, replay messages, mark as not snoozed. + Restore a snoozed thread. + - If channel was deleted (delete behavior), recreate and replay messages. + - If channel was moved (move behavior), move back to original category and position. + Mark as not snoozed and clear snooze data. """ + # Prevent concurrent unsnooze operations + if self._unsnoozing: + logger.warning(f"Unsnooze already in progress for thread {self.id}, skipping duplicate call") + return False + + # Mark that unsnooze is in progress + self._unsnoozing = True + if not self.snooze_data or not isinstance(self.snooze_data, dict): import logging logging.warning( f"[UNSNOOZE] Tried to restore thread {self.id} but snooze_data is None or not a dict." ) + self._unsnoozing = False return False - # Now safe to access self.snooze_data + + # Cache some fields we need later (before we potentially clear snooze_data) snoozed_by = self.snooze_data.get("snoozed_by") snooze_command = self.snooze_data.get("snooze_command") + guild = self.bot.modmail_guild - category = guild.get_channel(self.snooze_data["category_id"]) - overwrites = {} - for role_id, perm_values in self.snooze_data["overwrites"]: - role = guild.get_role(role_id) or guild.get_member(role_id) - if role: - overwrites[role] = discord.PermissionOverwrite(**perm_values) - channel = await guild.create_text_channel( - name=self.snooze_data["name"], - category=category, - topic=self.snooze_data["topic"], - slowmode_delay=self.snooze_data["slowmode_delay"], - overwrites=overwrites, - nsfw=self.snooze_data["nsfw"], - position=self.snooze_data["position"], - reason="Thread unsnoozed/restored", + behavior = (self.bot.config.get("snooze_behavior") or "delete").lower() + + # Determine original category; fall back to main_category_id if original missing + orig_category = ( + guild.get_channel(self.snooze_data.get("category_id")) + if self.snooze_data.get("category_id") + else None ) - self._channel = channel + if not isinstance(orig_category, discord.CategoryChannel): + main_cat_id = self.bot.config.get("main_category_id") + orig_category = guild.get_channel(int(main_cat_id)) if main_cat_id else None + + # Default: assume we'll need to recreate + channel: typing.Optional[discord.TextChannel] = None + + # If move-behavior and channel still exists, move it back and restore overwrites + if behavior == "move" and isinstance(self.channel, discord.TextChannel): + try: + await self.channel.edit( + category=orig_category, + position=self.snooze_data.get("position", self.channel.position), + reason="Thread unsnoozed/restored", + ) + # Restore original overwrites captured at snooze time + try: + ow_map: dict = {} + for role_id, perm_values in self.snooze_data.get("overwrites", []): + target = guild.get_role(role_id) or guild.get_member(role_id) + if target is None: + continue + ow_map[target] = discord.PermissionOverwrite(**perm_values) + if ow_map: + await self.channel.edit(overwrites=ow_map, reason="Restore original overwrites") + except Exception as e: + logger.warning("Failed to restore original overwrites on unsnooze: %s", e) + + channel = self.channel + except Exception as e: + logger.warning("Failed to move snoozed channel back, recreating: %s", e) + channel = None + + # If we couldn't move back (or behavior=delete), recreate the channel + if channel is None: + try: + ow_map: dict = {} + for role_id, perm_values in self.snooze_data.get("overwrites", []): + target = guild.get_role(role_id) or guild.get_member(role_id) + if target is None: + continue + ow_map[target] = discord.PermissionOverwrite(**perm_values) + + channel = await guild.create_text_channel( + name=self.snooze_data.get("name") or f"thread-{self.id}", + category=orig_category, + # discord.py expects a dict for overwrites; use empty dict if none + overwrites=ow_map or {}, + position=self.snooze_data.get("position"), + topic=self.snooze_data.get("topic"), + slowmode_delay=self.snooze_data.get("slowmode_delay") or 0, + nsfw=bool(self.snooze_data.get("nsfw")), + reason="Thread unsnoozed/restored (recreated)", + ) + self._channel = channel + except Exception: + logger.error("Failed to recreate thread channel during unsnooze.", exc_info=True) + return False + + # Helper to safely send to thread channel, recreating once if deleted + async def _safe_send_to_channel(*, content=None, embeds=None, allowed_mentions=None): + nonlocal channel + try: + return await channel.send(content=content, embeds=embeds, allowed_mentions=allowed_mentions) + except discord.NotFound: + # Channel was deleted between restore and send; try to recreate once + try: + ow_map: dict = {} + for role_id, perm_values in self.snooze_data.get("overwrites", []) or []: + target = guild.get_role(role_id) or guild.get_member(role_id) + if target is None: + continue + ow_map[target] = discord.PermissionOverwrite(**perm_values) + channel = await guild.create_text_channel( + name=(self.snooze_data.get("name") or f"thread-{self.id}"), + category=orig_category, + # discord.py expects a dict for overwrites; use empty dict if none + overwrites=ow_map or {}, + position=self.snooze_data.get("position"), + topic=self.snooze_data.get("topic"), + slowmode_delay=self.snooze_data.get("slowmode_delay") or 0, + nsfw=bool(self.snooze_data.get("nsfw")), + reason="Thread unsnoozed/restored (recreated after NotFound)", + ) + self._channel = channel + return await channel.send( + content=content, + embeds=embeds, + allowed_mentions=allowed_mentions, + ) + except Exception: + logger.error( + "Failed to recreate channel during unsnooze send.", + exc_info=True, + ) + return None + + # Ensure genesis message exists; always present after unsnooze + genesis_already_sent = False + + async def _ensure_genesis(force: bool = False): + nonlocal genesis_already_sent + try: + existing = await self.get_genesis_message() + except Exception: + existing = None + if existing is None or force: + # Build log_url and log_count best-effort + prefix = (self.bot.config.get("log_url_prefix") or "").strip("/") + if prefix == "NONE": + prefix = "" + key = self.snooze_data.get("log_key") or self.log_key + log_url = ( + f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{key}" + if key + else None + ) + log_count = None + try: + logs = await self.bot.api.get_user_logs(self.id) + log_count = sum(1 for log in logs if not log.get("open")) + except Exception: + log_count = None + # Resolve recipient object + user = self.recipient + if user is None: + try: + user = await self.bot.get_or_fetch_user(self.id) + except Exception: + user = SimpleNamespace( + id=self.id, + mention=f"<@{self.id}>", + created_at=datetime.now(timezone.utc), + ) + try: + info_embed = self._format_info_embed(user, log_url, log_count, self.bot.main_color) + msg = await channel.send(embed=info_embed) + try: + await msg.pin() + except Exception as e: + logger.warning("Failed to pin genesis message during unsnooze: %s", e) + self._genesis_message = msg + genesis_already_sent = True + except Exception: + logger.warning("Failed to send genesis message during unsnooze.", exc_info=True) + + # If we recreated the channel, force-send genesis; if moved back, ensure it's present + try: + if behavior == "move" and isinstance(channel, discord.TextChannel): + await _ensure_genesis(force=False) + else: + await _ensure_genesis(force=True) + except Exception: + logger.debug("Genesis ensure step encountered an error.") + # Strictly restore the log_key from snooze_data (never create a new one) self.log_key = self.snooze_data.get("log_key") - # Replay messages - for msg in self.snooze_data["messages"]: - author = self.bot.get_user(msg["author_id"]) or await self.bot.get_or_fetch_user(msg["author_id"]) - content = msg["content"] - embeds = [discord.Embed.from_dict(e) for e in msg.get("embeds", []) if e] - attachments = msg.get("attachments", []) - msg_type = msg.get("type") - # Only send if there is content, embeds, or attachments - if not content and not embeds and not attachments: - continue # Skip empty messages - author_is_mod = msg["author_id"] not in [r.id for r in self.recipients] - if author_is_mod: - username = msg.get("author_name") or (getattr(author, "name", None)) or "Unknown" - user_id = msg.get("author_id") + + # Replay messages only if we re-created the channel (delete behavior or move fallback) + if behavior != "move" or (behavior == "move" and not self.snooze_data.get("moved", False)): + # Get history limit from config (0 or None = show all) + history_limit = self.bot.config.get("unsnooze_history_limit") + all_messages = self.snooze_data.get("messages", []) + + # Separate genesis, notes, and regular messages + genesis_msg = None + notes = [] + regular_messages = [] + + for msg in all_messages: + msg_type = msg.get("type") + # Check if it's the genesis message (has Roles field) + if msg.get("embeds"): + for embed_dict in msg.get("embeds", []): + if embed_dict.get("fields"): + for field in embed_dict.get("fields", []): + if field.get("name") == "Roles": + genesis_msg = msg + break + if genesis_msg: + break + # Check if it's a note + if msg_type == "mod_only": + notes.append(msg) + elif genesis_msg != msg: + regular_messages.append(msg) + + # Apply limit if set + limited = False + if history_limit: + try: + history_limit = int(history_limit) + if history_limit > 0 and len(regular_messages) > history_limit: + regular_messages = regular_messages[-history_limit:] + limited = True + except (ValueError, TypeError): + pass + + # Replay genesis first (only if we didn't already create it above) + if genesis_msg and not genesis_already_sent: + msg = genesis_msg + try: + author = self.bot.get_user(msg["author_id"]) or await self.bot.get_or_fetch_user( + msg["author_id"] + ) + except discord.NotFound: + author = None + embeds = [discord.Embed.from_dict(e) for e in msg.get("embeds", []) if e] if embeds: - embeds[0].set_author( - name=f"{username} ({user_id})", - icon_url=( - author.display_avatar.url - if author and hasattr(author, "display_avatar") - else None - ), + await _safe_send_to_channel( + embeds=embeds, allowed_mentions=discord.AllowedMentions.none() ) - await channel.send(embeds=embeds) + + # Send history limit notification after genesis + if limited: + prefix = self.bot.config["log_url_prefix"].strip("/") + if prefix == "NONE": + prefix = "" + log_url = ( + f"{self.bot.config['log_url'].strip('/')}{'/' + prefix if prefix else ''}/{self.log_key}" + if self.log_key + else None + ) + + limit_embed = discord.Embed( + color=0xFFA500, + title="⚠️ History Limited", + description=f"Only showing the last **{history_limit}** messages due to the `unsnooze_history_limit` setting.", + ) + if log_url: + limit_embed.description += f"\n\n[View full history in logs]({log_url})" + await _safe_send_to_channel( + embeds=[limit_embed], + allowed_mentions=discord.AllowedMentions.none(), + ) + + # Build list of remaining messages to show + messages_to_show = [] + messages_to_show.extend(notes) + messages_to_show.extend(regular_messages) + + for msg in messages_to_show: + try: + author = self.bot.get_user(msg["author_id"]) or await self.bot.get_or_fetch_user( + msg["author_id"] + ) + except discord.NotFound: + author = None + + content = msg.get("content") + embeds = [discord.Embed.from_dict(e) for e in msg.get("embeds", []) if e] + attachments = msg.get("attachments", []) + + # Only send if there is something to send + if not content and not embeds and not attachments: + continue + + author_is_mod = msg["author_id"] not in [r.id for r in self.recipients] + if author_is_mod: + # Prefer stored author_name/avatar + username = ( + msg.get("author_name") + or (getattr(author, "name", None) if author else None) + or "Unknown" + ) + user_id = msg.get("author_id") + if embeds: + # Ensure embeds show author details + embeds[0].set_author( + name=f"{username} ({user_id})", + icon_url=msg.get("author_avatar") + or ( + author.display_avatar.url + if author and hasattr(author, "display_avatar") + else None + ), + ) + # If there were attachment URLs, include them as a field so mods can access them + if attachments: + try: + embeds[0].add_field( + name="Attachments", + value="\n".join(attachments), + inline=False, + ) + except Exception as e: + logger.info( + "Failed to add attachments field while replaying unsnoozed messages: %s", + e, + ) + await _safe_send_to_channel( + embeds=embeds, + allowed_mentions=discord.AllowedMentions.none(), + ) + else: + # Plain-text path (no embeds): prefix with username and user id + header = f"**{username} ({user_id})**" + body = content or "" + if attachments and not body: + # no content; include attachment URLs on new lines + body = "\n".join(attachments) + formatted = f"{header}: {body}" if body else header + await _safe_send_to_channel( + content=formatted, + allowed_mentions=discord.AllowedMentions.none(), + ) else: - formatted = ( - f"**{username} ({user_id})**: {content}" if content else f"**{username} ({user_id})**" + # Recipient message: include attachment URLs if content is empty + # When no embeds, prefix plain text with username and user id + username = ( + msg.get("author_name") + or (getattr(author, "name", None) if author else None) + or "Unknown" ) - await channel.send(formatted) - else: - await channel.send(content=content or None, embeds=embeds or None) + user_id = msg.get("author_id") + if embeds: + await _safe_send_to_channel( + content=None, + embeds=embeds or None, + allowed_mentions=discord.AllowedMentions.none(), + ) + else: + header = f"**{username} ({user_id})**" + body = content or "" + if attachments and not body: + body = "\n".join(attachments) + formatted = f"{header}: {body}" if body else header + await _safe_send_to_channel( + content=formatted, + allowed_mentions=discord.AllowedMentions.none(), + ) self.snoozed = False # Store snooze_data for notification before clearing snooze_data_for_notify = self.snooze_data @@ -313,18 +723,33 @@ async def restore_from_snooze(self): notify_channel = self.bot.config.get("unsnooze_notify_channel") or "thread" notify_text = self.bot.config.get("unsnooze_text") or "This thread has been unsnoozed and restored." if notify_channel == "thread": - await channel.send(notify_text) + await _safe_send_to_channel(content=notify_text, allowed_mentions=discord.AllowedMentions.none()) else: - ch = self.bot.get_channel(int(notify_channel)) + # Extract channel ID from mention format <#123> or use raw ID + channel_id = str(notify_channel).strip("<#>") + ch = self.bot.get_channel(int(channel_id)) if ch: - await ch.send(f"Thread for user <@{self.id}> has been unsnoozed and restored.") + await ch.send( + f"⏰ Thread for user <@{self.id}> has been unsnoozed and restored in {channel.mention}", + allowed_mentions=discord.AllowedMentions.none(), + ) # Show who ran the snooze command and the command used # Use snooze_data_for_notify to avoid accessing self.snooze_data after it is set to None snoozed_by = snooze_data_for_notify.get("snoozed_by") if snooze_data_for_notify else None snooze_command = snooze_data_for_notify.get("snooze_command") if snooze_data_for_notify else None if snoozed_by or snooze_command: info = f"Snoozed by: {snoozed_by or 'Unknown'} | Command: {snooze_command or '?snooze'}" - await channel.send(info) + await channel.send(info, allowed_mentions=discord.AllowedMentions.none()) + + # Ensure channel is set before processing commands + self._channel = channel + + # Mark unsnooze as complete + self._unsnoozing = False + + # Process queued commands + await self._process_command_queue() + return True @classmethod @@ -371,22 +796,25 @@ async def setup(self, *, creator=None, category=None, initial_message=None): if category is not None: overwrites = {} - try: - channel = await create_thread_channel(self.bot, recipient, category, overwrites) - 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) - - embed = discord.Embed(color=self.bot.error_color) - embed.title = "Error while trying to create a thread." - embed.description = str(e) - embed.add_field(name="Recipient", value=recipient.mention) - - if self.bot.log_channel is not None: - await self.bot.log_channel.send(embed=embed) - return - - self._channel = channel + # If thread menu is enabled and this setup call is marked as deferred genesis (initial_message carries flag), + # then we may have already created the channel earlier. Only create if channel missing. + if self._channel is None: + try: + channel = await create_thread_channel(self.bot, recipient, category, overwrites) + 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) + + embed = discord.Embed(color=self.bot.error_color) + embed.title = "Error while trying to create a thread." + embed.description = str(e) + embed.add_field(name="Recipient", value=recipient.mention) + + if self.bot.log_channel is not None: + await self.bot.log_channel.send(embed=embed) + return + else: + self._channel = channel try: log_url, log_data = await asyncio.gather( @@ -413,11 +841,40 @@ async def send_genesis_message(): msg = await channel.send(mention, embed=info_embed) self.bot.loop.create_task(msg.pin()) self._genesis_message = msg + # Option selection logging (if a thread-creation menu option was chosen prior to creation) + if getattr(self, "_selected_thread_creation_menu_option", None) and self.bot.config.get( + "thread_creation_menu_selection_log" + ): + opt = self._selected_thread_creation_menu_option + try: + log_txt = f"Selected menu option: {opt.get('label')} ({opt.get('type')})" + if opt.get("type") == "command": + log_txt += f" -> {opt.get('callback')}" + await channel.send(embed=discord.Embed(description=log_txt, color=self.bot.mod_color)) + except Exception: + logger.warning( + "Failed logging thread-creation menu selection", + exc_info=True, + ) except Exception: logger.error("Failed unexpectedly:", exc_info=True) async def send_recipient_genesis_message(): # Once thread is ready, tell the recipient (don't send if using contact on others) + # Allow disabling the DM receipt embed via config + if not self.bot.config.get("thread_creation_send_dm_embed"): + # If self-closable is enabled, add the close reaction to the user's + # original message instead so functionality is preserved without an embed. + try: + recipient_thread_close = self.bot.config.get("recipient_thread_close") + if recipient_thread_close and initial_message is not None: + close_emoji = self.bot.config["close_emoji"] + close_emoji = await self.bot.convert_emoji(close_emoji) + await self.bot.add_reaction(initial_message, close_emoji) + except Exception as e: + logger.info("Failed to add self-close reaction to initial message: %s", e) + return + thread_creation_response = self.bot.config["thread_creation_response"] embed = discord.Embed( @@ -434,7 +891,8 @@ async def send_recipient_genesis_message(): footer = self.bot.config["thread_creation_footer"] embed.set_footer( - text=footer, icon_url=self.bot.get_guild_icon(guild=self.bot.modmail_guild, size=128) + text=footer, + icon_url=self.bot.get_guild_icon(guild=self.bot.guild, size=128), ) embed.title = self.bot.config["thread_creation_title"] @@ -616,6 +1074,13 @@ async def close( await self._close(closer, silent, delete_channel, message) async def _close(self, closer, silent=False, delete_channel=True, message=None, scheduled=False): + # Proactively disable any DM thread-creation menu so users can't keep interacting + # with the menu after the thread is closed. + try: + await self._disable_dm_creation_menu() + except Exception: + # Non-fatal; continue closing even if we can't edit the DM menu + pass if self.channel: self.manager.closing.add(self.channel.id) try: @@ -702,11 +1167,11 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, tasks = [self.bot.config.update()] if self.bot.log_channel is not None and self.channel is not None: - if self.bot.config["show_log_url_button"]: + # Only create a URL button if we actually have a valid log_url + view = None + if self.bot.config.get("show_log_url_button") and log_url: view = discord.ui.View() view.add_item(discord.ui.Button(label="Log link", url=log_url, style=discord.ButtonStyle.url)) - else: - view = None tasks.append(self.bot.log_channel.send(embed=embed, view=view)) # Thread closed message @@ -725,12 +1190,18 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, 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 footer = self.bot.config["thread_close_footer"] - embed.set_footer(text=footer, icon_url=self.bot.get_guild_icon(guild=self.bot.guild, size=128)) + embed.set_footer( + text=footer, + icon_url=self.bot.get_guild_icon(guild=self.bot.guild, size=128), + ) if not silent: for user in self.recipients: @@ -755,6 +1226,43 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, self.bot.dispatch("thread_close", self, closer, silent, delete_channel, message, scheduled) + async def _disable_dm_creation_menu(self) -> None: + """Best-effort removal of the interactive DM menu view sent during thread creation.""" + if not self._dm_menu_msg_id: + return + # We only ever send the menu to the main recipient + user = self.recipient + if not isinstance(user, (discord.User, discord.Member)): + return + # Ensure we have a DM channel + dm: typing.Optional[discord.DMChannel] = getattr(user, "dm_channel", None) + if dm is None: + try: + dm = await user.create_dm() + except Exception as e: + logger.info("Failed creating DM channel for menu disable: %s", e) + dm = None + if not isinstance(dm, discord.DMChannel): + return + # If we stored the channel id and it differs, but it's still a DM, continue anyway + try: + msg = await dm.fetch_message(self._dm_menu_msg_id) + except (discord.NotFound, discord.Forbidden): + return + except Exception: + return + try: + closed_text = "This thread has been closed. Send a new message to start a new thread." # Grammar-friendly guidance + closed_embed = discord.Embed(description=closed_text) + await msg.edit(content=None, embed=closed_embed, view=None) + except Exception as e: + # Fallback: at least remove interaction so menu cannot be used + logger.warning("Failed editing DM menu message on close: %s", e) + try: + await msg.edit(view=None) + except Exception as inner_e: + logger.debug("Failed removing view from DM menu message: %s", inner_e) + 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() @@ -801,7 +1309,12 @@ async def _restart_close_timer(self): time_marker_regex, ) - await self.close(closer=self.bot.user, after=int(seconds), message=close_message, auto_close=True) + await self.close( + closer=self.bot.user, + after=int(seconds), + message=close_message, + auto_close=True, + ) async def find_linked_messages( self, @@ -811,31 +1324,73 @@ async def find_linked_messages( note: bool = True, ) -> typing.Tuple[discord.Message, typing.List[typing.Optional[discord.Message]]]: if message1 is not None: - if not message1.embeds or not message1.embeds[0].author.url or message1.author != self.bot.user: - raise ValueError("Malformed thread message.") + if note: + # For notes, don't require author.url; rely on footer/author.name markers + if not message1.embeds or message1.author != self.bot.user: + logger.warning( + f"Malformed note for deletion: embeds={bool(message1.embeds)}, author={message1.author}" + ) + raise ValueError("Malformed note message.") + else: + if ( + not message1.embeds + or not message1.embeds[0].author.url + or message1.author != self.bot.user + ): + logger.debug( + f"Malformed thread message for deletion: embeds={bool(message1.embeds)}, author_url={getattr(message1.embeds[0], 'author', None) and message1.embeds[0].author.url}, author={message1.author}" + ) + # Keep original error string to avoid extra failure embeds in on_message_delete + raise ValueError("Malformed thread message.") elif message_id is not None: try: message1 = await self.channel.fetch_message(message_id) except discord.NotFound: + logger.warning(f"Message ID {message_id} not found in channel history.") raise ValueError("Thread message not found.") + if note: + # Try to treat as note/persistent note first + if message1.embeds and message1.author == self.bot.user: + footer_text = (message1.embeds[0].footer and message1.embeds[0].footer.text) or "" + author_name = getattr(message1.embeds[0].author, "name", "") or "" + is_note = ( + "internal note" in footer_text.lower() + or "persistent internal note" in footer_text.lower() + or author_name.startswith("📝 Note") + or author_name.startswith("📝 Persistent Note") + ) + if is_note: + # Notes have no linked DM counterpart; keep None sentinel + return message1, None + # else: fall through to relay checks below + + # Non-note path (regular relayed messages): require author.url and colors if not ( message1.embeds and message1.embeds[0].author.url and message1.embeds[0].color and message1.author == self.bot.user ): + logger.warning( + f"Message {message_id} is not a valid modmail relay message. embeds={bool(message1.embeds)}, author_url={getattr(message1.embeds[0], 'author', None) and message1.embeds[0].author.url}, color={getattr(message1.embeds[0], 'color', None)}, author={message1.author}" + ) raise ValueError("Thread message not found.") if message1.embeds[0].footer and "Internal Message" in message1.embeds[0].footer.text: if not note: - raise ValueError("Thread message not found.") + logger.warning( + f"Message {message_id} is an internal message, but note deletion not requested." + ) + raise ValueError("Thread message is an internal message, not a note.") + # Internal bot-only message treated similarly; keep None sentinel return message1, None if message1.embeds[0].color.value != self.bot.mod_color and not ( either_direction and message1.embeds[0].color.value == self.bot.recipient_color ): + logger.warning("Message color does not match mod/recipient colors.") raise ValueError("Thread message not found.") else: async for message1 in self.channel.history(): @@ -890,8 +1445,11 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> embed1 = message1.embeds[0] embed1.description = message - tasks = [self.bot.api.edit_message(message1.id, message), message1.edit(embed=embed1)] - if message1.embeds[0].footer and "Persistent Internal Message" in message1.embeds[0].footer.text: + tasks = [ + self.bot.api.edit_message(message1.id, message), + message1.edit(embed=embed1), + ] + if message1.embeds[0].footer and "Persistent Internal Note" in message1.embeds[0].footer.text: tasks += [self.bot.api.edit_note(message1.id, message)] else: for m2 in message2: @@ -910,15 +1468,14 @@ async def delete_message( else: message1, *message2 = await self.find_linked_messages(message, note=note) tasks = [] - - if not isinstance(message, discord.Message): - tasks += [message1.delete()] + # Always delete the primary thread message + tasks += [message1.delete()] for m2 in message2: if m2 is not None: tasks += [m2.delete()] - if message1.embeds[0].footer and "Persistent Internal Message" in message1.embeds[0].footer.text: + if message1.embeds[0].footer and "Persistent Internal Note" in message1.embeds[0].footer.text: tasks += [self.bot.api.delete_note(message1.id)] if tasks: @@ -1024,9 +1581,56 @@ async def note( return msg async def reply( - self, message: discord.Message, anonymous: bool = False, plain: bool = False + self, + message: discord.Message, + content: typing.Optional[str] = None, + anonymous: bool = False, + plain: bool = False, ) -> typing.Tuple[typing.List[discord.Message], discord.Message]: - """Returns List[user_dm_msg] and thread_channel_msg""" + """Send a moderator reply to the thread. + + Parameters + ---------- + message: discord.Message + The invoking command message (contains attachments, stickers, etc.). + content: Optional[str] + Raw reply text to send instead of using ``message.content``. This avoids + mutating ``message.content`` upstream and lets command handlers pass the + processed text directly. + anonymous: bool + Whether to mask the moderator identity in the recipient DM. + plain: bool + Whether to send a plain (non-embed) message to the recipient. + + Returns + ------- + Tuple[List[discord.Message], discord.Message] + A list of messages sent to recipients and the copy sent in the thread channel. + """ + # If this thread was snoozed using move-behavior, unsnooze automatically when a mod replies + try: + behavior = (self.bot.config.get("snooze_behavior") or "delete").lower() + except Exception: + behavior = "delete" + if self.snoozed and behavior == "move": + # Ensure we have snooze_data to restore location + if not self.snooze_data: + try: + log_entry = await self.bot.api.logs.find_one( + {"recipient.id": str(self.id), "snoozed": True} + ) + if log_entry: + self.snooze_data = log_entry.get("snooze_data") + except Exception as e: + logger.info( + "Failed to fetch snooze_data before auto-unsnooze on reply: %s", + e, + ) + try: + await self.restore_from_snooze() + except Exception as e: + logger.warning("Auto-unsnooze on reply failed: %s", e) + if not message.content and not message.attachments and not message.stickers: raise MissingRequiredArgument(DummyParam("msg")) for guild in self.bot.guilds: @@ -1034,7 +1638,10 @@ async def reply( if await self.bot.get_or_fetch_member(guild, self.id): break except discord.NotFound: - pass + logger.info( + "Recipient not found in guild %s when checking mutual servers.", + guild.id if hasattr(guild, "id") else guild, + ) else: return await message.channel.send( embed=discord.Embed( @@ -1055,6 +1662,7 @@ async def reply( from_mod=True, anonymous=anonymous, plain=plain, + content_override=content, ) ) @@ -1084,18 +1692,34 @@ async def reply( ) else: # Send the same thing in the thread channel. - msg = await self.send( - message, destination=self.channel, from_mod=True, anonymous=anonymous, plain=plain - ) - - tasks.append( - self.bot.api.append_log( + try: + msg = await self.send( message, - message_id=msg.id, - channel_id=self.channel.id, - type_="anonymous" if anonymous else "thread_message", + destination=self.channel, + from_mod=True, + anonymous=anonymous, + plain=plain, + content_override=content, + ) + except discord.NotFound: + logger.warning( + "Thread channel not found while replying; skipping thread-channel copy of the message." + ) + msg = None + + if msg is not None: + tasks.append( + self.bot.api.append_log( + message, + message_id=msg.id, + channel_id=self.channel.id, + type_="anonymous" if anonymous else "thread_message", + ) + ) + else: + logger.warning( + "Thread channel message failed to send; skipping append_log. Channel may be missing." ) - ) # Cancel closing if a thread message is sent. if self.close_task is not None: @@ -1125,7 +1749,26 @@ async def send( plain: bool = False, persistent_note: bool = False, thread_creation: bool = False, + *, + content_override: typing.Optional[str] = None, ) -> None: + """Low-level send routine used by reply/note logic. + + Parameters + ---------- + message: discord.Message + The command invocation message (source of attachments/stickers). + destination: Channel/User/Member + Where to send the constructed message/embed. + from_mod: bool + Indicates this is a staff reply (affects author display + logging). + note: bool + Internal note style instead of a regular reply. + anonymous / plain / persistent_note / thread_creation: Various flags controlling style. + content_override: Optional[str] + Explicit text to use instead of ``message.content``. Provided by refactored + reply commands to avoid mutating the original message object. + """ # Handle notes with Discord-like system message format - return early if note: destination = destination or self.channel @@ -1184,11 +1827,9 @@ async def send( if destination is None: logger.error("Attempted to send a message to a thread with no channel (destination is None).") return - try: - await destination.typing() - except discord.NotFound: - logger.warning("Channel not found when trying to send message.") - return + # Initial typing was attempted here previously, but returning on NotFound caused callers to + # receive None and crash when accessing attributes on the message. We rely on the + # snooze-aware typing block below to handle typing and NotFound cases robustly. author = message.author member = self.bot.guild.get_member(author.id) @@ -1234,7 +1875,7 @@ async def send( ): content = "No content" else: - content = message.content or "" + content = (content_override if content_override is not None else message.content) or "" # Only set description if there's actual content to show if content: @@ -1258,7 +1899,7 @@ async def send( tag = str(get_top_role(author, self.bot.config["use_hoisted_top_role"])) name = self.bot.config["anon_username"] if name is None: - name = tag + name = "Anonymous" avatar_url = self.bot.config["anon_avatar_url"] if avatar_url is None: avatar_url = self.bot.get_guild_icon(guild=self.bot.guild, size=128) @@ -1269,8 +1910,15 @@ async def send( ) else: # Normal message - name = str(author) - avatar_url = avatar_url + # If this message originated from a thread-creation menu command callback + # (user selected an option whose type is command), we force the author + # display to be the bot to avoid showing the user as a replying moderator. + if getattr(message, "_menu_invoked", False): + name = str(self.bot.user) + avatar_url = getattr(self.bot.user.display_avatar, "url", system_avatar_url) + else: + name = str(author) + avatar_url = avatar_url embed.set_author( name=name, icon_url=avatar_url, @@ -1332,7 +1980,11 @@ def lottie_to_png(data): discord.StickerFormatType.gif, ): images.append( - (f"https://media.discordapp.net/stickers/{i.id}.{i.format.file_extension}", i.name, True) + ( + f"https://media.discordapp.net/stickers/{i.id}.{i.format.file_extension}", + i.name, + True, + ) ) elif i.format == discord.StickerFormatType.lottie: # save the json lottie representation @@ -1428,9 +2080,23 @@ def lottie_to_png(data): embed.set_footer(text="Anonymous Reply") # Normal messages elif not anonymous: + # Use configured mod_tag if provided; otherwise fallback to + # the author's top role when available, or their display name. mod_tag = self.bot.config["mod_tag"] if mod_tag is None: - mod_tag = str(get_top_role(message.author, self.bot.config["use_hoisted_top_role"])) + if hasattr(message.author, "roles"): + try: + mod_tag = str( + get_top_role( + message.author, + self.bot.config["use_hoisted_top_role"], + ) # type: ignore[arg-type] + ) + except Exception: + # As a safe fallback, prefer a stable display string + mod_tag = getattr(message.author, "display_name", str(message.author)) + else: + mod_tag = getattr(message.author, "display_name", str(message.author)) embed.set_footer(text=mod_tag) # Normal messages else: embed.set_footer(text=self.bot.config["anon_tag"]) @@ -1444,7 +2110,14 @@ def lottie_to_png(data): if (from_mod or note) and not thread_creation: delete_message = not bool(message.attachments) - if delete_message and destination == self.channel: + # Only delete the source command message when it's in a guild text + # channel; attempting to delete a DM message can raise 50003. + if ( + delete_message + and destination == self.channel + and hasattr(message, "channel") + and isinstance(message.channel, discord.TextChannel) + ): try: await message.delete() except Exception as e: @@ -1457,14 +2130,31 @@ def lottie_to_png(data): ): logger.info("Sending a message to %s when DM disabled is set.", self.recipient) - # Best-effort typing: never block message delivery if typing fails + # Best-effort typing with snooze-aware retry: if channel was deleted during snooze, restore and retry once + restored = False try: await destination.typing() except discord.NotFound: - logger.warning("Channel not found.") - raise + # Unknown Channel: if snoozed or we have snooze data, attempt to restore and retry once + if isinstance(destination, discord.TextChannel) and (self.snoozed or self.snooze_data): + logger.info("Thread channel missing while typing; attempting restore from snooze.") + try: + await self.restore_from_snooze() + destination = self.channel or destination + restored = True + await destination.typing() + except Exception as e: + logger.warning("Restore/typing retry failed: %s", e) + raise + else: + logger.warning("Channel not found.") + raise except (discord.Forbidden, discord.HTTPException, Exception) as e: - logger.warning("Unable to send typing to %s: %s. Continuing without typing.", destination, e) + logger.warning( + "Unable to send typing to %s: %s. Continuing without typing.", + destination, + e, + ) if not from_mod and not note: mentions = await self.get_notifications() @@ -1473,29 +2163,45 @@ def lottie_to_png(data): if plain: if from_mod and not isinstance(destination, discord.TextChannel): - # Plain to user + # Plain to user (DM) with warnings.catch_warnings(): - # Catch coroutines not awaited warning warnings.simplefilter("ignore") additional_images = [] - if embed.footer.text: - plain_message = f"**{embed.footer.text} " - else: - plain_message = "**" - plain_message += f"{embed.author.name}:** {embed.description}" + prefix = f"**{embed.footer.text} " if embed.footer and embed.footer.text else "**" + body = embed.description or "" + plain_message = f"{prefix}{embed.author.name}:** {body}" + files = [] - for i in message.attachments: - files.append(await i.to_file()) + for att in message.attachments: + try: + files.append(await att.to_file()) + except Exception: + logger.warning("Failed to attach file in plain DM.", exc_info=True) - msg = await destination.send(plain_message, files=files) + msg = await destination.send(plain_message, files=files or None) else: # Plain to mods - embed.set_footer(text="[PLAIN] " + embed.footer.text) + footer_text = embed.footer.text if embed.footer else "" + embed.set_footer(text=f"[PLAIN] {footer_text}".strip()) msg = await destination.send(mentions, embed=embed) else: - msg = await destination.send(mentions, embed=embed) + try: + msg = await destination.send(mentions, embed=embed) + except discord.NotFound: + if ( + isinstance(destination, discord.TextChannel) + and (self.snoozed or self.snooze_data) + and not restored + ): + logger.info("Thread channel missing while sending; attempting restore and resend.") + await self.restore_from_snooze() + destination = self.channel or destination + msg = await destination.send(mentions, embed=embed) + else: + logger.warning("Channel not found during send.") + raise if additional_images: self.ready = False @@ -1506,16 +2212,19 @@ def lottie_to_png(data): async def get_notifications(self) -> str: key = str(self.id) - - mentions = [] - mentions.extend(self.bot.config["subscriptions"].get(key, [])) - - if key in self.bot.config["notification_squad"]: - mentions.extend(self.bot.config["notification_squad"][key]) - self.bot.config["notification_squad"].pop(key) + mentions: typing.List[str] = [] + subs = self.bot.config["subscriptions"].get(key, []) + mentions.extend(subs) + one_time = self.bot.config["notification_squad"].get(key, []) + mentions.extend(one_time) + + if one_time: + self.bot.config["notification_squad"].pop(key, None) self.bot.loop.create_task(self.bot.config.update()) - return " ".join(set(mentions)) + if not mentions: + return "" + return " ".join(list(dict.fromkeys(mentions))) async def set_title(self, title: str) -> None: topic = f"Title: {title}\n" @@ -1585,6 +2294,59 @@ async def remove_users(self, users: typing.List[typing.Union[discord.Member, dis await self.channel.edit(topic=topic) await self._update_users_genesis() + async def queue_command(self, ctx, command) -> bool: + """ + Queue a command to be executed after unsnooze completes. + Close commands are automatically moved to the end of the queue. + Returns True if command was queued, False if it should execute immediately. + """ + if self._unsnoozing: + command_name = command.qualified_name if command else "" + + # If it's a close command, always add to end + if command_name == "close": + self._command_queue.append((ctx, command)) + else: + # For non-close commands, insert before any close commands + close_index = None + for i, (_, cmd) in enumerate(self._command_queue): + if cmd and cmd.qualified_name == "close": + close_index = i + break + + if close_index is not None: + self._command_queue.insert(close_index, (ctx, command)) + else: + self._command_queue.append((ctx, command)) + + return True + return False + + async def _process_command_queue(self) -> None: + """ + Process all queued commands after unsnooze completes. + Close commands are always last, so processing stops naturally after close. + """ + if not self._command_queue: + return + + logger.info(f"Processing {len(self._command_queue)} queued commands for thread {self.id}") + + # Process commands in order + while self._command_queue: + ctx, command = self._command_queue.pop(0) + try: + command_name = command.qualified_name if command else "" + await self.bot.invoke(ctx) + + # If close command was executed, stop (it's always last anyway) + if command_name == "close": + logger.info("Close command executed, queue processing complete") + break + + except Exception as e: + logger.error(f"Error processing queued command: {e}", exc_info=True) + class ThreadManager: """Class that handles storing, finding and creating Modmail threads.""" @@ -1621,7 +2383,8 @@ async def find( thread = await 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.") @@ -1636,12 +2399,42 @@ async def find( try: await thread.wait_until_ready() except asyncio.CancelledError: - logger.warning("Thread for %s cancelled.", recipient) + # Improve logging: include username and user ID when possible + try: + if recipient is not None: + label = f"{recipient} ({recipient.id})" + elif recipient_id is not None: + user = await self.bot.get_or_fetch_user(recipient_id) + label = f"{user} ({recipient_id})" if user else f"User ({recipient_id})" + else: + label = "Unknown User" + except Exception: + label = f"User ({recipient_id})" if recipient_id is not None else "Unknown User" + logger.warning("Thread for %s cancelled.", label) return thread else: # If the thread is snoozed (channel is None), return it for restoration if thread.cancelled: thread = None + else: + # If the cached thread points to a deleted channel, treat as non-existent + try: + ch = getattr(thread, "channel", None) + if ( + ch + and isinstance(ch, discord.TextChannel) + and self.bot.get_channel(getattr(ch, "id", None)) is None + ): + logger.info( + "Cached thread for %s references a deleted channel. Dropping stale cache entry.", + recipient_id, + ) + self.cache.pop(thread.id, None) + thread = None + except Exception: + # If any attribute access fails, be safe and drop it. + self.cache.pop(getattr(thread, "id", None), None) + thread = None else: def check(topic): @@ -1753,13 +2546,21 @@ async def create( try: await thread.wait_until_ready() except asyncio.CancelledError: - logger.warning("Thread for %s cancelled, abort creating.", recipient) + # Improve logging to include username and ID + try: + label = f"{recipient} ({recipient.id})" + except Exception: + label = f"User ({getattr(recipient, 'id', 'unknown')})" + logger.warning("Thread for %s cancelled, abort creating.", label) return thread else: 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) + 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) ) @@ -1768,16 +2569,35 @@ async def create( self.cache[recipient.id] = thread - if (message or not manual_trigger) and self.bot.config["confirm_thread_creation"]: + # Determine if the advanced thread-creation menu is enabled; if so and the user + # initiated via DM, we defer confirmation until AFTER the user selects an option. + adv_menu_enabled = self.bot.config.get("thread_creation_menu_enabled") and bool( + self.bot.config.get("thread_creation_menu_options") + ) + user_initiated_dm = (creator is None or creator == recipient) and manual_trigger + + if ( + (message or not manual_trigger) + and self.bot.config["confirm_thread_creation"] + and not (adv_menu_enabled and user_initiated_dm) + ): if not manual_trigger: destination = recipient else: destination = message.channel view = ConfirmThreadCreationView() view.add_item( - AcceptButton("accept-thread-creation", self.bot.config["confirm_thread_creation_accept"]) + AcceptButton( + "accept-thread-creation", + self.bot.config["confirm_thread_creation_accept"], + ) + ) + view.add_item( + DenyButton( + "deny-thread-creation", + self.bot.config["confirm_thread_creation_deny"], + ) ) - view.add_item(DenyButton("deny-thread-creation", self.bot.config["confirm_thread_creation_deny"])) confirm = await destination.send( embed=discord.Embed( title=self.bot.config["confirm_thread_creation_title"], @@ -1804,7 +2624,8 @@ async def create( self.bot.loop.create_task( destination.send( embed=discord.Embed( - title=self.bot.config["thread_cancelled"], color=self.bot.error_color + title=self.bot.config["thread_cancelled"], + color=self.bot.error_color, ) ) ) @@ -1812,7 +2633,738 @@ async def create( del self.cache[recipient.id] return thread - self.bot.loop.create_task(thread.setup(creator=creator, category=category, initial_message=message)) + # --- THREAD-CREATION MENU (deferred or precreate channel creation) --- + adv_enabled = self.bot.config.get("thread_creation_menu_enabled") and bool( + self.bot.config.get("thread_creation_menu_options") + ) + user_initiated = (creator is None or creator == recipient) and manual_trigger + precreate = ( + adv_enabled + and user_initiated + and bool(self.bot.config.get("thread_creation_menu_precreate_channel")) + ) + if adv_enabled and user_initiated and not precreate: + # Send menu prompt FIRST, wait for selection, then create channel. + # Build dummy message for menu DM + try: + embed_text = self.bot.config.get("thread_creation_menu_embed_text") + placeholder = self.bot.config.get("thread_creation_menu_dropdown_placeholder") + timeout = int(self.bot.config.get("thread_creation_menu_timeout") or 20) + except Exception: + embed_text = "Please select an option." + placeholder = "Select an option to contact the staff team." + timeout = 20 + + options = self.bot.config.get("thread_creation_menu_options") or {} + submenus = self.bot.config.get("thread_creation_menu_submenus") or {} + + # Minimal inline view implementation (avoid importing plugin code) + + thread.ready = False # not ready yet + + class _ThreadCreationMenuSelect(discord.ui.Select): + def __init__(self, outer_thread: Thread): + self.outer_thread = outer_thread + opts = [ + discord.SelectOption( + label=o["label"], + description=o["description"], + emoji=o["emoji"], + ) + for o in options.values() + ] + super().__init__( + placeholder=placeholder, + min_values=1, + max_values=1, + options=opts, + ) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=False) + # If the thread was snoozed before the user selected an option, + # restore it first so channel creation/setup & message relay work. + try: + if self.outer_thread.snoozed: + await self.outer_thread.restore_from_snooze() + except Exception: + logger.warning("Failed unsnoozing thread prior to menu selection; continuing.") + chosen_label = self.values[0] + # Resolve option key + key = chosen_label.lower().replace(" ", "_") + selected = options.get(key) + self.outer_thread._selected_thread_creation_menu_option = selected + # Reflect the selection in the original DM by editing the embed/body + try: + msg = getattr(interaction, "message", None) + if msg is None and self.view and hasattr(self.view, "message"): + msg = self.view.message + if msg is not None: + # Replace entire embed so only the selection line remains + try: + base_color = (msg.embeds[0].color if msg.embeds else None) or getattr( + self.outer_thread.bot, "mod_color", None + ) + except Exception: + base_color = getattr(self.outer_thread.bot, "mod_color", None) + selection_embed = ( + discord.Embed( + description=f"You selected: {chosen_label}", + color=base_color, + ) + if base_color is not None + else discord.Embed(description=f"You selected: {chosen_label}") + ) + await msg.edit(content=None, embed=selection_embed, view=None) + else: + try: + await interaction.edit_original_response( + content=f"You selected: {chosen_label}", view=None + ) + except Exception as e: + # Fallback: best-effort remove the view at least + logger.info( + "Primary edit_original_response failed; trying to remove view only: %s", + e, + ) + await interaction.edit_original_response(view=None) + except Exception as e: + # Ensure the menu is removed even if content edit failed + logger.warning("Failed to update selection message: %s", e) + try: + await interaction.edit_original_response(view=None) + except Exception as inner_e: + logger.debug( + "Failed to remove view after selection failure: %s", + inner_e, + ) + # Stop the view to end the interaction lifecycle + if self.view: + try: + self.view.stop() + except Exception as e: + logger.debug("Failed to stop menu view after selection: %s", e) + # Now create channel + # Determine category: prefer option-specific category if configured and valid + sel_category = None + try: + cat_id = selected.get("category_id") if isinstance(selected, dict) else None + if cat_id: + guild = self.outer_thread.bot.modmail_guild + if guild: + ch = guild.get_channel(cat_id) + if isinstance(ch, discord.CategoryChannel): + sel_category = ch + except Exception: + sel_category = None + # Fallback to provided category (from outer scope) or main category + fallback_category = category or self.outer_thread.bot.main_category + use_category = sel_category or fallback_category + # If confirmation is enabled, prompt now (after option selection) + try: + if self.outer_thread.bot.config.get("confirm_thread_creation"): + dest = message.channel if manual_trigger else recipient + view = ConfirmThreadCreationView() + view.add_item( + AcceptButton( + "accept-thread-creation", + self.outer_thread.bot.config["confirm_thread_creation_accept"], + ) + ) + view.add_item( + DenyButton( + "deny-thread-creation", + self.outer_thread.bot.config["confirm_thread_creation_deny"], + ) + ) + confirm = await dest.send( + embed=discord.Embed( + title=self.outer_thread.bot.config["confirm_thread_creation_title"], + description=self.outer_thread.bot.config["confirm_thread_response"], + color=self.outer_thread.bot.main_color, + ), + view=view, + ) + await view.wait() + if view.value is None: + # Timed out + self.outer_thread.cancelled = True + try: + await dest.send( + embed=discord.Embed( + title=self.outer_thread.bot.config["thread_cancelled"], + description="Timed out", + color=self.outer_thread.bot.error_color, + ) + ) + await confirm.edit(view=None) + except Exception: + logger.warning( + "Failed notifying user of thread creation timeout.", + exc_info=True, + ) + elif view.value is False: + self.outer_thread.cancelled = True + try: + await dest.send( + embed=discord.Embed( + title=self.outer_thread.bot.config["thread_cancelled"], + color=self.outer_thread.bot.error_color, + ) + ) + except Exception: + logger.warning( + "Failed notifying user of thread creation denial.", + exc_info=True, + ) + if self.outer_thread.cancelled: + # Clear pending/menu state and cache + try: + setattr(self.outer_thread, "_pending_menu", False) + self.outer_thread.manager.cache.pop(self.outer_thread.id, None) + except Exception: + logger.debug( + "Failed clearing pending menu/cache after cancellation.", + exc_info=True, + ) + return + except Exception: + # If confirm step fails, proceed to create thread to avoid dead-ends + logger.warning("Confirm step failed after menu selection; continuing.") + + self.outer_thread.bot.loop.create_task( + self.outer_thread.setup( + creator=creator, + category=use_category, + initial_message=message, + ) + ) + # Wait until channel is ready, then forward the original message like usual + try: + await self.outer_thread.wait_until_ready() + # Edge-case: unsnoozed restore might have re-created the channel but genesis send failed; ensure ready channel exists + if not self.outer_thread.channel: + logger.warning("Thread has no channel after unsnooze+selection; abort relay.") + setattr(self.outer_thread, "_pending_menu", False) + return + # Forward the user's initial DM to the thread channel + try: + await self.outer_thread.send(message) + except Exception: + logger.error( + "Failed to relay initial message after menu selection", + exc_info=True, + ) + else: + # React to the user's DM with the 'sent' emoji + try: + ( + sent_emoji, + _, + ) = await self.outer_thread.bot.retrieve_emoji() + await self.outer_thread.bot.add_reaction(message, sent_emoji) + except Exception as e: + logger.debug( + "Failed to add sent reaction to user's DM: %s", + e, + ) + # Dispatch thread_reply event for parity + self.outer_thread.bot.dispatch( + "thread_reply", + self.outer_thread, + False, + message, + False, + False, + ) + # Clear pending flag + setattr(self.outer_thread, "_pending_menu", False) + except Exception: + logger.warning( + "Unhandled failure after menu selection while waiting for channel readiness.", + exc_info=True, + ) + # Invoke command callback AFTER channel ready if type == command + if selected and selected.get("type") == "command": + alias = selected.get("callback") + if alias: + from discord.ext.commands.view import StringView + from core.utils import normalize_alias + + ctxs = [] + for al in normalize_alias(alias): + view_ = StringView(self.outer_thread.bot.prefix + al) + # Create a synthetic message object that makes the bot appear + # as the author for menu-invoked command replies so the user + # selecting the option is not shown as a "mod" sender. + synthetic = DummyMessage(copy.copy(message)) + try: + synthetic.author = ( + self.outer_thread.bot.modmail_guild.me or self.outer_thread.bot.user + ) + except Exception: + synthetic.author = self.outer_thread.bot.user + # Mark this message as menu-invoked for downstream formatting + setattr(synthetic, "_menu_invoked", True) + ctx_ = commands.Context( + prefix=self.outer_thread.bot.prefix, + view=view_, + bot=self.outer_thread.bot, + message=synthetic, + ) + ctx_.thread = self.outer_thread + discord.utils.find( + view_.skip_string, + await self.outer_thread.bot.get_prefix(), + ) + ctx_.invoked_with = view_.get_word().lower() + ctx_.command = self.outer_thread.bot.all_commands.get(ctx_.invoked_with) + # Mark context so downstream send/reply logic can treat as system/bot + setattr(ctx_, "_menu_invoked", True) + ctxs.append(ctx_) + for ctx_ in ctxs: + if ctx_.command: + old_checks = copy.copy(ctx_.command.checks) + ctx_.command.checks = [checks.has_permissions(PermissionLevel.INVALID)] + try: + await self.outer_thread.bot.invoke(ctx_) + finally: + ctx_.command.checks = old_checks + + class _ThreadCreationMenuView(discord.ui.View): + def __init__(self, outer_thread: Thread): + super().__init__(timeout=timeout) + self.outer_thread = outer_thread + self.add_item(_ThreadCreationMenuSelect(outer_thread)) + + async def on_timeout(self): + # Timeout -> abort thread creation + if self.outer_thread.bot.config.get("thread_creation_menu_close_on_timeout"): + try: + # Replace the entire embed with a minimal one containing only the timeout text + try: + color = (menu_msg.embeds[0].color if menu_msg.embeds else None) or getattr( + self.outer_thread.bot, "mod_color", None + ) + except Exception: + color = getattr(self.outer_thread.bot, "mod_color", None) + timeout_embed = ( + discord.Embed( + description="Menu timed out. Please send a new message to start again.", + color=color, + ) + if color + else discord.Embed( + description="Menu timed out. Please send a new message to start again." + ) + ) + await menu_msg.edit(content=None, embed=timeout_embed, view=None) + except Exception: + logger.info( + "Failed editing menu message on timeout (close_on_timeout enabled).", + exc_info=True, + ) + # remove thread from cache + try: + self.outer_thread.manager.cache.pop(self.outer_thread.id, None) + except Exception: + logger.debug( + "Failed popping thread from cache on timeout (close_on_timeout).", + exc_info=True, + ) + # Clear pending menu flag so a new message can recreate a fresh thread + setattr(self.outer_thread, "_pending_menu", False) + self.outer_thread.cancelled = True + else: + try: + # Replace the entire embed with a minimal one containing only the timeout text + try: + color = (menu_msg.embeds[0].color if menu_msg.embeds else None) or getattr( + self.outer_thread.bot, "mod_color", None + ) + except Exception: + color = getattr(self.outer_thread.bot, "mod_color", None) + timeout_embed = ( + discord.Embed( + description="Menu timed out. Please send a new message to start again.", + color=color, + ) + if color + else discord.Embed( + description="Menu timed out. Please send a new message to start again." + ) + ) + await menu_msg.edit(content=None, embed=timeout_embed, view=None) + except Exception: + logger.info( + "Failed editing menu message on timeout (keep alive mode).", + exc_info=True, + ) + # Allow subsequent messages to trigger a new menu/thread by clearing state + setattr(self.outer_thread, "_pending_menu", False) + try: + self.outer_thread.manager.cache.pop(self.outer_thread.id, None) + except Exception: + logger.debug( + "Failed popping thread from cache on timeout (keep alive mode).", + exc_info=True, + ) + self.outer_thread.cancelled = True + # Ensure view is stopped to release any internal tasks + try: + self.stop() + except Exception: + logger.debug( + "Failed stopping ThreadCreationMenuView on timeout.", + exc_info=True, + ) + + # Send DM prompt + try: + # Build embed with new customizable settings + try: + embed_title = self.bot.config.get("thread_creation_menu_embed_title") + embed_footer = self.bot.config.get("thread_creation_menu_embed_footer") + embed_thumb = self.bot.config.get("thread_creation_menu_embed_thumbnail_url") + embed_image = self.bot.config.get("thread_creation_menu_embed_image_url") + embed_footer_icon = self.bot.config.get("thread_creation_menu_embed_footer_icon_url") + embed_color_raw = self.bot.config.get("thread_creation_menu_embed_color") + except Exception: + embed_title = None + embed_footer = None + embed_thumb = None + embed_image = None + embed_footer_icon = None + embed_color_raw = None + embed_color = embed_color_raw or self.bot.mod_color + embed = discord.Embed(title=embed_title, description=embed_text, color=embed_color) + if embed_footer: + try: + if embed_footer_icon: + embed.set_footer(text=embed_footer, icon_url=embed_footer_icon) + else: + embed.set_footer(text=embed_footer) + except Exception as e: + logger.debug("Footer build failed (ignored): %s", e) + # Option A: prefer dedicated large image when provided + if embed_image: + try: + embed.set_image(url=embed_image) + except Exception as e: + logger.debug("Image set failed (ignored): %s", e) + elif embed_thumb: + try: + embed.set_thumbnail(url=embed_thumb) + except Exception as e: + logger.debug("Thumbnail set failed (ignored): %s", e) + menu_view = _ThreadCreationMenuView(thread) + menu_msg = await recipient.send(embed=embed, view=menu_view) + # mark thread as pending menu selection + thread._pending_menu = True + # Explicitly attach the message to the view for safety in callbacks + try: + menu_view.message = menu_msg + except Exception as e: + logger.info( + "Failed attaching menu message reference (initial menu send): %s", + e, + ) + # Store for later disabling on thread close + try: + thread._dm_menu_msg_id = menu_msg.id + thread._dm_menu_channel_id = menu_msg.channel.id + except Exception as e: + logger.info( + "Failed storing DM menu identifiers (initial menu send): %s", + e, + ) + except Exception: + logger.warning( + "Failed to send thread-creation menu DM, falling back to immediate thread creation.", + exc_info=True, + ) + self.bot.loop.create_task( + thread.setup(creator=creator, category=category, initial_message=message) + ) + return thread + + # If menu is enabled but precreate is requested, send the menu DM but do NOT defer creation. + # Selection becomes optional; thread channel will already be created below. + if adv_enabled and user_initiated and precreate: + try: + embed_text = self.bot.config.get("thread_creation_menu_embed_text") + placeholder = self.bot.config.get("thread_creation_menu_dropdown_placeholder") + timeout = int(self.bot.config.get("thread_creation_menu_timeout") or 20) + except Exception: + embed_text = "Please select an option." + placeholder = "Select an option to contact the staff team." + timeout = 20 + + options = self.bot.config.get("thread_creation_menu_options") or {} + + class _PrecreateMenuSelect(discord.ui.Select): + def __init__(self, outer_thread: Thread): + self.outer_thread = outer_thread + opts = [ + discord.SelectOption( + label=o["label"], + description=o["description"], + emoji=o["emoji"], + ) + for o in options.values() + ] + super().__init__( + placeholder=placeholder, + min_values=1, + max_values=1, + options=opts, + ) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=False) + # If thread somehow got snoozed before selection in precreate flow (rare), restore first. + try: + if self.outer_thread.snoozed: + await self.outer_thread.restore_from_snooze() + except Exception: + logger.warning( + "Failed unsnoozing thread prior to precreate menu selection; continuing.", + exc_info=True, + ) + chosen_label = self.values[0] + key = chosen_label.lower().replace(" ", "_") + selected = options.get(key) + self.outer_thread._selected_thread_creation_menu_option = selected + # Remove the view + try: + msg = getattr(interaction, "message", None) + if msg is None and self.view and hasattr(self.view, "message"): + msg = self.view.message + if msg is not None: + # Replace entire embed so only the selection line remains + try: + base_color = (msg.embeds[0].color if msg.embeds else None) or getattr( + self.outer_thread.bot, "mod_color", None + ) + except Exception: + base_color = getattr(self.outer_thread.bot, "mod_color", None) + selection_embed = ( + discord.Embed( + description=f"You selected: {chosen_label}", + color=base_color, + ) + if base_color is not None + else discord.Embed(description=f"You selected: {chosen_label}") + ) + await msg.edit(content=None, embed=selection_embed, view=None) + else: + try: + await interaction.edit_original_response( + content=f"You selected: {chosen_label}", view=None + ) + except Exception: + await interaction.edit_original_response(view=None) + except Exception: + try: + await interaction.edit_original_response(view=None) + except Exception: + logger.debug( + "Failed secondary edit_original_response path removing view.", + exc_info=True, + ) + if self.view: + try: + self.view.stop() + except Exception: + logger.debug( + "Failed removing view after selection (stop).", + exc_info=True, + ) + # Log selection to thread channel if configured + try: + await self.outer_thread.wait_until_ready() + if self.outer_thread.bot.config.get("thread_creation_menu_selection_log"): + opt = selected or {} + log_txt = f"Selected menu option: {opt.get('label')} ({opt.get('type')})" + if opt.get("type") == "command": + log_txt += f" -> {opt.get('callback')}" + await self.outer_thread.channel.send( + embed=discord.Embed( + description=log_txt, + color=self.outer_thread.bot.mod_color, + ) + ) + # If a category_id is set on the option, move the channel accordingly + try: + cat_id = selected.get("category_id") if isinstance(selected, dict) else None + if cat_id: + guild = self.outer_thread.bot.modmail_guild + target = guild and guild.get_channel(int(cat_id)) + if isinstance(target, discord.CategoryChannel): + await self.outer_thread.channel.edit( + category=target, + reason="Menu selection: move to category", + ) + except Exception: + logger.debug( + "Failed moving thread channel based on selected category_id.", + exc_info=True, + ) + except Exception: + logger.debug( + "Failed logging menu selection or moving category in precreate flow.", + exc_info=True, + ) + # If the option type is command, invoke it now within the created thread + if selected and selected.get("type") == "command": + alias = selected.get("callback") + if alias: + from discord.ext.commands.view import StringView + from core.utils import normalize_alias + + ctxs = [] + for al in normalize_alias(alias): + view_ = StringView(self.outer_thread.bot.prefix + al) + synthetic = DummyMessage(copy.copy(message)) + try: + synthetic.author = ( + self.outer_thread.bot.modmail_guild.me or self.outer_thread.bot.user + ) + except Exception: + synthetic.author = self.outer_thread.bot.user + setattr(synthetic, "_menu_invoked", True) + ctx_ = commands.Context( + prefix=self.outer_thread.bot.prefix, + view=view_, + bot=self.outer_thread.bot, + message=synthetic, + ) + ctx_.thread = self.outer_thread + discord.utils.find( + view_.skip_string, + await self.outer_thread.bot.get_prefix(), + ) + ctx_.invoked_with = view_.get_word().lower() + ctx_.command = self.outer_thread.bot.all_commands.get(ctx_.invoked_with) + setattr(ctx_, "_menu_invoked", True) + ctxs.append(ctx_) + for ctx_ in ctxs: + if ctx_.command: + old_checks = copy.copy(ctx_.command.checks) + ctx_.command.checks = [checks.has_permissions(PermissionLevel.INVALID)] + try: + await self.outer_thread.bot.invoke(ctx_) + finally: + ctx_.command.checks = old_checks + + class _PrecreateMenuView(discord.ui.View): + def __init__(self, outer_thread: Thread): + super().__init__(timeout=timeout) + self.add_item(_PrecreateMenuSelect(outer_thread)) + + async def on_timeout(self): + try: + # Replace the entire embed with a minimal one containing only the timeout text + try: + color = menu_msg.embeds[0].color if menu_msg.embeds else None + except Exception: + color = None + timeout_embed = ( + discord.Embed( + description="Menu timed out. Please send a new message to start again.", + color=color, + ) + if color + else discord.Embed( + description="Menu timed out. Please send a new message to start again." + ) + ) + await menu_msg.edit(content=None, embed=timeout_embed, view=None) + except Exception: + logger.info( + "Failed editing precreate menu message on timeout.", + exc_info=True, + ) + try: + self.stop() + except Exception: + logger.debug( + "Failed stopping PrecreateMenuView on timeout.", + exc_info=True, + ) + + try: + # Build embed with new customizable settings (precreate flow) + try: + embed_title = self.bot.config.get("thread_creation_menu_embed_title") + embed_footer = self.bot.config.get("thread_creation_menu_embed_footer") + embed_thumb = self.bot.config.get("thread_creation_menu_embed_thumbnail_url") + embed_image = self.bot.config.get("thread_creation_menu_embed_image_url") + embed_large = bool(self.bot.config.get("thread_creation_menu_embed_large_image")) + embed_footer_icon = self.bot.config.get("thread_creation_menu_embed_footer_icon_url") + embed_color_raw = self.bot.config.get("thread_creation_menu_embed_color") + except Exception: + embed_title = None + embed_footer = None + embed_thumb = None + embed_image = None + embed_large = False + embed_footer_icon = None + embed_color_raw = None + embed_color = embed_color_raw or self.bot.mod_color + embed = discord.Embed(title=embed_title, description=embed_text, color=embed_color) + if embed_footer: + try: + if embed_footer_icon: + embed.set_footer(text=embed_footer, icon_url=embed_footer_icon) + else: + embed.set_footer(text=embed_footer) + except Exception as e: + logger.debug("Footer build failed (ignored precreate): %s", e) + if embed_image: + try: + embed.set_image(url=embed_image) + except Exception as e: + logger.debug("Image set failed (ignored precreate): %s", e) + elif embed_thumb: + try: + if embed_large: + embed.set_image(url=embed_thumb) + else: + embed.set_thumbnail(url=embed_thumb) + except Exception as e: + logger.debug("Thumbnail/image set failed (ignored precreate): %s", e) + menu_view = _PrecreateMenuView(thread) + # Send menu DM AFTER channel creation initiation (channel will be created below) + menu_msg = await recipient.send(embed=embed, view=menu_view) + try: + menu_view.message = menu_msg + except Exception: + logger.debug( + "Failed attaching menu message reference (precreate menu).", + exc_info=True, + ) + # Store for later disabling on thread close + try: + thread._dm_menu_msg_id = menu_msg.id + thread._dm_menu_channel_id = menu_msg.channel.id + except Exception: + logger.debug( + "Failed storing DM menu identifiers (precreate menu).", + exc_info=True, + ) + except Exception: + logger.debug("Failed to send precreate menu DM; proceeding without menu.") + + # Regular immediate creation (force main_category for user-initiated menu flows) + forced_category = None + if adv_enabled and user_initiated and precreate: + # In precreate mode we still create immediately (main category override optional) + forced_category = self.bot.main_category + chosen_category = forced_category or category + self.bot.loop.create_task( + thread.setup(creator=creator, category=chosen_category, initial_message=message) + ) return thread async def find_or_create(self, recipient) -> Thread: diff --git a/core/time.py b/core/time.py index b6ce91f419..67351792ee 100644 --- a/core/time.py +++ b/core/time.py @@ -89,7 +89,12 @@ def __init__(self, argument: str, *, now: Optional[datetime.datetime] = None): 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) + dt = dt.replace( + hour=now.hour, + minute=now.minute, + second=now.second, + microsecond=now.microsecond, + ) self.dt: datetime.datetime = dt self._past: bool = dt < now @@ -158,7 +163,11 @@ def __init__(self, dt: datetime.datetime, now: datetime.datetime = None): self.arg = "" async def ensure_constraints( - self, ctx: Context, uft: UserFriendlyTime, now: datetime.datetime, remaining: str + self, + ctx: Context, + uft: UserFriendlyTime, + now: datetime.datetime, + remaining: str, ) -> None: # Strip stray connector words like "in", "to", or "at" that may # remain when the natural language parser isolates the time token @@ -301,7 +310,12 @@ async def convert(self, ctx: Context, argument: str, *, now=None) -> FriendlyTim 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) + dt = dt.replace( + hour=now.hour, + minute=now.minute, + second=now.second, + microsecond=now.microsecond, + ) # if midnight is provided, just default to next day if status.accuracy == pdt.pdtContext.ACU_HALFDAY: diff --git a/core/utils.py b/core/utils.py index 19ff4cd37b..dc8737ead6 100644 --- a/core/utils.py +++ b/core/utils.py @@ -258,7 +258,9 @@ def cleanup_code(content: str) -> str: UID_REGEX = re.compile(r"\bUser ID:\s*(\d{17,21})\b", flags=re.IGNORECASE) -def parse_channel_topic(text: str) -> typing.Tuple[typing.Optional[str], int, typing.List[int]]: +def parse_channel_topic( + text: str, +) -> typing.Tuple[typing.Optional[str], int, typing.List[int]]: """ A helper to parse channel topics and respectivefully returns all the required values at once. @@ -359,7 +361,8 @@ def match_other_recipients(text: str) -> typing.List[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: @@ -369,7 +372,7 @@ def create_not_found_embed(word, possibilities, name, n=2, cutoff=0.6) -> discor def parse_alias(alias, *, split=True): def encode_alias(m): - return "\x1AU" + base64.b64encode(m.group(1).encode()).decode() + "\x1AU" + return "\x1aU" + base64.b64encode(m.group(1).encode()).decode() + "\x1aU" def decode_alias(m): return base64.b64decode(m.group(1).encode()).decode() @@ -632,7 +635,8 @@ async def callback(self, interaction: discord.Interaction): class ConfirmThreadCreationView(discord.ui.View): def __init__(self): - super().__init__(timeout=20) + # Match thread_creation_menu_timeout default (30s) for consistency in UX + super().__init__(timeout=30) self.value = None @@ -721,8 +725,9 @@ def extract_forwarded_content(message) -> typing.Optional[str]: if len(ref_msg.attachments) > 3: attachment_info += f" (+{len(ref_msg.attachments) - 3} more)" return f"**{ref_author_name}:** [Attachments: {attachment_info}]" - except Exception: - pass + except Exception as e: + # Log and continue; failing to extract a reference preview shouldn't break flow + logger.debug("Failed to extract reference preview: %s", e) except Exception: # Silently handle any unexpected errors pass diff --git a/pyproject.toml b/pyproject.toml index 0a6d6eaa6c..719abc9447 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ extend-exclude = ''' [tool.poetry] name = 'Modmail' -version = '4.2.0' +version = '4.2.1' description = "Modmail is similar to Reddit's 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 17dd88564958a1eb07941233d0c1af36c039eadf Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Fri, 28 Nov 2025 00:34:44 -0800 Subject: [PATCH 1216/1221] Fix GH Workflow with black Formatting --- cogs/modmail.py | 4 +++- core/thread.py | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 0a97888646..b0e38ed9e0 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1803,7 +1803,9 @@ async def contact( try: await msg.delete(delay=10) except (discord.Forbidden, discord.NotFound) as e: - logger.debug(f"Failed to delete message (likely already deleted or lacking permissions): {e}") + logger.debug( + f"Failed to delete message (likely already deleted or lacking permissions): {e}" + ) # Don't try to create a new thread - we just unsnoozed existing ones return diff --git a/core/thread.py b/core/thread.py index 39edb2b482..bf77180f8c 100644 --- a/core/thread.py +++ b/core/thread.py @@ -224,12 +224,16 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None): "author_name": ( getattr(m.embeds[0].author, "name", "").split(" (")[0] if m.embeds and m.embeds[0].author and m.author == self.bot.user - else getattr(m.author, "name", None) if m.author != self.bot.user else None + else getattr(m.author, "name", None) + if m.author != self.bot.user + else None ), "author_avatar": ( getattr(m.embeds[0].author, "icon_url", None) if m.embeds and m.embeds[0].author and m.author == self.bot.user - else m.author.display_avatar.url if m.author != self.bot.user else None + else m.author.display_avatar.url + if m.author != self.bot.user + else None ), } async for m in channel.history(limit=None, oldest_first=True) From bcd11935a610949e9f4e889f825367fb372dca43 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Fri, 28 Nov 2025 00:39:38 -0800 Subject: [PATCH 1217/1221] Add new sponsor to json --- SPONSORS.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/SPONSORS.json b/SPONSORS.json index cdaf3621ac..955b839fd7 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -131,5 +131,21 @@ "description": "*Your Retail Journey*\n*\"Better choice and better value in food, fashion & homewares.\"*\n\n\n**------------------------------------------**\n*__About us__*\nSupermarket, CityStore PLC! Attend a training to become staff!\n\nThis game is currently in V3\n\nWe have a training Centre and applications center!\n\n**------------------------------------------**\n\n> *❤️ Don't hesitate! Dive into the excitement today by joining our vibrant community on Discord. Experience our unique perspective and become an integral part of our group. Your **journey** with us promises to be unforgettable no regrets, only great memories await! ❤️*\n\n*We hope to see you. *\n\n*Signed,*\n**CityStore PLC**\n> Discord: https://discord.gg/yjFQb5mrSk\n> Roblox Group: https://www.roblox.com/groups/32819373/CityStore-PLC#!/about\n\nJoin us now and become apart of Citystore PLC community! 🎉", "color": 15523550 } + }, + { + "embed": { + "description": "✨ *\"Let's bake it!\"* ✨ \n\nKistó is a very successful and well-known **Bakery Group** on the platform. Its goal is to give every guest the **ultimate bakery experience**. \n\nWe have a wide variety of hand-made treats, from our rich **drinks** to our freshly baked **pastries**. Every item is made with care by our **skilled and passionate team**, redefining what it means to be a modern bakery. \n\n💖 Come visit us today and taste the *sweet side of perfection* at **Kistó Bakery.** \n\n**Roblox Group:** [Click here!](https://www.roblox.com/communities/9318596/Kist#!/about) \n**Discord Server:** [Click here!](https://discord.gg/aGt8Wv3gP9)", + "color": 16736255, + "author": { + "name": "Kistó Bakery", + "icon_url": "https://cdn.discordapp.com/attachments/1413609998797242522/1436882475883298887/noFilter_10_1.png" + }, + "footer": { + "text": "Proudly serving since 2021" + }, + "image": { + "url": "https://cdn.discordapp.com/attachments/1413609998797242522/1436882475543429260/0904.png" + } + } } ] From 665e67ea580d3ddcb807c034fabe9bb10629cb11 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Sun, 30 Nov 2025 02:58:12 -0800 Subject: [PATCH 1218/1221] Update version to v4.2.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7470c9900..58243cab61 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ <br> <a href="#"> - <img src="https://img.shields.io/badge/Latest%20Version-v4.2.0-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> + <img src="https://img.shields.io/badge/Latest%20Version-v4.2.1-7289da?style=for-the-badge&logo=data:image/gif;base64,R0lGODlhGAAYAPcAAAAAADQ+Yj5MdThCaEFOekNQfWt5e1lqfEdVhVpriVx1iVtsnFZpll1xlF1znFx5mGNtjGR7hWNzjGd5iWJunGNslmN0lGl1lGN5lWt7lWR0nGp1nWR5nGt8nHJ9l3J9jFxto1xtqlZqqF5zpF15pV10rVx5q1Z2ql1us150s114sV50uVd1smJupGNurGN0pGp1pGR6o2t7o2F0rml0rGR5rGt8q3J9pGNus2Fvu2Bzs2p2smR5s2t7tGF0u2N6vGp7u2l1uXJ+uGJ1wWV7w2l+xWl+yl5zwHWDiWyCmHODnHiGmHSLkW2CpGuEqnSCpHqMo3ODrHWKrXyMq3iHp3yTqGyEtWqFunKDs3uLtHSGvXSMvHuMu3eItnyTs3yTu3iUuW+RsV+ApmuCxG2Dy22LxnSMxHuMxHGGzXSLzXqLzXWFxXyTw3uSy3WSx26E0W+N2m+L1XKG03SL1XmL1HOL2HmL23WF2XiS1m+QyHOK4XmL4XaH4nGM5ImXm4WOlJSjnoOOq4OOpYSUrImXqJOcqYONt4OUtIybtIOUu4ycvImYt5OcuIyiqZekqYyiupOjvJurvJimtp+yvqKruae2ua23trLCucPHvIOMxoOUxIuaw4eYyJKbyISW1YiW0o2kyJWlxJqpxZWkzJury5aoxp6xxZ2zyZio1Jyy2Yuk0aOsyKOyxKOzzKy8zKq2yLO7xqOr1KSz1Ku706W026y73Ki21bK716u75LS95KSu4Zyx6bPDzLjGx63B2LPD1LTE3LvL3brI17/S2bbKzrTE47rF47zM5L3M67bH577R5L7R67zK8rjM8MLM3MPK1sTT3crU3MjW2dLZ2srUy8PN5MTO68nP68jL48TS5MnT5cbR68jS7M3b7Mva5dTb6MTO8srO8szF8MXR8snS8c3a8cnY+NXb9tDL7dnm6dzk8tro99Pq9uTp6+r17fr77eTr/efr+Oz09OXz/Ov0/e37/uj39fX19fX89fz+9vP1/fX9/f7//vr2/ODl4sK75yH5BAEAAAAALAAAAAAYABgAAAj/AAEAmFOnoMGDCAvGmRNHYMKHEOdIhEgRIcGKGDMeTJVO00WNBj194/frFik5IOvgqdaP371N26xpefORYq57/HI+s7Vtmy0eKCmmapcz575FPXt+SfHmociiRae1SroNWIoVTQ2uxAk15yJg1awl7ZSCaUE82uRxhTpt1TNXq0Ip2pSobIqCc0iVoqsI0ipXr1bhywfVXxYdiA3Sourz2dqc8lbNQJz4bDWqix73E7ZjhgrKOjYmvfWLbZcZqCdTPjjHGLdtibi2U5Q6NWiEqqq64ifvVe3at1nX2tTuWQ/QyIMfxHOGi+rkyeMklPMDuvWaBqlbj469IPUU2ykPCOw+53t4AAEBADs="> </a> <br> From 7322e99c3510eb648b811d99b5cf13229ce3653c Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:05:01 -0800 Subject: [PATCH 1219/1221] Update gitignore --- .gitignore | 175 ++++++++++++++++++++--------------------------------- 1 file changed, 65 insertions(+), 110 deletions(-) diff --git a/.gitignore b/.gitignore index 635e3160e8..e165a06432 100644 --- a/.gitignore +++ b/.gitignore @@ -1,93 +1,29 @@ +#### Modmail +# Plugins +plugins/* +!plugins/registry.json +!plugins/@local + +# Config files +config.json +.env + +# Data +temp/ + +### Python # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - # Installer logs pip-log.txt pip-delete-this-directory.txt -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - # pyenv .python-version -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - # Environments .env .venv @@ -98,43 +34,62 @@ ENV/ env.bak/ venv.bak/ -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject +#### PyCharm +.idea/ -# mkdocs documentation -/site +#### VS Code +.vscode/ -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json +### Windows +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db -# Pyre type checker -.pyre/ +# Dump file +*.stackdump -# PyCharm -.idea/ +# Folder config file +[Dd]esktop.ini -# MacOS -.DS_Store +# Recycle Bin used on file shares +$RECYCLE.BIN/ -# VS Code -.vscode/ +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp -# Node -package-lock.json -node_modules/ +# Windows shortcuts +*.lnk -# Modmail -plugins/* -!plugins/registry.json -!plugins/@local -config.json -temp/ -test.py -stack.yml -.github/workflows/build_docker.yml \ No newline at end of file +### macOS +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk From ab863db0410be47ad50bcaadea387b4a774c6458 Mon Sep 17 00:00:00 2001 From: Martin <55140357+martinbndr@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:03:48 +0100 Subject: [PATCH 1220/1221] Replace claim plugin (#3437) Replaces the claim plugin by fourjr to my claim plugin due to being fundamentally broken as of the current time. It has been created few support issues already that were not successfull to use the plugin. --- plugins/registry.json | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index 4079001a50..ee6b17b792 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -1,13 +1,4 @@ { - "advanced-menu": { - "repository": "sebkuip/mm-plugins", - "branch": "master", - "description": "Advanced menu plugin using dropdown selectors. Supports submenus (and sub-submenus infinitely).", - "bot_version": "v4.0.0", - "title": "Advanced menu", - "icon_url": "https://raw.githubusercontent.com/sebkuip/mm-plugins/master/advanced-menu/logo.png", - "thumbnail_url": "https://raw.githubusercontent.com/sebkuip/mm-plugins/master/advanced-menu/logo.png" - }, "announcement": { "repository": "Jerrie-Aries/modmail-plugins", "branch": "master", @@ -72,13 +63,13 @@ "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" }, "claim": { - "repository": "fourjr/modmail-plugins", - "branch": "v4", - "description": "Allows supporters to claim thread by sending ?claim in the thread channel", - "bot_version": "4.0.0", + "repository": "martinbndr/kyb3r-modmail-plugins", + "branch": "master", + "description": "Adds claim functionality to your modmail bot.", + "bot_version": "4.2.1", "title": "Claim Thread", - "icon_url": "https://i.imgur.com/Mo60CdK.png", - "thumbnail_url": "https://i.imgur.com/Mo60CdK.png" + "icon_url": "https://i.ibb.co/dsPjgKLj/87249157.png", + "thumbnail_url": "https://i.ibb.co/dsPjgKLj/87249157.png" }, "emote-manager": { "repository": "fourjr/modmail-plugins", @@ -134,4 +125,4 @@ "icon_url": "https://i.imgur.com/A1auJ95.png", "thumbnail_url": "https://i.imgur.com/A1auJ95.png" } -} +} \ No newline at end of file From 49330b4a0eb1004af3085c0683e8d09608109277 Mon Sep 17 00:00:00 2001 From: Taku <45324516+Taaku18@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:48:52 +0800 Subject: [PATCH 1221/1221] Update sponsors info --- README.md | 7 ------- SPONSORS.json | 16 ---------------- 2 files changed, 23 deletions(-) diff --git a/README.md b/README.md index 58243cab61..8f78406ea9 100644 --- a/README.md +++ b/README.md @@ -152,13 +152,6 @@ Discord Advice Center: <a href='https://discord.gg/zmwZy5fd9v'> <img height=100 src='https://i.imgur.com/1hrjcHd.png' style='margin:5px'> </a> -<br> -<br> -Kistó Bakery: -<br> -<a href='https://www.roblox.com/communities/9318596/Kist#!/about'> - <img height=100 src='https://i.imgur.com/ck81JiJ.jpeg' style='margin:5px'> -</a> Become a sponsor on [Buy Me A Coffee](https://buymeacoffee.com/modmaildev/membership). diff --git a/SPONSORS.json b/SPONSORS.json index 955b839fd7..cdaf3621ac 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -131,21 +131,5 @@ "description": "*Your Retail Journey*\n*\"Better choice and better value in food, fashion & homewares.\"*\n\n\n**------------------------------------------**\n*__About us__*\nSupermarket, CityStore PLC! Attend a training to become staff!\n\nThis game is currently in V3\n\nWe have a training Centre and applications center!\n\n**------------------------------------------**\n\n> *❤️ Don't hesitate! Dive into the excitement today by joining our vibrant community on Discord. Experience our unique perspective and become an integral part of our group. Your **journey** with us promises to be unforgettable no regrets, only great memories await! ❤️*\n\n*We hope to see you. *\n\n*Signed,*\n**CityStore PLC**\n> Discord: https://discord.gg/yjFQb5mrSk\n> Roblox Group: https://www.roblox.com/groups/32819373/CityStore-PLC#!/about\n\nJoin us now and become apart of Citystore PLC community! 🎉", "color": 15523550 } - }, - { - "embed": { - "description": "✨ *\"Let's bake it!\"* ✨ \n\nKistó is a very successful and well-known **Bakery Group** on the platform. Its goal is to give every guest the **ultimate bakery experience**. \n\nWe have a wide variety of hand-made treats, from our rich **drinks** to our freshly baked **pastries**. Every item is made with care by our **skilled and passionate team**, redefining what it means to be a modern bakery. \n\n💖 Come visit us today and taste the *sweet side of perfection* at **Kistó Bakery.** \n\n**Roblox Group:** [Click here!](https://www.roblox.com/communities/9318596/Kist#!/about) \n**Discord Server:** [Click here!](https://discord.gg/aGt8Wv3gP9)", - "color": 16736255, - "author": { - "name": "Kistó Bakery", - "icon_url": "https://cdn.discordapp.com/attachments/1413609998797242522/1436882475883298887/noFilter_10_1.png" - }, - "footer": { - "text": "Proudly serving since 2021" - }, - "image": { - "url": "https://cdn.discordapp.com/attachments/1413609998797242522/1436882475543429260/0904.png" - } - } } ]

      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 Date: Tue, 10 Nov 2020 23:31:31 +0800 Subject: [PATCH 0677/1221] Bump version to dev12 --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index b85a61856b..7a04011382 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev11" +__version__ = "3.7.0-dev12" import asyncio From ff9888bc34d3dcaaa640dd08002458b5db7b43f7 Mon Sep 17 00:00:00 2001 From: Cyrus Yip Date: Tue, 10 Nov 2020 07:56:42 -0800 Subject: [PATCH 0678/1221] Create and delete persistent notes --- cogs/modmail.py | 2 +- core/clients.py | 34 +++++++++++++++++++++++++----- core/thread.py | 56 +++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 07fb0c5b7b..41040b5ead 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -901,7 +901,7 @@ async def note_persistent(self, ctx, *, msg: str = ""): async with ctx.typing(): msg = await ctx.thread.note(ctx.message, persistent=True) await msg.pin() - await self.bot.api.create_note(recipient=ctx.thread.recipient, message=ctx.message) + await self.bot.api.create_note(recipient=ctx.thread.recipient, message=ctx.message, message_id=msg.id) @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) diff --git a/core/clients.py b/core/clients.py index 6d74290105..b03b85044a 100644 --- a/core/clients.py +++ b/core/clients.py @@ -142,6 +142,21 @@ async def search_closed_by(self, user_id: Union[int, str]): async def search_by_text(self, text: str, limit: Optional[int]): return NotImplemented + async def create_note(self, recipient: Member, message: Message, message_id: Union[int, str]): + return NotImplemented + + async def find_notes(self, recipient: Member): + return NotImplemented + + async def update_note_ids(self, ids: dict): + return NotImplemented + + async def delete_note(self, message_id: Union[int, str]): + return NotImplemented + + async def edit_note(self, message_id: Union[int, str]): + return NotImplemented + def get_plugin_partition(self, cog): return NotImplemented @@ -401,7 +416,7 @@ async def search_by_text(self, text: str, limit: Optional[int]): {"messages": {"$slice": 5}}, ).to_list(limit) - async def create_note(self, recipient: Member, message: Message): + async def create_note(self, recipient: Member, message: Message, message_id: Union[int, str]): await self.db.notes.insert_one( { "recipient": str(recipient.id), @@ -409,17 +424,26 @@ async def create_note(self, recipient: Member, message: Message): "id": str(message.author.id), "name": message.author.name, "discriminator": message.author.discriminator, - "avatar_url": str(message.author.avatar_url), + "avatar_url": str(message.author.avatar_url) }, + "message": message.content, + "message_id": str(message_id), } ) - async def delete_note(self): - pass - async def find_notes(self, recipient: Member): return await self.db.notes.find({"recipient": str(recipient.id)}).to_list(None) + async def update_note_ids(self, ids: dict): + for object_id, message_id in ids.items(): + await self.db.notes.update_one({"_id": object_id}, {"$set": {"message_id": message_id}}) + + async def delete_note(self, message_id: Union[int, str]): + await self.db.notes.delete_one({"message_id": str(message_id)}) + + async def edit_note(self, message_id: Union[int, str]): + return NotImplemented + def get_plugin_partition(self, cog): cls_name = cog.__class__.__name__ return self.db.plugins[cls_name] diff --git a/core/thread.py b/core/thread.py index 7786e89ace..e4cac3e666 100644 --- a/core/thread.py +++ b/core/thread.py @@ -3,6 +3,7 @@ import re import typing from datetime import datetime, timedelta +import time from types import SimpleNamespace import isodate @@ -191,13 +192,42 @@ async def send_recipient_genesis_message(): await self.bot.add_reaction(msg, close_emoji) async def send_persistent_notes(): - notes = await self.bot.api.find_notes() + notes = await self.bot.api.find_notes(self.recipient) + ids = {} + + class State: + def store_user(self, user): + return user + for note in notes: - message = discord.Message() - await self.note(note.message) - pass + author = note["author"] + + class Author: + name = author["name"] + id = author["id"] + discriminator = author["discriminator"] + avatar_url = author["avatar_url"] + + data = { + "id": round(time.time() * 1000 - discord.utils.DISCORD_EPOCH) << 22, + "attachments": {}, + "embeds": {}, + "edited_timestamp": None, + "type": None, + "pinned": None, + "mention_everyone": None, + "tts": None, + "content": note["message"], + "author": Author(), + } + message = discord.Message(state=State(), channel=None, data=data) + ids[note["_id"]] = str((await self.note(message, persistent=True, thread_creation=True)).id) + + await self.bot.api.update_note_ids(ids) - await asyncio.gather(send_genesis_message(), send_recipient_genesis_message()) + await asyncio.gather( + send_genesis_message(), send_recipient_genesis_message(), send_persistent_notes() + ) self.bot.dispatch("thread_ready", self) def _format_info_embed(self, user, log_url, log_count, color): @@ -517,11 +547,14 @@ async def find_linked_messages( ): raise ValueError("Thread message not found.") - if message1.embeds[0].color.value == self.bot.main_color and message1.embeds[ - 0 - ].author.name.startswith("Note"): + if message1.embeds[0].color.value == self.bot.main_color and ( + message1.embeds[0].author.name.startswith("Note") + or message1.embeds[0].author.name.startswith("Persistent Note") + ): if not note: raise ValueError("Thread message not found.") + elif message1.embeds[0].author.name.startswith("Persistent Note"): + await self.bot.api.delete_note(message_id) return message1, None if message1.embeds[0].color.value != self.bot.mod_color and not ( @@ -636,11 +669,11 @@ async def edit_dm_message(self, message: discord.Message, content: str) -> None: self.bot.api.edit_message(message.id, content), linked_message.edit(embed=embed) ) - async def note(self, message: discord.Message, persistent=False) -> None: + async def note(self, message: discord.Message, persistent=False, thread_creation=False) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) - msg = await self.send(message, self.channel, note=True, persistent_note=persistent) + msg = await self.send(message, self.channel, note=True, persistent_note=persistent, thread_creation=thread_creation) self.bot.loop.create_task( self.bot.api.append_log( @@ -727,6 +760,7 @@ async def send( anonymous: bool = False, plain: bool = False, persistent_note: bool = False, + thread_creation: bool = False, ) -> None: self.bot.loop.create_task( @@ -876,7 +910,7 @@ async def send( embed.set_footer(text=f"Message ID: {message.id}") embed.colour = self.bot.recipient_color - if from_mod or note: + if (from_mod or note) and not thread_creation: delete_message = not bool(message.attachments) if delete_message and destination == self.channel: try: From c097db9c305182daad883cd276d8ae2b56904f61 Mon Sep 17 00:00:00 2001 From: Cyrus Yip Date: Tue, 10 Nov 2020 18:40:15 -0800 Subject: [PATCH 0679/1221] move api.delete_note to the right place --- core/thread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index 0f11aba6c7..0094b9bf2f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -569,8 +569,6 @@ async def find_linked_messages( ): if not note: raise ValueError("Thread message not found.") - elif message1.embeds[0].author.name.startswith("Persistent Note"): - await self.bot.api.delete_note(message_id) return message1, None if message1.embeds[0].color.value != self.bot.mod_color and not ( @@ -646,6 +644,8 @@ async def delete_message( tasks += [message1.delete()] if message2 is not None: tasks += [message2.delete()] + elif message1.embeds[0].author.name.startswith("Persistent Note"): + await self.bot.api.delete_note(message1.id) if tasks: await asyncio.gather(*tasks) From 55281cc27f682d610779b0f9c8350e748149919c Mon Sep 17 00:00:00 2001 From: Cyrus Yip Date: Tue, 10 Nov 2020 18:42:54 -0800 Subject: [PATCH 0680/1221] add to tasks --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 0094b9bf2f..2dd20ce4d2 100644 --- a/core/thread.py +++ b/core/thread.py @@ -645,7 +645,7 @@ async def delete_message( if message2 is not None: tasks += [message2.delete()] elif message1.embeds[0].author.name.startswith("Persistent Note"): - await self.bot.api.delete_note(message1.id) + tasks += [self.bot.api.delete_note(message1.id)] if tasks: await asyncio.gather(*tasks) From 18bcb9eb7505d1cc3421e429f6d89478b2ffce71 Mon Sep 17 00:00:00 2001 From: Cyrus Yip Date: Tue, 10 Nov 2020 18:47:15 -0800 Subject: [PATCH 0681/1221] edit persistent notes --- core/clients.py | 6 +++--- core/thread.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/clients.py b/core/clients.py index b03b85044a..300425be53 100644 --- a/core/clients.py +++ b/core/clients.py @@ -154,7 +154,7 @@ async def update_note_ids(self, ids: dict): async def delete_note(self, message_id: Union[int, str]): return NotImplemented - async def edit_note(self, message_id: Union[int, str]): + async def edit_note(self, message_id: Union[int, str], message: str): return NotImplemented def get_plugin_partition(self, cog): @@ -441,8 +441,8 @@ async def update_note_ids(self, ids: dict): async def delete_note(self, message_id: Union[int, str]): await self.db.notes.delete_one({"message_id": str(message_id)}) - async def edit_note(self, message_id: Union[int, str]): - return NotImplemented + async def edit_note(self, message_id: Union[int, str], message: str): + await self.db.notes.update_one({"message_id": str(message_id)}, {"$set": {"message": message}}) def get_plugin_partition(self, cog): cls_name = cog.__class__.__name__ diff --git a/core/thread.py b/core/thread.py index 2dd20ce4d2..3562d98ee3 100644 --- a/core/thread.py +++ b/core/thread.py @@ -629,6 +629,8 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) -> embed2 = message2.embeds[0] embed2.description = message tasks += [message2.edit(embed=embed2)] + elif message1.embeds[0].author.name.startswith("Persistent Note"): + tasks += [self.bot.api.edit_note(message1.id, message)] await asyncio.gather(*tasks) From 18d3b4b7e1a7c6f8b8d6fd5f992ac09281d0cf88 Mon Sep 17 00:00:00 2001 From: Cyrus Yip Date: Tue, 10 Nov 2020 18:49:58 -0800 Subject: [PATCH 0682/1221] format --- cogs/modmail.py | 4 +++- core/clients.py | 10 +++++++--- core/thread.py | 21 +++++++++++++++++---- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 3207bcdd5b..b977d8fdf4 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -899,7 +899,9 @@ async def note_persistent(self, ctx, *, msg: str = ""): async with ctx.typing(): msg = await ctx.thread.note(ctx.message, persistent=True) await msg.pin() - await self.bot.api.create_note(recipient=ctx.thread.recipient, message=ctx.message, message_id=msg.id) + await self.bot.api.create_note( + recipient=ctx.thread.recipient, message=ctx.message, message_id=msg.id + ) @commands.command() @checks.has_permissions(PermissionLevel.SUPPORTER) diff --git a/core/clients.py b/core/clients.py index 300425be53..62f565f91c 100644 --- a/core/clients.py +++ b/core/clients.py @@ -424,7 +424,7 @@ async def create_note(self, recipient: Member, message: Message, message_id: Uni "id": str(message.author.id), "name": message.author.name, "discriminator": message.author.discriminator, - "avatar_url": str(message.author.avatar_url) + "avatar_url": str(message.author.avatar_url), }, "message": message.content, "message_id": str(message_id), @@ -436,13 +436,17 @@ async def find_notes(self, recipient: Member): async def update_note_ids(self, ids: dict): for object_id, message_id in ids.items(): - await self.db.notes.update_one({"_id": object_id}, {"$set": {"message_id": message_id}}) + await self.db.notes.update_one( + {"_id": object_id}, {"$set": {"message_id": message_id}} + ) async def delete_note(self, message_id: Union[int, str]): await self.db.notes.delete_one({"message_id": str(message_id)}) async def edit_note(self, message_id: Union[int, str], message: str): - await self.db.notes.update_one({"message_id": str(message_id)}, {"$set": {"message": message}}) + await self.db.notes.update_one( + {"message_id": str(message_id)}, {"$set": {"message": message}} + ) def get_plugin_partition(self, cog): cls_name = cog.__class__.__name__ diff --git a/core/thread.py b/core/thread.py index 3562d98ee3..b9bf261b12 100644 --- a/core/thread.py +++ b/core/thread.py @@ -229,7 +229,9 @@ class Author: "author": Author(), } message = discord.Message(state=State(), channel=None, data=data) - ids[note["_id"]] = str((await self.note(message, persistent=True, thread_creation=True)).id) + ids[note["_id"]] = str( + (await self.note(message, persistent=True, thread_creation=True)).id + ) await self.bot.api.update_note_ids(ids) @@ -242,7 +244,10 @@ async def activate_auto_triggers(): pass await asyncio.gather( - send_genesis_message(), send_recipient_genesis_message(), activate_auto_triggers(), send_persistent_notes(), + send_genesis_message(), + send_recipient_genesis_message(), + activate_auto_triggers(), + send_persistent_notes(), ) self.bot.dispatch("thread_ready", self) @@ -687,11 +692,19 @@ async def edit_dm_message(self, message: discord.Message, content: str) -> None: self.bot.api.edit_message(message.id, content), linked_message.edit(embed=embed) ) - async def note(self, message: discord.Message, persistent=False, thread_creation=False) -> None: + async def note( + self, message: discord.Message, persistent=False, thread_creation=False + ) -> None: if not message.content and not message.attachments: raise MissingRequiredArgument(SimpleNamespace(name="msg")) - msg = await self.send(message, self.channel, note=True, persistent_note=persistent, thread_creation=thread_creation) + msg = await self.send( + message, + self.channel, + note=True, + persistent_note=persistent, + thread_creation=thread_creation, + ) self.bot.loop.create_task( self.bot.api.append_log( From 8e4f8d6a8f95a7623f8fde1e55129eb9aec4ef3d Mon Sep 17 00:00:00 2001 From: Cyrus Yip Date: Tue, 10 Nov 2020 18:53:20 -0800 Subject: [PATCH 0683/1221] changelog and version bump --- CHANGELOG.md | 3 ++- bot.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9123ab168f..dff6738aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev12 +# v3.7.0-dev13 ### Added @@ -27,6 +27,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added support for thread titles, `?title`. ([GH #2838](https://github.com/kyb3r/modmail/issues/2838)) - Added `data_collection` to specify if bot metadata should be collected by Modmail developers. - Added `?autotrigger`, `use_regex_autotrigger` config to specify keywords to trigger commands. ([GH #130](https://github.com/kyb3r/modmail/issues/130), [GH #649](https://github.com/kyb3r/modmail/issues/649)) +- Added `?note persistent` that creates notes that are persistent for a user. ### Fixed diff --git a/bot.py b/bot.py index 7a04011382..5aa4c1cb57 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev12" +__version__ = "3.7.0-dev13" import asyncio From 2ea4b368d27f59a852e48176dad3d2e407fed0b0 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 14:21:21 +0800 Subject: [PATCH 0684/1221] Fix bugs with autotriggers --- bot.py | 35 ++++++++++++++++------------------- cogs/utility.py | 12 +++++++----- core/config.py | 3 ++- core/utils.py | 2 +- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/bot.py b/bot.py index 5aa4c1cb57..f7a59a0944 100644 --- a/bot.py +++ b/bot.py @@ -849,7 +849,6 @@ async def get_contexts(self, message, *, cls=commands.Context): discord.utils.find(view.skip_string, prefixes) ctx_.invoked_with = view.get_word().lower() ctx_.command = self.all_commands.get(ctx_.invoked_with) - print(ctx_.invoked_with, ctx_.args, ctx_.kwargs) ctxs += [ctx_] return ctxs @@ -867,30 +866,28 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) thread = await self.threads.find(channel=ctx.channel) invoked_prefix = self.prefix - invoker = view.get_word().lower() + invoker = None # Check if there is any aliases being called. if self.config.get("use_regex_autotrigger"): - alias = self.auto_triggers[ - next(filter(lambda x: re.match(x, message.content), self.auto_triggers.keys())) - ] + trigger = next( + filter(lambda x: re.match(x, message.content), self.auto_triggers.keys()) + ) + if trigger: + invoker = re.match(trigger, message.content).group(0) else: - alias = self.auto_triggers[ - next( - filter( - lambda x: x.lower() in message.content.lower(), self.auto_triggers.keys() - ) - ) - ] + trigger = next( + filter(lambda x: x.lower() in message.content.lower(), self.auto_triggers.keys()) + ) + if trigger: + invoker = trigger.lower() - if alias is None: - ctx.thread = thread - ctx.invoked_with = invoker - ctx.command = self.all_commands.get(invoker) - ctxs = [ctx] - else: + alias = self.auto_triggers[trigger] + + ctxs = [] + if alias is not None: ctxs = [] - aliases = normalize_alias(alias, message.content[len(f"{invoked_prefix}{invoker}") :]) + aliases = normalize_alias(alias) if not aliases: logger.warning("Alias %s is invalid as called in automove.", invoker) diff --git a/cogs/utility.py b/cogs/utility.py index 950baaaf14..ea4db89fca 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1780,16 +1780,18 @@ async def autotrigger_remove(self, ctx, keyword): @checks.has_permissions(PermissionLevel.OWNER) async def autotrigger_test(self, ctx, *, text): """Tests a string against the current autotrigger setup""" - for keyword in list(self.bot.auto_triggers): + for keyword in self.bot.auto_triggers: if self.bot.config.get("use_regex_autotrigger"): check = re.match(keyword, text) + regex = True else: - check = keyword in text + check = keyword.lower() in text.lower() + regex = False if check: alias = self.bot.auto_triggers[keyword] embed = discord.Embed( - title="Keyword Found", + title=f"{'Regex ' if regex else ''}Keyword Found", color=self.bot.main_color, description=f"autotrigger keyword `{keyword}` found. Command executed: `{alias}`", ) @@ -1798,7 +1800,7 @@ async def autotrigger_test(self, ctx, *, text): embed = discord.Embed( title="Keyword Not Found", color=self.bot.error_color, - description=f"No autotrigger keyword found. Thread will stay in {self.bot.main_category}.", + description=f"No autotrigger keyword found.", ) return await ctx.send(embed=embed) @@ -1807,7 +1809,7 @@ async def autotrigger_test(self, ctx, *, text): async def autotrigger_list(self, ctx): """Lists all autotriggers set up""" embeds = [] - for keyword in list(self.bot.auto_triggers): + for keyword in self.bot.auto_triggers: command = self.bot.auto_triggers[keyword] embed = discord.Embed(title=keyword, color=self.bot.main_color, description=command,) embeds.append(embed) diff --git a/core/config.py b/core/config.py index 85d0797488..d92889f41e 100644 --- a/core/config.py +++ b/core/config.py @@ -160,7 +160,8 @@ class ConfigManager: "close_on_leave", "alert_on_mention", "confirm_thread_creation", - "use_regex_autotrigger" "enable_plugins", + "use_regex_autotrigger", + "enable_plugins", "data_collection", "enable_eval", } diff --git a/core/utils.py b/core/utils.py index a236adbc0f..b6640eaed8 100644 --- a/core/utils.py +++ b/core/utils.py @@ -302,7 +302,7 @@ def decode_alias(m): return aliases -def normalize_alias(alias, message): +def normalize_alias(alias, message=""): aliases = parse_alias(alias) contents = parse_alias(message, split=False) From 9920b0dee7aacc60542fb961073f679c1dc14559 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 14:23:10 +0800 Subject: [PATCH 0685/1221] Add git links to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dff6738aa1..04cae2085c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added support for thread titles, `?title`. ([GH #2838](https://github.com/kyb3r/modmail/issues/2838)) - Added `data_collection` to specify if bot metadata should be collected by Modmail developers. - Added `?autotrigger`, `use_regex_autotrigger` config to specify keywords to trigger commands. ([GH #130](https://github.com/kyb3r/modmail/issues/130), [GH #649](https://github.com/kyb3r/modmail/issues/649)) -- Added `?note persistent` that creates notes that are persistent for a user. +- Added `?note persistent` that creates notes that are persistent for a user. ([GH #2842](https://github.com/kyb3r/modmail/issues/2842), [PR #2878](https://github.com/kyb3r/modmail/pull/2878)) ### Fixed From da09dabe60bcbb382bd2d1822c65e6e92400b664 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 16:07:36 +0800 Subject: [PATCH 0686/1221] Update reaadme links --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 700b3bc10b..baf7c84c3b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - Bot instances + Bot instances @@ -54,7 +54,7 @@ This bot is free for everyone and always will be. If you like this project and w When a member sends a direct message to the bot, Modmail will create a channel or "thread" into a designated category. All further DM messages will automatically relay to that channel; any available staff can respond within the channel. -Our Logviewer will save the threads so you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.logviewer.tech/example). +Our Logviewer will save the threads so you can view previous threads through their corresponding log link. Here is an [**example**](https://logs.modmail.dev/example). ## Features @@ -67,7 +67,7 @@ Our Logviewer will save the threads so you can view previous threads through the * Minimum length for members to be in the guild before allowed to contact Modmail (`guild_age`). * **Advanced Logging Functionality:** - * When you close a thread, Modmail will generate a [log link](https://logs.logviewer.tech/example) and post it to your log channel. + * When you close a thread, Modmail will generate a [log link](https://logs.modmail.dev/example) and post it to your log channel. * Native Discord dark-mode feel. * Markdown/formatting support. * Login via Discord to protect your logs ([premium Patreon feature](https://patreon.com/kyber)). From 5a05869c15968e5c02e57635bde2585271eccaab Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 17:34:08 +0800 Subject: [PATCH 0687/1221] Add a bunch of new events, fix plugin ready --- CHANGELOG.md | 3 +++ bot.py | 21 +++++++++++++++++++++ cogs/modmail.py | 21 ++------------------- cogs/plugins.py | 4 ++++ core/thread.py | 3 +++ 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04cae2085c..c787e11655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?contact` now sends members a DM. - `level_permissions` and `command_permissions` would sometimes be reset. ([GH #2856](https://github.com/kyb3r/modmail/issues/2856)) - Command truncated after && in alias. ([GH #2870](https://github.com/kyb3r/modmail/issues/2870)) +- `on_plugins_ready` event for plugins works now. ### Improved @@ -42,6 +43,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Internal - Use enums in config. ([GH #2821](https://github.com/kyb3r/modmail/issues/2821)) +- `on_thread_close` event for plugins. +- `on_thread_reply` event for plugins. # v3.6.2 diff --git a/bot.py b/bot.py index f7a59a0944..ce2c7bdd6e 100644 --- a/bot.py +++ b/bot.py @@ -811,6 +811,8 @@ async def process_dm_modmail(self, message: discord.Message) -> None: await self.add_reaction(message, blocked_emoji) else: await self.add_reaction(message, sent_emoji) + self.bot.dispatch("thread_reply", thread, False, message, False, False) + async def get_contexts(self, message, *, cls=commands.Context): """ @@ -1131,6 +1133,25 @@ async def on_raw_reaction_add(self, payload): if self.config["transfer_reactions"]: await self.handle_reaction_events(payload) + react_message_id = tryint(self.bot.config.get("react_to_contact_message")) + react_message_emoji = self.bot.config.get("react_to_contact_emoji") + if all((react_message_id, react_message_emoji)): + if payload.message_id == react_message_id: + if payload.emoji.is_unicode_emoji(): + emoji_fmt = payload.emoji.name + else: + emoji_fmt = f"<:{payload.emoji.name}:{payload.emoji.id}>" + + if emoji_fmt == react_message_emoji: + channel = self.bot.get_channel(payload.channel_id) + member = channel.guild.get_member(payload.user_id) + message = await channel.fetch_message(payload.message_id) + await message.remove_reaction(payload.emoji, member) + + ctx = await self.bot.get_context(message) + ctx.author = member + await ctx.invoke(self.contact, user=member, manual_trigger=False) + async def on_raw_reaction_remove(self, payload): if self.config["transfer_reactions"]: await self.handle_reaction_events(payload) diff --git a/cogs/modmail.py b/cogs/modmail.py index b977d8fdf4..ee62f58137 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1012,25 +1012,8 @@ async def contact( await ctx.message.delete() @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - react_message_id = tryint(self.bot.config.get("react_to_contact_message")) - react_message_emoji = self.bot.config.get("react_to_contact_emoji") - if all((react_message_id, react_message_emoji)): - if payload.message_id == react_message_id: - if payload.emoji.is_unicode_emoji(): - emoji_fmt = payload.emoji.name - else: - emoji_fmt = f"<:{payload.emoji.name}:{payload.emoji.id}>" - - if emoji_fmt == react_message_emoji: - channel = self.bot.get_channel(payload.channel_id) - member = channel.guild.get_member(payload.user_id) - message = await channel.fetch_message(payload.message_id) - await message.remove_reaction(payload.emoji, member) - - ctx = await self.bot.get_context(message) - ctx.author = member - await ctx.invoke(self.contact, user=member, manual_trigger=False) + async def on_plugin_ready(self): + print('hi') @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) diff --git a/cogs/plugins.py b/cogs/plugins.py index e85907838f..1cce8449c7 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -15,6 +15,7 @@ import discord from discord.ext import commands +from discord.utils import async_all from pkg_resources import parse_version @@ -145,6 +146,9 @@ async def initial_load_plugins(self): continue logger.debug("Finished loading all plugins.") + + self.bot.dispatch('plugins_ready') + self._ready_event.set() await self.bot.config.update() diff --git a/core/thread.py b/core/thread.py index b9bf261b12..d5517fb375 100644 --- a/core/thread.py +++ b/core/thread.py @@ -486,6 +486,8 @@ async def _close( tasks.append(self.channel.delete()) await asyncio.gather(*tasks) + self.bot.dispatch("thread_close", self, closer, silent, delete_channel, message, scheduled) + 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): @@ -779,6 +781,7 @@ async def reply( ) await asyncio.gather(*tasks) + self.bot.dispatch("thread_reply", self, True, message, anonymous, plain) async def send( self, From 5d62d270dd753d72386c700c3917b742a52b75f1 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 17:34:38 +0800 Subject: [PATCH 0688/1221] Push version --- CHANGELOG.md | 2 +- bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c787e11655..c22fe36bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev13 +# v3.7.0-dev14 ### Added diff --git a/bot.py b/bot.py index ce2c7bdd6e..05b67ff3fe 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev13" +__version__ = "3.7.0-dev14" import asyncio From 2d56745e678a3468970e21fcea38721060589773 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 17:46:30 +0800 Subject: [PATCH 0689/1221] Fix CI --- .github/workflows/lints.yml | 2 +- bot.py | 1 - cogs/modmail.py | 4 ---- cogs/plugins.py | 2 +- core/thread.py | 1 - 5 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 52a538bef7..68b0de7c8a 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -31,4 +31,4 @@ jobs: continue-on-error: true - name: Black and flake8 run: | - black . --diff + black . --diff --check diff --git a/bot.py b/bot.py index 05b67ff3fe..28b19514ba 100644 --- a/bot.py +++ b/bot.py @@ -813,7 +813,6 @@ async def process_dm_modmail(self, message: discord.Message) -> None: await self.add_reaction(message, sent_emoji) self.bot.dispatch("thread_reply", thread, False, message, False, False) - async def get_contexts(self, message, *, cls=commands.Context): """ Returns all invocation contexts from the message. diff --git a/cogs/modmail.py b/cogs/modmail.py index ee62f58137..268d00d547 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1011,10 +1011,6 @@ async def contact( await asyncio.sleep(5) await ctx.message.delete() - @commands.Cog.listener() - async def on_plugin_ready(self): - print('hi') - @commands.group(invoke_without_command=True) @checks.has_permissions(PermissionLevel.MODERATOR) @trigger_typing diff --git a/cogs/plugins.py b/cogs/plugins.py index 1cce8449c7..fe70b5225b 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -147,7 +147,7 @@ async def initial_load_plugins(self): logger.debug("Finished loading all plugins.") - self.bot.dispatch('plugins_ready') + self.bot.dispatch("plugins_ready") self._ready_event.set() await self.bot.config.update() diff --git a/core/thread.py b/core/thread.py index d5517fb375..2713afd273 100644 --- a/core/thread.py +++ b/core/thread.py @@ -488,7 +488,6 @@ async def _close( await asyncio.gather(*tasks) self.bot.dispatch("thread_close", self, closer, silent, delete_channel, message, scheduled) - 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() From 97155865d6be1830e554a57e298445afd51efb9d Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 17:50:12 +0800 Subject: [PATCH 0690/1221] Fix CI --- cogs/utility.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index ea4db89fca..75d141496d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1811,7 +1811,11 @@ async def autotrigger_list(self, ctx): embeds = [] for keyword in self.bot.auto_triggers: command = self.bot.auto_triggers[keyword] - embed = discord.Embed(title=keyword, color=self.bot.main_color, description=command,) + embed = discord.Embed( + title=keyword, + color=self.bot.main_color, + description=command, + ) embeds.append(embed) if not embeds: From 615bad0984f3a9e48fe4a2ef211d1377ec979fb5 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 21:39:56 +0800 Subject: [PATCH 0691/1221] bugfix --- CHANGELOG.md | 2 +- bot.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c22fe36bdc..effb9f6269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev14 +# v3.7.0-dev15 ### Added diff --git a/bot.py b/bot.py index 28b19514ba..9d9bfaa24d 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev14" +__version__ = "3.7.0-dev15" import asyncio @@ -12,16 +12,16 @@ from types import SimpleNamespace import discord -from discord.ext import commands, tasks -from discord.ext.commands.view import StringView - import isodate - from aiohttp import ClientSession +from discord.ext import commands, tasks +from discord.ext.commands.view import StringView from emoji import UNICODE_EMOJI - from pkg_resources import parse_version +from core.utils import tryint + + try: # noinspection PyUnresolvedReferences from colorama import init @@ -31,13 +31,13 @@ pass from core import checks -from core.clients import ApiClient, PluginDatabaseClient, MongoDBClient +from core.clients import ApiClient, MongoDBClient, PluginDatabaseClient from core.config import ConfigManager -from core.utils import human_join, match_title, normalize_alias -from core.models import DMDisabled, PermissionLevel, SafeFormatter, getLogger, configure_logging +from core.models import (DMDisabled, PermissionLevel, SafeFormatter, + configure_logging, getLogger) from core.thread import ThreadManager from core.time import human_timedelta - +from core.utils import human_join, match_title, normalize_alias logger = getLogger(__name__) From fedbca8d8fc1b6e10f9d3f6b2370b2a61b671996 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 21:46:24 +0800 Subject: [PATCH 0692/1221] Lint --- .github/workflows/lints.yml | 2 +- bot.py | 3 +-- cogs/utility.py | 6 +----- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 68b0de7c8a..07339a6198 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -29,6 +29,6 @@ jobs: - 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 + - name: Black run: | black . --diff --check diff --git a/bot.py b/bot.py index 9d9bfaa24d..e4a4a57a5b 100644 --- a/bot.py +++ b/bot.py @@ -33,8 +33,7 @@ from core import checks from core.clients import ApiClient, MongoDBClient, PluginDatabaseClient from core.config import ConfigManager -from core.models import (DMDisabled, PermissionLevel, SafeFormatter, - configure_logging, getLogger) +from core.models import DMDisabled, PermissionLevel, SafeFormatter, configure_logging, getLogger from core.thread import ThreadManager from core.time import human_timedelta from core.utils import human_join, match_title, normalize_alias diff --git a/cogs/utility.py b/cogs/utility.py index 75d141496d..ea4db89fca 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1811,11 +1811,7 @@ async def autotrigger_list(self, ctx): embeds = [] for keyword in self.bot.auto_triggers: command = self.bot.auto_triggers[keyword] - embed = discord.Embed( - title=keyword, - color=self.bot.main_color, - description=command, - ) + embed = discord.Embed(title=keyword, color=self.bot.main_color, description=command,) embeds.append(embed) if not embeds: From b80c00800812454a8ab3891dc0717bf1961f0f44 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 21:50:11 +0800 Subject: [PATCH 0693/1221] lint --- .github/workflows/lints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 07339a6198..efda5bdcfa 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install bandit pylint black + python -m pip install bandit==1.6.2 pylint black====19.10b0 continue-on-error: true - name: Bandit syntax check run: bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json From 09e5929e6389b2f27296a43f85c9dc2e17b9fa98 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 21:52:52 +0800 Subject: [PATCH 0694/1221] lint --- .github/workflows/lints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index efda5bdcfa..689e611157 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install bandit==1.6.2 pylint black====19.10b0 + python -m pip install bandit==1.6.2 pylint black==19.10b0 continue-on-error: true - name: Bandit syntax check run: bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json From bfb1a17786f477a8c101580600c47668e2b9fad4 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 11 Nov 2020 21:59:49 +0800 Subject: [PATCH 0695/1221] bugfixing --- bot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index e4a4a57a5b..6d9f2d45ed 100644 --- a/bot.py +++ b/bot.py @@ -810,7 +810,7 @@ async def process_dm_modmail(self, message: discord.Message) -> None: await self.add_reaction(message, blocked_emoji) else: await self.add_reaction(message, sent_emoji) - self.bot.dispatch("thread_reply", thread, False, message, False, False) + self.dispatch("thread_reply", thread, False, message, False, False) async def get_contexts(self, message, *, cls=commands.Context): """ @@ -1131,8 +1131,8 @@ async def on_raw_reaction_add(self, payload): if self.config["transfer_reactions"]: await self.handle_reaction_events(payload) - react_message_id = tryint(self.bot.config.get("react_to_contact_message")) - react_message_emoji = self.bot.config.get("react_to_contact_emoji") + react_message_id = tryint(self.config.get("react_to_contact_message")) + react_message_emoji = self.config.get("react_to_contact_emoji") if all((react_message_id, react_message_emoji)): if payload.message_id == react_message_id: if payload.emoji.is_unicode_emoji(): @@ -1141,12 +1141,12 @@ async def on_raw_reaction_add(self, payload): emoji_fmt = f"<:{payload.emoji.name}:{payload.emoji.id}>" if emoji_fmt == react_message_emoji: - channel = self.bot.get_channel(payload.channel_id) + channel = self.get_channel(payload.channel_id) member = channel.guild.get_member(payload.user_id) message = await channel.fetch_message(payload.message_id) await message.remove_reaction(payload.emoji, member) - ctx = await self.bot.get_context(message) + ctx = await self.get_context(message) ctx.author = member await ctx.invoke(self.contact, user=member, manual_trigger=False) From c04f0e011d28198afffa8b712d3d979ea42c1d97 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 15:18:27 +0800 Subject: [PATCH 0696/1221] Add 10min cooldown to title --- CHANGELOG.md | 2 +- bot.py | 12 ++++++++++-- cogs/modmail.py | 8 +++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index effb9f6269..9917d564ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev15 +# v3.7.0-dev16 ### Added diff --git a/bot.py b/bot.py index 6d9f2d45ed..7ff45bfbfe 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev15" +__version__ = "3.7.0-dev16" import asyncio @@ -462,7 +462,7 @@ async def on_ready(self): log["channel_id"], { "open": False, - "title": match_title(thread.channel.topic), + "title": None, "closed_at": str(datetime.utcnow()), "close_message": "Channel has been deleted, no closer found.", "closer": { @@ -1323,6 +1323,14 @@ async def on_command_error(self, context, exception): logger.warning("CommandNotFound: %s", exception) elif isinstance(exception, commands.MissingRequiredArgument): await context.send_help(context.command) + elif isinstance(exception, commands.CommandOnCooldown): + await context.send( + embed=discord.Embed( + title="Command on cooldown", + description=f"Try again in {exception.retry_after:.2f} seconds", + color=self.error_color, + ) + ) elif isinstance(exception, commands.CheckFailure): for check in context.command.checks: if not await check(context): diff --git a/cogs/modmail.py b/cogs/modmail.py index 268d00d547..ed9a3dfec7 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -7,6 +7,7 @@ import discord from discord.ext import commands +from discord.ext.commands.cooldowns import BucketType from discord.role import Role from discord.utils import escape_markdown @@ -296,6 +297,7 @@ async def move(self, ctx, *, arguments): `options` is a string which takes in arguments on how to perform the move. Ex: "silently" """ split_args = arguments.strip('"').split(" ") + category = None # manually parse arguments, consumes as much of args as possible for category for i in range(len(split_args)): @@ -314,6 +316,9 @@ async def move(self, ctx, *, arguments): else: break + if not category: + raise commands.ChannelNotFound(arguments) + options = " ".join(arguments.split(" ")[-i:]) thread = ctx.thread @@ -646,9 +651,10 @@ def format_log_embeds(self, logs, avatar_url): embeds.append(embed) return embeds - @commands.command() + @commands.command(cooldown_after_parsing=True) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() + @commands.cooldown(1, 600, BucketType.channel) async def title(self, ctx, *, name: str): """Sets title for a thread""" await ctx.thread.set_title(name) From cdad32046de4d325db3dd72e76ccbff5a8d5a68d Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 15:26:59 +0800 Subject: [PATCH 0697/1221] bugfix --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 7ff45bfbfe..11c45dd050 100644 --- a/bot.py +++ b/bot.py @@ -1148,7 +1148,7 @@ async def on_raw_reaction_add(self, payload): ctx = await self.get_context(message) ctx.author = member - await ctx.invoke(self.contact, user=member, manual_trigger=False) + await ctx.invoke(self.get_cog("Modmail").contact, user=member, manual_trigger=False) async def on_raw_reaction_remove(self, payload): if self.config["transfer_reactions"]: From 60ac027152d33aed55f2aafcf48f2ed6d6df933f Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 15:27:18 +0800 Subject: [PATCH 0698/1221] bugfix --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 11c45dd050..1a54f2ce37 100644 --- a/bot.py +++ b/bot.py @@ -1148,7 +1148,7 @@ async def on_raw_reaction_add(self, payload): ctx = await self.get_context(message) ctx.author = member - await ctx.invoke(self.get_cog("Modmail").contact, user=member, manual_trigger=False) + await ctx.invoke(self.get_command("contact"), user=member, manual_trigger=False) async def on_raw_reaction_remove(self, payload): if self.config["transfer_reactions"]: From 29747b714ef034dc9bb69847dbc684e7a8f721a5 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 15:28:49 +0800 Subject: [PATCH 0699/1221] lint --- bot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 1a54f2ce37..799a230a84 100644 --- a/bot.py +++ b/bot.py @@ -1148,7 +1148,9 @@ async def on_raw_reaction_add(self, payload): ctx = await self.get_context(message) ctx.author = member - await ctx.invoke(self.get_command("contact"), user=member, manual_trigger=False) + await ctx.invoke( + self.get_command("contact"), user=member, manual_trigger=False + ) async def on_raw_reaction_remove(self, payload): if self.config["transfer_reactions"]: From a84cb9c7b80679134f450a5fe359e76d01ee47d9 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 22:54:08 +0800 Subject: [PATCH 0700/1221] Autoupdate functions --- CHANGELOG.md | 3 +- bot.py | 110 +++++++++++++++++++++- cogs/modmail.py | 9 ++ cogs/utility.py | 120 +++++++++++++++++++++++- core/checks.py | 22 ++++- core/clients.py | 207 ++++++++++++++++++++++++++++++++++++++++++ core/config.py | 2 + core/config_help.json | 18 ++++ core/models.py | 6 ++ core/utils.py | 1 + 10 files changed, 493 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9917d564ca..d096c8f4e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev16 +# v3.7.0-dev17 ### Added @@ -28,6 +28,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added `data_collection` to specify if bot metadata should be collected by Modmail developers. - Added `?autotrigger`, `use_regex_autotrigger` config to specify keywords to trigger commands. ([GH #130](https://github.com/kyb3r/modmail/issues/130), [GH #649](https://github.com/kyb3r/modmail/issues/649)) - Added `?note persistent` that creates notes that are persistent for a user. ([GH #2842](https://github.com/kyb3r/modmail/issues/2842), [PR #2878](https://github.com/kyb3r/modmail/pull/2878)) +- Autoupdates and `?update` which was removed in v3.0.0 ### Fixed diff --git a/bot.py b/bot.py index 799a230a84..0dc95df4c7 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev16" +__version__ = "3.7.0-dev17" import asyncio @@ -6,6 +6,7 @@ import logging import os import re +import subprocess import sys import typing from datetime import datetime @@ -31,9 +32,17 @@ pass from core import checks +from core.changelog import Changelog from core.clients import ApiClient, MongoDBClient, PluginDatabaseClient from core.config import ConfigManager -from core.models import DMDisabled, PermissionLevel, SafeFormatter, configure_logging, getLogger +from core.models import ( + DMDisabled, + HostingMethod, + PermissionLevel, + SafeFormatter, + configure_logging, + getLogger, +) from core.thread import ThreadManager from core.time import human_timedelta from core.utils import human_join, match_title, normalize_alias @@ -58,6 +67,7 @@ def __init__(self): self._session = None self._api = None self.metadata_loop = None + self.autoupdate_loop = None self.formatter = SafeFormatter() self.loaded_cogs = ["cogs.modmail", "cogs.plugins", "cogs.utility"] self._connected = asyncio.Event() @@ -88,6 +98,21 @@ def uptime(self) -> str: return self.formatter.format(fmt, d=days, h=hours, m=minutes, s=seconds) + @property + def hosting_method(self) -> HostingMethod: + # use enums + if ".heroku" in os.environ.get("PYTHONHOME", ""): + return HostingMethod.HEROKU + + if os.environ.get("pm_id"): + return HostingMethod.PM2 + + return HostingMethod.OTHER + + @property + def is_pm2(self) -> bool: + return ".heroku" in os.environ.get("PYTHONHOME", "") + def startup(self): logger.line() if os.name != "nt": @@ -494,6 +519,12 @@ async def on_ready(self): self.metadata_loop.before_loop(self.before_post_metadata) self.metadata_loop.start() + self.autoupdate_loop = tasks.Loop( + self.autoupdate, seconds=0, minutes=0, hours=1, count=None, reconnect=True, loop=None + ) + self.autoupdate_loop.before_loop(self.before_autoupdate) + self.autoupdate_loop.start() + other_guilds = [ guild for guild in self.guilds if guild not in {self.guild, self.modmail_guild} ] @@ -1398,6 +1429,81 @@ async def before_post_metadata(self): if not self.guild: self.metadata_loop.cancel() + async def autoupdate(self): + changelog = await Changelog.from_url(self) + latest = changelog.latest_version + + if self.version < parse_version(latest.version): + if self.hosting_method == HostingMethod.HEROKU: + data = await self.api.update_repository() + + embed = discord.Embed(color=self.main_color) + + commit_data = data["data"] + user = data["user"] + embed.set_author( + name=user["username"] + " - Updating Bot", + icon_url=user["avatar_url"], + url=user["url"], + ) + + embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + + embed.description = latest.description + for name, value in latest.fields.items(): + embed.add_field(name=name, value=value) + + if commit_data: + message = commit_data["commit"]["message"] + html_url = commit_data["html_url"] + short_sha = commit_data["sha"][:6] + embed.add_field( + name="Merge Commit", + value=f"[`{short_sha}`]({html_url}) " f"{message} - {user['username']}", + ) + logger.info("Bot has been updated.") + channel = self.log_channel + await channel.send(embed=embed) + else: + command = "git pull" + + cmd = subprocess.run( + command, + cwd=os.getcwd(), + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + shell=True, + ) + res = cmd.stdout.decode("utf-8").strip() + + if res != "Already up to date.": + logger.info("Bot has been updated.") + channel = self.log_channel + if self.hosting_method == HostingMethod.PM2: + embed = discord.Embed(title="Bot has been updated", color=self.main_color) + await channel.send(embed=embed) + else: + embed = discord.Embed( + title="Bot has been updated and is logging out.", + description="If you do not have an auto-restart setup, please manually start the bot.", + color=self.main_color, + ) + await channel.send(embed=embed) + await self.logout() + + async def before_autoupdate(self): + await self.wait_for_connected() + logger.debug("Starting autoupdate loop") + + if self.config.get("disable_autoupdates"): + logger.warning("Autoupdates disabled.") + self.autoupdate_loop.cancel() + + if not self.config.get("github_token"): + logger.warning("GitHub access token not found.") + logger.warning("Autoupdates disabled.") + self.autoupdate_loop.cancel() + def main(): try: diff --git a/cogs/modmail.py b/cogs/modmail.py index ed9a3dfec7..d036a6ee01 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1535,6 +1535,15 @@ async def isenable(self, ctx): return await ctx.send(embed=embed) + @commands.command(usage="[after] [close message]") + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def adduser(self, ctx, *, member: discord.Member = None): + await ctx.thread.add_user(member) + + sent_emoji, _ = await self.bot.retrieve_emoji() + await self.bot.add_reaction(ctx.message, sent_emoji) + def setup(bot): bot.add_cog(Modmail(bot)) diff --git a/cogs/utility.py b/cogs/utility.py index ea4db89fca..63bd70aac8 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -10,6 +10,7 @@ from io import BytesIO, StringIO from itertools import takewhile, zip_longest from json import JSONDecodeError, loads +import subprocess from textwrap import indent from types import SimpleNamespace from typing import Union @@ -24,12 +25,13 @@ from core import checks, utils from core.changelog import Changelog from core.models import ( + HostingMethod, InvalidConfigError, PermissionLevel, - SimilarCategoryConverter, UnseenFormatter, getLogger, ) +from core.utils import trigger_typing from core.paginator import EmbedPaginatorSession, MessagePaginatorSession @@ -1825,6 +1827,122 @@ async def autotrigger_list(self, ctx): await EmbedPaginatorSession(ctx, *embeds).run() + @commands.command() + @checks.has_permissions(PermissionLevel.OWNER) + @checks.github_token_required() + @trigger_typing + async def github(self, ctx): + """Shows the GitHub user your Github_Token is linked to.""" + data = await self.bot.api.get_user_info() + + embed = discord.Embed( + title="GitHub", description="Current User", color=self.bot.main_color + ) + user = data["user"] + embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) + embed.set_thumbnail(url=user["avatar_url"]) + await ctx.send(embed=embed) + + @commands.command() + @checks.has_permissions(PermissionLevel.OWNER) + @checks.github_token_required(ignore_if_not_heroku=True) + @trigger_typing + async def update(self, ctx, *, flag: str = ""): + """ + Update Modmail. + This only works for PM2 or Heroku users who have configured their bot for updates. + To stay up-to-date with the latest commit + from GitHub, specify "force" as the flag. + """ + + changelog = await Changelog.from_url(self.bot) + latest = changelog.latest_version + + desc = ( + f"The latest version is [`{self.bot.version}`]" + "(https://github.com/kyb3r/modmail/blob/master/bot.py#L25)" + ) + + if self.bot.version >= parse_version(latest.version) and flag.lower() != "force": + embed = discord.Embed( + title="Already up to date", description=desc, color=self.bot.main_color + ) + + data = await self.bot.api.get_user_info() + if not data.get("error"): + user = data["user"] + embed.set_author( + name=user["username"], icon_url=user["avatar_url"], url=user["url"] + ) + await ctx.send(embed=embed) + else: + if self.bot.hosting_method == HostingMethod.HEROKU: + data = await self.bot.api.update_repository() + + commit_data = data["data"] + user = data["user"] + + if commit_data: + embed = discord.Embed(color=self.bot.main_color) + + embed.set_footer( + text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}" + ) + + embed.set_author( + name=user["username"] + " - Updating bot", + icon_url=user["avatar_url"], + url=user["url"], + ) + + embed.description = latest.description + for name, value in latest.fields.items(): + embed.add_field(name=name, value=value) + # message = commit_data['commit']['message'] + html_url = commit_data["html_url"] + short_sha = commit_data["sha"][:6] + embed.add_field(name="Merge Commit", value=f"[`{short_sha}`]({html_url})") + else: + embed = discord.Embed( + title="Already up to date with master repository.", + description="No further updates required", + color=self.bot.main_color, + ) + embed.set_author( + name=user["username"], icon_url=user["avatar_url"], url=user["url"] + ) + await ctx.send(embed=embed) + else: + command = "git pull" + + cmd = subprocess.run( + command, + cwd=os.getcwd(), + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + shell=True, + ) + res = cmd.stdout.decode("utf-8").strip() + + if res != "Already up to date.": + logger.info("Bot has been updated.") + if self.bot.hosting_method == HostingMethod.PM2: + embed = discord.Embed( + title="Bot has been updated", color=self.bot.main_color + ) + await ctx.send(embed=embed) + else: + embed = discord.Embed( + title="Bot has been updated and is logging out.", + description="If you do not have an auto-restart setup, please manually start the bot.", + color=self.bot.main_color, + ) + await ctx.send(embed=embed) + await self.bot.logout() + else: + embed = discord.Embed(title="Already up to date.", color=self.bot.main_color,) + await ctx.send(embed=embed) + @commands.command(hidden=True, name="eval") @checks.has_permissions(PermissionLevel.OWNER) async def eval_(self, ctx, *, body: str): diff --git a/core/checks.py b/core/checks.py index 46f8424b96..0b7b2acfa3 100644 --- a/core/checks.py +++ b/core/checks.py @@ -1,6 +1,6 @@ from discord.ext import commands -from core.models import PermissionLevel, getLogger +from core.models import HostingMethod, PermissionLevel, getLogger logger = getLogger(__name__) @@ -100,3 +100,23 @@ async def predicate(ctx): predicate.fail_msg = "This is not a Modmail thread." return commands.check(predicate) + + +def github_token_required(ignore_if_not_heroku=False): + """ + A decorator that ensures github token + is set + """ + + async def predicate(ctx): + if ignore_if_not_heroku and ctx.bot.hosting_method != HostingMethod.HEROKU: + return True + else: + return ctx.bot.config.get("github_token") + + predicate.fail_msg = ( + "You can only use this command if you have a " + "configured `GITHUB_TOKEN`. Get a " + "personal access token from developer settings." + ) + return commands.check(predicate) diff --git a/core/clients.py b/core/clients.py index 62f565f91c..4f751ca723 100644 --- a/core/clients.py +++ b/core/clients.py @@ -5,6 +5,7 @@ from typing import Union, Optional from discord import Member, DMChannel, TextChannel, Message +from discord.ext import commands from aiohttp import ClientResponseError, ClientResponse from motor.motor_asyncio import AsyncIOMotorClient @@ -15,6 +16,198 @@ logger = getLogger(__name__) +class GitHub: + """ + The client for interacting with GitHub API. + Parameters + ---------- + bot : Bot + The Modmail bot. + access_token : str, optional + GitHub's access token. + username : str, optional + GitHub username. + avatar_url : str, optional + URL to the avatar in GitHub. + url : str, optional + URL to the GitHub profile. + Attributes + ---------- + bot : Bot + The Modmail bot. + access_token : str + GitHub's access token. + username : str + GitHub username. + avatar_url : str + URL to the avatar in GitHub. + url : str + URL to the GitHub profile. + Class Attributes + ---------------- + BASE : str + GitHub API base URL. + REPO : str + Modmail repo URL for GitHub API. + HEAD : str + Modmail HEAD URL for GitHub API. + MERGE_URL : str + URL for merging upstream to master. + FORK_URL : str + URL to fork Modmail. + STAR_URL : str + URL to star Modmail. + """ + + BASE = "https://api.github.com" + REPO = BASE + "/repos/kyb3r/modmail" + HEAD = REPO + "/git/refs/heads/master" + MERGE_URL = BASE + "/repos/{username}/modmail/merges" + FORK_URL = REPO + "/forks" + STAR_URL = BASE + "/user/starred/kyb3r/modmail" + + def __init__(self, bot, access_token: str = "", username: str = "", **kwargs): + self.bot = bot + self.session = bot.session + self.headers: dict = None + self.access_token = access_token + self.username = username + self.avatar_url: str = kwargs.pop("avatar_url", "") + self.url: str = kwargs.pop("url", "") + if self.access_token: + self.headers = {"Authorization": "token " + str(access_token)} + + async def request( + self, + url: str, + method: str = "GET", + payload: dict = None, + return_response: bool = False, + headers: dict = None, + ) -> Union[ClientResponse, dict, str]: + """ + Makes a HTTP request. + Parameters + ---------- + url : str + The destination URL of the request. + method : str + The HTTP method (POST, GET, PUT, DELETE, FETCH, etc.). + payload : Dict[str, Any] + The json payload to be sent along the request. + return_response : bool + Whether the `ClientResponse` object should be returned. + headers : Dict[str, str] + Additional headers to `headers`. + Returns + ------- + ClientResponse or Dict[str, Any] or List[Any] or str + `ClientResponse` if `return_response` is `True`. + `dict` if the returned data is a json object. + `list` if the returned data is a json list. + `str` if the returned data is not a valid json data, + the raw response. + """ + if headers is not None: + headers.update(self.headers) + else: + headers = self.headers + async with self.session.request(method, url, headers=headers, json=payload) as resp: + if return_response: + return resp + try: + return await resp.json() + except (JSONDecodeError, ClientResponseError): + return await resp.text() + + def filter_valid(self, data): + """ + Filters configuration keys that are accepted. + Parameters + ---------- + data : Dict[str, Any] + The data that needs to be cleaned. + Returns + ------- + Dict[str, Any] + Filtered `data` to keep only the accepted pairs. + """ + valid_keys = self.bot.config.valid_keys.difference(self.bot.config.protected_keys) + return {k: v for k, v in data.items() if k in valid_keys} + + async def update_repository(self, sha: str = None) -> Optional[dict]: + """ + Update the repository from Modmail main repo. + Parameters + ---------- + sha : Optional[str], optional + The commit SHA to update the repository. + Returns + ------- + Optional[dict] + If the response is a dict. + """ + if not self.username: + raise commands.CommandInvokeError("Username not found.") + + if sha is None: + resp: dict = await self.request(self.HEAD) + sha = resp["object"]["sha"] + + payload = {"base": "master", "head": sha, "commit_message": "Updating bot"} + + merge_url = self.MERGE_URL.format(username=self.username) + + resp = await self.request(merge_url, method="POST", payload=payload) + if isinstance(resp, dict): + return resp + + async def fork_repository(self) -> None: + """ + Forks Modmail's repository. + """ + await self.request(self.FORK_URL, method="POST") + + async def has_starred(self) -> bool: + """ + Checks if shared Modmail. + Returns + ------- + bool + `True`, if Modmail was starred. + Otherwise `False`. + """ + resp = await self.request(self.STAR_URL, return_response=True) + return resp.status == 204 + + async def star_repository(self) -> None: + """ + Stars Modmail's repository. + """ + await self.request(self.STAR_URL, method="PUT", headers={"Content-Length": "0"}) + + @classmethod + async def login(cls, bot) -> "GitHub": + """ + Logs in to GitHub with configuration variable information. + Parameters + ---------- + bot : Bot + The Modmail bot. + Returns + ------- + GitHub + The newly created `GitHub` object. + """ + self = cls(bot, bot.config.get("github_token")) + resp: dict = await self.request("https://api.github.com/user") + self.username = resp["login"] + self.avatar_url = resp["avatar_url"] + self.url = resp["html_url"] + logger.info(f"GitHub logged in to: {self.username}") + return self + + class ApiClient: """ This class represents the general request class for all type of clients. @@ -452,6 +645,20 @@ def get_plugin_partition(self, cog): cls_name = cog.__class__.__name__ return self.db.plugins[cls_name] + async def update_repository(self) -> dict: + user = await GitHub.login(self.bot) + data = await user.update_repository() + return { + "data": data, + "user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,}, + } + + async def get_user_info(self) -> dict: + user = await GitHub.login(self.bot) + return { + "user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,} + } + class PluginDatabaseClient: def __init__(self, bot): diff --git a/core/config.py b/core/config.py index d92889f41e..9aab61acf2 100644 --- a/core/config.py +++ b/core/config.py @@ -136,6 +136,7 @@ class ConfigManager: "enable_eval": True, # github access token for private repositories "github_token": None, + "disable_autoupdates": False, # Logging "log_level": "INFO", # data collection @@ -164,6 +165,7 @@ class ConfigManager: "enable_plugins", "data_collection", "enable_eval", + "disable_autoupdates", } enums = { diff --git a/core/config_help.json b/core/config_help.json index 970ebc6229..32cc694f32 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -779,5 +779,23 @@ "notes": [ "This configuration can only to be set through `.env` file or environment (config) variables." ] + }, + "github_token": { + "default": "None, required for update functionality", + "description": "A github personal access token with the repo scope: https://github.com/settings/tokens.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] + }, + "disable_autoupdates": { + "default": "No", + "description": "Controls if autoupdates should be disabled or not.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] } } diff --git a/core/models.py b/core/models.py index 0ad40bad1f..6882fe6b5f 100644 --- a/core/models.py +++ b/core/models.py @@ -264,3 +264,9 @@ class DMDisabled(IntEnum): NONE = 0 NEW_THREADS = 1 ALL_THREADS = 2 + + +class HostingMethod(IntEnum): + HEROKU = 0 + PM2 = 1 + OTHER = 2 diff --git a/core/utils.py b/core/utils.py index b6640eaed8..5eb8ea1680 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,4 +1,5 @@ import base64 +from core.models import HostingMethod import functools import re import string From 90d78cffd5a7ff96f9d7ffbe97a98684d1b37210 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:03:05 +0800 Subject: [PATCH 0701/1221] Fix a bug in update related commands --- CHANGELOG.md | 2 +- bot.py | 2 +- cogs/utility.py | 21 +++++++++++++-------- core/clients.py | 28 ++++++++++++++++++---------- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d096c8f4e3..87faf160e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev17 +# v3.7.0-dev18 ### Added diff --git a/bot.py b/bot.py index 0dc95df4c7..39d4895d83 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev17" +__version__ = "3.7.0-dev18" import asyncio diff --git a/cogs/utility.py b/cogs/utility.py index 63bd70aac8..3bdbe58fd0 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1835,13 +1835,18 @@ async def github(self, ctx): """Shows the GitHub user your Github_Token is linked to.""" data = await self.bot.api.get_user_info() - embed = discord.Embed( - title="GitHub", description="Current User", color=self.bot.main_color - ) - user = data["user"] - embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) - embed.set_thumbnail(url=user["avatar_url"]) - await ctx.send(embed=embed) + if data: + embed = discord.Embed( + title="GitHub", description="Current User", color=self.bot.main_color + ) + user = data["user"] + embed.set_author(name=user["username"], icon_url=user["avatar_url"], url=user["url"]) + embed.set_thumbnail(url=user["avatar_url"]) + await ctx.send(embed=embed) + else: + await ctx.send(embed=discord.Embed( + title="Invalid Github Token", color=self.bot.error_color + )) @commands.command() @checks.has_permissions(PermissionLevel.OWNER) @@ -1869,7 +1874,7 @@ async def update(self, ctx, *, flag: str = ""): ) data = await self.bot.api.get_user_info() - if not data.get("error"): + if data: user = data["user"] embed.set_author( name=user["username"], icon_url=user["avatar_url"], url=user["url"] diff --git a/core/clients.py b/core/clients.py index 4f751ca723..3ad3e5a93c 100644 --- a/core/clients.py +++ b/core/clients.py @@ -11,7 +11,7 @@ from motor.motor_asyncio import AsyncIOMotorClient from pymongo.errors import ConfigurationError -from core.models import getLogger +from core.models import InvalidConfigError, getLogger logger = getLogger(__name__) @@ -201,11 +201,15 @@ async def login(cls, bot) -> "GitHub": """ self = cls(bot, bot.config.get("github_token")) resp: dict = await self.request("https://api.github.com/user") - self.username = resp["login"] - self.avatar_url = resp["avatar_url"] - self.url = resp["html_url"] - logger.info(f"GitHub logged in to: {self.username}") - return self + if resp.get("login"): + self.username = resp["login"] + self.avatar_url = resp["avatar_url"] + self.url = resp["html_url"] + logger.info(f"GitHub logged in to: {self.username}") + return self + else: + raise InvalidConfigError("Invalid github token") + class ApiClient: @@ -654,10 +658,14 @@ async def update_repository(self) -> dict: } async def get_user_info(self) -> dict: - user = await GitHub.login(self.bot) - return { - "user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,} - } + try: + user = await GitHub.login(self.bot) + except InvalidConfigError: + return None + else: + return { + "user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,} + } class PluginDatabaseClient: From 07e51b4f498c9a633f0fcb5686130b97c4169e65 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:14:08 +0800 Subject: [PATCH 0702/1221] Use asyncio subprocess --- .github/workflows/lints.yml | 2 +- bot.py | 16 +++++----------- cogs/utility.py | 19 +++++++------------ core/clients.py | 7 +++++-- 4 files changed, 18 insertions(+), 26 deletions(-) diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 689e611157..94b322d968 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -25,7 +25,7 @@ jobs: python -m pip install bandit==1.6.2 pylint black==19.10b0 continue-on-error: true - name: Bandit syntax check - run: bandit ./bot.py cogs/*.py core/*.py -b .bandit_baseline.json + run: bandit -r . -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 diff --git a/bot.py b/bot.py index 39d4895d83..1827bb7e50 100644 --- a/bot.py +++ b/bot.py @@ -6,10 +6,10 @@ import logging import os import re -import subprocess import sys import typing from datetime import datetime +from subprocess import PIPE from types import SimpleNamespace import discord @@ -1466,15 +1466,9 @@ async def autoupdate(self): await channel.send(embed=embed) else: command = "git pull" - - cmd = subprocess.run( - command, - cwd=os.getcwd(), - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - shell=True, - ) - res = cmd.stdout.decode("utf-8").strip() + proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) + res = await proc.stdout.read() + res = res.decode("utf-8").rstrip() if res != "Already up to date.": logger.info("Bot has been updated.") @@ -1499,7 +1493,7 @@ async def before_autoupdate(self): logger.warning("Autoupdates disabled.") self.autoupdate_loop.cancel() - if not self.config.get("github_token"): + if not self.config.get("github_token") and self.hosting_method == HostingMethod.HEROKU: logger.warning("GitHub access token not found.") logger.warning("Autoupdates disabled.") self.autoupdate_loop.cancel() diff --git a/cogs/utility.py b/cogs/utility.py index 3bdbe58fd0..0b228bbdd2 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -10,7 +10,7 @@ from io import BytesIO, StringIO from itertools import takewhile, zip_longest from json import JSONDecodeError, loads -import subprocess +from subprocess import PIPE from textwrap import indent from types import SimpleNamespace from typing import Union @@ -1844,9 +1844,9 @@ async def github(self, ctx): embed.set_thumbnail(url=user["avatar_url"]) await ctx.send(embed=embed) else: - await ctx.send(embed=discord.Embed( - title="Invalid Github Token", color=self.bot.error_color - )) + await ctx.send( + embed=discord.Embed(title="Invalid Github Token", color=self.bot.error_color) + ) @commands.command() @checks.has_permissions(PermissionLevel.OWNER) @@ -1920,14 +1920,9 @@ async def update(self, ctx, *, flag: str = ""): else: command = "git pull" - cmd = subprocess.run( - command, - cwd=os.getcwd(), - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - shell=True, - ) - res = cmd.stdout.decode("utf-8").strip() + proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) + res = await proc.stdout.read() + res = res.decode("utf-8").rstrip() if res != "Already up to date.": logger.info("Bot has been updated.") diff --git a/core/clients.py b/core/clients.py index 3ad3e5a93c..90a9d23294 100644 --- a/core/clients.py +++ b/core/clients.py @@ -211,7 +211,6 @@ async def login(cls, bot) -> "GitHub": raise InvalidConfigError("Invalid github token") - class ApiClient: """ This class represents the general request class for all type of clients. @@ -664,7 +663,11 @@ async def get_user_info(self) -> dict: return None else: return { - "user": {"username": user.username, "avatar_url": user.avatar_url, "url": user.url,} + "user": { + "username": user.username, + "avatar_url": user.avatar_url, + "url": user.url, + } } From e7c479c4146eb29ab55ab5620f7b702d3249bbd4 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:18:20 +0800 Subject: [PATCH 0703/1221] bump version & update baseline --- .bandit_baseline.json | 172 ++++++++++++++++++++++++++---------------- CHANGELOG.md | 2 +- bot.py | 2 +- 3 files changed, 107 insertions(+), 69 deletions(-) diff --git a/.bandit_baseline.json b/.bandit_baseline.json index 539fe85a88..df81a6fecc 100644 --- a/.bandit_baseline.json +++ b/.bandit_baseline.json @@ -1,32 +1,20 @@ { "errors": [], - "generated_at": "2019-10-07T08:19:22Z", + "generated_at": "2020-11-12T15:17:38Z", "metrics": { - "./bot.py": { - "CONFIDENCE.HIGH": 0.0, + ".\\bot.py": { + "CONFIDENCE.HIGH": 1.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, + "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 933, - "nosec": 0 - }, - "_totals": { - "CONFIDENCE.HIGH": 2.0, - "CONFIDENCE.LOW": 0.0, - "CONFIDENCE.MEDIUM": 1.0, - "CONFIDENCE.UNDEFINED": 0.0, - "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 2.0, - "SEVERITY.MEDIUM": 1.0, - "SEVERITY.UNDEFINED": 0.0, - "loc": 7299, + "loc": 1264, "nosec": 0 }, - "cogs/modmail.py": { + ".\\cogs\\modmail.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -35,10 +23,10 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 973, + "loc": 1280, "nosec": 0 }, - "cogs/plugins.py": { + ".\\cogs\\plugins.py": { "CONFIDENCE.HIGH": 1.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -47,22 +35,22 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 537, + "loc": 572, "nosec": 0 }, - "cogs/utility.py": { - "CONFIDENCE.HIGH": 1.0, + ".\\cogs\\utility.py": { + "CONFIDENCE.HIGH": 2.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, + "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 1.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1587, + "loc": 1710, "nosec": 0 }, - "core/_color_data.py": { + ".\\core\\_color_data.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -71,10 +59,10 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1168, + "loc": 1166, "nosec": 0 }, - "core/changelog.py": { + ".\\core\\changelog.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -83,10 +71,10 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 154, + "loc": 145, "nosec": 0 }, - "core/checks.py": { + ".\\core\\checks.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -95,10 +83,22 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 75, + "loc": 89, + "nosec": 0 + }, + ".\\core\\clients.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 1.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 1.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 585, "nosec": 0 }, - "core/clients.py": { + ".\\core\\config.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -107,10 +107,10 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 200, + "loc": 327, "nosec": 0 }, - "core/config.py": { + ".\\core\\decorators.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -119,10 +119,10 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 276, + "loc": 9, "nosec": 0 }, - "core/decorators.py": { + ".\\core\\models.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -131,10 +131,10 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 7, + "loc": 199, "nosec": 0 }, - "core/models.py": { + ".\\core\\paginator.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -143,10 +143,10 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 91, + "loc": 209, "nosec": 0 }, - "core/paginator.py": { + ".\\core\\thread.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -155,10 +155,10 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 214, + "loc": 993, "nosec": 0 }, - "core/thread.py": { + ".\\core\\time.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -167,10 +167,10 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 716, + "loc": 158, "nosec": 0 }, - "core/time.py": { + ".\\core\\utils.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -179,65 +179,103 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 169, + "loc": 283, "nosec": 0 }, - "core/utils.py": { - "CONFIDENCE.HIGH": 0.0, + "_totals": { + "CONFIDENCE.HIGH": 4.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 1.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 1.0, - "SEVERITY.MEDIUM": 0.0, + "SEVERITY.LOW": 4.0, + "SEVERITY.MEDIUM": 1.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 199, + "loc": 8989, "nosec": 0 } }, "results": [ { - "code": "14 from site import USER_SITE\n15 from subprocess import PIPE\n16 \n17 import discord\n", - "filename": "cogs/plugins.py", + "code": "11 from datetime import datetime\n12 from subprocess import PIPE\n13 from types import SimpleNamespace\n", + "filename": ".\\bot.py", + "issue_confidence": "HIGH", + "issue_severity": "LOW", + "issue_text": "Consider possible security implications associated with PIPE module.", + "line_number": 12, + "line_range": [ + 12 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", + "test_id": "B404", + "test_name": "blacklist" + }, + { + "code": "13 from site import USER_SITE\n14 from subprocess import PIPE\n15 \n16 import discord\n", + "filename": ".\\cogs\\plugins.py", + "issue_confidence": "HIGH", + "issue_severity": "LOW", + "issue_text": "Consider possible security implications associated with PIPE module.", + "line_number": 14, + "line_range": [ + 14, + 15 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", + "test_id": "B404", + "test_name": "blacklist" + }, + { + "code": "12 from json import JSONDecodeError, loads\n13 from subprocess import PIPE\n14 from textwrap import indent\n", + "filename": ".\\cogs\\utility.py", "issue_confidence": "HIGH", "issue_severity": "LOW", "issue_text": "Consider possible security implications associated with PIPE module.", - "line_number": 15, + "line_number": 13, "line_range": [ - 15, - 16 + 13 ], "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", "test_id": "B404", "test_name": "blacklist" }, { - "code": "1824 try:\n1825 exec(to_compile, env) # pylint: disable=exec-used\n1826 except Exception as exc:\n", - "filename": "cogs/utility.py", + "code": "1985 try:\n1986 exec(to_compile, env) # pylint: disable=exec-used\n1987 except Exception as exc:\n", + "filename": ".\\cogs\\utility.py", "issue_confidence": "HIGH", "issue_severity": "MEDIUM", "issue_text": "Use of exec detected.", - "line_number": 1825, + "line_number": 1986, "line_range": [ - 1825 + 1986 ], "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b102_exec_used.html", "test_id": "B102", "test_name": "exec_used" }, { - "code": "219 for token in shlex.shlex(alias, punctuation_chars=\"&\"):\n220 if token != \"&&\":\n221 buffer += \" \" + token\n", - "filename": "core/utils.py", + "code": "68 \n69 def __init__(self, bot, access_token: str = \"\", username: str = \"\", **kwargs):\n70 self.bot = bot\n71 self.session = bot.session\n72 self.headers: dict = None\n73 self.access_token = access_token\n74 self.username = username\n75 self.avatar_url: str = kwargs.pop(\"avatar_url\", \"\")\n76 self.url: str = kwargs.pop(\"url\", \"\")\n77 if self.access_token:\n78 self.headers = {\"Authorization\": \"token \" + str(access_token)}\n79 \n80 async def request(\n", + "filename": ".\\core\\clients.py", "issue_confidence": "MEDIUM", "issue_severity": "LOW", - "issue_text": "Possible hardcoded password: '&&'", - "line_number": 220, + "issue_text": "Possible hardcoded password: ''", + "line_number": 69, "line_range": [ - 220 + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79 ], - "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b105_hardcoded_password_string.html", - "test_id": "B105", - "test_name": "hardcoded_password_string" + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", + "test_id": "B107", + "test_name": "hardcoded_password_default" } ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 87faf160e0..14a5065a7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev18 +# v3.7.0-dev19 ### Added diff --git a/bot.py b/bot.py index 1827bb7e50..a9d1fe6a0b 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev18" +__version__ = "3.7.0-dev19" import asyncio From abff6857daffcb01e7385ea33cd5fa525299c980 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:23:47 +0800 Subject: [PATCH 0704/1221] Fix issues with update --- .bandit_baseline.json | 40 ++++++++++++++++++++-------------------- cogs/utility.py | 8 +++++--- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.bandit_baseline.json b/.bandit_baseline.json index df81a6fecc..94fcfd0fc3 100644 --- a/.bandit_baseline.json +++ b/.bandit_baseline.json @@ -2,7 +2,7 @@ "errors": [], "generated_at": "2020-11-12T15:17:38Z", "metrics": { - ".\\bot.py": { + "./bot.py": { "CONFIDENCE.HIGH": 1.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -14,7 +14,7 @@ "loc": 1264, "nosec": 0 }, - ".\\cogs\\modmail.py": { + "./cogs/modmail.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -26,7 +26,7 @@ "loc": 1280, "nosec": 0 }, - ".\\cogs\\plugins.py": { + "./cogs/plugins.py": { "CONFIDENCE.HIGH": 1.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -38,7 +38,7 @@ "loc": 572, "nosec": 0 }, - ".\\cogs\\utility.py": { + "./cogs/utility.py": { "CONFIDENCE.HIGH": 2.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -50,7 +50,7 @@ "loc": 1710, "nosec": 0 }, - ".\\core\\_color_data.py": { + "./core/_color_data.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -62,7 +62,7 @@ "loc": 1166, "nosec": 0 }, - ".\\core\\changelog.py": { + "./core/changelog.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -74,7 +74,7 @@ "loc": 145, "nosec": 0 }, - ".\\core\\checks.py": { + "./core/checks.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -86,7 +86,7 @@ "loc": 89, "nosec": 0 }, - ".\\core\\clients.py": { + "./core/clients.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 1.0, @@ -98,7 +98,7 @@ "loc": 585, "nosec": 0 }, - ".\\core\\config.py": { + "./core/config.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -110,7 +110,7 @@ "loc": 327, "nosec": 0 }, - ".\\core\\decorators.py": { + "./core/decorators.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -122,7 +122,7 @@ "loc": 9, "nosec": 0 }, - ".\\core\\models.py": { + "./core/models.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -134,7 +134,7 @@ "loc": 199, "nosec": 0 }, - ".\\core\\paginator.py": { + "./core/paginator.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -146,7 +146,7 @@ "loc": 209, "nosec": 0 }, - ".\\core\\thread.py": { + "./core/thread.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -158,7 +158,7 @@ "loc": 993, "nosec": 0 }, - ".\\core\\time.py": { + "./core/time.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -170,7 +170,7 @@ "loc": 158, "nosec": 0 }, - ".\\core\\utils.py": { + "./core/utils.py": { "CONFIDENCE.HIGH": 0.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, @@ -198,7 +198,7 @@ "results": [ { "code": "11 from datetime import datetime\n12 from subprocess import PIPE\n13 from types import SimpleNamespace\n", - "filename": ".\\bot.py", + "filename": "./bot.py", "issue_confidence": "HIGH", "issue_severity": "LOW", "issue_text": "Consider possible security implications associated with PIPE module.", @@ -212,7 +212,7 @@ }, { "code": "13 from site import USER_SITE\n14 from subprocess import PIPE\n15 \n16 import discord\n", - "filename": ".\\cogs\\plugins.py", + "filename": "./cogs/plugins.py", "issue_confidence": "HIGH", "issue_severity": "LOW", "issue_text": "Consider possible security implications associated with PIPE module.", @@ -227,7 +227,7 @@ }, { "code": "12 from json import JSONDecodeError, loads\n13 from subprocess import PIPE\n14 from textwrap import indent\n", - "filename": ".\\cogs\\utility.py", + "filename": "./cogs/utility.py", "issue_confidence": "HIGH", "issue_severity": "LOW", "issue_text": "Consider possible security implications associated with PIPE module.", @@ -241,7 +241,7 @@ }, { "code": "1985 try:\n1986 exec(to_compile, env) # pylint: disable=exec-used\n1987 except Exception as exc:\n", - "filename": ".\\cogs\\utility.py", + "filename": "./cogs/utility.py", "issue_confidence": "HIGH", "issue_severity": "MEDIUM", "issue_text": "Use of exec detected.", @@ -255,7 +255,7 @@ }, { "code": "68 \n69 def __init__(self, bot, access_token: str = \"\", username: str = \"\", **kwargs):\n70 self.bot = bot\n71 self.session = bot.session\n72 self.headers: dict = None\n73 self.access_token = access_token\n74 self.username = username\n75 self.avatar_url: str = kwargs.pop(\"avatar_url\", \"\")\n76 self.url: str = kwargs.pop(\"url\", \"\")\n77 if self.access_token:\n78 self.headers = {\"Authorization\": \"token \" + str(access_token)}\n79 \n80 async def request(\n", - "filename": ".\\core\\clients.py", + "filename": "./core/clients.py", "issue_confidence": "MEDIUM", "issue_severity": "LOW", "issue_text": "Possible hardcoded password: ''", diff --git a/cogs/utility.py b/cogs/utility.py index 0b228bbdd2..cf83e92f67 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1928,17 +1928,19 @@ async def update(self, ctx, *, flag: str = ""): logger.info("Bot has been updated.") if self.bot.hosting_method == HostingMethod.PM2: embed = discord.Embed( - title="Bot has been updated", color=self.bot.main_color + title="Bot has been updated", + description=f"Version: {latest.version}", + color=self.bot.main_color, ) await ctx.send(embed=embed) else: embed = discord.Embed( title="Bot has been updated and is logging out.", - description="If you do not have an auto-restart setup, please manually start the bot.", + description=f"Version: {latest.version}\nIf you do not have an auto-restart setup, please manually start the bot.", color=self.bot.main_color, ) await ctx.send(embed=embed) - await self.bot.logout() + await self.bot.logout() else: embed = discord.Embed(title="Already up to date.", color=self.bot.main_color,) await ctx.send(embed=embed) From 342dc2091e130a94ea1f1b77dc2e9c9159b931d0 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:27:06 +0800 Subject: [PATCH 0705/1221] Bump version --- CHANGELOG.md | 2 +- bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a5065a7d..09a7d5dab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev19 +# v3.7.0-dev20 ### Added diff --git a/bot.py b/bot.py index a9d1fe6a0b..d69290d969 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev19" +__version__ = "3.7.0-dev20" import asyncio From a6eef115176cef5aeb8fdbc406f785c5368f6afc Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:31:16 +0800 Subject: [PATCH 0706/1221] Support heroku in prerelease update --- core/clients.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/clients.py b/core/clients.py index 90a9d23294..db4e9936b4 100644 --- a/core/clients.py +++ b/core/clients.py @@ -61,7 +61,6 @@ class GitHub: BASE = "https://api.github.com" REPO = BASE + "/repos/kyb3r/modmail" - HEAD = REPO + "/git/refs/heads/master" MERGE_URL = BASE + "/repos/{username}/modmail/merges" FORK_URL = REPO + "/forks" STAR_URL = BASE + "/user/starred/kyb3r/modmail" @@ -77,6 +76,10 @@ def __init__(self, bot, access_token: str = "", username: str = "", **kwargs): if self.access_token: self.headers = {"Authorization": "token " + str(access_token)} + @property + def BRANCH(self): + return "master" if not self.bot.version.is_prerelease else "development" + async def request( self, url: str, @@ -151,10 +154,10 @@ async def update_repository(self, sha: str = None) -> Optional[dict]: raise commands.CommandInvokeError("Username not found.") if sha is None: - resp: dict = await self.request(self.HEAD) + resp: dict = await self.request(self.REPO + "/git/refs/heads/" + self.BRANCH) sha = resp["object"]["sha"] - payload = {"base": "master", "head": sha, "commit_message": "Updating bot"} + payload = {"base": self.BRANCH, "head": sha, "commit_message": "Updating bot"} merge_url = self.MERGE_URL.format(username=self.username) From 1ae3cf6b8570da2a4492654b8606ba24df75c149 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:50:48 +0800 Subject: [PATCH 0707/1221] Hotfix heroku logging --- CHANGELOG.md | 2 +- bot.py | 6 +----- cogs/utility.py | 13 ++++++++++--- core/models.py | 6 ++++++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09a7d5dab6..688f8559b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev20 +# v3.7.0-dev21 ### Added diff --git a/bot.py b/bot.py index d69290d969..8c67b9ffa0 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev20" +__version__ = "3.7.0-dev21" import asyncio @@ -109,10 +109,6 @@ def hosting_method(self) -> HostingMethod: return HostingMethod.OTHER - @property - def is_pm2(self) -> bool: - return ".heroku" in os.environ.get("PYTHONHOME", "") - def startup(self): logger.line() if os.name != "nt": diff --git a/cogs/utility.py b/cogs/utility.py index cf83e92f67..064d88b250 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1929,20 +1929,25 @@ async def update(self, ctx, *, flag: str = ""): if self.bot.hosting_method == HostingMethod.PM2: embed = discord.Embed( title="Bot has been updated", - description=f"Version: {latest.version}", color=self.bot.main_color, ) + embed.set_footer( + text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}" + ) await ctx.send(embed=embed) else: embed = discord.Embed( title="Bot has been updated and is logging out.", - description=f"Version: {latest.version}\nIf you do not have an auto-restart setup, please manually start the bot.", + description="If you do not have an auto-restart setup, please manually start the bot.", color=self.bot.main_color, ) + embed.set_footer( + text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}" + ) await ctx.send(embed=embed) await self.bot.logout() else: - embed = discord.Embed(title="Already up to date.", color=self.bot.main_color,) + embed = discord.Embed(title="Already up to date", description=desc, color=self.bot.main_color,) await ctx.send(embed=embed) @commands.command(hidden=True, name="eval") @@ -2023,6 +2028,8 @@ def paginate(text: str): break await ctx.send(f"```py\n{page}\n```") + await self.bot.add_reaction(ctx.message, "\u2705") + def setup(bot): bot.add_cog(Utility(bot)) diff --git a/core/models.py b/core/models.py index 6882fe6b5f..66965a6d88 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,7 @@ import logging import re import sys +import os from enum import IntEnum from logging.handlers import RotatingFileHandler from string import Formatter @@ -16,6 +17,11 @@ Fore = Style = type("Dummy", (object,), {"__getattr__": lambda self, item: ""})() +if ".heroku" in os.environ.get("PYTHONHOME", ""): + # heroku + Fore = Style = type("Dummy", (object,), {"__getattr__": lambda self, item: ""})() + + class PermissionLevel(IntEnum): OWNER = 5 ADMINISTRATOR = 4 From 56496591da130707d0fc061b7b185d24f2177c7c Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:55:12 +0800 Subject: [PATCH 0708/1221] Failsafe for long changelogs --- cogs/utility.py | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 064d88b250..e4b90f4011 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1902,8 +1902,10 @@ async def update(self, ctx, *, flag: str = ""): embed.description = latest.description for name, value in latest.fields.items(): + if value > 200: + value = value[:200] + "..." + embed.add_field(name=name, value=value) - # message = commit_data['commit']['message'] html_url = commit_data["html_url"] short_sha = commit_data["sha"][:6] embed.add_field(name="Merge Commit", value=f"[`{short_sha}`]({html_url})") @@ -1926,28 +1928,29 @@ async def update(self, ctx, *, flag: str = ""): if res != "Already up to date.": logger.info("Bot has been updated.") - if self.bot.hosting_method == HostingMethod.PM2: - embed = discord.Embed( - title="Bot has been updated", - color=self.bot.main_color, - ) - embed.set_footer( - text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}" - ) - await ctx.send(embed=embed) - else: - embed = discord.Embed( - title="Bot has been updated and is logging out.", - description="If you do not have an auto-restart setup, please manually start the bot.", - color=self.bot.main_color, - ) - embed.set_footer( - text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}" + + embed = discord.Embed(title="Bot has been updated", color=self.bot.main_color,) + embed.set_footer( + text=f"Updating Modmail v{self.bot.version} " f"-> v{latest.version}" + ) + embed.description = latest.description + for name, value in latest.fields.items(): + if value > 200: + value = value[:200] + "..." + + embed.add_field(name=name, value=value) + + if self.bot.hosting_method == HostingMethod.OTHER: + embed.description = ( + "If you do not have an auto-restart setup, please manually start the bot.", ) - await ctx.send(embed=embed) + + await ctx.send(embed=embed) await self.bot.logout() else: - embed = discord.Embed(title="Already up to date", description=desc, color=self.bot.main_color,) + embed = discord.Embed( + title="Already up to date", description=desc, color=self.bot.main_color, + ) await ctx.send(embed=embed) @commands.command(hidden=True, name="eval") From bd187704a033e8bccc6a0d96ec8152faefc3f331 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:58:14 +0800 Subject: [PATCH 0709/1221] Tese --- CHANGELOG.md | 2 +- bot.py | 2 +- cogs/utility.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 688f8559b2..fd5f060d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev21 +# v3.7.0-dev22 ### Added diff --git a/bot.py b/bot.py index 8c67b9ffa0..370e583882 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev21" +__version__ = "3.7.0-dev22" import asyncio diff --git a/cogs/utility.py b/cogs/utility.py index e4b90f4011..5b379609d0 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1885,6 +1885,7 @@ async def update(self, ctx, *, flag: str = ""): data = await self.bot.api.update_repository() commit_data = data["data"] + print(commit_data) user = data["user"] if commit_data: @@ -1906,6 +1907,7 @@ async def update(self, ctx, *, flag: str = ""): value = value[:200] + "..." embed.add_field(name=name, value=value) + html_url = commit_data["html_url"] short_sha = commit_data["sha"][:6] embed.add_field(name="Merge Commit", value=f"[`{short_sha}`]({html_url})") From eddee03f1e885160864128e81077be1893141338 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 12 Nov 2020 23:59:49 +0800 Subject: [PATCH 0710/1221] Test --- cogs/utility.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/utility.py b/cogs/utility.py index 5b379609d0..96bfb48adb 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1886,6 +1886,7 @@ async def update(self, ctx, *, flag: str = ""): commit_data = data["data"] print(commit_data) + user = data["user"] if commit_data: From 37edddc582d43dcce8f561e834baba17375de9ed Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 13 Nov 2020 00:08:14 +0800 Subject: [PATCH 0711/1221] test again --- cogs/utility.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 96bfb48adb..5acc592fc9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1889,7 +1889,7 @@ async def update(self, ctx, *, flag: str = ""): user = data["user"] - if commit_data: + if not commit_data or not commit_data.get("html_url"): embed = discord.Embed(color=self.bot.main_color) embed.set_footer( @@ -1904,7 +1904,7 @@ async def update(self, ctx, *, flag: str = ""): embed.description = latest.description for name, value in latest.fields.items(): - if value > 200: + if len(value) > 200: value = value[:200] + "..." embed.add_field(name=name, value=value) From 4afc8d0e32f70a67ed28bd5fb9c2a3ec338bf51f Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 13 Nov 2020 00:11:46 +0800 Subject: [PATCH 0712/1221] Fixes --- cogs/utility.py | 4 ++-- core/clients.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 5acc592fc9..145bb34602 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1889,7 +1889,7 @@ async def update(self, ctx, *, flag: str = ""): user = data["user"] - if not commit_data or not commit_data.get("html_url"): + if commit_data and commit_data.get("html_url"): embed = discord.Embed(color=self.bot.main_color) embed.set_footer( @@ -1914,7 +1914,7 @@ async def update(self, ctx, *, flag: str = ""): embed.add_field(name="Merge Commit", value=f"[`{short_sha}`]({html_url})") else: embed = discord.Embed( - title="Already up to date with master repository.", + title="Already up to date", description="No further updates required", color=self.bot.main_color, ) diff --git a/core/clients.py b/core/clients.py index db4e9936b4..2a1517fbc4 100644 --- a/core/clients.py +++ b/core/clients.py @@ -159,6 +159,8 @@ async def update_repository(self, sha: str = None) -> Optional[dict]: payload = {"base": self.BRANCH, "head": sha, "commit_message": "Updating bot"} + print(payload) + merge_url = self.MERGE_URL.format(username=self.username) resp = await self.request(merge_url, method="POST", payload=payload) From e6ebf912f3f9a9cfd3eef22ff5b338db2b1b8b6e Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 13 Nov 2020 00:15:37 +0800 Subject: [PATCH 0713/1221] test again --- cogs/utility.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 145bb34602..ccffd5ccaa 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1886,7 +1886,6 @@ async def update(self, ctx, *, flag: str = ""): commit_data = data["data"] print(commit_data) - user = data["user"] if commit_data and commit_data.get("html_url"): From c5f999f3f11a0a2b30bb117e18c46db6236faa68 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 13 Nov 2020 00:16:57 +0800 Subject: [PATCH 0714/1221] Remove debug notes --- cogs/utility.py | 1 - core/clients.py | 1 - 2 files changed, 2 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index ccffd5ccaa..bf38021314 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1885,7 +1885,6 @@ async def update(self, ctx, *, flag: str = ""): data = await self.bot.api.update_repository() commit_data = data["data"] - print(commit_data) user = data["user"] if commit_data and commit_data.get("html_url"): diff --git a/core/clients.py b/core/clients.py index 2a1517fbc4..922b48d8af 100644 --- a/core/clients.py +++ b/core/clients.py @@ -159,7 +159,6 @@ async def update_repository(self, sha: str = None) -> Optional[dict]: payload = {"base": self.BRANCH, "head": sha, "commit_message": "Updating bot"} - print(payload) merge_url = self.MERGE_URL.format(username=self.username) From 02465e7d0e30d08f72f887945021d99117853721 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 13 Nov 2020 00:17:37 +0800 Subject: [PATCH 0715/1221] Linting --- core/clients.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/clients.py b/core/clients.py index 922b48d8af..db4e9936b4 100644 --- a/core/clients.py +++ b/core/clients.py @@ -159,7 +159,6 @@ async def update_repository(self, sha: str = None) -> Optional[dict]: payload = {"base": self.BRANCH, "head": sha, "commit_message": "Updating bot"} - merge_url = self.MERGE_URL.format(username=self.username) resp = await self.request(merge_url, method="POST", payload=payload) From 771ff966f9515e803dff4490d97fb7d31547d828 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 13 Nov 2020 13:21:55 +0800 Subject: [PATCH 0716/1221] Cleanup and add to app.json --- app.json | 4 ++++ cogs/modmail.py | 9 --------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app.json b/app.json index 41697cf064..326c54273e 100644 --- a/app.json +++ b/app.json @@ -30,6 +30,10 @@ "LOG_URL": { "description": "The url of the log viewer app for viewing self-hosted logs.", "required": true + }, + "GITHUB_TOKEN": { + "description": "A github personal access token with the repo scope.", + "required": true } } } \ No newline at end of file diff --git a/cogs/modmail.py b/cogs/modmail.py index d036a6ee01..ed9a3dfec7 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1535,15 +1535,6 @@ async def isenable(self, ctx): return await ctx.send(embed=embed) - @commands.command(usage="[after] [close message]") - @checks.has_permissions(PermissionLevel.SUPPORTER) - @checks.thread_only() - async def adduser(self, ctx, *, member: discord.Member = None): - await ctx.thread.add_user(member) - - sent_emoji, _ = await self.bot.retrieve_emoji() - await self.bot.add_reaction(ctx.message, sent_emoji) - def setup(bot): bot.add_cog(Modmail(bot)) From 97bfaccaa5b55abfd656d1e6005c5b2521496e7a Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 13 Nov 2020 13:23:11 +0800 Subject: [PATCH 0717/1221] fix bug --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index bf38021314..80496d4f1d 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1936,7 +1936,7 @@ async def update(self, ctx, *, flag: str = ""): ) embed.description = latest.description for name, value in latest.fields.items(): - if value > 200: + if len(value) > 200: value = value[:200] + "..." embed.add_field(name=name, value=value) From 9497f17310e936c8c549d11eb94a21c781ded53b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 13 Nov 2020 13:47:11 +0800 Subject: [PATCH 0718/1221] Cleanup --- bot.py | 8 ++------ cogs/plugins.py | 1 - cogs/utility.py | 12 +++--------- core/changelog.py | 2 +- core/utils.py | 1 - 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/bot.py b/bot.py index 370e583882..3ae6ed03fa 100644 --- a/bot.py +++ b/bot.py @@ -45,7 +45,7 @@ ) from core.thread import ThreadManager from core.time import human_timedelta -from core.utils import human_join, match_title, normalize_alias +from core.utils import human_join, normalize_alias, truncate logger = getLogger(__name__) @@ -996,13 +996,9 @@ async def on_message(self, message): and self.config["alert_on_mention"] and not message.author.bot ): - if len(message.content) > 50: - extra = "..." - else: - extra = "" em = discord.Embed( title="Bot mention", - description=f"[Jump URL]({message.jump_url})\n{message.content[:50]}{extra}", + description=f"[Jump URL]({message.jump_url})\n{truncate(message.content, 50)}", color=self.main_color, timestamp=datetime.utcnow(), ) diff --git a/cogs/plugins.py b/cogs/plugins.py index fe70b5225b..cdea3a4ce8 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -15,7 +15,6 @@ import discord from discord.ext import commands -from discord.utils import async_all from pkg_resources import parse_version diff --git a/cogs/utility.py b/cogs/utility.py index 80496d4f1d..129551af87 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -31,7 +31,7 @@ UnseenFormatter, getLogger, ) -from core.utils import trigger_typing +from core.utils import trigger_typing, truncate from core.paginator import EmbedPaginatorSession, MessagePaginatorSession @@ -1902,10 +1902,7 @@ async def update(self, ctx, *, flag: str = ""): embed.description = latest.description for name, value in latest.fields.items(): - if len(value) > 200: - value = value[:200] + "..." - - embed.add_field(name=name, value=value) + embed.add_field(name=name, value=truncate(value, 200)) html_url = commit_data["html_url"] short_sha = commit_data["sha"][:6] @@ -1936,10 +1933,7 @@ async def update(self, ctx, *, flag: str = ""): ) embed.description = latest.description for name, value in latest.fields.items(): - if len(value) > 200: - value = value[:200] + "..." - - embed.add_field(name=name, value=value) + embed.add_field(name=name, value=truncate(value, 200)) if self.bot.hosting_method == HostingMethod.OTHER: embed.description = ( diff --git a/core/changelog.py b/core/changelog.py index ace825482f..60d0179609 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -93,7 +93,7 @@ def embed(self) -> Embed: ) for name, value in self.fields.items(): - embed.add_field(name=name, value=truncate(value, 1024)) + embed.add_field(name=name, value=truncate(value, 1024), inline=False) embed.set_footer(text=f"Current version: v{self.bot.version}") embed.set_thumbnail(url=self.bot.user.avatar_url) return embed diff --git a/core/utils.py b/core/utils.py index 5eb8ea1680..b6640eaed8 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,5 +1,4 @@ import base64 -from core.models import HostingMethod import functools import re import string From a73afc34adca5b458384469bc3902778a3c6dde6 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 13 Nov 2020 21:39:54 +0800 Subject: [PATCH 0719/1221] Update a docstirng, push v --- CHANGELOG.md | 2 +- bot.py | 2 +- cogs/utility.py | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd5f060d58..8e69d44d8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.0-dev22 +# v3.7.0 ### Added diff --git a/bot.py b/bot.py index 3ae6ed03fa..3737f35471 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0-dev22" +__version__ = "3.7.0" import asyncio diff --git a/cogs/utility.py b/cogs/utility.py index 129551af87..a525c7b131 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1855,9 +1855,7 @@ async def github(self, ctx): async def update(self, ctx, *, flag: str = ""): """ Update Modmail. - This only works for PM2 or Heroku users who have configured their bot for updates. - To stay up-to-date with the latest commit - from GitHub, specify "force" as the flag. + To stay up-to-date with the latest commit rom GitHub, specify "force" as the flag. """ changelog = await Changelog.from_url(self.bot) From c5db7b91024d4ae86d5e301cf91e3de190c39261 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 13 Nov 2020 22:55:36 +0800 Subject: [PATCH 0720/1221] Improve react to contact, add docstring to selfcontact --- CHANGELOG.md | 7 +++++++ bot.py | 19 +++++++++++-------- cogs/modmail.py | 1 + 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e69d44d8d..38b43c0b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.1 + +### Fixed + +- Bot will now leave a reaction on the react to contact message. +- Added docstring to selfcontact + # v3.7.0 ### Added diff --git a/bot.py b/bot.py index 3737f35471..ffd0621ba5 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.0" +__version__ = "3.7.1" import asyncio @@ -1166,14 +1166,17 @@ async def on_raw_reaction_add(self, payload): if emoji_fmt == react_message_emoji: channel = self.get_channel(payload.channel_id) member = channel.guild.get_member(payload.user_id) - message = await channel.fetch_message(payload.message_id) - await message.remove_reaction(payload.emoji, member) + if not member.bot: + message = await channel.fetch_message(payload.message_id) + await message.remove_reaction(payload.emoji, member) + + ctx = await self.get_context(message) + ctx.author = member + await ctx.invoke( + self.get_command("contact"), user=member, manual_trigger=False + ) - ctx = await self.get_context(message) - ctx.author = member - await ctx.invoke( - self.get_command("contact"), user=member, manual_trigger=False - ) + await message.add_reaction(emoji_fmt) # bot adds as well async def on_raw_reaction_remove(self, payload): if self.config["transfer_reactions"]: diff --git a/cogs/modmail.py b/cogs/modmail.py index ed9a3dfec7..2a47124ad8 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -940,6 +940,7 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str): @commands.command() @checks.has_permissions(PermissionLevel.REGULAR) async def selfcontact(self, ctx): + """Creates a thread with yourself""" await ctx.invoke(self.contact, user=ctx.author) @commands.command(usage=" [category] [options]") From 7da3dc7d4122586341de9b72b28c41edb66f7cb0 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 13 Nov 2020 16:16:10 +0100 Subject: [PATCH 0721/1221] Update PRIVACY.md typo fix --- PRIVACY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRIVACY.md b/PRIVACY.md index 8ff361b702..ebe5ce6954 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -32,7 +32,7 @@ The Modmail Team collect some metadata to keep us updated on the number of insta - Bot uptime - Bot latency - Bot version -- Whether the bot is seflhosted +- Whether the bot is selfhosted No tokens/passwords/private data is ever being collected or sent to our servers. From dd9e25e4c2177ff840377dd5e4ccc778f6193045 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 00:07:45 +0800 Subject: [PATCH 0722/1221] check dpy v, mention_chnanel_id, resolves #2880 --- CHANGELOG.md | 5 +++++ bot.py | 27 +++++++++++++++++++++++++-- cogs/utility.py | 1 + core/config.py | 1 + core/config_help.json | 11 +++++++++++ 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b43c0b7e..eb7f5f664a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# 3.7.2 + +### Added +- Added `mention_channel_id` to specify which channel `alert_on_mention` was being sent to ([GH #2880](https://github.com/kyb3r/modmail/issues/2880)) + # v3.7.1 ### Fixed diff --git a/bot.py b/bot.py index ffd0621ba5..363784147d 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.1" +__version__ = "3.7.2" import asyncio @@ -252,6 +252,21 @@ def log_channel(self) -> typing.Optional[discord.TextChannel]: ) return None + @property + def mention_channel(self): + channel_id = self.config["mention_channel_id"] + if channel_id is not None: + try: + channel = self.get_channel(int(channel_id)) + if channel is not None: + return channel + except ValueError: + pass + logger.debug("MENTION_CHANNEL_ID was invalid, removed.") + self.config.remove("mention_channel_id") + + return self.log_channel + async def wait_for_connected(self) -> None: await self.wait_until_ready() await self._connected.wait() @@ -1002,7 +1017,7 @@ async def on_message(self, message): color=self.main_color, timestamp=datetime.utcnow(), ) - await self.log_channel.send(content=self.config["mention"], embed=em) + await self.mention_channel.send(content=self.config["mention"], embed=em) await self.process_commands(message) @@ -1504,6 +1519,14 @@ def main(): except ImportError: pass + # check discord version + if discord.__version__ != "1.5.2": + logger.error( + "Dependencies are not updated, run pipenv install. discord.py version expected 1.5.2, recieved %s", + discord.__version__, + ) + sys.exit(0) + bot = ModmailBot() bot.run() diff --git a/cogs/utility.py b/cogs/utility.py index a525c7b131..35fa482416 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -324,6 +324,7 @@ async def about(self, ctx): embed.add_field(name="Latency", value=f"{self.bot.latency * 1000:.2f} ms") embed.add_field(name="Version", value=f"`{self.bot.version}`") embed.add_field(name="Authors", value="`kyb3r`, `Taki`, `fourjr`") + embed.add_field(name="Hosting Method", value=self.bot.hosting_method.name) changelog = await Changelog.from_url(self.bot) latest = changelog.latest_version diff --git a/core/config.py b/core/config.py index 9aab61acf2..58061b8ae2 100644 --- a/core/config.py +++ b/core/config.py @@ -42,6 +42,7 @@ class ConfigManager: "plain_reply_without_command": False, # logging "log_channel_id": None, + "mention_channel_id": None, # threads "sent_emoji": "✅", "blocked_emoji": "🚫", diff --git a/core/config_help.json b/core/config_help.json index 32cc694f32..f935e8657c 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -174,6 +174,17 @@ "If the Modmail logging channel ended up being non-existent/invalid, no logs will be sent." ] }, + "mention_channel_id": { + "default": "Log Channel (normally `#bot-logs`)", + "description": "This is the channel where bot mentions are sent to.", + "examples": [ + "`{prefix}config set mention_channel_id 9234932582312` (9234932582312 is the channel ID)" + ], + "notes": [ + "This has no effect unless `alert_on_mention` is set to yes.", + "See also: `log_channel_id`" + ] + }, "sent_emoji": { "default": "✅", "description": "This is the emoji added to the message when when a Modmail action is invoked successfully (ie. DM Modmail, edit message, etc.).", From 6b58f3521228b18e04fb0a863163f95dcfc38af9 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 00:11:12 +0800 Subject: [PATCH 0723/1221] fix issue with config set --- CHANGELOG.md | 6 ++++++ cogs/utility.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7f5f664a..f56d6fcbbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,14 @@ however, insignificant breaking changes do not guarantee a major version bump, s # 3.7.2 ### Added + - Added `mention_channel_id` to specify which channel `alert_on_mention` was being sent to ([GH #2880](https://github.com/kyb3r/modmail/issues/2880)) +### Fixed + +- `?config set` would not respond if an invalid key was provided. + + # v3.7.1 ### Fixed diff --git a/cogs/utility.py b/cogs/utility.py index 35fa482416..801069d125 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -772,7 +772,7 @@ async def config_set(self, ctx, key: str.lower, *, value: str): 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)) + embed.add_field(name="Valid keys", value=truncate(", ".join(valid_keys), 1024)) return await ctx.send(embed=embed) From 49bd8d036ae80ac415b0b5b11ca7ab330e4c186b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 00:12:14 +0800 Subject: [PATCH 0724/1221] Add dpy version --- bot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot.py b/bot.py index 363784147d..56d518d17a 100644 --- a/bot.py +++ b/bot.py @@ -120,6 +120,8 @@ def startup(self): logger.info("v%s", __version__) logger.info("Authors: kyb3r, fourjr, Taaku18") logger.line() + logger.info("discord.py: v%s", discord.__version__) + logger.line() for cog in self.loaded_cogs: logger.debug("Loading %s.", cog) From bf8535adf372e69f9c16adea8a24782345a746a1 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 00:23:50 +0800 Subject: [PATCH 0725/1221] React to contact threads were treated like normal contact threads. --- CHANGELOG.md | 10 ++++++++-- core/thread.py | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f56d6fcbbd..07c670c5c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# 3.7.2 +# v3.7.3 + +### Fixed + +- React to contact threads were treated like normal contact threads. ([GH #2881](https://github.com/kyb3r/modmail/issues/2881)) + +# v3.7.2 ### Added -- Added `mention_channel_id` to specify which channel `alert_on_mention` was being sent to ([GH #2880](https://github.com/kyb3r/modmail/issues/2880)) +- Added `mention_channel_id` to specify which channel `alert_on_mention` was being sent to. ([GH #2880](https://github.com/kyb3r/modmail/issues/2880)) ### Fixed diff --git a/core/thread.py b/core/thread.py index 2713afd273..1cbf2720a4 100644 --- a/core/thread.py +++ b/core/thread.py @@ -155,7 +155,7 @@ async def setup(self, *, creator=None, category=None, initial_message=None): await channel.edit(topic=f"User ID: {recipient.id}") self.ready = True - if creator: + if creator != recipient: mention = None else: mention = self.bot.config["mention"] @@ -191,7 +191,7 @@ async def send_recipient_genesis_message(): embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) embed.title = self.bot.config["thread_creation_title"] - if creator is None: + if creator != recipient: msg = await recipient.send(embed=embed) if recipient_thread_close: From b578d7737ed85c0884d59866d217cdff7de7f211 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 00:29:54 +0800 Subject: [PATCH 0726/1221] Bump version --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 56d518d17a..c6f14ee924 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.2" +__version__ = "3.7.3" import asyncio From bfef29a98b6bf2cd6e35718afc209e895ff1bd36 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 01:06:32 +0800 Subject: [PATCH 0727/1221] fix close on emoji --- CHANGELOG.md | 6 ++++++ bot.py | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c670c5c8..0c368cfd1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.4 + +### Fixed + +- Close on emoji was not working. + # v3.7.3 ### Fixed diff --git a/bot.py b/bot.py index c6f14ee924..4378c9c4f9 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.3" +__version__ = "3.7.4" import asyncio @@ -1157,19 +1157,19 @@ async def handle_reaction_events(self, payload): logger.warning("Failed to find linked message for reactions: %s", e) return - if payload.event_type == "REACTION_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) + if self.config["transfer_reactions"]: + if payload.event_type == "REACTION_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): - if self.config["transfer_reactions"]: - await self.handle_reaction_events(payload) + await self.handle_reaction_events(payload) react_message_id = tryint(self.config.get("react_to_contact_message")) react_message_emoji = self.config.get("react_to_contact_emoji") From a7d3345864462efc27bc1d5084e95619d923165c Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 01:21:53 +0800 Subject: [PATCH 0728/1221] fix close on emoji --- CHANGELOG.md | 2 +- bot.py | 2 +- core/thread.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c368cfd1f..e8fe10301e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.4 +# v3.7.5 ### Fixed diff --git a/bot.py b/bot.py index 4378c9c4f9..ad22ad0381 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.4" +__version__ = "3.7.5" import asyncio diff --git a/core/thread.py b/core/thread.py index 1cbf2720a4..b49a7b7eda 100644 --- a/core/thread.py +++ b/core/thread.py @@ -191,7 +191,7 @@ async def send_recipient_genesis_message(): embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url) embed.title = self.bot.config["thread_creation_title"] - if creator != recipient: + if creator is None or creator == recipient: msg = await recipient.send(embed=embed) if recipient_thread_close: From d5ebae68e66a036d3a785766f9528b3fd45383cc Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Fri, 13 Nov 2020 19:07:52 +0100 Subject: [PATCH 0729/1221] Update registry.json removal of the music plugin, youtubedl source it used got striked and blocked --- plugins/registry.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/plugins/registry.json b/plugins/registry.json index ce2b76afc0..609345c395 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -8,15 +8,6 @@ "icon_url": "https://i.imgur.com/951szZ3.jpg", "thumbnail_url": "https://i.imgur.com/951szZ3.jpg" }, - "music": { - "repository": "lorenzo132/modmail-plugins", - "branch": "master", - "description": "Play your favourite music on your bot! Note: Only works on VPS follow this guide: https://gist.github.com/lorenzo132/5ef328e5dfcfaec19cb81dc7a63eaffa", - "bot_version": "2.20.1", - "title": "Music", - "icon_url": "https://i.imgur.com/R2olclk.png", - "thumbnail_url": "https://i.imgur.com/xuoQjPu.gif" - }, "dragory-migrate": { "repository": "kyb3r/modmail-plugins", "branch": "master", From c3052f66e62c08ce73b98720348324deda7a1bd2 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 14:22:59 +0800 Subject: [PATCH 0730/1221] Fix mention, spam in autoupdate --- CHANGELOG.md | 7 +++++++ bot.py | 11 +++++++++-- cogs/utility.py | 11 ++++++++++- core/thread.py | 2 +- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8fe10301e..03a6468a97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed +- Autoupdate persists despite errors +- Mention when normal thread created was not working. + +# v3.7.5 + +### Fixed + - Close on emoji was not working. # v3.7.3 diff --git a/bot.py b/bot.py index ad22ad0381..dbd013ba12 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.5" +__version__ = "3.7.6" import asyncio @@ -1479,10 +1479,17 @@ async def autoupdate(self): else: command = "git pull" proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) + err = await proc.stderr.read() + err = err.decode("utf-8").rstrip() res = await proc.stdout.read() res = res.decode("utf-8").rstrip() - if res != "Already up to date.": + if err: + logger.warning(f"Autoupdate failed: {err}") + self.autoupdate_loop.cancel() + return + + elif res != "Already up to date.": logger.info("Bot has been updated.") channel = self.log_channel if self.hosting_method == HostingMethod.PM2: diff --git a/cogs/utility.py b/cogs/utility.py index 801069d125..64a1ec6fe4 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -3,6 +3,7 @@ import os import random import re +from sys import stdout import traceback from contextlib import redirect_stdout from datetime import datetime @@ -1920,10 +1921,18 @@ async def update(self, ctx, *, flag: str = ""): command = "git pull" proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) + err = await proc.stderr.read() + err = err.decode("utf-8").rstrip() res = await proc.stdout.read() res = res.decode("utf-8").rstrip() - if res != "Already up to date.": + if err: + embed = discord.Embed( + title="Update failed", description=err, color=self.bot.error_color + ) + await ctx.send(embed=embed) + + elif res != "Already up to date.": logger.info("Bot has been updated.") embed = discord.Embed(title="Bot has been updated", color=self.bot.main_color,) diff --git a/core/thread.py b/core/thread.py index b49a7b7eda..28ceef7548 100644 --- a/core/thread.py +++ b/core/thread.py @@ -155,7 +155,7 @@ async def setup(self, *, creator=None, category=None, initial_message=None): await channel.edit(topic=f"User ID: {recipient.id}") self.ready = True - if creator != recipient: + if creator is not None and creator != recipient: mention = None else: mention = self.bot.config["mention"] From dff4955a5d23350c67276bd23c8852e846f9be12 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 20:00:44 +0800 Subject: [PATCH 0731/1221] Fix error in requirements --- pyproject.toml | 2 +- requirements.min.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e618107561..e7c56eb749 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ keywords = ['discord', 'modmail'] [tool.poetry.dependencies] python = "^3.7" -"discord.py" = "./discord.py-1.5.1.tar.gz" +"discord.py" = "./discord.py-1.5.2.tar.gz" uvloop = {version = ">=0.12.0", markers = "sys_platform != 'win32'"} python-dotenv = ">=0.10.3" parsedatetime = "^2.6" diff --git a/requirements.min.txt b/requirements.min.txt index 6d434f974d..cf1b4bdd26 100644 --- a/requirements.min.txt +++ b/requirements.min.txt @@ -6,7 +6,7 @@ aiohttp==3.6.2 async-timeout==3.0.1 attrs==19.3.0 chardet==3.0.4 -./discord.py-1.5.1.tar.gz +./discord.py-1.5.2.tar.gz dnspython==1.16.0 emoji==0.5.4 future==0.18.2 From a956073f06bff7c975637297f112a04e3384a342 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 21:02:13 +0800 Subject: [PATCH 0732/1221] Fix another issue with updates --- bot.py | 2 +- cogs/utility.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index dbd013ba12..179b792be1 100644 --- a/bot.py +++ b/bot.py @@ -1484,7 +1484,7 @@ async def autoupdate(self): res = await proc.stdout.read() res = res.decode("utf-8").rstrip() - if err: + if err and not res: logger.warning(f"Autoupdate failed: {err}") self.autoupdate_loop.cancel() return diff --git a/cogs/utility.py b/cogs/utility.py index 64a1ec6fe4..573b32e9b1 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1926,7 +1926,7 @@ async def update(self, ctx, *, flag: str = ""): res = await proc.stdout.read() res = res.decode("utf-8").rstrip() - if err: + if err and not res: embed = discord.Embed( title="Update failed", description=err, color=self.bot.error_color ) From b42277b8de35bf8d48a5cb17c6169917ff52d61f Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 22:30:32 +0800 Subject: [PATCH 0733/1221] push v --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a6468a97..79a935feab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.5 +# v3.7.6 ### Fixed From 99f5175730523d849470850d8c1b3c982f16228a Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 14 Nov 2020 23:13:29 +0800 Subject: [PATCH 0734/1221] Update fork even if not heroku, skip blocked roles --- CHANGELOG.md | 10 +++++++++ bot.py | 58 ++++++++++++++++++++++++++++--------------------- cogs/utility.py | 8 +++++++ 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a935feab..527b044301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.7 + +### Added + +- Added updating github fork if GITHUB_TOKEN was provided + +### Fixed + +- Skip blocked roles check if user is not in main guild + # v3.7.6 ### Fixed diff --git a/bot.py b/bot.py index 179b792be1..820aa51e9d 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.6" +__version__ = "3.7.7" import asyncio @@ -38,6 +38,7 @@ from core.models import ( DMDisabled, HostingMethod, + InvalidConfigError, PermissionLevel, SafeFormatter, configure_logging, @@ -633,32 +634,33 @@ def check_guild_age(self, author: discord.Member) -> bool: return True def check_manual_blocked_roles(self, author: discord.Member) -> bool: - for r in author.roles: - if str(r.id) in self.blocked_roles: + if isinstance(author, discord.Member): + for r in author.roles: + if str(r.id) in self.blocked_roles: + + blocked_reason = self.blocked_roles.get(str(r.id)) or "" + now = datetime.utcnow() + + # 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, + ) - blocked_reason = self.blocked_roles.get(str(r.id)) or "" - now = datetime.utcnow() - - # 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, - ) - - 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 + 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 return True @@ -1477,6 +1479,12 @@ async def autoupdate(self): channel = self.log_channel await channel.send(embed=embed) else: + try: + # update fork if gh_token exists + await self.api.update_repository() + except InvalidConfigError: + pass + command = "git pull" proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) err = await proc.stderr.read() diff --git a/cogs/utility.py b/cogs/utility.py index 573b32e9b1..be8a0c4ba6 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1913,11 +1913,18 @@ async def update(self, ctx, *, flag: str = ""): description="No further updates required", color=self.bot.main_color, ) + embed.set_footer(text="Force update") embed.set_author( name=user["username"], icon_url=user["avatar_url"], url=user["url"] ) await ctx.send(embed=embed) else: + # update fork if gh_token exists + try: + await self.bot.api.update_repository() + except InvalidConfigError: + pass + command = "git pull" proc = await asyncio.create_subprocess_shell(command, stderr=PIPE, stdout=PIPE,) @@ -1954,6 +1961,7 @@ async def update(self, ctx, *, flag: str = ""): embed = discord.Embed( title="Already up to date", description=desc, color=self.bot.main_color, ) + embed.set_footer(text="Force update") await ctx.send(embed=embed) @commands.command(hidden=True, name="eval") From 0d09d179debca7210376b4ed9d0a03890b17212f Mon Sep 17 00:00:00 2001 From: Zoe Martin Date: Sun, 15 Nov 2020 16:15:44 +0100 Subject: [PATCH 0735/1221] Added `thread_auto_open_silently` with default 'false' to restore old functionality if desired --- CHANGELOG.md | 6 ++++++ cogs/modmail.py | 2 +- core/config.py | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 527b044301..fb0ccc854d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.8 + +### Added + +- Added `thread_auto_open_silently` to allow opening threads silently by default + # v3.7.7 ### Added diff --git a/cogs/modmail.py b/cogs/modmail.py index 2a47124ad8..beb647b87d 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -989,7 +989,7 @@ async def contact( if self.bot.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): logger.info("Contacting user %s when Modmail DM is disabled.", user) - if not silent: + if not silent and not self.bot.config.get("thread_auto_open_silently"): if ctx.author.id == user.id: description = "You have opened a Modmail thread." else: diff --git a/core/config.py b/core/config.py index 58061b8ae2..198d2bec55 100644 --- a/core/config.py +++ b/core/config.py @@ -49,6 +49,7 @@ class ConfigManager: "close_emoji": "🔒", "recipient_thread_close": False, "thread_auto_close_silently": False, + "thread_auto_open_silently": False, "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.", From 5cb9bdf63309c557fac59839efb0a8c5d72f9e20 Mon Sep 17 00:00:00 2001 From: Zoe Martin Date: Sun, 15 Nov 2020 16:35:41 +0100 Subject: [PATCH 0736/1221] Fixed broken link to CONTRIBUTING.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index baf7c84c3b..70deada435 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ Plugins requests and support is available in our [Modmail Plugins Server](https: ## Contributing -Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/kyb3r/modmail/blob/master/CONTRIBUTING.md) before you get started. +Contributions to Modmail are always welcome, whether it be improvements to the documentation or new functionality, please feel free to make the change. Check out our [contributing guidelines](https://github.com/kyb3r/modmail/blob/master/.github/CONTRIBUTING.md) before you get started. If you like this project and would like to show your appreciation, support us on **[Patreon](https://www.patreon.com/kyber)**! From 348f6ce2bd9c33efb7c73d0de23cad2d00bcb33b Mon Sep 17 00:00:00 2001 From: Zoe Martin Date: Mon, 16 Nov 2020 14:34:07 +0100 Subject: [PATCH 0737/1221] Changed `thread_auto_open_silently` to `thread_creation_silent` to be more consistent with the other names and added it to config_help.json --- CHANGELOG.md | 2 +- cogs/modmail.py | 2 +- core/config.py | 2 +- core/config_help.json | 11 +++++++++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb0ccc854d..4648482afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Added -- Added `thread_auto_open_silently` to allow opening threads silently by default +- Added `thread_creation_silent` to allow opening threads silently by default # v3.7.7 diff --git a/cogs/modmail.py b/cogs/modmail.py index beb647b87d..70b43b2046 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -989,7 +989,7 @@ async def contact( if self.bot.config["dm_disabled"] in (DMDisabled.NEW_THREADS, DMDisabled.ALL_THREADS): logger.info("Contacting user %s when Modmail DM is disabled.", user) - if not silent and not self.bot.config.get("thread_auto_open_silently"): + if not silent and not self.bot.config.get("thread_creation_silent"): if ctx.author.id == user.id: description = "You have opened a Modmail thread." else: diff --git a/core/config.py b/core/config.py index 198d2bec55..f0b18196a6 100644 --- a/core/config.py +++ b/core/config.py @@ -49,11 +49,11 @@ class ConfigManager: "close_emoji": "🔒", "recipient_thread_close": False, "thread_auto_close_silently": False, - "thread_auto_open_silently": False, "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", + "thread_creation_silent": False, "thread_self_closable_creation_footer": "Click the lock to close the thread", "thread_creation_title": "Thread Created", "thread_close_footer": "Replying will create a new thread", diff --git a/core/config_help.json b/core/config_help.json index f935e8657c..da51149791 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -303,6 +303,17 @@ "See also: `thread_creation_title`, `thread_creation_response`, `thread_self_closable_creation_footer`, `thread_close_footer`." ] }, + "thread_creation_silent": { + "default": "No", + "description": "Setting this configuration will open a new thread silently by default.", + "examples": [ + "`{prefix}config set thread_creation_silent yes`", + "`{prefix}config set thread_creation_silent no`" + ], + "notes": [ + "Works like `{prefix}contact silent` for every new thread." + ] + }, "thread_self_closable_creation_footer": { "default": "\"Click the lock to close the thread\"", "description": "This is the message embed footer sent to the recipient upon the creation of a new thread.", From c534164169db1aeaa49a265844db90fb42f096e9 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Mon, 16 Nov 2020 20:13:41 +0100 Subject: [PATCH 0738/1221] Update README.md update latest version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index baf7c84c3b..c41f691c36 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      From 2a0e93e5d3fea4ce9f8905fe65f9b9ed8bbdf075 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 18 Nov 2020 22:39:09 +0800 Subject: [PATCH 0739/1221] bugfixing and optimisations --- CHANGELOG.md | 10 ++++++++++ bot.py | 8 +++++++- cogs/utility.py | 8 +++++--- core/checks.py | 4 ++-- core/config.py | 45 +++++++++++++++++++++++++++++++++++++-------- core/utils.py | 4 ++-- 6 files changed, 63 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 527b044301..360c8dbddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.8 + +### Fixed +- Permission levels were not respected +- `perms remove` was not working +- `logs` and `block` would not recognise users in a seperate server setup. + +### Internal +- Optimised `perms get`, bot should respond faster now + # v3.7.7 ### Added diff --git a/bot.py b/bot.py index 820aa51e9d..bec8dbda5f 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.7" +__version__ = "3.7.8" import asyncio @@ -1002,6 +1002,11 @@ async def update_perms( else: if value in permissions[name]: permissions[name].remove(value) + + if isinstance(name, PermissionLevel): + self.config["level_permissions"] = permissions + else: + self.config["command_permissions"] = permissions logger.info("Updating permissions for %s, %s (add=%s).", name, value, add) await self.config.update() @@ -1355,6 +1360,7 @@ async def on_error(self, event_method, *args, **kwargs): async def on_command_error(self, context, exception): if isinstance(exception, commands.BadUnionArgument): + logger.error("Expected exception:", exc_info=exception) msg = "Could not find the specified " + human_join( [c.__name__ for c in exception.converters] ) diff --git a/cogs/utility.py b/cogs/utility.py index be8a0c4ba6..4b6d72ee8b 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1496,23 +1496,25 @@ async def permissions_get( """ if name is None and user_or_role not in {"command", "level", "override"}: - value = self._verify_user_or_role(user_or_role) + value = str(self._verify_user_or_role(user_or_role)) cmds = [] levels = [] done = set() + command_permissions = self.bot.config["command_permissions"] + level_permissions = self.bot.config["level_permissions"] for command in self.bot.walk_commands(): if command not in done: done.add(command) - permissions = self.bot.config["command_permissions"].get( + permissions = command_permissions.get( command.qualified_name, [] ) if value in permissions: cmds.append(command.qualified_name) for level in PermissionLevel: - permissions = self.bot.config["level_permissions"].get(level.name, []) + permissions = level_permissions.get(level.name, []) if value in permissions: levels.append(level.name) diff --git a/core/checks.py b/core/checks.py index 0b7b2acfa3..22de08f2ce 100644 --- a/core/checks.py +++ b/core/checks.py @@ -62,7 +62,7 @@ async def check_permissions(ctx, command_name) -> bool: if command_name in command_permissions: # -1 is for @everyone return -1 in command_permissions[command_name] or any( - check.id in command_permissions[command_name] for check in checkables + str(check.id) in command_permissions[command_name] for check in checkables ) level_permissions = ctx.bot.config["level_permissions"] @@ -71,7 +71,7 @@ async def check_permissions(ctx, command_name) -> bool: if level >= permission_level and level.name in level_permissions: # -1 is for @everyone if -1 in level_permissions[level.name] or any( - check.id in level_permissions[level.name] for check in checkables + str(check.id) in level_permissions[level.name] for check in checkables ): return True return False diff --git a/core/config.py b/core/config.py index 58061b8ae2..cd1047b68e 100644 --- a/core/config.py +++ b/core/config.py @@ -14,7 +14,7 @@ from core._color_data import ALL_COLORS from core.models import DMDisabled, InvalidConfigError, Default, getLogger from core.time import UserFriendlyTimeSync -from core.utils import strtobool +from core.utils import strtobool, tryint logger = getLogger(__name__) load_dotenv() @@ -175,6 +175,11 @@ class ConfigManager: "activity_type": discord.ActivityType, } + force_str = { + "command_permissions", + "level_permissions" + } + defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) @@ -245,18 +250,19 @@ def __setitem__(self, key: str, item: typing.Any) -> None: self._cache[key] = item def __getitem__(self, key: str) -> typing.Any: - key = key.lower() - if key not in self.all_keys: - raise InvalidConfigError(f'Configuration "{key}" is invalid.') - if key not in self._cache: - self._cache[key] = deepcopy(self.defaults[key]) - return self._cache[key] + # make use of the custom methods in func:get: + return self.get(key) def __delitem__(self, key: str) -> None: return self.remove(key) def get(self, key: str, convert=True) -> typing.Any: - value = self.__getitem__(key) + key = key.lower() + if key not in self.all_keys: + raise InvalidConfigError(f'Configuration "{key}" is invalid.') + if key not in self._cache: + self._cache[key] = deepcopy(self.defaults[key]) + value = self._cache[key] if not convert: return value @@ -295,6 +301,29 @@ def get(self, key: str, convert=True) -> typing.Any: logger.warning("Invalid %s %s.", key, value) value = self.remove(key) + elif key in self.force_str: + # Temporary: as we saved in int previously, leading to int32 overflow, + # this is transitioning IDs to strings + new_value = {} + changed = False + for k, v in value.items(): + new_v = v + if isinstance(v, list): + new_v = [] + for n in v: + print('x', n, v) + if n != -1 and not isinstance(n, str): + changed = True + n = str(n) + new_v.append(n) + new_value[k] = new_v + + if changed: + # transition the database as well + self.set(key, new_value) + + value = new_value + return value def set(self, key: str, item: typing.Any, convert=True) -> None: diff --git a/core/utils.py b/core/utils.py index b6640eaed8..6803b97d1e 100644 --- a/core/utils.py +++ b/core/utils.py @@ -56,11 +56,11 @@ class User(commands.IDConverter): # noinspection PyCallByClass,PyTypeChecker async def convert(self, ctx, argument): try: - return await commands.MemberConverter.convert(self, ctx, argument) + return await commands.MemberConverter().convert(ctx, argument) except commands.BadArgument: pass try: - return await commands.UserConverter.convert(self, ctx, argument) + return await commands.UserConverter().convert(ctx, argument) except commands.BadArgument: pass match = self._get_id_match(argument) From d160b2b73f1bcd604ee74fe9d9051dde371be1e1 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Wed, 18 Nov 2020 22:52:49 +0800 Subject: [PATCH 0740/1221] linting --- bot.py | 2 +- cogs/utility.py | 4 +--- core/config.py | 9 +++------ 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/bot.py b/bot.py index bec8dbda5f..f93343854d 100644 --- a/bot.py +++ b/bot.py @@ -1002,7 +1002,7 @@ async def update_perms( else: if value in permissions[name]: permissions[name].remove(value) - + if isinstance(name, PermissionLevel): self.config["level_permissions"] = permissions else: diff --git a/cogs/utility.py b/cogs/utility.py index 4b6d72ee8b..aa916274f1 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1507,9 +1507,7 @@ async def permissions_get( for command in self.bot.walk_commands(): if command not in done: done.add(command) - permissions = command_permissions.get( - command.qualified_name, [] - ) + permissions = command_permissions.get(command.qualified_name, []) if value in permissions: cmds.append(command.qualified_name) diff --git a/core/config.py b/core/config.py index cd1047b68e..304b9fa837 100644 --- a/core/config.py +++ b/core/config.py @@ -175,11 +175,8 @@ class ConfigManager: "activity_type": discord.ActivityType, } - force_str = { - "command_permissions", - "level_permissions" - } - + force_str = {"command_permissions", "level_permissions"} + defaults = {**public_keys, **private_keys, **protected_keys} all_keys = set(defaults.keys()) @@ -311,7 +308,7 @@ def get(self, key: str, convert=True) -> typing.Any: if isinstance(v, list): new_v = [] for n in v: - print('x', n, v) + print("x", n, v) if n != -1 and not isinstance(n, str): changed = True n = str(n) From d52c2d4c76216703907744157a5c0da481082f16 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 19 Nov 2020 22:42:20 +0800 Subject: [PATCH 0741/1221] Typos --- core/config_help.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/config_help.json b/core/config_help.json index f935e8657c..187ed606c8 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -644,7 +644,7 @@ "`{prefix}config set confirm_thread_creation yes`" ], "notes": [ - "See also: `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" + "See also: `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny``" ] }, "confirm_thread_creation_title": { @@ -654,7 +654,7 @@ "`{prefix}config set confirm_thread_creation_title Are you sure you want to create a new thread?`" ], "notes": [ - "See also: `confirm_thread_creation`, `confirm_thread_response`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" + "See also: `confirm_thread_creation`, `confirm_thread_response`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny`" ] }, "confirm_thread_response": { @@ -664,7 +664,7 @@ "`{prefix}config set confirm_thread_response React to confirm`" ], "notes": [ - "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_accept`, confirm_thread_creation_deny`" + "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_creation_accept`, `confirm_thread_creation_deny`" ] }, "confirm_thread_creation_accept": { @@ -675,7 +675,7 @@ ], "notes": [ "This has no effect unless `confirm_thread_creation` is set", - "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, confirm_thread_creation_deny`" + "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_deny`" ] }, "confirm_thread_creation_deny": { @@ -686,7 +686,7 @@ ], "notes": [ "This has no effect unless `confirm_thread_creation` is set", - "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, confirm_thread_creation_accept`" + "See also: `confirm_thread_creation`, `confirm_thread_creation_title`, `confirm_thread_response`, `confirm_thread_creation_accept`" ] }, "use_regex_autotrigger": { From 92f5c8ec7af694094688973a38ea376ba90e72b0 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 19 Nov 2020 22:53:16 +0800 Subject: [PATCH 0742/1221] Fix various bugs with threads glitching --- bot.py | 2 +- core/thread.py | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/bot.py b/bot.py index f93343854d..bfdd837bce 100644 --- a/bot.py +++ b/bot.py @@ -1164,7 +1164,7 @@ async def handle_reaction_events(self, payload): logger.warning("Failed to find linked message for reactions: %s", e) return - if self.config["transfer_reactions"]: + if self.config["transfer_reactions"] and linked_message is not None: if payload.event_type == "REACTION_ADD": if await self.add_reaction(linked_message, reaction): await self.add_reaction(message, reaction) diff --git a/core/thread.py b/core/thread.py index 28ceef7548..ba6443c35a 100644 --- a/core/thread.py +++ b/core/thread.py @@ -663,22 +663,23 @@ async def find_linked_message_from_dm(self, message, either_direction=False): else: compare_url = 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 + if self.channel is not 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] - if not msg_id.isdigit(): - continue - msg_id = int(msg_id) - if int(msg_id) == message.id: - return linked_message - raise ValueError("Thread channel message not found.") + msg_id = url.split("#")[-1] + if not msg_id.isdigit(): + continue + msg_id = int(msg_id) + if int(msg_id) == message.id: + return linked_message + raise ValueError("Thread channel message not found.") async def edit_dm_message(self, message: discord.Message, content: str) -> None: try: From fae05a5cc4e2120e2d4829deaae3131fa914baba Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 19 Nov 2020 23:01:06 +0800 Subject: [PATCH 0743/1221] Fix custom emojis not working in confirm thread creation --- CHANGELOG.md | 1 + core/thread.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d2d599fd3..16021fcf93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Permission levels were not respected - `perms remove` was not working - `logs` and `block` would not recognise users in a seperate server setup. +- Custom emojis were not working with `confirm_thread_creation` ### Internal - Optimised `perms get`, bot should respond faster now diff --git a/core/thread.py b/core/thread.py index ba6443c35a..c4a3217da4 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1182,7 +1182,7 @@ async def create( check=lambda r, u: u.id == message.author.id and r.message.id == confirm.id and r.message.channel.id == confirm.channel.id - and r.emoji in (accept_emoji, deny_emoji), + and str(r.emoji) in (accept_emoji, deny_emoji), timeout=20, ) except asyncio.TimeoutError: From efc9feec1661fcfaebe47da3c1ad84361571e1f9 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 19 Nov 2020 23:02:09 +0800 Subject: [PATCH 0744/1221] Push v --- CHANGELOG.md | 2 +- README.md | 2 +- bot.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16021fcf93..e6078ffac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.8 +# v3.7.8-dev1 ### Added diff --git a/README.md b/README.md index baf7c84c3b..c41f691c36 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index bfdd837bce..4c41b88b6e 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.8" +__version__ = "3.7.8-dev1" import asyncio From 4acbbe4a90efd9fe85c09c9e85f0776d3de20250 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 19 Nov 2020 23:06:01 +0800 Subject: [PATCH 0745/1221] Fix discord server link in about, resolve #2890 --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index aa916274f1..9214553cfd 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -345,7 +345,7 @@ async def about(self, ctx): embed.add_field( name="Want Modmail in Your Server?", value="Follow the installation guide on [GitHub](https://github.com/kyb3r/modmail/) " - "and join our [Discord server](https://discord.gg/F34cRU8/)!", + "and join our [Discord server](https://discord.gg/F34cRU8)!", inline=False, ) From 23534f6d90d5d08fc7d9e6fe03e88323510d4a80 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 19 Nov 2020 23:07:00 +0800 Subject: [PATCH 0746/1221] 3.7.8! --- CHANGELOG.md | 2 +- README.md | 2 +- bot.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6078ffac3..16021fcf93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.8-dev1 +# v3.7.8 ### Added diff --git a/README.md b/README.md index 978f83aa07..03b41ab0da 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 4c41b88b6e..bfdd837bce 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.8-dev1" +__version__ = "3.7.8" import asyncio From 802e74040797c9c64b285d50c4267625b4a5b82e Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 19 Nov 2020 23:20:07 +0800 Subject: [PATCH 0747/1221] Cleanup changelog --- CHANGELOG.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16021fcf93..9e4c4c5fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,16 +10,16 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Added -- Added `thread_contact_silently` to allow opening threads silently by default +- Added `thread_contact_silently` to allow opening threads silently by default. ([PR #2887](https://github.com/kyb3r/modmail/pull/2887)) ### Fixed -- Permission levels were not respected -- `perms remove` was not working +- Permission levels were not respected. +- `perms remove` was not working. - `logs` and `block` would not recognise users in a seperate server setup. -- Custom emojis were not working with `confirm_thread_creation` +- Custom emojis were not working with `confirm_thread_creation`. ### Internal -- Optimised `perms get`, bot should respond faster now +- Optimised `perms get`, bot should respond faster now. # v3.7.7 @@ -29,14 +29,14 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed -- Skip blocked roles check if user is not in main guild +- Skip blocked roles check if user is not in main guild. # v3.7.6 ### Fixed -- Autoupdate persists despite errors -- Mention when normal thread created was not working. +- Autoupdate persists despite errors. +- Mention when normal thread created was not working. ([GH #2883](https://github.com/kyb3r/modmail/issues/2883)) # v3.7.5 @@ -60,7 +60,6 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?config set` would not respond if an invalid key was provided. - # v3.7.1 ### Fixed From 03899d28a2c9fa84c8f52cd00a9927a0edac1f15 Mon Sep 17 00:00:00 2001 From: Zoe Martin Date: Fri, 20 Nov 2020 00:30:47 +0100 Subject: [PATCH 0748/1221] Fixes kyb3r/modmail#2892 --- CHANGELOG.md | 6 ++++++ bot.py | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e4c4c5fef..01bdac54db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.9 + +### Fixed + +- `perms add/remove` should now work again + # v3.7.8 ### Added diff --git a/bot.py b/bot.py index bfdd837bce..2cd261218a 100644 --- a/bot.py +++ b/bot.py @@ -988,9 +988,11 @@ async def update_perms( ) -> None: value = str(value) if isinstance(name, PermissionLevel): + level = True permissions = self.config["level_permissions"] name = name.name else: + level = False permissions = self.config["command_permissions"] if name not in permissions: if add: @@ -1003,7 +1005,7 @@ async def update_perms( if value in permissions[name]: permissions[name].remove(value) - if isinstance(name, PermissionLevel): + if level: self.config["level_permissions"] = permissions else: self.config["command_permissions"] = permissions From 794384029c28bbe87e80d9ca821dd1b592c58e2e Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 20 Nov 2020 14:51:07 +0800 Subject: [PATCH 0749/1221] Add pr attribution --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01bdac54db..78b25fa76f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed -- `perms add/remove` should now work again +- `perms add/remove` should now work again. ([PR #2893](https://github.com/kyb3r/modmail/pull/2893)) # v3.7.8 From ded1bb6b9373404d4363fb2e980b8bedba5b3555 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 20 Nov 2020 15:31:35 +0800 Subject: [PATCH 0750/1221] Fix more bugs in permlevels --- CHANGELOG.md | 2 +- README.md | 2 +- bot.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78b25fa76f..71463960a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed -- `perms add/remove` should now work again. ([PR #2893](https://github.com/kyb3r/modmail/pull/2893)) +- `perms add/remove` with permission levels should now work again. ([PR #2893](https://github.com/kyb3r/modmail/pull/2893)) # v3.7.8 diff --git a/README.md b/README.md index 03b41ab0da..114fc5aa41 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 2cd261218a..b72e183fd3 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.8" +__version__ = "3.7.9" import asyncio @@ -986,7 +986,8 @@ async def get_context(self, message, *, cls=commands.Context): async def update_perms( self, name: typing.Union[PermissionLevel, str], value: int, add: bool = True ) -> None: - value = str(value) + if value != -1: + value = str(value) if isinstance(name, PermissionLevel): level = True permissions = self.config["level_permissions"] From 306df8a2d15cc9c6d5014df1f5207b59d4d0f661 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 20 Nov 2020 15:44:11 +0800 Subject: [PATCH 0751/1221] Clearer plugin debug messages when plugins are disabled --- CHANGELOG.md | 4 ++++ cogs/plugins.py | 8 ++++++++ core/config.py | 1 - 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71463960a4..ead227ffd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `perms add/remove` with permission levels should now work again. ([PR #2893](https://github.com/kyb3r/modmail/pull/2893)) +### Improved + +- Clearer plugin debug messages when plugins are disabled + # v3.7.8 ### Added diff --git a/cogs/plugins.py b/cogs/plugins.py index cdea3a4ce8..ce21551831 100644 --- a/cogs/plugins.py +++ b/cogs/plugins.py @@ -248,6 +248,14 @@ async def load_plugin(self, plugin): async def parse_user_input(self, ctx, plugin_name, check_version=False): + if not self.bot.config["enable_plugins"]: + embed = discord.Embed( + description="Plugins are disabled, enable them by setting `ENABLE_PLUGINS=true`", + color=self.bot.main_color, + ) + await ctx.send(embed=em) + return + if not self._ready_event.is_set(): embed = discord.Embed( description="Plugins are still loading, please try again later.", diff --git a/core/config.py b/core/config.py index 1fa2d9070c..38a022b769 100644 --- a/core/config.py +++ b/core/config.py @@ -310,7 +310,6 @@ def get(self, key: str, convert=True) -> typing.Any: if isinstance(v, list): new_v = [] for n in v: - print("x", n, v) if n != -1 and not isinstance(n, str): changed = True n = str(n) From 34befb593824aaeee2979fc7d30f837509ccfc9a Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 20 Nov 2020 21:14:13 +0800 Subject: [PATCH 0752/1221] update channel id and show timestamp config, resolves #2885 --- CHANGELOG.md | 9 ++++++++- README.md | 2 +- bot.py | 22 +++++++++++++++++++--- cogs/modmail.py | 7 +++---- core/config.py | 3 +++ core/config_help.json | 18 ++++++++++++++++++ core/thread.py | 10 ++++++---- 7 files changed, 58 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ead227ffd0..a7a13c67f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.10 + +### Added + +- Added `update_channel_id` to specify which channel autoupdate notifications were being sent to. +- Added `show_timestamp` to specify if timestamps should be displayed in message embeds. ([GH #2885](https://github.com/kyb3r/modmail/issues/2885)) + # v3.7.9 ### Fixed -- `perms add/remove` with permission levels should now work again. ([PR #2893](https://github.com/kyb3r/modmail/pull/2893)) +- `perms add/remove` with permission levels should now work again. ([GH #2892](https://github.com/kyb3r/modmail/issues/2892), [PR #2893](https://github.com/kyb3r/modmail/pull/2893)) ### Improved diff --git a/README.md b/README.md index 114fc5aa41..755d9e5ffa 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index b72e183fd3..151d4bbe60 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.9" +__version__ = "3.7.10" import asyncio @@ -270,6 +270,21 @@ def mention_channel(self): return self.log_channel + @property + def update_channel(self): + channel_id = self.config["update_channel_id"] + if channel_id is not None: + try: + channel = self.get_channel(int(channel_id)) + if channel is not None: + return channel + except ValueError: + pass + logger.debug("UPDATE_CHANNEL_ID was invalid, removed.") + self.config.remove("update_channel_id") + + return self.log_channel + async def wait_for_connected(self) -> None: await self.wait_until_ready() await self._connected.wait() @@ -1027,8 +1042,9 @@ async def on_message(self, message): title="Bot mention", description=f"[Jump URL]({message.jump_url})\n{truncate(message.content, 50)}", color=self.main_color, - timestamp=datetime.utcnow(), ) + if self.config["show_timestamp"]: + em.timestamp = datetime.utcnow() await self.mention_channel.send(content=self.config["mention"], embed=em) await self.process_commands(message) @@ -1508,7 +1524,7 @@ async def autoupdate(self): elif res != "Already up to date.": logger.info("Bot has been updated.") - channel = self.log_channel + channel = self.update_channel if self.hosting_method == HostingMethod.PM2: embed = discord.Embed(title="Bot has been updated", color=self.main_color) await channel.send(embed=embed) diff --git a/cogs/modmail.py b/cogs/modmail.py index c6491076c2..fe8cbce972 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -996,11 +996,10 @@ async def contact( description = f"{ctx.author.name} has opened a Modmail thread." em = discord.Embed( - title="New Thread", - description=description, - color=self.bot.main_color, - timestamp=datetime.utcnow(), + title="New Thread", description=description, color=self.bot.main_color, ) + if self.bot.config["show_timestamp"]: + em.timestamp = datetime.utcnow() em.set_footer(icon_url=ctx.author.avatar_url) await user.send(embed=em) diff --git a/core/config.py b/core/config.py index 38a022b769..888275c1fb 100644 --- a/core/config.py +++ b/core/config.py @@ -43,6 +43,7 @@ class ConfigManager: # logging "log_channel_id": None, "mention_channel_id": None, + "update_channel_id": None, # threads "sent_emoji": "✅", "blocked_emoji": "🚫", @@ -76,6 +77,7 @@ class ConfigManager: "close_on_leave": False, "close_on_leave_reason": "The recipient has left the server.", "alert_on_mention": False, + "show_timestamp": True, # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), @@ -162,6 +164,7 @@ class ConfigManager: "transfer_reactions", "close_on_leave", "alert_on_mention", + "show_timestamp", "confirm_thread_creation", "use_regex_autotrigger", "enable_plugins", diff --git a/core/config_help.json b/core/config_help.json index f5ccc0d17d..7c36f75cbc 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -185,6 +185,17 @@ "See also: `log_channel_id`" ] }, + "update_channel_id": { + "default": "Log Channel (normally `#bot-logs`)", + "description": "This is the channel where update notifications are sent to.", + "examples": [ + "`{prefix}config set update_channel_id 9234932582312` (9234932582312 is the channel ID)" + ], + "notes": [ + "This has no effect unless `disable_autoupdates` is set to no.", + "See also: `log_channel_id`" + ] + }, "sent_emoji": { "default": "✅", "description": "This is the emoji added to the message when when a Modmail action is invoked successfully (ie. DM Modmail, edit message, etc.).", @@ -648,6 +659,13 @@ "See also: `mention`" ] }, + "show_timestamp": { + "default": "Yes", + "description": "Shows timestamps on thread embeds", + "examples":[ + "`{prefix}config set show_timestamp no`" + ] + }, "confirm_thread_creation": { "default": "No", "description": "Ensure users confirm that they want to create a new thread", diff --git a/core/thread.py b/core/thread.py index c4a3217da4..5e1c28f15f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -460,10 +460,10 @@ async def _close( # Thread closed message embed = discord.Embed( - title=self.bot.config["thread_close_title"], - color=self.bot.error_color, - timestamp=datetime.utcnow(), + title=self.bot.config["thread_close_title"], color=self.bot.error_color, ) + if self.bot.config["show_timestamp"]: + embed.timestamp = datetime.utcnow() if not message: if self.id == closer.id: @@ -823,7 +823,9 @@ async def send( author = message.author - embed = discord.Embed(description=message.content, timestamp=message.created_at) + embed = discord.Embed(description=message.content) + if self.bot.config["show_timestamp"]: + embed.timestamp = message.created_at system_avatar_url = "https://discordapp.com/assets/f78426a064bc9dd24847519259bc42af.png" From eefb6c7c4a3d8212a807005373d4ea28f2c83b2d Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 20 Nov 2020 21:54:57 +0800 Subject: [PATCH 0753/1221] Improve role block, fix bot restart pm2 --- CHANGELOG.md | 14 ++++++++++++++ README.md | 2 +- bot.py | 22 +++++++++++++--------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7a13c67f2..03ec6214a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.11 + +### Improved + +- Role block will now work better with seperate server setups. + +### Fixed + +- Bot not restarting after autoupdate on PM2. + +### Internal + +- Removed unnecessary loggings. + # v3.7.10 ### Added diff --git a/README.md b/README.md index 755d9e5ffa..ac1fb6966c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 151d4bbe60..51a238a109 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.10" +__version__ = "3.7.11" import asyncio @@ -559,7 +559,7 @@ async def on_ready(self): ] if any(other_guilds): logger.warning( - "The bot is in more servers other than the main and staff server." + "The bot is in more servers other than the main and staff server. " "This may cause data compromise (%s).", ", ".join(guild.name for guild in other_guilds), ) @@ -729,8 +729,16 @@ async def is_blocked( member = self.guild.get_member(author.id) if member is None: - logger.debug("User not in guild, %s.", author.id) - else: + # try to find in other guilds + for g in self.guilds: + member = g.get_member(author.id) + if member: + break + + if member is None: + logger.debug("User not in guild, %s.", author.id) + + if member is not None: author = member if str(author.id) in self.blocked_whitelisted_users: @@ -1379,10 +1387,6 @@ async def on_error(self, event_method, *args, **kwargs): async def on_command_error(self, context, exception): if isinstance(exception, commands.BadUnionArgument): - logger.error("Expected exception:", exc_info=exception) - msg = "Could not find the specified " + human_join( - [c.__name__ for c in exception.converters] - ) await context.trigger_typing() await context.send(embed=discord.Embed(color=self.error_color, description=msg)) @@ -1535,7 +1539,7 @@ async def autoupdate(self): color=self.main_color, ) await channel.send(embed=embed) - await self.logout() + await self.logout() async def before_autoupdate(self): await self.wait_for_connected() From 6bec4d34c117c00cc81cd04c66713acf95f01e00 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Fri, 20 Nov 2020 21:57:05 +0800 Subject: [PATCH 0754/1221] lint --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 51a238a109..40b040c684 100644 --- a/bot.py +++ b/bot.py @@ -737,7 +737,7 @@ async def is_blocked( if member is None: logger.debug("User not in guild, %s.", author.id) - + if member is not None: author = member From 358de8a529821f522788cb7bcf59190a2040e237 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 21 Nov 2020 15:22:50 +0800 Subject: [PATCH 0755/1221] Fix bug in block, thread reason not working --- CHANGELOG.md | 7 +++++++ README.md | 2 +- bot.py | 8 ++------ cogs/modmail.py | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ec6214a0..21e9b7dde6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.12 + +### Fixed + +- Bot was not responding to union errors. +- `?block [reason]` now works in threads. + # v3.7.11 ### Improved diff --git a/README.md b/README.md index ac1fb6966c..04b343c85f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index 40b040c684..e520d9302f 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.11" +__version__ = "3.7.12" import asyncio @@ -1386,11 +1386,7 @@ async def on_error(self, event_method, *args, **kwargs): logger.error("Unexpected exception:", exc_info=sys.exc_info()) async def on_command_error(self, context, exception): - if isinstance(exception, commands.BadUnionArgument): - await context.trigger_typing() - await context.send(embed=discord.Embed(color=self.error_color, description=msg)) - - elif isinstance(exception, commands.BadArgument): + if isinstance(exception, commands.BadArgument): await context.trigger_typing() await context.send( embed=discord.Embed(color=self.error_color, description=str(exception)) diff --git a/cogs/modmail.py b/cogs/modmail.py index fe8cbce972..e71214d335 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1147,7 +1147,7 @@ async def blocked_whitelist(self, ctx, *, user: User = None): async def block( self, ctx, - user_or_role: Union[User, discord.Role] = None, + user_or_role: Optional[Union[User, discord.Role]] = None, *, after: UserFriendlyTime = None, ): From c4b90cbbc1f7908c86c68354e49db595bb08f40e Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Mon, 23 Nov 2020 19:46:01 +0800 Subject: [PATCH 0756/1221] Notes in config help are optional --- CHANGELOG.md | 6 ++++++ README.md | 2 +- bot.py | 2 +- cogs/utility.py | 2 +- core/config_help.json | 3 ++- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21e9b7dde6..ab7f5adc6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.13 + +### Fixed + +- Notes in config help are now optional. + # v3.7.12 ### Fixed diff --git a/README.md b/README.md index 04b343c85f..8e40426dd1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
      - +
      diff --git a/bot.py b/bot.py index e520d9302f..f670e7e76d 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.12" +__version__ = "3.7.13" import asyncio diff --git a/cogs/utility.py b/cogs/utility.py index 9214553cfd..03a90efe54 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -892,7 +892,7 @@ def fmt(val): embed.add_field(name="Example(s):", value=example_text, inline=False) note_text = "" - for note in info["notes"]: + for note in info.get("notes", []): note_text += f"- {fmt(note)}\n" if note_text: embed.add_field(name="Note(s):", value=note_text, inline=False) diff --git a/core/config_help.json b/core/config_help.json index 7c36f75cbc..549e3d9486 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -664,7 +664,8 @@ "description": "Shows timestamps on thread embeds", "examples":[ "`{prefix}config set show_timestamp no`" - ] + ], + "notes": [] }, "confirm_thread_creation": { "default": "No", From fc086bf731427846486cfa978c2178c68aba878e Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Tue, 24 Nov 2020 15:14:16 +0100 Subject: [PATCH 0757/1221] Update utility.py added new competing bot-presence to the help list --- cogs/utility.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/utility.py b/cogs/utility.py index 03a90efe54..ee61509dc8 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -506,6 +506,7 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): - `streaming` - `listening` - `watching` + - `competing` When activity type is set to `listening`, it must be followed by a "to": "listening to..." From 63461bbb276fe4f160d6da545fec7fd62d7338d5 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 26 Nov 2020 18:22:58 +0800 Subject: [PATCH 0758/1221] add update_notifications, resolves #2896 --- CHANGELOG.md | 10 ++++++++++ bot.py | 13 +++++++++---- core/config.py | 3 +++ core/config_help.json | 13 ++++++++++++- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7f5adc6f..73767c86bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. +# v3.7.14-dev0 + +### Fixed + +- Mentioned `competing` as an activity type ([PR #2902](https://github.com/kyb3r/modmail/pull/2902)) + +### Added + +- `update_notifications` configuration option to toggle bot autoupdate notifications ([GH #2896](https://github.com/kyb3r/modmail/issues/2896)) + # v3.7.13 ### Fixed diff --git a/bot.py b/bot.py index f670e7e76d..e46d50f73b 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.13" +__version__ = "3.7.14-dev0" import asyncio @@ -1502,7 +1502,8 @@ async def autoupdate(self): ) logger.info("Bot has been updated.") channel = self.log_channel - await channel.send(embed=embed) + if self.bot.config["update_notifications"]: + await channel.send(embed=embed) else: try: # update fork if gh_token exists @@ -1527,14 +1528,18 @@ async def autoupdate(self): channel = self.update_channel if self.hosting_method == HostingMethod.PM2: embed = discord.Embed(title="Bot has been updated", color=self.main_color) - await channel.send(embed=embed) + embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + if self.bot.config["update_notifications"]: + await channel.send(embed=embed) else: embed = discord.Embed( title="Bot has been updated and is logging out.", description="If you do not have an auto-restart setup, please manually start the bot.", color=self.main_color, ) - await channel.send(embed=embed) + embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + if self.bot.config["update_notifications"]: + await channel.send(embed=embed) await self.logout() async def before_autoupdate(self): diff --git a/core/config.py b/core/config.py index 888275c1fb..8317ed7df6 100644 --- a/core/config.py +++ b/core/config.py @@ -44,6 +44,8 @@ class ConfigManager: "log_channel_id": None, "mention_channel_id": None, "update_channel_id": None, + # updates + "update_notifications": True, # threads "sent_emoji": "✅", "blocked_emoji": "🚫", @@ -171,6 +173,7 @@ class ConfigManager: "data_collection", "enable_eval", "disable_autoupdates", + "update_notifications", "thread_contact_silently", } diff --git a/core/config_help.json b/core/config_help.json index 549e3d9486..815d2a6b17 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -192,10 +192,21 @@ "`{prefix}config set update_channel_id 9234932582312` (9234932582312 is the channel ID)" ], "notes": [ - "This has no effect unless `disable_autoupdates` is set to no.", + "This has no effect unless `disable_autoupdates` is set to no and `update_notifications` is set to yes.", "See also: `log_channel_id`" ] }, + "update_notifications": { + "default": "Yes", + "description": "This is the channel where update notifications are sent to.", + "examples": [ + "`{prefix}config set update_notifications no" + ], + "notes": [ + "This has no effect unless `disable_autoupdates` is set to no.", + "See also: `update_channel_id`" + ] + }, "sent_emoji": { "default": "✅", "description": "This is the emoji added to the message when when a Modmail action is invoked successfully (ie. DM Modmail, edit message, etc.).", From a97119d160942f67b90198a610a8c1f908eb8a5c Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 26 Nov 2020 18:35:48 +0800 Subject: [PATCH 0759/1221] Added command validation to autotrigger --- CHANGELOG.md | 12 ++++++---- bot.py | 9 +++++--- cogs/utility.py | 60 +++++++++++++++++++++++++++++++++++++------------ 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73767c86bb..ae277a9b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,17 @@ however, insignificant breaking changes do not guarantee a major version bump, s # v3.7.14-dev0 -### Fixed +### Added -- Mentioned `competing` as an activity type ([PR #2902](https://github.com/kyb3r/modmail/pull/2902)) +- `update_notifications` configuration option to toggle bot autoupdate notifications. ([GH #2896](https://github.com/kyb3r/modmail/issues/2896)) -### Added +### Improved + +- Added command validation to `autotrigger add/edit`. + +### Fixed -- `update_notifications` configuration option to toggle bot autoupdate notifications ([GH #2896](https://github.com/kyb3r/modmail/issues/2896)) +- Mentioned `competing` as an activity type. ([PR #2902](https://github.com/kyb3r/modmail/pull/2902)) # v3.7.13 diff --git a/bot.py b/bot.py index e46d50f73b..c663f2bb84 100644 --- a/bot.py +++ b/bot.py @@ -937,7 +937,6 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) invoked_prefix = self.prefix invoker = None - # Check if there is any aliases being called. if self.config.get("use_regex_autotrigger"): trigger = next( filter(lambda x: re.match(x, message.content), self.auto_triggers.keys()) @@ -1528,7 +1527,9 @@ async def autoupdate(self): channel = self.update_channel if self.hosting_method == HostingMethod.PM2: embed = discord.Embed(title="Bot has been updated", color=self.main_color) - embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + embed.set_footer( + text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" + ) if self.bot.config["update_notifications"]: await channel.send(embed=embed) else: @@ -1537,7 +1538,9 @@ async def autoupdate(self): description="If you do not have an auto-restart setup, please manually start the bot.", color=self.main_color, ) - embed.set_footer(text=f"Updating Modmail v{self.version} " f"-> v{latest.version}") + embed.set_footer( + text=f"Updating Modmail v{self.version} " f"-> v{latest.version}" + ) if self.bot.config["update_notifications"]: await channel.send(embed=embed) await self.logout() diff --git a/cogs/utility.py b/cogs/utility.py index ee61509dc8..eb17e9c212 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1727,14 +1727,30 @@ async def autotrigger_add(self, ctx, keyword, *, command): description=f"Another autotrigger with the same name already exists: `{keyword}`.", ) else: - self.bot.auto_triggers[keyword] = command - await self.bot.config.update() + # command validation + valid = False + split_cmd = command.split(" ") + for n in range(1, len(split_cmd) + 1): + if self.bot.get_command(" ".join(split_cmd[0:n])): + print(self.bot.get_command(" ".join(split_cmd[0:n]))) + valid = True + break + + if valid: + self.bot.auto_triggers[keyword] = command + await self.bot.config.update() - embed = discord.Embed( - title="Success", - color=self.bot.main_color, - description=f"Keyword `{keyword}` has been linked to `{command}`.", - ) + embed = discord.Embed( + title="Success", + color=self.bot.main_color, + description=f"Keyword `{keyword}` has been linked to `{command}`.", + ) + else: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="Invalid command. Note that autotriggers do not work with aliases.", + ) await ctx.send(embed=embed) @@ -1747,14 +1763,30 @@ async def autotrigger_edit(self, ctx, keyword, *, command): keyword, self.bot.auto_triggers.keys(), "Autotrigger" ) else: - self.bot.auto_triggers[keyword] = command - await self.bot.config.update() + # command validation + valid = False + split_cmd = command.split(" ") + for n in range(1, len(split_cmd) + 1): + if self.bot.get_command(" ".join(split_cmd[0:n])): + print(self.bot.get_command(" ".join(split_cmd[0:n]))) + valid = True + break + + if valid: + self.bot.auto_triggers[keyword] = command + await self.bot.config.update() - embed = discord.Embed( - title="Success", - color=self.bot.main_color, - description=f"Keyword `{keyword}` has been linked to `{command}`.", - ) + embed = discord.Embed( + title="Success", + color=self.bot.main_color, + description=f"Keyword `{keyword}` has been linked to `{command}`.", + ) + else: + embed = discord.Embed( + title="Error", + color=self.bot.error_color, + description="Invalid command. Note that autotriggers do not work with aliases.", + ) await ctx.send(embed=embed) From ccde7b0f5740ad945f872404773e614849d36624 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 26 Nov 2020 18:45:44 +0800 Subject: [PATCH 0760/1221] Make use of `git branch --show-current` to retrieve branch --- CHANGELOG.md | 4 ++++ core/changelog.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae277a9b61..4fe715b883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Mentioned `competing` as an activity type. ([PR #2902](https://github.com/kyb3r/modmail/pull/2902)) +### Internal + +- Make use of `git branch --show-current` to retrieve branch insteasd of using prerelease version check. + # v3.7.13 ### Fixed diff --git a/core/changelog.py b/core/changelog.py index 60d0179609..163aa0f340 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -1,4 +1,6 @@ +import asyncio import re +from subprocess import PIPE from typing import List from discord import Embed @@ -167,7 +169,17 @@ async def from_url(cls, bot, url: str = "") -> "Changelog": Changelog The newly created `Changelog` parsed from the `url`. """ - branch = "master" if not bot.version.is_prerelease else "development" + # get branch via git cli if available + proc = await asyncio.create_subprocess_shell( + "git branch --show-current", stderr=PIPE, stdout=PIPE, + ) + err = await proc.stderr.read() + err = err.decode("utf-8").rstrip() + res = await proc.stdout.read() + branch = res.decode("utf-8").rstrip() + if not branch or err: + branch = "master" if not bot.version.is_prerelease else "development" + url = url or f"https://raw.githubusercontent.com/kyb3r/modmail/{branch}/CHANGELOG.md" async with await bot.session.get(url) as resp: From f335d265c85aafd80d9d6d99721af5a5e20bddfd Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 26 Nov 2020 18:57:37 +0800 Subject: [PATCH 0761/1221] Fix bug where level permissions were not being checked if cmd perms were set --- CHANGELOG.md | 1 + core/checks.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fe715b883..a1547479c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Fixed - Mentioned `competing` as an activity type. ([PR #2902](https://github.com/kyb3r/modmail/pull/2902)) +- Level permissions were not checked if command permissions were set. ### Internal diff --git a/core/checks.py b/core/checks.py index 22de08f2ce..b7c56a1a7d 100644 --- a/core/checks.py +++ b/core/checks.py @@ -61,9 +61,10 @@ async def check_permissions(ctx, command_name) -> bool: if command_name in command_permissions: # -1 is for @everyone - return -1 in command_permissions[command_name] or any( + if -1 in command_permissions[command_name] or any( str(check.id) in command_permissions[command_name] for check in checkables - ) + ): + return True level_permissions = ctx.bot.config["level_permissions"] From 8b2bea4e533c11df3b1ac401f95a281549384ed4 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 26 Nov 2020 19:01:49 +0800 Subject: [PATCH 0762/1221] Resolve linting issues --- .bandit_baseline.json | 77 +++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/.bandit_baseline.json b/.bandit_baseline.json index 94fcfd0fc3..28a4e47b9c 100644 --- a/.bandit_baseline.json +++ b/.bandit_baseline.json @@ -1,6 +1,6 @@ { "errors": [], - "generated_at": "2020-11-12T15:17:38Z", + "generated_at": "2020-11-26T11:00:36Z", "metrics": { "./bot.py": { "CONFIDENCE.HIGH": 1.0, @@ -11,7 +11,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1264, + "loc": 1321, "nosec": 0 }, "./cogs/modmail.py": { @@ -23,7 +23,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1280, + "loc": 1273, "nosec": 0 }, "./cogs/plugins.py": { @@ -35,7 +35,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 572, + "loc": 578, "nosec": 0 }, "./cogs/utility.py": { @@ -47,7 +47,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 1.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 1710, + "loc": 1755, "nosec": 0 }, "./core/_color_data.py": { @@ -63,15 +63,15 @@ "nosec": 0 }, "./core/changelog.py": { - "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.HIGH": 1.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 0.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 0.0, + "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 145, + "loc": 155, "nosec": 0 }, "./core/checks.py": { @@ -83,7 +83,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 89, + "loc": 90, "nosec": 0 }, "./core/clients.py": { @@ -95,7 +95,7 @@ "SEVERITY.LOW": 1.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 585, + "loc": 587, "nosec": 0 }, "./core/config.py": { @@ -107,7 +107,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 327, + "loc": 352, "nosec": 0 }, "./core/decorators.py": { @@ -131,7 +131,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 199, + "loc": 202, "nosec": 0 }, "./core/paginator.py": { @@ -155,7 +155,7 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 993, + "loc": 996, "nosec": 0 }, "./core/time.py": { @@ -179,19 +179,31 @@ "SEVERITY.LOW": 0.0, "SEVERITY.MEDIUM": 0.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 283, + "loc": 282, + "nosec": 0 + }, + "./plugins/kyb3r/modmail-plugins/profanity-filter-master/profanity-filter.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 81, "nosec": 0 }, "_totals": { - "CONFIDENCE.HIGH": 4.0, + "CONFIDENCE.HIGH": 5.0, "CONFIDENCE.LOW": 0.0, "CONFIDENCE.MEDIUM": 1.0, "CONFIDENCE.UNDEFINED": 0.0, "SEVERITY.HIGH": 0.0, - "SEVERITY.LOW": 4.0, + "SEVERITY.LOW": 5.0, "SEVERITY.MEDIUM": 1.0, "SEVERITY.UNDEFINED": 0.0, - "loc": 8989, + "loc": 9214, "nosec": 0 } }, @@ -226,41 +238,56 @@ "test_name": "blacklist" }, { - "code": "12 from json import JSONDecodeError, loads\n13 from subprocess import PIPE\n14 from textwrap import indent\n", + "code": "13 from json import JSONDecodeError, loads\n14 from subprocess import PIPE\n15 from textwrap import indent\n", "filename": "./cogs/utility.py", "issue_confidence": "HIGH", "issue_severity": "LOW", "issue_text": "Consider possible security implications associated with PIPE module.", - "line_number": 13, + "line_number": 14, "line_range": [ - 13 + 14 ], "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", "test_id": "B404", "test_name": "blacklist" }, { - "code": "1985 try:\n1986 exec(to_compile, env) # pylint: disable=exec-used\n1987 except Exception as exc:\n", + "code": "2039 try:\n2040 exec(to_compile, env) # pylint: disable=exec-used\n2041 except Exception as exc:\n", "filename": "./cogs/utility.py", "issue_confidence": "HIGH", "issue_severity": "MEDIUM", "issue_text": "Use of exec detected.", - "line_number": 1986, + "line_number": 2040, "line_range": [ - 1986 + 2040 ], "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b102_exec_used.html", "test_id": "B102", "test_name": "exec_used" }, { - "code": "68 \n69 def __init__(self, bot, access_token: str = \"\", username: str = \"\", **kwargs):\n70 self.bot = bot\n71 self.session = bot.session\n72 self.headers: dict = None\n73 self.access_token = access_token\n74 self.username = username\n75 self.avatar_url: str = kwargs.pop(\"avatar_url\", \"\")\n76 self.url: str = kwargs.pop(\"url\", \"\")\n77 if self.access_token:\n78 self.headers = {\"Authorization\": \"token \" + str(access_token)}\n79 \n80 async def request(\n", + "code": "2 import re\n3 from subprocess import PIPE\n4 from typing import List\n", + "filename": "./core/changelog.py", + "issue_confidence": "HIGH", + "issue_severity": "LOW", + "issue_text": "Consider possible security implications associated with PIPE module.", + "line_number": 3, + "line_range": [ + 3 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess", + "test_id": "B404", + "test_name": "blacklist" + }, + { + "code": "67 \n68 def __init__(self, bot, access_token: str = \"\", username: str = \"\", **kwargs):\n69 self.bot = bot\n70 self.session = bot.session\n71 self.headers: dict = None\n72 self.access_token = access_token\n73 self.username = username\n74 self.avatar_url: str = kwargs.pop(\"avatar_url\", \"\")\n75 self.url: str = kwargs.pop(\"url\", \"\")\n76 if self.access_token:\n77 self.headers = {\"Authorization\": \"token \" + str(access_token)}\n78 \n79 @property\n80 def BRANCH(self):\n", "filename": "./core/clients.py", "issue_confidence": "MEDIUM", "issue_severity": "LOW", "issue_text": "Possible hardcoded password: ''", - "line_number": 69, + "line_number": 68, "line_range": [ + 68, 69, 70, 71, From 812494aede7b4d62655fb5c4d20c2ae26cfa54e1 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 26 Nov 2020 20:14:54 +0800 Subject: [PATCH 0763/1221] trim 'in' from competing activity --- CHANGELOG.md | 2 +- bot.py | 2 +- cogs/utility.py | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1547479c9..c8ffdf67c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.14-dev0 +# v3.7.14-dev1 ### Added diff --git a/bot.py b/bot.py index c663f2bb84..59b0e7341b 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.14-dev0" +__version__ = "3.7.14-dev1" import asyncio diff --git a/cogs/utility.py b/cogs/utility.py index eb17e9c212..64f9c679c9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -511,6 +511,9 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): When activity type is set to `listening`, it must be followed by a "to": "listening to..." + When activity type is set to `competing`, + it must be followed by a "in": "competing in..." + When activity type is set to `streaming`, you can set the linked twitch page: - `{prefix}config set twitch_url https://www.twitch.tv/somechannel/` @@ -545,6 +548,8 @@ async def activity(self, ctx, activity_type: str.lower, *, message: str = ""): msg = f"Activity set to: {activity.type.name.capitalize()} " if activity.type == ActivityType.listening: msg += f"to {activity.name}." + elif activity.type == ActivityType.competing: + msg += f"in {activity.name}." else: msg += f"{activity.name}." @@ -609,6 +614,11 @@ async def set_presence(self, *, status=None, activity_type=None, activity_messag # The actual message is after listening to [...] # discord automatically add the "to" activity_message = activity_message[3:].strip() + elif activity_type == ActivityType.competing: + if activity_message.lower().startswith("in "): + # The actual message is after listening to [...] + # discord automatically add the "in" + activity_message = activity_message[3:].strip() elif activity_type == ActivityType.streaming: url = self.bot.config["twitch_url"] From f5916be39e5af0731545c8d797913093fe30774d Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:37:41 +0800 Subject: [PATCH 0764/1221] fareply, anonsnippet config, disable_updates config --- CHANGELOG.md | 6 +++++- app.json | 2 +- bot.py | 7 +++++-- cogs/modmail.py | 23 +++++++++++++++++++++++ cogs/utility.py | 1 + core/checks.py | 16 ++++++++++++++++ core/config.py | 4 ++++ core/config_help.json | 9 +++++++++ 8 files changed, 64 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ffdf67c9..bf908d4bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.14-dev1 +# v3.7.14-dev2 ### Added - `update_notifications` configuration option to toggle bot autoupdate notifications. ([GH #2896](https://github.com/kyb3r/modmail/issues/2896)) +- `?fareply`, anonymously reply with variables. +- `anonymous_snippets` config variable to toggle if snippets should be anonymous. ([GH #2905](https://github.com/kyb3r/modmail/issues/2905)) +- `disable_updates` config variable to control if the update command should be disabled or not. ### Improved - Added command validation to `autotrigger add/edit`. +- `GITHUB_TOKEN` is now no longer required in Heroku setups. ### Fixed diff --git a/app.json b/app.json index 326c54273e..66b5c77752 100644 --- a/app.json +++ b/app.json @@ -33,7 +33,7 @@ }, "GITHUB_TOKEN": { "description": "A github personal access token with the repo scope.", - "required": true + "required": false } } } \ No newline at end of file diff --git a/bot.py b/bot.py index 59b0e7341b..e1955ff667 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.14-dev1" +__version__ = "3.7.14-dev2" import asyncio @@ -1069,7 +1069,10 @@ async def process_commands(self, message): # Process snippets if cmd in self.snippets: snippet = self.snippets[cmd] - message.content = f"{self.prefix}freply {snippet}" + if self.config["anonymous_snippets"]: + message.content = f"{self.prefix}fareply {snippet}" + else: + 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 e71214d335..8ba509078c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,4 +1,5 @@ import asyncio +from operator import truediv import re from datetime import datetime from itertools import zip_longest @@ -835,6 +836,28 @@ async def freply(self, ctx, *, msg: str = ""): async with ctx.typing(): await ctx.thread.reply(ctx.message) + @commands.command(aliases=["formatanonreply"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def fareply(self, ctx, *, msg: str = ""): + """ + Anonymously reply to a Modmail thread with variables. + + Works just like `{prefix}areply`, 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, anonymous=True) + @commands.command(aliases=["anonreply", "anonymousreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() diff --git a/cogs/utility.py b/cogs/utility.py index 64f9c679c9..401f1aa37c 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1896,6 +1896,7 @@ async def github(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.OWNER) @checks.github_token_required(ignore_if_not_heroku=True) + @checks.updates_enabled() @trigger_typing async def update(self, ctx, *, flag: str = ""): """ diff --git a/core/checks.py b/core/checks.py index b7c56a1a7d..1ff2a67126 100644 --- a/core/checks.py +++ b/core/checks.py @@ -121,3 +121,19 @@ async def predicate(ctx): "personal access token from developer settings." ) return commands.check(predicate) + +def updates_enabled(): + """ + A decorator that ensures + updates are enabled + """ + + async def predicate(ctx): + return not ctx.bot.config["disable_updates"] + + predicate.fail_msg = ( + "Updates are disabled on this bot instance. " + "View `?config help disable_updates` for " + "more information." + ) + return commands.check(predicate) diff --git a/core/config.py b/core/config.py index 8317ed7df6..6b827e74c0 100644 --- a/core/config.py +++ b/core/config.py @@ -80,6 +80,7 @@ class ConfigManager: "close_on_leave_reason": "The recipient has left the server.", "alert_on_mention": False, "show_timestamp": True, + "anonymous_snippets": False, # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), @@ -143,6 +144,7 @@ class ConfigManager: # github access token for private repositories "github_token": None, "disable_autoupdates": False, + "disable_updates": False, # Logging "log_level": "INFO", # data collection @@ -173,8 +175,10 @@ class ConfigManager: "data_collection", "enable_eval", "disable_autoupdates", + "disable_updates", "update_notifications", "thread_contact_silently", + "anonymous_snippets", } enums = { diff --git a/core/config_help.json b/core/config_help.json index 815d2a6b17..506b9e57bd 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -849,5 +849,14 @@ "notes": [ "This configuration can only to be set through `.env` file or environment (config) variables." ] + }, + "disable_updates": { + "default": "No", + "description": "Controls if the update command should be disabled or not.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] } } From 05b261b8ce87fb264e4fa992e0e769519f90c329 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:37:41 +0800 Subject: [PATCH 0765/1221] fareply, anonsnippet config, disable_updates config, resolves #2905 --- CHANGELOG.md | 6 +++++- app.json | 2 +- bot.py | 7 +++++-- cogs/modmail.py | 23 +++++++++++++++++++++++ cogs/utility.py | 1 + core/checks.py | 16 ++++++++++++++++ core/config.py | 4 ++++ core/config_help.json | 9 +++++++++ 8 files changed, 64 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ffdf67c9..bf908d4bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.14-dev1 +# v3.7.14-dev2 ### Added - `update_notifications` configuration option to toggle bot autoupdate notifications. ([GH #2896](https://github.com/kyb3r/modmail/issues/2896)) +- `?fareply`, anonymously reply with variables. +- `anonymous_snippets` config variable to toggle if snippets should be anonymous. ([GH #2905](https://github.com/kyb3r/modmail/issues/2905)) +- `disable_updates` config variable to control if the update command should be disabled or not. ### Improved - Added command validation to `autotrigger add/edit`. +- `GITHUB_TOKEN` is now no longer required in Heroku setups. ### Fixed diff --git a/app.json b/app.json index 326c54273e..66b5c77752 100644 --- a/app.json +++ b/app.json @@ -33,7 +33,7 @@ }, "GITHUB_TOKEN": { "description": "A github personal access token with the repo scope.", - "required": true + "required": false } } } \ No newline at end of file diff --git a/bot.py b/bot.py index 59b0e7341b..e1955ff667 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.14-dev1" +__version__ = "3.7.14-dev2" import asyncio @@ -1069,7 +1069,10 @@ async def process_commands(self, message): # Process snippets if cmd in self.snippets: snippet = self.snippets[cmd] - message.content = f"{self.prefix}freply {snippet}" + if self.config["anonymous_snippets"]: + message.content = f"{self.prefix}fareply {snippet}" + else: + 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 e71214d335..8ba509078c 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1,4 +1,5 @@ import asyncio +from operator import truediv import re from datetime import datetime from itertools import zip_longest @@ -835,6 +836,28 @@ async def freply(self, ctx, *, msg: str = ""): async with ctx.typing(): await ctx.thread.reply(ctx.message) + @commands.command(aliases=["formatanonreply"]) + @checks.has_permissions(PermissionLevel.SUPPORTER) + @checks.thread_only() + async def fareply(self, ctx, *, msg: str = ""): + """ + Anonymously reply to a Modmail thread with variables. + + Works just like `{prefix}areply`, 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, anonymous=True) + @commands.command(aliases=["anonreply", "anonymousreply"]) @checks.has_permissions(PermissionLevel.SUPPORTER) @checks.thread_only() diff --git a/cogs/utility.py b/cogs/utility.py index 64f9c679c9..401f1aa37c 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1896,6 +1896,7 @@ async def github(self, ctx): @commands.command() @checks.has_permissions(PermissionLevel.OWNER) @checks.github_token_required(ignore_if_not_heroku=True) + @checks.updates_enabled() @trigger_typing async def update(self, ctx, *, flag: str = ""): """ diff --git a/core/checks.py b/core/checks.py index b7c56a1a7d..1ff2a67126 100644 --- a/core/checks.py +++ b/core/checks.py @@ -121,3 +121,19 @@ async def predicate(ctx): "personal access token from developer settings." ) return commands.check(predicate) + +def updates_enabled(): + """ + A decorator that ensures + updates are enabled + """ + + async def predicate(ctx): + return not ctx.bot.config["disable_updates"] + + predicate.fail_msg = ( + "Updates are disabled on this bot instance. " + "View `?config help disable_updates` for " + "more information." + ) + return commands.check(predicate) diff --git a/core/config.py b/core/config.py index 8317ed7df6..6b827e74c0 100644 --- a/core/config.py +++ b/core/config.py @@ -80,6 +80,7 @@ class ConfigManager: "close_on_leave_reason": "The recipient has left the server.", "alert_on_mention": False, "show_timestamp": True, + "anonymous_snippets": False, # moderation "recipient_color": str(discord.Color.gold()), "mod_color": str(discord.Color.green()), @@ -143,6 +144,7 @@ class ConfigManager: # github access token for private repositories "github_token": None, "disable_autoupdates": False, + "disable_updates": False, # Logging "log_level": "INFO", # data collection @@ -173,8 +175,10 @@ class ConfigManager: "data_collection", "enable_eval", "disable_autoupdates", + "disable_updates", "update_notifications", "thread_contact_silently", + "anonymous_snippets", } enums = { diff --git a/core/config_help.json b/core/config_help.json index 815d2a6b17..506b9e57bd 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -849,5 +849,14 @@ "notes": [ "This configuration can only to be set through `.env` file or environment (config) variables." ] + }, + "disable_updates": { + "default": "No", + "description": "Controls if the update command should be disabled or not.", + "examples": [ + ], + "notes": [ + "This configuration can only to be set through `.env` file or environment (config) variables." + ] } } From 0eab4f3c2aaa000df923e96ecb3c304f3242635c Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:44:24 +0800 Subject: [PATCH 0766/1221] lint --- core/checks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/checks.py b/core/checks.py index 1ff2a67126..55eb4d4ea8 100644 --- a/core/checks.py +++ b/core/checks.py @@ -122,6 +122,7 @@ async def predicate(ctx): ) return commands.check(predicate) + def updates_enabled(): """ A decorator that ensures From e8be5d1855eaa8c3d2131fca57b632255082ece0 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 3 Dec 2020 21:25:48 +0800 Subject: [PATCH 0767/1221] Support only serverv members intent --- CHANGELOG.md | 1 + bot.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf908d4bb8..34a2b416f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?fareply`, anonymously reply with variables. - `anonymous_snippets` config variable to toggle if snippets should be anonymous. ([GH #2905](https://github.com/kyb3r/modmail/issues/2905)) - `disable_updates` config variable to control if the update command should be disabled or not. +- Support for only the "Server Members" intent. ### Improved diff --git a/bot.py b/bot.py index e1955ff667..fa980fe5a3 100644 --- a/bot.py +++ b/bot.py @@ -192,9 +192,17 @@ def run(self, *args, **kwargs): except discord.LoginFailure: logger.critical("Invalid token") except discord.PrivilegedIntentsRequired: - logger.critical( - "Privileged intents are not explicitly granted in the discord developers dashboard." - ) + intents = discord.Intents.default() + intents.members = True + # Try again with members intent + super().__init__(command_prefix=None, intents=intents) # implemented in `get_prefix` + logger.warning("Attempting to login with only the server members privileged intent. Some plugins might not work correctly.") + try: + self.loop.run_until_complete(self.start(self.token)) + except discord.PrivilegedIntentsRequired: + logger.critical( + "Privileged intents are not explicitly granted in the discord developers dashboard." + ) except Exception: logger.critical("Fatal exception", exc_info=True) finally: From d922683283ab47206a5de761ddfd5560f225a4e7 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 3 Dec 2020 21:33:33 +0800 Subject: [PATCH 0768/1221] silent_alert_on_mention, resolve #2907 --- bot.py | 9 +++++++-- core/config.py | 2 ++ core/config_help.json | 15 +++++++++++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/bot.py b/bot.py index fa980fe5a3..f51899127e 100644 --- a/bot.py +++ b/bot.py @@ -195,7 +195,7 @@ def run(self, *args, **kwargs): intents = discord.Intents.default() intents.members = True # Try again with members intent - super().__init__(command_prefix=None, intents=intents) # implemented in `get_prefix` + self._connection._intents = intents logger.warning("Attempting to login with only the server members privileged intent. Some plugins might not work correctly.") try: self.loop.run_until_complete(self.start(self.token)) @@ -1060,7 +1060,12 @@ async def on_message(self, message): ) if self.config["show_timestamp"]: em.timestamp = datetime.utcnow() - await self.mention_channel.send(content=self.config["mention"], embed=em) + + if not self.config["silent_alert_on_mention"]: + content = self.config["mention"] + else: + content = "" + await self.mention_channel.send(content=content, embed=em) await self.process_commands(message) diff --git a/core/config.py b/core/config.py index 6b827e74c0..2c88b10d14 100644 --- a/core/config.py +++ b/core/config.py @@ -79,6 +79,7 @@ class ConfigManager: "close_on_leave": False, "close_on_leave_reason": "The recipient has left the server.", "alert_on_mention": False, + "silent_alert_on_mention": False, "show_timestamp": True, "anonymous_snippets": False, # moderation @@ -168,6 +169,7 @@ class ConfigManager: "transfer_reactions", "close_on_leave", "alert_on_mention", + "silent_alert_on_mention", "show_timestamp", "confirm_thread_creation", "use_regex_autotrigger", diff --git a/core/config_help.json b/core/config_help.json index 506b9e57bd..e5fc4c6d17 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -662,12 +662,23 @@ }, "alert_on_mention": { "default": "No", - "description": "Mentions all mods (mention) in logs channel when bot is mentioned", + "description": "Mentions all mods (mention) in mention channel when bot is mentioned", "examples":[ "`{prefix}config set alert_on_mention yes`" ], "notes": [ - "See also: `mention`" + "See also: `mention`, `mention_channel_id`" + ] + }, + "silent_alert_on_mention": { + "default": "No", + "description": "Send a message in the mention channel without mentioning all mods (mention).", + "examples":[ + "`{prefix}config set alert_on_mention yes`" + ], + "notes": [ + "This has no effect unless `alert_on_mention` is set to yes.", + "See also: `mention`, `mention_channel_id`" ] }, "show_timestamp": { From a318aa43d12d9f8e8ceaa75c66aec45d00dc0f17 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 3 Dec 2020 21:35:57 +0800 Subject: [PATCH 0769/1221] Linting --- CHANGELOG.md | 3 ++- bot.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34a2b416f5..f9a7c8afe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.14-dev2 +# v3.7.14-dev3 ### Added @@ -14,6 +14,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?fareply`, anonymously reply with variables. - `anonymous_snippets` config variable to toggle if snippets should be anonymous. ([GH #2905](https://github.com/kyb3r/modmail/issues/2905)) - `disable_updates` config variable to control if the update command should be disabled or not. +- `silent_alert_on_mention` to alert mods silently. ([GH #2907](https://github.com/kyb3r/modmail/issues/2907)) - Support for only the "Server Members" intent. ### Improved diff --git a/bot.py b/bot.py index f51899127e..a920011205 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.14-dev2" +__version__ = "3.7.14-dev3" import asyncio @@ -196,7 +196,9 @@ def run(self, *args, **kwargs): intents.members = True # Try again with members intent self._connection._intents = intents - logger.warning("Attempting to login with only the server members privileged intent. Some plugins might not work correctly.") + logger.warning( + "Attempting to login with only the server members privileged intent. Some plugins might not work correctly." + ) try: self.loop.run_until_complete(self.start(self.token)) except discord.PrivilegedIntentsRequired: From 460758f9f364b1c416b12bc69a340beb41a4e7db Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Thu, 10 Dec 2020 00:07:34 +0800 Subject: [PATCH 0770/1221] use re.search not re.match in autotriggers --- CHANGELOG.md | 3 ++- bot.py | 6 +++--- cogs/utility.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a7c8afe9..a4b3741dd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.14-dev3 +# v3.7.14-dev4 ### Added @@ -26,6 +26,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Mentioned `competing` as an activity type. ([PR #2902](https://github.com/kyb3r/modmail/pull/2902)) - Level permissions were not checked if command permissions were set. +- Regex autotriggers were not working if term was in the middle of strings. ### Internal diff --git a/bot.py b/bot.py index a920011205..c0388dcb3e 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.14-dev3" +__version__ = "3.7.14-dev4" import asyncio @@ -949,10 +949,10 @@ async def trigger_auto_triggers(self, message, channel, *, cls=commands.Context) if self.config.get("use_regex_autotrigger"): trigger = next( - filter(lambda x: re.match(x, message.content), self.auto_triggers.keys()) + filter(lambda x: re.search(x, message.content), self.auto_triggers.keys()) ) if trigger: - invoker = re.match(trigger, message.content).group(0) + invoker = re.search(trigger, message.content).group(0) else: trigger = next( filter(lambda x: x.lower() in message.content.lower(), self.auto_triggers.keys()) diff --git a/cogs/utility.py b/cogs/utility.py index 401f1aa37c..93fbc36ad8 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1829,7 +1829,7 @@ async def autotrigger_test(self, ctx, *, text): """Tests a string against the current autotrigger setup""" for keyword in self.bot.auto_triggers: if self.bot.config.get("use_regex_autotrigger"): - check = re.match(keyword, text) + check = re.search(keyword, text) regex = True else: check = keyword.lower() in text.lower() From b8a8f48dfe602202bf67fc7033aa3c17771ab40b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sun, 13 Dec 2020 20:51:53 +0800 Subject: [PATCH 0771/1221] Add anon snippets into config help --- bot.py | 2 +- core/config_help.json | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index c0388dcb3e..aacae88639 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.14-dev4" +__version__ = "3.7.14-dev5" import asyncio diff --git a/core/config_help.json b/core/config_help.json index e5fc4c6d17..e6bf3730ab 100644 --- a/core/config_help.json +++ b/core/config_help.json @@ -689,6 +689,16 @@ ], "notes": [] }, + "anonymous_snippets": { + "default": "No", + "description": "Sends snippets anonymously.", + "examples":[ + "`{prefix}config set anonymous_snippets yes`" + ], + "notes": [ + "See also: `anon_avatar_url`, `anon_tag`." + ] + }, "confirm_thread_creation": { "default": "No", "description": "Ensure users confirm that they want to create a new thread", From 6e62ce86b717dd5123baff9fec456c77b579c150 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 15 Dec 2020 16:10:42 +0800 Subject: [PATCH 0772/1221] add Berkand Karadere sponsor --- README.md | 5 +++++ SPONSORS.json | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e40426dd1..5741f1db6e 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,11 @@ Special thanks to our sponsors for supporting the project. + + + + + Become a sponsor on [Patreon](https://patreon.com/kyber). ## Plugins diff --git a/SPONSORS.json b/SPONSORS.json index ceed37be23..fcabb119ac 100644 --- a/SPONSORS.json +++ b/SPONSORS.json @@ -26,5 +26,21 @@ } ] } + }, + { + "embed": { + "title": "Berkand Karadere", + "description": "Berkand Karadere is an German Community Manager who integrated new systems into the game industry. He also is hosting and developing web servers and game servers. He also plays American Football for the Dortmund Giants and his journey has just begun.", + "color": 2968248, + "thumbnail": { + "url": "https://i.imgur.com/cs2QEcp.png" + }, + "fields": [ + { + "name": "Discord Server!", + "value": "[**Click here**](https://discord.gg/BanCwptMJV)" + } + ] + } } -] +] \ No newline at end of file From 7ac59e0d841c5bf81e43be667f234c9373c588a7 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Tue, 15 Dec 2020 23:59:33 +0800 Subject: [PATCH 0773/1221] `?blocked` now no longers show blocks that have expired. --- CHANGELOG.md | 1 + bot.py | 2 +- cogs/modmail.py | 53 ++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4b3741dd2..8e941cd863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Mentioned `competing` as an activity type. ([PR #2902](https://github.com/kyb3r/modmail/pull/2902)) - Level permissions were not checked if command permissions were set. - Regex autotriggers were not working if term was in the middle of strings. +- `?blocked` now no longers show blocks that have expired. ### Internal diff --git a/bot.py b/bot.py index aacae88639..08a8cec02d 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.14-dev5" +__version__ = "3.7.14-dev6" import asyncio diff --git a/cogs/modmail.py b/cogs/modmail.py index 8ba509078c..29a3c79df4 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -1050,8 +1050,30 @@ async def blocked(self, ctx): roles = [] users = [] + now = ctx.message.created_at + + blocked_users = list(self.bot.blocked_users.items()) + for id_, reason in blocked_users: + # parse "reason" and check if block is expired + # 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.", + id_, + ) + + if end_time is not None: + after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() + if after <= 0: + # No longer blocked + self.bot.blocked_users.pop(str(id_)) + logger.debug("No longer blocked, user %s.", id_) + continue - for id_, reason in self.bot.blocked_users.items(): user = self.bot.get_user(int(id_)) if user: users.append((user.mention, reason)) @@ -1062,7 +1084,28 @@ async def blocked(self, ctx): except discord.NotFound: users.append((id_, reason)) - for id_, reason in self.bot.blocked_roles.items(): + blocked_roles = list(self.bot.blocked_roles.items()) + for id_, reason in blocked_roles: + # parse "reason" and check if block is expired + # 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 role %s, block and unblock again to update.", + id_, + ) + + if end_time is not None: + after = (datetime.fromisoformat(end_time.group(1)) - now).total_seconds() + if after <= 0: + # No longer blocked + self.bot.blocked_roles.pop(str(id_)) + logger.debug("No longer blocked, role %s.", id_) + continue + role = self.bot.guild.get_role(int(id_)) if role: roles.append((role.mention, reason)) @@ -1175,7 +1218,7 @@ async def block( after: UserFriendlyTime = None, ): """ - Block a user from using Modmail. + Block a user or role from using Modmail. You may choose to set a time as to when the user will automatically be unblocked. @@ -1190,9 +1233,9 @@ async def block( if thread: user_or_role = thread.recipient elif after is None: - raise commands.MissingRequiredArgument(SimpleNamespace(name="user")) + raise commands.MissingRequiredArgument(SimpleNamespace(name="user or role")) else: - raise commands.BadArgument(f'User "{after.arg}" not found.') + raise commands.BadArgument(f'User or role "{after.arg}" not found.') mention = getattr(user_or_role, "mention", f"`{user_or_role.id}`") From 450e341ad2010d70f765a40430ce584881f5c213 Mon Sep 17 00:00:00 2001 From: Matthew <57693543+XiehCanCode@users.noreply.github.com> Date: Tue, 22 Dec 2020 10:33:34 +0000 Subject: [PATCH 0774/1221] fix typo --- PRIVACY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRIVACY.md b/PRIVACY.md index ebe5ce6954..b4283e3660 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -81,7 +81,7 @@ The Modmail team does not track any data by users. ### Opting out -There is no way for users or moderators to opt out frmo this data collection. +There is no way for users or moderators to opt out from this data collection. ### Data deletion From b0df2b3ccde968e9c6a2af0932e6d69f49fb2d50 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Wed, 23 Dec 2020 22:19:46 +0100 Subject: [PATCH 0775/1221] Update utility.py (#2918) Change of domain for hastebin, old domain forwards to a different site now --- cogs/utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index 03a90efe54..634ca71de9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -441,7 +441,7 @@ async def debug(self, ctx): async def debug_hastebin(self, ctx): """Posts application-logs to Hastebin.""" - haste_url = os.environ.get("HASTE_URL", "https://hasteb.in") + haste_url = os.environ.get("HASTE_URL", "https://hastebin.cc") log_file_name = self.bot.token.split(".")[0] with open( From b33812159240f9e81bf899b9bc794bfce1b13478 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 2 Jan 2021 23:05:42 +0800 Subject: [PATCH 0776/1221] Blocked roles will no longer trigger an error during unblock --- CHANGELOG.md | 1 + bot.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e941cd863..f152053862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Level permissions were not checked if command permissions were set. - Regex autotriggers were not working if term was in the middle of strings. - `?blocked` now no longers show blocks that have expired. +- Blocked roles will no longer trigger an error during unblock ### Internal diff --git a/bot.py b/bot.py index 08a8cec02d..623228bb19 100644 --- a/bot.py +++ b/bot.py @@ -673,18 +673,18 @@ def check_manual_blocked_roles(self, author: discord.Member) -> bool: 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, + r"Deprecated time message for role %s, block and unblock again to update.", + r.name, ) 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) + self.blocked_roles.pop(str(r.id)) + logger.debug("No longer blocked, role %s.", r.name) return True - logger.debug("User blocked, user %s.", author.name) + logger.debug("User blocked, role %s.", r.name) return False return True From e31b07dda3e1a35e51c8f3d49c191acee7bd67d2 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 2 Jan 2021 23:10:11 +0800 Subject: [PATCH 0777/1221] Fix custom emojis in confirm_thread_creation_deny, resolves #2916 --- CHANGELOG.md | 3 ++- core/thread.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f152053862..48365b7701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,8 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Level permissions were not checked if command permissions were set. - Regex autotriggers were not working if term was in the middle of strings. - `?blocked` now no longers show blocks that have expired. -- Blocked roles will no longer trigger an error during unblock +- Blocked roles will no longer trigger an error during unblock. +- Custom emojis are now supported in `confirm_thread_creation_deny`. ([GH #2916](https://github.com/kyb3r/modmail/issues/2916)) ### Internal diff --git a/core/thread.py b/core/thread.py index 5e1c28f15f..b1248bd738 100644 --- a/core/thread.py +++ b/core/thread.py @@ -1201,7 +1201,7 @@ async def create( del self.cache[recipient.id] return thread else: - if r.emoji == deny_emoji: + if str(r.emoji) == deny_emoji: thread.cancelled = True await confirm.remove_reaction(accept_emoji, self.bot.user) From 99c3c99095b3beb09525c501ea7a006f59a16cfd Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 2 Jan 2021 23:14:11 +0800 Subject: [PATCH 0778/1221] Finding linked messages in replies work now, resolves #2920 --- CHANGELOG.md | 1 + core/thread.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48365b7701..835ab48e02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - `?blocked` now no longers show blocks that have expired. - Blocked roles will no longer trigger an error during unblock. - Custom emojis are now supported in `confirm_thread_creation_deny`. ([GH #2916](https://github.com/kyb3r/modmail/issues/2916)) +- Finding linked messages in replies work now. ([GH #2920](https://github.com/kyb3r/modmail/issues/2920), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2920#issuecomment-751530495)) ### Internal diff --git a/core/thread.py b/core/thread.py index b1248bd738..adb122dd79 100644 --- a/core/thread.py +++ b/core/thread.py @@ -660,8 +660,10 @@ async def delete_message( 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 + compare_id = compare_url.split('#')[-1] else: compare_url = None + compare_id = None if self.channel is not None: async for linked_message in self.channel.history(): @@ -679,6 +681,11 @@ async def find_linked_message_from_dm(self, message, either_direction=False): msg_id = int(msg_id) if int(msg_id) == message.id: return linked_message + + if compare_id is not None and compare_id.isdigit(): + if int(msg_id) == int (compare_id): + return linked_message + raise ValueError("Thread channel message not found.") async def edit_dm_message(self, message: discord.Message, content: str) -> None: From 1b24283e674e2fbf6abf9e4880e572b92a31df4b Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 2 Jan 2021 23:21:20 +0800 Subject: [PATCH 0779/1221] Up version to v3.8-dev7 --- CHANGELOG.md | 2 +- bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 835ab48e02..ab29f6922d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.7.14-dev4 +# v3.8.0-dev7 ### Added diff --git a/bot.py b/bot.py index 623228bb19..fdd348e269 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.7.14-dev6" +__version__ = "3.8.0-dev7" import asyncio From 528dcdbcfca681a76c45fc293425c584cd83595d Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sat, 2 Jan 2021 23:29:58 +0800 Subject: [PATCH 0780/1221] Linting --- core/thread.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index adb122dd79..71acdb8610 100644 --- a/core/thread.py +++ b/core/thread.py @@ -660,7 +660,7 @@ async def delete_message( 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 - compare_id = compare_url.split('#')[-1] + compare_id = compare_url.split("#")[-1] else: compare_url = None compare_id = None @@ -683,7 +683,7 @@ async def find_linked_message_from_dm(self, message, either_direction=False): return linked_message if compare_id is not None and compare_id.isdigit(): - if int(msg_id) == int (compare_id): + if int(msg_id) == int(compare_id): return linked_message raise ValueError("Thread channel message not found.") From fe114483e7d1069ebe76be2cad51d56f9175fad7 Mon Sep 17 00:00:00 2001 From: fourjr <28086837+fourjr@users.noreply.github.com> Date: Sun, 3 Jan 2021 00:02:07 +0800 Subject: [PATCH 0781/1221] Fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab29f6922d..dc5893f651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s ### Internal -- Make use of `git branch --show-current` to retrieve branch insteasd of using prerelease version check. +- Make use of `git branch --show-current` to retrieve branch instead of using prerelease version check. # v3.7.13 From c2b75dcaf17cad274828e862b86f421c2307d146 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 6 Jan 2021 21:12:12 +0800 Subject: [PATCH 0782/1221] Clearer error messages on reply fails. --- CHANGELOG.md | 1 + core/thread.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc5893f651..7b736a69ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Added command validation to `autotrigger add/edit`. - `GITHUB_TOKEN` is now no longer required in Heroku setups. +- Clearer error messages on reply fails. ### Fixed diff --git a/core/thread.py b/core/thread.py index 71acdb8610..c3918b2daa 100644 --- a/core/thread.py +++ b/core/thread.py @@ -747,16 +747,21 @@ async def reply( anonymous=anonymous, plain=plain, ) - except Exception: + except Exception as e: logger.error("Message delivery failed:", exc_info=True) + if isinstance(e, discord.Forbidden): + description = ("Your message could not be delivered as " + "the recipient is only accepting direct " + "messages from friends, or the bot was " + "blocked by the recipient.") + else: + description = ("Your message could not be delivered due " + "to an unknown error. ") tasks.append( message.channel.send( embed=discord.Embed( color=self.bot.error_color, - description="Your message could not be delivered as " - "the recipient is only accepting direct " - "messages from friends, or the bot was " - "blocked by the recipient.", + description=description, ) ) ) From a6ebd81d3fa40f713b2da188bc7a74fa47db8bcb Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 6 Jan 2021 21:18:14 +0800 Subject: [PATCH 0783/1221] Sending files in threads (non-images) now work. resolves #2926 --- CHANGELOG.md | 1 + core/thread.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b736a69ee..70109c97a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Blocked roles will no longer trigger an error during unblock. - Custom emojis are now supported in `confirm_thread_creation_deny`. ([GH #2916](https://github.com/kyb3r/modmail/issues/2916)) - Finding linked messages in replies work now. ([GH #2920](https://github.com/kyb3r/modmail/issues/2920), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2920#issuecomment-751530495)) +- Sending files in threads (non-images) now work. ([GH #2926](https://github.com/kyb3r/modmail/issues/2926)) ### Internal diff --git a/core/thread.py b/core/thread.py index c3918b2daa..041a601c21 100644 --- a/core/thread.py +++ b/core/thread.py @@ -756,7 +756,8 @@ async def reply( "blocked by the recipient.") else: description = ("Your message could not be delivered due " - "to an unknown error. ") + "to an unknown error. Check `?debug` for " + "more information") tasks.append( message.channel.send( embed=discord.Embed( @@ -933,7 +934,7 @@ async def send( file_upload_count = 1 - for url, filename in attachments: + for url, filename, _ in attachments: embed.add_field( name=f"File upload ({file_upload_count})", value=f"[{filename}]({url})" ) From beb73d8d82960429631c0aef5aea31f4f75f23b1 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 6 Jan 2021 21:24:18 +0800 Subject: [PATCH 0784/1221] Deleting messages no longer shows a false error, resolves #2910 --- CHANGELOG.md | 1 + core/thread.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70109c97a6..404869adae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Custom emojis are now supported in `confirm_thread_creation_deny`. ([GH #2916](https://github.com/kyb3r/modmail/issues/2916)) - Finding linked messages in replies work now. ([GH #2920](https://github.com/kyb3r/modmail/issues/2920), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2920#issuecomment-751530495)) - Sending files in threads (non-images) now work. ([GH #2926](https://github.com/kyb3r/modmail/issues/2926)) +- Deleting messages no longer shows a false error. ([GH #2910](https://github.com/kyb3r/modmail/issues/2910), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2910#issuecomment-753557313)) ### Internal diff --git a/core/thread.py b/core/thread.py index 041a601c21..5f630654fe 100644 --- a/core/thread.py +++ b/core/thread.py @@ -650,7 +650,7 @@ async def delete_message( tasks = [] if not isinstance(message, discord.Message): tasks += [message1.delete()] - if message2 is not None: + elif message2 is not None: tasks += [message2.delete()] elif message1.embeds[0].author.name.startswith("Persistent Note"): tasks += [self.bot.api.delete_note(message1.id)] From 1de48fd617996846ae7b776f59e03c287ded1846 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Wed, 6 Jan 2021 21:27:56 +0800 Subject: [PATCH 0785/1221] Linting --- core/changelog.py | 2 +- core/checks.py | 2 +- core/thread.py | 23 ++++++++++++----------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/core/changelog.py b/core/changelog.py index 163aa0f340..ada9fe6984 100644 --- a/core/changelog.py +++ b/core/changelog.py @@ -91,7 +91,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(): diff --git a/core/checks.py b/core/checks.py index 55eb4d4ea8..74b7dc38cd 100644 --- a/core/checks.py +++ b/core/checks.py @@ -5,7 +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/thread.py b/core/thread.py index 5f630654fe..6480160e8b 100644 --- a/core/thread.py +++ b/core/thread.py @@ -750,20 +750,21 @@ async def reply( except Exception as e: logger.error("Message delivery failed:", exc_info=True) if isinstance(e, discord.Forbidden): - description = ("Your message could not be delivered as " - "the recipient is only accepting direct " - "messages from friends, or the bot was " - "blocked by the recipient.") + description = ( + "Your message could not be delivered as " + "the recipient is only accepting direct " + "messages from friends, or the bot was " + "blocked by the recipient." + ) else: - description = ("Your message could not be delivered due " - "to an unknown error. Check `?debug` for " - "more information") + description = ( + "Your message could not be delivered due " + "to an unknown error. Check `?debug` for " + "more information" + ) tasks.append( message.channel.send( - embed=discord.Embed( - color=self.bot.error_color, - description=description, - ) + embed=discord.Embed(color=self.bot.error_color, description=description,) ) ) else: From 6cb26a70dd264edcef95205b0661774df8ba4311 Mon Sep 17 00:00:00 2001 From: Jerrie-Aries <70805800+Jerrie-Aries@users.noreply.github.com> Date: Thu, 7 Jan 2021 12:02:37 +0800 Subject: [PATCH 0786/1221] Fix `?perms get` command showing IDs instead of user or role mentions. --- cogs/utility.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 93fbc36ad8..c8a0011a33 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1453,15 +1453,15 @@ def _get_perm(self, ctx, name, type_): if perm == -1: values.insert(0, "**everyone**") continue - member = ctx.guild.get_member(perm) + member = ctx.guild.get_member(int(perm)) if member is not None: values.append(member.mention) continue - user = self.bot.get_user(perm) + user = self.bot.get_user(int(perm)) if user is not None: values.append(user.mention) continue - role = ctx.guild.get_role(perm) + role = ctx.guild.get_role(int(perm)) if role is not None: values.append(role.mention) else: From baf9a5b7044c9530cfd45412e277ff08ebfeb974 Mon Sep 17 00:00:00 2001 From: Jia Rong Yee <28086837+fourjr@users.noreply.github.com> Date: Fri, 8 Jan 2021 16:18:30 +0800 Subject: [PATCH 0787/1221] Support discord.py v1.6 & LOTTIE stickers --- CHANGELOG.md | 4 +- Pipfile | 2 +- Pipfile.lock | 350 ++++++++++++++++++++++------------------ bot.py | 4 +- core/thread.py | 24 ++- discord.py-1.5.2.tar.gz | Bin 649426 -> 0 bytes 6 files changed, 217 insertions(+), 167 deletions(-) delete mode 100644 discord.py-1.5.2.tar.gz diff --git a/CHANGELOG.md b/CHANGELOG.md index 404869adae..bbc9a029b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html); however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section. -# v3.8.0-dev7 +# v3.8.0-dev8 ### Added @@ -34,10 +34,12 @@ however, insignificant breaking changes do not guarantee a major version bump, s - Finding linked messages in replies work now. ([GH #2920](https://github.com/kyb3r/modmail/issues/2920), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2920#issuecomment-751530495)) - Sending files in threads (non-images) now work. ([GH #2926](https://github.com/kyb3r/modmail/issues/2926)) - Deleting messages no longer shows a false error. ([GH #2910](https://github.com/kyb3r/modmail/issues/2910), [Jerrie-Aries](https://github.com/kyb3r/modmail/issues/2910#issuecomment-753557313)) +- Display an error on [Lottie](https://airbnb.io/lottie/#/) stickers, instead of failing the send. ### Internal - Make use of `git branch --show-current` to retrieve branch instead of using prerelease version check. +- Use discord.py 1.6.0 from PyPi instead of the development clone. # v3.7.13 diff --git a/Pipfile b/Pipfile index b878d053e9..f9621c3493 100644 --- a/Pipfile +++ b/Pipfile @@ -27,7 +27,7 @@ parsedatetime = "==2.6" aiohttp = ">=3.6.0,<3.7.0" python-dotenv = ">=0.10.3" pipenv = "*" -"discord.py" = {file = "./discord.py-1.5.2.tar.gz"} +"discord.py" = "==1.6.0" [scripts] bot = "python bot.py" diff --git a/Pipfile.lock b/Pipfile.lock index 0eacd81d2c..aaa491011e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1f660c7237deeaa50a7098002c0f3c05cce254d6c0ba8bc02993c5400e335a59" + "sha256": "bfe06c9fe1db25178b01413114093b26f0d32e9a90a031c048ff3ddc7b6644b7" }, "pipfile-spec": 6, "requires": {}, @@ -50,6 +50,7 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -57,14 +58,15 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "certifi": { "hashes": [ - "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd", - "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2020.11.8" + "version": "==2020.12.5" }, "chardet": { "hashes": [ @@ -82,7 +84,12 @@ "version": "==0.4.4" }, "discord.py": { - "file": "./discord.py-1.5.2.tar.gz" + "hashes": [ + "sha256:3df148daf6fbcc7ab5b11042368a3cd5f7b730b62f09fb5d3cbceff59bcfbb12", + "sha256:ba8be99ff1b8c616f7b6dcb700460d0222b29d4c11048e74366954c465fdd05f" + ], + "index": "pypi", + "version": "==1.6.0" }, "distlib": { "hashes": [ @@ -115,10 +122,11 @@ }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", + "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" ], - "version": "==2.10" + "markers": "python_version >= '3.4'", + "version": "==3.1" }, "isodate": { "hashes": [ @@ -156,6 +164,7 @@ "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" ], + "markers": "python_version >= '3.5'", "version": "==4.7.6" }, "natural": { @@ -175,70 +184,80 @@ }, "pipenv": { "hashes": [ - "sha256:d6ac39d1721517b23aca12cdb4c726dc318ec4d7bdede5c1220bbb81775005c3", - "sha256:dce1fb1a6941f98764c62b00010f52143aed19e2fcd8f100aff4fb3bb1bbbbe3" + "sha256:4ab2f60742184d851ac44b9e1d423afe71dc2ea7a68bde07eb890c8b4ce5a420", + "sha256:8253fe6f9cfb3791a54da8a0571f73c918cb3457dd908684c1800a13a06ec4c1" ], "index": "pypi", - "version": "==2020.11.4" + "version": "==2020.11.15" }, "pymongo": { "hashes": [ - "sha256:03dc64a9aa7a5d405aea5c56db95835f6a2fa31b3502c5af1760e0e99210be30", - "sha256:05fcc6f9c60e6efe5219fbb5a30258adb3d3e5cbd317068f3d73c09727f2abb6", - "sha256:076a7f2f7c251635cf6116ac8e45eefac77758ee5a77ab7bd2f63999e957613b", - "sha256:137e6fa718c7eff270dbd2fc4b90d94b1a69c9e9eb3f3de9e850a7fd33c822dc", - "sha256:1f865b1d1c191d785106f54df9abdc7d2f45a946b45fd1ea0a641b4f982a2a77", - "sha256:213c445fe7e654621c6309e874627c35354b46ef3ee807f5a1927dc4b30e1a67", - "sha256:25e617daf47d8dfd4e152c880cd0741cbdb48e51f54b8de9ddbfe74ecd87dd16", - "sha256:3d9bb1ba935a90ec4809a8031efd988bdb13cdba05d9e9a3e9bf151bf759ecde", - "sha256:40696a9a53faa7d85aaa6fd7bef1cae08f7882640bad08c350fb59dee7ad069b", - "sha256:421aa1b92c291c429668bd8d8d8ec2bd00f183483a756928e3afbf2b6f941f00", - "sha256:4437300eb3a5e9cc1a73b07d22c77302f872f339caca97e9bf8cf45eca8fa0d2", - "sha256:455f4deb00158d5ec8b1d3092df6abb681b225774ab8a59b3510293b4c8530e3", - "sha256:475a34a0745c456ceffaec4ce86b7e0983478f1b6140890dff7b161e7bcd895b", - "sha256:4797c0080f41eba90404335e5ded3aa66731d303293a675ff097ce4ea3025bb9", - "sha256:4ae23fbbe9eadf61279a26eba866bbf161a6f7e2ffad14a42cf20e9cb8e94166", - "sha256:4b32744901ee9990aa8cd488ec85634f443526def1e5190a407dc107148249d7", - "sha256:50127b13b38e8e586d5e97d342689405edbd74ad0bd891d97ee126a8c7b6e45f", - "sha256:50531caa7b4be1c4ed5e2d5793a4e51cc9bd62a919a6fd3299ef7c902e206eab", - "sha256:63a5387e496a98170ffe638b435c0832c0f2011a6f4ff7a2880f17669fff8c03", - "sha256:68220b81850de8e966d4667d5c325a96c6ac0d6adb3d18935d6e3d325d441f48", - "sha256:689142dc0c150e9cb7c012d84cac2c346d40beb891323afb6caf18ec4caafae0", - "sha256:6a15e2bee5c4188369a87ed6f02de804651152634a46cca91966a11c8abd2550", - "sha256:7122ffe597b531fb065d3314e704a6fe152b81820ca5f38543e70ffcc95ecfd4", - "sha256:7307024b18266b302f4265da84bb1effb5d18999ef35b30d17592959568d5c0a", - "sha256:7a4a6f5b818988a3917ec4baa91d1143242bdfece8d38305020463955961266a", - "sha256:83c5a3ecd96a9f3f11cfe6dfcbcec7323265340eb24cc996acaecea129865a3a", - "sha256:890b0f1e18dbd898aeb0ab9eae1ab159c6bcbe87f0abb065b0044581d8614062", - "sha256:8deda1f7b4c03242f2a8037706d9584e703f3d8c74d6d9cac5833db36fe16c42", - "sha256:8ea13d0348b4c96b437d944d7068d59ed4a6c98aaa6c40d8537a2981313f1c66", - "sha256:91e96bf85b7c07c827d339a386e8a3cf2e90ef098c42595227f729922d0851df", - "sha256:96782ebb3c9e91e174c333208b272ea144ed2a684413afb1038e3b3342230d72", - "sha256:9755c726aa6788f076114dfdc03b92b03ff8860316cca00902cce88bcdb5fedd", - "sha256:9dbab90c348c512e03f146e93a5e2610acec76df391043ecd46b6b775d5397e6", - "sha256:9ee0eef254e340cc11c379f797af3977992a7f2c176f1a658740c94bf677e13c", - "sha256:9fc17fdac8f1973850d42e51e8ba6149d93b1993ed6768a24f352f926dd3d587", - "sha256:a2787319dc69854acdfd6452e6a8ba8f929aeb20843c7f090e04159fc18e6245", - "sha256:b7c522292407fa04d8195032493aac937e253ad9ae524aab43b9d9d242571f03", - "sha256:bd312794f51e37dcf77f013d40650fe4fbb211dd55ef2863839c37480bd44369", - "sha256:c0d660a186e36c526366edf8a64391874fe53cf8b7039224137aee0163c046df", - "sha256:c4869141e20769b65d2d72686e7a7eb141ce9f3168106bed3e7dcced54eb2422", - "sha256:cc4057f692ac35bbe82a0a908d42ce3a281c9e913290fac37d7fa3bd01307dfb", - "sha256:cccf1e7806f12300e3a3b48f219e111000c2538483e85c869c35c1ae591e6ce9", - "sha256:ce208f80f398522e49d9db789065c8ad2cd37b21bd6b23d30053474b7416af11", - "sha256:d0565481dc196986c484a7fb13214fc6402201f7fb55c65fd215b3324962fe6c", - "sha256:d1b3366329c45a474b3bbc9b9c95d4c686e03f35da7fd12bc144626d1f2a7c04", - "sha256:d226e0d4b9192d95079a9a29c04dd81816b1ce8903b8c174a39224fe978547cb", - "sha256:d38b35f6eef4237b1d0d8e845fc1546dad85c55eba447e28c211da8c7ef9697c", - "sha256:d64c98277ea80e4484f1332ab107e8dfd173a7dcf1bdbf10a9cccc97aaab145f", - "sha256:d9de8427a5601799784eb0e7fa1b031aa64086ce04de29df775a8ca37eedac41", - "sha256:e6a15cf8f887d9f578dd49c6fb3a99d53e1d922fdd67a245a67488d77bf56eb2", - "sha256:e8c446882cbb3774cd78c738c9f58220606b702b7c1655f1423357dc51674054", - "sha256:e8d188ee39bd0ffe76603da887706e4e7b471f613625899ddf1e27867dc6a0d3", - "sha256:ef76535776c0708a85258f6dc51d36a2df12633c735f6d197ed7dfcaa7449b99", - "sha256:f6efca006a81e1197b925a7d7b16b8f61980697bb6746587aad8842865233218" - ], - "version": "==3.11.0" + "sha256:019ddf7ced8e42cc6c8c608927c799be8097237596c94ffe551f6ef70e55237e", + "sha256:047c325c4a96e7be7d11acf58639bcf71a81ca212d9c6590e3369bc28678647a", + "sha256:047cc2007b280672ddfdf2e7b862aad8d898f481f65bbc9067bfa4e420a019a9", + "sha256:061d59f525831c4051af0b6dbafa62b0b8b168d4ef5b6e3c46d0811b8499d100", + "sha256:082832a59da18efab4d9148cca396451bac99da9757f31767f706e828b5b8500", + "sha256:0a53a751d977ad02f1bd22ddb6288bb4816c4758f44a50225462aeeae9cbf6a0", + "sha256:1222025db539641071a1b67f6950f65a6342a39db5b454bf306abd6954f1ad8a", + "sha256:1580fad512c678b720784e5c9018621b1b3bd37fb5b1633e874738862d6435c7", + "sha256:202ea1d4edc8a5439fc179802d807b49e7e563207fea5610779e56674ac770c6", + "sha256:21d7b48567a1c80f9266e0ab61c1218a31279d911da345679188733e354f81cc", + "sha256:264843ce2af0640994a4331148ef5312989bc004678c457460758766c9b4decc", + "sha256:270a1f6a331eac3a393090af06df68297cb31a8b2df0bdcbd97dc613c5758e78", + "sha256:29a6840c2ac778010547cad5870f3db2e080ad7fad01197b07fff993c08692c8", + "sha256:3646c2286d889618d43e01d9810ac1fc17709d2b4dec61366df5edc8ba228b3e", + "sha256:36b9b98a39565a8f33803c81569442b35e749a72fb1aa7d0bcdb1a33052f8bcc", + "sha256:3ec8f8e106a1476659d8c020228b45614daabdbdb6c6454a843a1d4f77d13339", + "sha256:422069f2cebf58c9dd9e8040b4768f7be4f228c95bc4505e8fa8e7b4f7191ad8", + "sha256:44376a657717de8847d5d71a9305f3595c7e78c91ac77edbb87058d12ede87a6", + "sha256:45728e6aae3023afb5b2829586d1d2bfd9f0d71cfd7d3c924b71a5e9aef617a8", + "sha256:46792b71ab802d9caf1fc9d52e83399ef8e1a36e91eef4d827c06e36b8df2230", + "sha256:4942a5659ae927bb764a123a6409870ca5dd572d83b3bfb71412c9a191bbf792", + "sha256:4be4fe9d18523da98deeb0b554ac76e1dc1562ee879d62572b34dda8593efcc1", + "sha256:523804bd8fcb5255508052b50073a27c701b90a73ea46e29be46dad5fe01bde6", + "sha256:540dafd6f4a0590fc966465c726b80fa7c0804490c39786ef29236fe68c94401", + "sha256:5980509801cbd2942df31714d055d89863684b4de26829c349362e610a48694e", + "sha256:5ad7b96c27acd7e256b33f47cf3d23bd7dd902f9c033ae43f32ffcbc37bebafd", + "sha256:6122470dfa61d4909b75c98012c1577404ba4ab860d0095e0c6980560cb3711f", + "sha256:6175fd105da74a09adb38f93be96e1f64873294c906e5e722cbbc5bd10c44e3b", + "sha256:646d4d30c5aa7c0ddbfe9b990f0f77a88621024a21ad0b792bd9d58caa9611f0", + "sha256:6700e251c6396cc05d7460dc05ef8e19e60a7b53b62c007725b48e123aaa2b1c", + "sha256:6aac7e0e8de92f11a410eb68c24a2decbac6f094e82fd95d22546d0168e7a18b", + "sha256:6e7a6057481a644970e43475292e1c0af095ca39a20fe83781196bd6e6690a38", + "sha256:76579fcf77052b39796fe4a11818d1289dd48cffe15951b3403288fa163c29f6", + "sha256:7e69fa025a1db189443428f345fea5555d16413df6addc056e17bb8c9794b006", + "sha256:7f0c507e1f108790840d6c4b594019ebf595025c324c9f7e9c9b2b15b41f884e", + "sha256:813db97e9955b6b1b50b5cebd18cb148580603bb9b067ea4c5cc656b333bc906", + "sha256:82d5ded5834b6c92380847860eb28dcaf20b847a27cee5811c4aaceef87fd280", + "sha256:82f6e42ba40440a7e0a20bfe12465a3b62d65966a4c7ad1a21b36ffff88de6fe", + "sha256:8d669c720891781e7c82d412cad39f9730ef277e3957b48a3344dae47d3caa03", + "sha256:944ed467feb949e103555863fa934fb84216a096b0004ca364d3ddf9d18e2b9e", + "sha256:96c6aef7ffb0d37206c0342abb82d874fa8cdc344267277ec63f562b94335c22", + "sha256:9be785bd4e1ba0148fb00ca84e4dbfbd1c74df3af3a648559adc60b0782f34de", + "sha256:9d19843568df9d263dc92ae4cc2279879add8a26996473f9155590cac635b321", + "sha256:a118a1df7280ffab7fe0f3eab325868339ff1c4d5b8e0750db0f0a796da8f849", + "sha256:b4294ddf76452459433ecfa6a93258608b5e462c76ef15e4695ed5e2762f009f", + "sha256:b50af6701b4a5288b77fb4db44a363aa9485caf2c3e7a40c0373fd45e34440af", + "sha256:b875bb4b438931dce550e170bfb558597189b8d0160f4ac60f14a21955161699", + "sha256:b95d2c2829b5956bf54d9a22ffec911dea75abf0f0f7e0a8a57423434bfbde91", + "sha256:c046e09e886f4539f8626afba17fa8f2e6552731f9384e2827154e3e3b7fda4e", + "sha256:c1d1992bbdf363b22b5a9543ab7d7c6f27a1498826d50d91319b803ddcf1142e", + "sha256:c2b67881392a9e85aa108e75f62cdbe372d5a3f17ea5f8d3436dcf4662052f14", + "sha256:c6cf288c9e03195d8e12b72a6388b32f18a5e9c2545622417a963e428e1fe496", + "sha256:c812b6e53344e92f10f12235219fb769c491a4a87a02c9c3f93fe632e493bda8", + "sha256:cc421babc687dc52ce0fc19787b2404518ca749d9db59576100946ff886f38ed", + "sha256:ce53c00be204ec4428d3c1f3c478ae89d388efec575544c27f57b61e9fa4a7f2", + "sha256:ce9964c117cbe5cf6269f30a2b334d28675956e988b7dbd0b4f7370924afda2e", + "sha256:d6f82e86896a8db70e8ae8fa4b7556a0f188f1d8a6c53b2ba229889d55a59308", + "sha256:d9d3ae537f61011191b2fd6f8527b9f9f8a848b37d4c85a0f7bb28004c42b546", + "sha256:e565d1e4388765c135052717f15f9e0314f9d172062444c6b3fc0002e93ed04b", + "sha256:ed98683d8f01f1c46ef2d02469e04e9a8fe9a73a9741a4e6e66677a73b59bec8", + "sha256:ef18aa15b1aa18c42933deed5233b3284186e9ed85c25d2704ceff5099a3964c", + "sha256:fa741e9c805567239f845c7e9a016aff797f9bb02ff9bc8ccd2fbd9eafefedd4", + "sha256:fc4946acb6cdada08f60aca103b61334995523da65be5fe816ea8571c9967d46", + "sha256:fcc66d17a3363b7bd6d2655de8706e25a3cd1be2bd1b8e8d8a5c504a6ef893ae" + ], + "version": "==3.11.2" }, "python-dateutil": { "hashes": [ @@ -261,6 +280,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.15.0" }, "uvloop": { @@ -275,22 +295,23 @@ "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" ], - "index": "pypi", "markers": "sys_platform != 'win32'", "version": "==0.14.0" }, "virtualenv": { "hashes": [ - "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", - "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" + "sha256:54b05fc737ea9c9ee9f8340f579e5da5b09fb64fd010ab5757eb90268616907c", + "sha256:b7a8ec323ee02fb2312f098b6b4c9de99559b462775bc8fe3627a73706603c1b" ], - "version": "==20.1.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.2" }, "virtualenv-clone": { "hashes": [ "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27", "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.5.4" }, "yarl": { @@ -313,6 +334,7 @@ "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" ], + "markers": "python_version >= '3.5'", "version": "==1.5.1" } }, @@ -329,6 +351,7 @@ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], + "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "attrs": { @@ -336,6 +359,7 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "bandit": { @@ -359,6 +383,7 @@ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==7.1.2" }, "colorama": { @@ -382,21 +407,24 @@ "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], + "markers": "python_version >= '3.4'", "version": "==4.0.5" }, "gitpython": { "hashes": [ - "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b", - "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8" + "sha256:42dbefd8d9e2576c496ed0059f3103dcef7125b9ce16f9d5f9c834aed44a1dac", + "sha256:867ec3dfb126aac0f8296b19fb63b8c4a399f32b4b6fafe84c4b10af5fa9f7b5" ], - "version": "==3.1.11" + "markers": "python_version >= '3.4'", + "version": "==3.1.12" }, "isort": { "hashes": [ - "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7", - "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58" + "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e", + "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc" ], - "version": "==5.6.4" + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==5.7.0" }, "lazy-object-proxy": { "hashes": [ @@ -422,6 +450,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -443,6 +472,7 @@ "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9", "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00" ], + "markers": "python_version >= '2.6'", "version": "==5.5.1" }, "pycodestyle": { @@ -450,6 +480,7 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pyflakes": { @@ -457,6 +488,7 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pylint": { @@ -472,11 +504,13 @@ "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" @@ -485,57 +519,56 @@ }, "regex": { "hashes": [ - "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a", - "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f", - "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb", - "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5", - "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de", - "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c", - "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0", - "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c", - "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64", - "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53", - "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12", - "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740", - "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c", - "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd", - "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504", - "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427", - "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b", - "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e", - "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582", - "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0", - "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c", - "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9", - "sha256:9b6305295b6591e45f069d3553c54d50cc47629eb5c218aac99e0f7fafbf90a1", - "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0", - "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf", - "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898", - "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd", - "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d", - "sha256:bd904c0dec29bbd0769887a816657491721d5f545c29e30fd9d7a1a275dc80ab", - "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f", - "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e", - "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786", - "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b", - "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de", - "sha256:c454ad88e56e80e44f824ef8366bb7e4c3def12999151fd5c0ea76a18fe9aa3e", - "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789", - "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520", - "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa", - "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b", - "sha256:de7fd57765398d141949946c84f3590a68cf5887dac3fc52388df0639b01eda4", - "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625", - "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d", - "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26" - ], - "version": "==2020.10.28" + "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538", + "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4", + "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc", + "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa", + "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444", + "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1", + "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af", + "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8", + "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9", + "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88", + "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba", + "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364", + "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e", + "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7", + "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0", + "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31", + "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683", + "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee", + "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b", + "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884", + "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c", + "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e", + "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562", + "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85", + "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c", + "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6", + "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d", + "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b", + "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70", + "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b", + "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b", + "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f", + "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0", + "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5", + "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5", + "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f", + "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e", + "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512", + "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d", + "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917", + "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f" + ], + "version": "==2020.11.13" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.15.0" }, "smmap": { @@ -543,56 +576,59 @@ "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "stevedore": { "hashes": [ - "sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62", - "sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0" + "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee", + "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a" ], - "version": "==3.2.2" + "markers": "python_version >= '3.6'", + "version": "==3.3.0" }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==0.10.2" }, "typed-ast": { "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", - "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", - "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" - ], - "version": "==1.4.1" + "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", + "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", + "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", + "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", + "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", + "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", + "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", + "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", + "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", + "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", + "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", + "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", + "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", + "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", + "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", + "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", + "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", + "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", + "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", + "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", + "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", + "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", + "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", + "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", + "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", + "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", + "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", + "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", + "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", + "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" + ], + "version": "==1.4.2" }, "wrapt": { "hashes": [ diff --git a/bot.py b/bot.py index fdd348e269..eff4000aca 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -__version__ = "3.8.0-dev7" +__version__ = "3.8.0-dev8" import asyncio @@ -1588,7 +1588,7 @@ def main(): pass # check discord version - if discord.__version__ != "1.5.2": + if discord.__version__ != "1.6.0": logger.error( "Dependencies are not updated, run pipenv install. discord.py version expected 1.5.2, recieved %s", discord.__version__, diff --git a/core/thread.py b/core/thread.py index 6480160e8b..aed4275abc 100644 --- a/core/thread.py +++ b/core/thread.py @@ -898,7 +898,11 @@ async def send( if is_image_url(url, convert_size=False) ] images.extend(image_urls) - images.extend((str(i.image_url), f"{i.name} Sticker", True) for i in message.stickers) + images.extend(( + str(i.image_url) if isinstance(i.image_url, discord.Asset) else i.image_url, + f"{i.name} Sticker", + True + ) for i in message.stickers) embedded_image = False @@ -908,11 +912,16 @@ async def send( additional_count = 1 for url, filename, is_sticker in images: - if not prioritize_uploads or (is_image_url(url) and not embedded_image and filename): - embed.set_image(url=url) + if not prioritize_uploads or ((url is None or is_image_url(url)) and not embedded_image and filename): + if url is not None: + embed.set_image(url=url) if filename: if is_sticker: - embed.add_field(name=filename, value=f"\u200b") + if url is None: + description = 'Unable to retrieve sticker image' + else: + description = "\u200b" + embed.add_field(name=filename, value=description) else: embed.add_field(name="Image", value=f"[{filename}]({url})") embedded_image = True @@ -925,9 +934,12 @@ async def send( color = self.bot.recipient_color img_embed = discord.Embed(color=color) - img_embed.set_image(url=url) + + if url is None: + img_embed.set_image(url=url) + img_embed.url = url + img_embed.title = filename - img_embed.url = url 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)) diff --git a/discord.py-1.5.2.tar.gz b/discord.py-1.5.2.tar.gz deleted file mode 100644 index df0732cccac17a0d0b0f878dde24c204773b8d80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 649426 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/Q@d9Q`4-0o)w7tj34MYxDAsHT3*DWV&mBT3N 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