From a903f55954a0d9422e0e559988fe7192ed04a08f Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 15 Jan 2019 22:02:30 -0800 Subject: [PATCH 01/22] Use call_later for close --- core/thread.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/core/thread.py b/core/thread.py index 461500f8c1..ce60a4b69d 100644 --- a/core/thread.py +++ b/core/thread.py @@ -42,21 +42,18 @@ def ready(self, flag): if flag is True: self.ready_event.set() - async def _close_after(self, after, **kwargs): - await asyncio.sleep(after) - kwargs['scheduled'] = True - await self.close(**kwargs) + async def _close_after(self, closer, silent, delete_channel, message): + return self.bot.loop.create_task(self._close(closer, silent, delete_channel, message, True)) - async def close(self, *, closer, after=0, silent=False, delete_channel=True, message=None, scheduled=False): + async def close(self, *, closer, after=0, silent=False, delete_channel=True, message=None): '''Close a thread now or after a set time in seconds''' - if self.close_task is not None and not self.close_task.cancelled(): - if not scheduled or after > 0: - self.close_task.cancel() if after > 0: - self.close_task = asyncio.create_task(self._close_after(after, closer=closer, silent=silent, message=message)) - return + return await self.bot.loop.call_later(after, silent, delete_channel, message) + + return await self._close(closer, silent, delete_channel, message) + async def _close(self, closer, silent=False, delete_channel=True, message=None, scheduled=False): del self.manager.cache[self.id] if str(self.id) in self.bot.config.subscriptions: del self.bot.config.subscriptions[str(self.id)] @@ -73,7 +70,8 @@ async def close(self, *, closer, after=0, silent=False, delete_channel=True, mes }) if isinstance(log_data, str): - print(log_data) # errored somehow on server + print(log_data) # errored somehow on server + return if self.bot.selfhosted: log_url = f'{self.bot.config.log_url}/logs/{log_data["key"]}' From 561ac20140116d1b9b54f471e6f4264769c6739e Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 15 Jan 2019 22:07:38 -0800 Subject: [PATCH 02/22] Use call_later for close --- core/thread.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index ce60a4b69d..7d1d7599d0 100644 --- a/core/thread.py +++ b/core/thread.py @@ -23,8 +23,6 @@ def __init__(self, manager, recipient): self.recipient = recipient self.channel = None self.ready_event = asyncio.Event() - self.close_task = None - self.close_after = 0 # seconds def __repr__(self): return f'Thread(recipient="{self.recipient}", channel={self.channel.id})' From 668c4ba35d46db0b5a2ea08d975282f7c3f990bc Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 15 Jan 2019 22:22:43 -0800 Subject: [PATCH 03/22] Fix flawed statement --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 7d1d7599d0..cf4e2a3bed 100644 --- a/core/thread.py +++ b/core/thread.py @@ -47,7 +47,7 @@ async def close(self, *, closer, after=0, silent=False, delete_channel=True, mes '''Close a thread now or after a set time in seconds''' if after > 0: - return await self.bot.loop.call_later(after, silent, delete_channel, message) + return self.bot.loop.call_later(after, silent, delete_channel, message) return await self._close(closer, silent, delete_channel, message) From d16204ac7c777010a6c441d0e5f32586a8a0c04c Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 15 Jan 2019 22:25:13 -0800 Subject: [PATCH 04/22] Fix flawed statement --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index cf4e2a3bed..fca9428aea 100644 --- a/core/thread.py +++ b/core/thread.py @@ -40,7 +40,7 @@ def ready(self, flag): if flag is True: self.ready_event.set() - async def _close_after(self, closer, silent, delete_channel, message): + def _close_after(self, closer, silent, delete_channel, message): return self.bot.loop.create_task(self._close(closer, silent, delete_channel, message, True)) async def close(self, *, closer, after=0, silent=False, delete_channel=True, message=None): From e32fa3424992266661d80cb6ba1ec5bcb290a233 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 15 Jan 2019 22:33:28 -0800 Subject: [PATCH 05/22] Fix flawed statement --- core/thread.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index fca9428aea..94953891ec 100644 --- a/core/thread.py +++ b/core/thread.py @@ -23,6 +23,7 @@ def __init__(self, manager, recipient): self.recipient = recipient self.channel = None self.ready_event = asyncio.Event() + self.close_task = None def __repr__(self): return f'Thread(recipient="{self.recipient}", channel={self.channel.id})' @@ -46,8 +47,12 @@ def _close_after(self, closer, silent, delete_channel, message): async def close(self, *, closer, after=0, silent=False, delete_channel=True, message=None): '''Close a thread now or after a set time in seconds''' + if self.close_task is not None: + self.close_task.cancel() + if after > 0: - return self.bot.loop.call_later(after, silent, delete_channel, message) + self.close_task = self.bot.loop.call_later(after, self._close_after, silent, delete_channel, message) + return return await self._close(closer, silent, delete_channel, message) From 272a0aafd7562686a71210f31ae897591cd39aea Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 15 Jan 2019 22:35:03 -0800 Subject: [PATCH 06/22] Fix flawed statement --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 94953891ec..ce2f95e9a5 100644 --- a/core/thread.py +++ b/core/thread.py @@ -51,7 +51,7 @@ async def close(self, *, closer, after=0, silent=False, delete_channel=True, mes self.close_task.cancel() if after > 0: - self.close_task = self.bot.loop.call_later(after, self._close_after, silent, delete_channel, message) + self.close_task = self.bot.loop.call_later(after, self._close_after, closer, silent, delete_channel, message) return return await self._close(closer, silent, delete_channel, message) From 75865eb6f8b010395c31b69368f663f9fb886c43 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 15 Jan 2019 23:04:35 -0800 Subject: [PATCH 07/22] Style/Convention for thread.py --- core/thread.py | 165 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 112 insertions(+), 53 deletions(-) diff --git a/core/thread.py b/core/thread.py index ce2f95e9a5..2c6b071d62 100644 --- a/core/thread.py +++ b/core/thread.py @@ -26,7 +26,8 @@ def __init__(self, manager, recipient): self.close_task = None def __repr__(self): - return f'Thread(recipient="{self.recipient}", channel={self.channel.id})' + return f'Thread(recipient="{self.recipient}", ' \ + f'channel={self.channel.id})' def wait_until_ready(self): """Blocks execution until the thread is fully set up.""" @@ -42,28 +43,35 @@ def ready(self, flag): self.ready_event.set() 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, *, closer, after=0, silent=False, delete_channel=True, message=None): - '''Close a thread now or after a set time in seconds''' + async def close(self, *, closer, after=0, silent=False, + delete_channel=True, message=None): + """Close a thread now or after a set time in seconds""" if self.close_task is not None: self.close_task.cancel() if after > 0: - self.close_task = self.bot.loop.call_later(after, self._close_after, closer, silent, delete_channel, message) + self.close_task = self.bot.loop.call_later( + after, self._close_after, closer, + silent, delete_channel, message) return return 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] if str(self.id) in self.bot.config.subscriptions: del self.bot.config.subscriptions[str(self.id)] # Logging log_data = await self.bot.modmail_api.post_log(self.channel.id, { - 'open': False, 'closed_at': str(datetime.datetime.utcnow()), 'closer': { + 'open': False, + 'closed_at': str(datetime.datetime.utcnow()), + 'closer': { 'id': str(closer.id), 'name': closer.name, 'discriminator': closer.discriminator, @@ -79,9 +87,10 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, if self.bot.selfhosted: log_url = f'{self.bot.config.log_url}/logs/{log_data["key"]}' else: - log_url = f"https://logs.modmail.tk/{log_data['user_id']}/{log_data['key']}" + log_url = f"https://logs.modmail.tk/" \ + f"{log_data['user_id']}/{log_data['key']}" - user = self.recipient.mention if self.recipient else f'`{self.id}`' + user = self.recipient.mention if self.recipient else str(self.id) if log_data['messages']: msg = str(log_data['messages'][0]['content']) @@ -105,9 +114,9 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, # Thread closed message - em = discord.Embed(title='Thread Closed') - em.description = message or f'{closer.mention} has closed this modmail thread.' - em.color = discord.Color.red() + em = discord.Embed(title='Thread Closed', color=discord.Color.red()) + em.description = message or \ + f'{closer.mention} has closed this modmail thread.' if not silent and self.recipient is not None: tasks.append(self.recipient.send(embed=em)) @@ -117,7 +126,8 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, await asyncio.gather(*tasks) - async def _edit_thread_message(self, channel, message_id, message): + @staticmethod + async def _edit_thread_message(channel, message_id, message): async for msg in channel.history(): if not msg.embeds: continue @@ -125,7 +135,8 @@ async def _edit_thread_message(self, channel, message_id, message): if embed and embed.author and embed.author.url: if str(message_id) == str(embed.author.url).split('/')[-1]: if ' - (Edited)' not in embed.footer.text: - embed.set_footer(text=embed.footer.text + ' - (Edited)') + embed.set_footer( + text=embed.footer.text + ' - (Edited)') embed.description = message await msg.edit(embed=embed) break @@ -140,31 +151,47 @@ async def reply(self, message): if not message.content and not message.attachments: raise commands.UserInputError 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='This user shares no servers with me and is thus unreachable.')) + return await message.channel.send( + embed=discord.Embed( + color=discord.Color.red(), + description='This user shares no servers with ' + 'me and is thus unreachable.')) tasks = [ - self.send(message, self.channel, from_mod=True), # in thread channel - self.send(message, self.recipient, from_mod=True) # to user + # in thread channel + self.send(message, self.channel, from_mod=True), + # to user + self.send(message, self.recipient, from_mod=True) ] if self.close_task is not None and not self.close_task.cancelled(): - self.close_task.cancel() # cancel closing if a thread message is sent. - tasks.append(self.channel.send(embed=discord.Embed(color=discord.Color.red(), description='Scheduled close has been cancelled.'))) + # cancel closing if a thread message is sent. + self.close_task.cancel() + tasks.append(self.channel.send( + embed=discord.Embed(color=discord.Color.red(), + description='Scheduled close has ' + 'been cancelled.'))) await asyncio.gather(*tasks) - async def send(self, message, destination=None, from_mod=False, delete_message=True): + async def send(self, message, destination=None, from_mod=False): if self.close_task is not None and not self.close_task.cancelled(): - self.close_task.cancel() # cancel closing if a thread message is sent. - await self.channel.send(embed=discord.Embed(color=discord.Color.red(), description='Scheduled close has been cancelled.')) + # cancel closing if a thread message is sent. + self.close_task.cancel() + await 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() destination = destination or self.channel if from_mod and not isinstance(destination, discord.User): - asyncio.create_task(self.bot.modmail_api.append_log(message)) + self.bot.loop.create_task( + self.bot.modmail_api.append_log(message)) elif not from_mod: - asyncio.create_task(self.bot.modmail_api.append_log(message, destination.id)) + self.bot.loop.create_task( + self.bot.modmail_api.append_log(message, destination.id)) author = message.author @@ -173,10 +200,16 @@ async def send(self, message, destination=None, from_mod=False, delete_message=T timestamp=message.created_at ) - em.set_author(name=str(author), icon_url=author.avatar_url, url=message.jump_url) # store message id in hidden url + # store message id in hidden url + em.set_author(name=str(author), + icon_url=author.avatar_url, + url=message.jump_url) image_types = ['.png', '.jpg', '.gif', '.jpeg', '.webp'] - is_image_url = lambda u, _: any(urlparse(u.lower()).path.endswith(x) for x in image_types) + + def is_image_url(u, _): + return any(urlparse(u.lower()).path.endswith(x) + for x in image_types) delete_message = not bool(message.attachments) @@ -185,7 +218,8 @@ async def send(self, message, destination=None, from_mod=False, delete_message=T images = [x for x in attachments if is_image_url(*x)] 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)] + image_links = [(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) @@ -196,19 +230,24 @@ async def send(self, message, destination=None, from_mod=False, delete_message=T additional_count = 1 for att in images: - if is_image_url(*att) and not embedded_image and att[1] if prioritize_uploads else True: + if is_image_url(*att) and not embedded_image and att[1] \ + if prioritize_uploads else True: em.set_image(url=att[0]) embedded_image = True elif att[1] is not None: link = f'[{att[1]}]({att[0]})' - em.add_field(name=f'Additional Image upload ({additional_count})', value=link, inline=False) + em.add_field( + name=f'Additional Image upload ({additional_count})', + value=link, + inline=False + ) additional_count += 1 file_upload_count = 1 - for att in attachments: - em.add_field(name=f'File upload ({file_upload_count})', value=f'[{att[1]}]({att[0]})') + em.add_field(name=f'File upload ({file_upload_count})', + value=f'[{att[1]}]({att[0]})') file_upload_count += 1 if from_mod: @@ -257,7 +296,8 @@ def __init__(self, bot): async def populate_cache(self): for channel in self.bot.modmail_guild.text_channels: - if not self.bot.using_multiple_server_setup and channel.category != self.bot.main_category: + if not self.bot.using_multiple_server_setup and \ + channel.category != self.bot.main_category: continue await self.find(channel=channel) @@ -274,6 +314,8 @@ async def find(self, *, recipient=None, channel=None): """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) + + thread = None try: thread = self.cache[recipient.id] except KeyError: @@ -281,9 +323,7 @@ async def find(self, *, recipient=None, channel=None): self.bot.modmail_guild.text_channels, topic=f'User ID: {recipient.id}' ) - if not channel: - thread = None - else: + if channel: self.cache[recipient.id] = thread = Thread(self, recipient) thread.channel = channel thread.ready = True @@ -294,19 +334,22 @@ 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 - searching channel history for genesis embed and extracts user_id fron that. + searching channel history for genesis embed and + extracts user_id from that. """ user_id = None if channel.topic and 'User ID: ' in channel.topic: user_id = int(re.findall(r'\d+', channel.topic)[0]) - # BUG: When discord fails to create channel topic. search through message history + # BUG: When discord fails to create channel topic. + # search through message history elif channel.topic is None: async for message in channel.history(limit=50): if message.embeds: em = message.embeds[0] - matches = re.findall(r'User ID: (\d+)', str(em.footer.text)) + # TODO: use re.search instead + matches = re.findall(r'User ID: (\d+)', em.footer.text) if matches: user_id = int(matches[0]) break @@ -329,12 +372,15 @@ async def create(self, recipient, *, creator=None): em = discord.Embed( title='Thread created!', - description=self.bot.config.get('thread_creation_response', 'The moderation team will get back to you as soon as possible!'), + description=self.bot.config.get( + 'thread_creation_response', + 'The moderation team will get back to you as soon as possible!' + ), color=discord.Color.green() ) if creator is None: - asyncio.create_task(recipient.send(embed=em)) + self.bot.loop.create_task(recipient.send(embed=em)) self.cache[recipient.id] = thread = Thread(self, recipient) @@ -346,16 +392,20 @@ async def create(self, recipient, *, creator=None): thread.channel = channel log_url, log_data = await asyncio.gather( - self.bot.modmail_api.get_log_url(recipient, channel, creator or recipient), + self.bot.modmail_api.get_log_url(recipient, channel, + creator or recipient), self.bot.modmail_api.get_user_logs(recipient.id) # self.get_dominant_color(recipient.avatar_url) ) log_count = sum(1 for log in log_data if not log['open']) - info_embed = self._format_info_embed(recipient, creator, log_url, log_count, discord.Color.green()) + info_embed = self._format_info_embed(recipient, creator, + log_url, log_count, + discord.Color.green()) topic = f'User ID: {recipient.id}' - mention = self.bot.config.get('mention', '@here') if not creator else None + mention = self.bot.config.get('mention', '@here') \ + if not creator else None _, msg = await asyncio.gather( channel.edit(topic=topic), @@ -369,7 +419,8 @@ async def create(self, recipient, *, creator=None): return thread async def find_or_create(self, recipient): - return await self.find(recipient=recipient) or await self.create(recipient) + return await self.find(recipient=recipient) or \ + await self.create(recipient) @staticmethod def valid_image_url(url): @@ -398,7 +449,7 @@ async def get_dominant_color(self, url=None, quality=10): async with self.bot.session.get(url) as resp: image = await resp.read() color = await self._do_get_dc(image, quality) - except Exception: + except: traceback.print_exc() return discord.Color.blurple() else: @@ -411,7 +462,8 @@ def _format_channel_name(self, author): 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]: + 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 @@ -422,21 +474,26 @@ def _format_info_embed(self, user, creator, log_url, log_count, dc): member = self.bot.guild.get_member(user.id) avi = user.avatar_url time = datetime.datetime.utcnow() - desc = f'{creator.mention} has created a thread with {user.mention}' if creator else f'{user.mention} has started a thread' + desc = f'{creator.mention} has created a thread with {user.mention}' \ + if creator else f'{user.mention} has started a thread' key = log_url.split('/')[-1] desc = f'{desc} [`{key}`]({log_url})' + role_names = '' if member: seperate_server = self.bot.guild != self.bot.modmail_guild roles = sorted(member.roles, key=lambda c: c.position) if seperate_server: - rolenames = ', '.join(r.name for r in roles if r.name != "@everyone") + role_names = ', '.join(r.name for r in roles + if r.name != "@everyone") else: - rolenames = ' '.join(r.mention for r in roles if r.name != "@everyone") + role_names = ' '.join(r.mention for r in roles + if r.name != "@everyone") em = discord.Embed(colour=dc, description=desc, timestamp=time) - days = lambda d: (' day ago.' if d == '1' else ' days ago.') + def days(d): + return ' day ago.' if d == '1' else ' days ago.' created = str((time - user.created_at).days) # em.add_field(name='Mention', value=user.mention) @@ -453,9 +510,11 @@ def _format_info_embed(self, user, creator, log_url, log_count, dc): em.add_field(name='Joined', value=joined + days(joined)) if member.nick: em.add_field(name='Nickname', value=member.nick, inline=True) - if rolenames: - em.add_field(name='Roles', value=rolenames, inline=False) + if role_names: + em.add_field(name='Roles', value=role_names, inline=False) else: - em.set_footer(text=footer + ' | Note: this member is not part of this server.') + em.set_footer( + text=f'{footer} | Note: this member' + f' is not part of this server.') return em From fe88794ae23b82809801077fcd2df27f6cbbdae9 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Tue, 15 Jan 2019 23:31:19 -0800 Subject: [PATCH 08/22] Removed reference as it's not necessary --- core/config.py | 11 +++++++---- core/thread.py | 7 ++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/core/config.py b/core/config.py index 7f2bba4ad9..79a0d0ed9d 100644 --- a/core/config.py +++ b/core/config.py @@ -7,8 +7,10 @@ class ConfigManager: """Class that manages a cached configuration""" allowed_to_change_in_command = { - 'activity_message', 'activity_type', 'log_channel_id', 'mention', 'disable_autoupdates', 'prefix', - 'main_category_id', 'sent_emoji', 'blocked_emoji', 'thread_creation_response', 'twitch_url' + 'activity_message', 'activity_type', 'log_channel_id', + 'mention', 'disable_autoupdates', 'prefix', + 'main_category_id', 'sent_emoji', 'blocked_emoji', + 'thread_creation_response', 'twitch_url' } internal_keys = { @@ -20,7 +22,7 @@ class ConfigManager: 'mongo_uri', 'github_access_token', 'log_url' } - valid_keys = allowed_to_change_in_command.union(internal_keys).union(protected_keys) + valid_keys = allowed_to_change_in_command | internal_keys | protected_keys def __init__(self, bot): self.bot = bot @@ -47,7 +49,8 @@ def populate_cache(self): pass finally: data.update(os.environ) - data = {k.lower(): v for k, v in data.items() if k.lower() in self.valid_keys} + data = {k.lower(): v for k, v in data.items() + if k.lower() in self.valid_keys} self.cache = data async def update(self, data=None): diff --git a/core/thread.py b/core/thread.py index 2c6b071d62..1a89254e95 100644 --- a/core/thread.py +++ b/core/thread.py @@ -51,6 +51,7 @@ async def close(self, *, closer, after=0, silent=False, """Close a thread now or after a set time in seconds""" if self.close_task is not None: + # restarts the after timer self.close_task.cancel() if after > 0: @@ -164,7 +165,7 @@ async def reply(self, message): self.send(message, self.recipient, from_mod=True) ] - if self.close_task is not None and not self.close_task.cancelled(): + if self.close_task is not None: # cancel closing if a thread message is sent. self.close_task.cancel() tasks.append(self.channel.send( @@ -175,7 +176,7 @@ async def reply(self, message): await asyncio.gather(*tasks) async def send(self, message, destination=None, from_mod=False): - if self.close_task is not None and not self.close_task.cancelled(): + if self.close_task is not None: # cancel closing if a thread message is sent. self.close_task.cancel() await self.channel.send(embed=discord.Embed( @@ -282,7 +283,7 @@ def get_notifications(self): if key in config['notification_squad']: mentions.extend(config['notification_squad'][key]) del config['notification_squad'][key] - asyncio.create_task(config.update()) + self.bot.loop.create_task(config.update()) return ' '.join(mentions) From 7a2fde7d62a62d34f44b6f9d6c9dee2b4fc07d62 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 10:17:30 -0800 Subject: [PATCH 09/22] Added the use of config vars Known issue: channel will still close when the bot is down --- bot.py | 16 +++++++++++++++- core/config.py | 4 +++- core/thread.py | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index c630e44366..04af2c9f4f 100644 --- a/bot.py +++ b/bot.py @@ -203,7 +203,21 @@ async def on_ready(self): else: await self.threads.populate_cache() - + closures = self.config.get('closures', {}) + for recipient_id, items in closures.items(): + after = (datetime.datetime.fromisoformat(items['time']) - + datetime.datetime.utcnow()).total_seconds() + if after < 0: + after = 0 + thread = await self.threads.find( + recipient=self.get_user(recipient_id)) + + # TODO: Retrieve messages/replies when bot is down, from history? + await thread.close(closer=self.get_user(items['closer_id']), + after=after, + silent=items['silent'], + delete_channel=items['delete_channel'], + message=items['message']) async def process_modmail(self, message): """Processes messages sent to the bot.""" diff --git a/core/config.py b/core/config.py index 29b7356495..f4348baf14 100644 --- a/core/config.py +++ b/core/config.py @@ -14,7 +14,9 @@ class ConfigManager: } internal_keys = { - 'snippets', 'aliases', 'blocked', 'notification_squad', 'subscriptions' + 'snippets', 'aliases', 'blocked', + 'notification_squad', 'subscriptions', + 'closures' } protected_keys = { diff --git a/core/thread.py b/core/thread.py index 1a89254e95..5c81776368 100644 --- a/core/thread.py +++ b/core/thread.py @@ -55,6 +55,22 @@ async def close(self, *, closer, after=0, silent=False, self.close_task.cancel() if after > 0: + # TODO: Add somewhere to clean up broken closures + # (when channel is already deleted) + closures = self.bot.config.get('closures', {}) + now = datetime.datetime.utcnow() + items = { + # 'initiation_time': now.isoformat(), + 'time': (now + datetime.timedelta(seconds=after)).isoformat(), + 'closer_id': closer.id, + 'silent': silent, + 'delete_channel': delete_channel, + 'message': message + } + closures[self.id] = items + self.bot.config['closure'] = closures + await self.bot.config.update() + self.close_task = self.bot.loop.call_later( after, self._close_after, closer, silent, delete_channel, message) @@ -326,6 +342,7 @@ async def find(self, *, recipient=None, channel=None): ) if channel: self.cache[recipient.id] = thread = Thread(self, recipient) + # TODO: Fix this: thread.channel = channel thread.ready = True finally: From ef58811eb60a979ce6acc0c02e43634cdaaa0ff7 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 10:25:57 -0800 Subject: [PATCH 10/22] Attempt to fix a problem where it cancels scheduled close 3 times --- core/thread.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/thread.py b/core/thread.py index 5c81776368..be1577fcfc 100644 --- a/core/thread.py +++ b/core/thread.py @@ -53,6 +53,7 @@ async def close(self, *, closer, after=0, silent=False, if self.close_task is not None: # restarts the after timer self.close_task.cancel() + self.close_task = None if after > 0: # TODO: Add somewhere to clean up broken closures @@ -184,6 +185,7 @@ async def reply(self, message): if self.close_task is not None: # cancel closing if a thread message is sent. self.close_task.cancel() + self.close_task = None tasks.append(self.channel.send( embed=discord.Embed(color=discord.Color.red(), description='Scheduled close has ' @@ -195,6 +197,7 @@ async def send(self, message, destination=None, from_mod=False): if self.close_task is not None: # cancel closing if a thread message is sent. self.close_task.cancel() + self.close_task = None await self.channel.send(embed=discord.Embed( color=discord.Color.red(), description='Scheduled close has been cancelled.')) From a06cf97662957a441252f3913d755f8931d50a8b Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 10:35:45 -0800 Subject: [PATCH 11/22] Added debugging print statement --- bot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot.py b/bot.py index 04af2c9f4f..998f22ba9b 100644 --- a/bot.py +++ b/bot.py @@ -204,6 +204,7 @@ async def on_ready(self): await self.threads.populate_cache() closures = self.config.get('closures', {}) + print(closures) # for debugging for recipient_id, items in closures.items(): after = (datetime.datetime.fromisoformat(items['time']) - datetime.datetime.utcnow()).total_seconds() From 581246e4d8b6630395df98c6d69e004042955dea Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 10:43:30 -0800 Subject: [PATCH 12/22] Attempt to fix missing config --- core/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/config.py b/core/config.py index f4348baf14..898ff8de40 100644 --- a/core/config.py +++ b/core/config.py @@ -42,7 +42,8 @@ def populate_cache(self): 'aliases': {}, 'blocked': {}, 'notification_squad': {}, - 'subscriptions': {} + 'subscriptions': {}, + 'closures': {} } try: From 1a43eebc8c693e11582a4fcf0f6237c6a81b0c80 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 10:45:19 -0800 Subject: [PATCH 13/22] Attempt to fix missing config (found typo) --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index be1577fcfc..358b65a692 100644 --- a/core/thread.py +++ b/core/thread.py @@ -69,7 +69,7 @@ async def close(self, *, closer, after=0, silent=False, 'message': message } closures[self.id] = items - self.bot.config['closure'] = closures + self.bot.config['closures'] = closures await self.bot.config.update() self.close_task = self.bot.loop.call_later( From 8300316513045271dad0b3ec94d1774fe5b69542 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 10:51:28 -0800 Subject: [PATCH 14/22] Perhaps refreshing will help? --- bot.py | 1 + core/config.py | 1 - core/thread.py | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 998f22ba9b..8b6459bd42 100644 --- a/bot.py +++ b/bot.py @@ -202,6 +202,7 @@ async def on_ready(self): print(Fore.RED + Style.BRIGHT + 'WARNING - The GUILD_ID provided does not exist!' + Style.RESET_ALL) else: await self.threads.populate_cache() + await self.config.update() closures = self.config.get('closures', {}) print(closures) # for debugging diff --git a/core/config.py b/core/config.py index 898ff8de40..8baa3ed9c8 100644 --- a/core/config.py +++ b/core/config.py @@ -43,7 +43,6 @@ def populate_cache(self): 'blocked': {}, 'notification_squad': {}, 'subscriptions': {}, - 'closures': {} } try: diff --git a/core/thread.py b/core/thread.py index 358b65a692..a94acbb76f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -58,6 +58,7 @@ async def close(self, *, closer, after=0, silent=False, if after > 0: # TODO: Add somewhere to clean up broken closures # (when channel is already deleted) + await self.bot.config.update() closures = self.bot.config.get('closures', {}) now = datetime.datetime.utcnow() items = { From 28e59c5d8c6c4ad00031df0124306c06c8aad869 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 10:58:01 -0800 Subject: [PATCH 15/22] Added debugging print statement --- bot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 8b6459bd42..2dd52ffd69 100644 --- a/bot.py +++ b/bot.py @@ -211,9 +211,11 @@ async def on_ready(self): datetime.datetime.utcnow()).total_seconds() if after < 0: after = 0 + recipient = self.get_user(recipient_id) + print(recipient) thread = await self.threads.find( - recipient=self.get_user(recipient_id)) - + recipient=recipient) + print(thread) # TODO: Retrieve messages/replies when bot is down, from history? await thread.close(closer=self.get_user(items['closer_id']), after=after, From 9c4de114ff866bf0675937483c858c65a41e65d9 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 11:03:28 -0800 Subject: [PATCH 16/22] Attempted to fix problem & clears closure when it's closed --- bot.py | 2 +- core/thread.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bot.py b/bot.py index 2dd52ffd69..987cde1412 100644 --- a/bot.py +++ b/bot.py @@ -211,7 +211,7 @@ async def on_ready(self): datetime.datetime.utcnow()).total_seconds() if after < 0: after = 0 - recipient = self.get_user(recipient_id) + recipient = self.get_user(int(recipient_id)) print(recipient) thread = await self.threads.find( recipient=recipient) diff --git a/core/thread.py b/core/thread.py index a94acbb76f..29ed57d978 100644 --- a/core/thread.py +++ b/core/thread.py @@ -69,7 +69,7 @@ async def close(self, *, closer, after=0, silent=False, 'delete_channel': delete_channel, 'message': message } - closures[self.id] = items + closures[str(self.id)] = items self.bot.config['closures'] = closures await self.bot.config.update() @@ -83,6 +83,12 @@ async def close(self, *, closer, after=0, silent=False, async def _close(self, closer, silent=False, delete_channel=True, message=None, scheduled=False): del self.manager.cache[self.id] + + closures = self.bot.config.get('closures', {}) + closures.pop(str(self.id)) + self.bot.config['closures'] = closures + await self.bot.config.update() + if str(self.id) in self.bot.config.subscriptions: del self.bot.config.subscriptions[str(self.id)] From a63d1b8242ca8e4088a2450ab410ce33e7c417e6 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 11:12:14 -0800 Subject: [PATCH 17/22] Added handling for when the channel's deleted when the bot's down --- bot.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 987cde1412..ecb7c32bdb 100644 --- a/bot.py +++ b/bot.py @@ -204,7 +204,7 @@ async def on_ready(self): await self.threads.populate_cache() await self.config.update() - closures = self.config.get('closures', {}) + closures = self.config.get('closures', {}).copy() print(closures) # for debugging for recipient_id, items in closures.items(): after = (datetime.datetime.fromisoformat(items['time']) - @@ -212,10 +212,18 @@ async def on_ready(self): if after < 0: after = 0 recipient = self.get_user(int(recipient_id)) - print(recipient) + thread = await self.threads.find( recipient=recipient) - print(thread) + + if not thread: + # If the recipient is gone or channel is deleted + closures = self.config.get('closures', {}) + closures.pop(str(recipient_id)) + self.config['closures'] = closures + await self.config.update() + continue + # TODO: Retrieve messages/replies when bot is down, from history? await thread.close(closer=self.get_user(items['closer_id']), after=after, From fdc6428456b14da94c7ddb922e6a1e7e1b7c968f Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 16:39:27 -0800 Subject: [PATCH 18/22] Removed debugging print statement --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index ecb7c32bdb..ee15f39983 100644 --- a/bot.py +++ b/bot.py @@ -205,7 +205,7 @@ async def on_ready(self): await self.config.update() closures = self.config.get('closures', {}).copy() - print(closures) # for debugging + for recipient_id, items in closures.items(): after = (datetime.datetime.fromisoformat(items['time']) - datetime.datetime.utcnow()).total_seconds() From ee7e1bd594bbf3502471ebf8424965c4dbeab834 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 17:52:05 -0800 Subject: [PATCH 19/22] Attempt to address PR issues --- bot.py | 6 ++---- core/config.py | 1 + core/thread.py | 11 ++++------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/bot.py b/bot.py index ee15f39983..c5b39a915a 100644 --- a/bot.py +++ b/bot.py @@ -204,7 +204,7 @@ async def on_ready(self): await self.threads.populate_cache() await self.config.update() - closures = self.config.get('closures', {}).copy() + closures = self.config.closures.copy() for recipient_id, items in closures.items(): after = (datetime.datetime.fromisoformat(items['time']) - @@ -218,9 +218,7 @@ async def on_ready(self): if not thread: # If the recipient is gone or channel is deleted - closures = self.config.get('closures', {}) - closures.pop(str(recipient_id)) - self.config['closures'] = closures + self.config.closures.pop(str(recipient_id)) await self.config.update() continue diff --git a/core/config.py b/core/config.py index 8baa3ed9c8..cfb00934b8 100644 --- a/core/config.py +++ b/core/config.py @@ -43,6 +43,7 @@ def populate_cache(self): 'blocked': {}, 'notification_squad': {}, 'subscriptions': {}, + 'closures': {}, } try: diff --git a/core/thread.py b/core/thread.py index 29ed57d978..628d1df438 100644 --- a/core/thread.py +++ b/core/thread.py @@ -59,7 +59,6 @@ async def close(self, *, closer, after=0, silent=False, # TODO: Add somewhere to clean up broken closures # (when channel is already deleted) await self.bot.config.update() - closures = self.bot.config.get('closures', {}) now = datetime.datetime.utcnow() items = { # 'initiation_time': now.isoformat(), @@ -69,8 +68,7 @@ async def close(self, *, closer, after=0, silent=False, 'delete_channel': delete_channel, 'message': message } - closures[str(self.id)] = items - self.bot.config['closures'] = closures + self.bot.config.closures[str(self.id)] = items await self.bot.config.update() self.close_task = self.bot.loop.call_later( @@ -84,10 +82,9 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, scheduled=False): del self.manager.cache[self.id] - closures = self.bot.config.get('closures', {}) - closures.pop(str(self.id)) - self.bot.config['closures'] = closures - await self.bot.config.update() + if scheduled: + self.bot.config.closures.pop(str(self.id), None) + await self.bot.config.update() if str(self.id) in self.bot.config.subscriptions: del self.bot.config.subscriptions[str(self.id)] From 81aba61d935329fbe3bfc8d2c32d9baf75ee2792 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 18:36:29 -0800 Subject: [PATCH 20/22] Should address a bug and some issues --- bot.py | 3 ++- cogs/modmail.py | 10 +++++----- core/thread.py | 27 +++++++++++++++------------ 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/bot.py b/bot.py index c5b39a915a..a139dca0e0 100644 --- a/bot.py +++ b/bot.py @@ -222,7 +222,8 @@ async def on_ready(self): await self.config.update() continue - # TODO: Retrieve messages/replies when bot is down, from history? + # TODO: Low priority, + # Retrieve messages/replies when bot is down, from history? await thread.close(closer=self.get_user(items['closer_id']), after=after, silent=items['silent'], diff --git a/cogs/modmail.py b/cogs/modmail.py index 5ae3eb1534..56aa90aeff 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -174,11 +174,11 @@ async def _close(self, ctx, *, after: UserFriendlyTime=None): silent = str(message).lower() in {'silent', 'silently'} cancel = str(message).lower() == 'cancel' - if cancel and thread.close_task is not None and not thread.close_task.cancelled(): - thread.close_task.cancel() - await ctx.send(embed=discord.Embed(color=discord.Color.red(), description='Scheduled close has been cancelled.')) - return - elif cancel: + if cancel: + if thread.close_task is not None: + await thread.cancel_closure() + await ctx.send(embed=discord.Embed(color=discord.Color.red(), description='Scheduled close has been cancelled.')) + return return await ctx.send(embed=discord.Embed(color=discord.Color.red(), description='This thread has not already been scheduled to close.')) if after and after.dt > now: diff --git a/core/thread.py b/core/thread.py index 628d1df438..37969b6099 100644 --- a/core/thread.py +++ b/core/thread.py @@ -50,10 +50,8 @@ async def close(self, *, closer, after=0, silent=False, delete_channel=True, message=None): """Close a thread now or after a set time in seconds""" - if self.close_task is not None: - # restarts the after timer - self.close_task.cancel() - self.close_task = None + # restarts the after timer + await self.cancel_closure() if after > 0: # TODO: Add somewhere to clean up broken closures @@ -82,9 +80,7 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None, scheduled=False): del self.manager.cache[self.id] - if scheduled: - self.bot.config.closures.pop(str(self.id), None) - await self.bot.config.update() + await self.cancel_closure() if str(self.id) in self.bot.config.subscriptions: del self.bot.config.subscriptions[str(self.id)] @@ -112,7 +108,7 @@ async def _close(self, closer, silent=False, delete_channel=True, log_url = f"https://logs.modmail.tk/" \ f"{log_data['user_id']}/{log_data['key']}" - user = self.recipient.mention if self.recipient else str(self.id) + user = self.recipient.mention if self.recipient else f'`{self.id}`' if log_data['messages']: msg = str(log_data['messages'][0]['content']) @@ -148,6 +144,15 @@ async def _close(self, closer, silent=False, delete_channel=True, await asyncio.gather(*tasks) + async def cancel_closure(self): + if self.close_task is not None: + self.close_task.cancel() + self.close_task = None + + to_update = self.bot.config.closures.pop(str(self.id), None) + if to_update is not None: + await self.bot.config.update() + @staticmethod async def _edit_thread_message(channel, message_id, message): async for msg in channel.history(): @@ -188,8 +193,7 @@ async def reply(self, message): if self.close_task is not None: # cancel closing if a thread message is sent. - self.close_task.cancel() - self.close_task = None + await self.cancel_closure() tasks.append(self.channel.send( embed=discord.Embed(color=discord.Color.red(), description='Scheduled close has ' @@ -200,8 +204,7 @@ async def reply(self, message): async def send(self, message, destination=None, from_mod=False): if self.close_task is not None: # cancel closing if a thread message is sent. - self.close_task.cancel() - self.close_task = None + await self.cancel_closure() await self.channel.send(embed=discord.Embed( color=discord.Color.red(), description='Scheduled close has been cancelled.')) From 6013ba8d99ff3428eed7e54f013c057bf606c2b4 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 18:59:20 -0800 Subject: [PATCH 21/22] Address a preformance concern --- bot.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bot.py b/bot.py index a139dca0e0..2682af36ad 100644 --- a/bot.py +++ b/bot.py @@ -224,11 +224,15 @@ async def on_ready(self): # TODO: Low priority, # Retrieve messages/replies when bot is down, from history? - await thread.close(closer=self.get_user(items['closer_id']), - after=after, - silent=items['silent'], - delete_channel=items['delete_channel'], - message=items['message']) + self.loop.create_task( + thread.close( + closer=self.get_user(items['closer_id']), + after=after, + silent=items['silent'], + delete_channel=items['delete_channel'], + message=items['message'] + ) + ) async def process_modmail(self, message): """Processes messages sent to the bot.""" From 03560c930f4c769c11ac96b55ce59cf7f083f462 Mon Sep 17 00:00:00 2001 From: taaku251 <45324516+Taaku18@users.noreply.github.com> Date: Wed, 16 Jan 2019 20:17:35 -0800 Subject: [PATCH 22/22] Bump version to v1.5.0 --- CHANGELOG.md | 19 +++++++++++++++++++ bot.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3569f5e1e6..007e75f1a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ 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.5.0 + +Non-Breaking Changes: + +### 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. +[PR #135](https://github.com/kyb3r/modmail/pull/135) + + +### Changed + - Created a new internal config var: `closures`. + - Store closure details into `closures` when the scheduled time isn't "now". + - Loaded upon bot restart. + - Deleted when a thread is closed. + - Use `call_later()` instead of `sleep()` for scheduling. + # v2.4.5 ### Fixed diff --git a/bot.py b/bot.py index 2682af36ad..8df5db1e0e 100644 --- a/bot.py +++ b/bot.py @@ -22,7 +22,7 @@ SOFTWARE. """ -__version__ = '2.4.5' +__version__ = '2.5.0' import asyncio import textwrap