From 8283043d1b60be3f7ad9094983c2bf1b959fb70b Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 13 Feb 2022 21:26:47 +0000 Subject: Add a cog to bump threads Quite often we want threads such as event discussions, or moderation discussions to live beyond their maximum of 1 week of auto-archival. This cog allows staff to add a thread to a list that will get 'bumped' back open by the bot when they are auto-archived --- bot/exts/utils/thread_bumper.py | 114 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 bot/exts/utils/thread_bumper.py diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py new file mode 100644 index 000000000..a10d151aa --- /dev/null +++ b/bot/exts/utils/thread_bumper.py @@ -0,0 +1,114 @@ +import typing as t + +import discord +from async_rediscache import RedisCache +from discord.ext import commands + +from bot import constants +from bot.bot import Bot +from bot.log import get_logger +from bot.pagination import LinePaginator +from bot.utils import scheduling + +log = get_logger(__name__) + + +class ThreadBumper(commands.Cog): + """Cog that allow users to add the current thread to a list that get reopened on archive.""" + + # RedisCache[discord.Thread.id, "sentinel"] + threads_to_bump = RedisCache() + + def __init__(self, bot: Bot): + self.bot = bot + self.init_task = scheduling.create_task(self.ensure_bumped_threads_are_active(), event_loop=self.bot.loop) + + async def ensure_bumped_threads_are_active(self) -> None: + """Ensure bumped threads are active, since threads could have been archived while the bot was down.""" + await self.bot.wait_until_guild_available() + + for thread_id, _ in await self.threads_to_bump.items(): + if thread := self.bot.get_channel(thread_id): + if not thread.archived: + continue + + try: + thread = await self.bot.fetch_channel(thread_id) + except discord.NotFound: + log.info(f"Thread {thread_id} has been deleted, removing from bumped threads.") + await self.threads_to_bump.delete(thread_id) + if thread.archived: + await thread.edit(archived=False) + + @commands.group(name="bump") + async def thread_bump_group(self, ctx: commands.Context) -> None: + """A group of commands to manage the bumping of threads.""" + if not ctx.invoked_subcommand: + await ctx.send_help(ctx.command) + + @thread_bump_group.command(name="add") + async def add_thread_to_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: + """Add a thread to the bump list.""" + await self.init_task + + if not thread: + if isinstance(ctx.channel, discord.Thread): + thread = ctx.channel + else: + raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + + await self.threads_to_bump.set(thread.id, "sentinel") + await ctx.send(f":ok_hand:{thread.mention} has been added to the bump list.") + + @thread_bump_group.command(name="remove", aliases=("r", "rem", "d", "del", "delete")) + async def remove_thread_from_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: + """Remove a thread from the bump list.""" + await self.init_task + + if not thread: + if isinstance(ctx.channel, discord.Thread): + thread = ctx.channel + else: + raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + + await self.threads_to_bump.delete(thread.id) + await ctx.send(f":ok_hand: {thread.mention} has been removed from the bump list.") + + @thread_bump_group.command(name="list", aliases=("get",)) + async def list_all_threads_in_bump_list(self, ctx: commands.Context) -> None: + """List all the threads in the bump list.""" + await self.init_task + + lines = [f"<#{k}>" for k, _ in await self.threads_to_bump.items()] + embed = discord.Embed( + title="Threads in the bump list", + colour=constants.Colours.blue + ) + await LinePaginator.paginate(lines, ctx, embed) + + @commands.Cog.listener() + async def on_thread_update(self, _: discord.Thread, after: discord.Thread) -> None: + """ + Listen for thread updates and check if the thread has been archived. + + If the thread has been archived, and is in the bump list, un-archive it. + """ + await self.init_task + + if not after.archived: + return + + bumped_threads = [k for k, _ in await self.threads_to_bump.items()] + if after.id in bumped_threads: + await after.edit(archived=False) + + async def cog_check(self, ctx: commands.Context) -> bool: + """Only allow staff & partner roles to invoke the commands in this cog.""" + return await commands.has_any_role( + *constants.STAFF_PARTNERS_COMMUNITY_ROLES + ).predicate(ctx) + + +def setup(bot: Bot) -> None: + """Load the ThreadBumper cog.""" + bot.add_cog(ThreadBumper(bot)) -- cgit v1.2.3 From abdfd0db7caa2961428e6cf6b601df4aaccd9151 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 17 Feb 2022 01:23:56 +0000 Subject: Add logic so that manually archived threads bypass the thread bump list --- bot/exts/utils/thread_bumper.py | 46 +++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py index a10d151aa..8c6f3518e 100644 --- a/bot/exts/utils/thread_bumper.py +++ b/bot/exts/utils/thread_bumper.py @@ -8,7 +8,7 @@ from bot import constants from bot.bot import Bot from bot.log import get_logger from bot.pagination import LinePaginator -from bot.utils import scheduling +from bot.utils import channel, scheduling log = get_logger(__name__) @@ -23,22 +23,50 @@ class ThreadBumper(commands.Cog): self.bot = bot self.init_task = scheduling.create_task(self.ensure_bumped_threads_are_active(), event_loop=self.bot.loop) + async def unarchive_threads_not_manually_archived(self, threads: list[discord.Thread]) -> None: + """ + Iterate through and unarchive any threads that weren't manually archived recently. + + This is done by extracting the manually archived threads from the audit log. + + Only the last 200 thread_update logs are checked, + as this is assumed to be more than enough to cover bot downtime. + """ + guild = self.bot.get_guild(constants.Guild.id) + + recent_manually_archived_thread_ids = [] + async for thread_update in guild.audit_logs(limit=200, action=discord.AuditLogAction.thread_update): + if getattr(thread_update.after, "archived", False): + recent_manually_archived_thread_ids.append(thread_update.target.id) + + for thread in threads: + if thread.id in recent_manually_archived_thread_ids: + log.info( + "#%s (%d) was manually archived. Leaving archived, and removing from bumped threads.", + thread.name, + thread.id + ) + await self.threads_to_bump.delete(thread.id) + else: + await thread.edit(archived=False) + async def ensure_bumped_threads_are_active(self) -> None: """Ensure bumped threads are active, since threads could have been archived while the bot was down.""" await self.bot.wait_until_guild_available() + threads_to_maybe_bump = [] for thread_id, _ in await self.threads_to_bump.items(): - if thread := self.bot.get_channel(thread_id): - if not thread.archived: - continue - try: - thread = await self.bot.fetch_channel(thread_id) + thread = await channel.get_or_fetch_channel(thread_id) except discord.NotFound: - log.info(f"Thread {thread_id} has been deleted, removing from bumped threads.") + log.info("Thread %d has been deleted, removing from bumped threads.", thread_id) await self.threads_to_bump.delete(thread_id) + continue + if thread.archived: - await thread.edit(archived=False) + threads_to_maybe_bump.append(thread) + + await self.unarchive_threads_not_manually_archived(threads_to_maybe_bump) @commands.group(name="bump") async def thread_bump_group(self, ctx: commands.Context) -> None: @@ -100,7 +128,7 @@ class ThreadBumper(commands.Cog): bumped_threads = [k for k, _ in await self.threads_to_bump.items()] if after.id in bumped_threads: - await after.edit(archived=False) + await self.unarchive_threads_not_manually_archived([after]) async def cog_check(self, ctx: commands.Context) -> bool: """Only allow staff & partner roles to invoke the commands in this cog.""" -- cgit v1.2.3 From 3346d71416fdb3223d0c4998f92e420886445fac Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 18 Feb 2022 17:13:31 +0000 Subject: fixup: implemeent code review comments --- bot/exts/utils/thread_bumper.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py index 8c6f3518e..35057f1fe 100644 --- a/bot/exts/utils/thread_bumper.py +++ b/bot/exts/utils/thread_bumper.py @@ -74,7 +74,7 @@ class ThreadBumper(commands.Cog): if not ctx.invoked_subcommand: await ctx.send_help(ctx.command) - @thread_bump_group.command(name="add") + @thread_bump_group.command(name="add", aliases=("a",)) async def add_thread_to_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: """Add a thread to the bump list.""" await self.init_task @@ -85,6 +85,9 @@ class ThreadBumper(commands.Cog): else: raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + if await self.threads_to_bump.contains(thread.id): + raise commands.BadArgument("This thread is already in the bump list.") + await self.threads_to_bump.set(thread.id, "sentinel") await ctx.send(f":ok_hand:{thread.mention} has been added to the bump list.") @@ -99,6 +102,9 @@ class ThreadBumper(commands.Cog): else: raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + if not await self.threads_to_bump.contains(thread.id): + raise commands.BadArgument("This thread is not in the bump list.") + await self.threads_to_bump.delete(thread.id) await ctx.send(f":ok_hand: {thread.mention} has been removed from the bump list.") @@ -126,8 +132,7 @@ class ThreadBumper(commands.Cog): if not after.archived: return - bumped_threads = [k for k, _ in await self.threads_to_bump.items()] - if after.id in bumped_threads: + if await self.threads_to_bump.contains(after.id): await self.unarchive_threads_not_manually_archived([after]) async def cog_check(self, ctx: commands.Context) -> bool: -- cgit v1.2.3