def run_db_migrations(): dbv = int(Configuration.get_persistent_var('db_version', 0)) Logging.info(f"db version is {dbv}") dbv_list = [f for f in glob.glob("db_migrations/db_migrate_*.py")] dbv_pattern = re.compile(r'db_migrations/db_migrate_(\d+)\.py', re.IGNORECASE) migration_count = 0 for filename in sorted(dbv_list): # get the int version number from filename version = int(re.match(dbv_pattern, filename)[1]) if version > dbv: try: Logging.info( f"--- running db migration version number {version}") spec = importlib.util.spec_from_file_location( f"migrator_{version}", filename) dbm = importlib.util.module_from_spec(spec) spec.loader.exec_module(dbm) Configuration.set_persistent_var('db_version', version) migration_count = migration_count + 1 except Exception as e: # throw a fit if it doesn't work raise e Logging.info( f"--- {migration_count if migration_count else 'no'} db migration{'' if migration_count == 1 else 's'} run" )
async def periodic_task(self): # periodic task to run while cog is loaded # remove expired cooldowns now = datetime.now().timestamp() cooldown = Configuration.get_persistent_var(f"mischief_cooldown", dict()) try: # key for loaded dict is a string updated_cooldown = {} for str_uid, member_last_access_time in cooldown.items(): if (now - member_last_access_time) < self.cooldown_time: updated_cooldown[str_uid] = member_last_access_time Configuration.set_persistent_var(f"mischief_cooldown", updated_cooldown) except: Logging.info("can't clear cooldown") # update role count storage (because it's slow) try: guild = Utils.get_home_guild() for role_id in self.role_map.values(): my_role = guild.get_role(role_id) if my_role is not None: self.role_counts[str(role_id)] = len(my_role.members) except: Logging.info("can't update role counts")
async def process_reaction_add(self, timestamp, event): emoji_used = event.emoji member = event.member guild = self.bot.get_guild(event.guild_id) channel = self.bot.get_channel(event.channel_id) log_channel = self.bot.get_guild_log_channel(event.guild_id) # Act on log, remove, and mute: e_db = None if str(emoji_used) in self.emoji[event.guild_id]: e_db = self.emoji[event.guild_id][str(emoji_used)] if not e_db.log and not e_db.remove and not e_db.mute: # No actions to take. Stop processing return if not e_db: return # check mute/warn list for reaction_add - log to channel # for reaction_add, remove if threshold for quick-remove is passed try: # message fetch is API call. Only do it if needed message = await channel.fetch_message(event.message_id) except (NotFound, HTTPException) as e: # Can't track reactions on a message I can't find # Happens for deleted messages. Safe to ignore. # await Utils.handle_exception(f"Failed to get message {channel.id}/{event.message_id}", self, e) return log_msg = f"{Utils.get_member_log_name(member)} used emoji "\ f"[ {emoji_used} ] in #{channel.name}.\n"\ f"{message.jump_url}" if e_db.remove: await message.clear_reaction(emoji_used) log_msg = f"{log_msg}\n--- I **removed** the reaction" if e_db.mute: guild_config = self.bot.get_guild_db_config(guild.id) if guild_config and guild_config.mutedrole: try: mute_role = guild.get_role(guild_config.mutedrole) await member.add_roles(mute_role) self.mutes[guild.id][str(member.id)] = timestamp Configuration.set_persistent_var(f"react_mutes_{guild.id}", self.mutes[guild.id]) log_msg = f"{log_msg}\n--- I **muted** them" except Exception as e: await Utils.handle_exception( "reactmon failed to mute member", self.bot, e) else: await self.bot.guild_log( event.guild_id, "**I can't mute for reacts because `!guildconfig` mute role is not set." ) if (e_db.log or e_db.remove or e_db.mute) and log_channel: await log_channel.send(log_msg)
async def react_time(self, ctx: commands.Context, react_time: float): """ Reacts removed before this duration will trigger react-watch react_time: time in seconds, floating point e.g. 0.25 """ self.min_react_lifespan[ctx.guild.id] = react_time Configuration.set_persistent_var(f"min_react_lifespan_{ctx.guild.id}", react_time) await ctx.send(f"Reactions that are removed before {react_time} seconds have passed will be flagged")
async def startup_cleanup(self): for name, cid in Configuration.get_var("channels").items(): channel = self.bot.get_channel(cid) shutdown_id = Configuration.get_persistent_var(f"{name}_shutdown") if shutdown_id is not None: message = await channel.fetch_message(shutdown_id) if message is not None: await message.delete() Configuration.set_persistent_var(f"{name}_shutdown", None) await self.send_bug_info(name)
async def shutdown(self): for row in BugReportingChannel.select(): try: cid = row.channelid name = f"{row.platform.platform}_{row.platform.branch}" guild_id = row.guild.serverid channel = self.bot.get_channel(cid) message = await channel.send(Lang.get_locale_string("bugs/shutdown_message")) Configuration.set_persistent_var(f"{guild_id}_{name}_shutdown", message.id) except Exception as e: message = f"Failed sending shutdown message <#{cid}> in server {guild_id} for {name}" await self.bot.guild_log(guild_id, message) await Utils.handle_exception(message, self.bot, e)
async def send_bug_info(self, key): channel = self.bot.get_channel(Configuration.get_var("channels")[key]) bug_info_id = Configuration.get_persistent_var(f"{key}_message") if bug_info_id is not None: try: message = await channel.fetch_message(bug_info_id) except NotFound: pass else: await message.delete() if message.id in self.bug_messages: self.bug_messages.remove(message.id) bugemoji = Emoji.get_emoji('BUG') message = await channel.send( Lang.get_string("bugs/bug_info", bug_emoji=bugemoji)) await message.add_reaction(bugemoji) self.bug_messages.add(message.id) Configuration.set_persistent_var(f"{key}_message", message.id)
async def send_bug_info(self, *args): for channel_id in args: channel = self.bot.get_channel(channel_id) if channel is None: await Logging.bot_log(f"can't send bug info to nonexistent channel {channel_id}") continue bug_info_id = Configuration.get_persistent_var(f"{channel.guild.id}_{channel_id}_bug_message") ctx = None tries = 0 while not ctx and tries < 5: tries += 1 # this API call fails on startup because connection is not made yet. # TODO: properly wait for connection to be initialized try: last_message = await channel.send('preparing bug reporting...') ctx = await self.bot.get_context(last_message) if bug_info_id is not None: try: message = await channel.fetch_message(bug_info_id) except (NotFound, HTTPException): pass else: await message.delete() if message.id in self.bug_messages: self.bug_messages.remove(message.id) bugemoji = Emoji.get_emoji('BUG') message = await channel.send(Lang.get_locale_string("bugs/bug_info", ctx, bug_emoji=bugemoji)) self.bug_messages.add(message.id) await message.add_reaction(bugemoji) Configuration.set_persistent_var(f"{channel.guild.id}_{channel_id}_bug_message", message.id) Logging.info(f"Bug report message sent in channel #{channel.name} ({channel.id})") await last_message.delete() except Exception as e: await self.bot.guild_log(channel.guild.id, f'Having trouble sending bug message in {channel.mention}') await Utils.handle_exception( f"Bug report message failed to send in channel #{channel.name} ({channel.id})", self.bot, e) await asyncio.sleep(0.5)
async def mute_new_member(self, member): # is this feature turned on? if not self.mute_new_members: return # give other bots a chance to perform other actions first (like mute) await asyncio.sleep(0.5) # refresh member for up-to-date roles member = member.guild.get_member(member.id) mute_role = member.guild.get_role(Configuration.get_var("muted_role")) # only add mute if it hasn't already been added. This allows other mod-bots (gearbot) to mute re-joined members # and not interfere by allowing skybot to automatically un-muting later. if mute_role not in member.roles: self.join_cooldown[str(member.guild.id)][str( member.id)] = datetime.now().timestamp() Configuration.set_persistent_var("join_cooldown", self.join_cooldown) log_channel = self.bot.get_config_channel(member.guild.id, Utils.log_channel) if mute_role: # Auto-mute new members, pending cooldown await member.add_roles(mute_role)
async def restart(self, ctx): """Restart the bot""" shutdown_message = await ctx.send("Restarting...") if shutdown_message: cid = shutdown_message.channel.id mid = shutdown_message.id Configuration.set_persistent_var("bot_restart_channel_id", cid) Configuration.set_persistent_var("bot_restart_message_id", mid) Configuration.set_persistent_var("bot_restart_author_id", ctx.author.id) await self.bot.close()
async def mute_config(self, ctx, active: bool = None, mute_minutes_old: int = 10, mute_minutes_new: int = 20): """ Mute settings for new members active: mute on or off mute_minutes_old: how long (minutes) to mute established accounts (default 10) mute_minutes_new: how long (minutes) to mute accounts < 1 day old (default 20) """ self.set_verification_mode(ctx.guild) if self.discord_verification_flow[ctx.guild.id]: # discord verification flow precludes new-member muting await ctx.send(""" Discord verification flow is in effect. Mute is configured in discord moderation settings. To enable skybot muting, unset entry_channel: `!channel_config set entry_channel 0` """) return if active is not None: self.mute_new_members[ctx.guild.id] = active self.mute_minutes_old_account[ctx.guild.id] = mute_minutes_old self.mute_minutes_new_account[ctx.guild.id] = mute_minutes_new Configuration.set_persistent_var( f"{ctx.guild.id}_mute_new_members", active) Configuration.set_persistent_var( f"{ctx.guild.id}_mute_minutes_old_account", mute_minutes_old) Configuration.set_persistent_var( f"{ctx.guild.id}_mute_minutes_new_account", mute_minutes_new) status = discord.Embed(timestamp=ctx.message.created_at, color=0x663399, title=Lang.get_locale_string( "welcome/mute_settings_title", ctx, server_name=ctx.guild.name)) status.add_field( name="Mute new members", value= f"**{'ON' if self.mute_new_members[ctx.guild.id] else 'OFF'}**", inline=False) status.add_field( name="Mute duration", value=f"{self.mute_minutes_old_account[ctx.guild.id]} minutes", inline=False) status.add_field( name="New account mute duration\n(< 1 day old) ", value=f"{self.mute_minutes_new_account[ctx.guild.id]} minutes", inline=False) await ctx.send(embed=status)
async def on_message(self, message: discord.Message): if message.author.bot: return uid = message.author.id try: guild = Utils.get_home_guild() my_member: discord.Member = guild.get_member(uid) if my_member is None or len(message.content) > 60: return except: return # try to create DM channel try: channel = await my_member.create_dm() except: # Don't message member because creating DM channel failed channel = None now = datetime.now().timestamp() triggers = [ "i wish i was", "i wish i were", "i wish i could be", "i wish to be", "i wish to become", "i wish i could become", "i wish i could turn into", "i wish to turn into", "i wish you could make me", "i wish you would make me", "i wish you could turn me into", "i wish you would turn me into", ] remove = False pattern = re.compile(f"(skybot,? *)?({'|'.join(triggers)}) (.*)", re.I) result = pattern.match(message.content) if result is None: # no match. don't remove or add roles return # get selection out of matching message selection = result.group(3).lower().strip() if selection in ["myself", "myself again", "me"]: selection = "me again" if selection not in self.role_map: return # Selection is now validated # Check Cooldown cooldown = Configuration.get_persistent_var(f"mischief_cooldown", dict()) member_last_access_time = 0 if str(uid) not in cooldown else cooldown[str(uid)] cooldown_elapsed = now - member_last_access_time remaining = self.cooldown_time - cooldown_elapsed ctx = await self.bot.get_context(message) if not Utils.can_mod_official(ctx) and (cooldown_elapsed < self.cooldown_time): try: remaining_time = Utils.to_pretty_time(remaining) await channel.send(f"wait {remaining_time} longer before you make another wish...") except: pass return # END cooldown if selection == "me again": remove = True # remove all roles for key, role_id in self.role_map.items(): try: old_role = guild.get_role(role_id) if old_role in my_member.roles: await my_member.remove_roles(old_role) except: pass try: member_counts = Configuration.get_persistent_var(f"mischief_usage", dict()) member_count = 0 if str(uid) not in member_counts else member_counts[str(uid)] member_counts[str(uid)] = member_count + 1 Configuration.set_persistent_var("mischief_usage", member_counts) cooldown = Configuration.get_persistent_var("mischief_cooldown", dict()) cooldown[str(uid)] = now Configuration.set_persistent_var("mischief_cooldown", cooldown) except Exception as e: await Utils.handle_exception("mischief role tracking error", self.bot, e) if not remove: # add the selected role new_role = guild.get_role(self.role_map[selection]) await my_member.add_roles(new_role) if channel is not None: try: if remove: await channel.send("fine, you're demoted!") else: await channel.send(f"""Congratulations, you are now **{selection}**!! You can wish again in my DMs if you want! You can also use the `!team_mischief` command right here to find out more""") except: pass
def remove_member_from_cooldown(self, guildid, memberid): if str(guildid) in self.join_cooldown and str( memberid) in self.join_cooldown[str(guildid)]: del self.join_cooldown[str(guildid)][str(memberid)] Configuration.set_persistent_var("join_cooldown", self.join_cooldown)
async def shutdown(self): for name, cid in Configuration.get_var("channels").items(): channel = self.bot.get_channel(cid) message = await channel.send( Lang.get_string("bugs/shutdown_message")) Configuration.set_persistent_var(f"{name}_shutdown", message.id)