From 690e2de9c6bfc2a2b007bf85a63fe82433784b40 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:09:46 -0400 Subject: [PATCH 01/21] Add forum py --- techsupport_bot/commands/forum.py | 143 ++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 techsupport_bot/commands/forum.py diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py new file mode 100644 index 00000000..b38f7a5f --- /dev/null +++ b/techsupport_bot/commands/forum.py @@ -0,0 +1,143 @@ +""" ""The channel slowmode modification extension +Holds only a single slash command""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, Self + +import discord +from core import auxiliary, cogs +from discord import app_commands +from discord.ext import commands + +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Registers the slowmode cog + + Args: + bot (bot.TechSupportBot): The bot to register the cog to + """ + await bot.add_cog(ForumChannel(bot=bot, extension_name="forum")) + + +class ForumChannel(cogs.BaseCog): + """The cog that holds the slowmode commands and helper functions""" + + forum_group: app_commands.Group = app_commands.Group( + name="forum", description="...", extras={"module": "forum"} + ) + + channel_id = "1288279278839926855" + disallowed_title_patterns = [ + re.compile( + r"^(?:I)?(?:\s)?(?:need|please I need|please|pls|plz)?(?:\s)?help(?:\s)?(?:me|please)?(?:\?|!)?$", + re.IGNORECASE, + ), + re.compile(r"^\S+$"), # Very short single-word titles + re.compile( + r"\b(it('?s)? not working|not working|issue|problem|error)\b", re.IGNORECASE + ), + re.compile(r"\b(urgent|ASAP|quick help|fast)\b", re.IGNORECASE), + re.compile(r"[!?]{3,}"), # Titles with excessive punctuation + ] + + disallowed_body_patterns = [ + re.compile(r"^.{0,14}$"), # Bodies shorter than 15 characters + re.compile(r"^(\[[^\]]*\])?https?://\S+$"), # Only links in the body + ] + + @forum_group.command( + name="solved", + description="Ban someone from making new applications", + extras={"module": "forum"}, + ) + async def markSolved(self: Self, interaction: discord.Interaction) -> None: + channel = await interaction.guild.fetch_channel(int(self.channel_id)) + if interaction.channel.parent == channel: + if interaction.user != interaction.channel.owner: + embed = discord.Embed( + title="Permission Denied", + description="You cannot do this", + color=discord.Color.red(), + ) + await interaction.response.send_message(embed=embed) + return + embed = discord.Embed( + title="Thread Marked as Solved", + description="This thread has been archived and locked.", + color=discord.Color.green(), + ) + await interaction.response.send_message(embed=embed) + await interaction.channel.edit( + name=f"[SOLVED] {interaction.channel.name}"[:100], + archived=True, + locked=True, + ) + + @commands.Cog.listener() + async def on_thread_create(self: Self, thread: discord.Thread) -> None: + channel = await thread.guild.fetch_channel(int(self.channel_id)) + if thread.parent != channel: + return + + embed = discord.Embed( + title="Thread Rejected", + description="Your thread doesn't meet our posting requirements. Please make sure you have a descriptive title and good body.", + color=discord.Color.red(), + ) + + # Check if the thread title is disallowed + if any( + pattern.search(thread.name) for pattern in self.disallowed_title_patterns + ): + await thread.send(embed=embed) + await thread.edit( + name=f"[REJECTED] {thread.name}"[:100], + archived=True, + locked=True, + ) + return + + # Check if the thread body is disallowed + messages = [message async for message in thread.history(limit=5)] + if messages: + body = messages[-1].content + if any(pattern.search(body) for pattern in self.disallowed_body_patterns): + await thread.send(embed=embed) + await thread.edit( + name=f"[REJECTED] {thread.name}"[:100], + archived=True, + locked=True, + ) + return + + # Check if the thread creator has an existing open thread + for existing_thread in channel.threads: + if ( + existing_thread.owner_id == thread.owner_id + and not existing_thread.archived + and existing_thread.id != thread.id + ): + embed = discord.Embed( + title="Duplicate Thread Detected", + description="You already have an open thread. Please continue in your existing thread.", + color=discord.Color.orange(), + ) + await thread.send(embed=embed) + await thread.edit( + name=f"[DUPLICATE] {thread.name}"[:100], + archived=True, + locked=True, + ) + return + + embed = discord.Embed( + title="Welcome!", + description="Your thread has been created successfully!", + color=discord.Color.blue(), + ) + await thread.send(embed=embed) \ No newline at end of file From 8995366587799a892c9bbb9f7e0716318f8042f8 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:55:26 -0400 Subject: [PATCH 02/21] Add thread timeout feature --- techsupport_bot/commands/forum.py | 53 +++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index b38f7a5f..3f1478bc 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -3,10 +3,13 @@ from __future__ import annotations +import asyncio +import datetime import re from typing import TYPE_CHECKING, Self import discord +import munch from core import auxiliary, cogs from discord import app_commands from discord.ext import commands @@ -24,7 +27,7 @@ async def setup(bot: bot.TechSupportBot) -> None: await bot.add_cog(ForumChannel(bot=bot, extension_name="forum")) -class ForumChannel(cogs.BaseCog): +class ForumChannel(cogs.LoopCog): """The cog that holds the slowmode commands and helper functions""" forum_group: app_commands.Group = app_commands.Group( @@ -32,6 +35,7 @@ class ForumChannel(cogs.BaseCog): ) channel_id = "1288279278839926855" + max_age_minutes = 1 disallowed_title_patterns = [ re.compile( r"^(?:I)?(?:\s)?(?:need|please I need|please|pls|plz)?(?:\s)?help(?:\s)?(?:me|please)?(?:\?|!)?$", @@ -137,7 +141,50 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: embed = discord.Embed( title="Welcome!", - description="Your thread has been created successfully!", + description=( + "Your thread has been created successfully!\n" + "Run the command /forum solved when your issue gets solved\n" + ), color=discord.Color.blue(), ) - await thread.send(embed=embed) \ No newline at end of file + await thread.send(embed=embed) + + async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None: + """The main entry point for the loop for kanye + This is executed automatically and shouldn't be called manually + + Args: + config (munch.Munch): The guild config where the loop is taking place + guild (discord.Guild): The guild where the loop is taking place + """ + channel = await guild.fetch_channel(int(self.channel_id)) + for existing_thread in channel.threads: + if not existing_thread.archived and not existing_thread.locked: + most_recent_message_id = existing_thread.last_message_id + most_recent_message = await existing_thread.fetch_message( + most_recent_message_id + ) + if datetime.datetime.now( + datetime.timezone.utc + ) - most_recent_message.created_at > datetime.timedelta( + minutes=self.max_age_minutes + ): + embed = discord.Embed( + title="Old thread archived", + description="This thread it too old and has been closed. You are welcome to create another thread", + color=discord.Color.blurple(), + ) + await existing_thread.send(embed=embed) + await existing_thread.edit( + name=f"[OLD] {existing_thread.name}"[:100], + archived=True, + locked=True, + ) + + async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: + """This sleeps a random amount of time between Kanye quotes + + Args: + config (munch.Munch): The guild config where the loop is taking place + """ + await asyncio.sleep(self.max_age_minutes * 60) From 970f6ff04d2200939a9f3ff803c659bfa9d46a9f Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:59:16 -0400 Subject: [PATCH 03/21] minor changes, cosmetic changes --- techsupport_bot/commands/forum.py | 88 ++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 3f1478bc..bc2db54b 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -30,6 +30,31 @@ async def setup(bot: bot.TechSupportBot) -> None: class ForumChannel(cogs.LoopCog): """The cog that holds the slowmode commands and helper functions""" + # Hard code default embed types + reject_embed = discord.Embed( + title="Thread rejected", + description="Your thread doesn't meet our posting requirements. Please make sure you have a descriptive title and good body.", + color=discord.Color.red(), + ) + + duplicate_embed = discord.Embed( + title="Duplicate thread detected", + description="You already have an open thread. Please continue in your existing thread.", + color=discord.Color.orange(), + ) + + abandoned_embed = discord.Embed( + title="Abandoned thread archived", + description="It appears this thread has been abandoned. You are welcome to create another thread", + color=discord.Color.blurple(), + ) + + solved_embed = discord.Embed( + title="Thread marked as solved", + description="This thread has been archived and locked.", + color=discord.Color.green(), + ) + forum_group: app_commands.Group = app_commands.Group( name="forum", description="...", extras={"module": "forum"} ) @@ -56,31 +81,36 @@ class ForumChannel(cogs.LoopCog): @forum_group.command( name="solved", - description="Ban someone from making new applications", + description="Mark a support forum thread as solved", extras={"module": "forum"}, ) async def markSolved(self: Self, interaction: discord.Interaction) -> None: channel = await interaction.guild.fetch_channel(int(self.channel_id)) - if interaction.channel.parent == channel: + if ( + hasattr(interaction.channel, "parent") + and interaction.channel.parent == channel + ): if interaction.user != interaction.channel.owner: embed = discord.Embed( - title="Permission Denied", + title="Permission denied", description="You cannot do this", color=discord.Color.red(), ) - await interaction.response.send_message(embed=embed) + await interaction.response.send_message(embed=embed, ephemeral=True) return - embed = discord.Embed( - title="Thread Marked as Solved", - description="This thread has been archived and locked.", - color=discord.Color.green(), - ) - await interaction.response.send_message(embed=embed) + await interaction.response.send_message(embed=self.solved_embed) await interaction.channel.edit( name=f"[SOLVED] {interaction.channel.name}"[:100], archived=True, locked=True, ) + else: + embed = discord.Embed( + title="Invalid location", + description="The location this was run isn't a valid support forum", + color=discord.Color.red(), + ) + await interaction.response.send_message(embed=embed, ephemeral=True) @commands.Cog.listener() async def on_thread_create(self: Self, thread: discord.Thread) -> None: @@ -88,17 +118,11 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: if thread.parent != channel: return - embed = discord.Embed( - title="Thread Rejected", - description="Your thread doesn't meet our posting requirements. Please make sure you have a descriptive title and good body.", - color=discord.Color.red(), - ) - # Check if the thread title is disallowed if any( pattern.search(thread.name) for pattern in self.disallowed_title_patterns ): - await thread.send(embed=embed) + await thread.send(embed=self.reject_embed) await thread.edit( name=f"[REJECTED] {thread.name}"[:100], archived=True, @@ -111,7 +135,17 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: if messages: body = messages[-1].content if any(pattern.search(body) for pattern in self.disallowed_body_patterns): - await thread.send(embed=embed) + await thread.send(embed=self.reject_embed) + await thread.edit( + name=f"[REJECTED] {thread.name}"[:100], + archived=True, + locked=True, + ) + return + if body.lower() == thread.name.lower() or len(body.lower()) < len( + thread.name.lower() + ): + await thread.send(embed=self.reject_embed) await thread.edit( name=f"[REJECTED] {thread.name}"[:100], archived=True, @@ -126,12 +160,7 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: and not existing_thread.archived and existing_thread.id != thread.id ): - embed = discord.Embed( - title="Duplicate Thread Detected", - description="You already have an open thread. Please continue in your existing thread.", - color=discord.Color.orange(), - ) - await thread.send(embed=embed) + await thread.send(embed=self.duplicate_embed) await thread.edit( name=f"[DUPLICATE] {thread.name}"[:100], archived=True, @@ -143,7 +172,7 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: title="Welcome!", description=( "Your thread has been created successfully!\n" - "Run the command /forum solved when your issue gets solved\n" + "Run the command when your issue gets solved" ), color=discord.Color.blue(), ) @@ -169,14 +198,9 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None ) - most_recent_message.created_at > datetime.timedelta( minutes=self.max_age_minutes ): - embed = discord.Embed( - title="Old thread archived", - description="This thread it too old and has been closed. You are welcome to create another thread", - color=discord.Color.blurple(), - ) - await existing_thread.send(embed=embed) + await existing_thread.send(embed=self.abandoned_embed) await existing_thread.edit( - name=f"[OLD] {existing_thread.name}"[:100], + name=f"[ABANDONED] {existing_thread.name}"[:100], archived=True, locked=True, ) From 00de935abd26d81f605e5a68d80440e4ff1085d0 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:06:03 -0400 Subject: [PATCH 04/21] Some formatting changes --- techsupport_bot/commands/forum.py | 32 ++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index bc2db54b..e742f288 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -1,5 +1,4 @@ -""" ""The channel slowmode modification extension -Holds only a single slash command""" +"""The support forum management features.s""" from __future__ import annotations @@ -19,7 +18,7 @@ async def setup(bot: bot.TechSupportBot) -> None: - """Registers the slowmode cog + """Registers the forum channel cog Args: bot (bot.TechSupportBot): The bot to register the cog to @@ -28,12 +27,12 @@ async def setup(bot: bot.TechSupportBot) -> None: class ForumChannel(cogs.LoopCog): - """The cog that holds the slowmode commands and helper functions""" + """The cog that holds the forum channel commands and helper functions""" # Hard code default embed types reject_embed = discord.Embed( title="Thread rejected", - description="Your thread doesn't meet our posting requirements. Please make sure you have a descriptive title and good body.", + description="Your thread doesn't meet our posting requirements. Please make sure you have a well written title and a detailed body.", color=discord.Color.red(), ) @@ -67,15 +66,12 @@ class ForumChannel(cogs.LoopCog): re.IGNORECASE, ), re.compile(r"^\S+$"), # Very short single-word titles - re.compile( - r"\b(it('?s)? not working|not working|issue|problem|error)\b", re.IGNORECASE - ), re.compile(r"\b(urgent|ASAP|quick help|fast)\b", re.IGNORECASE), re.compile(r"[!?]{3,}"), # Titles with excessive punctuation ] disallowed_body_patterns = [ - re.compile(r"^.{0,14}$"), # Bodies shorter than 15 characters + re.compile(r"^.{0,29}$"), # Bodies shorter than 15 characters re.compile(r"^(\[[^\]]*\])?https?://\S+$"), # Only links in the body ] @@ -85,6 +81,12 @@ class ForumChannel(cogs.LoopCog): extras={"module": "forum"}, ) async def markSolved(self: Self, interaction: discord.Interaction) -> None: + """A command to mark the thread as solved + Usable by OP and staff + + Args: + interaction (discord.Interaction): The interaction that called the command + """ channel = await interaction.guild.fetch_channel(int(self.channel_id)) if ( hasattr(interaction.channel, "parent") @@ -114,6 +116,11 @@ async def markSolved(self: Self, interaction: discord.Interaction) -> None: @commands.Cog.listener() async def on_thread_create(self: Self, thread: discord.Thread) -> None: + """A listener for threads being created anywhere on the server + + Args: + thread (discord.Thread): The thread that was created + """ channel = await thread.guild.fetch_channel(int(self.channel_id)) if thread.parent != channel: return @@ -179,8 +186,7 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: await thread.send(embed=embed) async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None: - """The main entry point for the loop for kanye - This is executed automatically and shouldn't be called manually + """This is what closes threads after inactivity Args: config (munch.Munch): The guild config where the loop is taking place @@ -206,9 +212,9 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None ) async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: - """This sleeps a random amount of time between Kanye quotes + """This waits and rechecks every 5 minutes to search for old threads Args: config (munch.Munch): The guild config where the loop is taking place """ - await asyncio.sleep(self.max_age_minutes * 60) + await asyncio.sleep(5) From 16d103ff12fa13c8050d8bcf4a818459fc6df605 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:09:55 -0400 Subject: [PATCH 05/21] Formatting the second --- techsupport_bot/commands/forum.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index e742f288..27526f67 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -9,7 +9,7 @@ import discord import munch -from core import auxiliary, cogs +from core import cogs from discord import app_commands from discord.ext import commands @@ -32,13 +32,19 @@ class ForumChannel(cogs.LoopCog): # Hard code default embed types reject_embed = discord.Embed( title="Thread rejected", - description="Your thread doesn't meet our posting requirements. Please make sure you have a well written title and a detailed body.", + description=( + "Your thread doesn't meet our posting requirements. Please make sure you have " + "a well written title and a detailed body." + ), color=discord.Color.red(), ) duplicate_embed = discord.Embed( title="Duplicate thread detected", - description="You already have an open thread. Please continue in your existing thread.", + description=( + "You already have an open thread. " + "Please continue in your existing thread." + ), color=discord.Color.orange(), ) @@ -61,12 +67,13 @@ class ForumChannel(cogs.LoopCog): channel_id = "1288279278839926855" max_age_minutes = 1 disallowed_title_patterns = [ + # pylint: disable=C0301 re.compile( r"^(?:I)?(?:\s)?(?:need|please I need|please|pls|plz)?(?:\s)?help(?:\s)?(?:me|please)?(?:\?|!)?$", re.IGNORECASE, ), re.compile(r"^\S+$"), # Very short single-word titles - re.compile(r"\b(urgent|ASAP|quick help|fast)\b", re.IGNORECASE), + re.compile(r"\b(urgent|ASAP|quick help|fast help)\b", re.IGNORECASE), re.compile(r"[!?]{3,}"), # Titles with excessive punctuation ] From 2804bc9f44df5e06f10347603da1fc9e3508f1ba Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:11:58 -0400 Subject: [PATCH 06/21] formatting --- techsupport_bot/commands/forum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 27526f67..7f51f674 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -67,8 +67,8 @@ class ForumChannel(cogs.LoopCog): channel_id = "1288279278839926855" max_age_minutes = 1 disallowed_title_patterns = [ - # pylint: disable=C0301 re.compile( + # pylint: disable=C0301 r"^(?:I)?(?:\s)?(?:need|please I need|please|pls|plz)?(?:\s)?help(?:\s)?(?:me|please)?(?:\?|!)?$", re.IGNORECASE, ), From eacf71acca9c9b2dc8689ccd2a3913be3d5eabf9 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:14:39 -0400 Subject: [PATCH 07/21] Final formatting? --- techsupport_bot/commands/forum.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 7f51f674..f72f6a2d 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -50,7 +50,10 @@ class ForumChannel(cogs.LoopCog): abandoned_embed = discord.Embed( title="Abandoned thread archived", - description="It appears this thread has been abandoned. You are welcome to create another thread", + description=( + "It appears this thread has been abandoned. " + "You are welcome to create another thread" + ), color=discord.Color.blurple(), ) From 1c13fe2ef3e3097513fcda0d4c5fadf33509861b Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:36:25 -0400 Subject: [PATCH 08/21] move to config system for many things --- techsupport_bot/commands/forum.py | 82 +++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index f72f6a2d..8f414007 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -9,7 +9,7 @@ import discord import munch -from core import cogs +from core import cogs, extensionconfig from discord import app_commands from discord.ext import commands @@ -23,7 +23,37 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot to register the cog to """ + config = extensionconfig.ExtensionConfig() + config.add( + key="forum_channel_id", + datatype="str", + title="forum channel", + description="The forum channel id as a string to manage threads in", + default="", + ), + config.add( + key="max_age_minutes", + datatype="int", + title="Max age in minutes", + description="The max age of a thread before it times out", + default=1440, + ), + config.add( + key="title_regex_list", + datatype="list[str]", + title="List of regex to ban in titles", + description="List of regex to ban in titles", + default=["^\S+$"], + ), + config.add( + key="body_regex_list", + datatype="list[str]", + title="List of regex to ban in bodies", + description="List of regex to ban in bodies", + default=["^\S+$"], + ) await bot.add_cog(ForumChannel(bot=bot, extension_name="forum")) + bot.add_extension_config("forum", config) class ForumChannel(cogs.LoopCog): @@ -67,24 +97,6 @@ class ForumChannel(cogs.LoopCog): name="forum", description="...", extras={"module": "forum"} ) - channel_id = "1288279278839926855" - max_age_minutes = 1 - disallowed_title_patterns = [ - re.compile( - # pylint: disable=C0301 - r"^(?:I)?(?:\s)?(?:need|please I need|please|pls|plz)?(?:\s)?help(?:\s)?(?:me|please)?(?:\?|!)?$", - re.IGNORECASE, - ), - re.compile(r"^\S+$"), # Very short single-word titles - re.compile(r"\b(urgent|ASAP|quick help|fast help)\b", re.IGNORECASE), - re.compile(r"[!?]{3,}"), # Titles with excessive punctuation - ] - - disallowed_body_patterns = [ - re.compile(r"^.{0,29}$"), # Bodies shorter than 15 characters - re.compile(r"^(\[[^\]]*\])?https?://\S+$"), # Only links in the body - ] - @forum_group.command( name="solved", description="Mark a support forum thread as solved", @@ -97,7 +109,10 @@ async def markSolved(self: Self, interaction: discord.Interaction) -> None: Args: interaction (discord.Interaction): The interaction that called the command """ - channel = await interaction.guild.fetch_channel(int(self.channel_id)) + config = self.bot.guild_configs[str(interaction.guild.id)] + channel = await interaction.guild.fetch_channel( + int(config.extensions.forum.forum_channel_id.value) + ) if ( hasattr(interaction.channel, "parent") and interaction.channel.parent == channel @@ -131,14 +146,18 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: Args: thread (discord.Thread): The thread that was created """ - channel = await thread.guild.fetch_channel(int(self.channel_id)) + config = self.bot.guild_configs[str(thread.guild.id)] + channel = await thread.guild.fetch_channel( + int(config.extensions.forum.forum_channel_id.value) + ) if thread.parent != channel: return + disallowed_title_patterns = create_regex_list( + config.extensions.forum.title_regex_list.value + ) # Check if the thread title is disallowed - if any( - pattern.search(thread.name) for pattern in self.disallowed_title_patterns - ): + if any(pattern.search(thread.name) for pattern in disallowed_title_patterns): await thread.send(embed=self.reject_embed) await thread.edit( name=f"[REJECTED] {thread.name}"[:100], @@ -151,7 +170,10 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: messages = [message async for message in thread.history(limit=5)] if messages: body = messages[-1].content - if any(pattern.search(body) for pattern in self.disallowed_body_patterns): + disallowed_body_patterns = create_regex_list( + config.extensions.forum.body_regex_list.value + ) + if any(pattern.search(body) for pattern in disallowed_body_patterns): await thread.send(embed=self.reject_embed) await thread.edit( name=f"[REJECTED] {thread.name}"[:100], @@ -202,7 +224,9 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None config (munch.Munch): The guild config where the loop is taking place guild (discord.Guild): The guild where the loop is taking place """ - channel = await guild.fetch_channel(int(self.channel_id)) + channel = await guild.fetch_channel( + int(config.extensions.forum.forum_channel_id.value) + ) for existing_thread in channel.threads: if not existing_thread.archived and not existing_thread.locked: most_recent_message_id = existing_thread.last_message_id @@ -212,7 +236,7 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None if datetime.datetime.now( datetime.timezone.utc ) - most_recent_message.created_at > datetime.timedelta( - minutes=self.max_age_minutes + minutes=config.extensions.forum.max_age_minutes.value ): await existing_thread.send(embed=self.abandoned_embed) await existing_thread.edit( @@ -228,3 +252,7 @@ async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: config (munch.Munch): The guild config where the loop is taking place """ await asyncio.sleep(5) + + +def create_regex_list(str_list: list[str]) -> list[re.Pattern[str]]: + return [re.compile(p, re.IGNORECASE) for p in str_list] From f92e2ce00b53acd5ad444c5769c0a9047a0722a3 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:15:12 -0400 Subject: [PATCH 09/21] Make most embed messages configurable --- techsupport_bot/commands/forum.py | 105 ++++++++++++++++++------------ 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 8f414007..140082c2 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -1,4 +1,4 @@ -"""The support forum management features.s""" +"""The support forum management features""" from __future__ import annotations @@ -51,48 +51,45 @@ async def setup(bot: bot.TechSupportBot) -> None: title="List of regex to ban in bodies", description="List of regex to ban in bodies", default=["^\S+$"], + ), + config.add( + key="reject_message", + datatype="str", + title="The message displayed on rejected threads", + description="The message displayed on rejected threads", + default="thread rejected", + ), + config.add( + key="duplicate_message", + datatype="str", + title="The message displayed on duplicated threads", + description="The message displayed on duplicated threads", + default="thread duplicated", + ), + config.add( + key="solve_message", + datatype="str", + title="The message displayed on solved threads", + description="The message displayed on solved threads", + default="thread solved", + ), + config.add( + key="abandoned_message", + datatype="str", + title="The message displayed on abandoned threads", + description="The message displayed on abandoned threads", + default="thread abandoned", ) await bot.add_cog(ForumChannel(bot=bot, extension_name="forum")) bot.add_extension_config("forum", config) class ForumChannel(cogs.LoopCog): - """The cog that holds the forum channel commands and helper functions""" - - # Hard code default embed types - reject_embed = discord.Embed( - title="Thread rejected", - description=( - "Your thread doesn't meet our posting requirements. Please make sure you have " - "a well written title and a detailed body." - ), - color=discord.Color.red(), - ) - - duplicate_embed = discord.Embed( - title="Duplicate thread detected", - description=( - "You already have an open thread. " - "Please continue in your existing thread." - ), - color=discord.Color.orange(), - ) - - abandoned_embed = discord.Embed( - title="Abandoned thread archived", - description=( - "It appears this thread has been abandoned. " - "You are welcome to create another thread" - ), - color=discord.Color.blurple(), - ) - - solved_embed = discord.Embed( - title="Thread marked as solved", - description="This thread has been archived and locked.", - color=discord.Color.green(), - ) + """The cog that holds the forum channel commands and helper functions + Attributes: + forum_group (app_commands.Group): The group for the /forum commands + """ forum_group: app_commands.Group = app_commands.Group( name="forum", description="...", extras={"module": "forum"} ) @@ -125,7 +122,12 @@ async def markSolved(self: Self, interaction: discord.Interaction) -> None: ) await interaction.response.send_message(embed=embed, ephemeral=True) return - await interaction.response.send_message(embed=self.solved_embed) + solved_embed = discord.Embed( + title="Thread marked as solved", + description=config.extensions.forum.solve_message.value, + color=discord.Color.green(), + ) + await interaction.response.send_message(embed=solved_embed) await interaction.channel.edit( name=f"[SOLVED] {interaction.channel.name}"[:100], archived=True, @@ -156,9 +158,16 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: disallowed_title_patterns = create_regex_list( config.extensions.forum.title_regex_list.value ) + + reject_embed = discord.Embed( + title="Thread rejected", + description=config.extensions.forum.reject_message.value, + color=discord.Color.red(), + ) + # Check if the thread title is disallowed if any(pattern.search(thread.name) for pattern in disallowed_title_patterns): - await thread.send(embed=self.reject_embed) + await thread.send(embed=reject_embed) await thread.edit( name=f"[REJECTED] {thread.name}"[:100], archived=True, @@ -174,7 +183,7 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: config.extensions.forum.body_regex_list.value ) if any(pattern.search(body) for pattern in disallowed_body_patterns): - await thread.send(embed=self.reject_embed) + await thread.send(embed=reject_embed) await thread.edit( name=f"[REJECTED] {thread.name}"[:100], archived=True, @@ -184,7 +193,7 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: if body.lower() == thread.name.lower() or len(body.lower()) < len( thread.name.lower() ): - await thread.send(embed=self.reject_embed) + await thread.send(embed=reject_embed) await thread.edit( name=f"[REJECTED] {thread.name}"[:100], archived=True, @@ -199,7 +208,13 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: and not existing_thread.archived and existing_thread.id != thread.id ): - await thread.send(embed=self.duplicate_embed) + duplicate_embed = discord.Embed( + title="Duplicate thread detected", + description=config.extensions.forum.duplicate_message.value, + color=discord.Color.orange(), + ) + + await thread.send(embed=duplicate_embed) await thread.edit( name=f"[DUPLICATE] {thread.name}"[:100], archived=True, @@ -238,7 +253,13 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None ) - most_recent_message.created_at > datetime.timedelta( minutes=config.extensions.forum.max_age_minutes.value ): - await existing_thread.send(embed=self.abandoned_embed) + abandoned_embed = discord.Embed( + title="Abandoned thread archived", + description=config.extensions.forum.abandoned_message.value, + color=discord.Color.blurple(), + ) + + await existing_thread.send(embed=abandoned_embed) await existing_thread.edit( name=f"[ABANDONED] {existing_thread.name}"[:100], archived=True, From 3394a007dde782d528b0dd0fea30a581b1490bca Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:18:36 -0400 Subject: [PATCH 10/21] formatting --- techsupport_bot/commands/forum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 140082c2..a3742053 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -90,6 +90,7 @@ class ForumChannel(cogs.LoopCog): Attributes: forum_group (app_commands.Group): The group for the /forum commands """ + forum_group: app_commands.Group = app_commands.Group( name="forum", description="...", extras={"module": "forum"} ) From 8bf1fb112b7b31816b1f4826c1a34c77e298867d Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:21:55 -0400 Subject: [PATCH 11/21] test --- techsupport_bot/commands/forum.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index a3742053..76387ebc 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -43,14 +43,14 @@ async def setup(bot: bot.TechSupportBot) -> None: datatype="list[str]", title="List of regex to ban in titles", description="List of regex to ban in titles", - default=["^\S+$"], + default=[""], ), config.add( key="body_regex_list", datatype="list[str]", title="List of regex to ban in bodies", description="List of regex to ban in bodies", - default=["^\S+$"], + default=[""], ), config.add( key="reject_message", From 567e19c0b931919b9c96096eee9715a85c838a43 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:23:28 -0400 Subject: [PATCH 12/21] fixed config --- techsupport_bot/commands/forum.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 76387ebc..4ec5cb40 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -30,49 +30,49 @@ async def setup(bot: bot.TechSupportBot) -> None: title="forum channel", description="The forum channel id as a string to manage threads in", default="", - ), + ) config.add( key="max_age_minutes", datatype="int", title="Max age in minutes", description="The max age of a thread before it times out", default=1440, - ), + ) config.add( key="title_regex_list", datatype="list[str]", title="List of regex to ban in titles", description="List of regex to ban in titles", default=[""], - ), + ) config.add( key="body_regex_list", datatype="list[str]", title="List of regex to ban in bodies", description="List of regex to ban in bodies", default=[""], - ), + ) config.add( key="reject_message", datatype="str", title="The message displayed on rejected threads", description="The message displayed on rejected threads", default="thread rejected", - ), + ) config.add( key="duplicate_message", datatype="str", title="The message displayed on duplicated threads", description="The message displayed on duplicated threads", default="thread duplicated", - ), + ) config.add( key="solve_message", datatype="str", title="The message displayed on solved threads", description="The message displayed on solved threads", default="thread solved", - ), + ) config.add( key="abandoned_message", datatype="str", From 5d9f2f86cc4dfd75374a46de4b717025669b4672 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:27:35 -0400 Subject: [PATCH 13/21] Add doc string --- techsupport_bot/commands/forum.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 4ec5cb40..342bea47 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -277,4 +277,12 @@ async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: def create_regex_list(str_list: list[str]) -> list[re.Pattern[str]]: + """This turns a list of strings into a list of complied regex + + Args: + str_list (list[str]): The list of string versions of regexs + + Returns: + list[re.Pattern[str]]: The compiled list of regex for later use + """ return [re.compile(p, re.IGNORECASE) for p in str_list] From 4fc98ca130e8cf885cb421d3186c92cce2c7ab22 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:42:45 -0400 Subject: [PATCH 14/21] Add new commands, make functions to close threads --- techsupport_bot/commands/forum.py | 269 ++++++++++++++++++++++-------- 1 file changed, 196 insertions(+), 73 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 342bea47..ed06f541 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -9,7 +9,7 @@ import discord import munch -from core import cogs, extensionconfig +from core import auxiliary, cogs, extensionconfig from discord import app_commands from discord.ext import commands @@ -80,6 +80,13 @@ async def setup(bot: bot.TechSupportBot) -> None: description="The message displayed on abandoned threads", default="thread abandoned", ) + config.add( + key="staff_role_ids", + datatype="list[int]", + title="List of role ids as ints for staff, able to mark threads solved/abandoned/rejected", + description="List of role ids as ints for staff, able to mark threads solved/abandoned/rejected", + default=[], + ) await bot.add_cog(ForumChannel(bot=bot, extension_name="forum")) bot.add_extension_config("forum", config) @@ -107,40 +114,128 @@ async def markSolved(self: Self, interaction: discord.Interaction) -> None: Args: interaction (discord.Interaction): The interaction that called the command """ + await interaction.response.defer(ephemeral=True) config = self.bot.guild_configs[str(interaction.guild.id)] channel = await interaction.guild.fetch_channel( int(config.extensions.forum.forum_channel_id.value) ) - if ( - hasattr(interaction.channel, "parent") - and interaction.channel.parent == channel + + invalid_embed = discord.Embed( + title="Invalid location", + description="The location this was run isn't a valid support forum", + color=discord.Color.red(), + ) + + if not hasattr(interaction.channel, "parent"): + await interaction.followup.send(embed=invalid_embed, ephemeral=True) + return + if not interaction.channel.parent == channel: + await interaction.followup.send(embed=invalid_embed, ephemeral=True) + return + + if not ( + interaction.user == interaction.channel.owner + or is_thread_staff(interaction.user, interaction.guild, config) ): - if interaction.user != interaction.channel.owner: - embed = discord.Embed( - title="Permission denied", - description="You cannot do this", - color=discord.Color.red(), - ) - await interaction.response.send_message(embed=embed, ephemeral=True) - return - solved_embed = discord.Embed( - title="Thread marked as solved", - description=config.extensions.forum.solve_message.value, - color=discord.Color.green(), + embed = discord.Embed( + title="Permission denied", + description="You cannot do this", + color=discord.Color.red(), ) - await interaction.response.send_message(embed=solved_embed) - await interaction.channel.edit( - name=f"[SOLVED] {interaction.channel.name}"[:100], - archived=True, - locked=True, + await interaction.followup.send(embed=embed, ephemeral=True) + return + + embed = auxiliary.prepare_confirm_embed("Thread marked as solved!") + await interaction.followup.send(embed=embed, ephemeral=True) + await mark_thread_solved(interaction.channel, config) + + @forum_group.command( + name="reject", + description="Mark a support forum thread as solved", + extras={"module": "forum"}, + ) + async def markRejected(self: Self, interaction: discord.Interaction) -> None: + """A command to mark the thread as rejected + Usable by staff + + Args: + interaction (discord.Interaction): The interaction that called the command + """ + await interaction.response.defer(ephemeral=True) + config = self.bot.guild_configs[str(interaction.guild.id)] + channel = await interaction.guild.fetch_channel( + int(config.extensions.forum.forum_channel_id.value) + ) + + invalid_embed = discord.Embed( + title="Invalid location", + description="The location this was run isn't a valid support forum", + color=discord.Color.red(), + ) + + if not hasattr(interaction.channel, "parent"): + await interaction.followup.send(embed=invalid_embed, ephemeral=True) + return + if not interaction.channel.parent == channel: + await interaction.followup.send(embed=invalid_embed, ephemeral=True) + return + + if not (is_thread_staff(interaction.user, interaction.guild, config)): + embed = discord.Embed( + title="Permission denied", + description="You cannot do this", + color=discord.Color.red(), ) - else: + await interaction.followup.send(embed=embed, ephemeral=True) + return + + embed = auxiliary.prepare_confirm_embed("Thread marked as rejected!") + await interaction.followup.send(embed=embed, ephemeral=True) + await mark_thread_rejected(interaction.channel, config) + + @forum_group.command( + name="abandon", + description="Mark a support forum thread as solved", + extras={"module": "forum"}, + ) + async def markAbandoned(self: Self, interaction: discord.Interaction) -> None: + """A command to mark the thread as abandoned + Usable by staff + + Args: + interaction (discord.Interaction): The interaction that called the command + """ + await interaction.response.defer(ephemeral=True) + config = self.bot.guild_configs[str(interaction.guild.id)] + channel = await interaction.guild.fetch_channel( + int(config.extensions.forum.forum_channel_id.value) + ) + + invalid_embed = discord.Embed( + title="Invalid location", + description="The location this was run isn't a valid support forum", + color=discord.Color.red(), + ) + + if not hasattr(interaction.channel, "parent"): + await interaction.followup.send(embed=invalid_embed, ephemeral=True) + return + if not interaction.channel.parent == channel: + await interaction.followup.send(embed=invalid_embed, ephemeral=True) + return + + if not (is_thread_staff(interaction.user, interaction.guild, config)): embed = discord.Embed( - title="Invalid location", - description="The location this was run isn't a valid support forum", + title="Permission denied", + description="You cannot do this", color=discord.Color.red(), ) - await interaction.response.send_message(embed=embed, ephemeral=True) + await interaction.followup.send(embed=embed, ephemeral=True) + return + + embed = auxiliary.prepare_confirm_embed("Thread marked as abandoned!") + await interaction.followup.send(embed=embed, ephemeral=True) + await mark_thread_abandoned(interaction.channel, config) @commands.Cog.listener() async def on_thread_create(self: Self, thread: discord.Thread) -> None: @@ -160,20 +255,9 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: config.extensions.forum.title_regex_list.value ) - reject_embed = discord.Embed( - title="Thread rejected", - description=config.extensions.forum.reject_message.value, - color=discord.Color.red(), - ) - # Check if the thread title is disallowed if any(pattern.search(thread.name) for pattern in disallowed_title_patterns): - await thread.send(embed=reject_embed) - await thread.edit( - name=f"[REJECTED] {thread.name}"[:100], - archived=True, - locked=True, - ) + await mark_thread_rejected(thread, config) return # Check if the thread body is disallowed @@ -184,22 +268,12 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: config.extensions.forum.body_regex_list.value ) if any(pattern.search(body) for pattern in disallowed_body_patterns): - await thread.send(embed=reject_embed) - await thread.edit( - name=f"[REJECTED] {thread.name}"[:100], - archived=True, - locked=True, - ) + await mark_thread_rejected(thread, config) return if body.lower() == thread.name.lower() or len(body.lower()) < len( thread.name.lower() ): - await thread.send(embed=reject_embed) - await thread.edit( - name=f"[REJECTED] {thread.name}"[:100], - archived=True, - locked=True, - ) + await mark_thread_rejected(thread, config) return # Check if the thread creator has an existing open thread @@ -209,18 +283,7 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: and not existing_thread.archived and existing_thread.id != thread.id ): - duplicate_embed = discord.Embed( - title="Duplicate thread detected", - description=config.extensions.forum.duplicate_message.value, - color=discord.Color.orange(), - ) - - await thread.send(embed=duplicate_embed) - await thread.edit( - name=f"[DUPLICATE] {thread.name}"[:100], - archived=True, - locked=True, - ) + await mark_thread_duplicated(thread, config) return embed = discord.Embed( @@ -254,18 +317,7 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None ) - most_recent_message.created_at > datetime.timedelta( minutes=config.extensions.forum.max_age_minutes.value ): - abandoned_embed = discord.Embed( - title="Abandoned thread archived", - description=config.extensions.forum.abandoned_message.value, - color=discord.Color.blurple(), - ) - - await existing_thread.send(embed=abandoned_embed) - await existing_thread.edit( - name=f"[ABANDONED] {existing_thread.name}"[:100], - archived=True, - locked=True, - ) + await mark_thread_abandoned(existing_thread, config) async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: """This waits and rechecks every 5 minutes to search for old threads @@ -286,3 +338,74 @@ def create_regex_list(str_list: list[str]) -> list[re.Pattern[str]]: list[re.Pattern[str]]: The compiled list of regex for later use """ return [re.compile(p, re.IGNORECASE) for p in str_list] + + +def is_thread_staff( + user: discord.User, guild: discord.Guild, config: munch.Munch +) -> bool: + if staff_roles := config.extensions.forum.staff_role_ids.value: + roles = (discord.utils.get(guild.roles, id=int(role)) for role in staff_roles) + status = any((role in user.roles for role in roles)) + if status: + return True + return False + + +async def mark_thread_solved(thread: discord.Thread, config: munch.Munch) -> None: + solved_embed = discord.Embed( + title="Thread marked as solved", + description=config.extensions.forum.solve_message.value, + color=discord.Color.green(), + ) + + await thread.send(content=thread.owner.mention, embed=solved_embed) + await thread.edit( + name=f"[SOLVED] {thread.name}"[:100], + archived=True, + locked=True, + ) + + +async def mark_thread_rejected(thread: discord.Thread, config: munch.Munch) -> None: + reject_embed = discord.Embed( + title="Thread rejected", + description=config.extensions.forum.reject_message.value, + color=discord.Color.red(), + ) + + await thread.send(content=thread.owner.mention, embed=reject_embed) + await thread.edit( + name=f"[REJECTED] {thread.name}"[:100], + archived=True, + locked=True, + ) + + +async def mark_thread_duplicated(thread: discord.Thread, config: munch.Munch) -> None: + duplicate_embed = discord.Embed( + title="Duplicate thread detected", + description=config.extensions.forum.duplicate_message.value, + color=discord.Color.orange(), + ) + + await thread.send(content=thread.owner.mention, embed=duplicate_embed) + await thread.edit( + name=f"[DUPLICATE] {thread.name}"[:100], + archived=True, + locked=True, + ) + + +async def mark_thread_abandoned(thread: discord.Thread, config: munch.Munch) -> None: + abandoned_embed = discord.Embed( + title="Abandoned thread archived", + description=config.extensions.forum.abandoned_message.value, + color=discord.Color.blurple(), + ) + + await thread.send(content=thread.owner.mention, embed=abandoned_embed) + await thread.edit( + name=f"[ABANDONED] {thread.name}"[:100], + archived=True, + locked=True, + ) From 99e53fc2631f4bbe9199f15f6dbaa1f9370a2cac Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 18:19:57 -0400 Subject: [PATCH 15/21] add a show unsolved command --- techsupport_bot/commands/forum.py | 35 ++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index ed06f541..fd4a26b9 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -4,6 +4,7 @@ import asyncio import datetime +import random import re from typing import TYPE_CHECKING, Self @@ -151,7 +152,7 @@ async def markSolved(self: Self, interaction: discord.Interaction) -> None: @forum_group.command( name="reject", - description="Mark a support forum thread as solved", + description="Mark a support forum thread as rejected", extras={"module": "forum"}, ) async def markRejected(self: Self, interaction: discord.Interaction) -> None: @@ -195,7 +196,7 @@ async def markRejected(self: Self, interaction: discord.Interaction) -> None: @forum_group.command( name="abandon", - description="Mark a support forum thread as solved", + description="Mark a support forum thread as abandoned", extras={"module": "forum"}, ) async def markAbandoned(self: Self, interaction: discord.Interaction) -> None: @@ -237,6 +238,34 @@ async def markAbandoned(self: Self, interaction: discord.Interaction) -> None: await interaction.followup.send(embed=embed, ephemeral=True) await mark_thread_abandoned(interaction.channel, config) + @forum_group.command( + name="get-unsolved", + description="Gets a collection of unsolved issues", + extras={"module": "forum"}, + ) + async def showUnsolved(self: Self, interaction: discord.Interaction) -> None: + """A command to mark the thread as abandoned + Usable by all + + Args: + interaction (discord.Interaction): The interaction that called the command + """ + await interaction.response.defer(ephemeral=True) + config = self.bot.guild_configs[str(interaction.guild.id)] + channel = await interaction.guild.fetch_channel( + int(config.extensions.forum.forum_channel_id.value) + ) + mention_threads = "\n".join( + [ + thread.mention + for thread in random.sample( + channel.threads, min(len(channel.threads), 5) + ) + ] + ) + embed = discord.Embed(title="Unsolved", description=mention_threads) + await interaction.followup.send(embed=embed, ephemeral=True) + @commands.Cog.listener() async def on_thread_create(self: Self, thread: discord.Thread) -> None: """A listener for threads being created anywhere on the server @@ -325,7 +354,7 @@ async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: Args: config (munch.Munch): The guild config where the loop is taking place """ - await asyncio.sleep(5) + await asyncio.sleep(300) def create_regex_list(str_list: list[str]) -> list[re.Pattern[str]]: From ac54b52b276a4a9b4f633d7fa82272ad24b3dbfd Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:40:59 -0400 Subject: [PATCH 16/21] Configurable welcome message --- techsupport_bot/commands/forum.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index fd4a26b9..97f6ad7e 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -88,6 +88,13 @@ async def setup(bot: bot.TechSupportBot) -> None: description="List of role ids as ints for staff, able to mark threads solved/abandoned/rejected", default=[], ) + config.add( + key="welcome_message", + datatype="str", + title="The message displayed on new threads", + description="The message displayed on new threads", + default="thread welcome", + ) await bot.add_cog(ForumChannel(bot=bot, extension_name="forum")) bot.add_extension_config("forum", config) @@ -317,10 +324,7 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: embed = discord.Embed( title="Welcome!", - description=( - "Your thread has been created successfully!\n" - "Run the command when your issue gets solved" - ), + description=config.extensions.forum.welcome_message.value, color=discord.Color.blue(), ) await thread.send(embed=embed) From 3668e27dcf54b72e2cfea1034c706f12a555a0f4 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:13:05 -0700 Subject: [PATCH 17/21] Improve the way threads get status changes --- techsupport_bot/commands/forum.py | 284 +++++++++++++++--------------- 1 file changed, 139 insertions(+), 145 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 97f6ad7e..24a37db5 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -74,6 +74,13 @@ async def setup(bot: bot.TechSupportBot) -> None: description="The message displayed on solved threads", default="thread solved", ) + config.add( + key="close_message", + datatype="str", + title="The message displayed on closed threads", + description="The message displayed on closed threads", + default="thread closed", + ) config.add( key="abandoned_message", datatype="str", @@ -99,6 +106,40 @@ async def setup(bot: bot.TechSupportBot) -> None: bot.add_extension_config("forum", config) +STATUS_CONFIG = { + "solved": { + "title": "Thread marked as solved", + "prefix": "[SOLVED]", + "color": discord.Color.green(), + "message_key": "solve_message", + }, + "closed": { + "title": "Thread marked as closed", + "prefix": "[CLOSED]", + "color": discord.Color.red(), + "message_key": "close_message", + }, + "rejected": { + "title": "Thread rejected", + "prefix": "[REJECTED]", + "color": discord.Color.red(), + "message_key": "reject_message", + }, + "duplicate": { + "title": "Duplicate thread detected", + "prefix": "[DUPLICATE]", + "color": discord.Color.orange(), + "message_key": "duplicate_message", + }, + "abandoned": { + "title": "Abandoned thread archived", + "prefix": "[ABANDONED]", + "color": discord.Color.blurple(), + "message_key": "abandoned_message", + }, +} + + class ForumChannel(cogs.LoopCog): """The cog that holds the forum channel commands and helper functions @@ -111,20 +152,19 @@ class ForumChannel(cogs.LoopCog): ) @forum_group.command( - name="solved", - description="Mark a support forum thread as solved", + name="mark", + description="Mark a support forum thread", extras={"module": "forum"}, ) - async def markSolved(self: Self, interaction: discord.Interaction) -> None: - """A command to mark the thread as solved - Usable by OP and staff - - Args: - interaction (discord.Interaction): The interaction that called the command - """ + async def mark_thread_command( + self: Self, + interaction: discord.Interaction, + status: str, + ) -> None: await interaction.response.defer(ephemeral=True) + config = self.bot.guild_configs[str(interaction.guild.id)] - channel = await interaction.guild.fetch_channel( + forum_channel = await interaction.guild.fetch_channel( int(config.extensions.forum.forum_channel_id.value) ) @@ -134,61 +174,36 @@ async def markSolved(self: Self, interaction: discord.Interaction) -> None: color=discord.Color.red(), ) - if not hasattr(interaction.channel, "parent"): - await interaction.followup.send(embed=invalid_embed, ephemeral=True) - return - if not interaction.channel.parent == channel: + # Check 1: Ensure command was run in the forum channel + if ( + not hasattr(interaction.channel, "parent") + or interaction.channel.parent != forum_channel + ): await interaction.followup.send(embed=invalid_embed, ephemeral=True) return - if not ( - interaction.user == interaction.channel.owner - or is_thread_staff(interaction.user, interaction.guild, config) - ): + is_staff = is_thread_staff(interaction.user, interaction.guild, config) + is_owner = interaction.user == interaction.channel.owner + + # Check 2: Ensure status is valid + if status not in ("solved", "closed", "rejected", "abandoned"): embed = discord.Embed( - title="Permission denied", - description="You cannot do this", + title="Invalid status", + description="That status is not valid", color=discord.Color.red(), ) await interaction.followup.send(embed=embed, ephemeral=True) return - embed = auxiliary.prepare_confirm_embed("Thread marked as solved!") - await interaction.followup.send(embed=embed, ephemeral=True) - await mark_thread_solved(interaction.channel, config) - - @forum_group.command( - name="reject", - description="Mark a support forum thread as rejected", - extras={"module": "forum"}, - ) - async def markRejected(self: Self, interaction: discord.Interaction) -> None: - """A command to mark the thread as rejected - Usable by staff - - Args: - interaction (discord.Interaction): The interaction that called the command - """ - await interaction.response.defer(ephemeral=True) - config = self.bot.guild_configs[str(interaction.guild.id)] - channel = await interaction.guild.fetch_channel( - int(config.extensions.forum.forum_channel_id.value) - ) - - invalid_embed = discord.Embed( - title="Invalid location", - description="The location this was run isn't a valid support forum", - color=discord.Color.red(), - ) - - if not hasattr(interaction.channel, "parent"): - await interaction.followup.send(embed=invalid_embed, ephemeral=True) - return - if not interaction.channel.parent == channel: - await interaction.followup.send(embed=invalid_embed, ephemeral=True) - return + if status in ("rejected", "abandoned") and not is_staff: + denied = True + elif status in ("solved", "closed") and not (is_staff or is_owner): + denied = True + else: + denied = False - if not (is_thread_staff(interaction.user, interaction.guild, config)): + # Check 3: Ensure permissions are valid + if denied: embed = discord.Embed( title="Permission denied", description="You cannot do this", @@ -197,53 +212,62 @@ async def markRejected(self: Self, interaction: discord.Interaction) -> None: await interaction.followup.send(embed=embed, ephemeral=True) return - embed = auxiliary.prepare_confirm_embed("Thread marked as rejected!") - await interaction.followup.send(embed=embed, ephemeral=True) - await mark_thread_rejected(interaction.channel, config) + confirm_embed = auxiliary.prepare_confirm_embed(f"Thread marked as {status}!") + await interaction.followup.send(embed=confirm_embed, ephemeral=True) - @forum_group.command( - name="abandon", - description="Mark a support forum thread as abandoned", - extras={"module": "forum"}, - ) - async def markAbandoned(self: Self, interaction: discord.Interaction) -> None: - """A command to mark the thread as abandoned - Usable by staff + await mark_thread(interaction.channel, config, status) + + @mark_thread_command.autocomplete("status") + async def status_autocomplete( + self: Self, + interaction: discord.Interaction, + current: str, + ) -> list[app_commands.Choice[str]]: + """This is the autocomplete function for status on the thread mark command + This parses a list of valid statuses and shows the user the list they can actually use Args: - interaction (discord.Interaction): The interaction that called the command + self (Self): _description_ + interaction (discord.Interaction): The interaction that is calling the command + current (str): The current choice the user is typing + + Returns: + list[app_commands.Choice[str]]: The list of all valid choices + that fit with the users current selection """ - await interaction.response.defer(ephemeral=True) + config = self.bot.guild_configs[str(interaction.guild.id)] - channel = await interaction.guild.fetch_channel( - int(config.extensions.forum.forum_channel_id.value) - ) - invalid_embed = discord.Embed( - title="Invalid location", - description="The location this was run isn't a valid support forum", - color=discord.Color.red(), + is_staff = is_thread_staff(interaction.user, interaction.guild, config) + is_owner = ( + hasattr(interaction.channel, "owner") + and interaction.user == interaction.channel.owner ) - if not hasattr(interaction.channel, "parent"): - await interaction.followup.send(embed=invalid_embed, ephemeral=True) - return - if not interaction.channel.parent == channel: - await interaction.followup.send(embed=invalid_embed, ephemeral=True) - return + choices = [] + + # Staff can do all 4 options + if is_staff: + choices.extend( + [ + app_commands.Choice(name="Rejected", value="rejected"), + app_commands.Choice(name="Abandoned", value="abandoned"), + app_commands.Choice(name="Closed", value="closed"), + app_commands.Choice(name="Solved", value="solved"), + ] + ) - if not (is_thread_staff(interaction.user, interaction.guild, config)): - embed = discord.Embed( - title="Permission denied", - description="You cannot do this", - color=discord.Color.red(), + # The OP can mark their thread closed or solved, but not rejected or abandoned + elif is_owner: + choices.extend( + [ + app_commands.Choice(name="Closed", value="closed"), + app_commands.Choice(name="Solved", value="solved"), + ] ) - await interaction.followup.send(embed=embed, ephemeral=True) - return - embed = auxiliary.prepare_confirm_embed("Thread marked as abandoned!") - await interaction.followup.send(embed=embed, ephemeral=True) - await mark_thread_abandoned(interaction.channel, config) + # This just filters out anything not matching what the user is typing + return [choice for choice in choices if current.lower() in choice.name.lower()] @forum_group.command( name="get-unsolved", @@ -293,7 +317,7 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: # Check if the thread title is disallowed if any(pattern.search(thread.name) for pattern in disallowed_title_patterns): - await mark_thread_rejected(thread, config) + await mark_thread(thread, config, "rejected") return # Check if the thread body is disallowed @@ -304,12 +328,12 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: config.extensions.forum.body_regex_list.value ) if any(pattern.search(body) for pattern in disallowed_body_patterns): - await mark_thread_rejected(thread, config) + await mark_thread(thread, config, "rejected") return if body.lower() == thread.name.lower() or len(body.lower()) < len( thread.name.lower() ): - await mark_thread_rejected(thread, config) + await mark_thread(thread, config, "rejected") return # Check if the thread creator has an existing open thread @@ -319,7 +343,7 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: and not existing_thread.archived and existing_thread.id != thread.id ): - await mark_thread_duplicated(thread, config) + await mark_thread(thread, config, "duplicate") return embed = discord.Embed( @@ -350,7 +374,7 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None ) - most_recent_message.created_at > datetime.timedelta( minutes=config.extensions.forum.max_age_minutes.value ): - await mark_thread_abandoned(existing_thread, config) + await mark_thread(existing_thread, config, "abandoned") async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: """This waits and rechecks every 5 minutes to search for old threads @@ -384,61 +408,31 @@ def is_thread_staff( return False -async def mark_thread_solved(thread: discord.Thread, config: munch.Munch) -> None: - solved_embed = discord.Embed( - title="Thread marked as solved", - description=config.extensions.forum.solve_message.value, - color=discord.Color.green(), - ) - - await thread.send(content=thread.owner.mention, embed=solved_embed) - await thread.edit( - name=f"[SOLVED] {thread.name}"[:100], - archived=True, - locked=True, - ) - - -async def mark_thread_rejected(thread: discord.Thread, config: munch.Munch) -> None: - reject_embed = discord.Embed( - title="Thread rejected", - description=config.extensions.forum.reject_message.value, - color=discord.Color.red(), - ) - - await thread.send(content=thread.owner.mention, embed=reject_embed) - await thread.edit( - name=f"[REJECTED] {thread.name}"[:100], - archived=True, - locked=True, - ) +async def mark_thread( + thread: discord.Thread, + config: munch.Munch, + status: str, +) -> None: + """This modifies a thread, can be marked as any of the options in STATUS_CONFIG + No validation is done, assuming data passed here is always valid + Args: + thread (discord.Thread): The thread to modify + config (munch.Munch): The guild config + status (str): The status to modify the thread with + """ + data = STATUS_CONFIG[status] -async def mark_thread_duplicated(thread: discord.Thread, config: munch.Munch) -> None: - duplicate_embed = discord.Embed( - title="Duplicate thread detected", - description=config.extensions.forum.duplicate_message.value, - color=discord.Color.orange(), + embed = discord.Embed( + title=data["title"], + description=getattr(config.extensions.forum, data["message_key"]).value, + color=data["color"], ) - await thread.send(content=thread.owner.mention, embed=duplicate_embed) - await thread.edit( - name=f"[DUPLICATE] {thread.name}"[:100], - archived=True, - locked=True, - ) - - -async def mark_thread_abandoned(thread: discord.Thread, config: munch.Munch) -> None: - abandoned_embed = discord.Embed( - title="Abandoned thread archived", - description=config.extensions.forum.abandoned_message.value, - color=discord.Color.blurple(), - ) + await thread.send(content=thread.owner.mention, embed=embed) - await thread.send(content=thread.owner.mention, embed=abandoned_embed) await thread.edit( - name=f"[ABANDONED] {thread.name}"[:100], + name=f"{data['prefix']} {thread.name}"[:100], archived=True, locked=True, ) From 39558d0edc86c4ad3b4578be96abf19765781312 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:46:32 -0700 Subject: [PATCH 18/21] Paginate unsolved command --- techsupport_bot/commands/forum.py | 42 +++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 24a37db5..516b8f53 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -10,6 +10,7 @@ import discord import munch +import ui from core import auxiliary, cogs, extensionconfig from discord import app_commands from discord.ext import commands @@ -270,7 +271,7 @@ async def status_autocomplete( return [choice for choice in choices if current.lower() in choice.name.lower()] @forum_group.command( - name="get-unsolved", + name="unsolved", description="Gets a collection of unsolved issues", extras={"module": "forum"}, ) @@ -286,16 +287,37 @@ async def showUnsolved(self: Self, interaction: discord.Interaction) -> None: channel = await interaction.guild.fetch_channel( int(config.extensions.forum.forum_channel_id.value) ) - mention_threads = "\n".join( - [ - thread.mention - for thread in random.sample( - channel.threads, min(len(channel.threads), 5) - ) - ] + mention_threads: list[discord.Thread] = channel.threads + if len(mention_threads) == 0: + embed = discord.Embed( + title="Unsolved", + description="No unsolved issues. Hopefully not a bug", + color=discord.Color.blurple(), + ) + await interaction.followup.send(embed=embed, ephemeral=True) + return + # To prevent bias, we randomize the open threads + random.shuffle(mention_threads) + embeds = [] + index = 1 + running_desc = "" + embed = discord.Embed(title="Unsolved", color=discord.Color.blurple()) + for thread in mention_threads: + if index % 10 == 0: + embed.description = running_desc + embeds.append(embed) + embed = discord.Embed(title="Unsolved", color=discord.Color.blurple()) + running_desc = "" + running_desc += f"{thread.name}: {thread.mention}\n" + index += 1 + + embed.description = running_desc + embeds.append(embed) + + view = ui.PaginateView() + await view.send( + interaction.channel, interaction.user, embeds, interaction, True ) - embed = discord.Embed(title="Unsolved", description=mention_threads) - await interaction.followup.send(embed=embed, ephemeral=True) @commands.Cog.listener() async def on_thread_create(self: Self, thread: discord.Thread) -> None: From 5fd415a6821c3cf1f434d3607bd80cf14e4f4ba6 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:13:32 -0700 Subject: [PATCH 19/21] Add reasons and authors to thread marking --- techsupport_bot/commands/forum.py | 68 +++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 516b8f53..502258c6 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -158,10 +158,16 @@ class ForumChannel(cogs.LoopCog): extras={"module": "forum"}, ) async def mark_thread_command( - self: Self, - interaction: discord.Interaction, - status: str, + self: Self, interaction: discord.Interaction, status: str, reason: str = "" ) -> None: + """This is the command to change the status of a thread + This has autofill for stauts and does permissions checks + + Args: + interaction (discord.Interaction): The interaction calling the command + status (str): The status to change the command to + reason (str): The reason the status is being changed. Defaults to "" + """ await interaction.response.defer(ephemeral=True) config = self.bot.guild_configs[str(interaction.guild.id)] @@ -216,7 +222,7 @@ async def mark_thread_command( confirm_embed = auxiliary.prepare_confirm_embed(f"Thread marked as {status}!") await interaction.followup.send(embed=confirm_embed, ephemeral=True) - await mark_thread(interaction.channel, config, status) + await mark_thread(interaction.channel, config, status, reason, interaction.user) @mark_thread_command.autocomplete("status") async def status_autocomplete( @@ -339,7 +345,15 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: # Check if the thread title is disallowed if any(pattern.search(thread.name) for pattern in disallowed_title_patterns): - await mark_thread(thread, config, "rejected") + await mark_thread( + thread, + config, + "rejected", + reason=( + "Your thread doesn't meet our posting requirements. " + "Please make sure you have a well written title and a detailed body." + ), + ) return # Check if the thread body is disallowed @@ -350,12 +364,28 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: config.extensions.forum.body_regex_list.value ) if any(pattern.search(body) for pattern in disallowed_body_patterns): - await mark_thread(thread, config, "rejected") + await mark_thread( + thread, + config, + "rejected", + reason=( + "Your thread doesn't meet our posting requirements. " + "Please make sure you have a well written title and a detailed body." + ), + ) return if body.lower() == thread.name.lower() or len(body.lower()) < len( thread.name.lower() ): - await mark_thread(thread, config, "rejected") + await mark_thread( + thread, + config, + "rejected", + reason=( + "Your thread doesn't meet our posting requirements. " + "Please make sure you have a well written title and a detailed body." + ), + ) return # Check if the thread creator has an existing open thread @@ -365,7 +395,15 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: and not existing_thread.archived and existing_thread.id != thread.id ): - await mark_thread(thread, config, "duplicate") + await mark_thread( + thread, + config, + "duplicate", + reason=( + "You are only allowed to have 1 open thread at any time. " + f"You must use {existing_thread.mention}" + ), + ) return embed = discord.Embed( @@ -434,6 +472,8 @@ async def mark_thread( thread: discord.Thread, config: munch.Munch, status: str, + reason: str, + editor: discord.Member | None = None, ) -> None: """This modifies a thread, can be marked as any of the options in STATUS_CONFIG No validation is done, assuming data passed here is always valid @@ -442,6 +482,8 @@ async def mark_thread( thread (discord.Thread): The thread to modify config (munch.Munch): The guild config status (str): The status to modify the thread with + reason (str): The reason the thread was changed + editor (discord.Member | None): The user who edited the thread """ data = STATUS_CONFIG[status] @@ -450,6 +492,16 @@ async def mark_thread( description=getattr(config.extensions.forum, data["message_key"]).value, color=data["color"], ) + # If there is a reason, add the reason to the embed + if reason: + embed.add_field(name="Reason", value=reason) + + # If an editor was passed, it was done by a human + # Otherwise, mark is as being done automatically + if editor: + embed.set_footer(text=f"Changed by {editor.display_name}") + else: + embed.set_footer(text="Changed automatically") await thread.send(content=thread.owner.mention, embed=embed) From c8e5ad511150b91937250ab451a6e76b47b1c46c Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:17:47 -0700 Subject: [PATCH 20/21] Formatting --- techsupport_bot/commands/forum.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 502258c6..b4571cda 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -92,8 +92,8 @@ async def setup(bot: bot.TechSupportBot) -> None: config.add( key="staff_role_ids", datatype="list[int]", - title="List of role ids as ints for staff, able to mark threads solved/abandoned/rejected", - description="List of role ids as ints for staff, able to mark threads solved/abandoned/rejected", + title="Staff role ids as ints able to mark threads solved/abandoned/rejected", + description="Staff role ids as ints able to mark threads solved/abandoned/rejected", default=[], ) config.add( @@ -434,7 +434,12 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None ) - most_recent_message.created_at > datetime.timedelta( minutes=config.extensions.forum.max_age_minutes.value ): - await mark_thread(existing_thread, config, "abandoned") + await mark_thread( + existing_thread, + config, + "abandoned", + "Threads are automatically closed after periods of no activity", + ) async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: """This waits and rechecks every 5 minutes to search for old threads @@ -458,8 +463,19 @@ def create_regex_list(str_list: list[str]) -> list[re.Pattern[str]]: def is_thread_staff( - user: discord.User, guild: discord.Guild, config: munch.Munch + user: discord.Member, guild: discord.Guild, config: munch.Munch ) -> bool: + """This checks if a user is staff in a given thread + This uses the staff roles config + + Args: + user (discord.Member): The user to check + guild (discord.Guild): The guild this thread is in + config (munch.Munch): The config of the guild + + Returns: + bool: Whether the user is staff or not + """ if staff_roles := config.extensions.forum.staff_role_ids.value: roles = (discord.utils.get(guild.roles, id=int(role)) for role in staff_roles) status = any((role in user.roles for role in roles)) From c555cdee3af05f746d25d3d70c0f3d7705266e45 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:20:18 -0700 Subject: [PATCH 21/21] Formatting 2 --- techsupport_bot/commands/forum.py | 1 - 1 file changed, 1 deletion(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index b4571cda..1f8f3338 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -234,7 +234,6 @@ async def status_autocomplete( This parses a list of valid statuses and shows the user the list they can actually use Args: - self (Self): _description_ interaction (discord.Interaction): The interaction that is calling the command current (str): The current choice the user is typing