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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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)