From 815cfcb0315bb88a099c566d7c5faa6aecc11c71 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 2 Oct 2020 12:01:17 -0400 Subject: [PATCH 01/30] Add member role WIP --- timerole/timerole.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/timerole/timerole.py b/timerole/timerole.py index 7484267..45d147a 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -27,10 +27,15 @@ class Timerole(Cog): self.bot = bot self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True) default_global = {} - default_guild = {"announce": None, "roles": {}} + default_guild = {"announce": None} + default_memberrole = {"had_role": False, "check_again_time": None} self.config.register_global(**default_global) self.config.register_guild(**default_guild) + + self.config.init_custom("MemberRole", 2) + self.config.register_custom("MemberRole", **default_memberrole) + self.updating = asyncio.create_task(self.check_hour()) async def red_delete_data_for_user(self, **kwargs): From 44035b78f7b5e4cb6f31518173af50e33c85e969 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 2 Oct 2020 15:13:08 -0400 Subject: [PATCH 02/30] Timerole rewrite --- timerole/timerole.py | 236 ++++++++++++++++++++++++++++--------------- 1 file changed, 156 insertions(+), 80 deletions(-) diff --git a/timerole/timerole.py b/timerole/timerole.py index 45d147a..ac23bd7 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -19,6 +19,15 @@ async def sleep_till_next_hour(): await asyncio.sleep((next_hour - datetime.utcnow()).seconds) +async def announce_to_channel(channel, remove_results, title): + if channel is not None and remove_results: + await channel.send(title) + for page in pagify(remove_results, shorten_by=50): + await channel.send(page) + elif remove_results: # Channel is None, log the results + log.info(remove_results) + + class Timerole(Cog): """Add roles to users based on time on server""" @@ -27,7 +36,7 @@ class Timerole(Cog): self.bot = bot self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True) default_global = {} - default_guild = {"announce": None} + default_guild = {"announce": None, "reapply": True, "roles": {}} default_memberrole = {"had_role": False, "check_again_time": None} self.config.register_global(**default_global) @@ -89,9 +98,7 @@ class Timerole(Cog): await self.config.guild(guild).roles.set_raw(role.id, value=to_set) await ctx.maybe_send_embed( - "Time Role for {0} set to {1} days and {2} hours until added".format( - role.name, days, hours - ) + f"Time Role for {role.name} set to {days} days and {hours} hours until added" ) @timerole.command() @@ -119,9 +126,7 @@ class Timerole(Cog): await self.config.guild(guild).roles.set_raw(role.id, value=to_set) await ctx.maybe_send_embed( - "Time Role for {0} set to {1} days and {2} hours until removed".format( - role.name, days, hours - ) + f"Time Role for {role.name} set to {days} days and {hours} hours until removed" ) @timerole.command() @@ -130,7 +135,15 @@ class Timerole(Cog): guild = ctx.guild await self.config.guild(guild).announce.set(channel.id) - await ctx.send("Announce channel set to {0}".format(channel.mention)) + await ctx.send(f"Announce channel set to {channel.mention}") + + @timerole.command() + async def reapply(self, ctx: commands.Context): + """Toggle reapplying roles if the member loses it somehow. Defaults to True""" + guild = ctx.guild + current_setting = await self.config.guild(guild).reapply() + await self.config.guild(guild).reapply.set(not current_setting) + await ctx.send(f"Reapplying roles is now set to: {not current_setting}") @timerole.command() async def delrole(self, ctx: commands.Context, role: discord.Role): @@ -138,7 +151,7 @@ class Timerole(Cog): guild = ctx.guild await self.config.guild(guild).roles.set_raw(role.id, value=None) - await ctx.send("{0} will no longer be applied".format(role.name)) + await ctx.send(f"{role.name} will no longer be applied") @timerole.command() async def list(self, ctx: commands.Context): @@ -158,89 +171,152 @@ class Timerole(Cog): str(discord.utils.get(guild.roles, id=int(new_id))) for new_id in r_data["required"] ] - out += "{} | {} days | requires: {}\n".format(str(role), r_data["days"], r_roles) + out += f"{role} | {r_data['days']} days | requires: {r_roles}\n" await ctx.maybe_send_embed(out) async def timerole_update(self): - async for guild in AsyncIter(self.bot.guilds): - addlist = [] - removelist = [] + utcnow = datetime.utcnow() + all_guilds = await self.config.all_guilds() - role_dict = await self.config.guild(guild).roles() - if not any(role_data for role_data in role_dict.values()): # No roles + all_mrs = await self.config.custom("MemberRole").all() + + for guild in self.bot.guilds: + guild_id = str(guild.id) + if guild_id not in all_guilds: continue - async for member in AsyncIter(guild.members): - has_roles = [r.id for r in member.roles] - - add_roles = [ - int(rID) - for rID, r_data in role_dict.items() - if r_data is not None and not r_data["remove"] - ] - remove_roles = [ - int(rID) - for rID, r_data in role_dict.items() - if r_data is not None and r_data["remove"] - ] - - check_add_roles = set(add_roles) - set(has_roles) - check_remove_roles = set(remove_roles) & set(has_roles) - - await self.check_required_and_date( - addlist, check_add_roles, has_roles, member, role_dict - ) - await self.check_required_and_date( - removelist, check_remove_roles, has_roles, member, role_dict - ) + add_results = "" + remove_results = "" + reapply = all_guilds[guild_id]["reapply"] + role_dict = all_guilds[guild_id]["roles"] + if not any(role_data for role_data in role_dict.values()): # No roles + continue + + async for member in AsyncIter(guild.members, steps=100): + addlist = [] + removelist = [] + + for role_id, role_data in role_dict.items(): + mr_dict = all_mrs[str(member.id)][role_id] + + # Stop if they've had the role and reapplying is disabled + if not reapply and mr_dict["had_role"]: + continue + + # Stop if the check_again_time hasn't passed yet + if ( + mr_dict["check_again_time"] is not None + and datetime.fromisoformat(mr_dict["check_again_time"]) >= utcnow + ): + continue + member: discord.Member + has_roles = set(r.id for r in member.roles) + + # Stop if they currently have the role (and mark they had it) + if role_id in has_roles: + if not mr_dict["had_role"]: + await self.config.custom( + "MemberRole", member.id, role_id + ).had_role.set(True) + continue + + # Stop if they don't have all the required roles + if "required" in role_data and not set(role_data["required"]) & has_roles: + # Doesn't have required role + continue + + check_time = member.joined_at + timedelta( + days=role_data["days"], + hours=role_data.get("hours", 0), + ) + + # Check if enough time has passed to get the role and save the check_again_time + if check_time <= utcnow: + await self.config.custom( + "MemberRole", member.id, role_id + ).check_again_time.set(check_time.isoformat()) + continue + + if role_data["remove"]: + removelist.append(role_id) + else: + addlist.append(role_id) + + # Done iterating through roles, now add or remove the roles + addlist = [discord.utils.get(guild.roles, id=role_id) for role_id in addlist] + removelist = [discord.utils.get(guild.roles, id=role_id) for role_id in removelist] + + if addlist: + try: + await member.add_roles(*addlist, reason="Timerole", atomic=False) + except (discord.Forbidden, discord.NotFound) as e: + log.exception("Failed Adding Roles") + add_results += f"{member.display_name} : **(Failed Adding Roles)**\n" + else: + add_results += "\n".join( + f"{member.display_name} : {role.name}" for role in addlist + ) + + if removelist: + try: + await member.remove_roles(*removelist, reason="Timerole", atomic=False) + except (discord.Forbidden, discord.NotFound) as e: + log.exception("Failed Removing Roles") + remove_results += f"{member.display_name} : **(Failed Removing Roles)**\n" + else: + remove_results += "\n".join( + f"{member.display_name} : {role.name}" for role in removelist + ) + + # Done iterating through members, now maybe announce to the guild channel = await self.config.guild(guild).announce() if channel is not None: channel = guild.get_channel(channel) - title = "**These members have received the following roles**\n" - await self.announce_roles(title, addlist, channel, guild, to_add=True) + await announce_to_channel(channel, remove_results, title) title = "**These members have lost the following roles**\n" - await self.announce_roles(title, removelist, channel, guild, to_add=False) - - async def announce_roles(self, title, role_list, channel, guild, to_add: True): - results = "" - async for member, role_id in AsyncIter(role_list): - role = discord.utils.get(guild.roles, id=role_id) - try: - if to_add: - await member.add_roles(role, reason="Timerole") - else: - await member.remove_roles(role, reason="Timerole") - except (discord.Forbidden, discord.NotFound) as e: - results += "{} : {} **(Failed)**\n".format(member.display_name, role.name) - else: - results += "{} : {}\n".format(member.display_name, role.name) - if channel is not None and results: - await channel.send(title) - for page in pagify(results, shorten_by=50): - await channel.send(page) - elif results: # Channel is None, log the results - log.info(results) - - async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict): - async for role_id in AsyncIter(check_roles): - # Check for required role - if "required" in role_dict[str(role_id)]: - if not set(role_dict[str(role_id)]["required"]) & set(has_roles): - # Doesn't have required role - continue - - if ( - member.joined_at - + timedelta( - days=role_dict[str(role_id)]["days"], - hours=role_dict[str(role_id)].get("hours", 0), - ) - <= datetime.today() - ): - # Qualifies - role_list.append((member, role_id)) + await announce_to_channel(channel, remove_results, title) + # End + + # async def announce_roles(self, title, role_list, channel, guild, to_add: True): + # results = "" + # async for member, role_id in AsyncIter(role_list): + # role = discord.utils.get(guild.roles, id=role_id) + # try: + # if to_add: + # await member.add_roles(role, reason="Timerole") + # else: + # await member.remove_roles(role, reason="Timerole") + # except (discord.Forbidden, discord.NotFound) as e: + # results += f"{member.display_name} : {role.name} **(Failed)**\n" + # else: + # results += f"{member.display_name} : {role.name}\n" + # if channel is not None and results: + # await channel.send(title) + # for page in pagify(results, shorten_by=50): + # await channel.send(page) + # elif results: # Channel is None, log the results + # log.info(results) + + # async def check_required_and_date(self, role_list, check_roles, has_roles, member, role_dict): + # async for role_id in AsyncIter(check_roles): + # # Check for required role + # if "required" in role_dict[str(role_id)]: + # if not set(role_dict[str(role_id)]["required"]) & set(has_roles): + # # Doesn't have required role + # continue + # + # if ( + # member.joined_at + # + timedelta( + # days=role_dict[str(role_id)]["days"], + # hours=role_dict[str(role_id)].get("hours", 0), + # ) + # <= datetime.utcnow() + # ): + # # Qualifies + # role_list.append((member, role_id)) async def check_hour(self): await sleep_till_next_hour() From b141accbd96ac861eec9eda4fcefa3e69a909b7c Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 2 Oct 2020 15:32:55 -0400 Subject: [PATCH 03/30] Timerole rewrite WIP --- timerole/timerole.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/timerole/timerole.py b/timerole/timerole.py index ac23bd7..af6f4b4 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -178,11 +178,14 @@ class Timerole(Cog): utcnow = datetime.utcnow() all_guilds = await self.config.all_guilds() - all_mrs = await self.config.custom("MemberRole").all() + # all_mrs = await self.config.custom("MemberRole").all() + + log.debug(f"Begin timerole update") for guild in self.bot.guilds: - guild_id = str(guild.id) + guild_id = guild.id if guild_id not in all_guilds: + log.debug(f"Guild has no configured settings: {guild}") continue add_results = "" @@ -191,6 +194,7 @@ class Timerole(Cog): role_dict = all_guilds[guild_id]["roles"] if not any(role_data for role_data in role_dict.values()): # No roles + log.debug(f"No roles are configured for guild: {guild}") continue async for member in AsyncIter(guild.members, steps=100): @@ -198,7 +202,7 @@ class Timerole(Cog): removelist = [] for role_id, role_data in role_dict.items(): - mr_dict = all_mrs[str(member.id)][role_id] + mr_dict = await self.config.custom("MemberRole", member.id, role_id).all() # Stop if they've had the role and reapplying is disabled if not reapply and mr_dict["had_role"]: @@ -222,7 +226,9 @@ class Timerole(Cog): continue # Stop if they don't have all the required roles - if "required" in role_data and not set(role_data["required"]) & has_roles: + if role_data is None or ( + "required" in role_data and not set(role_data["required"]) & has_roles + ): # Doesn't have required role continue From c63a4923e7c947a795aac332838b31001ee2422c Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 5 Oct 2020 11:21:57 -0400 Subject: [PATCH 04/30] Actually do the logic right --- timerole/timerole.py | 61 ++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/timerole/timerole.py b/timerole/timerole.py index af6f4b4..c079596 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -19,13 +19,13 @@ async def sleep_till_next_hour(): await asyncio.sleep((next_hour - datetime.utcnow()).seconds) -async def announce_to_channel(channel, remove_results, title): - if channel is not None and remove_results: +async def announce_to_channel(channel, results, title): + if channel is not None and results: await channel.send(title) - for page in pagify(remove_results, shorten_by=50): + for page in pagify(results, shorten_by=50): await channel.send(page) - elif remove_results: # Channel is None, log the results - log.info(remove_results) + elif results: # Channel is None, log the results + log.info(results) class Timerole(Cog): @@ -197,6 +197,9 @@ class Timerole(Cog): log.debug(f"No roles are configured for guild: {guild}") continue + # all_mr = await self.config.all_custom("MemberRole") + # log.debug(f"{all_mr=}") + async for member in AsyncIter(guild.members, steps=100): addlist = [] removelist = [] @@ -206,6 +209,7 @@ class Timerole(Cog): # Stop if they've had the role and reapplying is disabled if not reapply and mr_dict["had_role"]: + log.debug(f"{member.display_name} - Not reapplying") continue # Stop if the check_again_time hasn't passed yet @@ -213,6 +217,7 @@ class Timerole(Cog): mr_dict["check_again_time"] is not None and datetime.fromisoformat(mr_dict["check_again_time"]) >= utcnow ): + log.debug(f"{member.display_name} - Not time to check again yet") continue member: discord.Member has_roles = set(r.id for r in member.roles) @@ -223,6 +228,9 @@ class Timerole(Cog): await self.config.custom( "MemberRole", member.id, role_id ).had_role.set(True) + log.debug( + f"{member.display_name} - Already has the role, maybe applying `had_role`" + ) continue # Stop if they don't have all the required roles @@ -230,6 +238,7 @@ class Timerole(Cog): "required" in role_data and not set(role_data["required"]) & has_roles ): # Doesn't have required role + # log.debug(f"{member.display_name} - Missing required roles") continue check_time = member.joined_at + timedelta( @@ -238,10 +247,14 @@ class Timerole(Cog): ) # Check if enough time has passed to get the role and save the check_again_time - if check_time <= utcnow: + if check_time >= utcnow: await self.config.custom( "MemberRole", member.id, role_id ).check_again_time.set(check_time.isoformat()) + log.debug( + f"{member.display_name} - Not enough time has passed to qualify for the role\n" + f"Waiting until {check_time}" + ) continue if role_data["remove"]: @@ -250,39 +263,55 @@ class Timerole(Cog): addlist.append(role_id) # Done iterating through roles, now add or remove the roles - addlist = [discord.utils.get(guild.roles, id=role_id) for role_id in addlist] - removelist = [discord.utils.get(guild.roles, id=role_id) for role_id in removelist] + if not addlist and not removelist: + continue + + log.debug(f"{addlist=}\n{removelist=}") + add_roles = [ + discord.utils.get(guild.roles, id=int(role_id)) for role_id in addlist + ] + remove_roles = [ + discord.utils.get(guild.roles, id=int(role_id)) for role_id in removelist + ] + + if None in add_roles or None in remove_roles: + log.info( + f"Timerole ran into an error with the roles in: {add_roles + remove_roles}" + ) if addlist: try: - await member.add_roles(*addlist, reason="Timerole", atomic=False) + await member.add_roles(*add_roles, reason="Timerole", atomic=False) except (discord.Forbidden, discord.NotFound) as e: log.exception("Failed Adding Roles") add_results += f"{member.display_name} : **(Failed Adding Roles)**\n" else: add_results += "\n".join( - f"{member.display_name} : {role.name}" for role in addlist + f"{member.display_name} : {role.name}" for role in add_roles ) if removelist: try: - await member.remove_roles(*removelist, reason="Timerole", atomic=False) + await member.remove_roles(*remove_roles, reason="Timerole", atomic=False) except (discord.Forbidden, discord.NotFound) as e: log.exception("Failed Removing Roles") remove_results += f"{member.display_name} : **(Failed Removing Roles)**\n" else: remove_results += "\n".join( - f"{member.display_name} : {role.name}" for role in removelist + f"{member.display_name} : {role.name}" for role in remove_roles ) # Done iterating through members, now maybe announce to the guild channel = await self.config.guild(guild).announce() if channel is not None: channel = guild.get_channel(channel) - title = "**These members have received the following roles**\n" - await announce_to_channel(channel, remove_results, title) - title = "**These members have lost the following roles**\n" - await announce_to_channel(channel, remove_results, title) + + if add_results: + title = "**These members have received the following roles**\n" + await announce_to_channel(channel, add_results, title) + if remove_results: + title = "**These members have lost the following roles**\n" + await announce_to_channel(channel, remove_results, title) # End # async def announce_roles(self, title, role_list, channel, guild, to_add: True): From 10767da507f02c0b273e60d9ce10606a3001837a Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 5 Oct 2020 13:00:58 -0400 Subject: [PATCH 05/30] Clear the reapply logic if the role is deleted --- timerole/timerole.py | 69 +++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/timerole/timerole.py b/timerole/timerole.py index c079596..fdfe584 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -1,6 +1,7 @@ import asyncio import logging from datetime import datetime, timedelta +from typing import Optional import discord from redbot.core import Config, checks, commands @@ -37,13 +38,13 @@ class Timerole(Cog): self.config = Config.get_conf(self, identifier=9811198108111121, force_registration=True) default_global = {} default_guild = {"announce": None, "reapply": True, "roles": {}} - default_memberrole = {"had_role": False, "check_again_time": None} + default_rolemember = {"had_role": False, "check_again_time": None} self.config.register_global(**default_global) self.config.register_guild(**default_guild) - self.config.init_custom("MemberRole", 2) - self.config.register_custom("MemberRole", **default_memberrole) + self.config.init_custom("RoleMember", 2) + self.config.register_custom("RoleMember", **default_rolemember) self.updating = asyncio.create_task(self.check_hour()) @@ -63,10 +64,12 @@ class Timerole(Cog): Useful for troubleshooting the initial setup """ - + pre_run = datetime.utcnow() async with ctx.typing(): await self.timerole_update() await ctx.tick() + after_run = datetime.utcnow() + await ctx.maybe_send_embed(f"Took {after_run-pre_run} seconds") @commands.group() @checks.mod_or_permissions(administrator=True) @@ -130,12 +133,15 @@ class Timerole(Cog): ) @timerole.command() - async def channel(self, ctx: commands.Context, channel: discord.TextChannel): + async def channel(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None): """Sets the announce channel for role adds""" guild = ctx.guild - - await self.config.guild(guild).announce.set(channel.id) - await ctx.send(f"Announce channel set to {channel.mention}") + if channel is None: + await self.config.guild(guild).announce.clear() + await ctx.maybe_send_embed(f"Announce channel has been cleared") + else: + await self.config.guild(guild).announce.set(channel.id) + await ctx.send(f"Announce channel set to {channel.mention}") @timerole.command() async def reapply(self, ctx: commands.Context): @@ -143,7 +149,7 @@ class Timerole(Cog): guild = ctx.guild current_setting = await self.config.guild(guild).reapply() await self.config.guild(guild).reapply.set(not current_setting) - await ctx.send(f"Reapplying roles is now set to: {not current_setting}") + await ctx.maybe_send_embed(f"Reapplying roles is now set to: {not current_setting}") @timerole.command() async def delrole(self, ctx: commands.Context, role: discord.Role): @@ -151,7 +157,8 @@ class Timerole(Cog): guild = ctx.guild await self.config.guild(guild).roles.set_raw(role.id, value=None) - await ctx.send(f"{role.name} will no longer be applied") + await self.config.custom("RoleMember", role.id).clear() + await ctx.maybe_send_embed(f"{role.name} will no longer be applied") @timerole.command() async def list(self, ctx: commands.Context): @@ -178,7 +185,7 @@ class Timerole(Cog): utcnow = datetime.utcnow() all_guilds = await self.config.all_guilds() - # all_mrs = await self.config.custom("MemberRole").all() + # all_mrs = await self.config.custom("RoleMember").all() log.debug(f"Begin timerole update") @@ -197,15 +204,19 @@ class Timerole(Cog): log.debug(f"No roles are configured for guild: {guild}") continue - # all_mr = await self.config.all_custom("MemberRole") + # all_mr = await self.config.all_custom("RoleMember") # log.debug(f"{all_mr=}") - async for member in AsyncIter(guild.members, steps=100): + async for member in AsyncIter(guild.members, steps=10): addlist = [] removelist = [] for role_id, role_data in role_dict.items(): - mr_dict = await self.config.custom("MemberRole", member.id, role_id).all() + # Skip non-configured roles + if not role_data: + continue + + mr_dict = await self.config.custom("RoleMember", role_id, member.id).all() # Stop if they've had the role and reapplying is disabled if not reapply and mr_dict["had_role"]: @@ -222,23 +233,21 @@ class Timerole(Cog): member: discord.Member has_roles = set(r.id for r in member.roles) - # Stop if they currently have the role (and mark they had it) - if role_id in has_roles: + # Stop if they currently have or don't have the role, and mark had_role + if (role_id in has_roles and not role_data["remove"]) or ( + role_id not in has_roles and role_data["remove"] + ): if not mr_dict["had_role"]: await self.config.custom( - "MemberRole", member.id, role_id + "RoleMember", role_id, member.id ).had_role.set(True) - log.debug( - f"{member.display_name} - Already has the role, maybe applying `had_role`" - ) + log.debug(f"{member.display_name} - applying had_role") continue # Stop if they don't have all the required roles if role_data is None or ( "required" in role_data and not set(role_data["required"]) & has_roles ): - # Doesn't have required role - # log.debug(f"{member.display_name} - Missing required roles") continue check_time = member.joined_at + timedelta( @@ -249,7 +258,7 @@ class Timerole(Cog): # Check if enough time has passed to get the role and save the check_again_time if check_time >= utcnow: await self.config.custom( - "MemberRole", member.id, role_id + "RoleMember", role_id, member.id ).check_again_time.set(check_time.isoformat()) log.debug( f"{member.display_name} - Not enough time has passed to qualify for the role\n" @@ -266,7 +275,7 @@ class Timerole(Cog): if not addlist and not removelist: continue - log.debug(f"{addlist=}\n{removelist=}") + # log.debug(f"{addlist=}\n{removelist=}") add_roles = [ discord.utils.get(guild.roles, id=int(role_id)) for role_id in addlist ] @@ -286,9 +295,13 @@ class Timerole(Cog): log.exception("Failed Adding Roles") add_results += f"{member.display_name} : **(Failed Adding Roles)**\n" else: - add_results += "\n".join( + add_results += " \n".join( f"{member.display_name} : {role.name}" for role in add_roles ) + for role_id in addlist: + await self.config.custom( + "RoleMember", role_id, member.id + ).had_role.set(True) if removelist: try: @@ -297,9 +310,13 @@ class Timerole(Cog): log.exception("Failed Removing Roles") remove_results += f"{member.display_name} : **(Failed Removing Roles)**\n" else: - remove_results += "\n".join( + remove_results += " \n".join( f"{member.display_name} : {role.name}" for role in remove_roles ) + for role_id in removelist: + await self.config.custom( + "RoleMember", role_id, member.id + ).had_role.set(True) # Done iterating through members, now maybe announce to the guild channel = await self.config.guild(guild).announce() From 4c1cd869303e1407e8d651f11aeffade3b680c52 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 5 Oct 2020 13:06:53 -0400 Subject: [PATCH 06/30] Better time keepking doesn't include tick --- timerole/timerole.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/timerole/timerole.py b/timerole/timerole.py index fdfe584..1b56b69 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -64,11 +64,12 @@ class Timerole(Cog): Useful for troubleshooting the initial setup """ - pre_run = datetime.utcnow() async with ctx.typing(): + pre_run = datetime.utcnow() await self.timerole_update() + after_run = datetime.utcnow() await ctx.tick() - after_run = datetime.utcnow() + await ctx.maybe_send_embed(f"Took {after_run-pre_run} seconds") @commands.group() @@ -187,7 +188,7 @@ class Timerole(Cog): # all_mrs = await self.config.custom("RoleMember").all() - log.debug(f"Begin timerole update") + # log.debug(f"Begin timerole update") for guild in self.bot.guilds: guild_id = guild.id From a92c373b497402cff00265e98195eacbc3d3f83f Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 7 Oct 2020 15:13:53 -0400 Subject: [PATCH 07/30] New gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7a224ea..3732d09 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ venv/ v-data/ database.sqlite3 /venv3.4/ +/.venv/ From 1e8d1efb57ffcf764cbfb63b583d760e41b22ec3 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 9 Oct 2020 13:54:05 -0400 Subject: [PATCH 08/30] Respect embed color --- lseen/lseen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lseen/lseen.py b/lseen/lseen.py index 3348b65..0aff69c 100644 --- a/lseen/lseen.py +++ b/lseen/lseen.py @@ -83,7 +83,7 @@ class LastSeen(Cog): # description="{} was last seen at this date and time".format(member.display_name), # timestamp=last_seen) - embed = discord.Embed(timestamp=last_seen) + embed = discord.Embed(timestamp=last_seen, color=self.bot.get_embed_color(ctx)) await ctx.send(embed=embed) @commands.Cog.listener() From 5611f7abe7e58f6998011cb52455affae543f2aa Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 9 Oct 2020 13:55:14 -0400 Subject: [PATCH 09/30] Don't commit before testing --- lseen/lseen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lseen/lseen.py b/lseen/lseen.py index 0aff69c..69bcf87 100644 --- a/lseen/lseen.py +++ b/lseen/lseen.py @@ -83,7 +83,7 @@ class LastSeen(Cog): # description="{} was last seen at this date and time".format(member.display_name), # timestamp=last_seen) - embed = discord.Embed(timestamp=last_seen, color=self.bot.get_embed_color(ctx)) + embed = discord.Embed(timestamp=last_seen, color=await self.bot.get_embed_color(ctx)) await ctx.send(embed=embed) @commands.Cog.listener() From d71e3afb86573fecd7463787a439d3d77a816ba6 Mon Sep 17 00:00:00 2001 From: bobloy Date: Sun, 11 Oct 2020 14:01:12 -0400 Subject: [PATCH 10/30] Fix re-adding roles bug --- timerole/timerole.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/timerole/timerole.py b/timerole/timerole.py index 1b56b69..714bcc8 100644 --- a/timerole/timerole.py +++ b/timerole/timerole.py @@ -235,8 +235,8 @@ class Timerole(Cog): has_roles = set(r.id for r in member.roles) # Stop if they currently have or don't have the role, and mark had_role - if (role_id in has_roles and not role_data["remove"]) or ( - role_id not in has_roles and role_data["remove"] + if (int(role_id) in has_roles and not role_data["remove"]) or ( + int(role_id) not in has_roles and role_data["remove"] ): if not mr_dict["had_role"]: await self.config.custom( From 5ddafff59f62abb3211aaeec8dbd4b6da844b4de Mon Sep 17 00:00:00 2001 From: bobloy Date: Sat, 17 Oct 2020 12:24:43 -0400 Subject: [PATCH 11/30] Add ability to delete autobanked guildbanks --- stealemoji/stealemoji.py | 49 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/stealemoji/stealemoji.py b/stealemoji/stealemoji.py index 492ef70..a492527 100644 --- a/stealemoji/stealemoji.py +++ b/stealemoji/stealemoji.py @@ -50,6 +50,7 @@ class StealEmoji(Cog): default_global = { "stolemoji": {}, "guildbanks": [], + "autobanked_guilds": [], "on": False, "notify": 0, "autobank": False, @@ -145,11 +146,54 @@ class StealEmoji(Cog): await ctx.maybe_send_embed("AutoBanking is now " + str(not curr_setting)) + @checks.is_owner() + @commands.guild_only() + @stealemoji.command(name="deleteserver", aliases=["deleteguild"]) + async def se_deleteserver(self, ctx: commands.Context, guild_id=None): + """Delete servers the bot is the owner of. + + Useful for auto-generated guildbanks.""" + if guild_id is None: + guild = ctx.guild + else: + guild = await self.bot.get_guild(guild_id) + + if guild is None: + await ctx.maybe_send_embed("Failed to get guild, cancelling") + return + guild: discord.Guild + await ctx.maybe_send_embed( + f"Will attempt to delete {guild.name} ({guild.id})\n" f"Okay to continue? (yes/no)" + ) + + def check(m): + return m.author == ctx.author and m.channel == ctx.channel + + try: + answer = await self.bot.wait_for("message", timeout=120, check=check) + except asyncio.TimeoutError: + await ctx.send("Timed out, canceling") + return + + if answer.content.upper() not in ["Y", "YES"]: + await ctx.maybe_send_embed("Cancelling") + return + try: + await guild.delete() + except discord.Forbidden: + log.exception("No permission to delete. I'm probably not the guild owner") + await ctx.maybe_send_embed("No permission to delete. I'm probably not the guild owner") + except discord.HTTPException: + log.exception("Unexpected error when deleting guild") + await ctx.maybe_send_embed("Unexpected error when deleting guild") + else: + await self.bot.send_to_owners(f"Guild {guild.name} deleted") + @checks.is_owner() @commands.guild_only() @stealemoji.command(name="bank") async def se_bank(self, ctx): - """Add current server as emoji bank""" + """Add or remove current server as emoji bank""" def check(m): return ( @@ -235,6 +279,9 @@ class StealEmoji(Cog): return async with self.config.guildbanks() as guildbanks: guildbanks.append(guildbank.id) + # Track generated guilds for easier deletion + async with self.config.autobanked_guilds() as autobanked_guilds: + autobanked_guilds.append(guildbank.id) await asyncio.sleep(2) From 675e9b82c88cc58813d0da5f7ccdd85acd165c78 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 21 Oct 2020 14:22:40 -0400 Subject: [PATCH 12/30] Update README.md Fix typo in cog name and unnecessary <> --- chatter/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chatter/README.md b/chatter/README.md index 8ef6734..d92ad2b 100644 --- a/chatter/README.md +++ b/chatter/README.md @@ -95,7 +95,8 @@ pip install --no-deps "chatterbot>=1.1" #### Step 1: Built-in Downloader ``` -[p]cog install Chatter +[p]repo add Fox https://github.com/bobloy/Fox-V3 +[p]cog install Fox chatter ``` #### Step 2: Install Requirements From 6e2c62d897683ded7799a12241ba9ec653daf77f Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 21 Oct 2020 14:47:39 -0400 Subject: [PATCH 13/30] Add appropriate checks --- chatter/chat.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/chatter/chat.py b/chatter/chat.py index ef75bb8..d63a664 100644 --- a/chatter/chat.py +++ b/chatter/chat.py @@ -10,7 +10,7 @@ from chatterbot import ChatBot from chatterbot.comparisons import JaccardSimilarity, LevenshteinDistance, SpacySimilarity from chatterbot.response_selection import get_random_response from chatterbot.trainers import ChatterBotCorpusTrainer, ListTrainer, UbuntuCorpusTrainer -from redbot.core import Config, commands +from redbot.core import Config, checks, commands from redbot.core.commands import Cog from redbot.core.data_manager import cog_data_path from redbot.core.utils.predicates import MessagePredicate @@ -191,6 +191,7 @@ class Chatter(Cog): if ctx.invoked_subcommand is None: pass + @checks.admin() @chatter.command(name="channel") async def chatter_channel( self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None @@ -210,6 +211,7 @@ class Chatter(Cog): await self.config.guild(ctx.guild).chatchannel.set(channel.id) await ctx.maybe_send_embed(f"Chat channel is now {channel.mention}") + @checks.is_owner() @chatter.command(name="cleardata") async def chatter_cleardata(self, ctx: commands.Context, confirm: bool = False): """ @@ -242,6 +244,7 @@ class Chatter(Cog): await ctx.tick() + @checks.is_owner() @chatter.command(name="algorithm", aliases=["algo"]) async def chatter_algorithm( self, ctx: commands.Context, algo_number: int, threshold: float = None @@ -275,6 +278,7 @@ class Chatter(Cog): await ctx.tick() + @checks.is_owner() @chatter.command(name="model") async def chatter_model(self, ctx: commands.Context, model_number: int): """ @@ -312,6 +316,7 @@ class Chatter(Cog): f"Model has been switched to {self.tagger_language.ISO_639_1}" ) + @checks.is_owner() @chatter.command(name="minutes") async def minutes(self, ctx: commands.Context, minutes: int): """ @@ -327,6 +332,7 @@ class Chatter(Cog): await ctx.tick() + @checks.is_owner() @chatter.command(name="age") async def age(self, ctx: commands.Context, days: int): """ @@ -341,6 +347,7 @@ class Chatter(Cog): await self.config.guild(ctx.guild).days.set(days) await ctx.tick() + @checks.is_owner() @chatter.command(name="backup") async def backup(self, ctx, backupname): """ @@ -362,6 +369,7 @@ class Chatter(Cog): else: await ctx.maybe_send_embed("Error occurred :(") + @checks.is_owner() @chatter.command(name="trainubuntu") async def chatter_train_ubuntu(self, ctx: commands.Context, confirmation: bool = False): """ @@ -383,6 +391,7 @@ class Chatter(Cog): else: await ctx.send("Error occurred :(") + @checks.is_owner() @chatter.command(name="trainenglish") async def chatter_train_english(self, ctx: commands.Context): """ @@ -396,6 +405,7 @@ class Chatter(Cog): else: await ctx.maybe_send_embed("Error occurred :(") + @checks.is_owner() @chatter.command() async def train(self, ctx: commands.Context, channel: discord.TextChannel): """ From 5fffaf489397a3e0c6ab1ebc4039c2d60998cef0 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 23 Oct 2020 11:41:11 -0400 Subject: [PATCH 14/30] cog_unload, not __unload, whoops. --- planttycoon/planttycoon.py | 2 +- werewolf/werewolf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/planttycoon/planttycoon.py b/planttycoon/planttycoon.py index 665fc9a..4209b53 100644 --- a/planttycoon/planttycoon.py +++ b/planttycoon/planttycoon.py @@ -793,7 +793,7 @@ class PlantTycoon(commands.Cog): pass await asyncio.sleep(self.defaults["timers"]["notification"] * 60) - def __unload(self): + def cog_unload(self): self.completion_task.cancel() # self.degradation_task.cancel() self.notification_task.cancel() diff --git a/werewolf/werewolf.py b/werewolf/werewolf.py index bd68a6f..dd711ed 100644 --- a/werewolf/werewolf.py +++ b/werewolf/werewolf.py @@ -56,7 +56,7 @@ class Werewolf(Cog): """Nothing to delete""" return - def __unload(self): + def cog_unload(self): log.debug("Unload called") for game in self.games.values(): del game From fc0870af68607b3f3e79b2b202fa0f05b7a20a6a Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 28 Oct 2020 13:08:03 -0400 Subject: [PATCH 15/30] Fix multi-triggers, add `checktask` to see more scheduled executions --- fifo/fifo.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++-- fifo/task.py | 16 +--------------- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index acd01ac..c42e4df 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -1,5 +1,6 @@ +import itertools import logging -from datetime import datetime, timedelta, tzinfo +from datetime import datetime, timedelta, tzinfo, MAXYEAR from typing import Optional, Union import discord @@ -10,7 +11,7 @@ from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import TimedeltaConverter -from redbot.core.utils.chat_formatting import pagify +from redbot.core.utils.chat_formatting import humanize_list, humanize_timedelta, pagify from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter from .task import Task @@ -37,6 +38,27 @@ def _disassemble_job_id(job_id: str): return job_id.split("_") +def _get_run_times(job: Job, now: datetime = None): + """ + Computes the scheduled run times between ``next_run_time`` and ``now`` (inclusive). + + Modified to be asynchronous and yielding instead of all-or-nothing + + """ + if not job.next_run_time: + raise StopIteration() + + if now is None: + now = datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999, tzinfo=job.next_run_time.tzinfo) + yield from _get_run_times(job, now) + raise StopIteration() + + next_run_time = job.next_run_time + while next_run_time and next_run_time <= now: + yield next_run_time + next_run_time = job.trigger.get_next_fire_time(next_run_time, now) + + class FIFO(commands.Cog): """ Simple Scheduling Cog @@ -173,6 +195,30 @@ class FIFO(commands.Cog): if ctx.invoked_subcommand is None: pass + @fifo.command(name="checktask", aliases=["checkjob", "check"]) + async def fifo_checktask(self, ctx: commands.Context, task_name: str): + """Returns the next 10 scheduled executions of the task""" + task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) + await task.load_from_config() + + if task.data is None: + await ctx.maybe_send_embed( + f"Task by the name of {task_name} is not found in this guild" + ) + return + + job = await self._get_job(task) + if job is None: + await ctx.maybe_send_embed("No job scheduled for this task") + return + now = datetime.now(job.next_run_time.tzinfo) + + times = [ + humanize_timedelta(timedelta=x - now) + for x in itertools.islice(_get_run_times(job), 10) + ] + await ctx.maybe_send_embed("\n\n".join(times)) + @fifo.command(name="set") async def fifo_set( self, @@ -326,6 +372,8 @@ class FIFO(commands.Cog): for task_name, task_data in all_tasks.items(): out += f"{task_name}: {task_data}\n" + out = humanize_list(out) + if out: if len(out) > 2000: for page in pagify(out): @@ -394,6 +442,7 @@ class FIFO(commands.Cog): return await task.clear_triggers() + await self._remove_job(task) await ctx.tick() @fifo.group(name="addtrigger", aliases=["trigger"]) diff --git a/fifo/task.py b/fifo/task.py index f7dc45a..4c77e8b 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -39,7 +39,7 @@ def parse_triggers(data: Union[Dict, None]): return None if len(data["triggers"]) > 1: # Multiple triggers - return OrTrigger(get_trigger(t_data) for t_data in data["triggers"]) + return OrTrigger([get_trigger(t_data) for t_data in data["triggers"]]) return get_trigger(data["triggers"][0]) @@ -108,20 +108,6 @@ class Task: "tzinfo": getattr(t["tzinfo"], "zone", None), } ) - # triggers.append( - # { - # "type": t["type"], - # "time_data": { - # "year": dt.year, - # "month": dt.month, - # "day": dt.day, - # "hour": dt.hour, - # "minute": dt.minute, - # "second": dt.second, - # "tzinfo": dt.tzinfo, - # }, - # } - # ) continue if t["type"] == "cron": From db538f75304cd4acf858771d41ff4c83f8d065f9 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 28 Oct 2020 15:19:28 -0400 Subject: [PATCH 16/30] Hotfix cause I didn't test this --- fifo/fifo.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index c42e4df..4a0e622 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -370,9 +370,7 @@ class FIFO(commands.Cog): out = "" all_tasks = await self.config.guild(ctx.guild).tasks() for task_name, task_data in all_tasks.items(): - out += f"{task_name}: {task_data}\n" - - out = humanize_list(out) + out += f"{task_name}: {task_data}\n\n" if out: if len(out) > 2000: From 1f1d116a560e302c5bcc3fc0980536cddead696c Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 30 Oct 2020 12:17:35 -0400 Subject: [PATCH 17/30] Docstring update --- nudity/nudity.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nudity/nudity.py b/nudity/nudity.py index 4233460..64ec02a 100644 --- a/nudity/nudity.py +++ b/nudity/nudity.py @@ -8,9 +8,7 @@ from redbot.core.data_manager import cog_data_path class Nudity(commands.Cog): - """ - V3 Cog Template - """ + """Monitor images for NSFW content and moves them to a nsfw channel if possible""" def __init__(self, bot: Red): super().__init__() From 8015e4a46d00b20778bad524775940b930db6148 Mon Sep 17 00:00:00 2001 From: bobloy Date: Mon, 2 Nov 2020 11:49:42 -0500 Subject: [PATCH 18/30] Hotfix scheduling snowflake issue --- fifo/fifo.py | 1 + fifo/task.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index 4a0e622..c6479b4 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -27,6 +27,7 @@ async def _execute_task(task_state): task = Task(**task_state) if await task.load_from_config(): return await task.execute() + log.warning(f"Failed to load data on {task_state=}") return False diff --git a/fifo/task.py b/fifo/task.py index 4c77e8b..9e0e545 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -253,15 +253,15 @@ class Task: actual_message = await author.history(limit=1).flatten() if not actual_message: # Okay, the *author* has never sent a message? log.warning("No message found in channel cache yet, skipping execution") - return + return False actual_message = actual_message[0] message = FakeMessage(actual_message) # message = FakeMessage2 message.author = author - message.guild = guild # Just in case we got desperate + message.guild = guild # Just in case we got desperate, see above message.channel = channel - message.id = time_snowflake(datetime.now()) # Pretend to be now + message.id = time_snowflake(datetime.utcnow(), high=False) # Pretend to be now message = neuter_message(message) # absolutely weird that this takes a message object instead of guild @@ -273,7 +273,12 @@ class Task: message.content = f"{prefix}{self.get_command_str()}" - if not message.guild or not message.author or not message.content: + if ( + not message.guild + or not message.author + or not message.content + or message.content == prefix + ): log.warning(f"Could not execute task due to message problem: {message}") return False From 419863b07a89ae1d3b8963fee82adb835b4e0993 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 4 Nov 2020 09:04:32 -0500 Subject: [PATCH 19/30] Better error logging. --- fifo/task.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/fifo/task.py b/fifo/task.py index 9e0e545..7c51ee4 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -225,20 +225,26 @@ class Task: async def execute(self): if not self.data or not self.get_command_str(): - log.warning(f"Could not execute task due to data problem: {self.data=}") + log.warning(f"Could not execute Task[{self.name}] due to data problem: {self.data=}") return False guild: discord.Guild = self.bot.get_guild(self.guild_id) # used for get_prefix if guild is None: - log.warning(f"Could not execute task due to missing guild: {self.guild_id}") + log.warning( + f"Could not execute Task[{self.name}] due to missing guild: {self.guild_id}" + ) return False channel: discord.TextChannel = guild.get_channel(self.channel_id) if channel is None: - log.warning(f"Could not execute task due to missing channel: {self.channel_id}") + log.warning( + f"Could not execute Task[{self.name}] due to missing channel: {self.channel_id}" + ) return False author: discord.User = guild.get_member(self.author_id) if author is None: - log.warning(f"Could not execute task due to missing author: {self.author_id}") + log.warning( + f"Could not execute Task[{self.name}] due to missing author: {self.author_id}" + ) return False actual_message: discord.Message = channel.last_message @@ -279,14 +285,15 @@ class Task: or not message.content or message.content == prefix ): - log.warning(f"Could not execute task due to message problem: {message}") + log.warning(f"Could not execute Task[{self.name}] due to message problem: {message}") return False new_ctx: commands.Context = await self.bot.get_context(message) new_ctx.assume_yes = True if not new_ctx.valid: log.warning( - f"Could not execute Task[{self.name}] due invalid context: {new_ctx.invoked_with}" + f"Could not execute Task[{self.name}] due invalid context: " + f"{new_ctx.invoked_with=} {new_ctx.prefix=} {new_ctx.command=}" ) return False From a2948322f9d37f53d9a63edaf1932a926be26537 Mon Sep 17 00:00:00 2001 From: bobloy Date: Fri, 6 Nov 2020 12:42:03 -0500 Subject: [PATCH 20/30] Download ubuntu data to the cog data directory --- chatter/chat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chatter/chat.py b/chatter/chat.py index d63a664..4e3400c 100644 --- a/chatter/chat.py +++ b/chatter/chat.py @@ -159,7 +159,9 @@ class Chatter(Cog): return out def _train_ubuntu(self): - trainer = UbuntuCorpusTrainer(self.chatbot) + trainer = UbuntuCorpusTrainer( + self.chatbot, ubuntu_corpus_data_directory=cog_data_path(self) / "ubuntu_data" + ) trainer.train() return True From 99ab9fc1b484bd6f0c18f185b48238a65a3c474a Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 11 Nov 2020 09:44:49 -0500 Subject: [PATCH 21/30] [HOTFIX] Fix not applying the similarity threshold --- chatter/chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatter/chat.py b/chatter/chat.py index 4e3400c..41affb6 100644 --- a/chatter/chat.py +++ b/chatter/chat.py @@ -272,7 +272,7 @@ class Chatter(Cog): ) return else: - self.similarity_algo = threshold + self.similarity_threshold = threshold self.similarity_algo = algos[algo_number] async with ctx.typing(): From 2ea077bb0cec5a9fe338e49f89377559d74d72cd Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 11 Nov 2020 10:38:54 -0500 Subject: [PATCH 22/30] Add relative trigger, better error handling --- fifo/fifo.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index c6479b4..9bfe2e1 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -152,10 +152,16 @@ class FIFO(commands.Cog): return job async def _pause_job(self, task: Task): - return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id)) + try: + return self.scheduler.pause_job(job_id=_assemble_job_id(task.name, task.guild_id)) + except JobLookupError: + return False async def _remove_job(self, task: Task): - return self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id)) + try: + self.scheduler.remove_job(job_id=_assemble_job_id(task.name, task.guild_id)) + except JobLookupError: + pass async def _get_tz(self, user: Union[discord.User, discord.Member]) -> Union[None, tzinfo]: if self.tz_cog is None: @@ -483,6 +489,40 @@ class FIFO(commands.Cog): f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)" ) + @fifo_trigger.command(name="relative") + async def fifo_trigger_relative( + self, ctx: commands.Context, task_name: str, *, time_from_now: TimedeltaConverter + ): + """ + Add a "run once" trigger at a time relative from now to the specified task + """ + + task = Task(task_name, ctx.guild.id, self.config) + await task.load_from_config() + + if task.data is None: + await ctx.maybe_send_embed( + f"Task by the name of {task_name} is not found in this guild" + ) + return + + time_to_run = datetime.now() + time_from_now + + result = await task.add_trigger("date", time_to_run, time_to_run.tzinfo) + if not result: + await ctx.maybe_send_embed( + "Failed to add a date trigger to this task, see console for logs" + ) + return + + await task.save_data() + job: Job = await self._process_task(task) + delta_from_now: timedelta = job.next_run_time - datetime.now(job.next_run_time.tzinfo) + await ctx.maybe_send_embed( + f"Task `{task_name}` added {time_to_run} to its scheduled runtimes\n" + f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)" + ) + @fifo_trigger.command(name="date") async def fifo_trigger_date( self, ctx: commands.Context, task_name: str, *, datetime_str: DatetimeConverter From 40b01cff262e49aac82d5421bce6ea03e4845dc2 Mon Sep 17 00:00:00 2001 From: bobloy Date: Wed, 11 Nov 2020 10:40:49 -0500 Subject: [PATCH 23/30] Black formatting --- fifo/fifo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index 9bfe2e1..f060211 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -491,7 +491,7 @@ class FIFO(commands.Cog): @fifo_trigger.command(name="relative") async def fifo_trigger_relative( - self, ctx: commands.Context, task_name: str, *, time_from_now: TimedeltaConverter + self, ctx: commands.Context, task_name: str, *, time_from_now: TimedeltaConverter ): """ Add a "run once" trigger at a time relative from now to the specified task From ca8c762e691b33fc29821fad947c2b5284484c4e Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 17 Nov 2020 15:14:50 -0500 Subject: [PATCH 24/30] FIFO resturcture --- fifo/fifo.py | 54 ++++++++-- fifo/redconfigjobstore.py | 218 ++++++++++++++++++++++---------------- fifo/task.py | 4 +- 3 files changed, 176 insertions(+), 100 deletions(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index f060211..b3272b1 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -22,8 +22,8 @@ schedule_log.setLevel(logging.DEBUG) log = logging.getLogger("red.fox_v3.fifo") -async def _execute_task(task_state): - log.info(f"Executing {task_state=}") +async def _execute_task(**task_state): + log.info(f"Executing {task_state.get('name')}") task = Task(**task_state) if await task.load_from_config(): return await task.execute() @@ -60,6 +60,19 @@ def _get_run_times(job: Job, now: datetime = None): next_run_time = job.trigger.get_next_fire_time(next_run_time, now) +class CapturePrint: + """Silly little class to get `print` output""" + + def __init__(self): + self.string = None + + def write(self, string): + if self.string is None: + self.string = string + else: + self.string = self.string + "\n" + string + + class FIFO(commands.Cog): """ Simple Scheduling Cog @@ -78,7 +91,7 @@ class FIFO(commands.Cog): self.config.register_global(**default_global) self.config.register_guild(**default_guild) - self.scheduler = None + self.scheduler: Optional[AsyncIOScheduler] = None self.jobstore = None self.tz_cog = None @@ -94,7 +107,12 @@ class FIFO(commands.Cog): async def initialize(self): - job_defaults = {"coalesce": False, "max_instances": 1} + job_defaults = { + "coalesce": True, + "max_instances": 5, + "misfire_grace_time": 15, + "replace_existing": True, + } # executors = {"default": AsyncIOExecutor()} @@ -104,7 +122,7 @@ class FIFO(commands.Cog): from .redconfigjobstore import RedConfigJobStore self.jobstore = RedConfigJobStore(self.config, self.bot) - await self.jobstore.load_from_config(self.scheduler, "default") + await self.jobstore.load_from_config() self.scheduler.add_jobstore(self.jobstore, "default") self.scheduler.start() @@ -139,9 +157,10 @@ class FIFO(commands.Cog): async def _add_job(self, task: Task): return self.scheduler.add_job( _execute_task, - args=[task.__getstate__()], + kwargs=task.__getstate__(), id=_assemble_job_id(task.name, task.guild_id), trigger=await task.get_combined_trigger(), + name=task.name, ) async def _resume_job(self, task: Task): @@ -372,7 +391,7 @@ class FIFO(commands.Cog): Do `[p]fifo list True` to see tasks from all guilds """ if all_guilds: - pass + pass # TODO: All guilds else: out = "" all_tasks = await self.config.guild(ctx.guild).tasks() @@ -388,6 +407,27 @@ class FIFO(commands.Cog): else: await ctx.maybe_send_embed("No tasks to list") + @fifo.command(name="printschedule") + async def fifo_printschedule(self, ctx: commands.Context): + """ + Print the current schedule of execution. + + Useful for debugging. + """ + cp = CapturePrint() + self.scheduler.print_jobs(out=cp) + + out = cp.string + + if out: + if len(out) > 2000: + for page in pagify(out): + await ctx.maybe_send_embed(page) + else: + await ctx.maybe_send_embed(out) + else: + await ctx.maybe_send_embed("Failed to get schedule from scheduler") + @fifo.command(name="add") async def fifo_add(self, ctx: commands.Context, task_name: str, *, command_to_execute: str): """ diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py index 7e68697..27324f6 100644 --- a/fifo/redconfigjobstore.py +++ b/fifo/redconfigjobstore.py @@ -28,7 +28,7 @@ class RedConfigJobStore(MemoryJobStore): self.config = config self.bot = bot self.pickle_protocol = pickle.HIGHEST_PROTOCOL - self._eventloop = self.bot.loop + self._eventloop = self.bot.loop # Used for @run_in_event_loop # TODO: self.config.jobs_index is never used, # fine but maybe a sign of inefficient use of config @@ -40,32 +40,50 @@ class RedConfigJobStore(MemoryJobStore): @run_in_event_loop def start(self, scheduler, alias): super().start(scheduler, alias) + for job, timestamp in self._jobs: + job._scheduler = self._scheduler + job._jobstore_alias = self._alias - async def load_from_config(self, scheduler, alias): - super().start(scheduler, alias) + async def load_from_config(self): _jobs = await self.config.jobs() - self._jobs = [ - (await self._decode_job(job), timestamp) async for (job, timestamp) in AsyncIter(_jobs) - ] + # self._jobs = [ + # (await self._decode_job(job), timestamp) async for (job, timestamp) in AsyncIter(_jobs) + # ] + async for job, timestamp in AsyncIter(_jobs): + job = await self._decode_job(job) + index = self._get_job_index(timestamp, job.id) + self._jobs.insert(index, (job, timestamp)) + self._jobs_index[job.id] = (job, timestamp) + + async def save_to_config(self): + """Yea that's basically it""" + await self.config.jobs.set( + [(self._encode_job(job), timestamp) for job, timestamp in self._jobs] + ) + # self._jobs_index = await self.config.jobs_index.all() # Overwritten by next - self._jobs_index = {job.id: (job, timestamp) for job, timestamp in self._jobs} + # self._jobs_index = {job.id: (job, timestamp) for job, timestamp in self._jobs} def _encode_job(self, job: Job): job_state = job.__getstate__() - new_args = list(job_state["args"]) - new_args[0]["config"] = None - new_args[0]["bot"] = None - job_state["args"] = tuple(new_args) + job_state["kwargs"]["config"] = None + job_state["kwargs"]["bot"] = None + # new_kwargs = job_state["kwargs"] + # new_kwargs["config"] = None + # new_kwargs["bot"] = None + # job_state["kwargs"] = new_kwargs encoded = base64.b64encode(pickle.dumps(job_state, self.pickle_protocol)) out = { "_id": job.id, "next_run_time": datetime_to_utc_timestamp(job.next_run_time), "job_state": encoded.decode("ascii"), } - new_args = list(job_state["args"]) - new_args[0]["config"] = self.config - new_args[0]["bot"] = self.bot - job_state["args"] = tuple(new_args) + job_state["kwargs"]["config"] = self.config + job_state["kwargs"]["bot"] = self.bot + # new_kwargs = job_state["kwargs"] + # new_kwargs["config"] = self.config + # new_kwargs["bot"] = self.bot + # job_state["kwargs"] = new_kwargs # log.debug(f"Encoding job id: {job.id}\n" # f"Encoded as: {out}") @@ -76,10 +94,20 @@ class RedConfigJobStore(MemoryJobStore): return None job_state = in_job["job_state"] job_state = pickle.loads(base64.b64decode(job_state)) - new_args = list(job_state["args"]) - new_args[0]["config"] = self.config - new_args[0]["bot"] = self.bot - job_state["args"] = tuple(new_args) + if job_state["args"]: # Backwards compatibility on args to kwargs + job_state["kwargs"] = { + "name": job_state["args"][0], + "guild_id": job_state["args"][1], + "author_id": job_state["args"][2], + "channel_id": job_state["args"][3], + "bot": job_state["args"][4], + } + job_state["kwargs"]["config"] = self.config + job_state["kwargs"]["bot"] = self.bot + # new_kwargs = job_state["kwargs"] + # new_kwargs["config"] = self.config + # new_kwargs["bot"] = self.bot + # job_state["kwargs"] = new_kwargs job = Job.__new__(Job) job.__setstate__(job_state) job._scheduler = self._scheduler @@ -96,78 +124,82 @@ class RedConfigJobStore(MemoryJobStore): return job - @run_in_event_loop - def add_job(self, job: Job): - if job.id in self._jobs_index: - raise ConflictingIdError(job.id) - # log.debug(f"Check job args: {job.args=}") - timestamp = datetime_to_utc_timestamp(job.next_run_time) - index = self._get_job_index(timestamp, job.id) # This is fine - self._jobs.insert(index, (job, timestamp)) - self._jobs_index[job.id] = (job, timestamp) - asyncio.create_task(self._async_add_job(job, index, timestamp)) - # log.debug(f"Added job: {self._jobs[index][0].args}") - - async def _async_add_job(self, job, index, timestamp): - encoded_job = self._encode_job(job) - job_tuple = tuple([encoded_job, timestamp]) - async with self.config.jobs() as jobs: - jobs.insert(index, job_tuple) - # await self.config.jobs_index.set_raw(job.id, value=job_tuple) - return True - - @run_in_event_loop - def update_job(self, job): - old_tuple: Tuple[Union[Job, None], Union[datetime, None]] = self._jobs_index.get( - job.id, (None, None) - ) - old_job = old_tuple[0] - old_timestamp = old_tuple[1] - if old_job is None: - raise JobLookupError(job.id) - - # If the next run time has not changed, simply replace the job in its present index. - # Otherwise, reinsert the job to the list to preserve the ordering. - old_index = self._get_job_index(old_timestamp, old_job.id) - new_timestamp = datetime_to_utc_timestamp(job.next_run_time) - asyncio.create_task( - self._async_update_job(job, new_timestamp, old_index, old_job, old_timestamp) - ) - - async def _async_update_job(self, job, new_timestamp, old_index, old_job, old_timestamp): - encoded_job = self._encode_job(job) - if old_timestamp == new_timestamp: - self._jobs[old_index] = (job, new_timestamp) - async with self.config.jobs() as jobs: - jobs[old_index] = (encoded_job, new_timestamp) - else: - del self._jobs[old_index] - new_index = self._get_job_index(new_timestamp, job.id) # This is fine - self._jobs.insert(new_index, (job, new_timestamp)) - async with self.config.jobs() as jobs: - del jobs[old_index] - jobs.insert(new_index, (encoded_job, new_timestamp)) - self._jobs_index[old_job.id] = (job, new_timestamp) - # await self.config.jobs_index.set_raw(old_job.id, value=(encoded_job, new_timestamp)) - - log.debug(f"Async Updated {job.id=}") - log.debug(f"Check job args: {job.args=}") - - @run_in_event_loop - def remove_job(self, job_id): - job, timestamp = self._jobs_index.get(job_id, (None, None)) - if job is None: - raise JobLookupError(job_id) - - index = self._get_job_index(timestamp, job_id) - del self._jobs[index] - del self._jobs_index[job.id] - asyncio.create_task(self._async_remove_job(index, job)) - - async def _async_remove_job(self, index, job): - async with self.config.jobs() as jobs: - del jobs[index] - # await self.config.jobs_index.clear_raw(job.id) + # @run_in_event_loop + # def add_job(self, job: Job): + # if job.id in self._jobs_index: + # raise ConflictingIdError(job.id) + # # log.debug(f"Check job args: {job.args=}") + # timestamp = datetime_to_utc_timestamp(job.next_run_time) + # index = self._get_job_index(timestamp, job.id) # This is fine + # self._jobs.insert(index, (job, timestamp)) + # self._jobs_index[job.id] = (job, timestamp) + # task = asyncio.create_task(self._async_add_job(job, index, timestamp)) + # self._eventloop.run_until_complete(task) + # # log.debug(f"Added job: {self._jobs[index][0].args}") + # + # async def _async_add_job(self, job, index, timestamp): + # encoded_job = self._encode_job(job) + # job_tuple = tuple([encoded_job, timestamp]) + # async with self.config.jobs() as jobs: + # jobs.insert(index, job_tuple) + # # await self.config.jobs_index.set_raw(job.id, value=job_tuple) + # return True + + # @run_in_event_loop + # def update_job(self, job): + # old_tuple: Tuple[Union[Job, None], Union[datetime, None]] = self._jobs_index.get( + # job.id, (None, None) + # ) + # old_job = old_tuple[0] + # old_timestamp = old_tuple[1] + # if old_job is None: + # raise JobLookupError(job.id) + # + # # If the next run time has not changed, simply replace the job in its present index. + # # Otherwise, reinsert the job to the list to preserve the ordering. + # old_index = self._get_job_index(old_timestamp, old_job.id) + # new_timestamp = datetime_to_utc_timestamp(job.next_run_time) + # task = asyncio.create_task( + # self._async_update_job(job, new_timestamp, old_index, old_job, old_timestamp) + # ) + # self._eventloop.run_until_complete(task) + # + # async def _async_update_job(self, job, new_timestamp, old_index, old_job, old_timestamp): + # encoded_job = self._encode_job(job) + # if old_timestamp == new_timestamp: + # self._jobs[old_index] = (job, new_timestamp) + # async with self.config.jobs() as jobs: + # jobs[old_index] = (encoded_job, new_timestamp) + # else: + # del self._jobs[old_index] + # new_index = self._get_job_index(new_timestamp, job.id) # This is fine + # self._jobs.insert(new_index, (job, new_timestamp)) + # async with self.config.jobs() as jobs: + # del jobs[old_index] + # jobs.insert(new_index, (encoded_job, new_timestamp)) + # self._jobs_index[old_job.id] = (job, new_timestamp) + # # await self.config.jobs_index.set_raw(old_job.id, value=(encoded_job, new_timestamp)) + # + # log.debug(f"Async Updated {job.id=}") + # # log.debug(f"Check job args: {job.kwargs=}") + + # @run_in_event_loop + # def remove_job(self, job_id): + # """Copied instead of super for the asyncio args""" + # job, timestamp = self._jobs_index.get(job_id, (None, None)) + # if job is None: + # raise JobLookupError(job_id) + # + # index = self._get_job_index(timestamp, job_id) + # del self._jobs[index] + # del self._jobs_index[job.id] + # task = asyncio.create_task(self._async_remove_job(index, job)) + # self._eventloop.run_until_complete(task) + # + # async def _async_remove_job(self, index, job): + # async with self.config.jobs() as jobs: + # del jobs[index] + # # await self.config.jobs_index.clear_raw(job.id) @run_in_event_loop def remove_all_jobs(self): @@ -180,4 +212,8 @@ class RedConfigJobStore(MemoryJobStore): def shutdown(self): """Removes all jobs without clearing config""" + asyncio.create_task(self.async_shutdown()) + + async def async_shutdown(self): + await self.save_to_config() super().remove_all_jobs() diff --git a/fifo/task.py b/fifo/task.py index 7c51ee4..005230b 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -40,8 +40,8 @@ def parse_triggers(data: Union[Dict, None]): if len(data["triggers"]) > 1: # Multiple triggers return OrTrigger([get_trigger(t_data) for t_data in data["triggers"]]) - - return get_trigger(data["triggers"][0]) + else: + return get_trigger(data["triggers"][0]) class FakeMessage: From a946b1b83b35526d872eb8617d66bbbbcce32a53 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 17 Nov 2020 15:30:07 -0500 Subject: [PATCH 25/30] Catch error better --- fifo/redconfigjobstore.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py index 27324f6..dee020a 100644 --- a/fifo/redconfigjobstore.py +++ b/fifo/redconfigjobstore.py @@ -95,13 +95,16 @@ class RedConfigJobStore(MemoryJobStore): job_state = in_job["job_state"] job_state = pickle.loads(base64.b64decode(job_state)) if job_state["args"]: # Backwards compatibility on args to kwargs - job_state["kwargs"] = { - "name": job_state["args"][0], - "guild_id": job_state["args"][1], - "author_id": job_state["args"][2], - "channel_id": job_state["args"][3], - "bot": job_state["args"][4], - } + try: + job_state["kwargs"] = { + "name": job_state["args"][0], + "guild_id": job_state["args"][1], + "author_id": job_state["args"][2], + "channel_id": job_state["args"][3], + "bot": job_state["args"][4], + } + except IndexError as e: + raise Exception(job_state["args"]) from e job_state["kwargs"]["config"] = self.config job_state["kwargs"]["bot"] = self.bot # new_kwargs = job_state["kwargs"] From 3c3dd2d6cde3d9cbc8c2e25c35490a0b415623c2 Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 17 Nov 2020 15:35:17 -0500 Subject: [PATCH 26/30] Correctly handle backwards compatibility --- fifo/redconfigjobstore.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py index dee020a..17caec9 100644 --- a/fifo/redconfigjobstore.py +++ b/fifo/redconfigjobstore.py @@ -95,16 +95,8 @@ class RedConfigJobStore(MemoryJobStore): job_state = in_job["job_state"] job_state = pickle.loads(base64.b64decode(job_state)) if job_state["args"]: # Backwards compatibility on args to kwargs - try: - job_state["kwargs"] = { - "name": job_state["args"][0], - "guild_id": job_state["args"][1], - "author_id": job_state["args"][2], - "channel_id": job_state["args"][3], - "bot": job_state["args"][4], - } - except IndexError as e: - raise Exception(job_state["args"]) from e + job_state["kwargs"] = {**job_state["args"][0]} + job_state["args"] = [] job_state["kwargs"]["config"] = self.config job_state["kwargs"]["bot"] = self.bot # new_kwargs = job_state["kwargs"] From b2ebddc82544584fd57f2d5ffecc72594f980565 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 19 Nov 2020 10:27:22 -0500 Subject: [PATCH 27/30] I forgot to add the bot object for some reason. --- fifo/fifo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index b3272b1..61eb597 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -537,7 +537,7 @@ class FIFO(commands.Cog): Add a "run once" trigger at a time relative from now to the specified task """ - task = Task(task_name, ctx.guild.id, self.config) + task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) await task.load_from_config() if task.data is None: @@ -571,7 +571,7 @@ class FIFO(commands.Cog): Add a "run once" datetime trigger to the specified task """ - task = Task(task_name, ctx.guild.id, self.config) + task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) await task.load_from_config() if task.data is None: @@ -611,7 +611,7 @@ class FIFO(commands.Cog): See https://crontab.guru/ for help generating the cron_str """ - task = Task(task_name, ctx.guild.id, self.config) + task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) await task.load_from_config() if task.data is None: From 19ee6e6f245ba1c1e46022c1896f7274531a7c08 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 19 Nov 2020 10:40:20 -0500 Subject: [PATCH 28/30] Add and remove comments --- fifo/fifo.py | 16 ++++++++-------- fifo/redconfigjobstore.py | 12 +----------- fifo/task.py | 2 +- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/fifo/fifo.py b/fifo/fifo.py index 61eb597..d60d777 100644 --- a/fifo/fifo.py +++ b/fifo/fifo.py @@ -1,6 +1,6 @@ import itertools import logging -from datetime import datetime, timedelta, tzinfo, MAXYEAR +from datetime import MAXYEAR, datetime, timedelta, tzinfo from typing import Optional, Union import discord @@ -11,7 +11,7 @@ from apscheduler.schedulers.base import STATE_PAUSED, STATE_RUNNING from redbot.core import Config, checks, commands from redbot.core.bot import Red from redbot.core.commands import TimedeltaConverter -from redbot.core.utils.chat_formatting import humanize_list, humanize_timedelta, pagify +from redbot.core.utils.chat_formatting import humanize_timedelta, pagify from .datetime_cron_converters import CronConverter, DatetimeConverter, TimezoneConverter from .task import Task @@ -108,10 +108,10 @@ class FIFO(commands.Cog): async def initialize(self): job_defaults = { - "coalesce": True, - "max_instances": 5, - "misfire_grace_time": 15, - "replace_existing": True, + "coalesce": True, # Multiple missed triggers within the grace time will only fire once + "max_instances": 5, # This is probably way too high, should likely only be one + "misfire_grace_time": 15, # 15 seconds ain't much, but it's honest work + "replace_existing": True, # Very important for persistent data } # executors = {"default": AsyncIOExecutor()} @@ -119,7 +119,7 @@ class FIFO(commands.Cog): # Default executor is already AsyncIOExecutor self.scheduler = AsyncIOScheduler(job_defaults=job_defaults, logger=schedule_log) - from .redconfigjobstore import RedConfigJobStore + from .redconfigjobstore import RedConfigJobStore # Wait to import to prevent cyclic import self.jobstore = RedConfigJobStore(self.config, self.bot) await self.jobstore.load_from_config() @@ -507,7 +507,7 @@ class FIFO(commands.Cog): """ task = Task(task_name, ctx.guild.id, self.config, bot=self.bot) - await task.load_from_config() + await task.load_from_config() # Will set the channel and author if task.data is None: await ctx.maybe_send_embed( diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py index 17caec9..126cfc0 100644 --- a/fifo/redconfigjobstore.py +++ b/fifo/redconfigjobstore.py @@ -2,17 +2,13 @@ import asyncio import base64 import logging import pickle -from datetime import datetime -from typing import Tuple, Union from apscheduler.job import Job -from apscheduler.jobstores.base import ConflictingIdError, JobLookupError from apscheduler.jobstores.memory import MemoryJobStore from apscheduler.schedulers.asyncio import run_in_event_loop from apscheduler.util import datetime_to_utc_timestamp from redbot.core import Config - -# TODO: use get_lock on config +# TODO: use get_lock on config maybe from redbot.core.bot import Red from redbot.core.utils import AsyncIter @@ -29,13 +25,7 @@ class RedConfigJobStore(MemoryJobStore): self.bot = bot self.pickle_protocol = pickle.HIGHEST_PROTOCOL self._eventloop = self.bot.loop # Used for @run_in_event_loop - # TODO: self.config.jobs_index is never used, - # fine but maybe a sign of inefficient use of config - # task = asyncio.create_task(self.load_from_config()) - # while not task.done(): - # sleep(0.1) - # future = asyncio.ensure_future(self.load_from_config(), loop=self.bot.loop) @run_in_event_loop def start(self, scheduler, alias): diff --git a/fifo/task.py b/fifo/task.py index 005230b..271c59c 100644 --- a/fifo/task.py +++ b/fifo/task.py @@ -166,7 +166,7 @@ class Task: return self.author_id = data["author_id"] - self.guild_id = data["guild_id"] + self.guild_id = data["guild_id"] # Weird I'm doing this, since self.guild_id was just used self.channel_id = data["channel_id"] self.data = data["data"] From 8ab6c5062539faa9423d5ba174a9f2852a822c87 Mon Sep 17 00:00:00 2001 From: bobloy Date: Thu, 19 Nov 2020 13:07:29 -0500 Subject: [PATCH 29/30] HOTFIX: Don't be crzy with pytz timezones --- fifo/timezones.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fifo/timezones.py b/fifo/timezones.py index 54d7c3e..bd1c239 100644 --- a/fifo/timezones.py +++ b/fifo/timezones.py @@ -5,6 +5,8 @@ All credit to https://github.com/prefrontal/dateutil-parser-timezones """ # from dateutil.tz import gettz +from datetime import datetime + from pytz import timezone @@ -227,4 +229,6 @@ def assemble_timezones(): timezones["YAKT"] = timezone("Asia/Yakutsk") # Yakutsk Time (UTC+09) timezones["YEKT"] = timezone("Asia/Yekaterinburg") # Yekaterinburg Time (UTC+05) + dt = datetime(2020, 1, 1) + timezones.update((x, y.localize(dt).tzinfo) for x, y in timezones.items()) return timezones From bf3c292fee9bc3275bc7382d6e002d211b8cfd2c Mon Sep 17 00:00:00 2001 From: bobloy Date: Tue, 8 Dec 2020 10:57:36 -0500 Subject: [PATCH 30/30] Black formatting --- fifo/redconfigjobstore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fifo/redconfigjobstore.py b/fifo/redconfigjobstore.py index 126cfc0..51b3cdc 100644 --- a/fifo/redconfigjobstore.py +++ b/fifo/redconfigjobstore.py @@ -8,6 +8,7 @@ from apscheduler.jobstores.memory import MemoryJobStore from apscheduler.schedulers.asyncio import run_in_event_loop from apscheduler.util import datetime_to_utc_timestamp from redbot.core import Config + # TODO: use get_lock on config maybe from redbot.core.bot import Red from redbot.core.utils import AsyncIter @@ -26,7 +27,6 @@ class RedConfigJobStore(MemoryJobStore): self.pickle_protocol = pickle.HIGHEST_PROTOCOL self._eventloop = self.bot.loop # Used for @run_in_event_loop - @run_in_event_loop def start(self, scheduler, alias): super().start(scheduler, alias)