From 6cd508c9fc455a0840141e101ac947b39e644e15 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 16:45:58 -0700 Subject: [PATCH 01/40] The start of the new config system --- .gitignore | 2 + docker-compose.yml | 1 + techsupport_bot/commands/application.py | 141 +++++------------- techsupport_bot/configuration/__init__.py | 3 + .../configuration/config.default.json | 11 ++ .../configuration/config.meta.json | 45 ++++++ techsupport_bot/configuration/config.py | 113 ++++++++++++++ 7 files changed, 211 insertions(+), 105 deletions(-) create mode 100644 techsupport_bot/configuration/__init__.py create mode 100644 techsupport_bot/configuration/config.default.json create mode 100644 techsupport_bot/configuration/config.meta.json create mode 100644 techsupport_bot/configuration/config.py diff --git a/.gitignore b/.gitignore index 4dae09124..b907d27db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env +.venv config.yml *.pyc .vscode @@ -8,3 +9,4 @@ __pycache__ .coverage .hypothesis .idea/ +techsupport_bot/configuration/guild_configs diff --git a/docker-compose.yml b/docker-compose.yml index 9e2eee68d..3c1ebe59a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: stop_signal: SIGINT volumes: - ./config.yml:/var/TechSupportBot/techsupport_bot/config.yml + - ./techsupport_bot/configuration/guild_configs:/var/TechSupportBot/techsupport_bot/configuration/guild_configs networks: - all depends_on: diff --git a/techsupport_bot/commands/application.py b/techsupport_bot/commands/application.py index 70530850e..f374a5c16 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -7,10 +7,11 @@ from typing import TYPE_CHECKING, Self import aiocron +import configuration import discord import munch import ui -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord import app_commands if TYPE_CHECKING: @@ -40,91 +41,8 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="management_channel", - datatype="str", - title="ID of the staff side channel", - description=( - "The ID of the channel the application notifications and reminders should" - " appear in" - ), - default=None, - ) - config.add( - key="notification_channels", - datatype="list", - title="List of channels to get application", - description=( - "The list of channel IDs that should receive periodic messages about the" - " application, with a button to apply" - ), - default=None, - ) - config.add( - key="reminder_cron_config", - datatype="string", - title="Cronjob config for the reminder about pending applications for staff", - description=( - "Crontab syntax for executing pending reminder events (example: 0 17 * * *)" - ), - default="0 17 * * *", - ) - config.add( - key="notification_cron_config", - datatype="string", - title="Cronjob config for the user facing notification", - description=( - "Crontab syntax for users being notified about the application (example: 0" - " */3 * * *)" - ), - default="0 */3 * * *", - ) - config.add( - key="application_message", - datatype="str", - title="Message on the application reminder", - description=( - "The message to show users when they are prompted to apply in the" - " notification_channels" - ), - default="Apply now!", - ) - config.add( - key="application_role", - datatype="str", - title="ID of the role to give applicants", - description=( - "The ID of the role to give applicants when there application is approved" - ), - default=None, - ) - config.add( - key="manage_roles", - datatype="list", - title="Manage application roles", - description=( - "The role IDs required to manage the applications (not required to apply)" - ), - default=[], - ) - config.add( - key="ping_role", - datatype="str", - title="New application ping role", - description="The ID of the role to ping when a new application is created", - default="", - ) - config.add( - key="max_age", - datatype="int", - title="Max days an application can live", - description="After this many days, the system will auto reject the applications.", - default=30, - ) await bot.add_cog(ApplicationManager(bot=bot, extension_name="application")) await bot.add_cog(ApplicationNotifier(bot=bot, extension_name="application")) - bot.add_extension_config("application", config) async def command_permission_check(interaction: discord.Interaction) -> bool: @@ -141,15 +59,11 @@ async def command_permission_check(interaction: discord.Interaction) -> bool: Returns: bool: Will return true if the command is allowed to execute, false if it should not execute """ - # Get the bot object for easier access - bot = interaction.client - - # Get the config - config = bot.guild_configs[str(interaction.guild.id)] - # Gets permitted roles allowed_roles = [] - for role_id in config.extensions.application.manage_roles.value: + for role_id in configuration.get_config_entry( + interaction.guild.id, "application_manage_role_ids" + ): if not role_id: continue role = interaction.guild.get_role(int(role_id)) @@ -182,7 +96,7 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None config (munch.Munch): The guild config for the executing loop guild (discord.Guild): The guild the loop is executing for """ - channels = config.extensions.application.notification_channels.value + channels = configuration.get_config_entry(guild.id, "application_notification_channels") for channel in channels: channel = guild.get_channel(int(channel)) if not channel: @@ -190,7 +104,9 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None await ui.AppNotice(timeout=None).send( channel=channel, - message=config.extensions.application.application_message.value, + message=configuration.get_config_entry( + guild.id, "application_application_message" + ), ) async def wait(self: Self, config: munch.Munch, guild: discord.Guild) -> None: @@ -201,7 +117,7 @@ async def wait(self: Self, config: munch.Munch, guild: discord.Guild) -> None: guild (discord.Guild): The guild the loop is executing for """ await aiocron.crontab( - config.extensions.application.notification_cron_config.value + configuration.get_config_entry(guild.id, "application_notification_cron_config") ).next() @@ -688,12 +604,12 @@ async def handle_new_application( # Find the channel to send to config = self.bot.guild_configs[str(applicant.guild.id)] channel = applicant.guild.get_channel( - int(config.extensions.application.management_channel.value) + int(configuration.get_config_entry(applicant.guild.id, "application_management_channel")) ) # Send notice to staff channel role = applicant.guild.get_role( - int(config.extensions.application.ping_role.value) + int(configuration.get_config_entry(applicant.guild.id, "application_ping_role")) ) content_string = "" if role: @@ -721,7 +637,11 @@ async def check_if_can_apply(self: Self, applicant: discord.Member) -> bool: """ config = self.bot.guild_configs[str(applicant.guild.id)] role = applicant.guild.get_role( - int(config.extensions.application.application_role.value) + int( + configuration.get_config_entry( + applicant.guild.id, "application_application_role_id" + ) + ) ) # Don't allow applications if extension is disabled if "application" not in config.enabled_extensions: @@ -737,7 +657,9 @@ async def check_if_can_apply(self: Self, applicant: discord.Member) -> bool: # Don't allow users who can manage the applications to apply allowed_roles = [] - for role_id in config.extensions.application.manage_roles.value: + for role_id in configuration.get_config_entry( + applicant.guild.id, "application_manage_role_ids" + ): role = applicant.guild.get_role(int(role_id)) if not role: continue @@ -763,8 +685,13 @@ async def get_application_role( discord.Role | None: Will return the role object from the guild, or none if the role could not be found """ - config = self.bot.guild_configs[str(guild.id)] - role = guild.get_role(int(config.extensions.application.application_role.value)) + role = guild.get_role( + int( + configuration.get_config_entry( + guild.id, "application_application_role_id" + ) + ) + ) return role async def notify_for_application_change( @@ -824,7 +751,7 @@ async def notify_for_application_change( config = self.bot.guild_configs[str(interaction.guild.id)] management_channel = interaction.guild.get_channel( - int(config.extensions.application.management_channel.value) + int(configuration.get_config_entry(interaction.guild.id, "application_management_channel")) ) embed.description = confirm_message + f"\n{message}" @@ -921,7 +848,7 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None guild (discord.Guild): The guild the loop is executing for """ channel = guild.get_channel( - int(config.extensions.application.management_channel.value) + int(configuration.get_config_entry(guild.id, "application_management_channel")) ) if not channel: return @@ -950,7 +877,7 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None continue # Application has been pending for max_age days - max_age_config = config.extensions.application.max_age.value + max_age_config = configuration.get_config_entry(guild.id, "application_max_age") if app.application_time < datetime.datetime.now() - datetime.timedelta( days=max_age_config ): @@ -972,7 +899,11 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None await app.update(applicant_name=user.name).apply() role = guild.get_role( - int(config.extensions.application.application_role.value) + int( + configuration.get_config_entry( + guild.id, "application_application_role_id" + ) + ) ) # User has the helper role @@ -1022,5 +953,5 @@ async def wait(self: Self, config: munch.Munch, guild: discord.Guild) -> None: guild (discord.Guild): The guild the loop is executing for """ await aiocron.crontab( - config.extensions.application.reminder_cron_config.value + configuration.get_config_entry(guild.id, "application_reminder_cron_config") ).next() diff --git a/techsupport_bot/configuration/__init__.py b/techsupport_bot/configuration/__init__.py new file mode 100644 index 000000000..bacda799d --- /dev/null +++ b/techsupport_bot/configuration/__init__.py @@ -0,0 +1,3 @@ +"""This is the bot config library""" + +from .config import * diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json new file mode 100644 index 000000000..491ad599c --- /dev/null +++ b/techsupport_bot/configuration/config.default.json @@ -0,0 +1,11 @@ +{ + "application_application_message": "Apply Default!", + "application_application_role_id": "", + "application_manage_role_ids": [], + "application_management_channel": "", + "application_max_age": 30, + "application_notification_channels": [], + "application_notification_cron_config": "0 */3 * * *", + "application_ping_role": "", + "application_reminder_cron_config": "0 17 * * *" +} diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json new file mode 100644 index 000000000..addf53b51 --- /dev/null +++ b/techsupport_bot/configuration/config.meta.json @@ -0,0 +1,45 @@ +{ + "application_application_message": { + "datatype": "str", + "description": "The message to show users when they are prompted to apply in the notification_channels" + }, + "application_application_role_id": { + "datatype": "discord.Role", + "description": "The role to give applicants when their application is approved" + }, + "application_manage_role_ids": { + "datatype": "list[discord.Role]", + "description": "The roles required to manage the applications (not required to apply)" + }, + "application_management_channel": { + "datatype": "discord.TextChannel", + "description": "The channel the application notifications and reminders should appear in" + }, + "application_max_age": { + "datatype": "int", + "description": "After this many days, the system will auto reject the applications" + }, + "application_notification_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The channels that should receive periodic messages about the application, with a button to apply" + }, + "application_notification_cron_config": { + "datatype": "CronTab", + "description": "CronTab for users being notified about the application" + }, + "application_ping_role": { + "datatype": "discord.Role", + "description": "The role to ping when a new application is created" + }, + "application_reminder_cron_config": { + "datatype": "CronTab", + "description": "Crontab for executing pending reminder events" + } +} + + + +"": { + "datatype": "", + "description": "" + } diff --git a/techsupport_bot/configuration/config.py b/techsupport_bot/configuration/config.py new file mode 100644 index 000000000..53dd06124 --- /dev/null +++ b/techsupport_bot/configuration/config.py @@ -0,0 +1,113 @@ +""" +Normal usage: +get_config_entry(guild, key) +get_default_config_entry(key) + + +/config commands: +get_json_config(guild) +update_json_config(guild, config_json) + +Backend commands: +generate_blank_config_file() +check_key_valid(key) + +""" + +import json +from pathlib import Path +from typing import Any + +import munch + +BASE_PATH = "configuration/" + + +def get_config_entry(guild_id: int, key: str) -> Any: + """This searches for a guild specific config entry + + Args: + guild_id (int): The ID of the guild + key (str): The config key to look for + + Returns: + Any: The value of the config, which may be of many types + """ + + if not check_key_valid(key): + raise AttributeError(f"Key {key} is invalid") + + default_entry = get_default_config_entry(key) + + if not does_guild_config_exist(guild_id): + return default_entry + + guild_config = read_json_file(f"guild_configs/{guild_id}.json") + + if key in guild_config: + return guild_config[key] + return default_entry + + +def get_default_config_entry(key: str) -> Any: + """This gets the value from the default config file for the passed key + + Args: + key (str): The key to search the config file for + + Returns: + Any: The value from the default config file + """ + if not check_key_valid(key): + raise AttributeError(f"Key {key} is invalid") + + default_config = read_json_file("config.default.json") + + return default_config[key] + + +# WORKING +def check_key_valid(key: str) -> bool: + """This will check if the key is valid and present in default config + + Args: + key (str): The key to check for validity + + Returns: + bool: True if the key exists, false if it doesn't + """ + default_config = read_json_file("config.default.json") + + if key in default_config: + return True + return False + + +def does_guild_config_exist(guild_id: int) -> bool: + """This checks if a guild specific config file exists + + Args: + guild_id (int): The ID of the guild to look for + + Returns: + bool: True if exists, false if doesn't + """ + path = Path(f"{BASE_PATH}guild_configs/{guild_id}.json") + + if path.exists(): + return True + return False + + +def read_json_file(path: str) -> munch.Munch: + """This reads a json file from disk and parses it into a munch.Munch + This functions assumes the json file exists + + Args: + path (str): The path of the json file to read + + Returns: + munch.Munch: The read json file + """ + with open(f"{BASE_PATH}{path}", encoding="utf-8") as file: + return munch.munchify(json.load(file)) From 1719232c3582716d657f5e9b7033ed09dc852ac9 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 16:47:24 -0700 Subject: [PATCH 02/40] Clean up some unused variables in application --- techsupport_bot/commands/application.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/techsupport_bot/commands/application.py b/techsupport_bot/commands/application.py index f374a5c16..856db0bb7 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -602,7 +602,6 @@ async def handle_new_application( await application.create() # Find the channel to send to - config = self.bot.guild_configs[str(applicant.guild.id)] channel = applicant.guild.get_channel( int(configuration.get_config_entry(applicant.guild.id, "application_management_channel")) ) @@ -749,7 +748,6 @@ async def notify_for_application_change( await interaction.response.send_message(embed=embed) - config = self.bot.guild_configs[str(interaction.guild.id)] management_channel = interaction.guild.get_channel( int(configuration.get_config_entry(interaction.guild.id, "application_management_channel")) ) From 3cd54653815d9cd2036fef96e02214524f7d55c9 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 16:48:54 -0700 Subject: [PATCH 03/40] Format with black --- techsupport_bot/commands/application.py | 36 ++++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/techsupport_bot/commands/application.py b/techsupport_bot/commands/application.py index 856db0bb7..49f7ae978 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -96,7 +96,9 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None config (munch.Munch): The guild config for the executing loop guild (discord.Guild): The guild the loop is executing for """ - channels = configuration.get_config_entry(guild.id, "application_notification_channels") + channels = configuration.get_config_entry( + guild.id, "application_notification_channels" + ) for channel in channels: channel = guild.get_channel(int(channel)) if not channel: @@ -117,7 +119,9 @@ async def wait(self: Self, config: munch.Munch, guild: discord.Guild) -> None: guild (discord.Guild): The guild the loop is executing for """ await aiocron.crontab( - configuration.get_config_entry(guild.id, "application_notification_cron_config") + configuration.get_config_entry( + guild.id, "application_notification_cron_config" + ) ).next() @@ -603,12 +607,20 @@ async def handle_new_application( # Find the channel to send to channel = applicant.guild.get_channel( - int(configuration.get_config_entry(applicant.guild.id, "application_management_channel")) + int( + configuration.get_config_entry( + applicant.guild.id, "application_management_channel" + ) + ) ) # Send notice to staff channel role = applicant.guild.get_role( - int(configuration.get_config_entry(applicant.guild.id, "application_ping_role")) + int( + configuration.get_config_entry( + applicant.guild.id, "application_ping_role" + ) + ) ) content_string = "" if role: @@ -749,7 +761,11 @@ async def notify_for_application_change( await interaction.response.send_message(embed=embed) management_channel = interaction.guild.get_channel( - int(configuration.get_config_entry(interaction.guild.id, "application_management_channel")) + int( + configuration.get_config_entry( + interaction.guild.id, "application_management_channel" + ) + ) ) embed.description = confirm_message + f"\n{message}" @@ -846,7 +862,11 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None guild (discord.Guild): The guild the loop is executing for """ channel = guild.get_channel( - int(configuration.get_config_entry(guild.id, "application_management_channel")) + int( + configuration.get_config_entry( + guild.id, "application_management_channel" + ) + ) ) if not channel: return @@ -875,7 +895,9 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None continue # Application has been pending for max_age days - max_age_config = configuration.get_config_entry(guild.id, "application_max_age") + max_age_config = configuration.get_config_entry( + guild.id, "application_max_age" + ) if app.application_time < datetime.datetime.now() - datetime.timedelta( days=max_age_config ): From d120fa1b0a97f9aca12625dc9a5a859365276b30 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 17:22:05 -0700 Subject: [PATCH 04/40] Migrate automod alert channel --- techsupport_bot/commands/relay.py | 7 ++++++- techsupport_bot/configuration/config.default.json | 3 ++- techsupport_bot/configuration/config.meta.json | 4 ++++ techsupport_bot/functions/automod.py | 14 ++++++-------- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index 4aee8d976..d75aba2d8 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Self +import configuration import discord import irc.client import munch @@ -432,7 +433,11 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No embed.color = discord.Color.red() try: alert_channel = discord_channel.guild.get_channel( - int(config.extensions.automod.alert_channel.value) + int( + configuration.get_config_entry( + discord_channel.guild.id, "automod_alert_channel" + ) + ) ) except TypeError: alert_channel = None diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 491ad599c..5f53381ee 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -7,5 +7,6 @@ "application_notification_channels": [], "application_notification_cron_config": "0 */3 * * *", "application_ping_role": "", - "application_reminder_cron_config": "0 17 * * *" + "application_reminder_cron_config": "0 17 * * *", + "automod_alert_channel": "" } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index addf53b51..4d1f58ecd 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -34,6 +34,10 @@ "application_reminder_cron_config": { "datatype": "CronTab", "description": "Crontab for executing pending reminder events" + }, + "automod_alert_channel": { + "datatype": "discord.TextChannel", + "description": "The channel to send auto-protect alerts to" } } diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index be57fa4ef..da99a9408 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Self +import configuration import discord import munch from botlogging import LogContext, LogLevel @@ -62,13 +63,6 @@ async def setup(bot: bot.TechSupportBot) -> None: ), default=[], ) - config.add( - key="alert_channel", - datatype="int", - title="Alert channel ID", - description="The ID of the channel to send auto-protect alerts to", - default=None, - ) config.add( key="max_mentions", datatype="int", @@ -300,7 +294,11 @@ async def response( try: alert_channel = ctx.guild.get_channel( - int(config.extensions.automod.alert_channel.value) + int( + configuration.get_config_entry( + ctx.guild.id, "automod_alert_channel" + ) + ) ) except TypeError: alert_channel = None From 28e5e2fd335b4d95298a68ab9ba3eb0c2b01bd56 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 17:46:04 -0700 Subject: [PATCH 05/40] Migrate some more config items --- techsupport_bot/bot.py | 6 ++-- techsupport_bot/botlogging/logger.py | 10 +++---- techsupport_bot/commands/hangman.py | 13 ++------- .../configuration/config.default.json | 8 +++++- .../configuration/config.meta.json | 25 +++++++++++++++++ techsupport_bot/functions/autoreact.py | 28 +++++++------------ techsupport_bot/functions/nickname.py | 3 +- 7 files changed, 54 insertions(+), 39 deletions(-) diff --git a/techsupport_bot/bot.py b/techsupport_bot/bot.py index eefe70a6e..d876887b9 100644 --- a/techsupport_bot/bot.py +++ b/techsupport_bot/bot.py @@ -13,6 +13,7 @@ from typing import Self import botlogging +import configuration import discord import expiringdict import gino @@ -845,10 +846,7 @@ async def get_prefix(self: Self, message: discord.Message) -> str: Returns: str: The string of the command prefix by the bot, for the given guild """ - guild_config = self.guild_configs[str(message.guild.id)] - return getattr( - guild_config, "command_prefix", self.file_config.bot_config.default_prefix - ) + return configuration.get_config_entry(message.guild.id, "core_command_prefix") # Can run command checks diff --git a/techsupport_bot/botlogging/logger.py b/techsupport_bot/botlogging/logger.py index b2dd32829..a2cec65d2 100644 --- a/techsupport_bot/botlogging/logger.py +++ b/techsupport_bot/botlogging/logger.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Self import botlogging.embed as embed_lib +import configuration import discord from .common import LogContext, LogLevel @@ -140,15 +141,14 @@ async def check_if_should_log( if not context.guild: return True - # Get the guilds config - config = self.bot.guild_configs[str(context.guild.id)] - # Checking to see if guild logging is enabled - if not config.enable_logging: + if not configuration.get_config_entry(context.guild.id, "core_enable_logging"): return False # Checking to see if log occured in private channels - if context.channel and str(context.channel.id) in config.private_channels: + if context.channel and str( + context.channel.id + ) in configuration.get_config_entry(context.guild.id, "core_private_channels"): return False return True diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index aafef1522..e1791484d 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -6,6 +6,7 @@ import uuid from typing import TYPE_CHECKING, Self +import configuration import discord import ui from core import auxiliary, cogs, extensionconfig @@ -489,18 +490,10 @@ async def generate_game_embed( """ hangman_drawing = game.draw_hang_state() hangman_word = game.draw_word_state() - # Determine the guild ID - guild_id = None - if isinstance(ctx_or_interaction, commands.Context): - guild_id = ctx_or_interaction.guild.id if ctx_or_interaction.guild else None - elif isinstance(ctx_or_interaction, discord.Interaction): - guild_id = ctx_or_interaction.guild_id + guild_id = ctx_or_interaction.guild.id # Fetch the prefix manually since get_prefix expects a Message - if guild_id and str(guild_id) in self.bot.guild_configs: - prefix = self.bot.guild_configs[str(guild_id)].command_prefix - else: - prefix = self.file_config.bot_config.default_prefix + prefix = configuration.get_config_entry(guild_id, "core_command_prefix") embed = discord.Embed( title=f"`{hangman_word}`", diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 5f53381ee..82221b83e 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -8,5 +8,11 @@ "application_notification_cron_config": "0 */3 * * *", "application_ping_role": "", "application_reminder_cron_config": "0 17 * * *", - "automod_alert_channel": "" + "automod_alert_channel": "", + "autoreact_react_map": { "hello": "👋" }, + "core_command_prefix": ".", + "core_enable_logging": true, + "core_nickname_filter": false, + "core_private_channels": [], + "core_guild_id": "" } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index 4d1f58ecd..cedf2e170 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -38,7 +38,32 @@ "automod_alert_channel": { "datatype": "discord.TextChannel", "description": "The channel to send auto-protect alerts to" + }, + "autoreact_react_map": { + "datatype": "dict[str, str]", + "description": "Lowercase phrase to reaction wanted" + }, + "core_command_prefix": { + "datatype": "str", + "description": "The prefix to use for legacy commands" + }, + "core_enable_logging": { + "datatype": "bool", + "description": "Whether the botlogging module should be enabled. When disabled, core logs will all be turned off" + }, + "core_nickname_filter": { + "datatype": "bool", + "description": "Whether to run the nickname filter or not" + }, + "core_private_channels": { + "datatype": "list[discord.TextChannel]", + "description": "A list of channels to exclude from logging events" + }, + "core_guild_id": { + "datatype": "discord.Guild", + "description": "This is used as a validation tool, the guild ID that this config belongs to" } + } diff --git a/techsupport_bot/functions/autoreact.py b/techsupport_bot/functions/autoreact.py index affced92d..f436d1195 100644 --- a/techsupport_bot/functions/autoreact.py +++ b/techsupport_bot/functions/autoreact.py @@ -4,8 +4,9 @@ from typing import TYPE_CHECKING, Self +import configuration import munch -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: @@ -18,24 +19,14 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="react_map", - datatype="dict", - title="Mapping of phrases", - description="Lowercase phrase to reaction wanted", - default={"hello": "👋"}, - ) - await bot.add_cog(AutoReact(bot=bot, extension_name="autoreact")) - bot.add_extension_config("autoreact", config) class AutoReact(cogs.MatchCog): """Class for the autoreact to make it to discord.""" async def match( - self: Self, config: munch.Munch, _: commands.Context, content: str + self: Self, config: munch.Munch, ctx: commands.Context, content: str ) -> bool: """A match function to determine if somehting should be reacted to @@ -48,7 +39,7 @@ async def match( """ search_content = f" {content} " search_content = search_content.lower() - for word in config.extensions.autoreact.react_map.value: + for word in configuration.get_config_entry(ctx.guild.id, "autoreact_react_map"): if f" {word.lower()} " in search_content: return True return False @@ -66,11 +57,12 @@ async def response( search_content = f" {content} " search_content = search_content.lower() reactions = [] - for word in config.extensions.autoreact.react_map.value: + reaction_map = configuration.get_config_entry( + ctx.guild.id, "autoreact_react_map" + ) + for word in reaction_map: if f" {word.lower()} " in search_content: - reaction = config.extensions.autoreact.react_map.value.get(word) + reaction = reaction_map.get(word) if reaction not in reactions: - reactions.append( - config.extensions.autoreact.react_map.value.get(word) - ) + reactions.append(reaction_map.value.get(word)) await auxiliary.add_list_of_reactions(message=ctx.message, reactions=reactions) diff --git a/techsupport_bot/functions/nickname.py b/techsupport_bot/functions/nickname.py index b7f84ca63..3fb7f944a 100644 --- a/techsupport_bot/functions/nickname.py +++ b/techsupport_bot/functions/nickname.py @@ -13,6 +13,7 @@ import string from typing import TYPE_CHECKING, Self +import configuration import discord import munch from botlogging import LogContext, LogLevel @@ -152,7 +153,7 @@ async def on_member_join(self: Self, member: discord.Member) -> None: config = self.bot.guild_configs[str(member.guild.id)] # Don't do anything if the filter is off for the guild - if not config.get("nickname_filter", False): + if not configuration.get_config_entry(member.guild.id, "core_nickname_filter"): return modified_name = format_username(member.display_name) From 25bef8a1be8bdd1be9f933462221e6e9707f9ea1 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:32:17 -0700 Subject: [PATCH 06/40] Process all of dumpdbg and embed, most of duck --- techsupport_bot/commands/duck.py | 95 +++++-------------- techsupport_bot/commands/dumpdbg.py | 15 +-- techsupport_bot/commands/embed.py | 15 +-- .../configuration/config.default.json | 10 +- .../configuration/config.meta.json | 38 ++++++++ 5 files changed, 77 insertions(+), 96 deletions(-) diff --git a/techsupport_bot/commands/duck.py b/techsupport_bot/commands/duck.py index d59660068..b059b6d5e 100644 --- a/techsupport_bot/commands/duck.py +++ b/techsupport_bot/commands/duck.py @@ -9,6 +9,7 @@ from datetime import timedelta from typing import TYPE_CHECKING, Self +import configuration import discord import munch import ui @@ -36,48 +37,6 @@ async def setup(bot: bot.TechSupportBot) -> None: description="The IDs of the channels the duck should appear in", default=[], ) - config.add( - key="use_category", - datatype="bool", - title="Whether to use the whole category for ducks", - description="Whether to use the whole category for ducks", - default=False, - ) - config.add( - key="min_wait", - datatype="int", - title="Min wait (hours)", - description="The minimum number of hours to wait between duck events", - default=2, - ) - config.add( - key="max_wait", - datatype="int", - title="Max wait (hours)", - description="The maximum number of hours to wait between duck events", - default=4, - ) - config.add( - key="timeout", - datatype="int", - title="Duck timeout (seconds)", - description="The amount of time before the duck disappears", - default=60, - ) - config.add( - key="cooldown", - datatype="int", - title="Duck cooldown (seconds)", - description="The amount of time to wait between bef/bang messages", - default=5, - ) - config.add( - key="mute_for_cooldown", - datatype="bool", - title="Uses the timeout feature for cooldown", - description="If enabled, users who miss will be timed out for the cooldown seconds", - default=True, - ) config.add( key="success_rate", datatype="int", @@ -85,20 +44,6 @@ async def setup(bot: bot.TechSupportBot) -> None: description="The success rate of bef/bang messages", default=50, ) - config.add( - key="spawn_user", - datatype="list[int]", - title="Allow user to spawn duck", - description="Set up who you want to allow to spawn a duck", - default=[], - ) - config.add( - key="allow_manipulation", - datatype="bool", - title="Whether or not user manipulation is allowed", - description="Controls whether release, donate, or kill commands are enabled", - default=True, - ) await bot.add_cog(DuckHunt(bot=bot, extension_name="duck")) bot.add_extension_config("duck", config) @@ -133,7 +78,7 @@ async def loop_preconfig(self: Self) -> None: """Preconfig for cooldowns""" self.cooldowns = {} - async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: + async def wait(self: Self, config: munch.Munch, guild: discord.Guild) -> None: """Waits a random amount of time before sending another duck This function shouldn't be manually called @@ -142,8 +87,8 @@ async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: """ await asyncio.sleep( random.randint( - config.extensions.duck.min_wait.value * 3600, - config.extensions.duck.max_wait.value * 3600, + configuration.get_config_entry(guild.id, "duck_min_wait") * 3600, + configuration.get_config_entry(guild.id, "duck_max_wait") * 3600, ) ) @@ -174,7 +119,7 @@ async def execute( ) return - if config.extensions.duck.use_category.value: + if configuration.get_config_entry(guild.id, "duck_use_category"): all_valid_channels = channel.category.text_channels use_channel = random.choice(all_valid_channels) else: @@ -196,7 +141,7 @@ async def execute( try: response_message = await self.bot.wait_for( "message", - timeout=config.extensions.duck.timeout.value, + timeout=configuration.get_config_entry(guild.id, "duck_timeout"), # can't pull the config in a non-coroutine check=functools.partial( self.message_check, config, use_channel, duck_message, banned_user @@ -367,15 +312,18 @@ def message_check( return False cooldowns = self.cooldowns.get(message.guild.id, {}) + cooldown_seconds = configuration.get_config_entry( + message.guild.id, "duck_cooldown" + ) if ( datetime.datetime.now() - cooldowns.get(message.author.id, datetime.datetime.now()) - ).seconds < config.extensions.duck.cooldown.value: + ).seconds < cooldown_seconds: cooldowns[message.author.id] = datetime.datetime.now() asyncio.create_task( message.author.send( - f"I said to wait {config.extensions.duck.cooldown.value}" + f"I said to wait {cooldown_seconds}" + " seconds! Resetting timer..." ) ) @@ -391,14 +339,17 @@ def message_check( embed = auxiliary.prepare_deny_embed(message=quote) embed.set_footer( text=( - f"You missed. Try again in {config.extensions.duck.cooldown.value} " + f"You missed. Try again in {configuration.get_config_entry(message.guild.id, "duck_cooldown")} " f"seconds. Time would have been {duration_exact} seconds" ) ) if ( - config.extensions.duck.mute_for_cooldown.value - and config.extensions.duck.cooldown.value > 0 + configuration.get_config_entry( + message.guild.id, "duck_mute_for_cooldown" + ) + and configuration.get_config_entry(message.guild.id, "duck_cooldown") + > 0 ): # Only attempt timeout if we know we can do it if ( @@ -410,7 +361,9 @@ def message_check( user=message.author, reason="Missed a duck", duration=timedelta( - seconds=config.extensions.duck.cooldown.value + seconds=configuration.get_config_entry( + message.guild.id, "duck_cooldown" + ) ), ) ) @@ -738,7 +691,7 @@ async def release(self: Self, ctx: commands.Context) -> None: ctx (commands.Context): The context in which the command was run """ config = self.bot.guild_configs[str(ctx.guild.id)] - if not config.extensions.duck.allow_manipulation.value: + if not configuration.get_config_entry(ctx.guild.id, "duck_allow_manipulation"): await auxiliary.send_deny_embed( channel=ctx.channel, message="This command is disabled in this server" ) @@ -784,7 +737,7 @@ async def kill(self: Self, ctx: commands.Context) -> None: ctx (commands.Context): The context in which the command was run """ config = self.bot.guild_configs[str(ctx.guild.id)] - if not config.extensions.duck.allow_manipulation.value: + if not configuration.get_config_entry(ctx.guild.id, "duck_allow_manipulation"): await auxiliary.send_deny_embed( channel=ctx.channel, message="This command is disabled in this server" ) @@ -839,7 +792,7 @@ async def donate(self: Self, ctx: commands.Context, user: discord.Member) -> Non user (discord.Member): The user to donate a duck to """ config = self.bot.guild_configs[str(ctx.guild.id)] - if not config.extensions.duck.allow_manipulation.value: + if not configuration.get_config_entry(ctx.guild.id, "duck_allow_manipulation"): await auxiliary.send_deny_embed( channel=ctx.channel, message="This command is disabled in this server" ) @@ -960,7 +913,7 @@ async def spawn(self: Self, ctx: commands.Context) -> None: ctx (commands.Context): The context in which the command was run """ config = self.bot.guild_configs[str(ctx.guild.id)] - spawn_user = config.extensions.duck.spawn_user.value + spawn_user = configuration.get_config_entry(ctx.guild.id, "duck_spawn_user") for person in spawn_user: if ctx.author.id == int(person): await self.execute(config, ctx.guild, ctx.channel) diff --git a/techsupport_bot/commands/dumpdbg.py b/techsupport_bot/commands/dumpdbg.py index 1aa4173fd..0a1dd4c18 100644 --- a/techsupport_bot/commands/dumpdbg.py +++ b/techsupport_bot/commands/dumpdbg.py @@ -5,9 +5,10 @@ import json from typing import TYPE_CHECKING, Self +import configuration import discord from botlogging import LogContext, LogLevel -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: @@ -31,17 +32,7 @@ async def setup(bot: bot.TechSupportBot) -> None: except AttributeError as exc: raise AttributeError("Dumpdbg was not loaded due to missing API key") from exc - config = extensionconfig.ExtensionConfig() - config.add( - key="roles", - datatype="list", - title="Permitted roles", - description="Roles permitted to use this command", - default=["super op"], - ) - await bot.add_cog(Dumpdbg(bot=bot)) - bot.add_extension_config("dumpdbg", config) class Dumpdbg(cogs.BaseCog): @@ -68,7 +59,7 @@ async def debug_dump(self: Self, ctx: commands.Context) -> None: config = self.bot.guild_configs[str(ctx.guild.id)] api_endpoint = self.bot.file_config.api.api_url.dumpdbg - permitted_roles = config.extensions.dumpdbg.roles.value + permitted_roles = configuration.get_config_entry(ctx.guild.id, "dumpdbg_roles") if not permitted_roles: await auxiliary.send_deny_embed( diff --git a/techsupport_bot/commands/embed.py b/techsupport_bot/commands/embed.py index abc0b300d..d13fbb6fe 100644 --- a/techsupport_bot/commands/embed.py +++ b/techsupport_bot/commands/embed.py @@ -14,9 +14,10 @@ from typing import TYPE_CHECKING, Self +import configuration import discord import munch -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: @@ -29,16 +30,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="embed_roles", - datatype="list", - title="Allowed embed roles", - description="The list of role names able to use the embed commands", - default=[], - ) await bot.add_cog(Embedder(bot=bot)) - bot.add_extension_config("embed", config) async def has_embed_role(ctx: commands.Context) -> bool: @@ -55,10 +47,9 @@ async def has_embed_role(ctx: commands.Context) -> bool: Returns: bool: Whether the invoker has the role """ - config = ctx.bot.guild_configs[str(ctx.guild.id)] embed_roles = [] # Gets the embed roles from the config if they exist - for name in config.extensions.embed.embed_roles.value: + for name in configuration.get_config_entry(ctx.guild.id, "embed_embed_roles"): embed_role = discord.utils.get(ctx.guild.roles, name=name) if not embed_role: continue diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 82221b83e..71dc55206 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -14,5 +14,13 @@ "core_enable_logging": true, "core_nickname_filter": false, "core_private_channels": [], - "core_guild_id": "" + "core_guild_id": "", + "duck_allow_manipulation": true, + "duck_cooldown": 5, + "duck_max_wait": 4, + "duck_mute_for_cooldown": true, + "duck_spawn_user": [], + "duck_timeout": 60, + "duck_use_category": false, + "dumpdbg_roles": [] } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index cedf2e170..a4dfb728b 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -62,8 +62,46 @@ "core_guild_id": { "datatype": "discord.Guild", "description": "This is used as a validation tool, the guild ID that this config belongs to" + }, + "duck_allow_manipulation": { + "datatype": "bool", + "description": "Controls whether release, donate, or kill commands are enabled" + }, + "duck_cooldown": { + "datatype": "int", + "description": "The amount of time in seconds to wait between bef/bang messages" + }, + "duck_max_wait": { + "datatype": "int", + "description": "The maximum number of hours to wait between duck events" + }, + "duck_min_wait": { + "datatype": "int", + "description": "The minimum number of hours to wait between duck events" + }, + "duck_mute_for_cooldown": { + "datatype": "bool", + "description": "If enabled, users who miss will be timed out for the cooldown seconds" + }, + "duck_spawn_user": { + "datatype": "list[discord.Member]", + "description": "A list of users allowed to use thje .duck spawn command" + }, + "duck_timeout": { + "datatype": "int", + "description": "The amount of time in seconds before the duck disappears" + }, + "duck_use_category": { + "datatype": "bool", + "description": "Whether to use the whole category for ducks" + }, + "dumpdbg_roles": { + "datatype": "list[discord.Role]", + "description": "Roles permitted to use the dump debug command" } + + } From 5b2ba895231f0a1b6abe07d5680798c6c1aeebbb Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:33:48 -0700 Subject: [PATCH 07/40] Add embed roles to default/meta --- techsupport_bot/configuration/config.default.json | 3 ++- techsupport_bot/configuration/config.meta.json | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 71dc55206..9823a9927 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -22,5 +22,6 @@ "duck_spawn_user": [], "duck_timeout": 60, "duck_use_category": false, - "dumpdbg_roles": [] + "dumpdbg_roles": [], + "embed_embed_roles": [] } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index a4dfb728b..93dc18b27 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -98,6 +98,10 @@ "dumpdbg_roles": { "datatype": "list[discord.Role]", "description": "Roles permitted to use the dump debug command" + }, + "embed_embed_roles": { + "datatype": "list[discord.Role]", + "description": "Roles permitted to use the embed command" } From 4105f451327085d7807703ad889ff92ac73d0dc7 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 19:01:07 -0700 Subject: [PATCH 08/40] Migrate enabled_extenions (mostly) --- techsupport_bot/commands/application.py | 5 +++-- techsupport_bot/commands/echo.py | 5 ++++- techsupport_bot/commands/extension.py | 18 +++++++++++------- techsupport_bot/commands/factoids.py | 5 ++++- techsupport_bot/commands/help.py | 11 +++++++---- techsupport_bot/commands/modlog.py | 11 +++++++---- techsupport_bot/commands/relay.py | 8 ++++++-- techsupport_bot/commands/whois.py | 16 ++++++++-------- .../configuration/config.default.json | 3 ++- techsupport_bot/configuration/config.meta.json | 4 ++++ techsupport_bot/core/cogs.py | 9 +++++++-- techsupport_bot/functions/paste.py | 5 ++++- techsupport_bot/functions/xp.py | 5 ++++- 13 files changed, 71 insertions(+), 34 deletions(-) diff --git a/techsupport_bot/commands/application.py b/techsupport_bot/commands/application.py index 49f7ae978..758a1c469 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -646,7 +646,6 @@ async def check_if_can_apply(self: Self, applicant: discord.Member) -> bool: Returns: bool: True if they can apply, False if they cannot apply """ - config = self.bot.guild_configs[str(applicant.guild.id)] role = applicant.guild.get_role( int( configuration.get_config_entry( @@ -655,7 +654,9 @@ async def check_if_can_apply(self: Self, applicant: discord.Member) -> bool: ) ) # Don't allow applications if extension is disabled - if "application" not in config.enabled_extensions: + if "application" not in configuration.get_config_entry( + applicant.guild.id, "core_enabled_extensions" + ): return False # Don't allow people to apply if they already have the role diff --git a/techsupport_bot/commands/echo.py b/techsupport_bot/commands/echo.py index 92f585774..83284354a 100644 --- a/techsupport_bot/commands/echo.py +++ b/techsupport_bot/commands/echo.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Self +import configuration from core import auxiliary, cogs from discord.ext import commands from functions import logger as function_logger @@ -78,7 +79,9 @@ async def echo_channel( config = self.bot.guild_configs[str(channel.guild.id)] # Don't allow logging if extension is disabled - if "logger" not in config.enabled_extensions: + if "logger" not in configuration.get_config_entry( + channel.guild.id, "core_enabled_extensions" + ): return target_logging_channel = await function_logger.pre_log_checks( diff --git a/techsupport_bot/commands/extension.py b/techsupport_bot/commands/extension.py index 7cd53ef11..fb96ca905 100644 --- a/techsupport_bot/commands/extension.py +++ b/techsupport_bot/commands/extension.py @@ -15,6 +15,7 @@ import json from typing import TYPE_CHECKING, Self +import configuration import discord import ui from core import auxiliary, cogs @@ -58,11 +59,13 @@ async def list_disabled(self: Self, interaction: discord.Interaction) -> None: Args: interaction (discord.Interaction): The interaction that triggered the slash command """ - config = self.bot.guild_configs[str(interaction.guild.id)] missing_extensions = [ item for item in self.bot.extension_name_list - if item not in config.enabled_extensions + if item + not in configuration.get_config_entry( + interaction.guild.id, "core_enabled_extensions" + ) ] if len(missing_extensions) == 0: embed = auxiliary.prepare_confirm_embed( @@ -88,10 +91,11 @@ async def enable_everything(self: Self, interaction: discord.Interaction) -> Non interaction (discord.Interaction): The interaction that triggered the slash command """ config = self.bot.guild_configs[str(interaction.guild.id)] + extension_list = configuration.get_config_entry( + interaction.guild.id, "core_enabled_extensions" + ) missing_extensions = [ - item - for item in self.bot.extension_name_list - if item not in config.enabled_extensions + item for item in self.bot.extension_name_list if item not in extension_list ] if len(missing_extensions) == 0: embed = auxiliary.prepare_confirm_embed( @@ -99,9 +103,9 @@ async def enable_everything(self: Self, interaction: discord.Interaction) -> Non ) else: for extension in missing_extensions: - config.enabled_extensions.append(extension) + extension_list(extension) - config.enabled_extensions.sort() + extension_list.sort() # Modify the database await self.bot.write_new_config( str(interaction.guild.id), json.dumps(config) diff --git a/techsupport_bot/commands/factoids.py b/techsupport_bot/commands/factoids.py index 5490f7f6d..c1f611afd 100644 --- a/techsupport_bot/commands/factoids.py +++ b/techsupport_bot/commands/factoids.py @@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Self import aiocron +import configuration import discord import expiringdict import munch @@ -954,7 +955,9 @@ async def send_to_logger( config = self.bot.guild_configs[str(channel.guild.id)] # Don't allow logging if extension is disabled - if "logger" not in config.enabled_extensions: + if "logger" not in configuration.get_config_entry( + factoid_caller.guild.id, "core_enabled_extensions" + ): return target_logging_channel = await function_logger.pre_log_checks( diff --git a/techsupport_bot/commands/help.py b/techsupport_bot/commands/help.py index 3752f15fd..757539c75 100644 --- a/techsupport_bot/commands/help.py +++ b/techsupport_bot/commands/help.py @@ -6,6 +6,7 @@ from itertools import product from typing import TYPE_CHECKING, Self +import configuration import discord import ui from core import auxiliary, cogs @@ -83,8 +84,9 @@ async def help_command( # Check if extension is enabled extension_name = self.bot.get_command_extension_name(command) - config = self.bot.guild_configs[str(ctx.guild.id)] - if extension_name not in config.enabled_extensions: + if extension_name not in configuration.get_config_entry( + ctx.guild.id, "core_enabled_extensions" + ): continue # Deal with aliases by looping through all parent groups and alises @@ -122,8 +124,9 @@ async def help_command( # Check if extension is enabled extension_name = command.extras["module"] - config = self.bot.guild_configs[str(ctx.guild.id)] - if extension_name not in config.enabled_extensions: + if extension_name not in configuration.get_config_entry( + ctx.guild.id, "core_enabled_extensions" + ): continue # We have to manually build a string representation of the usage diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 475605060..f449d7a9d 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -6,6 +6,7 @@ from collections import Counter from typing import TYPE_CHECKING, Self +import configuration import discord import munch import ui @@ -287,8 +288,9 @@ async def log_ban( guild (discord.Guild): The guild the member was banned from reason (str): The reason for the ban """ - config = bot.guild_configs[str(guild.id)] - if "modlog" not in config.get("enabled_extensions", []): + if "modlog" not in configuration.get_config_entry( + guild.id, "core_enabled_extensions" + ): return if not reason: @@ -343,8 +345,9 @@ async def log_unban( guild (discord.Guild): The guild the member was unbanned from reason (str): The reason for the unban """ - config = bot.guild_configs[str(guild.id)] - if "modlog" not in config.get("enabled_extensions", []): + if "modlog" not in configuration.get_config_entry( + guild.id, "core_enabled_extensions" + ): return if not reason: diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index d75aba2d8..3e396f34e 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -416,7 +416,9 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No ) config = self.bot.guild_configs[str(discord_channel.guild.id)] - if "automod" in config.get("enabled_extensions", []): + if "automod" in configuration.get_config_entry( + discord_channel.guild.id, "core_enabled_extensions" + ): automod_actions = automod.run_only_string_checks( config, split_message["content"] ) @@ -457,7 +459,9 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No config = self.bot.guild_configs[str(discord_channel.guild.id)] # Don't allow logging if extension is disabled - if "logger" not in config.enabled_extensions: + if "logger" not in configuration.get_config_entry( + discord_channel.guild.id, "core_enabled_extensions" + ): return target_logging_channel = await function_logger.pre_log_checks( self.bot, config, discord_channel diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index 8e2dfe548..793b0e094 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -5,6 +5,7 @@ import datetime from typing import TYPE_CHECKING, Self +import configuration import discord import ui from commands import application, moderator, notes @@ -67,16 +68,18 @@ async def whois_command( role_string = ", ".join(role.name for role in member.roles[1:]) embed.add_field(name="Roles", value=role_string or "No roles") - config = self.bot.guild_configs[str(interaction.guild.id)] + enabled_extensions = configuration.get_config_entry( + interaction.guild.id, "core_enabled_extensions" + ) - if "application" in config.enabled_extensions: + if "application" in enabled_extensions: try: await application.command_permission_check(interaction) embed = await add_application_info_field(interaction, member, embed) except (app_commands.MissingAnyRole, app_commands.AppCommandError): pass - if "xp" in config.enabled_extensions: + if "xp" in enabled_extensions: current_XP = await xp.get_current_XP(self.bot, member, interaction.guild) embed.add_field(name="XP", value=current_XP) @@ -110,7 +113,7 @@ async def whois_command( embeds = [embed] - if "notes" in config.enabled_extensions: + if "notes" in enabled_extensions: try: await notes.is_reader(interaction) all_notes = await moderation.get_all_notes( @@ -126,10 +129,7 @@ async def whois_command( except (app_commands.MissingAnyRole, app_commands.AppCommandError): pass - if ( - "moderator" in config.enabled_extensions - and interaction.permissions.kick_members - ): + if "moderator" in enabled_extensions and interaction.permissions.kick_members: all_warnings = await moderation.get_all_warnings( self.bot, member, interaction.guild ) diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 9823a9927..69fb3f2b9 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -23,5 +23,6 @@ "duck_timeout": 60, "duck_use_category": false, "dumpdbg_roles": [], - "embed_embed_roles": [] + "embed_embed_roles": [], + "core_enabled_extensions": ["config", "extension"] } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index 93dc18b27..e7c73b511 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -102,6 +102,10 @@ "embed_embed_roles": { "datatype": "list[discord.Role]", "description": "Roles permitted to use the embed command" + }, + "core_enabled_extensions": { + "datatype": "list[str]", + "description": "A list of all extensions enabled in the guild" } diff --git a/techsupport_bot/core/cogs.py b/techsupport_bot/core/cogs.py index be95a762c..6ce8c8193 100644 --- a/techsupport_bot/core/cogs.py +++ b/techsupport_bot/core/cogs.py @@ -6,6 +6,7 @@ from collections.abc import Awaitable, Callable from typing import TYPE_CHECKING, Any, Self +import configuration import discord import munch from botlogging import LogContext, LogLevel @@ -369,8 +370,12 @@ async def _loop_execute( # exit task if the channel is no longer configured break - if guild is None or self.extension_name in getattr( - config, "enabled_extensions", [] + if ( + guild is None + or self.extension_name + in configuration.get_config_entry( + guild.id, "core_enabled_extensions" + ) ): try: if target_channel: diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index ef8d07447..b03e4b45a 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -5,6 +5,7 @@ import io from typing import TYPE_CHECKING, Self +import configuration import discord import munch from botlogging import LogContext, LogLevel @@ -115,7 +116,9 @@ async def response( if len(content) > config.extensions.paste.length_limit.value or content.count( "\n" ) > self.max_newlines(config.extensions.paste.length_limit.value): - if "automod" in config.get("enabled_extensions", []): + if "automod" in configuration.get_config_entry( + ctx.guild.id, "core_enabled_extensions" + ): automod_actions = await automod.run_all_checks(config, ctx.message) automod_final = automod.process_automod_violations(automod_actions) if automod_final and automod_final.delete_message: diff --git a/techsupport_bot/functions/xp.py b/techsupport_bot/functions/xp.py index 94e7ce1e4..f7c4a3db9 100644 --- a/techsupport_bot/functions/xp.py +++ b/techsupport_bot/functions/xp.py @@ -5,6 +5,7 @@ import random from typing import TYPE_CHECKING, Self +import configuration import discord import expiringdict import munch @@ -97,7 +98,9 @@ async def match( return False # Ignore messages that are factoid calls - if "factoids" in config.enabled_extensions: + if "factoids" in configuration.get_config_entry( + ctx.guild.id, "core_enabled_extensions" + ): factoid_prefix = config.extensions.factoids.prefix.value if ctx.message.clean_content.startswith(factoid_prefix): return False From 821ff8df5da4af840aea4c69f53c269d2dd062a9 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 20:01:54 -0700 Subject: [PATCH 09/40] Rewrite MatchCog --- techsupport_bot/commands/factoids.py | 11 ++++--- techsupport_bot/commands/gate.py | 11 ++++--- techsupport_bot/commands/modlog.py | 4 +-- techsupport_bot/commands/notes.py | 2 +- techsupport_bot/commands/relay.py | 7 +---- .../configuration/config.default.json | 3 +- .../configuration/config.meta.json | 4 +++ techsupport_bot/core/cogs.py | 30 +++++++------------ techsupport_bot/functions/automod.py | 11 +++---- techsupport_bot/functions/autoreact.py | 8 ++--- techsupport_bot/functions/honeypot.py | 10 +++---- techsupport_bot/functions/logger.py | 12 +++----- techsupport_bot/functions/nickname.py | 7 ++--- techsupport_bot/functions/paste.py | 11 +++---- techsupport_bot/functions/xp.py | 10 +++---- 15 files changed, 55 insertions(+), 86 deletions(-) diff --git a/techsupport_bot/commands/factoids.py b/techsupport_bot/commands/factoids.py index c1f611afd..d6f60368e 100644 --- a/techsupport_bot/commands/factoids.py +++ b/techsupport_bot/commands/factoids.py @@ -779,23 +779,22 @@ async def delete_factoid( return True # -- Getting and responding with a factoid -- - async def match( - self: Self, config: munch.Munch, _: commands.Context, message_contents: str - ) -> bool: + async def match(self: Self, ctx: commands.Context, message_contents: str) -> bool: """Checks if a message started with the prefix from the config Args: - config (munch.Munch): The config to get the prefix from message_contents (str): The message to check Returns: bool: Whether the message starts with the prefix or not """ + if not ctx.guild: + return + config = self.bot.guild_configs[str(ctx.guild.id)] return message_contents.startswith(config.extensions.factoids.prefix.value) async def response( self: Self, - config: munch.Munch, ctx: commands.Context, message_content: str, _: bool, @@ -803,7 +802,6 @@ async def response( """Responds to a factoid call Args: - config (munch.Munch): The server config ctx (commands.Context): Context of the call message_content (str): Content of the call @@ -815,6 +813,7 @@ async def response( return # Checks if the first word of the content after the prefix is a valid factoid # Replaces \n with spaces so factoid can be called even with newlines + config = self.bot.guild_configs[str(ctx.guild.id)] prefix = config.extensions.factoids.prefix.value query = message_content[len(prefix) :].replace("\n", " ").split(" ")[0].lower() try: diff --git a/techsupport_bot/commands/gate.py b/techsupport_bot/commands/gate.py index 48c921b2c..65abcd6f2 100644 --- a/techsupport_bot/commands/gate.py +++ b/techsupport_bot/commands/gate.py @@ -79,31 +79,28 @@ async def setup(bot: bot.TechSupportBot) -> None: class ServerGate(cogs.MatchCog): """Class to get the server gate from config.""" - async def match( - self: Self, config: munch.Munch, ctx: commands.Context, _: str - ) -> bool: + async def match(self: Self, ctx: commands.Context, _: str) -> bool: """Matches any message and checks if it is in the gate channel Args: - config (munch.Munch): The config for the guild where the message was sent ctx (commands.Context): The context of the original message Returns: bool: Whether the message should be subject to the gate policy or not """ + config = self.bot.guild_configs[str(ctx.guild.id)] if not config.extensions.gate.channel.value: return False return ctx.channel.id == int(config.extensions.gate.channel.value) async def response( - self: Self, config: munch.Munch, ctx: commands.Context, content: str, _: bool + self: Self, ctx: commands.Context, content: str, _: bool ) -> None: """Prepares a response to the gate policy, deleting the message and assigning roles if needed Args: - config (munch.Munch): The config of the guild with the gate ctx (commands.Context): The context of the message that triggered the gate content (str): The string contents of the message from the gate channel """ @@ -114,6 +111,8 @@ async def response( await ctx.message.delete() + config = self.bot.guild_configs[str(ctx.guild.id)] + if content.lower() == config.extensions.gate.verify_text.value: roles = await self.get_roles(config, ctx) if not roles: diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index f449d7a9d..8da9a0768 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -216,7 +216,7 @@ async def on_member_ban( ) config = self.bot.guild_configs[str(guild.id)] - if not self.extension_enabled(config): + if not self.extension_enabled(guild): return entry = None @@ -252,7 +252,7 @@ async def on_member_unban( ) config = self.bot.guild_configs[str(guild.id)] - if not self.extension_enabled(config): + if not self.extension_enabled(guild): return entry = None diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 2afe3dd6a..7a3b3ce1b 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -311,7 +311,7 @@ async def on_member_join(self: Self, member: discord.Member) -> None: member (discord.Member): The member who has just joined """ config = self.bot.guild_configs[str(member.guild.id)] - if not self.extension_enabled(config): + if not self.extension_enabled(member.guild): return role = discord.utils.get( diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index 3e396f34e..9af99839f 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -63,13 +63,10 @@ async def preconfig(self: Self) -> None: irc_discord_map.discord_channel_id, irc_discord_map.irc_channel_id ) - async def match( - self: Self, config: munch.Munch, ctx: commands.Context, content: str - ) -> str: + async def match(self: Self, ctx: commands.Context, content: str) -> str: """Checks to see if the message should be sent to discord Args: - config (munch.Munch): The config of the guild where the message was sent ctx (commands.Context): The context the message was sent in content (str): The string content of the message @@ -98,7 +95,6 @@ async def match( async def response( self: Self, - config: munch.Munch, ctx: commands.Context, content: str, result: str, @@ -106,7 +102,6 @@ async def response( """Send the message to IRC Args: - config (munch.Munch): The config of the guild where the message was sent ctx (commands.Context): The context the message was sent in content (str): The string content of the message result (str): The string representation of the IRC channel diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 69fb3f2b9..a4b5e4e52 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -24,5 +24,6 @@ "duck_use_category": false, "dumpdbg_roles": [], "embed_embed_roles": [], - "core_enabled_extensions": ["config", "extension"] + "core_enabled_extensions": ["config", "extension"], + "core_logging_channel": "" } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index e7c73b511..46863be8d 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -106,6 +106,10 @@ "core_enabled_extensions": { "datatype": "list[str]", "description": "A list of all extensions enabled in the guild" + }, + "core_logging_channel": { + "datatype": "discord.TextChannel", + "description": "This is the bot events logging channel" } diff --git a/techsupport_bot/core/cogs.py b/techsupport_bot/core/cogs.py index 6ce8c8193..ffbdb7cd9 100644 --- a/techsupport_bot/core/cogs.py +++ b/techsupport_bot/core/cogs.py @@ -75,7 +75,7 @@ async def _preconfig(self: Self) -> None: async def preconfig(self: Self) -> None: """Preconfigures the environment before starting the cog.""" - def extension_enabled(self: Self, config: munch.Munch) -> bool: + def extension_enabled(self: Self, guild: discord.Guild) -> bool: """Checks if an extension is currently enabled for a given config. Args: @@ -85,9 +85,9 @@ def extension_enabled(self: Self, config: munch.Munch) -> bool: bool: True if the extension is enabled for the context False if it isn't """ - if config is None: - config = {} - if self.no_guild or self.extension_name in config.get("enabled_extensions", []): + if self.no_guild or self.extension_name in configuration.get_config_entry( + guild.id, "core_enabled_extensions" + ): return True return False @@ -116,11 +116,7 @@ async def on_message(self: Self, message: discord.Message) -> None: ctx = await self.bot.get_context(message) - config = self.bot.guild_configs[str(ctx.guild.id)] - if not config: - return - - if not self.extension_enabled(config): + if not self.extension_enabled(message.guild): return if ( @@ -129,20 +125,21 @@ async def on_message(self: Self, message: discord.Message) -> None: ): message.content = message.message_snapshots[0].content - result = await self.match(config, ctx, message.content) + result = await self.match(ctx, message.content) if not result: return try: - await self.response(config, ctx, message.content, result) + await self.response(ctx, message.content, result) except Exception as exception: await self.bot.logger.send_log( message="Checking config for log channel", level=LogLevel.DEBUG, context=LogContext(guild=ctx.guild, channel=ctx.channel), ) - config = self.bot.guild_configs[str(ctx.guild.id)] - bot_logging_channel = config.get("logging_channel") + bot_logging_channel = configuration.get_config_entry( + message.guild, "core_logging_channel" + ) await self.bot.logger.send_log( message=f"Match cog error: {self.__class__.__name__} {exception}!", level=LogLevel.ERROR, @@ -151,13 +148,10 @@ async def on_message(self: Self, message: discord.Message) -> None: exception=exception, ) - async def match( - self: Self, _config: munch.Munch, _ctx: commands.Context, _content: str - ) -> bool: + async def match(self: Self, _ctx: commands.Context, _content: str) -> bool: """Runs a boolean check on message content. Args: - _config (munch.Munch): the config associated with the context _ctx (commands.Context): the context object _content (str): the message content @@ -169,7 +163,6 @@ async def match( async def response( self: Self, - _config: munch.Munch, _ctx: commands.Context, _content: str, _result: bool, @@ -177,7 +170,6 @@ async def response( """Performs a response if the match is valid. Args: - _config (munch.Munch): the config associated with the context _ctx (commands.Context): the context object _content (str): the message content _result (bool): the boolean result from match() diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index da99a9408..097b95642 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -148,19 +148,17 @@ class AutoMod(cogs.MatchCog): """Holds all of the discord message specific automod functions Most of the automod is a class function""" - async def match( - self: Self, config: munch.Munch, ctx: commands.Context, content: str - ) -> bool: + async def match(self: Self, ctx: commands.Context, content: str) -> bool: """Checks to see if a message should be considered for automod violations Args: - config (munch.Munch): The config of the guild to check ctx (commands.Context): The context of the original message content (str): The string representation of the message Returns: bool: Whether the message should be inspected for automod violations """ + config = self.bot.guild_configs[str(ctx.guild.id)] if not str(ctx.channel.id) in config.extensions.automod.channels.value: await self.bot.logger.send_log( message="Channel not in automod channels - ignoring automod check", @@ -181,7 +179,6 @@ async def match( async def response( self: Self, - config: munch.Munch, ctx: commands.Context, content: str, result: bool, @@ -189,11 +186,11 @@ async def response( """Handles a discord automod violation Args: - config (munch.Munch): The config of the guild where the message was sent ctx (commands.Context): The context the message was sent in content (str): The string content of the message result (bool): What the match() function returned """ + config = self.bot.guild_configs[str(ctx.guild.id)] # If user outranks bot, do nothing if ctx.message.author.top_role >= ctx.channel.guild.me.top_role: @@ -323,7 +320,7 @@ async def on_raw_message_edit( return config = self.bot.guild_configs[str(guild.id)] - if not self.extension_enabled(config): + if not self.extension_enabled(guild): return channel = self.bot.get_channel(payload.channel_id) diff --git a/techsupport_bot/functions/autoreact.py b/techsupport_bot/functions/autoreact.py index f436d1195..507d14fba 100644 --- a/techsupport_bot/functions/autoreact.py +++ b/techsupport_bot/functions/autoreact.py @@ -25,13 +25,10 @@ async def setup(bot: bot.TechSupportBot) -> None: class AutoReact(cogs.MatchCog): """Class for the autoreact to make it to discord.""" - async def match( - self: Self, config: munch.Munch, ctx: commands.Context, content: str - ) -> bool: + async def match(self: Self, ctx: commands.Context, content: str) -> bool: """A match function to determine if somehting should be reacted to Args: - config (munch.Munch): The guild config for the running bot content (str): The string content of the message Returns: @@ -45,12 +42,11 @@ async def match( return False async def response( - self: Self, config: munch.Munch, ctx: commands.Context, content: str, _: bool + self: Self, ctx: commands.Context, content: str, _: bool ) -> None: """The function to generate and add reactions Args: - config (munch.Munch): The guild config for the running bot ctx (commands.Context): The context in which the message was sent in content (str): The string content of the message """ diff --git a/techsupport_bot/functions/honeypot.py b/techsupport_bot/functions/honeypot.py index fc681f348..d89cdd53f 100644 --- a/techsupport_bot/functions/honeypot.py +++ b/techsupport_bot/functions/honeypot.py @@ -35,19 +35,17 @@ async def setup(bot: bot.TechSupportBot) -> None: class HoneyPot(cogs.MatchCog): """The pasting module""" - async def match( - self: Self, config: munch.Munch, ctx: commands.Context, content: str - ) -> bool: + async def match(self: Self, ctx: commands.Context, content: str) -> bool: """Checks to see if a message was sent in a honeypot channel Args: - config (munch.Munch): The config of the guild to check ctx (commands.Context): The context of the original message content (str): The string representation of the message Returns: bool: Whether the author sent in a honeypot channel """ + config = self.bot.guild_configs[str(ctx.guild.id)] # If the channel isn't a honeypot, do nothing. if not str(ctx.channel.id) in config.extensions.honeypot.channels.value: return False @@ -55,7 +53,6 @@ async def match( async def response( self: Self, - config: munch.Munch, ctx: commands.Context, content: str, result: bool, @@ -63,7 +60,6 @@ async def response( """Handles a honeypot check Args: - config (munch.Munch): The config of the guild where the message was sent ctx (commands.Context): The context the message was sent in content (str): The string content of the message result (bool): What the match() function returned @@ -73,6 +69,8 @@ async def response( await ctx.author.ban(delete_message_days=1, reason="triggered honeypot") await ctx.guild.unban(ctx.author, reason="triggered honeypot") + config = self.bot.guild_configs[str(ctx.guild.id)] + # Send an alert in the alert channel, if its configured try: alert_channel = ctx.guild.get_channel(int(config.moderation.alert_channel)) diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index e9b4b8dde..d9c0988a3 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -121,33 +121,29 @@ async def pre_log_checks( class Logger(cogs.MatchCog): """Class for the logger to make it to discord.""" - async def match( - self: Self, config: munch.Munch, ctx: commands.Context, _: str - ) -> bool: + async def match(self: Self, ctx: commands.Context, _: str) -> bool: """Matches any message and checks if it is in a channel with a logger rule Args: - config (munch.Munch): The config for the guild where the message was sent ctx (commands.Context): The context of the original message Returns: bool: Whether the message should be logged or not """ + config = self.bot.guild_configs[str(ctx.guild.id)] channel_id = get_channel_id(ctx.channel) if not str(channel_id) in config.extensions.logger.channel_map.value: return False return True - async def response( - self: Self, config: munch.Munch, ctx: commands.Context, _: str, __: bool - ) -> None: + async def response(self: Self, ctx: commands.Context, _: str, __: bool) -> None: """If a message should be logged, this logs the message Args: - config (munch.Munch): The guild config where the message was sent ctx (commands.Context): The context that was generated when the message was sent """ + config = self.bot.guild_configs[str(ctx.guild.id)] target_logging_channel = await pre_log_checks(self.bot, config, ctx.channel) await send_message( diff --git a/techsupport_bot/functions/nickname.py b/techsupport_bot/functions/nickname.py index 3fb7f944a..4ca3f5088 100644 --- a/techsupport_bot/functions/nickname.py +++ b/techsupport_bot/functions/nickname.py @@ -83,9 +83,7 @@ class AutoNickName(cogs.MatchCog): The class that holds the listener and functions to auto change peoples nicknames """ - async def match( - self: Self, config: munch.Munch, ctx: commands.Context, content: str - ) -> bool: + async def match(self: Self, ctx: commands.Context, content: str) -> bool: """On every message, check if the authors nickname should be changed Args: @@ -105,7 +103,6 @@ async def match( async def response( self: Self, - config: munch.Munch, ctx: commands.Context, content: str, result: bool, @@ -113,11 +110,11 @@ async def response( """Changes the nickname of a given user, on message Args: - config (munch.Munch): The guild config ctx (commands.Context): The context that sent the message content (str): The content of the message result (bool): The return value of the match function """ + config = self.bot.guild_configs[str(ctx.guild.id)] # If user outranks bot, do nothing if ctx.message.author.top_role >= ctx.channel.guild.me.top_role: return diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index b03e4b45a..5851a3698 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -66,19 +66,17 @@ async def setup(bot: bot.TechSupportBot) -> None: class Paster(cogs.MatchCog): """The pasting module""" - async def match( - self: Self, config: munch.Munch, ctx: commands.Context, content: str - ) -> bool: + async def match(self: Self, ctx: commands.Context, content: str) -> bool: """Checks to see if a message should be considered for a paste Args: - config (munch.Munch): The config of the guild to check ctx (commands.Context): The context of the original message content (str): The string representation of the message Returns: bool: Whether the message should be inspected for a paste """ + config = self.bot.guild_configs[str(ctx.guild.id)] # exit the match based on exclusion parameters if not str(ctx.channel.id) in config.extensions.paste.channels.value: await self.bot.logger.send_log( @@ -100,7 +98,6 @@ async def match( async def response( self: Self, - config: munch.Munch, ctx: commands.Context, content: str, result: bool, @@ -108,11 +105,11 @@ async def response( """Handles a paste check Args: - config (munch.Munch): The config of the guild where the message was sent ctx (commands.Context): The context the message was sent in content (str): The string content of the message result (bool): What the match() function returned """ + config = self.bot.guild_configs[str(ctx.guild.id)] if len(content) > config.extensions.paste.length_limit.value or content.count( "\n" ) > self.max_newlines(config.extensions.paste.length_limit.value): @@ -151,7 +148,7 @@ async def on_raw_message_edit( return config = self.bot.guild_configs[str(guild.id)] - if not self.extension_enabled(config): + if not self.extension_enabled(guild): return channel = self.bot.get_channel(payload.channel_id) diff --git a/techsupport_bot/functions/xp.py b/techsupport_bot/functions/xp.py index f7c4a3db9..dba6efb3c 100644 --- a/techsupport_bot/functions/xp.py +++ b/techsupport_bot/functions/xp.py @@ -59,18 +59,16 @@ async def preconfig(self: Self) -> None: max_age_seconds=60, ) - async def match( - self: Self, config: munch.Munch, ctx: commands.Context, _: str - ) -> bool: + async def match(self: Self, ctx: commands.Context, _: str) -> bool: """Checks a given message to determine if XP should be applied Args: - config (munch.Munch): The guild config for the running bot ctx (commands.Context): The context that the original message was sent in Returns: bool: True if XP should be granted, False if it shouldn't be. """ + config = self.bot.guild_configs[str(ctx.guild.id)] # Ignore all bot messages if ctx.message.author.bot: return False @@ -117,16 +115,16 @@ async def match( return True async def response( - self: Self, config: munch.Munch, ctx: commands.Context, content: str, _: bool + self: Self, ctx: commands.Context, content: str, _: bool ) -> None: """Updates XP for the given user. Message has already been validated when you reach this function. Args: - config (munch.Munch): The guild config for the running bot ctx (commands.Context): The context in which the message was sent in content (str): The string content of the message """ + config = self.bot.guild_configs[str(ctx.guild.id)] current_XP = await get_current_XP(self.bot, ctx.author, ctx.guild) new_XP = random.randint(10, 20) From 511c52491ab1c9874b1fa13d6dacf12e042522aa Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 20:08:10 -0700 Subject: [PATCH 10/40] Rewrite modmail management function --- techsupport_bot/commands/modmail.py | 20 +++++++++---------- .../configuration/config.default.json | 3 ++- .../configuration/config.meta.json | 4 ++++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/techsupport_bot/commands/modmail.py b/techsupport_bot/commands/modmail.py index 4f348d2b9..b8d0e0823 100644 --- a/techsupport_bot/commands/modmail.py +++ b/techsupport_bot/commands/modmail.py @@ -18,6 +18,7 @@ from datetime import datetime from typing import TYPE_CHECKING, Self +import configuration import discord import expiringdict import munch @@ -29,15 +30,12 @@ import bot -async def has_modmail_management_role( - ctx: commands.Context, config: munch.Munch = None -) -> bool: +async def has_modmail_management_role(ctx: commands.Context | discord.Message) -> bool: """-COMMAND CHECK- Checks if the invoker has a modmail management role Args: - ctx (commands.Context): Context used for getting the config file - config (munch.Munch): Can be defined manually to run this without providing actual ctx + ctx (commands.Context | discord.Message): Context used for getting the config file Raises: CommandError: No modmail management roles were assigned in the config @@ -48,10 +46,10 @@ async def has_modmail_management_role( """ # Only running this line of code if config isn't manually defined allows the use of # a discord.Message object in place of ctx - if not config: - config = ctx.bot.guild_configs[str(ctx.guild.id)] user_roles = getattr(ctx.author, "roles", []) - unparsed_roles = config.extensions.modmail.modmail_roles.value + unparsed_roles = configuration.get_config_entry( + ctx.guild.id, "modmail_modmail_roles" + ) modmail_roles = [] if not unparsed_roles: @@ -62,7 +60,9 @@ async def has_modmail_management_role( # Two for loops are needed, because an array containing all modmail roles is needed for # the error thrown when the user doesn't have any relevant roles. - for role_id in config.extensions.modmail.modmail_roles.value: + for role_id in configuration.get_config_entry( + ctx.guild.id, "modmail_modmail_roles" + ): role = discord.utils.get(ctx.guild.roles, id=int(role_id)) if not role: @@ -961,7 +961,7 @@ async def on_message(self: Self, message: discord.Message) -> None: # Makes sure the person is actually allowed to run modmail commands config = self.bot.guild_configs[str(message.guild.id)] try: - await has_modmail_management_role(message, config) + await has_modmail_management_role(message) except commands.MissingAnyRole as e: await auxiliary.send_deny_embed(message=f"{e}", channel=message.channel) return diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index a4b5e4e52..328c61e88 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -25,5 +25,6 @@ "dumpdbg_roles": [], "embed_embed_roles": [], "core_enabled_extensions": ["config", "extension"], - "core_logging_channel": "" + "core_logging_channel": "", + "modmail_modmail_roles": [] } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index 46863be8d..cafb1b115 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -110,6 +110,10 @@ "core_logging_channel": { "datatype": "discord.TextChannel", "description": "This is the bot events logging channel" + }, + "modmail_modmail_roles": { + "datatype": "list[discord.Role]", + "description": "Roles that can access modmail and its commands" } From b60c403a12eea8e19d43149f548ad64881009d32 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 20:36:27 -0700 Subject: [PATCH 11/40] Rewrite LoopCog --- techsupport_bot/commands/application.py | 12 ++-- techsupport_bot/commands/duck.py | 11 ++-- techsupport_bot/commands/forum.py | 11 ++-- techsupport_bot/commands/kanye.py | 6 +- techsupport_bot/commands/news.py | 6 +- techsupport_bot/commands/voting.py | 12 ++-- .../configuration/config.default.json | 4 +- .../configuration/config.meta.json | 4 ++ techsupport_bot/core/cogs.py | 59 ++++++++----------- 9 files changed, 58 insertions(+), 67 deletions(-) diff --git a/techsupport_bot/commands/application.py b/techsupport_bot/commands/application.py index 758a1c469..73bb9af1e 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -89,11 +89,10 @@ class ApplicationNotifier(cogs.LoopCog): """This cog is soley tasked with looping the application reminder for users Everything else is handled in ApplicationManager""" - async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None: + async def execute(self: Self, guild: discord.Guild) -> None: """The function that executes the from the LoopCog structure Args: - config (munch.Munch): The guild config for the executing loop guild (discord.Guild): The guild the loop is executing for """ channels = configuration.get_config_entry( @@ -111,11 +110,10 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None ), ) - async def wait(self: Self, config: munch.Munch, guild: discord.Guild) -> None: + async def wait(self: Self, guild: discord.Guild) -> None: """The function that causes the sleep/delay the from the LoopCog structure Args: - config (munch.Munch): The guild config for the executing loop guild (discord.Guild): The guild the loop is executing for """ await aiocron.crontab( @@ -855,11 +853,10 @@ async def get_ban_entry(self: Self, member: discord.Member) -> bot.models.AppBan # Loop stuff - async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None: + async def execute(self: Self, guild: discord.Guild) -> None: """The executes the reminder of pending applications Args: - config (munch.Munch): The guild config for the executing loop guild (discord.Guild): The guild the loop is executing for """ channel = guild.get_channel( @@ -966,11 +963,10 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None await channel.send(embed=embed) - async def wait(self: Self, config: munch.Munch, guild: discord.Guild) -> None: + async def wait(self: Self, guild: discord.Guild) -> None: """The queues the pending application reminder based on the cron config Args: - config (munch.Munch): The guild config for the executing loop guild (discord.Guild): The guild the loop is executing for """ await aiocron.crontab( diff --git a/techsupport_bot/commands/duck.py b/techsupport_bot/commands/duck.py index b059b6d5e..5904e3c71 100644 --- a/techsupport_bot/commands/duck.py +++ b/techsupport_bot/commands/duck.py @@ -72,18 +72,18 @@ class DuckHunt(cogs.LoopCog): "https://www.iconarchive.com/download/i97188/iconsmind/outline/Target.512.png" ) ON_START: bool = False - CHANNELS_KEY: str = "hunt_channels" + CHANNELS_KEY: str = "duck_hunt_channels" async def loop_preconfig(self: Self) -> None: """Preconfig for cooldowns""" self.cooldowns = {} - async def wait(self: Self, config: munch.Munch, guild: discord.Guild) -> None: + async def wait(self: Self, guild: discord.Guild) -> None: """Waits a random amount of time before sending another duck This function shouldn't be manually called Args: - config (munch.Munch): The guild config to use to determine the min and max wait times + guild (discord.Guild): The guild where the duck is running """ await asyncio.sleep( random.randint( @@ -94,7 +94,6 @@ async def wait(self: Self, config: munch.Munch, guild: discord.Guild) -> None: async def execute( self: Self, - config: munch.Munch, guild: discord.Guild, channel: discord.TextChannel, banned_user: discord.User = None, @@ -103,12 +102,12 @@ async def execute( Can be manually called, and will be called automatically after wait() Args: - config (munch.Munch): The config of the guild where the duck is going guild (discord.Guild): The guild where the duck is going channel (discord.TextChannel): The channel to spawn the duck in banned_user (discord.User, optional): A user that is not allowed to claim the duck. Defaults to None. """ + config = self.bot.guild_configs[str(guild.id)] if not channel: log_channel = config.get("logging_channel") await self.bot.logger.send_log( @@ -339,7 +338,7 @@ def message_check( embed = auxiliary.prepare_deny_embed(message=quote) embed.set_footer( text=( - f"You missed. Try again in {configuration.get_config_entry(message.guild.id, "duck_cooldown")} " + f"You missed. Try again in {configuration.get_config_entry(message.guild.id, 'duck_cooldown')} " f"seconds. Time would have been {duration_exact} seconds" ) ) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 7e12a151e..929015028 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -641,13 +641,14 @@ async def on_raw_message_delete( reason=("It appears the original post for this thread was deleted."), ) - async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None: + async def execute(self: Self, guild: discord.Guild) -> None: """This is what closes threads after inactivity Args: config (munch.Munch): The guild config where the loop is taking place guild (discord.Guild): The guild where the loop is taking place """ + config = self.bot.guild_configs[str(guild.id)] channel = await guild.fetch_channel( int(config.extensions.forum.forum_channel_id.value) ) @@ -673,12 +674,8 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None "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 - - Args: - config (munch.Munch): The guild config where the loop is taking place - """ + async def wait(self: Self, _: discord.Guild) -> None: + """This waits and rechecks every 5 minutes to search for old threads""" await asyncio.sleep(300) diff --git a/techsupport_bot/commands/kanye.py b/techsupport_bot/commands/kanye.py index 66e21e34d..bd844de10 100644 --- a/techsupport_bot/commands/kanye.py +++ b/techsupport_bot/commands/kanye.py @@ -92,7 +92,7 @@ async def get_quote(self: Self) -> str: response = await self.bot.http_functions.http_call("get", self.API_URL) return response.get("quote") - async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None: + async def execute(self: Self, guild: discord.Guild) -> None: """The main entry point for the loop for kanye This is executed automatically and shouldn't be called manually @@ -100,6 +100,7 @@ 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 """ + config = self.bot.guild_configs[str(guild.id)] quote = await self.get_quote() embed = self.generate_themed_embed(quote=quote) @@ -109,12 +110,13 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None await channel.send(embed=embed) - async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: + async def wait(self: Self, guild: 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 """ + config = self.bot.guild_configs[str(guild.id)] await asyncio.sleep( random.randint( config.extensions.kanye.min_wait.value * 3600, diff --git a/techsupport_bot/commands/news.py b/techsupport_bot/commands/news.py index 957d12629..e678ad0d5 100644 --- a/techsupport_bot/commands/news.py +++ b/techsupport_bot/commands/news.py @@ -172,7 +172,7 @@ async def get_random_headline( # Choose a random article from the filtered list return random.choice(filtered_articles) - async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None: + async def execute(self: Self, guild: discord.Guild) -> None: """Loop entry point for the news command If a channel is configured to loop news headlines, this will execute that @@ -180,6 +180,7 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None config (munch.Munch): The guild config for the guild looping guild (discord.Guild): The guild where the loop is running """ + config = self.bot.guild_configs[str(guild.id)] channel = guild.get_channel(int(config.extensions.news.channel.value)) if not channel: return @@ -206,12 +207,13 @@ async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None url = url[:-1] await channel.send(url) - async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: + async def wait(self: Self, guild: discord.Guild) -> None: """Waits the defined time set for the loop, based on the cronjob Args: config (munch.Munch): The guild config where the loop will occur """ + config = self.bot.guild_configs[str(guild.id)] await aiocron.crontab(config.extensions.news.cron_config.value).next() @app_commands.command( diff --git a/techsupport_bot/commands/voting.py b/techsupport_bot/commands/voting.py index 774387bcc..bcb5050a1 100644 --- a/techsupport_bot/commands/voting.py +++ b/techsupport_bot/commands/voting.py @@ -634,22 +634,18 @@ def clear_vote_record( return db_entry - async def wait(self: Self, config: munch.Munch, _: discord.Guild) -> None: - """Makes a check every hour for if any votes have concluded - - Args: - config (munch.Munch): The guild config where the vote was started - """ + async def wait(self: Self, _: discord.Guild) -> None: + """Makes a check every hour for if any votes have concluded""" # We check every hour on the hour for completed votes await aiocron.crontab("0 * * * *").next() - async def execute(self: Self, config: munch.Munch, guild: discord.Guild) -> None: + async def execute(self: Self, guild: discord.Guild) -> None: """This looks for completed votes and ends then Args: - config (munch.Munch): The guild config for the guild with the vote guild (discord.Guild): The guild the vote is being run in """ + config = self.bot.guild_configs[str(guild.id)] # pylint: disable=C0121 active_votes = ( await self.bot.models.Votes.query.where( diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 328c61e88..77da6b96c 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -18,6 +18,7 @@ "duck_allow_manipulation": true, "duck_cooldown": 5, "duck_max_wait": 4, + "duck_min_wait": 2, "duck_mute_for_cooldown": true, "duck_spawn_user": [], "duck_timeout": 60, @@ -26,5 +27,6 @@ "embed_embed_roles": [], "core_enabled_extensions": ["config", "extension"], "core_logging_channel": "", - "modmail_modmail_roles": [] + "modmail_modmail_roles": [], + "duck_hunt_channels": [] } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index cafb1b115..ff95b5020 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -114,6 +114,10 @@ "modmail_modmail_roles": { "datatype": "list[discord.Role]", "description": "Roles that can access modmail and its commands" + }, + "duck_hunt_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The list of channels the duck should appear in" } diff --git a/techsupport_bot/core/cogs.py b/techsupport_bot/core/cogs.py index ffbdb7cd9..b6a9df80f 100644 --- a/techsupport_bot/core/cogs.py +++ b/techsupport_bot/core/cogs.py @@ -138,7 +138,7 @@ async def on_message(self: Self, message: discord.Message) -> None: context=LogContext(guild=ctx.guild, channel=ctx.channel), ) bot_logging_channel = configuration.get_config_entry( - message.guild, "core_logging_channel" + message.guild.id, "core_logging_channel" ) await self.bot.logger.send_log( message=f"Match cog error: {self.__class__.__name__} {exception}!", @@ -210,12 +210,11 @@ async def register_new_tasks(self: Self, guild: discord.Guild) -> None: Args: guild (discord.Guild): the guild to add the tasks for """ - config = self.bot.guild_configs[str(guild.id)] - channels = ( - config.extensions.get(self.extension_name, {}) - .get(self.CHANNELS_KEY, {}) - .get("value") - ) + try: + channels = configuration.get_config_entry(guild.id, self.CHANNELS_KEY) + except AttributeError: + channels = None + if channels is not None: channels = sorted(set(channels)) self.channels[guild.id] = [ @@ -274,12 +273,9 @@ async def _track_new_channels(self: Self) -> None: level=LogLevel.DEBUG, ) for guild_id, registered_channels in self.channels.items(): - guild = self.bot.get_guild(guild_id) - config = self.bot.guild_configs[str(guild.id)] - configured_channels = ( - config.extensions.get(self.extension_name, {}) - .get(self.CHANNELS_KEY, {}) - .get("value") + guild: discord.Guild = self.bot.get_guild(guild_id) + configured_channels = configuration.get_config_entry( + guild.id, self.CHANNELS_KEY ) if not isinstance(configured_channels, list): await self.bot.logger.send_log( @@ -339,26 +335,21 @@ async def _loop_execute( target_channel (discord.abc.Messageable): The channel to run the loop in, if the loop is channel specific """ - config = self.bot.guild_configs[str(guild.id)] - if not self.ON_START: - await self.wait(config, guild) + await self.wait(guild) for folder_dir in [self.bot.EXTENSIONS_DIR_NAME, self.bot.FUNCTIONS_DIR_NAME]: while self.bot.extensions.get(f"{folder_dir}.{self.extension_name}"): if guild and guild not in self.bot.guilds: break - # refresh the config on every loop step - config = self.bot.guild_configs[str(guild.id)] + channels_list = configuration.get_config_entry( + guild.id, self.CHANNELS_KEY + ) + if not channels_list: + channels_list = [] - if target_channel and not str( - target_channel.id - ) in config.extensions.get(self.extension_name, {}).get( - self.CHANNELS_KEY, {} - ).get( - "value", [] - ): + if target_channel and not str(target_channel.id) in channels_list: # exit task if the channel is no longer configured break @@ -371,25 +362,30 @@ async def _loop_execute( ): try: if target_channel: - await self.execute(config, guild, target_channel) + await self.execute(guild, target_channel) else: - await self.execute(config, guild) + await self.execute(guild) except Exception as exception: # always try to wait even when execute fails await self.bot.logger.send_log( message=f"Loop cog execute error: {self.__class__.__name__}!", level=LogLevel.ERROR, - channel=getattr(config, "logging_channel", None), + channel=configuration.get_config_entry( + guild.id, "core_logging_channel" + ), context=LogContext(guild=guild), exception=exception, ) try: - await self.wait(config, guild) + await self.wait(guild) except Exception as exception: await self.bot.logger.send_log( message=f"Loop wait cog error: {self.__class__.__name__}!", level=LogLevel.ERROR, + channel=configuration.get_config_entry( + guild.id, "core_logging_channel" + ), context=LogContext(guild=guild), exception=exception, ) @@ -398,14 +394,12 @@ async def _loop_execute( async def execute( self: Self, - _config: munch.Munch, _guild: discord.Guild, _target_channel: discord.abc.Messageable = None, ) -> None: """Runs sequentially after each wait method. Args: - _config (munch.Munch): the config object for the guild _guild (discord.Guild): the guild associated with the execution _target_channel (discord.abc.Messageable): the channel object to use """ @@ -414,11 +408,10 @@ async def _default_wait(self: Self) -> None: """The default method used for waiting.""" await asyncio.sleep(self.DEFAULT_WAIT) - async def wait(self: Self, _config: munch.Munch, _guild: discord.Guild) -> None: + async def wait(self: Self, _guild: discord.Guild) -> None: """The default wait method. Args: - _config (munch.Munch): the config object for the guild _guild (discord.Guild): the guild associated with the execution """ await self._default_wait() From 1095c3c2b491adbb74adfafb9f092a2963296e63 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 20:44:14 -0700 Subject: [PATCH 12/40] Pylint formatting changes --- techsupport_bot/commands/application.py | 1 - techsupport_bot/commands/duck.py | 11 ++++++----- techsupport_bot/commands/kanye.py | 1 - techsupport_bot/commands/modlog.py | 2 -- techsupport_bot/commands/modmail.py | 1 - techsupport_bot/commands/relay.py | 1 - techsupport_bot/commands/xp.py | 2 -- techsupport_bot/core/cogs.py | 3 +-- techsupport_bot/functions/automod.py | 4 ++-- techsupport_bot/functions/autoreact.py | 1 - techsupport_bot/functions/honeypot.py | 1 - techsupport_bot/functions/nickname.py | 1 - techsupport_bot/functions/paste.py | 4 ++-- 13 files changed, 11 insertions(+), 22 deletions(-) diff --git a/techsupport_bot/commands/application.py b/techsupport_bot/commands/application.py index 73bb9af1e..e02baddfc 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -9,7 +9,6 @@ import aiocron import configuration import discord -import munch import ui from core import auxiliary, cogs from discord import app_commands diff --git a/techsupport_bot/commands/duck.py b/techsupport_bot/commands/duck.py index 5904e3c71..6c0bed103 100644 --- a/techsupport_bot/commands/duck.py +++ b/techsupport_bot/commands/duck.py @@ -333,12 +333,15 @@ def message_check( if not choice: time = message.created_at - duck_message.created_at duration_exact = float(str(time.seconds) + "." + str(time.microseconds)) + pause_time = configuration.get_config_entry( + message.guild.id, "duck_cooldown" + ) cooldowns[message.author.id] = datetime.datetime.now() quote = self.pick_quote() embed = auxiliary.prepare_deny_embed(message=quote) embed.set_footer( text=( - f"You missed. Try again in {configuration.get_config_entry(message.guild.id, 'duck_cooldown')} " + f"You missed. Try again in {pause_time} " f"seconds. Time would have been {duration_exact} seconds" ) ) @@ -689,7 +692,6 @@ async def release(self: Self, ctx: commands.Context) -> None: Args: ctx (commands.Context): The context in which the command was run """ - config = self.bot.guild_configs[str(ctx.guild.id)] if not configuration.get_config_entry(ctx.guild.id, "duck_allow_manipulation"): await auxiliary.send_deny_embed( channel=ctx.channel, message="This command is disabled in this server" @@ -717,7 +719,7 @@ async def release(self: Self, ctx: commands.Context) -> None: channel=ctx.channel, ) - await self.execute(config, ctx.guild, ctx.channel, banned_user=ctx.author) + await self.execute(ctx.guild, ctx.channel, banned_user=ctx.author) @auxiliary.with_typing @commands.guild_only() @@ -911,11 +913,10 @@ async def spawn(self: Self, ctx: commands.Context) -> None: Args: ctx (commands.Context): The context in which the command was run """ - config = self.bot.guild_configs[str(ctx.guild.id)] spawn_user = configuration.get_config_entry(ctx.guild.id, "duck_spawn_user") for person in spawn_user: if ctx.author.id == int(person): - await self.execute(config, ctx.guild, ctx.channel) + await self.execute(ctx.guild, ctx.channel) return await auxiliary.send_deny_embed( message="It looks like you don't have permissions to spawn a duck", diff --git a/techsupport_bot/commands/kanye.py b/techsupport_bot/commands/kanye.py index bd844de10..9133a408b 100644 --- a/techsupport_bot/commands/kanye.py +++ b/techsupport_bot/commands/kanye.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Self import discord -import munch from core import auxiliary, cogs, extensionconfig from discord.ext import commands diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 8da9a0768..c92bdcc61 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -215,7 +215,6 @@ async def on_member_ban( discord.utils.utcnow() + datetime.timedelta(seconds=2) ) - config = self.bot.guild_configs[str(guild.id)] if not self.extension_enabled(guild): return @@ -251,7 +250,6 @@ async def on_member_unban( discord.utils.utcnow() + datetime.timedelta(seconds=2) ) - config = self.bot.guild_configs[str(guild.id)] if not self.extension_enabled(guild): return diff --git a/techsupport_bot/commands/modmail.py b/techsupport_bot/commands/modmail.py index b8d0e0823..946f1f304 100644 --- a/techsupport_bot/commands/modmail.py +++ b/techsupport_bot/commands/modmail.py @@ -21,7 +21,6 @@ import configuration import discord import expiringdict -import munch import ui from core import auxiliary, cogs, extensionconfig from discord.ext import commands diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index 9af99839f..d715df53e 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -7,7 +7,6 @@ import configuration import discord import irc.client -import munch import ui from bidict import bidict from core import auxiliary, cogs diff --git a/techsupport_bot/commands/xp.py b/techsupport_bot/commands/xp.py index 3ba25244f..bdd0b00c2 100644 --- a/techsupport_bot/commands/xp.py +++ b/techsupport_bot/commands/xp.py @@ -8,7 +8,6 @@ import configuration import discord import expiringdict -import munch from core import auxiliary, cogs, extensionconfig from discord import app_commands from discord.ext import commands @@ -189,7 +188,6 @@ async def response( ctx (commands.Context): The context in which the message was sent in content (str): The string content of the message """ - config = self.bot.guild_configs[str(ctx.guild.id)] current_XP = await get_current_XP(self.bot, ctx.author, ctx.guild) new_XP = random.randint(10, 20) diff --git a/techsupport_bot/core/cogs.py b/techsupport_bot/core/cogs.py index b6a9df80f..e1a938a5d 100644 --- a/techsupport_bot/core/cogs.py +++ b/techsupport_bot/core/cogs.py @@ -8,7 +8,6 @@ import configuration import discord -import munch from botlogging import LogContext, LogLevel from discord.ext import commands @@ -349,7 +348,7 @@ async def _loop_execute( if not channels_list: channels_list = [] - if target_channel and not str(target_channel.id) in channels_list: + if target_channel and str(target_channel.id) not in channels_list: # exit task if the channel is no longer configured break diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 097b95642..e6d13c6a2 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -336,11 +336,11 @@ async def on_raw_message_edit( return ctx = await self.bot.get_context(message) - matched = await self.match(config, ctx, message.content) + matched = await self.match(ctx, message.content) if not matched: return - await self.response(config, ctx, message.content, matched) + await self.response(ctx, message.content, matched) def process_automod_violations( diff --git a/techsupport_bot/functions/autoreact.py b/techsupport_bot/functions/autoreact.py index 507d14fba..4463a6851 100644 --- a/techsupport_bot/functions/autoreact.py +++ b/techsupport_bot/functions/autoreact.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Self import configuration -import munch from core import auxiliary, cogs from discord.ext import commands diff --git a/techsupport_bot/functions/honeypot.py b/techsupport_bot/functions/honeypot.py index d89cdd53f..974dd3c03 100644 --- a/techsupport_bot/functions/honeypot.py +++ b/techsupport_bot/functions/honeypot.py @@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Self import discord -import munch from core import cogs, extensionconfig from discord.ext import commands diff --git a/techsupport_bot/functions/nickname.py b/techsupport_bot/functions/nickname.py index 4ca3f5088..87ba66517 100644 --- a/techsupport_bot/functions/nickname.py +++ b/techsupport_bot/functions/nickname.py @@ -15,7 +15,6 @@ import configuration import discord -import munch from botlogging import LogContext, LogLevel from core import cogs from discord.ext import commands diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index 5851a3698..cb5cc010e 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -164,11 +164,11 @@ async def on_raw_message_edit( return ctx = await self.bot.get_context(message) - matched = await self.match(config, ctx, message.content) + matched = await self.match(ctx, message.content) if not matched: return - await self.response(config, ctx, message.content, None) + await self.response(ctx, message.content, None) async def paste_message( self: Self, config: munch.Munch, ctx: commands.Context, content: str From 707b33a2fa1dd9e917b25ad106d0525645c5ce3f Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 20:47:10 -0700 Subject: [PATCH 13/40] Pylint formatting changes 2 --- techsupport_bot/functions/automod.py | 1 - techsupport_bot/functions/paste.py | 1 - 2 files changed, 2 deletions(-) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index e6d13c6a2..29efc4218 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -319,7 +319,6 @@ async def on_raw_message_edit( if not guild: return - config = self.bot.guild_configs[str(guild.id)] if not self.extension_enabled(guild): return diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index cb5cc010e..92f3e9023 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -147,7 +147,6 @@ async def on_raw_message_edit( if not guild: return - config = self.bot.guild_configs[str(guild.id)] if not self.extension_enabled(guild): return From 945ad520f828662230f7155a1874e45d3f4043a6 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 20:57:07 -0700 Subject: [PATCH 14/40] flake8 formatting changes --- techsupport_bot/commands/factoids.py | 1 + techsupport_bot/commands/forum.py | 1 - techsupport_bot/commands/kanye.py | 3 +-- techsupport_bot/commands/news.py | 3 +-- techsupport_bot/configuration/config.py | 10 ++++++++-- techsupport_bot/core/cogs.py | 4 ++-- techsupport_bot/functions/autoreact.py | 1 + techsupport_bot/functions/nickname.py | 1 - 8 files changed, 14 insertions(+), 10 deletions(-) diff --git a/techsupport_bot/commands/factoids.py b/techsupport_bot/commands/factoids.py index d6f60368e..6748c2c42 100644 --- a/techsupport_bot/commands/factoids.py +++ b/techsupport_bot/commands/factoids.py @@ -783,6 +783,7 @@ async def match(self: Self, ctx: commands.Context, message_contents: str) -> boo """Checks if a message started with the prefix from the config Args: + ctx (commands.Context): The context of which the message was sent message_contents (str): The message to check Returns: diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 929015028..087fac558 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -645,7 +645,6 @@ async def execute(self: Self, guild: discord.Guild) -> None: """This is what closes threads after inactivity Args: - config (munch.Munch): The guild config where the loop is taking place guild (discord.Guild): The guild where the loop is taking place """ config = self.bot.guild_configs[str(guild.id)] diff --git a/techsupport_bot/commands/kanye.py b/techsupport_bot/commands/kanye.py index 9133a408b..d3906c96d 100644 --- a/techsupport_bot/commands/kanye.py +++ b/techsupport_bot/commands/kanye.py @@ -96,7 +96,6 @@ async def execute(self: Self, guild: discord.Guild) -> None: 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 """ config = self.bot.guild_configs[str(guild.id)] @@ -113,7 +112,7 @@ async def wait(self: Self, guild: 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 + guild (discord.Guild): The guild config where the loop is taking place """ config = self.bot.guild_configs[str(guild.id)] await asyncio.sleep( diff --git a/techsupport_bot/commands/news.py b/techsupport_bot/commands/news.py index e678ad0d5..48ee56a2a 100644 --- a/techsupport_bot/commands/news.py +++ b/techsupport_bot/commands/news.py @@ -177,7 +177,6 @@ async def execute(self: Self, guild: discord.Guild) -> None: If a channel is configured to loop news headlines, this will execute that Args: - config (munch.Munch): The guild config for the guild looping guild (discord.Guild): The guild where the loop is running """ config = self.bot.guild_configs[str(guild.id)] @@ -211,7 +210,7 @@ async def wait(self: Self, guild: discord.Guild) -> None: """Waits the defined time set for the loop, based on the cronjob Args: - config (munch.Munch): The guild config where the loop will occur + guild (discord.Guild): The guild where the loop will occur """ config = self.bot.guild_configs[str(guild.id)] await aiocron.crontab(config.extensions.news.cron_config.value).next() diff --git a/techsupport_bot/configuration/config.py b/techsupport_bot/configuration/config.py index 53dd06124..98e5d6add 100644 --- a/techsupport_bot/configuration/config.py +++ b/techsupport_bot/configuration/config.py @@ -23,7 +23,7 @@ BASE_PATH = "configuration/" -def get_config_entry(guild_id: int, key: str) -> Any: +def get_config_entry(guild_id: int, key: str) -> Any: # noqa: ANN401 """This searches for a guild specific config entry Args: @@ -32,6 +32,9 @@ def get_config_entry(guild_id: int, key: str) -> Any: Returns: Any: The value of the config, which may be of many types + + Raises: + AttributeError: Raised if the passed key is not valid """ if not check_key_valid(key): @@ -49,7 +52,7 @@ def get_config_entry(guild_id: int, key: str) -> Any: return default_entry -def get_default_config_entry(key: str) -> Any: +def get_default_config_entry(key: str) -> Any: # noqa: ANN401 """This gets the value from the default config file for the passed key Args: @@ -57,6 +60,9 @@ def get_default_config_entry(key: str) -> Any: Returns: Any: The value from the default config file + + Raises: + AttributeError: Raised if the passed key is not valid """ if not check_key_valid(key): raise AttributeError(f"Key {key} is invalid") diff --git a/techsupport_bot/core/cogs.py b/techsupport_bot/core/cogs.py index e1a938a5d..3038a9382 100644 --- a/techsupport_bot/core/cogs.py +++ b/techsupport_bot/core/cogs.py @@ -75,10 +75,10 @@ async def preconfig(self: Self) -> None: """Preconfigures the environment before starting the cog.""" def extension_enabled(self: Self, guild: discord.Guild) -> bool: - """Checks if an extension is currently enabled for a given config. + """Checks if an extension is currently enabled for a given guild. Args: - config (munch.Munch): the context/guild config + guild (discord.Guild): The guild to lookup extension status for Returns: bool: True if the extension is enabled for the context diff --git a/techsupport_bot/functions/autoreact.py b/techsupport_bot/functions/autoreact.py index 4463a6851..d431954dc 100644 --- a/techsupport_bot/functions/autoreact.py +++ b/techsupport_bot/functions/autoreact.py @@ -28,6 +28,7 @@ async def match(self: Self, ctx: commands.Context, content: str) -> bool: """A match function to determine if somehting should be reacted to Args: + ctx (commands.Context): The context in which the message was sent content (str): The string content of the message Returns: diff --git a/techsupport_bot/functions/nickname.py b/techsupport_bot/functions/nickname.py index 87ba66517..b4699da58 100644 --- a/techsupport_bot/functions/nickname.py +++ b/techsupport_bot/functions/nickname.py @@ -86,7 +86,6 @@ async def match(self: Self, ctx: commands.Context, content: str) -> bool: """On every message, check if the authors nickname should be changed Args: - config (munch.Munch): The guild config ctx (commands.Context): The context that sent the message content (str): The content of the message From ebc57219659e3cd95507bd1b98459c21c28c2672 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:05:28 -0700 Subject: [PATCH 15/40] Rewrite 2 functions in duck, clear old config system from duck --- techsupport_bot/commands/duck.py | 52 ++++++------------- .../configuration/config.default.json | 3 +- .../configuration/config.meta.json | 4 ++ 3 files changed, 21 insertions(+), 38 deletions(-) diff --git a/techsupport_bot/commands/duck.py b/techsupport_bot/commands/duck.py index 6c0bed103..0f79204e7 100644 --- a/techsupport_bot/commands/duck.py +++ b/techsupport_bot/commands/duck.py @@ -11,10 +11,9 @@ import configuration import discord -import munch import ui from botlogging import LogContext, LogLevel -from core import auxiliary, cogs, extensionconfig, moderation +from core import auxiliary, cogs, moderation from discord import Color as embed_colors from discord.ext import commands @@ -28,25 +27,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - - config = extensionconfig.ExtensionConfig() - config.add( - key="hunt_channels", - datatype="list", - title="DuckHunt Channel IDs", - description="The IDs of the channels the duck should appear in", - default=[], - ) - config.add( - key="success_rate", - datatype="int", - title="Success rate (percent %)", - description="The success rate of bef/bang messages", - default=50, - ) - await bot.add_cog(DuckHunt(bot=bot, extension_name="duck")) - bot.add_extension_config("duck", config) class DuckHunt(cogs.LoopCog): @@ -107,9 +88,10 @@ async def execute( banned_user (discord.User, optional): A user that is not allowed to claim the duck. Defaults to None. """ - config = self.bot.guild_configs[str(guild.id)] if not channel: - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message="Channel not found for Duckhunt loop - continuing", level=LogLevel.WARNING, @@ -143,14 +125,15 @@ async def execute( timeout=configuration.get_config_entry(guild.id, "duck_timeout"), # can't pull the config in a non-coroutine check=functools.partial( - self.message_check, config, use_channel, duck_message, banned_user + self.message_check, use_channel, duck_message, banned_user ), ) except asyncio.TimeoutError: pass except Exception as exception: - config = self.bot.guild_configs[str(guild.id)] - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message="Exception thrown waiting for duckhunt input", level=LogLevel.ERROR, @@ -204,8 +187,7 @@ async def handle_winner( channel (discord.abc.Messageable): The channel in which the duck game happened in """ - config_ = self.bot.guild_configs[str(guild.id)] - log_channel = config_.get("logging_channel") + log_channel = configuration.get_config_entry(guild.id, "core_logging_channel") await self.bot.logger.send_log( message=f"Duck {action} by {winner} in #{channel.name}", level=LogLevel.INFO, @@ -277,7 +259,6 @@ def pick_quote(self: Self) -> str: def message_check( self: Self, - config: munch.Munch, channel: discord.abc.GuildChannel, duck_message: discord.Message, banned_user: discord.User, @@ -286,7 +267,6 @@ def message_check( """Checks if a message after the duck is a valid call to own the duck Args: - config (munch.Munch): The config of the guild where the duck is channel (discord.abc.GuildChannel): The channel that the duck is in duck_message (discord.Message): The message object of the duck embed banned_user (discord.User): A user who is banned from claiming the duck @@ -329,7 +309,7 @@ def message_check( return False # Check to see if random failure - choice = self.random_choice(config) + choice = self.random_choice(message.guild) if not choice: time = message.created_at - duck_message.created_at duration_exact = float(str(time.seconds) + "." + str(time.microseconds)) @@ -737,7 +717,6 @@ async def kill(self: Self, ctx: commands.Context) -> None: Args: ctx (commands.Context): The context in which the command was run """ - config = self.bot.guild_configs[str(ctx.guild.id)] if not configuration.get_config_entry(ctx.guild.id, "duck_allow_manipulation"): await auxiliary.send_deny_embed( channel=ctx.channel, message="This command is disabled in this server" @@ -761,7 +740,7 @@ async def kill(self: Self, ctx: commands.Context) -> None: await duck_user.update(befriend_count=duck_user.befriend_count - 1).apply() - passed = self.random_choice(config) + passed = self.random_choice(ctx.guild) if not passed: await auxiliary.send_deny_embed( message="The duck got away before you could kill it.", @@ -792,7 +771,6 @@ async def donate(self: Self, ctx: commands.Context, user: discord.Member) -> Non ctx (commands.Context): The context in which the command was run user (discord.Member): The user to donate a duck to """ - config = self.bot.guild_configs[str(ctx.guild.id)] if not configuration.get_config_entry(ctx.guild.id, "duck_allow_manipulation"): await auxiliary.send_deny_embed( channel=ctx.channel, message="This command is disabled in this server" @@ -834,7 +812,7 @@ async def donate(self: Self, ctx: commands.Context, user: discord.Member) -> Non await duck_user.update(befriend_count=duck_user.befriend_count - 1).apply() - passed = self.random_choice(config) + passed = self.random_choice(ctx.guild) if not passed: await auxiliary.send_deny_embed( message="The duck got away before you could donate it.", @@ -923,7 +901,7 @@ async def spawn(self: Self, ctx: commands.Context) -> None: channel=ctx.channel, ) - def random_choice(self: Self, config: munch.Munch) -> bool: + def random_choice(self: Self, guild: discord.Guild) -> bool: """A function to pick true or false randomly based on the success_rate in the config Args: @@ -934,8 +912,8 @@ def random_choice(self: Self, config: munch.Munch) -> bool: """ weights = ( - config.extensions.duck.success_rate.value, - 100 - config.extensions.duck.success_rate.value, + configuration.get_config_entry(guild.id, "duck_success_rate"), + 100 - configuration.get_config_entry(guild.id, "duck_success_rate"), ) # Check to see if random failure diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 77da6b96c..c7e621468 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -28,5 +28,6 @@ "core_enabled_extensions": ["config", "extension"], "core_logging_channel": "", "modmail_modmail_roles": [], - "duck_hunt_channels": [] + "duck_hunt_channels": [], + "duck_success_rate": 50 } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index ff95b5020..96ba09318 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -118,6 +118,10 @@ "duck_hunt_channels": { "datatype": "list[discord.TextChannel]", "description": "The list of channels the duck should appear in" + }, + "duck_success_rate": { + "datatype": "int", + "description": "The success rate of bef/bang messages" } From 8555e387cfbe92d0d270b70f36a0fbf61539cdbe Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:07:48 -0700 Subject: [PATCH 16/40] Update docstring in duck --- documentation/Extension-howto.md | 10 +++------- techsupport_bot/commands/duck.py | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/documentation/Extension-howto.md b/documentation/Extension-howto.md index 74641cbbc..2a634980d 100644 --- a/documentation/Extension-howto.md +++ b/documentation/Extension-howto.md @@ -262,14 +262,10 @@ To access the values, you can use the following: self.bot.file_config.group.subgroup.key ``` --- -To access the json config, you can add the following line of code, which loads the guild config file: +To access the config for a given guild, use the "get_config_entry" in the configuration module: ```py -config = self.bot.guild_configs[guild_id] -``` - -Afterwards you can access the values with -```py -config.extensions...value +import configuration +value = configuration.get_config_entry(guild.id, "config_key") ``` ## Calling an API diff --git a/techsupport_bot/commands/duck.py b/techsupport_bot/commands/duck.py index 0f79204e7..c749aa754 100644 --- a/techsupport_bot/commands/duck.py +++ b/techsupport_bot/commands/duck.py @@ -905,7 +905,7 @@ def random_choice(self: Self, guild: discord.Guild) -> bool: """A function to pick true or false randomly based on the success_rate in the config Args: - config (munch.Munch): The config for the guild + guild (discord.Guild): The guild the duck was bang/bef'd in Returns: bool: Whether the random choice should succeed or not From 0e162086105d1afbbaf2c567434e70f592ba8f19 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:18:47 -0700 Subject: [PATCH 17/40] Rewrite a bunch of easy functions --- techsupport_bot/commands/gate.py | 8 +++----- techsupport_bot/commands/joke.py | 14 +++++--------- techsupport_bot/commands/voting.py | 18 ++++++------------ techsupport_bot/functions/paste.py | 13 ++++++------- 4 files changed, 20 insertions(+), 33 deletions(-) diff --git a/techsupport_bot/commands/gate.py b/techsupport_bot/commands/gate.py index 65abcd6f2..f96d9aae4 100644 --- a/techsupport_bot/commands/gate.py +++ b/techsupport_bot/commands/gate.py @@ -114,7 +114,7 @@ async def response( config = self.bot.guild_configs[str(ctx.guild.id)] if content.lower() == config.extensions.gate.verify_text.value: - roles = await self.get_roles(config, ctx) + roles = await self.get_roles(ctx) if not roles: config = self.bot.guild_configs[str(ctx.guild.id)] log_channel = config.get("logging_channel") @@ -147,19 +147,17 @@ async def response( delete_after=float(delete_wait), ) - async def get_roles( - self: Self, config: munch.Munch, ctx: commands.Context - ) -> list[discord.Role]: + async def get_roles(self: Self, ctx: commands.Context) -> list[discord.Role]: """Builds a list of roles that the user in ctx doesn't have, but are listed in the gate config roles to be applied Args: - config (munch.Munch): The config of the guild ctx (commands.Context): The context of the message that triggered the gate Returns: list[discord.Role]: A list of all the roles that should be given to the user """ + config = self.bot.guild_configs[str(ctx.guild.id)] roles = [] for role_name in config.extensions.gate.roles.value: role = discord.utils.get(ctx.guild.roles, name=role_name) diff --git a/techsupport_bot/commands/joke.py b/techsupport_bot/commands/joke.py index ce2892e91..b80b6c16e 100644 --- a/techsupport_bot/commands/joke.py +++ b/techsupport_bot/commands/joke.py @@ -52,36 +52,33 @@ class Joker(cogs.BaseCog): API_URL: str = "https://v2.jokeapi.dev/joke/Any" - async def call_api( - self: Self, ctx: commands.Context, config: munch.Munch - ) -> munch.Munch: + async def call_api(self: Self, ctx: commands.Context) -> munch.Munch: """Calls the joke API and returns the raw response Args: ctx (commands.Context): The context in which the joke command was run in - config (munch.Munch): The guild config for the guild where the joke command was run Returns: munch.Munch: The reply from the API """ - url = self.build_url(ctx, config) + url = self.build_url(ctx) response = await self.bot.http_functions.http_call( "get", url, get_raw_response=True ) return response - def build_url(self: Self, ctx: commands.Context, config: munch.Munch) -> str: + def build_url(self: Self, ctx: commands.Context) -> str: """Builds the API URL based on exclusions of categories Will exclude NSFW jokes if the channel isn't NSFW Will exclude offensive jokes if the PC jokes config is enabled Args: ctx (commands.Context): The context in which the original joke command was run in - config (munch.Munch): The config for the guild where the original command was run Returns: str: The URL, properly formatted and ready to be called """ + config = self.bot.guild_configs[str(ctx.guild.id)] blacklist_flags = [] if ( config.extensions.joke.apply_in_nsfw_channels.value @@ -122,8 +119,7 @@ async def joke(self: Self, ctx: commands.Context) -> None: Args: ctx (commands.Context): The context in which the command was run in """ - config = self.bot.guild_configs[str(ctx.guild.id)] - response = await self.call_api(ctx, config) + response = await self.call_api(ctx) text = response["text"] embed = self.generate_embed(text) await ctx.send(embed=embed) diff --git a/techsupport_bot/commands/voting.py b/techsupport_bot/commands/voting.py index bcb5050a1..a1b191c82 100644 --- a/techsupport_bot/commands/voting.py +++ b/techsupport_bot/commands/voting.py @@ -130,7 +130,6 @@ async def votingbutton( if not self.user_can_use_vote_channel( member=interaction.user, channel=channel, - config=config, ): embed = auxiliary.prepare_deny_embed( "You do not have rights to start that vote!" @@ -233,7 +232,6 @@ async def vote_channel_autocomplete( if not self.user_can_use_vote_channel( member=member, channel=channel, - config=config, ): continue @@ -250,18 +248,17 @@ def user_can_use_vote_channel( self: Self, member: discord.Member, channel: discord.abc.GuildChannel, - config: munch.Munch, ) -> bool: """This checks if the user can start a vote in a given channel Args: member (discord.Member): The member that is trying to start a vote channel (discord.abc.GuildChannel): The channel the vote is going to be started in - config (munch.Munch): The guild config for the current guild Returns: bool: True if the channel is valid, false if its not """ + config = self.bot.guild_configs[str(member.guild.id)] if not isinstance(channel, discord.ForumChannel): return False @@ -668,7 +665,7 @@ async def execute(self: Self, guild: discord.Guild) -> None: # End expired votes if end_time <= timestamp_now: - await self.end_vote(vote, guild, config) + await self.end_vote(vote, guild) continue # Reminder checks @@ -680,13 +677,12 @@ async def execute(self: Self, guild: discord.Guild) -> None: ) if abs(timestamp_now - reminder_timestamp) <= 300: - await self.remind_vote(vote, guild, config, reminder_hour) + await self.remind_vote(vote, guild, reminder_hour) async def remind_vote( self: Self, vote: munch.Munch, guild: discord.Guild, - config: munch.Munch, reminder_hour: int, ) -> None: """This sends a reminder to vote, based on who hasn't voted in the current vote @@ -695,9 +691,9 @@ async def remind_vote( Args: vote (munch.Munch): The vote object we are reminding for guild (discord.Guild): The guild the vote is in - config (munch.Munch): The guild config reminder_hour (int): The hours remining until the vote closes """ + config = self.bot.guild_configs[str(guild.id)] # Get all eligible voters eligible_voters = [v for v in vote.vote_ids_eligible.split(",") if v] # Get all voted voters @@ -721,17 +717,15 @@ async def remind_vote( await channel.send(content=mention_string, embed=embed) - async def end_vote( - self: Self, vote: munch.Munch, guild: discord.Guild, config: munch.Munch - ) -> None: + async def end_vote(self: Self, vote: munch.Munch, guild: discord.Guild) -> None: """This ends a vote, and if it was anonymous purges who voted for what from the database This will edit the vote message and remove the buttons, and mention the vote owner Args: vote (munch.Munch): The vote database object that needs to be ended guild (discord.Guild): The guild that vote belongs to - config (munch.Munch): The guild config for the guild of the vote """ + config = self.bot.guild_configs[str(guild.id)] await vote.update(vote_active=False).apply() embed = await self.build_vote_embed(vote.vote_id, guild) pass_embed = self.build_vote_pass_embed(vote, config) diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index 92f3e9023..c4dcfa158 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -120,7 +120,7 @@ async def response( automod_final = automod.process_automod_violations(automod_actions) if automod_final and automod_final.delete_message: return - await self.paste_message(config, ctx, content) + await self.paste_message(ctx, content) def max_newlines(self: Self, max_length: int) -> int: """Gets a theoretical maximum number of new lines in a given message @@ -169,9 +169,7 @@ async def on_raw_message_edit( await self.response(ctx, message.content, None) - async def paste_message( - self: Self, config: munch.Munch, ctx: commands.Context, content: str - ) -> None: + async def paste_message(self: Self, ctx: commands.Context, content: str) -> None: """Moves message into a linx paste if it's too long Args: @@ -179,6 +177,7 @@ async def paste_message( ctx (commands.Context): The context where the original message was sent content (str): The string content of the flagged message """ + config = self.bot.guild_configs[str(ctx.guild.id)] log_channel = config.get("logging_channel") if not self.bot.file_config.api.api_url.linx: await self.bot.logger.send_log( @@ -192,7 +191,7 @@ async def paste_message( ) return - linx_embed = await self.create_linx_embed(config, ctx, content) + linx_embed = await self.create_linx_embed(ctx, content) if not linx_embed: await self.bot.logger.send_log( @@ -232,19 +231,19 @@ async def paste_message( await ctx.message.delete() async def create_linx_embed( - self: Self, config: munch.Munch, ctx: commands.Context, content: str + self: Self, ctx: commands.Context, content: str ) -> discord.Embed | None: """This function sends a message to the linx url and puts the result in an embed to be sent to the user Args: - config (munch.Munch): The guild config where the message was sent ctx (commands.Context): The context that generated the need for a paste content (str): The context of the message to be pasted Returns: discord.Embed | None: The formatted embed, or None if there was an API error """ + config = self.bot.guild_configs[str(ctx.guild.id)] if not content: return None From f118f5110bf533047ebbedbfb15d0964bbf1f228 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:53:49 -0700 Subject: [PATCH 18/40] Rewrite forum channel --- techsupport_bot/commands/forum.py | 206 ++++++------------ .../configuration/config.default.json | 15 +- .../configuration/config.meta.json | 55 ++++- 3 files changed, 127 insertions(+), 149 deletions(-) diff --git a/techsupport_bot/commands/forum.py b/techsupport_bot/commands/forum.py index 087fac558..57d5c956e 100644 --- a/techsupport_bot/commands/forum.py +++ b/techsupport_bot/commands/forum.py @@ -8,10 +8,10 @@ import re from typing import TYPE_CHECKING, Self +import configuration import discord -import munch import ui -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord import app_commands from discord.ext import commands @@ -25,100 +25,7 @@ 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=[""], - ) - 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="close_message", - datatype="str", - title="The message displayed on closed threads", - description="The message displayed on closed threads", - default="thread closed", - ) - config.add( - key="left_message", - datatype="str", - title="The message displayed on left threads", - description="The message displayed on left threads", - default="thread left", - ) - config.add( - key="delete_message", - datatype="str", - title="The message displayed on deleted threads", - description="The message displayed on deleted threads", - default="thread deleted", - ) - config.add( - key="abandoned_message", - datatype="str", - title="The message displayed on abandoned threads", - description="The message displayed on abandoned threads", - default="thread abandoned", - ) - config.add( - key="staff_role_ids", - datatype="list[int]", - 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( - 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) STATUS_CONFIG = { @@ -126,43 +33,43 @@ async def setup(bot: bot.TechSupportBot) -> None: "title": "Thread marked as solved", "prefix": "[SOLVED]", "color": discord.Color.green(), - "message_key": "solve_message", + "message_key": "forum_solve_message", }, "closed": { "title": "Thread marked as closed", "prefix": "[CLOSED]", "color": discord.Color.red(), - "message_key": "close_message", + "message_key": "forum_close_message", }, "left": { "title": "OP has left the server", "prefix": "[LEFT]", "color": discord.Color.red(), - "message_key": "left_message", + "message_key": "forum_left_message", }, "deleted": { "title": "Thread message was deleted", "prefix": "[DELETED]", "color": discord.Color.red(), - "message_key": "delete_message", + "message_key": "forum_delete_message", }, "rejected": { "title": "Thread rejected", "prefix": "[REJECTED]", "color": discord.Color.red(), - "message_key": "reject_message", + "message_key": "forum_reject_message", }, "duplicate": { "title": "Duplicate thread detected", "prefix": "[DUPLICATE]", "color": discord.Color.orange(), - "message_key": "duplicate_message", + "message_key": "forum_duplicate_message", }, "abandoned": { "title": "Abandoned thread archived", "prefix": "[ABANDONED]", "color": discord.Color.blurple(), - "message_key": "abandoned_message", + "message_key": "forum_abandoned_message", }, } @@ -201,9 +108,12 @@ async def mark_thread_command( status = status.lower() await interaction.response.defer(ephemeral=True) - config = self.bot.guild_configs[str(interaction.guild.id)] forum_channel = await interaction.guild.fetch_channel( - int(config.extensions.forum.forum_channel_id.value) + int( + configuration.get_config_entry( + interaction.guild.id, "forum_forum_channel_id" + ) + ) ) invalid_embed = discord.Embed( @@ -220,7 +130,7 @@ async def mark_thread_command( await interaction.followup.send(embed=invalid_embed, ephemeral=True) return - is_staff = is_thread_staff(interaction.user, interaction.guild, config) + is_staff = is_thread_staff(interaction.user, interaction.guild) is_owner = interaction.user == interaction.channel.owner # Check 2: Ensure status is valid @@ -255,7 +165,6 @@ async def mark_thread_command( await mark_thread( interaction.channel, - config, self.thread_ID_closed, status, reason, @@ -279,10 +188,7 @@ async def status_autocomplete( list[app_commands.Choice[str]]: The list of all valid choices that fit with the users current selection """ - - config = self.bot.guild_configs[str(interaction.guild.id)] - - is_staff = is_thread_staff(interaction.user, interaction.guild, config) + is_staff = is_thread_staff(interaction.user, interaction.guild) is_owner = ( hasattr(interaction.channel, "owner") and interaction.user == interaction.channel.owner @@ -327,9 +233,12 @@ async def showUnsolved(self: Self, interaction: discord.Interaction) -> None: 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) + int( + configuration.get_config_entry( + interaction.guild.id, "forum_forum_channel_id" + ) + ) ) mention_threads: list[discord.Thread] = channel.threads if len(mention_threads) == 0: @@ -375,10 +284,12 @@ async def reopen_thread(self: Self, interaction: discord.Interaction) -> None: interaction (discord.Interaction): The interaction calling the command """ await interaction.response.defer(ephemeral=True) - - config = self.bot.guild_configs[str(interaction.guild.id)] forum_channel = await interaction.guild.fetch_channel( - int(config.extensions.forum.forum_channel_id.value) + int( + configuration.get_config_entry( + interaction.guild.id, "forum_forum_channel_id" + ) + ) ) invalid_embed = discord.Embed( @@ -395,7 +306,7 @@ async def reopen_thread(self: Self, interaction: discord.Interaction) -> None: await interaction.followup.send(embed=invalid_embed, ephemeral=True) return - is_staff = is_thread_staff(interaction.user, interaction.guild, config) + is_staff = is_thread_staff(interaction.user, interaction.guild) # Check 2: Called must be staff: if not is_staff: @@ -438,9 +349,12 @@ async def on_thread_update( before (discord.Thread): The original thread after (discord.Thread): The thread after the update """ - config = self.bot.guild_configs[str(before.guild.id)] channel = await before.guild.fetch_channel( - int(config.extensions.forum.forum_channel_id.value) + int( + configuration.get_config_entry( + before.guild.id, "forum_forum_channel_id" + ) + ) ) if before.parent != channel: return @@ -476,22 +390,24 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: """ # Fuck if I know what causes this bug await asyncio.sleep(5) - config = self.bot.guild_configs[str(thread.guild.id)] channel = await thread.guild.fetch_channel( - int(config.extensions.forum.forum_channel_id.value) + int( + configuration.get_config_entry( + thread.guild.id, "forum_forum_channel_id" + ) + ) ) if thread.parent != channel: return disallowed_title_patterns = create_regex_list( - config.extensions.forum.title_regex_list.value + configuration.get_config_entry(thread.guild.id, "forum_title_regex_list") ) # Check if the thread title is disallowed if any(pattern.search(thread.name) for pattern in disallowed_title_patterns): await mark_thread( thread, - config, self.thread_ID_closed, "rejected", reason=( @@ -506,12 +422,11 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: if messages: body = messages[-1].content disallowed_body_patterns = create_regex_list( - config.extensions.forum.body_regex_list.value + configuration.get_config_entry(thread.guild.id, "forum_body_regex_list") ) if any(pattern.search(body) for pattern in disallowed_body_patterns): await mark_thread( thread, - config, self.thread_ID_closed, "rejected", reason=( @@ -525,7 +440,6 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: ): await mark_thread( thread, - config, self.thread_ID_closed, "rejected", reason=( @@ -544,7 +458,6 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: ): await mark_thread( thread, - config, self.thread_ID_closed, "duplicate", reason=( @@ -556,7 +469,9 @@ async def on_thread_create(self: Self, thread: discord.Thread) -> None: embed = discord.Embed( title="Welcome!", - description=config.extensions.forum.welcome_message.value, + description=configuration.get_config_entry( + thread.guild.id, "forum_welcome_message" + ), color=discord.Color.blue(), ) await thread.send(embed=embed) @@ -568,9 +483,12 @@ async def on_member_remove(self: Self, member: discord.Member) -> None: Args: member (discord.Member): The member who has left the server """ - config = self.bot.guild_configs[str(member.guild.id)] channel = await member.guild.fetch_channel( - int(config.extensions.forum.forum_channel_id.value) + int( + configuration.get_config_entry( + member.guild.id, "forum_forum_channel_id" + ) + ) ) for thread in channel.threads: if thread.archived: @@ -583,7 +501,6 @@ async def on_member_remove(self: Self, member: discord.Member) -> None: # Mark the thread as left await mark_thread( thread, - config, self.thread_ID_closed, "left", reason=( @@ -617,9 +534,12 @@ async def on_raw_message_delete( if not isinstance(thread, discord.Thread): return - config = self.bot.guild_configs[str(thread.guild.id)] channel = await thread.guild.fetch_channel( - int(config.extensions.forum.forum_channel_id.value) + int( + configuration.get_config_entry( + thread.guild.id, "forum_forum_channel_id" + ) + ) ) if thread.parent != channel: return @@ -635,7 +555,6 @@ async def on_raw_message_delete( # Mark the thread as deleted await mark_thread( thread, - config, self.thread_ID_closed, "deleted", reason=("It appears the original post for this thread was deleted."), @@ -647,9 +566,8 @@ async def execute(self: Self, guild: discord.Guild) -> None: Args: guild (discord.Guild): The guild where the loop is taking place """ - config = self.bot.guild_configs[str(guild.id)] channel = await guild.fetch_channel( - int(config.extensions.forum.forum_channel_id.value) + int(configuration.get_config_entry(guild.id, "forum_forum_channel_id")) ) for existing_thread in channel.threads: if not existing_thread.archived and not existing_thread.locked: @@ -663,11 +581,12 @@ async def execute(self: Self, guild: discord.Guild) -> None: datetime.datetime.now(datetime.timezone.utc) - message_timestamp ) if timestamp_delta > datetime.timedelta( - minutes=config.extensions.forum.max_age_minutes.value + minutes=configuration.get_config_entry( + guild.id, "forum_max_age_minutes" + ) ): await mark_thread( existing_thread, - config, self.thread_ID_closed, "abandoned", "Threads are automatically closed after periods of no activity", @@ -690,21 +609,18 @@ def create_regex_list(str_list: list[str]) -> list[re.Pattern[str]]: return [re.compile(p, re.IGNORECASE) for p in str_list] -def is_thread_staff( - user: discord.Member, guild: discord.Guild, config: munch.Munch -) -> bool: +def is_thread_staff(user: discord.Member, guild: discord.Guild) -> 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: + if staff_roles := configuration.get_config_entry(guild.id, "forum_staff_role_ids"): 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: @@ -714,7 +630,6 @@ def is_thread_staff( async def mark_thread( thread: discord.Thread, - config: munch.Munch, closed_list: list[int], status: str, reason: str, @@ -725,7 +640,6 @@ async def mark_thread( Args: thread (discord.Thread): The thread to modify - config (munch.Munch): The guild config closed_list (list[int]): The list of threads closed by TS status (str): The status to modify the thread with reason (str): The reason the thread was changed @@ -737,7 +651,9 @@ async def mark_thread( embed = discord.Embed( title=data["title"], - description=getattr(config.extensions.forum, data["message_key"]).value, + description=configuration.get_config_entry( + thread.guild.id, data["message_key"] + ), color=data["color"], ) # If there is a reason, add the reason to the embed diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index c7e621468..ebc43043c 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -29,5 +29,18 @@ "core_logging_channel": "", "modmail_modmail_roles": [], "duck_hunt_channels": [], - "duck_success_rate": 50 + "duck_success_rate": 50, + "forum_abandoned_message": "thread abandoned", + "forum_body_regex_list": [], + "forum_close_message": "thread closed", + "forum_delete_message": "thread deleted", + "forum_duplicate_message": "thread duplicated", + "forum_forum_channel_id": "", + "forum_left_message": "thread left", + "forum_max_age_minutes": 1440, + "forum_reject_message": "thread rejected", + "forum_solve_message": "thread solved", + "forum_staff_role_ids": [], + "forum_title_regex_list": [], + "forum_welcome_message": "thread welcome" } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index 96ba09318..8883fc642 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -122,10 +122,59 @@ "duck_success_rate": { "datatype": "int", "description": "The success rate of bef/bang messages" + }, + "forum_abandoned_message": { + "datatype": "str", + "description": "The message displayed on abandoned threads" + }, + "forum_body_regex_list": { + "datatype": "list[str]", + "description": "List of regex to ban in bodies" + }, + "forum_close_message": { + "datatype": "str", + "description": "The message displayed on closed threads" + }, + "forum_delete_message": { + "datatype": "str", + "description": "The message displayed on deleted threads" + }, + "forum_duplicate_message": { + "datatype": "str", + "description": "The message displayed on duplicated threads" + }, + "forum_forum_channel_id": { + "datatype": "discord.ForumChannel", + "description": "The forum channel to manage threads in" + }, + "forum_left_message": { + "datatype": "str", + "description": "The message displayed on left threads" + }, + "forum_max_age_minutes": { + "datatype": "int", + "description": "The max age of a thread before it times out" + }, + "forum_reject_message": { + "datatype": "str", + "description": "The message displayed on rejected threads" + }, + "forum_solve_message": { + "datatype": "str", + "description": "The message displayed on solved threads" + }, + "forum_staff_role_ids": { + "datatype": "list[discord.Role]", + "description": "Staff roles able to mark threads solved/abandoned/rejected" + }, + "forum_title_regex_list": { + "datatype": "list[str]", + "description": "List of regex to ban in titles" + }, + "forum_welcome_message": { + "datatype": "str", + "description": "The message displayed on new threads" } - - - } From 6e66249b76d9ee46500467e9d527e9e876ffca26 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:13:01 -0700 Subject: [PATCH 19/40] More config work --- techsupport_bot/bot.py | 8 +- techsupport_bot/commands/chatgpt.py | 5 +- techsupport_bot/commands/dumpdbg.py | 4 +- techsupport_bot/commands/echo.py | 4 +- techsupport_bot/commands/factoids.py | 60 +++++++++---- techsupport_bot/commands/gate.py | 5 +- techsupport_bot/commands/moderator.py | 23 +++-- techsupport_bot/commands/news.py | 7 +- techsupport_bot/commands/notes.py | 5 +- techsupport_bot/commands/relay.py | 4 +- techsupport_bot/commands/voting.py | 49 ++++++----- .../configuration/config.default.json | 15 +++- .../configuration/config.meta.json | 52 ++++++++++++ techsupport_bot/functions/automod.py | 85 ++++++++++--------- techsupport_bot/functions/events.py | 5 +- techsupport_bot/functions/logger.py | 35 ++++---- techsupport_bot/functions/nickname.py | 8 +- techsupport_bot/functions/paste.py | 7 +- 18 files changed, 254 insertions(+), 127 deletions(-) diff --git a/techsupport_bot/bot.py b/techsupport_bot/bot.py index d876887b9..6cb7ce77f 100644 --- a/techsupport_bot/bot.py +++ b/techsupport_bot/bot.py @@ -555,9 +555,7 @@ async def handle_error( error_message = message_template.get_message(exception) - log_channel = await self.get_log_channel_from_guild( - guild=guild, key="logging_channel" - ) + log_channel = configuration.get_config_entry(guild.id, "core_logging_channel") # Ensure that error messages aren't too long. # This ONLY changes the user facing error, the stack trace isn't impacted @@ -1006,8 +1004,8 @@ async def slash_command_log(self: Self, interaction: discord.Interaction) -> Non for parameter in interaction.namespace: parameters.append(f"{parameter[0]}: {parameter[1]}") - log_channel = await self.get_log_channel_from_guild( - interaction.guild, key="logging_channel" + log_channel = configuration.get_config_entry( + interaction.guild.id, "core_logging_channel" ) sliced_content = interaction.command.qualified_name[:100] diff --git a/techsupport_bot/commands/chatgpt.py b/techsupport_bot/commands/chatgpt.py index 1784956f3..ad6b485c0 100644 --- a/techsupport_bot/commands/chatgpt.py +++ b/techsupport_bot/commands/chatgpt.py @@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Self +import configuration import discord import expiringdict import ui @@ -138,7 +139,9 @@ async def gpt(self: Self, ctx: commands.Context, *, prompt: str) -> None: if not choices: # Tries to figure out what error happened if error := response.get("error", []): - channel = config.get("logging_channel") + channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=( "OpenAI API responded with an error! Contents:" diff --git a/techsupport_bot/commands/dumpdbg.py b/techsupport_bot/commands/dumpdbg.py index 0a1dd4c18..931541946 100644 --- a/techsupport_bot/commands/dumpdbg.py +++ b/techsupport_bot/commands/dumpdbg.py @@ -130,7 +130,9 @@ async def debug_dump(self: Self, ctx: commands.Context) -> None: + f"Api response: `{response['error']}`", channel=ctx.channel, ) - channel = config.get("logging_channel") + channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=( f"Dumpdbg API responded with the error `{response['error']}`" diff --git a/techsupport_bot/commands/echo.py b/techsupport_bot/commands/echo.py index 83284354a..b046dd4a8 100644 --- a/techsupport_bot/commands/echo.py +++ b/techsupport_bot/commands/echo.py @@ -84,9 +84,7 @@ async def echo_channel( ): return - target_logging_channel = await function_logger.pre_log_checks( - self.bot, config, channel - ) + target_logging_channel = await function_logger.pre_log_checks(self.bot, channel) if not target_logging_channel: return diff --git a/techsupport_bot/commands/factoids.py b/techsupport_bot/commands/factoids.py index 6748c2c42..aa80641d9 100644 --- a/techsupport_bot/commands/factoids.py +++ b/techsupport_bot/commands/factoids.py @@ -845,7 +845,9 @@ async def response( try: embed = self.get_embed_from_factoid(factoid) except TypeError as exception: - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=f"Unable to make embed for factoid `{factoid.name}`, sending fallback.", level=LogLevel.ERROR, @@ -880,7 +882,9 @@ async def response( ) # log it in the logging channel with type info and generic content config = self.bot.guild_configs[str(ctx.guild.id)] - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=( f"Sending factoid: {query} (triggered by {ctx.author} in" @@ -893,7 +897,9 @@ async def response( # If something breaks, also log it except discord.errors.HTTPException as exception: config = self.bot.guild_configs[str(ctx.guild.id)] - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message="Could not send factoid", level=LogLevel.ERROR, @@ -960,9 +966,7 @@ async def send_to_logger( ): return - target_logging_channel = await function_logger.pre_log_checks( - self.bot, config, channel - ) + target_logging_channel = await function_logger.pre_log_checks(self.bot, channel) if not target_logging_channel: return @@ -1039,7 +1043,9 @@ async def factoid_call_command( try: embed = self.get_embed_from_factoid(factoid) except TypeError as exception: - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + interaction.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=f"Unable to make embed for factoid `{factoid.name}`, sending fallback.", level=LogLevel.ERROR, @@ -1071,7 +1077,9 @@ async def factoid_call_command( # define the message and send it await interaction.response.send_message(content=content, embed=embed) # log it in the logging channel with type info and generic content - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + interaction.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=( f"Sending factoid: {query} (triggered by {interaction.user} in" @@ -1085,7 +1093,9 @@ async def factoid_call_command( ) # If something breaks, also log it except discord.errors.HTTPException as exception: - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + interaction.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message="Could not send factoid", level=LogLevel.ERROR, @@ -1144,7 +1154,9 @@ async def cronjob( if ctx: config = self.bot.guild_configs[str(ctx.guild.id)] - channel = config.get("logging_channel") + channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) log_context = LogContext(guild=ctx.guild, channel=ctx.channel) await self.bot.logger.send_log( @@ -1170,7 +1182,9 @@ async def cronjob( if ctx: config = self.bot.guild_configs[str(ctx.guild.id)] - channel = config.get("logging_channel") + channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) log_context = LogContext(guild=ctx.guild, channel=ctx.channel) await self.bot.logger.send_log( @@ -1192,7 +1206,9 @@ async def cronjob( if ctx: config = self.bot.guild_configs[str(ctx.guild.id)] - channel = config.get("logging_channel") + channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) log_context = LogContext(guild=ctx.guild, channel=ctx.channel) await self.bot.logger.send_log( @@ -1213,7 +1229,9 @@ async def cronjob( if ctx: config = self.bot.guild_configs[str(ctx.guild.id)] - channel = config.get("logging_channel") + channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) log_context = LogContext(guild=ctx.guild, channel=ctx.channel) await self.bot.logger.send_log( @@ -1247,7 +1265,9 @@ async def cronjob( try: embed = self.get_embed_from_factoid(factoid) except TypeError as exception: - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=( f"Unable to make embed for factoid `{factoid.name}`, sending fallback." @@ -1270,7 +1290,9 @@ async def cronjob( except discord.errors.HTTPException as exception: config = self.bot.guild_configs[str(ctx.guild.id)] - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message="Could not send looped factoid", level=LogLevel.ERROR, @@ -1954,7 +1976,9 @@ async def build_factoid_all( # If an error happened while calling the api except (gaierror, InvalidURL) as exception: config = self.bot.guild_configs[str(guild.id)] - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message="Could not render/send all-factoid HTML", level=LogLevel.ERROR, @@ -2477,7 +2501,9 @@ async def dealias( # Logs the new parent change config = self.bot.guild_configs[str(ctx.guild.id)] - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=( f"Factoid dealias: Deleted the alias `{factoid_name}`, new" diff --git a/techsupport_bot/commands/gate.py b/techsupport_bot/commands/gate.py index f96d9aae4..39b3e1d2d 100644 --- a/techsupport_bot/commands/gate.py +++ b/techsupport_bot/commands/gate.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Self +import configuration import discord import munch from botlogging import LogContext, LogLevel @@ -117,7 +118,9 @@ async def response( roles = await self.get_roles(ctx) if not roles: config = self.bot.guild_configs[str(ctx.guild.id)] - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=( "No roles to give user in gate plugin channel - ignoring" diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 7a467b750..6b17e2fe1 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING, Self +import configuration import dateparser import discord import ui @@ -106,7 +107,9 @@ async def handle_ban_user( if not delete_days: config = self.bot.guild_configs[str(interaction.guild.id)] - delete_days = config.extensions.moderator.ban_delete_duration.value + delete_days = configuration.get_config_entry( + interaction.guild.id, "moderator_ban_delete_duration" + ) # Ban the user using the core moderation cog result = await moderation.ban_user( @@ -484,12 +487,15 @@ async def handle_warn_user( ) should_ban = False - if new_count_of_warnings >= config.moderation.max_warnings: + max_warnings = configuration.get_config_entry( + interaction.guild.id, "moderation_max_warnings" + ) + if new_count_of_warnings >= max_warnings: await interaction.response.defer(ephemeral=False) view = ui.Confirm() await view.send( message="This user has exceeded the max warnings of " - + f"{config.moderation.max_warnings}. Would " + + f"{max_warnings}. Would " + "you like to ban them instead?", channel=interaction.channel, author=interaction.user, @@ -508,11 +514,14 @@ async def handle_warn_user( guild=interaction.guild, user=target, delete_seconds=( - config.extensions.moderator.ban_delete_duration.value * 86400 + configuration.get_config_entry( + interaction.guild.id, "moderator_ban_delete_duration" + ) + * 86400 ), reason=( f"Over max warning count {new_count_of_warnings} out of" - f" {config.moderation.max_warnings} (final warning:" + f" {max_warnings} (final warning:" f" {reason}) - banned by {interaction.user}" ), ) @@ -560,7 +569,9 @@ async def handle_warn_user( try: await target.send(embed=embed) except (discord.HTTPException, discord.Forbidden): - channel = config.get("logging_channel") + channel = configuration.get_config_entry( + interaction.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=f"Failed to DM warning to {target}", level=LogLevel.WARNING, diff --git a/techsupport_bot/commands/news.py b/techsupport_bot/commands/news.py index 48ee56a2a..d71cb073c 100644 --- a/techsupport_bot/commands/news.py +++ b/techsupport_bot/commands/news.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Self import aiocron +import configuration import discord import munch from botlogging import LogContext, LogLevel @@ -195,7 +196,7 @@ async def execute(self: Self, guild: discord.Guild) -> None: if article is None: return - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry(guild.id, "core_logging_channel") await self.bot.logger.send_log( message=f"Sending news headline to #{channel.name}", level=LogLevel.INFO, @@ -255,7 +256,9 @@ async def news_command( await interaction.response.send_message(content=url) # Log the command execution - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + interaction.guild.id, "core_logging_channel" + ) if log_channel: await self.bot.logger.send_log( message=( diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 7a3b3ce1b..0588f4be3 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Self +import configuration import discord import ui from botlogging import LogContext, LogLevel @@ -326,7 +327,9 @@ async def on_member_join(self: Self, member: discord.Member) -> None: await member.add_roles(role, reason="Noted user has joined the guild") - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + member.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=f"Found noted user with ID {member.id} joining - re-adding role", level=LogLevel.INFO, diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index d715df53e..012239fbb 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -414,7 +414,7 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No discord_channel.guild.id, "core_enabled_extensions" ): automod_actions = automod.run_only_string_checks( - config, split_message["content"] + discord_channel.guild, split_message["content"] ) automod_final = automod.process_automod_violations(automod_actions) if automod_final and automod_final.delete_message: @@ -458,7 +458,7 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No ): return target_logging_channel = await function_logger.pre_log_checks( - self.bot, config, discord_channel + self.bot, discord_channel ) if not target_logging_channel: return diff --git a/techsupport_bot/commands/voting.py b/techsupport_bot/commands/voting.py index a1b191c82..bd6d92c7a 100644 --- a/techsupport_bot/commands/voting.py +++ b/techsupport_bot/commands/voting.py @@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Self import aiocron +import configuration import discord import munch import ui @@ -124,7 +125,6 @@ async def votingbutton( This also hides who voted for what forever, and triggers it to be deleted from the database upon completion of the vote """ - config = self.bot.guild_configs[str(interaction.guild.id)] channel = await interaction.guild.fetch_channel(int(channel)) if not self.user_can_use_vote_channel( @@ -145,8 +145,8 @@ async def votingbutton( roles = await interaction.guild.fetch_roles() # Get the allowed role IDs for this channel from the config - channel_role_map: dict[str, list[str]] = ( - config.extensions.voting.votes_channel_roles.value + channel_role_map: dict[str, list[str]] = configuration.get_config_entry( + interaction.guild.id, "voting_votes_channel_roles" ) allowed_role_ids = channel_role_map.get(str(channel.id), []) @@ -208,15 +208,13 @@ async def vote_channel_autocomplete( Returns: list[app_commands.Choice[str]]: The list of channels that match the current string """ - config = self.bot.guild_configs.get(str(interaction.guild.id)) - if not config: - return [] - member = interaction.user if not isinstance(member, discord.Member): return [] - channel_role_map = config.extensions.voting.votes_channel_roles.value + channel_role_map = configuration.get_config_entry( + interaction.guild.id, "voting_votes_channel_roles" + ) choices: list[app_commands.Choice[str]] = [] @@ -258,14 +256,15 @@ def user_can_use_vote_channel( Returns: bool: True if the channel is valid, false if its not """ - config = self.bot.guild_configs[str(member.guild.id)] if not isinstance(channel, discord.ForumChannel): return False - voting_config = config.extensions.voting - - active_role_id: str = voting_config.active_role_id.value - channel_role_map: dict[str, list[str]] = voting_config.votes_channel_roles.value + active_role_id: str = configuration.get_config_entry( + member.guild.id, "voting_active_role_id" + ) + channel_role_map: dict[str, list[str]] = configuration.get_config_entry( + member.guild.id, "voting_votes_channel_roles" + ) # Channel must be configured allowed_role_ids = channel_role_map.get(str(channel.id)) @@ -324,11 +323,12 @@ async def calculate_eligible_voters( Returns: list[discord.Member]: The list of eligible voters """ - config = self.bot.guild_configs[str(guild.id)] - voting_config = config.extensions.voting - - channel_role_map: dict[str, list[str]] = voting_config.votes_channel_roles.value - active_role_id: str = voting_config.active_role_id.value + channel_role_map: dict[str, list[str]] = configuration.get_config_entry( + guild.id, "voting_votes_channel_roles" + ) + active_role_id: str = configuration.get_config_entry( + guild.id, "voting_active_role_id" + ) active_role = guild.get_role(int(active_role_id)) if not active_role: @@ -642,7 +642,6 @@ async def execute(self: Self, guild: discord.Guild) -> None: Args: guild (discord.Guild): The guild the vote is being run in """ - config = self.bot.guild_configs[str(guild.id)] # pylint: disable=C0121 active_votes = ( await self.bot.models.Votes.query.where( @@ -651,7 +650,7 @@ async def execute(self: Self, guild: discord.Guild) -> None: .where(self.bot.models.Votes.guild_id == str(guild.id)) .gino.all() ) - reminder_times = config.extensions.voting.reminders_at.value + reminder_times = configuration.get_config_entry(guild.id, "voting_reminders_at") timestamp_now = int(datetime.datetime.utcnow().timestamp()) @@ -693,7 +692,6 @@ async def remind_vote( guild (discord.Guild): The guild the vote is in reminder_hour (int): The hours remining until the vote closes """ - config = self.bot.guild_configs[str(guild.id)] # Get all eligible voters eligible_voters = [v for v in vote.vote_ids_eligible.split(",") if v] # Get all voted voters @@ -725,10 +723,9 @@ async def end_vote(self: Self, vote: munch.Munch, guild: discord.Guild) -> None: vote (munch.Munch): The vote database object that needs to be ended guild (discord.Guild): The guild that vote belongs to """ - config = self.bot.guild_configs[str(guild.id)] await vote.update(vote_active=False).apply() embed = await self.build_vote_embed(vote.vote_id, guild) - pass_embed = self.build_vote_pass_embed(vote, config) + pass_embed = self.build_vote_pass_embed(vote, guild) # If the vote is anonymous, at this point we need to clear the vote record forever if vote.anonymous: await vote.update( @@ -745,7 +742,7 @@ async def end_vote(self: Self, vote: munch.Munch, guild: discord.Guild) -> None: ) def build_vote_pass_embed( - self: Self, vote: munch.Munch, config: munch.Munch + self: Self, vote: munch.Munch, guild: discord.Guild ) -> discord.Embed: """This builds an embed that shows if the vote passed or failed, based on configurable thresholds @@ -765,7 +762,9 @@ def build_vote_pass_embed( no_voters = vote.votes_no abstain_voters = vote.votes_abstain - thresholds = config.extensions.voting.voting_thresholds.value + thresholds = configuration.get_config_entry( + guild.id, "voting_voting_thresholds" + ) # Percentages percent_eligible_yes = (yes_voters / eligible_voters) * 100 diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index ebc43043c..4b7bedbc6 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -42,5 +42,18 @@ "forum_solve_message": "thread solved", "forum_staff_role_ids": [], "forum_title_regex_list": [], - "forum_welcome_message": "thread welcome" + "forum_welcome_message": "thread welcome", + "voting_active_role_id": "", + "voting_reminders_at": [36, 6], + "voting_votes_channel_roles": {}, + "voting_voting_thresholds": [50, 67, 75], + "automod_banned_file_extensions": ["exe"], + "automod_banned_file_hashes": [], + "automod_bypass_roles": [], + "automod_channels": [], + "automod_max_mentions": 3, + "automod_string_map": {}, + "moderation_max_warnings": 3, + "moderator_ban_delete_duration": 7, + "logger_channel_map": {} } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index 8883fc642..4533aef97 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -174,6 +174,58 @@ "forum_welcome_message": { "datatype": "str", "description": "The message displayed on new threads" + }, + "voting_active_role_id": { + "datatype": "discord.Role", + "description": "User must have this role to start or participate in votes" + }, + "voting_reminders_at": { + "datatype": "list[int]", + "description": "The list of hours remaining in vote to remind non voters" + }, + "voting_votes_channel_roles": { + "datatype": "dict[discord.ForumChannel, list[discord.Role]]", + "description": "Map of forum channel IDs to a list of role IDs. User must have at least one role from the list." + }, + "voting_voting_thresholds": { + "datatype": "list[int]", + "description": "1, % of eligible voters who must vote yes, 2, % of yes/no voters who must have voted yes, 3, % of eligible voters who must have voted anything at all" + }, + "automod_banned_file_extensions": { + "datatype": "list[str]", + "description": "A list of all file extensions to be blocked and have a auto warning issued" + }, + "automod_banned_file_hashes": { + "datatype": "list[str]", + "description": "A list of all file hashes to be blocked and have a auto warning issued" + }, + "automod_bypass_roles": { + "datatype": "list[discord.Role]", + "description": "The list of role names associated with bypassed roles by the auto-protect" + }, + "automod_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The list of channels to enable automod in" + }, + "automod_max_mentions": { + "datatype": "int", + "description": "Max number of mentions allowed in a message before triggering auto-protect" + }, + "automod_string_map": { + "datatype": "dict[str, dict[str, Any]]", + "description": "Mapping of keyword strings to data defining the action taken by auto-protect" + }, + "moderation_max_warnings": { + "datatype": "int", + "description": "The maximum number of warnings before /warn prompts to ban, and automod autobans" + }, + "moderator_ban_delete_duration": { + "datatype": "int", + "description": "The default amount of days to delete messages for a user after they are banned" + }, + "logger_channel_map": { + "datatype": "dict[discord.TextChannel, discord.TextChannel]", + "description": "Input Channel to Logging Channel mapping" } } diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 29efc4218..bcc5fba39 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -158,8 +158,9 @@ async def match(self: Self, ctx: commands.Context, content: str) -> bool: Returns: bool: Whether the message should be inspected for automod violations """ - config = self.bot.guild_configs[str(ctx.guild.id)] - if not str(ctx.channel.id) in config.extensions.automod.channels.value: + if not str(ctx.channel.id) in configuration.get_config_entry( + ctx.guild.id, "automod_channels" + ): await self.bot.logger.send_log( message="Channel not in automod channels - ignoring automod check", level=LogLevel.DEBUG, @@ -171,7 +172,9 @@ async def match(self: Self, ctx: commands.Context, content: str) -> bool: if any( role_name.lower() in role_names - for role_name in config.extensions.automod.bypass_roles.value + for role_name in configuration.get_config_entry( + ctx.guild.id, "automod_bypass_roles" + ) ): return False @@ -190,13 +193,11 @@ async def response( content (str): The string content of the message result (bool): What the match() function returned """ - config = self.bot.guild_configs[str(ctx.guild.id)] - # If user outranks bot, do nothing if ctx.message.author.top_role >= ctx.channel.guild.me.top_role: return - all_punishments = await run_all_checks(config, ctx.message) + all_punishments = await run_all_checks(ctx.message) if len(all_punishments) == 0: return @@ -227,13 +228,16 @@ async def response( ctx.channel.guild.me, total_punishment.violation_string, ) - if count_of_warnings >= config.moderation.max_warnings: + max_warnings = configuration.get_config_entry( + ctx.guild.id, "moderation_max_warnings" + ) + if count_of_warnings >= max_warnings: ban_embed = moderator.generate_response_embed( ctx.author, "ban", reason=( f"Over max warning count {count_of_warnings} out of" - f" {config.moderation.max_warnings} (final warning:" + f" {max_warnings} (final warning:" f" {total_punishment.violation_string}) - banned by automod" ), ) @@ -252,7 +256,10 @@ async def response( ctx.guild, ctx.author, delete_seconds=( - config.extensions.moderator.ban_delete_duration.value * 86400 + configuration.get_config_entry( + ctx.guild.id, "moderator_ban_delete_duration" + ) + * 86400 ), reason=total_punishment.violation_string, ) @@ -286,9 +293,6 @@ async def response( alert_channel_embed = generate_automod_alert_embed( ctx, total_punishment.total_punishments, total_punishment.action_string ) - - config = self.bot.guild_configs[str(ctx.guild.id)] - try: alert_channel = ctx.guild.get_channel( int( @@ -449,9 +453,7 @@ def generate_automod_alert_embed( # All checks will return a list of AutoModPunishment, which may be nothing -async def run_all_checks( - config: munch.Munch, message: discord.Message -) -> list[AutoModPunishment]: +async def run_all_checks(message: discord.Message) -> list[AutoModPunishment]: """This runs all 4 checks on a given discord.Message handle_file_extensions handle_mentions @@ -465,17 +467,19 @@ async def run_all_checks( Returns: list[AutoModPunishment]: The automod violations that the given message violated """ + guild = message.guild all_violations = ( - run_only_string_checks(config, message.clean_content) - + handle_file_extensions(config, message.attachments) - + handle_mentions(config, message) - + await handle_file_hashes(config, message.attachments) + run_only_string_checks(guild, message.clean_content) + + handle_file_extensions(guild, message.attachments) + + handle_mentions(guild, message) + + await handle_file_hashes(guild, message.attachments) ) return all_violations def run_only_string_checks( - config: munch.Munch, content: str + guild: discord.Guild, + content: str, ) -> list[AutoModPunishment]: """This runs the plaintext string texts and returns the combined list of violations handle_exact_string @@ -488,19 +492,19 @@ def run_only_string_checks( Returns: list[AutoModPunishment]: The automod violations that the given message violated """ - all_violations = handle_exact_string(config, content) + handle_regex_string( - config, content + all_violations = handle_exact_string(guild, content) + handle_regex_string( + guild, content ) return all_violations def handle_file_extensions( - config: munch.Munch, attachments: list[discord.Attachment] + guild: discord.Guild, attachments: list[discord.Attachment] ) -> list[AutoModPunishment]: """This checks a list of attachments for attachments that violate the automod rules Args: - config (munch.Munch): The guild config to check with + guild (discord.Guild): The guild to check with attachments (list[discord.Attachment]): The list of attachments to search Returns: @@ -508,9 +512,8 @@ def handle_file_extensions( """ violations = [] for attachment in attachments: - if ( - attachment.filename.split(".")[-1] - in config.extensions.automod.banned_file_extensions.value + if attachment.filename.split(".")[-1] in configuration.get_config_entry( + guild.id, "automod_banned_file_extensions" ): violations.append( AutoModPunishment( @@ -524,12 +527,12 @@ def handle_file_extensions( async def handle_file_hashes( - config: munch.Munch, attachments: list[discord.Attachment] + guild: discord.Guild, attachments: list[discord.Attachment] ) -> list[AutoModPunishment]: """This checks a list of attachments for attachments that match the configured list of hashes Args: - config (munch.Munch): The guild config to check with + guild (discord.Guild): The guild to check with attachments (list[discord.Attachment]): The list of attachments to search Returns: @@ -539,7 +542,9 @@ async def handle_file_hashes( for attachment in attachments: file_hash = await auxiliary.get_attachment_hash(attachment) - if file_hash in config.extensions.automod.banned_file_hashes.value: + if file_hash in configuration.get_config_entry( + guild.id, "automod_banned_file_hashes" + ): violations.append( AutoModPunishment( f"{attachment.filename} matches a banned file hash", @@ -553,18 +558,20 @@ async def handle_file_hashes( def handle_mentions( - config: munch.Munch, message: discord.Message + guild: discord.Guild, message: discord.Message ) -> list[AutoModPunishment]: """This checks a given discord message to make sure it doesn't violate the mentions maximum Args: - config (munch.Munch): The guild config to check with + guild (discord.Guild): The guild to check with message (discord.Message): The message to check for mentions with Returns: list[AutoModPunishment]: The automod violations that the given message violated """ - if len(message.mentions) > config.extensions.automod.max_mentions.value: + if len(message.mentions) > configuration.get_config_entry( + guild.id, "automod_max_mentions" + ): return [ AutoModPunishment( "Mass Mentions", @@ -576,12 +583,12 @@ def handle_mentions( return [] -def handle_exact_string(config: munch.Munch, content: str) -> list[AutoModPunishment]: +def handle_exact_string(guild: discord.Guild, content: str) -> list[AutoModPunishment]: """This checks the configued automod exact string blocks If the content matches the string, it's added to a list Args: - config (munch.Munch): The guild config to check with + guild (discord.Guild): The guild to check with content (str): The content of the message to search Returns: @@ -591,7 +598,7 @@ def handle_exact_string(config: munch.Munch, content: str) -> list[AutoModPunish for ( keyword, filter_config, - ) in config.extensions.automod.string_map.value.items(): + ) in configuration.get_config_entry(guild.id, "automod_string_map").items(): if keyword.lower() in content.lower(): violations.append( AutoModPunishment( @@ -605,12 +612,12 @@ def handle_exact_string(config: munch.Munch, content: str) -> list[AutoModPunish return violations -def handle_regex_string(config: munch.Munch, content: str) -> list[AutoModPunishment]: +def handle_regex_string(guild: discord.Guild, content: str) -> list[AutoModPunishment]: """This checks the configued automod regex blocks If the content matches the regex, it's added to a list Args: - config (munch.Munch): The guild config to check with + guild (discord.Guild): The guild to check with content (str): The content of the message to search Returns: @@ -620,7 +627,7 @@ def handle_regex_string(config: munch.Munch, content: str) -> list[AutoModPunish for ( _, filter_config, - ) in config.extensions.automod.string_map.value.items(): + ) in configuration.get_config_entry(guild.id, "automod_string_map").items(): regex = filter_config.get("regex") if regex: try: diff --git a/techsupport_bot/functions/events.py b/techsupport_bot/functions/events.py index 459c7f36e..c53c7f2c0 100644 --- a/techsupport_bot/functions/events.py +++ b/techsupport_bot/functions/events.py @@ -7,6 +7,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Self +import configuration import discord from botlogging import LogContext, LogLevel from core import auxiliary, cogs @@ -811,8 +812,8 @@ async def on_command(self: Self, ctx: commands.Context) -> None: embed.add_field(name="Channel", value=getattr(ctx.channel, "name", "DM")) embed.add_field(name="Server", value=getattr(ctx.guild, "name", "None")) - log_channel = await self.bot.get_log_channel_from_guild( - ctx.guild, key="logging_channel" + log_channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" ) sliced_content = ctx.message.content[:100] diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index d9c0988a3..4a7ac53f4 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -5,6 +5,7 @@ import datetime from typing import TYPE_CHECKING, Self +import configuration import discord import munch from botlogging import LogContext, LogLevel @@ -50,7 +51,7 @@ def get_channel_id(channel: discord.abc.GuildChannel | discord.Thread) -> int: def get_mapped_channel_object( - config: munch.Munch, src_channel: int + guild: discord.Guild, src_channel: int ) -> discord.TextChannel: """Gets the destination channel object from the integer ID of the source channel Will return none if the channel doesn't exist in the config @@ -63,7 +64,7 @@ def get_mapped_channel_object( discord.TextChannel: The logging channel object """ # Get the ID of the channel, or parent channel in the case of threads - mapped_id = config.extensions.logger.channel_map.value.get( + mapped_id = configuration.get_config_entry(guild.id, "logger_channel_map").get( str(get_channel_id(src_channel)) ) if not mapped_id: @@ -79,7 +80,6 @@ def get_mapped_channel_object( async def pre_log_checks( bot: bot.TechSupportBot, - config: munch.Munch, src_channel: discord.abc.GuildChannel | discord.Thread, ) -> discord.TextChannel: """This does checks that are needed to pre log. @@ -96,17 +96,20 @@ async def pre_log_checks( """ channel_id = get_channel_id(src_channel) - if not str(channel_id) in config.extensions.logger.channel_map.value: + if not str(channel_id) in configuration.get_config_entry( + src_channel.guild.id, "logger_channel_map" + ): return None - target_logging_channel = get_mapped_channel_object(config, src_channel) + target_logging_channel = get_mapped_channel_object(src_channel.guild, src_channel) if not target_logging_channel: return None # Don't log stuff cross-guild if target_logging_channel.guild.id != src_channel.guild.id: - config = bot.guild_configs[str(src_channel.guild.id)] - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + src_channel.guild.id, "logger_channel_map" + ) await bot.logger.send_log( message="Configured channel not in associated guild - aborting log", level=LogLevel.WARNING, @@ -130,9 +133,10 @@ async def match(self: Self, ctx: commands.Context, _: str) -> bool: Returns: bool: Whether the message should be logged or not """ - config = self.bot.guild_configs[str(ctx.guild.id)] channel_id = get_channel_id(ctx.channel) - if not str(channel_id) in config.extensions.logger.channel_map.value: + if not str(channel_id) in configuration.get_config_entry( + ctx.guild.id, "logger_channel_map" + ): return False return True @@ -143,8 +147,7 @@ async def response(self: Self, ctx: commands.Context, _: str, __: bool) -> None: Args: ctx (commands.Context): The context that was generated when the message was sent """ - config = self.bot.guild_configs[str(ctx.guild.id)] - target_logging_channel = await pre_log_checks(self.bot, config, ctx.channel) + target_logging_channel = await pre_log_checks(self.bot, ctx.channel) await send_message( self.bot, @@ -180,10 +183,8 @@ async def send_message( special_flags (list[str], optional): If supplied, a new field on the embed will be added that shows this. Defaults to []. """ - config = bot.guild_configs[str(message.guild.id)] - # Ensure we have attachments re-uploaded - attachments = await build_attachments(bot, config, message) + attachments = await build_attachments(bot, message) # Add avatar to attachments to all it to be added to the embed try: @@ -316,7 +317,7 @@ def generate_role_list(author: discord.Member) -> list[str]: async def build_attachments( - bot: bot.TechSupportBot, config: munch.Munch, message: discord.Message + bot: bot.TechSupportBot, message: discord.Message ) -> list[discord.File]: """Reuploads and builds a list of attachments to send along side the embed @@ -337,7 +338,9 @@ async def build_attachments( ) <= message.guild.filesize_limit: attachments.append(await attch.to_file()) if (lf := len(message.attachments) - len(attachments)) != 0: - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + message.guild.id, "logger_channel_map" + ) await bot.logger.send_log( message=( f"Logger did not reupload {lf} file(s) due to file size limit" diff --git a/techsupport_bot/functions/nickname.py b/techsupport_bot/functions/nickname.py index b4699da58..d33bc321d 100644 --- a/techsupport_bot/functions/nickname.py +++ b/techsupport_bot/functions/nickname.py @@ -128,7 +128,9 @@ async def response( f" ping your name. Your new nickname is {modified_name}." ) except discord.Forbidden: - channel = config.get("logging_channel") + channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=f"Could not DM {ctx.author.name} about nickname changes", level=LogLevel.WARNING, @@ -164,7 +166,9 @@ async def on_member_join(self: Self, member: discord.Member) -> None: f" ping your name. Your new nickname is {modified_name}." ) except discord.Forbidden: - channel = config.get("logging_channel") + channel = configuration.get_config_entry( + member.guild.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=f"Could not DM {member.name} about nickname changes", level=LogLevel.WARNING, diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index c4dcfa158..a3b970606 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -116,7 +116,7 @@ async def response( if "automod" in configuration.get_config_entry( ctx.guild.id, "core_enabled_extensions" ): - automod_actions = await automod.run_all_checks(config, ctx.message) + automod_actions = await automod.run_all_checks(ctx.message) automod_final = automod.process_automod_violations(automod_actions) if automod_final and automod_final.delete_message: return @@ -177,8 +177,9 @@ async def paste_message(self: Self, ctx: commands.Context, content: str) -> None ctx (commands.Context): The context where the original message was sent content (str): The string content of the flagged message """ - config = self.bot.guild_configs[str(ctx.guild.id)] - log_channel = config.get("logging_channel") + log_channel = configuration.get_config_entry( + ctx.guild.id, "core_logging_channel" + ) if not self.bot.file_config.api.api_url.linx: await self.bot.logger.send_log( message=( From 8b5e18082498f16ac13a9ebfc7d129e1e5e4f049 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:01:38 -0700 Subject: [PATCH 20/40] Process gate, joke, grab, honeypot and hangman --- techsupport_bot/commands/gate.py | 90 +++++-------------- techsupport_bot/commands/grab.py | 35 ++------ techsupport_bot/commands/hangman.py | 16 +--- techsupport_bot/commands/joke.py | 8 +- .../configuration/config.default.json | 15 +++- .../configuration/config.meta.json | 52 +++++++++++ techsupport_bot/functions/honeypot.py | 13 +-- 7 files changed, 111 insertions(+), 118 deletions(-) diff --git a/techsupport_bot/commands/gate.py b/techsupport_bot/commands/gate.py index 39b3e1d2d..535366315 100644 --- a/techsupport_bot/commands/gate.py +++ b/techsupport_bot/commands/gate.py @@ -6,9 +6,8 @@ import configuration import discord -import munch from botlogging import LogContext, LogLevel -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: @@ -21,60 +20,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="channel", - datatype="int", - title="Server Gate Channel ID", - description="The ID of the channel the gate is in", - default=None, - ) - config.add( - key="roles", - datatype="list", - title="Roles to add", - description="The list of roles to add after user is verified", - default=[], - ) - config.add( - key="intro_message", - datatype="str", - title="Server Gate intro message", - description="The message that's sent when running the intro message command", - default=( - "Welcome to our server! 👋 Please read the rules then type agree below to" - " verify yourself" - ), - ) - config.add( - key="welcome_message", - datatype="str", - title="Server Gate welcome message", - description="The message to send to the user after they are verified", - default="You are now verified! Welcome to the server!", - ) - config.add( - key="delete_wait", - datatype="int", - title="Welcome message delete time", - description=( - "The amount of time to wait (in seconds) before deleting the welcome" - " message" - ), - default=60, - ) - config.add( - key="verify_text", - datatype="str", - title="Verification text", - description=( - "The case-insensitive text the user should type to verify themselves" - ), - default="agree", - ) - await bot.add_cog(ServerGate(bot=bot, extension_name="gate")) - bot.add_extension_config("gate", config) class ServerGate(cogs.MatchCog): @@ -89,11 +35,11 @@ async def match(self: Self, ctx: commands.Context, _: str) -> bool: Returns: bool: Whether the message should be subject to the gate policy or not """ - config = self.bot.guild_configs[str(ctx.guild.id)] - if not config.extensions.gate.channel.value: + channel = configuration.get_config_entry(ctx.guild.id, "gate_channel") + if not channel: return False - return ctx.channel.id == int(config.extensions.gate.channel.value) + return ctx.channel.id == int(channel) async def response( self: Self, ctx: commands.Context, content: str, _: bool @@ -112,12 +58,11 @@ async def response( await ctx.message.delete() - config = self.bot.guild_configs[str(ctx.guild.id)] - - if content.lower() == config.extensions.gate.verify_text.value: + if content.lower() == configuration.get_config_entry( + ctx.guild.id, "gate_verify_text" + ): roles = await self.get_roles(ctx) if not roles: - config = self.bot.guild_configs[str(ctx.guild.id)] log_channel = configuration.get_config_entry( ctx.guild.id, "core_logging_channel" ) @@ -134,8 +79,12 @@ async def response( await ctx.author.add_roles(*roles, reason="Gate passed successfully") - welcome_message = config.extensions.gate.welcome_message.value - delete_wait = config.extensions.gate.delete_wait.value + welcome_message = configuration.get_config_entry( + ctx.guild.id, "gate_welcome_message" + ) + delete_wait = configuration.get_config_entry( + ctx.guild.id, "gate_delete_wait" + ) embed = auxiliary.generate_basic_embed( title="Server Gate", @@ -160,9 +109,8 @@ async def get_roles(self: Self, ctx: commands.Context) -> list[discord.Role]: Returns: list[discord.Role]: A list of all the roles that should be given to the user """ - config = self.bot.guild_configs[str(ctx.guild.id)] roles = [] - for role_name in config.extensions.gate.roles.value: + for role_name in configuration.get_config_entry(ctx.guild.id, "gate_roles"): role = discord.utils.get(ctx.guild.roles, name=role_name) if role in ctx.author.roles: @@ -200,13 +148,15 @@ async def intro_message(self: Self, ctx: commands.Context) -> None: Args: ctx (commands.Context): The context in which the command occured """ - config = self.bot.guild_configs[str(ctx.guild.id)] - - if ctx.channel.id != int(config.extensions.gate.channel.value): + if ctx.channel.id != int( + configuration.get_config_entry(ctx.guild.id, "gate_channel") + ): await auxiliary.send_deny_embed( message="That command is only usable in the gate channel", channel=ctx.channel, ) return - await ctx.channel.send(config.extensions.gate.intro_message.value) + await ctx.channel.send( + configuration.get_config_entry(ctx.guild.id, "gate_intro_message") + ) diff --git a/techsupport_bot/commands/grab.py b/techsupport_bot/commands/grab.py index fcdb9c60d..c976f7749 100644 --- a/techsupport_bot/commands/grab.py +++ b/techsupport_bot/commands/grab.py @@ -7,9 +7,10 @@ import random from typing import TYPE_CHECKING, Self +import configuration import discord import ui -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: @@ -23,24 +24,7 @@ async def setup(bot: bot.TechSupportBot) -> None: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="per_page", - datatype="int", - title="Grabs per page", - description="The number of grabs per page when retrieving all grabs", - default=3, - ) - config.add( - key="allowed_channels", - datatype="list", - title="List of allowed channels", - description="The list of channels to enable the grabs plugin", - default=[], - ) - await bot.add_cog(Grabber(bot=bot)) - bot.add_extension_config("grab", config) async def invalid_channel(ctx: commands.Context) -> bool: @@ -57,13 +41,14 @@ async def invalid_channel(ctx: commands.Context) -> bool: Returns: bool: If the grabs are allowed in the channel the command was run in """ - - config = ctx.bot.guild_configs[str(ctx.guild.id)] + allowed_channels = configuration.get_config_entry( + ctx.guild.id, "grab_allowed_channels" + ) # Check if list is empty. If it is, allow all channels - if not config.extensions.grab.allowed_channels.value: + if not allowed_channels: return True # If this list is not empty, it is a strict whitelist - if str(ctx.channel.id) in config.extensions.grab.allowed_channels.value: + if str(ctx.channel.id) in allowed_channels: return True raise commands.CommandError("Grabs are disabled for this channel") @@ -181,9 +166,6 @@ async def all_grabs( user_to_grab (discord.Member): The user to get all the grabs from """ is_nsfw = ctx.channel.is_nsfw() - - config = self.bot.guild_configs[str(ctx.guild.id)] - if user_to_grab.bot: await auxiliary.send_deny_embed( message="Ain't gonna catch me slipping!", channel=ctx.channel @@ -228,7 +210,8 @@ async def all_grabs( inline=False, ) if ( - field_counter == config.extensions.grab.per_page.value + field_counter + == configuration.get_config_entry(ctx.guild.id, "grab_per_page") or index == len(list(grabs)) - 1 ): embed.set_thumbnail(url=user_to_grab.display_avatar.url) diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index e1791484d..176e3c260 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -9,7 +9,7 @@ import configuration import discord import ui -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord import app_commands from discord.ext import commands @@ -23,17 +23,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="hangman_roles", - datatype="list", - title="Hangman admin roles", - description="The list of role names able to control hangman games", - default=[], - ) - await bot.add_cog(HangmanCog(bot=bot)) - bot.add_extension_config("hangman", config) class HangmanGame: @@ -292,7 +282,9 @@ async def can_stop_game(ctx: commands.Context) -> bool: config = ctx.bot.guild_configs[str(ctx.guild.id)] roles = [] - for role_name in config.extensions.hangman.hangman_roles.value: + for role_name in configuration.get_config_entry( + ctx.guild.id, "hangman_hangman_roles" + ): role = discord.utils.get(ctx.guild.roles, name=role_name) if not role: continue diff --git a/techsupport_bot/commands/joke.py b/techsupport_bot/commands/joke.py index b80b6c16e..e5fb804f7 100644 --- a/techsupport_bot/commands/joke.py +++ b/techsupport_bot/commands/joke.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Self +import configuration import discord import munch from core import auxiliary, cogs, extensionconfig @@ -78,13 +79,14 @@ def build_url(self: Self, ctx: commands.Context) -> str: Returns: str: The URL, properly formatted and ready to be called """ - config = self.bot.guild_configs[str(ctx.guild.id)] blacklist_flags = [] if ( - config.extensions.joke.apply_in_nsfw_channels.value + configuration.get_config_entry(ctx.guild.id, "joke_apply_in_nsfw_channels") or not ctx.channel.is_nsfw() ): - blacklist_flags = config.extensions.joke.blacklisted_filters.value + blacklist_flags = configuration.get_config_entry( + ctx.guild.id, "joke_blacklisted_filters" + ) blacklists = ",".join(blacklist_flags) url = f"{self.API_URL}?blacklistFlags={blacklists}&format=txt" diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 4b7bedbc6..c5216b65d 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -55,5 +55,18 @@ "automod_string_map": {}, "moderation_max_warnings": 3, "moderator_ban_delete_duration": 7, - "logger_channel_map": {} + "logger_channel_map": {}, + "gate_channel": "", + "gate_delete_wait": 60, + "gate_intro_message": "Welcome to our server! 👋 Please read the rules then type agree below to verify yourself", + "gate_roles": [], + "gate_verify_text": "agree", + "gate_welcome_message": "You are now verified! Welcome to the server!", + "grab_allowed_channels": [], + "grab_per_page": 3, + "hangman_hangman_roles": [], + "honeypot_channels": [], + "moderation_alert_channel": "", + "joke_apply_in_nsfw_channels": false, + "joke_blacklisted_filters": ["nsfw", "explicit"] } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index 4533aef97..007ef2b42 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -226,6 +226,58 @@ "logger_channel_map": { "datatype": "dict[discord.TextChannel, discord.TextChannel]", "description": "Input Channel to Logging Channel mapping" + }, + "gate_channel": { + "datatype": "discord.TextChannel", + "description": "The channel the gate is in" + }, + "gate_delete_wait": { + "datatype": "int", + "description": "The amount of time to wait (in seconds) before deleting the welcome message" + }, + "gate_intro_message": { + "datatype": "str", + "description": "The message that's sent when running the intro message command" + }, + "gate_roles": { + "datatype": "list[discord.Role]", + "description": "The list of roles to add after user is verified" + }, + "gate_verify_text": { + "datatype": "str", + "description": "The case-insensitive text the user should type to verify themselves" + }, + "gate_welcome_message": { + "datatype": "str", + "description": "The message to send to the user after they are verified" + }, + "grab_allowed_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The list of channels to enable the grabs plugin" + }, + "grab_per_page": { + "datatype": "int", + "description": "The number of grabs per page when retrieving all grabs" + }, + "hangman_hangman_roles": { + "datatype": "list[discord.Role]", + "description": "The list of role names able to control hangman games" + }, + "honeypot_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The list of channels that are honeypots" + }, + "moderation_alert_channel": { + "datatype": "discord.TextChannel", + "description": "The channel to send moderation and honeypot alerts to" + }, + "joke_apply_in_nsfw_channels": { + "datatype": "bool", + "description": "Toggles whether or not filters are applies in NSFW channels" + }, + "joke_blacklisted_filters": { + "datatype": "list[str]", + "description": "Filters all categories listed (nsfw,religious,political,racist,sexist,explicit)" } } diff --git a/techsupport_bot/functions/honeypot.py b/techsupport_bot/functions/honeypot.py index 974dd3c03..d3d952a1e 100644 --- a/techsupport_bot/functions/honeypot.py +++ b/techsupport_bot/functions/honeypot.py @@ -5,6 +5,7 @@ import datetime from typing import TYPE_CHECKING, Self +import configuration import discord from core import cogs, extensionconfig from discord.ext import commands @@ -44,9 +45,10 @@ async def match(self: Self, ctx: commands.Context, content: str) -> bool: Returns: bool: Whether the author sent in a honeypot channel """ - config = self.bot.guild_configs[str(ctx.guild.id)] # If the channel isn't a honeypot, do nothing. - if not str(ctx.channel.id) in config.extensions.honeypot.channels.value: + if not str(ctx.channel.id) in configuration.get_config_entry( + ctx.guild.id, "honeypot_channels" + ): return False return True @@ -67,12 +69,11 @@ async def response( # This should be replaced with a guild wide purge when discord.py can be updated. await ctx.author.ban(delete_message_days=1, reason="triggered honeypot") await ctx.guild.unban(ctx.author, reason="triggered honeypot") - - config = self.bot.guild_configs[str(ctx.guild.id)] - # Send an alert in the alert channel, if its configured try: - alert_channel = ctx.guild.get_channel(int(config.moderation.alert_channel)) + alert_channel = ctx.guild.get_channel( + configuration.get_config_entry(ctx.guild.id, "moderation_alert_channel") + ) except TypeError: alert_channel = None From aab0383794fd80b95b8679c89882cc0a2fa4ac15 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:02:45 -0700 Subject: [PATCH 21/40] Remove config seutp from joke and honeypot --- techsupport_bot/commands/joke.py | 21 +-------------------- techsupport_bot/functions/honeypot.py | 11 +---------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/techsupport_bot/commands/joke.py b/techsupport_bot/commands/joke.py index e5fb804f7..50e678c75 100644 --- a/techsupport_bot/commands/joke.py +++ b/techsupport_bot/commands/joke.py @@ -7,7 +7,7 @@ import configuration import discord import munch -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: @@ -21,26 +21,7 @@ async def setup(bot: bot.TechSupportBot) -> None: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="blacklisted_filters", - datatype="list[str]", - title="Enable filter", - description=( - "Filters all categories listed" - "(nsfw,religious,political,racist,sexist,explicit)" - ), - default=["nsfw", "explicit"], - ) - config.add( - key="apply_in_nsfw_channels", - datatype="bool", - title="Apply in NSFW Channels", - description=("Toggles whether or not filters are applies in NSFW channels"), - default=False, - ) await bot.add_cog(Joker(bot=bot)) - bot.add_extension_config("joke", config) class Joker(cogs.BaseCog): diff --git a/techsupport_bot/functions/honeypot.py b/techsupport_bot/functions/honeypot.py index d3d952a1e..a42d83e11 100644 --- a/techsupport_bot/functions/honeypot.py +++ b/techsupport_bot/functions/honeypot.py @@ -7,7 +7,7 @@ import configuration import discord -from core import cogs, extensionconfig +from core import cogs from discord.ext import commands if TYPE_CHECKING: @@ -20,16 +20,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - config = extensionconfig.ExtensionConfig() - config.add( - key="channels", - datatype="list", - title="Honeypot channels", - description=("The list of channel ID's that are honeypots"), - default=[], - ) await bot.add_cog(HoneyPot(bot=bot, extension_name="honeypot")) - bot.add_extension_config("honeypot", config) class HoneyPot(cogs.MatchCog): From 70d24a98e5d92e1662224fc5fdadc4cf29a9f0d9 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:33:57 -0700 Subject: [PATCH 22/40] Process factoids --- techsupport_bot/commands/factoids.py | 76 ++++++++----------- techsupport_bot/commands/modmail.py | 4 +- techsupport_bot/commands/xp.py | 4 +- .../configuration/config.default.json | 7 +- .../configuration/config.meta.json | 20 +++++ 5 files changed, 64 insertions(+), 47 deletions(-) diff --git a/techsupport_bot/commands/factoids.py b/techsupport_bot/commands/factoids.py index aa80641d9..1d7dbb5ca 100644 --- a/techsupport_bot/commands/factoids.py +++ b/techsupport_bot/commands/factoids.py @@ -104,9 +104,10 @@ async def has_manage_factoids_role(ctx: commands.Context) -> bool: Returns: bool: True if the command can be run, False if it can't """ - config = ctx.bot.guild_configs[str(ctx.guild.id)] return await has_given_factoids_role( - ctx.guild, ctx.author, config.extensions.factoids.manage_roles.value + ctx.guild, + ctx.author, + configuration.get_config_entry(ctx.guild.id, "factoids_manage_roles"), ) @@ -119,9 +120,10 @@ async def has_admin_factoids_role(ctx: commands.Context) -> bool: Returns: bool: True if the command can be run, False if it can't """ - config = ctx.bot.guild_configs[str(ctx.guild.id)] return await has_given_factoids_role( - ctx.guild, ctx.author, config.extensions.factoids.admin_roles.value + ctx.guild, + ctx.author, + configuration.get_config_entry(ctx.guild.id, "factoids_admin_roles"), ) @@ -791,8 +793,9 @@ async def match(self: Self, ctx: commands.Context, message_contents: str) -> boo """ if not ctx.guild: return - config = self.bot.guild_configs[str(ctx.guild.id)] - return message_contents.startswith(config.extensions.factoids.prefix.value) + return message_contents.startswith( + configuration.get_config_entry(ctx.guild.id, "factoids_prefix") + ) async def response( self: Self, @@ -814,8 +817,7 @@ async def response( return # Checks if the first word of the content after the prefix is a valid factoid # Replaces \n with spaces so factoid can be called even with newlines - config = self.bot.guild_configs[str(ctx.guild.id)] - prefix = config.extensions.factoids.prefix.value + prefix = configuration.get_config_entry(ctx.guild.id, "factoids_prefix") query = message_content[len(prefix) :].replace("\n", " ").split(" ")[0].lower() try: factoid = await self.get_factoid(query, str(ctx.guild.id)) @@ -832,14 +834,14 @@ async def response( if factoid.disabled: return - if ( - factoid.restricted - and str(ctx.channel.id) - not in config.extensions.factoids.restricted_list.value + if factoid.restricted and str( + ctx.channel.id + ) not in configuration.get_config_entry( + ctx.guild.id, "factoids_restricted_list" ): return - if config.extensions.factoids.disable_embeds.value: + if configuration.get_config_entry(ctx.guild.id, "factoids_disable_embeds"): embed = None else: try: @@ -881,7 +883,6 @@ async def response( content=content, embed=embed, mention_author=not mentions ) # log it in the logging channel with type info and generic content - config = self.bot.guild_configs[str(ctx.guild.id)] log_channel = configuration.get_config_entry( ctx.guild.id, "core_logging_channel" ) @@ -896,7 +897,6 @@ async def response( ) # If something breaks, also log it except discord.errors.HTTPException as exception: - config = self.bot.guild_configs[str(ctx.guild.id)] log_channel = configuration.get_config_entry( ctx.guild.id, "core_logging_channel" ) @@ -958,8 +958,6 @@ async def send_to_logger( factoid was sent in factoid_message (str): The plaintext message content of the factoid """ - config = self.bot.guild_configs[str(channel.guild.id)] - # Don't allow logging if extension is disabled if "logger" not in configuration.get_config_entry( factoid_caller.guild.id, "core_enabled_extensions" @@ -1001,7 +999,6 @@ async def factoid_call_command( TooLongFactoidMessageError: If the plaintext exceed 2000 characters """ query = factoid_name.replace("\n", " ").split(" ")[0].lower() - config = self.bot.guild_configs[str(interaction.guild.id)] try: factoid = await self.get_factoid(query, str(interaction.guild.id)) @@ -1027,17 +1024,19 @@ async def factoid_call_command( await interaction.response.send_message(embed=embed, ephemeral=True) return - if ( - factoid.restricted - and str(interaction.channel.id) - not in config.extensions.factoids.restricted_list.value + if factoid.restricted and str( + interaction.channel.id + ) not in configuration.get_config_entry( + interaction.guild.id, "factoids_restricted_list" ): embed = auxiliary.prepare_deny_embed( message=f"The factoid {factoid_name} is restricted and not allowed in this channel." ) await interaction.response.send_message(embed=embed, ephemeral=True) return - if config.extensions.factoids.disable_embeds.value: + if configuration.get_config_entry( + interaction.guild.id, "factoids_disable_embeds" + ): embed = None else: try: @@ -1153,7 +1152,6 @@ async def cronjob( channel = None if ctx: - config = self.bot.guild_configs[str(ctx.guild.id)] channel = configuration.get_config_entry( ctx.guild.id, "core_logging_channel" ) @@ -1181,7 +1179,6 @@ async def cronjob( log_context = None if ctx: - config = self.bot.guild_configs[str(ctx.guild.id)] channel = configuration.get_config_entry( ctx.guild.id, "core_logging_channel" ) @@ -1205,7 +1202,6 @@ async def cronjob( log_context = None if ctx: - config = self.bot.guild_configs[str(ctx.guild.id)] channel = configuration.get_config_entry( ctx.guild.id, "core_logging_channel" ) @@ -1228,7 +1224,6 @@ async def cronjob( log_context = None if ctx: - config = self.bot.guild_configs[str(ctx.guild.id)] channel = configuration.get_config_entry( ctx.guild.id, "core_logging_channel" ) @@ -1244,22 +1239,19 @@ async def cronjob( context=log_context, ) continue - - config = self.bot.guild_configs[str(channel.guild.id)] - # Checking for disabled or restricted if factoid.disabled: return - if ( - factoid.restricted - and str(channel.id) - not in config.extensions.factoids.restricted_list.value + if factoid.restricted and str( + channel.id + ) not in configuration.get_config_entry( + ctx.guild.id, "factoids_restricted_list" ): return # Get_embed accepts job as a factoid object - if config.extensions.factoids.disable_embeds.value: + if configuration.get_config_entry(ctx.guild.id, "factoids_disable_embeds"): embed = None else: try: @@ -1289,7 +1281,6 @@ async def cronjob( message = await channel.send(content=content, embed=embed) except discord.errors.HTTPException as exception: - config = self.bot.guild_configs[str(ctx.guild.id)] log_channel = configuration.get_config_entry( ctx.guild.id, "core_logging_channel" ) @@ -1434,7 +1425,6 @@ async def loop( channel (discord.TextChannel): The channel to loop the factoid in cron_config (str): The cron config of the loop """ - config = self.bot.guild_configs[str(ctx.guild.id)] factoid = await self.get_factoid(factoid_name, str(ctx.guild.id)) if factoid.protected: @@ -1451,9 +1441,8 @@ async def loop( ) return - if ( - factoid.restricted - and str(channel.id) not in config.extensions.factoids.restricted_list.value + if factoid.restricted and str(channel.id) not in configuration.get_config_entry( + ctx.guild.id, "factoids_restricted_list" ): await auxiliary.send_deny_embed( message=( @@ -1804,11 +1793,12 @@ async def app_command_all( guild = str(interaction.guild.id) # Check for admin roles if ignoring hidden if true_all or show_hidden: - config = self.bot.guild_configs[str(interaction.guild.id)] await has_given_factoids_role( interaction.guild, interaction.user, - config.extensions.factoids.admin_roles.value, + configuration.get_config_entry( + interaction.guild.id, "factoids_admin_roles" + ), ) if true_all: @@ -1975,7 +1965,6 @@ async def build_factoid_all( # If an error happened while calling the api except (gaierror, InvalidURL) as exception: - config = self.bot.guild_configs[str(guild.id)] log_channel = configuration.get_config_entry( guild.id, "core_logging_channel" ) @@ -2500,7 +2489,6 @@ async def dealias( ) # Logs the new parent change - config = self.bot.guild_configs[str(ctx.guild.id)] log_channel = configuration.get_config_entry( ctx.guild.id, "core_logging_channel" ) diff --git a/techsupport_bot/commands/modmail.py b/techsupport_bot/commands/modmail.py index 946f1f304..756b32e7e 100644 --- a/techsupport_bot/commands/modmail.py +++ b/techsupport_bot/commands/modmail.py @@ -1102,7 +1102,9 @@ async def on_message(self: Self, message: discord.Message) -> None: if factoid.disabled or ( factoid.restricted and str(self.modmail_forum.id) - not in config.extensions.factoids.restricted_list.value + not in configuration.get_config_entry( + message.guild.id, "factoids_restricted_list" + ) ): return diff --git a/techsupport_bot/commands/xp.py b/techsupport_bot/commands/xp.py index bdd0b00c2..c99b35722 100644 --- a/techsupport_bot/commands/xp.py +++ b/techsupport_bot/commands/xp.py @@ -163,7 +163,9 @@ async def match(self: Self, ctx: commands.Context, _: str) -> bool: if "factoids" in configuration.get_config_entry( ctx.guild.id, "core_enabled_extensions" ): - factoid_prefix = config.extensions.factoids.prefix.value + factoid_prefix = configuration.get_config_entry( + ctx.guild.id, "factoids_prefix" + ) if ctx.message.clean_content.startswith(factoid_prefix): return False diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index c5216b65d..79f0b6837 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -68,5 +68,10 @@ "honeypot_channels": [], "moderation_alert_channel": "", "joke_apply_in_nsfw_channels": false, - "joke_blacklisted_filters": ["nsfw", "explicit"] + "joke_blacklisted_filters": ["nsfw", "explicit"], + "factoids_admin_roles": [], + "factoids_disable_embeds": false, + "factoids_manage_roles": [], + "factoids_prefix": "?", + "factoids_restricted_list": [] } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index 007ef2b42..92a6cb815 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -278,6 +278,26 @@ "joke_blacklisted_filters": { "datatype": "list[str]", "description": "Filters all categories listed (nsfw,religious,political,racist,sexist,explicit)" + }, + "factoids_admin_roles": { + "datatype": "list[discord.Role]", + "description": "The roles required to administrate factoids" + }, + "factoids_disable_embeds": { + "datatype": "bool", + "description": "This will force all factoids to not use embeds." + }, + "factoids_manage_roles": { + "datatype": "list[discord.Role]", + "description": "The roles required to manage factoids" + }, + "factoids_prefix": { + "datatype": "str", + "description": "Prefix for calling factoids" + }, + "factoids_restricted_list": { + "datatype": "list[discord.TextChannel]", + "description": "List of channel IDs that restricted factoids are allowed to be used in" } } From 1961fc4a07013e46ab20456ba164f828708cdb17 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:35:49 -0700 Subject: [PATCH 23/40] Remove unused config variables --- techsupport_bot/commands/chatgpt.py | 1 - techsupport_bot/commands/dumpdbg.py | 1 - techsupport_bot/commands/echo.py | 3 --- techsupport_bot/commands/hangman.py | 1 - techsupport_bot/commands/moderator.py | 3 --- techsupport_bot/commands/relay.py | 2 -- techsupport_bot/functions/nickname.py | 3 --- 7 files changed, 14 deletions(-) diff --git a/techsupport_bot/commands/chatgpt.py b/techsupport_bot/commands/chatgpt.py index ad6b485c0..cacd779f2 100644 --- a/techsupport_bot/commands/chatgpt.py +++ b/techsupport_bot/commands/chatgpt.py @@ -134,7 +134,6 @@ async def gpt(self: Self, ctx: commands.Context, *, prompt: str) -> None: response = await self.call_api(ctx, api_key, prompt) # -> Response processing <- - config = self.bot.guild_configs[str(ctx.guild.id)] choices = response.get("choices", []) if not choices: # Tries to figure out what error happened diff --git a/techsupport_bot/commands/dumpdbg.py b/techsupport_bot/commands/dumpdbg.py index 931541946..d8c798a38 100644 --- a/techsupport_bot/commands/dumpdbg.py +++ b/techsupport_bot/commands/dumpdbg.py @@ -57,7 +57,6 @@ async def debug_dump(self: Self, ctx: commands.Context) -> None: ctx (commands.Context): The context in which the command was run """ - config = self.bot.guild_configs[str(ctx.guild.id)] api_endpoint = self.bot.file_config.api.api_url.dumpdbg permitted_roles = configuration.get_config_entry(ctx.guild.id, "dumpdbg_roles") diff --git a/techsupport_bot/commands/echo.py b/techsupport_bot/commands/echo.py index b046dd4a8..3a8268c92 100644 --- a/techsupport_bot/commands/echo.py +++ b/techsupport_bot/commands/echo.py @@ -75,9 +75,6 @@ async def echo_channel( sent_message = await channel.send(content=message) await auxiliary.send_confirm_embed(message="Message sent", channel=ctx.channel) - - config = self.bot.guild_configs[str(channel.guild.id)] - # Don't allow logging if extension is disabled if "logger" not in configuration.get_config_entry( channel.guild.id, "core_enabled_extensions" diff --git a/techsupport_bot/commands/hangman.py b/techsupport_bot/commands/hangman.py index 176e3c260..63869d2e6 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -280,7 +280,6 @@ async def can_stop_game(ctx: commands.Context) -> bool: if getattr(user, "id", 0) == ctx.author.id: return True - config = ctx.bot.guild_configs[str(ctx.guild.id)] roles = [] for role_name in configuration.get_config_entry( ctx.guild.id, "hangman_hangman_roles" diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 6b17e2fe1..6ffedcbc2 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -106,7 +106,6 @@ async def handle_ban_user( return if not delete_days: - config = self.bot.guild_configs[str(interaction.guild.id)] delete_days = configuration.get_config_entry( interaction.guild.id, "moderator_ban_delete_duration" ) @@ -479,8 +478,6 @@ async def handle_warn_user( await interaction.response.send_message(embed=embed) return - config = self.bot.guild_configs[str(interaction.guild.id)] - new_count_of_warnings = ( len(await moderation.get_all_warnings(self.bot, target, interaction.guild)) + 1 diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index 012239fbb..91d72ad3d 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -409,7 +409,6 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No message=split_message["content"], channel=discord_channel ) - config = self.bot.guild_configs[str(discord_channel.guild.id)] if "automod" in configuration.get_config_entry( discord_channel.guild.id, "core_enabled_extensions" ): @@ -451,7 +450,6 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No sent_message = await discord_channel.send(content=mentions_string, embed=embed) - config = self.bot.guild_configs[str(discord_channel.guild.id)] # Don't allow logging if extension is disabled if "logger" not in configuration.get_config_entry( discord_channel.guild.id, "core_enabled_extensions" diff --git a/techsupport_bot/functions/nickname.py b/techsupport_bot/functions/nickname.py index d33bc321d..3b8b12fa0 100644 --- a/techsupport_bot/functions/nickname.py +++ b/techsupport_bot/functions/nickname.py @@ -112,7 +112,6 @@ async def response( content (str): The content of the message result (bool): The return value of the match function """ - config = self.bot.guild_configs[str(ctx.guild.id)] # If user outranks bot, do nothing if ctx.message.author.top_role >= ctx.channel.guild.me.top_role: return @@ -147,8 +146,6 @@ async def on_member_join(self: Self, member: discord.Member) -> None: Args: member (discord.Member): The member who joined """ - config = self.bot.guild_configs[str(member.guild.id)] - # Don't do anything if the filter is off for the guild if not configuration.get_config_entry(member.guild.id, "core_nickname_filter"): return From aac51722106cd780547e524f0b2d8276802fe299 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 03:15:23 -0700 Subject: [PATCH 24/40] Kanye, Moderator, Modlog and Modmail config migrated --- techsupport_bot/commands/kanye.py | 33 ++------ techsupport_bot/commands/moderator.py | 22 +----- techsupport_bot/commands/modlog.py | 20 +---- techsupport_bot/commands/modmail.py | 79 +++---------------- .../configuration/config.default.json | 12 ++- .../configuration/config.meta.json | 40 ++++++++++ 6 files changed, 70 insertions(+), 136 deletions(-) diff --git a/techsupport_bot/commands/kanye.py b/techsupport_bot/commands/kanye.py index d3906c96d..a07bb8731 100644 --- a/techsupport_bot/commands/kanye.py +++ b/techsupport_bot/commands/kanye.py @@ -7,8 +7,9 @@ from typing import TYPE_CHECKING, Self import discord -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands +import configuration if TYPE_CHECKING: import bot @@ -20,32 +21,8 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="channel", - datatype="int", - title="Kanye Channel ID", - description="The ID of the channel the Kanye West quote should appear in", - default=None, - ) - config.add( - key="min_wait", - datatype="int", - title="Min wait (hours)", - description="The minimum number of hours to wait between Kanye events", - default=24, - ) - config.add( - key="max_wait", - datatype="int", - title="Max wait (hours)", - description="The minimum number of hours to wait between Kanye events", - default=48, - ) await bot.add_cog(KanyeQuotes(bot=bot, extension_name="kanye")) - bot.add_extension_config("kanye", config) - class KanyeQuotes(cogs.LoopCog): """Class to get the Kanye quotes from the api. @@ -102,7 +79,7 @@ async def execute(self: Self, guild: discord.Guild) -> None: quote = await self.get_quote() embed = self.generate_themed_embed(quote=quote) - channel = guild.get_channel(int(config.extensions.kanye.channel.value)) + channel = guild.get_channel(int(configuration.get_config_entry(guild.id, "kanye_channel"))) if not channel: return @@ -117,8 +94,8 @@ async def wait(self: Self, guild: discord.Guild) -> None: config = self.bot.guild_configs[str(guild.id)] await asyncio.sleep( random.randint( - config.extensions.kanye.min_wait.value * 3600, - config.extensions.kanye.max_wait.value * 3600, + configuration.get_config_entry(guild.id, "kanye_min_wait") * 3600, + configuration.get_config_entry(guild.id, "kanye_max_wait") * 3600, ) ) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 6ffedcbc2..9901ef3bb 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -11,7 +11,7 @@ import ui from botlogging import LogContext, LogLevel from commands import modlog -from core import auxiliary, cogs, extensionconfig, moderation +from core import auxiliary, cogs, moderation from discord import app_commands if TYPE_CHECKING: @@ -24,25 +24,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - config = extensionconfig.ExtensionConfig() - config.add( - key="immune_roles", - datatype="list", - title="Immune role names", - description="The list of role names that are immune to protect commands", - default=[], - ) - config.add( - key="ban_delete_duration", - datatype="int", - title="Ban delete duration (days)", - description=( - "The default amount of days to delete messages for a user after they are banned" - ), - default=7, - ) await bot.add_cog(ProtectCommands(bot=bot, extension_name="moderator")) - bot.add_extension_config("moderator", config) class ProtectCommands(cogs.BaseCog): @@ -773,7 +755,7 @@ async def permission_check( # Check to see if target has any immune roles try: - for name in config.extensions.moderator.immune_roles.value: + for name in configuration.get_config_entry(target.guild.id, "moderator_immune_roles"): role_check = discord.utils.get(target.guild.roles, name=name) if role_check and role_check in getattr(target, "roles", []): return ( diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index c92bdcc61..788da8860 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -10,7 +10,7 @@ import discord import munch import ui -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord import app_commands from discord.ext import commands @@ -24,17 +24,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - config = extensionconfig.ExtensionConfig() - config.add( - key="alert_channel", - datatype="int", - title="Alert channel ID", - description="The ID of the channel to send auto-protect alerts to", - default=None, - ) await bot.add_cog(BanLogger(bot=bot, extension_name="modlog")) - bot.add_extension_config("modlog", config) - class BanLogger(cogs.BaseCog): """The class that holds the /modlog commands @@ -312,11 +302,9 @@ async def log_ban( embed.timestamp = datetime.datetime.utcnow() embed.color = discord.Color.red() - config = bot.guild_configs[str(guild.id)] - try: alert_channel = guild.get_channel( - int(config.extensions.modlog.alert_channel.value) + int(configuration.get_config_entry(guild.id, "modlog_alert_channel")) ) except TypeError: alert_channel = None @@ -361,11 +349,9 @@ async def log_unban( embed.timestamp = datetime.datetime.utcnow() embed.color = discord.Color.green() - config = bot.guild_configs[str(guild.id)] - try: alert_channel = guild.get_channel( - int(config.extensions.modlog.alert_channel.value) + int(configuration.get_config_entry(guild.id, "modlog_alert_channel")) ) except TypeError: alert_channel = None diff --git a/techsupport_bot/commands/modmail.py b/techsupport_bot/commands/modmail.py index 756b32e7e..cc691a4f6 100644 --- a/techsupport_bot/commands/modmail.py +++ b/techsupport_bot/commands/modmail.py @@ -22,7 +22,7 @@ import discord import expiringdict import ui -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: @@ -301,9 +301,6 @@ async def handle_dm(message: discord.Message, guild_id: int, forum_id: int) -> N ) ) return - - config = Ts_client.guild_configs[str(guild_id)] - # The user already has an open thread if message.author.id in active_threads: thread = Ts_client.get_channel(active_threads[message.author.id]) @@ -349,7 +346,7 @@ async def handle_dm(message: discord.Message, guild_id: int, forum_id: int) -> N # - No thread was found, create one - - auto_rejections = config.extensions.modmail.automatic_rejections.value + auto_rejections = configuration.get_config_entry(guild_id, "modmail_automatic_rejections") for regex in auto_rejections: if re.match(regex, message.content): await auxiliary.send_deny_embed( @@ -369,7 +366,7 @@ async def handle_dm(message: discord.Message, guild_id: int, forum_id: int) -> N confirmation = ui.Confirm() await confirmation.send( - message=config.extensions.modmail.thread_creation_message.value, + message=configuration.get_config_entry(guild_id, "modmail_thread_creation_message"), channel=message.channel, author=message.author, ) @@ -425,7 +422,6 @@ async def create_thread( Returns: bool: Whether the thread was created succesfully """ - config = Ts_client.guild_configs[str(channel.guild.id)] # --> CHECKS <-- # These checks can be triggered on both the users and server side using .contact @@ -505,7 +501,7 @@ async def create_thread( # Handling for roles to ping, not performed if the func was invoked by the contact command role_string = "" - roles_to_ping = list(dict.fromkeys(config.extensions.modmail.roles_to_ping.value)) + roles_to_ping = list(dict.fromkeys(configuration.get_config_entry(channel.guild.id, "modmail_roles_to_ping"))) if message and roles_to_ping: for role_id in roles_to_ping: role_string += f"<@&{role_id}> " @@ -552,7 +548,7 @@ async def create_thread( await message.author.send(embed=embed) # - Auto responses - - automatic_responses = config.extensions.modmail.automatic_responses.value + automatic_responses = configuration.get_config_entry(channel.guild.id, "modmail_automatic_responses") for regex in automatic_responses: if re.match(regex, message.content): await reply_to_thread( @@ -841,57 +837,7 @@ async def setup(bot: bot.TechSupportBot) -> None: # the most reliable way to ensure the modmail bot or code doesn't run raise AttributeError("Modmail was not loaded because it's disabled") - config = extensionconfig.ExtensionConfig() - - config.add( - key="aliases", - datatype="dict", - title="Aliases for modmail messages", - description="Custom modmail commands to send message slices", - default={}, - ) - - config.add( - key="automatic_responses", - datatype="dict", - title="Modmail autoresponses", - description="If someone sends a message containing a key, sends its value", - default={}, - ) - - config.add( - key="automatic_rejections", - datatype="dict", - title="Modmail auto-rejections", - description="If someone sends a message matching regex, blocks thread creation", - default={}, - ) - - config.add( - key="modmail_roles", - datatype="list", - title="Roles that can access modmail and its commands", - description="Roles that can access modmail and its commands", - default=[], - ) - - config.add( - key="roles_to_ping", - datatype="list", - title="Roles to ping on thread creation", - description="Roles to ping on thread creation", - default=[], - ) - - config.add( - key="thread_creation_message", - datatype="str", - title="Thread creation message", - description="The message sent to the user when confirming a thread creation.", - default="Create modmail thread?", - ) await bot.add_cog(Modmail(bot=bot)) - bot.add_extension_config("modmail", config) class Modmail(cogs.BaseCog): @@ -958,7 +904,6 @@ async def on_message(self: Self, message: discord.Message) -> None: return # Makes sure the person is actually allowed to run modmail commands - config = self.bot.guild_configs[str(message.guild.id)] try: await has_modmail_management_role(message) except commands.MissingAnyRole as e: @@ -1097,8 +1042,6 @@ async def on_message(self: Self, message: discord.Message) -> None: return # Checks for restricted and disabled factoids - config = self.bot.guild_configs[str(message.guild.id)] - if factoid.disabled or ( factoid.restricted and str(self.modmail_forum.id) @@ -1116,7 +1059,7 @@ async def on_message(self: Self, message: discord.Message) -> None: ) # Checks if the command was an alias - aliases = config.extensions.modmail.aliases.value + aliases = configuration.get_config_entry(message.guild.id, "modmail_aliases") for alias in aliases: if alias != content.split()[0]: @@ -1359,11 +1302,8 @@ async def list_aliases(self: Self, ctx: commands.context) -> None: Args: ctx (commands.context): Context of the command execution """ - - config = self.bot.guild_configs[str(ctx.guild.id)] - # Checks if the command was an alias - aliases = config.extensions.modmail.aliases.value + aliases = configuration.get_config_entry(ctx.guild.id, "modmail_aliases") if not aliases: embed = auxiliary.prepare_deny_embed( message="There are no aliases registered for this guild", @@ -1405,16 +1345,15 @@ async def modmail_ban( return # Checking against the user to see if they have the roles specified in the config - config = self.bot.guild_configs[str(ctx.guild.id)] user_roles = getattr(user, "roles", []) - unparsed_roles = config.extensions.modmail.modmail_roles.value + unparsed_roles = configuration.get_config_entry(ctx.guild.id, "modmail_modmail_roles") modmail_roles = list(dict.fromkeys(unparsed_roles)) # No error has to be thrown if unparsed_roles is None, it's already checked in # has_modmail_management_role # Gets permitted roles - for role_id in config.extensions.modmail.modmail_roles.value: + for role_id in unparsed_roles: modmail_role = discord.utils.get(ctx.guild.roles, id=int(role_id)) if not modmail_role: continue diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 79f0b6837..729237d03 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -73,5 +73,15 @@ "factoids_disable_embeds": false, "factoids_manage_roles": [], "factoids_prefix": "?", - "factoids_restricted_list": [] + "factoids_restricted_list": [], + "kanye_channel": "", + "kanye_max_wait": 48, + "kanye_min_wait": 24, + "moderator_immune_roles": [], + "modlog_alert_channel": "", + "modmail_aliases": {}, + "modmail_automatic_rejections": {}, + "modmail_automatic_responses": {}, + "modmail_roles_to_ping": [], + "modmail_thread_creation_message": "Create modmail thread?" } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index 92a6cb815..36209b7c6 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -298,6 +298,46 @@ "factoids_restricted_list": { "datatype": "list[discord.TextChannel]", "description": "List of channel IDs that restricted factoids are allowed to be used in" + }, + "kanye_channel": { + "datatype": "discord.TextChannel", + "description": "The ID of the channel the Kanye West quote should appear in" + }, + "kanye_max_wait": { + "datatype": "int", + "description": "The maximum number of hours to wait between Kanye events" + }, + "kanye_min_wait": { + "datatype": "int", + "description": "The minimum number of hours to wait between Kanye events" + }, + "moderator_immune_roles": { + "datatype": "list[discord.Role]", + "description": "The list of role names that are immune to protect commands" + }, + "modlog_alert_channel": { + "datatype": "discord.TextChannel", + "description": "The ID of the channel to send auto-protect alerts to" + }, + "modmail_aliases": { + "datatype": "dict[str, str]", + "description": "Custom modmail commands to send message slices" + }, + "modmail_automatic_rejections": { + "datatype": "dict[str, str]", + "description": "If someone sends a message matching regex, blocks thread creation" + }, + "modmail_automatic_responses": { + "datatype": "dict[str, str]", + "description": "If someone sends a message containing a key, sends its value" + }, + "modmail_roles_to_ping": { + "datatype": "list[discord.Role]", + "description": "Roles to ping on thread creation" + }, + "modmail_thread_creation_message": { + "datatype": "str", + "description": "The message sent to the user when confirming a thread creation." } } From b695cd4ca3e165968f4a02cd7ae46b59fc177ca5 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 03:44:06 -0700 Subject: [PATCH 25/40] News, Paste, Purge and Report done --- techsupport_bot/commands/kanye.py | 9 +-- techsupport_bot/commands/moderator.py | 5 +- techsupport_bot/commands/modlog.py | 1 + techsupport_bot/commands/modmail.py | 22 +++++-- techsupport_bot/commands/news.py | 55 ++++------------ techsupport_bot/commands/purge.py | 21 ++---- techsupport_bot/commands/report.py | 42 +++++------- .../configuration/config.default.json | 18 ++++- .../configuration/config.meta.json | 64 ++++++++++++++++++ techsupport_bot/functions/paste.py | 66 +++++-------------- 10 files changed, 161 insertions(+), 142 deletions(-) diff --git a/techsupport_bot/commands/kanye.py b/techsupport_bot/commands/kanye.py index a07bb8731..3cce7a92f 100644 --- a/techsupport_bot/commands/kanye.py +++ b/techsupport_bot/commands/kanye.py @@ -6,10 +6,10 @@ import random from typing import TYPE_CHECKING, Self +import configuration import discord from core import auxiliary, cogs from discord.ext import commands -import configuration if TYPE_CHECKING: import bot @@ -24,6 +24,7 @@ async def setup(bot: bot.TechSupportBot) -> None: await bot.add_cog(KanyeQuotes(bot=bot, extension_name="kanye")) + class KanyeQuotes(cogs.LoopCog): """Class to get the Kanye quotes from the api. @@ -75,11 +76,12 @@ async def execute(self: Self, guild: discord.Guild) -> None: Args: guild (discord.Guild): The guild where the loop is taking place """ - config = self.bot.guild_configs[str(guild.id)] quote = await self.get_quote() embed = self.generate_themed_embed(quote=quote) - channel = guild.get_channel(int(configuration.get_config_entry(guild.id, "kanye_channel"))) + channel = guild.get_channel( + int(configuration.get_config_entry(guild.id, "kanye_channel")) + ) if not channel: return @@ -91,7 +93,6 @@ async def wait(self: Self, guild: discord.Guild) -> None: Args: guild (discord.Guild): The guild config where the loop is taking place """ - config = self.bot.guild_configs[str(guild.id)] await asyncio.sleep( random.randint( configuration.get_config_entry(guild.id, "kanye_min_wait") * 3600, diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 9901ef3bb..d1bc7ae3c 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -740,7 +740,6 @@ async def permission_check( Returns: str: The rejection string, if one exists. Otherwise, None is returned """ - config = self.bot.guild_configs[str(invoker.guild.id)] # Check to see if executed on author if invoker == target: return f"You cannot {action_name} yourself" @@ -755,7 +754,9 @@ async def permission_check( # Check to see if target has any immune roles try: - for name in configuration.get_config_entry(target.guild.id, "moderator_immune_roles"): + for name in configuration.get_config_entry( + target.guild.id, "moderator_immune_roles" + ): role_check = discord.utils.get(target.guild.roles, name=name) if role_check and role_check in getattr(target, "roles", []): return ( diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 788da8860..4b6e05cfd 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -26,6 +26,7 @@ async def setup(bot: bot.TechSupportBot) -> None: """ await bot.add_cog(BanLogger(bot=bot, extension_name="modlog")) + class BanLogger(cogs.BaseCog): """The class that holds the /modlog commands diff --git a/techsupport_bot/commands/modmail.py b/techsupport_bot/commands/modmail.py index cc691a4f6..c6a6bff63 100644 --- a/techsupport_bot/commands/modmail.py +++ b/techsupport_bot/commands/modmail.py @@ -346,7 +346,9 @@ async def handle_dm(message: discord.Message, guild_id: int, forum_id: int) -> N # - No thread was found, create one - - auto_rejections = configuration.get_config_entry(guild_id, "modmail_automatic_rejections") + auto_rejections = configuration.get_config_entry( + guild_id, "modmail_automatic_rejections" + ) for regex in auto_rejections: if re.match(regex, message.content): await auxiliary.send_deny_embed( @@ -366,7 +368,9 @@ async def handle_dm(message: discord.Message, guild_id: int, forum_id: int) -> N confirmation = ui.Confirm() await confirmation.send( - message=configuration.get_config_entry(guild_id, "modmail_thread_creation_message"), + message=configuration.get_config_entry( + guild_id, "modmail_thread_creation_message" + ), channel=message.channel, author=message.author, ) @@ -501,7 +505,11 @@ async def create_thread( # Handling for roles to ping, not performed if the func was invoked by the contact command role_string = "" - roles_to_ping = list(dict.fromkeys(configuration.get_config_entry(channel.guild.id, "modmail_roles_to_ping"))) + roles_to_ping = list( + dict.fromkeys( + configuration.get_config_entry(channel.guild.id, "modmail_roles_to_ping") + ) + ) if message and roles_to_ping: for role_id in roles_to_ping: role_string += f"<@&{role_id}> " @@ -548,7 +556,9 @@ async def create_thread( await message.author.send(embed=embed) # - Auto responses - - automatic_responses = configuration.get_config_entry(channel.guild.id, "modmail_automatic_responses") + automatic_responses = configuration.get_config_entry( + channel.guild.id, "modmail_automatic_responses" + ) for regex in automatic_responses: if re.match(regex, message.content): await reply_to_thread( @@ -1346,7 +1356,9 @@ async def modmail_ban( # Checking against the user to see if they have the roles specified in the config user_roles = getattr(user, "roles", []) - unparsed_roles = configuration.get_config_entry(ctx.guild.id, "modmail_modmail_roles") + unparsed_roles = configuration.get_config_entry( + ctx.guild.id, "modmail_modmail_roles" + ) modmail_roles = list(dict.fromkeys(unparsed_roles)) # No error has to be thrown if unparsed_roles is None, it's already checked in diff --git a/techsupport_bot/commands/news.py b/techsupport_bot/commands/news.py index d71cb073c..c3982a1b0 100644 --- a/techsupport_bot/commands/news.py +++ b/techsupport_bot/commands/news.py @@ -11,7 +11,7 @@ import discord import munch from botlogging import LogContext, LogLevel -from core import cogs, extensionconfig +from core import cogs from discord import app_commands if TYPE_CHECKING: @@ -35,38 +35,7 @@ async def setup(bot: bot.TechSupportBot) -> None: except AttributeError as exc: raise AttributeError("News was not loaded due to missing API key") from exc - config = extensionconfig.ExtensionConfig() - config.add( - key="channel", - datatype="int", - title="Daily News Channel ID", - description="The ID of the channel the news should appear in", - default=None, - ) - config.add( - key="cron_config", - datatype="string", - title="Cronjob config for news", - description="Crontab syntax for executing news events (example: 0 17 * * *)", - default="0 17 * * *", - ) - config.add( - key="country", - datatype="string", - title="Country code", - description="Country code to receive news for (example: US)", - default="US", - ) - config.add( - key="category", - datatype="str", - title="Category", - description="The category to use when receiving cronjob headlines", - default=None, - ) - await bot.add_cog(News(bot=bot, extension_name="news")) - bot.add_extension_config("news", config) class Category(enum.Enum): @@ -180,16 +149,19 @@ async def execute(self: Self, guild: discord.Guild) -> None: Args: guild (discord.Guild): The guild where the loop is running """ - config = self.bot.guild_configs[str(guild.id)] - channel = guild.get_channel(int(config.extensions.news.channel.value)) + channel = guild.get_channel( + int(configuration.get_config_entry(guild.id, "news_channel")) + ) if not channel: return url = None while not url: article = await self.get_random_headline( - config.extensions.news.country.value, - Category(config.extensions.news.category.value).value, + configuration.get_config_entry(guild.id, "news_country"), + Category( + configuration.get_config_entry(guild.id, "news_category") + ).value, ) url = article.get("url") @@ -213,8 +185,9 @@ async def wait(self: Self, guild: discord.Guild) -> None: Args: guild (discord.Guild): The guild where the loop will occur """ - config = self.bot.guild_configs[str(guild.id)] - await aiocron.crontab(config.extensions.news.cron_config.value).next() + await aiocron.crontab( + configuration.get_config_entry(guild.id, "news_cron_config") + ).next() @app_commands.command( name="news", @@ -238,12 +211,12 @@ async def news_command( else: category.lower() - config = self.bot.guild_configs[str(interaction.guild.id)] - url = None while not url: article = await self.get_random_headline( - config.extensions.news.country.value, category, True + configuration.get_config_entry(interaction.guild.id, "news_country"), + category, + True, ) url = article.get("url") diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index 0a25fb8c8..98d6507be 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -5,8 +5,9 @@ import datetime from typing import TYPE_CHECKING, Self +import configuration import discord -from core import auxiliary, cogs, extensionconfig, moderation +from core import auxiliary, cogs, moderation from discord import app_commands if TYPE_CHECKING: @@ -19,16 +20,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - config = extensionconfig.ExtensionConfig() - config.add( - key="max_purge_amount", - datatype="int", - title="Max Purge Amount", - description="The max amount of messages allowed to be purged in one command", - default=50, - ) await bot.add_cog(Purger(bot=bot, extension_name="purge")) - bot.add_extension_config("purge", config) class Purger(cogs.BaseCog): @@ -54,13 +46,14 @@ async def purge_command( amount (int): The max amount of messages to purge duration_minutes (int, optional): The max age of a message to purge. Defaults to None. """ - config = self.bot.guild_configs[str(interaction.guild.id)] + max_purge_amount = configuration.get_config_entry( + interaction.guild.id, "purge_max_purge_amount" + ) - if amount <= 0 or amount > config.extensions.purge.max_purge_amount.value: + if amount <= 0 or amount > max_purge_amount: embed = auxiliary.prepare_deny_embed( message=( - "Messages to purge must be between 1 " - f"and {config.extensions.purge.max_purge_amount.value}" + "Messages to purge must be between 1 " f"and {max_purge_amount}" ), ) await interaction.response.send_message(embed=embed, ephemeral=True) diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index 1d25c2dfb..17d95a92b 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -6,8 +6,9 @@ import re from typing import TYPE_CHECKING, Self +import configuration import discord -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord import app_commands if TYPE_CHECKING: @@ -20,30 +21,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - config = extensionconfig.ExtensionConfig() - config.add( - key="alert_channel", - datatype="int", - title="Alert channel ID", - description="The ID of the channel to send auto-protect alerts to", - default=None, - ) - config.add( - key="anonymous", - datatype="bool", - title="If reports are anonymous", - description="Whether reports are anonymous", - default=False, - ) - config.add( - key="ping_role", - datatype="int", - title="New report ping role", - description="The ID of the role to ping when a new report is created", - default=None, - ) await bot.add_cog(Report(bot=bot, extension_name="report")) - bot.add_extension_config("report", config) class Report(cogs.BaseCog): @@ -76,7 +54,9 @@ async def report_command( config = self.bot.guild_configs[str(interaction.guild.id)] - is_anonymous = config.extensions.report.anonymous.value + is_anonymous = configuration.get_config_entry( + interaction.guild.id, "report_anonymous" + ) if is_anonymous: embed.set_author(name="Anonymous") @@ -135,7 +115,11 @@ async def report_command( try: alert_channel = interaction.guild.get_channel( - int(config.extensions.report.alert_channel.value) + int( + configuration.get_config_entry( + interaction.guild.id, "report_alert_channel" + ) + ) ) except TypeError: alert_channel = None @@ -147,7 +131,11 @@ async def report_command( await interaction.response.send_message(embed=user_embed, ephemeral=True) return - role = interaction.guild.get_role(int(config.extensions.report.ping_role.value)) + role = interaction.guild.get_role( + int( + configuration.get_config_entry(interaction.guild.id, "report_ping_role") + ) + ) await alert_channel.send( content=role.mention, diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index 729237d03..c672cb47d 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -83,5 +83,21 @@ "modmail_automatic_rejections": {}, "modmail_automatic_responses": {}, "modmail_roles_to_ping": [], - "modmail_thread_creation_message": "Create modmail thread?" + "modmail_thread_creation_message": "Create modmail thread?", + "news_category": "", + "news_channel": "", + "news_country": "US", + "news_cron_config": "0 17 * * *", + "notes_note_bypass": "", + "notes_note_readers": [], + "notes_note_role": [], + "notes_note_writers": [], + "paste_bypass_roles": [], + "paste_channels": [], + "paste_length_limit": 500, + "paste_paste_footer_message": "Note: Long messages are automatically pasted", + "purge_max_purge_amount": 50, + "report_alert_channel": "", + "report_anonymous": false, + "report_ping_role": "" } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index 36209b7c6..547042272 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -338,6 +338,70 @@ "modmail_thread_creation_message": { "datatype": "str", "description": "The message sent to the user when confirming a thread creation." + }, + "news_category": { + "datatype": "str", + "description": "The category to use when receiving cronjob headlines" + }, + "news_channel": { + "datatype": "discord.TextChannel", + "description": "The ID of the channel the news should appear in" + }, + "news_country": { + "datatype": "str", + "description": "Country code to receive news for (example: US)" + }, + "news_cron_config": { + "datatype": "CronTab", + "description": "Crontab syntax for executing news events" + }, + "notes_note_bypass": { + "datatype": "list[discord.Role]", + "description": "A list of roles that shouldn't have notes set or the note role assigned" + }, + "notes_note_readers": { + "datatype": "list[discord.Role]", + "description": "Users with roles in this list will be able to use whois" + }, + "notes_note_role": { + "datatype": "discord.Role", + "description": "The name of the role to be added when a note is added to a user" + }, + "notes_note_writers": { + "datatype": "list[discord.Role]", + "description": "Users with roles in this list will be able to create or delete notes" + }, + "paste_bypass_roles": { + "datatype": "list[discord.Role]", + "description": "The list of role names that will bypass the paste system" + }, + "paste_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The list of channel ID's that will have the paste service enabled" + }, + "paste_length_limit": { + "datatype": "int", + "description": "The max char limit on messages before they trigger a paste" + }, + "paste_paste_footer_message": { + "datatype": "str", + "description": "The message used on the footer of the large message paste embed" + }, + "purge_max_purge_amount": { + "datatype": "int", + "description": "The max amount of messages allowed to be purged in one command" + }, + "report_alert_channel": { + "datatype": "discord.TextChannel", + "description": "The ID of the channel to send reports to" + }, + "report_anonymous": { + "datatype": "bool", + "description": "Whether reports are anonymous" + }, + "report_ping_role": { + "datatype": "discord.Role", + "description": "The ID of the role to ping when a new report is created" } } diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index a3b970606..875f9cbc7 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -7,9 +7,8 @@ import configuration import discord -import munch from botlogging import LogContext, LogLevel -from core import cogs, extensionconfig +from core import cogs from discord.ext import commands from functions import automod @@ -23,44 +22,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - config = extensionconfig.ExtensionConfig() - config.add( - key="channels", - datatype="list", - title="Protected channels", - description=( - "The list of channel ID's associated with the channels to auto-protect" - ), - default=[], - ) - config.add( - key="bypass_roles", - datatype="list", - title="Bypassed role names", - description=( - "The list of role names associated with bypassed roles by the auto-protect" - ), - default=[], - ) - config.add( - key="length_limit", - datatype="int", - title="Max length limit", - description=( - "The max char limit on messages before they trigger an action by" - " auto-protect" - ), - default=500, - ) - config.add( - key="paste_footer_message", - datatype="str", - title="The linx embed footer", - description="The message used on the footer of the large message paste URL", - default="Note: Long messages are automatically pasted", - ) await bot.add_cog(Paster(bot=bot, extension_name="paste")) - bot.add_extension_config("paste", config) class Paster(cogs.MatchCog): @@ -76,9 +38,10 @@ async def match(self: Self, ctx: commands.Context, content: str) -> bool: Returns: bool: Whether the message should be inspected for a paste """ - config = self.bot.guild_configs[str(ctx.guild.id)] # exit the match based on exclusion parameters - if not str(ctx.channel.id) in config.extensions.paste.channels.value: + if not str(ctx.channel.id) in configuration.get_config_entry( + ctx.guild.id, "paste_channels" + ): await self.bot.logger.send_log( message="Channel not in protected channels - ignoring protect check", level=LogLevel.DEBUG, @@ -90,7 +53,9 @@ async def match(self: Self, ctx: commands.Context, content: str) -> bool: if any( role_name.lower() in role_names - for role_name in config.extensions.paste.bypass_roles.value + for role_name in configuration.get_config_entry( + ctx.guild.id, "paste_bypass_roles" + ) ): return False @@ -109,10 +74,12 @@ async def response( content (str): The string content of the message result (bool): What the match() function returned """ - config = self.bot.guild_configs[str(ctx.guild.id)] - if len(content) > config.extensions.paste.length_limit.value or content.count( - "\n" - ) > self.max_newlines(config.extensions.paste.length_limit.value): + length_limit = configuration.get_config_entry( + ctx.guild.id, "paste_length_limit" + ) + if len(content) > length_limit or content.count("\n") > self.max_newlines( + length_limit + ): if "automod" in configuration.get_config_entry( ctx.guild.id, "core_enabled_extensions" ): @@ -244,7 +211,6 @@ async def create_linx_embed( Returns: discord.Embed | None: The formatted embed, or None if there was an API error """ - config = self.bot.guild_configs[str(ctx.guild.id)] if not content: return None @@ -272,7 +238,11 @@ async def create_linx_embed( embed.set_author( name=f"Paste by {ctx.author}", icon_url=ctx.author.display_avatar.url ) - embed.set_footer(text=config.extensions.paste.paste_footer_message.value) + embed.set_footer( + text=configuration.get_config_entry( + ctx.guild.id, "paste_paste_footer_message" + ) + ) embed.color = discord.Color.blue() return embed From 9348e85307bcf6f61fc4bb48db3193412d60fe83 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:08:25 -0700 Subject: [PATCH 26/40] Finish migrating config to new system, clear out extensionconfig --- documentation/Extension-howto.md | 32 - documentation/core/bot.md | 4 - techsupport_bot/bot.py | 71 +- techsupport_bot/commands/factoids.py | 42 +- techsupport_bot/commands/notes.py | 67 +- techsupport_bot/commands/report.py | 2 - techsupport_bot/commands/role.py | 55 +- techsupport_bot/commands/urban.py | 18 +- techsupport_bot/commands/voting.py | 39 +- techsupport_bot/commands/xp.py | 42 +- .../configuration/config.default.json | 69 +- .../configuration/config.meta.json | 869 +++++++++--------- techsupport_bot/core/extensionconfig.py | 42 - techsupport_bot/core/moderation.py | 7 +- techsupport_bot/functions/automod.py | 50 +- techsupport_bot/functions/events.py | 93 +- techsupport_bot/functions/logger.py | 12 +- 17 files changed, 612 insertions(+), 902 deletions(-) delete mode 100644 techsupport_bot/core/extensionconfig.py diff --git a/documentation/Extension-howto.md b/documentation/Extension-howto.md index 2a634980d..9422f4ae6 100644 --- a/documentation/Extension-howto.md +++ b/documentation/Extension-howto.md @@ -10,45 +10,13 @@ async def setup(bot): ``` This code is run when loading the extension, is used to add model classes and config entries. -## Config entries - -```py -from core import extensionconfig -config = extensionconfig.ExtensionConfig() -config.add( - key="", - datatype="": { - "": { - "datatype": "", - "title": "", - "description": "<description>", - "default": "<default>", - "value": "<default>" - } -} -``` -NOTE: The entry might not automatically get added to the file and will have to be added in manually according to the template above. - - ## Registering the extension ```py await bot.add_cog(extension-name(bot=bot)) -bot.add_extension_config("extension-name", config) ``` This registers the extension, assumes the extension name is the filename if the `extension_name` argument wasn't supplied. -The second line adds the extension to the config .json file. - ## Optional: Postgres This defines any database models used in this extension. diff --git a/documentation/core/bot.md b/documentation/core/bot.md index 6cc160286..874c2a4af 100644 --- a/documentation/core/bot.md +++ b/documentation/core/bot.md @@ -27,10 +27,6 @@ The bot.py file is the primary file of the program. It contains all of the core ### write_new_config -### add_extension_config - -### get_log_channel_from_guild - ## File config loading functions ### load_file_config diff --git a/techsupport_bot/bot.py b/techsupport_bot/bot.py index 6cb7ce77f..ca4879154 100644 --- a/techsupport_bot/bot.py +++ b/techsupport_bot/bot.py @@ -22,7 +22,7 @@ import ui import yaml from botlogging import LogContext, LogLevel -from core import auxiliary, custom_errors, databases, extensionconfig, http +from core import auxiliary, custom_errors, databases, http from discord import app_commands from discord.ext import commands @@ -392,53 +392,6 @@ async def write_new_config(self: Self, guild_id: str, config: str) -> None: ) await new_database_config.create() - def add_extension_config( - self: Self, extension_name: str, config: extensionconfig.ExtensionConfig - ) -> None: - """Adds an extensions defined config to the guild config as a whole - - Args: - extension_name (str): The name of the extension to add config for. - Will be the key in the config file - config (extensionconfig.ExtensionConfig): The config class with all - of the config keys to add - - Raises: - ValueError: Will be raised if config is not an extensionconfig.ExtensionConfig - """ - if not isinstance(config, extensionconfig.ExtensionConfig): - raise ValueError("config must be of type extensionconfig.ExtensionConfig") - self.extension_configs[extension_name] = config - - async def get_log_channel_from_guild( - self: Self, guild: discord.Guild, key: str - ) -> str | None: - """Gets the log channel ID associated with the given guild. - - This also checks if the channel exists in the correct guild. - - Args: - guild (discord.Guild): the guild object to reference - key (str): the key to use when looking up the channel - - Returns: - str | None: If the log channel exists, this will be the string of the ID - Otherwise it will be None - """ - if not guild: - return None - - config = self.guild_configs[str(guild.id)] - channel_id = config.get(key) - - if not channel_id: - return None - - if not guild.get_channel(int(channel_id)): - return None - - return channel_id - # File config loading functions def load_file_config(self: Self, validate: bool = True) -> None: @@ -874,7 +827,6 @@ def command_run_rate_limit_check( bool: True if the command should be run, False if under rate limit """ # Assume this is only run if rate limit is enabled - config = self.guild_configs[str(guild.id)] identifier = f"{member.id}-{guild.id}" # If this person hasn't run a command in the rate_limit.time @@ -882,7 +834,9 @@ def command_run_rate_limit_check( if identifier not in self.command_execute_history: self.command_execute_history[identifier] = expiringdict.ExpiringDict( max_len=20, - max_age_seconds=config.rate_limit.time, + max_age_seconds=configuration.get_config_entry( + guild.id, "rate_limit_time" + ), ) # Ensure that a single command is only ever counted once @@ -890,7 +844,9 @@ def command_run_rate_limit_check( self.command_execute_history[identifier][command_id] = True # Ban the person if they are over the rate limit - if len(self.command_execute_history[identifier]) > config.rate_limit.commands: + if len( + self.command_execute_history[identifier] + ) > configuration.get_config_entry(guild.id, "rate_limit_commands"): self.command_rate_limit_bans[identifier] = True # If this person is banned, raise an error @@ -916,8 +872,9 @@ def command_run_extension_disabled_check( Returns: bool: False if disabled, True if enabled """ - config = self.guild_configs[str(guild.id)] - if extension_name not in config.enabled_extensions: + if extension_name not in configuration.get_config_entry( + guild.id, "core_enabled_extensions" + ): return False return True @@ -949,8 +906,6 @@ async def interaction_check(self: Self, interaction: discord.Interaction) -> boo context=LogContext(guild=interaction.guild, channel=interaction.channel), console_only=True, ) - config = self.guild_configs[str(interaction.guild.id)] - # Check 1 - Ensure extension is enabled try: extension_name = interaction.command.extras["module"] @@ -975,7 +930,7 @@ async def interaction_check(self: Self, interaction: discord.Interaction) -> boo # Check 3 - If rate limiter is enabled, run through the rate limiter # If the user is under a rate limit, raise an error to show it and block execution - if config.rate_limit.get("enabled", False): + if configuration.get_config_entry(interaction.guild.id, "rate_limit_enabled"): if not self.command_run_rate_limit_check( member=interaction.user, guild=interaction.guild, @@ -1048,8 +1003,6 @@ async def can_run( context=LogContext(guild=ctx.guild, channel=ctx.channel), console_only=True, ) - config = self.guild_configs[str(ctx.guild.id)] - # Check 1 - Ensure extension is enabled extension_name = self.get_command_extension_name(ctx.command) if extension_name: @@ -1063,7 +1016,7 @@ async def can_run( return result # Check 3 - If rate limiter is enabled, run through the rate limiter - if config.rate_limit.get("enabled", False): + if configuration.get_config_entry(ctx.guild.id, "rate_limit_enabled"): # If the user is under a rate limit, raise an error to show it and block execution if not self.command_run_rate_limit_check( member=ctx.author, guild=ctx.guild, command_id=ctx.message.id diff --git a/techsupport_bot/commands/factoids.py b/techsupport_bot/commands/factoids.py index 1d7dbb5ca..8426cb24d 100644 --- a/techsupport_bot/commands/factoids.py +++ b/techsupport_bot/commands/factoids.py @@ -32,7 +32,7 @@ import yaml from aiohttp.client_exceptions import InvalidURL from botlogging import LogContext, LogLevel -from core import auxiliary, cogs, custom_errors, extensionconfig +from core import auxiliary, cogs, custom_errors from discord import app_commands from discord.ext import commands from functions import logger as function_logger @@ -47,52 +47,12 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - - # Sets up the config - config = extensionconfig.ExtensionConfig() - config.add( - key="manage_roles", - datatype="list", - title="Manage factoids roles", - description="The roles required to manage factoids", - default=["Factoids"], - ) - config.add( - key="admin_roles", - datatype="list", - title="Admin factoids roles", - description="The roles required to administrate factoids", - default=["Admin"], - ) - config.add( - key="prefix", - datatype="str", - title="Factoid prefix", - description="Prefix for calling factoids", - default="?", - ) - config.add( - key="restricted_list", - datatype="list", - title="Restricted channels list", - description="List of channel IDs that restricted factoids are allowed to be used in", - default=[], - ) - config.add( - key="disable_embeds", - datatype="bool", - title="Force disable embeds, for debug purposes", - description="This will force all factoids to not use embeds.", - default=False, - ) - await bot.add_cog( FactoidManager( bot=bot, extension_name="factoids", ) ) - bot.add_extension_config("factoids", config) async def has_manage_factoids_role(ctx: commands.Context) -> bool: diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 0588f4be3..5eaeb3f28 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -8,7 +8,7 @@ import discord import ui from botlogging import LogContext, LogLevel -from core import auxiliary, cogs, extensionconfig, moderation +from core import auxiliary, cogs, moderation from discord import app_commands from discord.ext import commands @@ -22,41 +22,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - - config = extensionconfig.ExtensionConfig() - config.add( - key="note_role", - datatype="str", - title="Note role", - description="The name of the role to be added when a note is added to a user", - default=None, - ) - config.add( - key="note_bypass", - datatype="list", - title="Note bypass list", - description=( - "A list of roles that shouldn't have notes set or the note role assigned" - ), - default=["Moderator"], - ) - config.add( - key="note_readers", - datatype="list", - title="Note Reader Roles", - description="Users with roles in this list will be able to use whois", - default=[], - ) - config.add( - key="note_writers", - datatype="list", - title="Note Writer Roles", - description="Users with roles in this list will be able to create or delete notes", - default=[], - ) - await bot.add_cog(Notes(bot=bot, extension_name="notes")) - bot.add_extension_config("notes", config) async def is_reader(interaction: discord.Interaction) -> bool: @@ -75,8 +41,9 @@ async def is_reader(interaction: discord.Interaction) -> bool: bool: True if the user can run, False if they cannot """ - config = interaction.client.guild_configs[str(interaction.guild.id)] - if reader_roles := config.extensions.notes.note_readers.value: + if reader_roles := configuration.get_config_entry( + interaction.guild.id, "notes_note_readers" + ): roles = ( discord.utils.get(interaction.guild.roles, name=role) for role in reader_roles @@ -107,8 +74,9 @@ async def is_writer(interaction: discord.Interaction) -> bool: Returns: bool: True if the user can run, False if they cannot """ - config = interaction.client.guild_configs[str(interaction.guild.id)] - if writer_roles := config.extensions.notes.note_writers.value: + if writer_roles := configuration.get_config_entry( + interaction.guild.id, "notes_note_writers" + ): roles = ( discord.utils.get(interaction.guild.roles, name=role) for role in writer_roles @@ -172,10 +140,10 @@ async def set_note( body=body, ) - config = self.bot.guild_configs[str(interaction.guild.id)] - # Check to make sure notes are allowed to be assigned - for name in config.extensions.notes.note_bypass.value: + for name in configuration.get_config_entry( + interaction.guild.id, "notes_note_bypass" + ): role_check = discord.utils.get(interaction.guild.roles, name=name) if not role_check: continue @@ -190,7 +158,10 @@ async def set_note( await note.create() role = discord.utils.get( - interaction.guild.roles, name=config.extensions.notes.note_role.value + interaction.guild.roles, + name=configuration.get_config_entry( + interaction.guild.id, "notes_note_role" + ), ) if not role: @@ -260,9 +231,11 @@ async def clear_notes( for note in notes: await note.delete() - config = self.bot.guild_configs[str(interaction.guild.id)] role = discord.utils.get( - interaction.guild.roles, name=config.extensions.notes.note_role.value + interaction.guild.roles, + name=configuration.get_config_entry( + interaction.guild.id, "notes_note_role" + ), ) if role: await user.remove_roles( @@ -311,12 +284,12 @@ async def on_member_join(self: Self, member: discord.Member) -> None: Args: member (discord.Member): The member who has just joined """ - config = self.bot.guild_configs[str(member.guild.id)] if not self.extension_enabled(member.guild): return role = discord.utils.get( - member.guild.roles, name=config.extensions.notes.note_role.value + member.guild.roles, + name=configuration.get_config_entry(member.guild.id, "notes_note_role"), ) if not role: return diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index 17d95a92b..959b7ef9d 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -52,8 +52,6 @@ async def report_command( embed = discord.Embed(title="New Report", description=report_str) embed.color = discord.Color.red() - config = self.bot.guild_configs[str(interaction.guild.id)] - is_anonymous = configuration.get_config_entry( interaction.guild.id, "report_anonymous" ) diff --git a/techsupport_bot/commands/role.py b/techsupport_bot/commands/role.py index 00e827127..448bdd615 100644 --- a/techsupport_bot/commands/role.py +++ b/techsupport_bot/commands/role.py @@ -5,9 +5,10 @@ from typing import TYPE_CHECKING, Self +import configuration import discord import ui -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord import app_commands if TYPE_CHECKING: @@ -20,37 +21,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object """ - config = extensionconfig.ExtensionConfig() - config.add( - key="allow_self_assign", - datatype="list", - title="List of roles allowed to use /role self", - description="The list of roles that are allowed to assign themselves roles", - default=[], - ) - config.add( - key="all_assignable_roles", - datatype="list", - title="Roles moderators can assign", - description="The list of roles by name that moderators can assign to people", - default=[], - ) - config.add( - key="allow_all_assign", - datatype="list", - title="List of roles allowed to use /role manage", - description="The list of roles that are allowed to assign others roles", - default=[], - ) - config.add( - key="self_assign_map", - datatype="dict", - title="Map of all the roles allowed in self assign", - description="A map in the format target: [use, use]", - default={}, - ) await bot.add_cog(RoleGiver(bot=bot)) - bot.add_extension_config("role", config) class RoleGiver(cogs.BaseCog): @@ -89,14 +60,13 @@ async def self_role(self: Self, interaction: discord.Interaction) -> None: Args: interaction (discord.Interaction): The interaction that called this command """ - # Pull config - config = self.bot.guild_configs[str(interaction.guild.id)] - # Interaction user roles current_roles = getattr(interaction.user, "roles", []) # Get the roles map - roles_map = config.extensions.role.self_assign_map.value + roles_map = configuration.get_config_entry( + interaction.guild.id, "role_self_assign_map" + ) allowed_roles_list = [] @@ -118,7 +88,9 @@ async def self_role(self: Self, interaction: discord.Interaction) -> None: return # Get needed config items - allowed_to_execute = config.extensions.role.allow_self_assign.value + allowed_to_execute = configuration.get_config_entry( + interaction.guild.id, "role_allow_self_assign" + ) # Call the base function await self.role_command_base( @@ -151,12 +123,13 @@ async def assign_role_command( interaction (discord.Interaction): The interaction that triggered this member (discord.Member): The member to modify roles of """ - # Pull config - config = self.bot.guild_configs[str(interaction.guild.id)] - # Get needed config items - roles = config.extensions.role.all_assignable_roles.value - allowed_to_execute = config.extensions.role.allow_all_assign.value + roles = configuration.get_config_entry( + interaction.guild.id, "role_all_assignable_roles" + ) + allowed_to_execute = configuration.get_config_entry( + interaction.guild.id, "role_allow_all_assign" + ) # Call the base function await self.role_command_base(interaction, roles, allowed_to_execute, member) diff --git a/techsupport_bot/commands/urban.py b/techsupport_bot/commands/urban.py index c1d44e762..a3334e935 100644 --- a/techsupport_bot/commands/urban.py +++ b/techsupport_bot/commands/urban.py @@ -4,9 +4,10 @@ from typing import TYPE_CHECKING, Self +import configuration import discord import ui -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: @@ -19,17 +20,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="max_responses", - datatype="int", - title="Max Responses", - description="The max amount of responses per embed page", - default=1, - ) - await bot.add_cog(UrbanDictionary(bot=bot)) - bot.add_extension_config("urban", config) class UrbanDictionary(cogs.BaseCog): @@ -68,8 +59,6 @@ async def urban(self: Self, ctx: commands.Context, *, query: str) -> None: ) definitions = response.get("list") - config = self.bot.guild_configs[str(ctx.guild.id)] - if not definitions: await auxiliary.send_deny_embed( message=f"No results found for: *{query}*", channel=ctx.channel @@ -100,7 +89,8 @@ async def urban(self: Self, ctx: commands.Context, *, query: str) -> None: inline=False, ) if ( - field_counter == config.extensions.urban.max_responses.value + field_counter + == configuration.get_config_entry(ctx.guild.id, "urban_max_responses") or index == len(definitions) - 1 ): embed.set_thumbnail(url=self.ICON_URL) diff --git a/techsupport_bot/commands/voting.py b/techsupport_bot/commands/voting.py index bd6d92c7a..175a08896 100644 --- a/techsupport_bot/commands/voting.py +++ b/techsupport_bot/commands/voting.py @@ -18,7 +18,7 @@ import discord import munch import ui -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord import app_commands if TYPE_CHECKING: @@ -31,44 +31,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot to register the cog to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="votes_channel_roles", - datatype="dict[str, list[str]]", - title="Votes channels → allowed roles", - description=( - "Map of forum channel IDs to a list of role IDs. " - "User must have at least one role from the list." - ), - default={}, - ) - config.add( - key="active_role_id", - datatype="str", - title="Active voter role", - description="User must have this role to start or participate in votes", - default="", - ) - config.add( - key="voting_thresholds", - datatype="list[int]", - title="The 3 percentage thresholds for voting pass/fail", - description=( - "1, % of eligible voters who must vote yes" - "2, % of yes/no voters who must have voted yes" - "3, % of eligible voters who must have voted anything at all" - ), - default=[50, 67, 75], - ) - config.add( - key="reminders_at", - datatype="list[int]", - title="The list of hours remaining in vote to remind non voters", - description="The list of hours remaining in vote to remind non voters", - default=[36, 6], - ) await bot.add_cog(Voting(bot=bot, extension_name="voting")) - bot.add_extension_config("voting", config) class Voting(cogs.LoopCog): diff --git a/techsupport_bot/commands/xp.py b/techsupport_bot/commands/xp.py index c99b35722..2197d552c 100644 --- a/techsupport_bot/commands/xp.py +++ b/techsupport_bot/commands/xp.py @@ -8,7 +8,7 @@ import configuration import discord import expiringdict -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord import app_commands from discord.ext import commands @@ -22,31 +22,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="categories_counted", - datatype="list", - title="List of category IDs to count for XP", - description="List of category IDs to count for XP", - default=[], - ) - config.add( - key="excluded_channels", - datatype="list", - title="List of channel IDs to exclude for XP", - description="List of channel IDs to exclude for XP", - default=[], - ) - config.add( - key="level_roles", - datatype="dict", - title="Dict of levels in XP:Role ID.", - description="Dict of levels in XP:Role ID", - default={}, - ) - await bot.add_cog(LevelXP(bot=bot, extension_name="xp")) - bot.add_extension_config("xp", config) class LevelXP(cogs.MatchCog): @@ -132,7 +108,6 @@ async def match(self: Self, ctx: commands.Context, _: str) -> bool: Returns: bool: True if XP should be granted, False if it shouldn't be. """ - config = self.bot.guild_configs[str(ctx.guild.id)] # Ignore all bot messages if ctx.message.author.bot: return False @@ -142,11 +117,15 @@ async def match(self: Self, ctx: commands.Context, _: str) -> bool: return False # Ignore messages outside of tracked categories - if ctx.channel.category_id not in config.extensions.xp.categories_counted.value: + if ctx.channel.category_id not in configuration.get_config_entry( + ctx.guild.id, "xp_categories_counted" + ): return False # Ignore messages in exlucded channels - if ctx.channel.id in config.extensions.xp.excluded_channels.value: + if ctx.channel.id in configuration.get_config_entry( + ctx.guild.id, "xp_excluded_channels" + ): return False # Ignore messages that are too short @@ -206,8 +185,7 @@ async def apply_level_ups(self: Self, user: discord.Member, new_xp: int) -> None user (discord.Member): The user who just gained XP new_xp (int): The new amount of XP the user has """ - config = self.bot.guild_configs[str(user.guild.id)] - levels = config.extensions.xp.level_roles.value + levels = configuration.get_config_entry(user.guild.id, "xp_level_roles") if len(levels) == 0: return @@ -304,9 +282,7 @@ async def get_current_XP_role(bot: object, user: discord.Member) -> discord.Role Returns: discord.Role: The XP role that the user currently has """ - - config = bot.guild_configs[str(user.guild.id)] - levels = config.extensions.xp.level_roles.value + levels = configuration.get_config_entry(user.guild.id, "xp_level_roles") if len(levels) == 0: return None diff --git a/techsupport_bot/configuration/config.default.json b/techsupport_bot/configuration/config.default.json index c672cb47d..f578e87e3 100644 --- a/techsupport_bot/configuration/config.default.json +++ b/techsupport_bot/configuration/config.default.json @@ -9,27 +9,39 @@ "application_ping_role": "", "application_reminder_cron_config": "0 17 * * *", "automod_alert_channel": "", - "autoreact_react_map": { "hello": "👋" }, + "automod_banned_file_extensions": ["exe"], + "automod_banned_file_hashes": [], + "automod_bypass_roles": [], + "automod_channels": [], + "automod_max_mentions": 3, + "automod_string_map": {}, + "autoreact_react_map": {"hello": "👋"}, "core_command_prefix": ".", "core_enable_logging": true, + "core_enabled_extensions": ["config", "extension"], + "core_guild_events_channel": "", + "core_guild_id": "", + "core_logging_channel": "", + "core_member_events_channel": "", "core_nickname_filter": false, "core_private_channels": [], - "core_guild_id": "", "duck_allow_manipulation": true, "duck_cooldown": 5, + "duck_hunt_channels": [], "duck_max_wait": 4, "duck_min_wait": 2, "duck_mute_for_cooldown": true, "duck_spawn_user": [], + "duck_success_rate": 50, "duck_timeout": 60, "duck_use_category": false, "dumpdbg_roles": [], "embed_embed_roles": [], - "core_enabled_extensions": ["config", "extension"], - "core_logging_channel": "", - "modmail_modmail_roles": [], - "duck_hunt_channels": [], - "duck_success_rate": 50, + "factoids_admin_roles": [], + "factoids_disable_embeds": false, + "factoids_manage_roles": [], + "factoids_prefix": "?", + "factoids_restricted_list": [], "forum_abandoned_message": "thread abandoned", "forum_body_regex_list": [], "forum_close_message": "thread closed", @@ -43,19 +55,6 @@ "forum_staff_role_ids": [], "forum_title_regex_list": [], "forum_welcome_message": "thread welcome", - "voting_active_role_id": "", - "voting_reminders_at": [36, 6], - "voting_votes_channel_roles": {}, - "voting_voting_thresholds": [50, 67, 75], - "automod_banned_file_extensions": ["exe"], - "automod_banned_file_hashes": [], - "automod_bypass_roles": [], - "automod_channels": [], - "automod_max_mentions": 3, - "automod_string_map": {}, - "moderation_max_warnings": 3, - "moderator_ban_delete_duration": 7, - "logger_channel_map": {}, "gate_channel": "", "gate_delete_wait": 60, "gate_intro_message": "Welcome to our server! 👋 Please read the rules then type agree below to verify yourself", @@ -66,22 +65,21 @@ "grab_per_page": 3, "hangman_hangman_roles": [], "honeypot_channels": [], - "moderation_alert_channel": "", "joke_apply_in_nsfw_channels": false, - "joke_blacklisted_filters": ["nsfw", "explicit"], - "factoids_admin_roles": [], - "factoids_disable_embeds": false, - "factoids_manage_roles": [], - "factoids_prefix": "?", - "factoids_restricted_list": [], + "joke_blacklisted_filters": ["explicit", "nsfw"], "kanye_channel": "", "kanye_max_wait": 48, "kanye_min_wait": 24, + "logger_channel_map": {}, + "moderation_alert_channel": "", + "moderation_max_warnings": 3, + "moderator_ban_delete_duration": 7, "moderator_immune_roles": [], "modlog_alert_channel": "", "modmail_aliases": {}, "modmail_automatic_rejections": {}, "modmail_automatic_responses": {}, + "modmail_modmail_roles": [], "modmail_roles_to_ping": [], "modmail_thread_creation_message": "Create modmail thread?", "news_category": "", @@ -97,7 +95,22 @@ "paste_length_limit": 500, "paste_paste_footer_message": "Note: Long messages are automatically pasted", "purge_max_purge_amount": 50, + "rate_limit_commands": 4, + "rate_limit_enabled": false, + "rate_limit_time": 10, "report_alert_channel": "", "report_anonymous": false, - "report_ping_role": "" + "report_ping_role": "", + "role_all_assignable_roles": [], + "role_allow_all_assign": [], + "role_allow_self_assign": [], + "role_self_assign_map": {}, + "urban_max_responses": 1, + "voting_active_role_id": "", + "voting_reminders_at": [36, 6], + "voting_votes_channel_roles": {}, + "voting_voting_thresholds": [50, 67, 75], + "xp_categories_counted": [], + "xp_excluded_channels": [], + "xp_level_roles": {} } diff --git a/techsupport_bot/configuration/config.meta.json b/techsupport_bot/configuration/config.meta.json index 547042272..c543e9a65 100644 --- a/techsupport_bot/configuration/config.meta.json +++ b/techsupport_bot/configuration/config.meta.json @@ -1,413 +1,458 @@ { - "application_application_message": { - "datatype": "str", - "description": "The message to show users when they are prompted to apply in the notification_channels" - }, - "application_application_role_id": { - "datatype": "discord.Role", - "description": "The role to give applicants when their application is approved" - }, - "application_manage_role_ids": { - "datatype": "list[discord.Role]", - "description": "The roles required to manage the applications (not required to apply)" - }, - "application_management_channel": { - "datatype": "discord.TextChannel", - "description": "The channel the application notifications and reminders should appear in" - }, - "application_max_age": { - "datatype": "int", - "description": "After this many days, the system will auto reject the applications" - }, - "application_notification_channels": { - "datatype": "list[discord.TextChannel]", - "description": "The channels that should receive periodic messages about the application, with a button to apply" - }, - "application_notification_cron_config": { - "datatype": "CronTab", - "description": "CronTab for users being notified about the application" - }, - "application_ping_role": { - "datatype": "discord.Role", - "description": "The role to ping when a new application is created" - }, - "application_reminder_cron_config": { - "datatype": "CronTab", - "description": "Crontab for executing pending reminder events" - }, - "automod_alert_channel": { - "datatype": "discord.TextChannel", - "description": "The channel to send auto-protect alerts to" - }, - "autoreact_react_map": { - "datatype": "dict[str, str]", - "description": "Lowercase phrase to reaction wanted" - }, - "core_command_prefix": { - "datatype": "str", - "description": "The prefix to use for legacy commands" - }, - "core_enable_logging": { - "datatype": "bool", - "description": "Whether the botlogging module should be enabled. When disabled, core logs will all be turned off" - }, - "core_nickname_filter": { - "datatype": "bool", - "description": "Whether to run the nickname filter or not" - }, - "core_private_channels": { - "datatype": "list[discord.TextChannel]", - "description": "A list of channels to exclude from logging events" - }, - "core_guild_id": { - "datatype": "discord.Guild", - "description": "This is used as a validation tool, the guild ID that this config belongs to" - }, - "duck_allow_manipulation": { - "datatype": "bool", - "description": "Controls whether release, donate, or kill commands are enabled" - }, - "duck_cooldown": { - "datatype": "int", - "description": "The amount of time in seconds to wait between bef/bang messages" - }, - "duck_max_wait": { - "datatype": "int", - "description": "The maximum number of hours to wait between duck events" - }, - "duck_min_wait": { - "datatype": "int", - "description": "The minimum number of hours to wait between duck events" - }, - "duck_mute_for_cooldown": { - "datatype": "bool", - "description": "If enabled, users who miss will be timed out for the cooldown seconds" - }, - "duck_spawn_user": { - "datatype": "list[discord.Member]", - "description": "A list of users allowed to use thje .duck spawn command" - }, - "duck_timeout": { - "datatype": "int", - "description": "The amount of time in seconds before the duck disappears" - }, - "duck_use_category": { - "datatype": "bool", - "description": "Whether to use the whole category for ducks" - }, - "dumpdbg_roles": { - "datatype": "list[discord.Role]", - "description": "Roles permitted to use the dump debug command" - }, - "embed_embed_roles": { - "datatype": "list[discord.Role]", - "description": "Roles permitted to use the embed command" - }, - "core_enabled_extensions": { - "datatype": "list[str]", - "description": "A list of all extensions enabled in the guild" - }, - "core_logging_channel": { - "datatype": "discord.TextChannel", - "description": "This is the bot events logging channel" - }, - "modmail_modmail_roles": { - "datatype": "list[discord.Role]", - "description": "Roles that can access modmail and its commands" - }, - "duck_hunt_channels": { - "datatype": "list[discord.TextChannel]", - "description": "The list of channels the duck should appear in" - }, - "duck_success_rate": { - "datatype": "int", - "description": "The success rate of bef/bang messages" - }, - "forum_abandoned_message": { - "datatype": "str", - "description": "The message displayed on abandoned threads" - }, - "forum_body_regex_list": { - "datatype": "list[str]", - "description": "List of regex to ban in bodies" - }, - "forum_close_message": { - "datatype": "str", - "description": "The message displayed on closed threads" - }, - "forum_delete_message": { - "datatype": "str", - "description": "The message displayed on deleted threads" - }, - "forum_duplicate_message": { - "datatype": "str", - "description": "The message displayed on duplicated threads" - }, - "forum_forum_channel_id": { - "datatype": "discord.ForumChannel", - "description": "The forum channel to manage threads in" - }, - "forum_left_message": { - "datatype": "str", - "description": "The message displayed on left threads" - }, - "forum_max_age_minutes": { - "datatype": "int", - "description": "The max age of a thread before it times out" - }, - "forum_reject_message": { - "datatype": "str", - "description": "The message displayed on rejected threads" - }, - "forum_solve_message": { - "datatype": "str", - "description": "The message displayed on solved threads" - }, - "forum_staff_role_ids": { - "datatype": "list[discord.Role]", - "description": "Staff roles able to mark threads solved/abandoned/rejected" - }, - "forum_title_regex_list": { - "datatype": "list[str]", - "description": "List of regex to ban in titles" - }, - "forum_welcome_message": { - "datatype": "str", - "description": "The message displayed on new threads" - }, - "voting_active_role_id": { - "datatype": "discord.Role", - "description": "User must have this role to start or participate in votes" - }, - "voting_reminders_at": { - "datatype": "list[int]", - "description": "The list of hours remaining in vote to remind non voters" - }, - "voting_votes_channel_roles": { - "datatype": "dict[discord.ForumChannel, list[discord.Role]]", - "description": "Map of forum channel IDs to a list of role IDs. User must have at least one role from the list." - }, - "voting_voting_thresholds": { - "datatype": "list[int]", - "description": "1, % of eligible voters who must vote yes, 2, % of yes/no voters who must have voted yes, 3, % of eligible voters who must have voted anything at all" - }, - "automod_banned_file_extensions": { - "datatype": "list[str]", - "description": "A list of all file extensions to be blocked and have a auto warning issued" - }, - "automod_banned_file_hashes": { - "datatype": "list[str]", - "description": "A list of all file hashes to be blocked and have a auto warning issued" - }, - "automod_bypass_roles": { - "datatype": "list[discord.Role]", - "description": "The list of role names associated with bypassed roles by the auto-protect" - }, - "automod_channels": { - "datatype": "list[discord.TextChannel]", - "description": "The list of channels to enable automod in" - }, - "automod_max_mentions": { - "datatype": "int", - "description": "Max number of mentions allowed in a message before triggering auto-protect" - }, - "automod_string_map": { - "datatype": "dict[str, dict[str, Any]]", - "description": "Mapping of keyword strings to data defining the action taken by auto-protect" - }, - "moderation_max_warnings": { - "datatype": "int", - "description": "The maximum number of warnings before /warn prompts to ban, and automod autobans" - }, - "moderator_ban_delete_duration": { - "datatype": "int", - "description": "The default amount of days to delete messages for a user after they are banned" - }, - "logger_channel_map": { - "datatype": "dict[discord.TextChannel, discord.TextChannel]", - "description": "Input Channel to Logging Channel mapping" - }, - "gate_channel": { - "datatype": "discord.TextChannel", - "description": "The channel the gate is in" - }, - "gate_delete_wait": { - "datatype": "int", - "description": "The amount of time to wait (in seconds) before deleting the welcome message" - }, - "gate_intro_message": { - "datatype": "str", - "description": "The message that's sent when running the intro message command" - }, - "gate_roles": { - "datatype": "list[discord.Role]", - "description": "The list of roles to add after user is verified" - }, - "gate_verify_text": { - "datatype": "str", - "description": "The case-insensitive text the user should type to verify themselves" - }, - "gate_welcome_message": { - "datatype": "str", - "description": "The message to send to the user after they are verified" - }, - "grab_allowed_channels": { - "datatype": "list[discord.TextChannel]", - "description": "The list of channels to enable the grabs plugin" - }, - "grab_per_page": { - "datatype": "int", - "description": "The number of grabs per page when retrieving all grabs" - }, - "hangman_hangman_roles": { - "datatype": "list[discord.Role]", - "description": "The list of role names able to control hangman games" - }, - "honeypot_channels": { - "datatype": "list[discord.TextChannel]", - "description": "The list of channels that are honeypots" - }, - "moderation_alert_channel": { - "datatype": "discord.TextChannel", - "description": "The channel to send moderation and honeypot alerts to" - }, - "joke_apply_in_nsfw_channels": { - "datatype": "bool", - "description": "Toggles whether or not filters are applies in NSFW channels" - }, - "joke_blacklisted_filters": { - "datatype": "list[str]", - "description": "Filters all categories listed (nsfw,religious,political,racist,sexist,explicit)" - }, - "factoids_admin_roles": { - "datatype": "list[discord.Role]", - "description": "The roles required to administrate factoids" - }, - "factoids_disable_embeds": { - "datatype": "bool", - "description": "This will force all factoids to not use embeds." - }, - "factoids_manage_roles": { - "datatype": "list[discord.Role]", - "description": "The roles required to manage factoids" - }, - "factoids_prefix": { - "datatype": "str", - "description": "Prefix for calling factoids" - }, - "factoids_restricted_list": { - "datatype": "list[discord.TextChannel]", - "description": "List of channel IDs that restricted factoids are allowed to be used in" - }, - "kanye_channel": { - "datatype": "discord.TextChannel", - "description": "The ID of the channel the Kanye West quote should appear in" - }, - "kanye_max_wait": { - "datatype": "int", - "description": "The maximum number of hours to wait between Kanye events" - }, - "kanye_min_wait": { - "datatype": "int", - "description": "The minimum number of hours to wait between Kanye events" - }, - "moderator_immune_roles": { - "datatype": "list[discord.Role]", - "description": "The list of role names that are immune to protect commands" - }, - "modlog_alert_channel": { - "datatype": "discord.TextChannel", - "description": "The ID of the channel to send auto-protect alerts to" - }, - "modmail_aliases": { - "datatype": "dict[str, str]", - "description": "Custom modmail commands to send message slices" - }, - "modmail_automatic_rejections": { - "datatype": "dict[str, str]", - "description": "If someone sends a message matching regex, blocks thread creation" - }, - "modmail_automatic_responses": { - "datatype": "dict[str, str]", - "description": "If someone sends a message containing a key, sends its value" - }, - "modmail_roles_to_ping": { - "datatype": "list[discord.Role]", - "description": "Roles to ping on thread creation" - }, - "modmail_thread_creation_message": { - "datatype": "str", - "description": "The message sent to the user when confirming a thread creation." - }, - "news_category": { - "datatype": "str", - "description": "The category to use when receiving cronjob headlines" - }, - "news_channel": { - "datatype": "discord.TextChannel", - "description": "The ID of the channel the news should appear in" - }, - "news_country": { - "datatype": "str", - "description": "Country code to receive news for (example: US)" - }, - "news_cron_config": { - "datatype": "CronTab", - "description": "Crontab syntax for executing news events" - }, - "notes_note_bypass": { - "datatype": "list[discord.Role]", - "description": "A list of roles that shouldn't have notes set or the note role assigned" - }, - "notes_note_readers": { - "datatype": "list[discord.Role]", - "description": "Users with roles in this list will be able to use whois" - }, - "notes_note_role": { - "datatype": "discord.Role", - "description": "The name of the role to be added when a note is added to a user" - }, - "notes_note_writers": { - "datatype": "list[discord.Role]", - "description": "Users with roles in this list will be able to create or delete notes" - }, - "paste_bypass_roles": { - "datatype": "list[discord.Role]", - "description": "The list of role names that will bypass the paste system" - }, - "paste_channels": { - "datatype": "list[discord.TextChannel]", - "description": "The list of channel ID's that will have the paste service enabled" - }, - "paste_length_limit": { - "datatype": "int", - "description": "The max char limit on messages before they trigger a paste" - }, - "paste_paste_footer_message": { - "datatype": "str", - "description": "The message used on the footer of the large message paste embed" - }, - "purge_max_purge_amount": { - "datatype": "int", - "description": "The max amount of messages allowed to be purged in one command" - }, - "report_alert_channel": { - "datatype": "discord.TextChannel", - "description": "The ID of the channel to send reports to" - }, - "report_anonymous": { - "datatype": "bool", - "description": "Whether reports are anonymous" - }, - "report_ping_role": { - "datatype": "discord.Role", - "description": "The ID of the role to ping when a new report is created" - } -} - - - -"": { - "datatype": "", - "description": "" - } + "application_application_message": { + "datatype": "str", + "description": "The message to show users when they are prompted to apply in the notification_channels" + }, + "application_application_role_id": { + "datatype": "discord.Role", + "description": "The role to give applicants when their application is approved" + }, + "application_manage_role_ids": { + "datatype": "list[discord.Role]", + "description": "The roles required to manage the applications (not required to apply)" + }, + "application_management_channel": { + "datatype": "discord.TextChannel", + "description": "The channel the application notifications and reminders should appear in" + }, + "application_max_age": { + "datatype": "int", + "description": "After this many days, the system will auto reject the applications" + }, + "application_notification_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The channels that should receive periodic messages about the application, with a button to apply" + }, + "application_notification_cron_config": { + "datatype": "CronTab", + "description": "CronTab for users being notified about the application" + }, + "application_ping_role": { + "datatype": "discord.Role", + "description": "The role to ping when a new application is created" + }, + "application_reminder_cron_config": { + "datatype": "CronTab", + "description": "Crontab for executing pending reminder events" + }, + "automod_alert_channel": { + "datatype": "discord.TextChannel", + "description": "The channel to send auto-protect alerts to" + }, + "automod_banned_file_extensions": { + "datatype": "list[str]", + "description": "A list of all file extensions to be blocked and have a auto warning issued" + }, + "automod_banned_file_hashes": { + "datatype": "list[str]", + "description": "A list of all file hashes to be blocked and have a auto warning issued" + }, + "automod_bypass_roles": { + "datatype": "list[discord.Role]", + "description": "The list of role names associated with bypassed roles by the auto-protect" + }, + "automod_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The list of channels to enable automod in" + }, + "automod_max_mentions": { + "datatype": "int", + "description": "Max number of mentions allowed in a message before triggering auto-protect" + }, + "automod_string_map": { + "datatype": "dict[str, dict[str, Any]]", + "description": "Mapping of keyword strings to data defining the action taken by auto-protect" + }, + "autoreact_react_map": { + "datatype": "dict[str, str]", + "description": "Lowercase phrase to reaction wanted" + }, + "core_command_prefix": { + "datatype": "str", + "description": "The prefix to use for legacy commands" + }, + "core_enable_logging": { + "datatype": "bool", + "description": "Whether the botlogging module should be enabled. When disabled, core logs will all be turned off" + }, + "core_enabled_extensions": { + "datatype": "list[str]", + "description": "A list of all extensions enabled in the guild" + }, + "core_guild_events_channel": { + "datatype": "discord.TextChannel", + "description": "The channel to log guild events to. This includes message events" + }, + "core_guild_id": { + "datatype": "discord.Guild", + "description": "This is used as a validation tool, the guild ID that this config belongs to" + }, + "core_logging_channel": { + "datatype": "discord.TextChannel", + "description": "This is the bot events logging channel" + }, + "core_member_events_channel": { + "datatype": "discord.TextChannel", + "description": "The channel to log member events to." + }, + "core_nickname_filter": { + "datatype": "bool", + "description": "Whether to run the nickname filter or not" + }, + "core_private_channels": { + "datatype": "list[discord.TextChannel]", + "description": "A list of channels to exclude from logging events" + }, + "duck_allow_manipulation": { + "datatype": "bool", + "description": "Controls whether release, donate, or kill commands are enabled" + }, + "duck_cooldown": { + "datatype": "int", + "description": "The amount of time in seconds to wait between bef/bang messages" + }, + "duck_hunt_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The list of channels the duck should appear in" + }, + "duck_max_wait": { + "datatype": "int", + "description": "The maximum number of hours to wait between duck events" + }, + "duck_min_wait": { + "datatype": "int", + "description": "The minimum number of hours to wait between duck events" + }, + "duck_mute_for_cooldown": { + "datatype": "bool", + "description": "If enabled, users who miss will be timed out for the cooldown seconds" + }, + "duck_spawn_user": { + "datatype": "list[discord.Member]", + "description": "A list of users allowed to use thje .duck spawn command" + }, + "duck_success_rate": { + "datatype": "int", + "description": "The success rate of bef/bang messages" + }, + "duck_timeout": { + "datatype": "int", + "description": "The amount of time in seconds before the duck disappears" + }, + "duck_use_category": { + "datatype": "bool", + "description": "Whether to use the whole category for ducks" + }, + "dumpdbg_roles": { + "datatype": "list[discord.Role]", + "description": "Roles permitted to use the dump debug command" + }, + "embed_embed_roles": { + "datatype": "list[discord.Role]", + "description": "Roles permitted to use the embed command" + }, + "factoids_admin_roles": { + "datatype": "list[discord.Role]", + "description": "The roles required to administrate factoids" + }, + "factoids_disable_embeds": { + "datatype": "bool", + "description": "This will force all factoids to not use embeds." + }, + "factoids_manage_roles": { + "datatype": "list[discord.Role]", + "description": "The roles required to manage factoids" + }, + "factoids_prefix": { + "datatype": "str", + "description": "Prefix for calling factoids" + }, + "factoids_restricted_list": { + "datatype": "list[discord.TextChannel]", + "description": "List of channel IDs that restricted factoids are allowed to be used in" + }, + "forum_abandoned_message": { + "datatype": "str", + "description": "The message displayed on abandoned threads" + }, + "forum_body_regex_list": { + "datatype": "list[str]", + "description": "List of regex to ban in bodies" + }, + "forum_close_message": { + "datatype": "str", + "description": "The message displayed on closed threads" + }, + "forum_delete_message": { + "datatype": "str", + "description": "The message displayed on deleted threads" + }, + "forum_duplicate_message": { + "datatype": "str", + "description": "The message displayed on duplicated threads" + }, + "forum_forum_channel_id": { + "datatype": "discord.ForumChannel", + "description": "The forum channel to manage threads in" + }, + "forum_left_message": { + "datatype": "str", + "description": "The message displayed on left threads" + }, + "forum_max_age_minutes": { + "datatype": "int", + "description": "The max age of a thread before it times out" + }, + "forum_reject_message": { + "datatype": "str", + "description": "The message displayed on rejected threads" + }, + "forum_solve_message": { + "datatype": "str", + "description": "The message displayed on solved threads" + }, + "forum_staff_role_ids": { + "datatype": "list[discord.Role]", + "description": "Staff roles able to mark threads solved/abandoned/rejected" + }, + "forum_title_regex_list": { + "datatype": "list[str]", + "description": "List of regex to ban in titles" + }, + "forum_welcome_message": { + "datatype": "str", + "description": "The message displayed on new threads" + }, + "gate_channel": { + "datatype": "discord.TextChannel", + "description": "The channel the gate is in" + }, + "gate_delete_wait": { + "datatype": "int", + "description": "The amount of time to wait (in seconds) before deleting the welcome message" + }, + "gate_intro_message": { + "datatype": "str", + "description": "The message that's sent when running the intro message command" + }, + "gate_roles": { + "datatype": "list[discord.Role]", + "description": "The list of roles to add after user is verified" + }, + "gate_verify_text": { + "datatype": "str", + "description": "The case-insensitive text the user should type to verify themselves" + }, + "gate_welcome_message": { + "datatype": "str", + "description": "The message to send to the user after they are verified" + }, + "grab_allowed_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The list of channels to enable the grabs plugin" + }, + "grab_per_page": { + "datatype": "int", + "description": "The number of grabs per page when retrieving all grabs" + }, + "hangman_hangman_roles": { + "datatype": "list[discord.Role]", + "description": "The list of role names able to control hangman games" + }, + "honeypot_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The list of channels that are honeypots" + }, + "joke_apply_in_nsfw_channels": { + "datatype": "bool", + "description": "Toggles whether or not filters are applies in NSFW channels" + }, + "joke_blacklisted_filters": { + "datatype": "list[str]", + "description": "Filters all categories listed (nsfw,religious,political,racist,sexist,explicit)" + }, + "kanye_channel": { + "datatype": "discord.TextChannel", + "description": "The ID of the channel the Kanye West quote should appear in" + }, + "kanye_max_wait": { + "datatype": "int", + "description": "The maximum number of hours to wait between Kanye events" + }, + "kanye_min_wait": { + "datatype": "int", + "description": "The minimum number of hours to wait between Kanye events" + }, + "logger_channel_map": { + "datatype": "dict[discord.TextChannel, discord.TextChannel]", + "description": "Input Channel to Logging Channel mapping" + }, + "moderation_alert_channel": { + "datatype": "discord.TextChannel", + "description": "The channel to send moderation and honeypot alerts to" + }, + "moderation_max_warnings": { + "datatype": "int", + "description": "The maximum number of warnings before /warn prompts to ban, and automod autobans" + }, + "moderator_ban_delete_duration": { + "datatype": "int", + "description": "The default amount of days to delete messages for a user after they are banned" + }, + "moderator_immune_roles": { + "datatype": "list[discord.Role]", + "description": "The list of role names that are immune to protect commands" + }, + "modlog_alert_channel": { + "datatype": "discord.TextChannel", + "description": "The ID of the channel to send auto-protect alerts to" + }, + "modmail_aliases": { + "datatype": "dict[str, str]", + "description": "Custom modmail commands to send message slices" + }, + "modmail_automatic_rejections": { + "datatype": "dict[str, str]", + "description": "If someone sends a message matching regex, blocks thread creation" + }, + "modmail_automatic_responses": { + "datatype": "dict[str, str]", + "description": "If someone sends a message containing a key, sends its value" + }, + "modmail_modmail_roles": { + "datatype": "list[discord.Role]", + "description": "Roles that can access modmail and its commands" + }, + "modmail_roles_to_ping": { + "datatype": "list[discord.Role]", + "description": "Roles to ping on thread creation" + }, + "modmail_thread_creation_message": { + "datatype": "str", + "description": "The message sent to the user when confirming a thread creation." + }, + "news_category": { + "datatype": "str", + "description": "The category to use when receiving cronjob headlines" + }, + "news_channel": { + "datatype": "discord.TextChannel", + "description": "The ID of the channel the news should appear in" + }, + "news_country": { + "datatype": "str", + "description": "Country code to receive news for (example: US)" + }, + "news_cron_config": { + "datatype": "CronTab", + "description": "Crontab syntax for executing news events" + }, + "notes_note_bypass": { + "datatype": "list[discord.Role]", + "description": "A list of roles that shouldn't have notes set or the note role assigned" + }, + "notes_note_readers": { + "datatype": "list[discord.Role]", + "description": "Users with roles in this list will be able to use whois" + }, + "notes_note_role": { + "datatype": "discord.Role", + "description": "The name of the role to be added when a note is added to a user" + }, + "notes_note_writers": { + "datatype": "list[discord.Role]", + "description": "Users with roles in this list will be able to create or delete notes" + }, + "paste_bypass_roles": { + "datatype": "list[discord.Role]", + "description": "The list of role names that will bypass the paste system" + }, + "paste_channels": { + "datatype": "list[discord.TextChannel]", + "description": "The list of channel ID's that will have the paste service enabled" + }, + "paste_length_limit": { + "datatype": "int", + "description": "The max char limit on messages before they trigger a paste" + }, + "paste_paste_footer_message": { + "datatype": "str", + "description": "The message used on the footer of the large message paste embed" + }, + "purge_max_purge_amount": { + "datatype": "int", + "description": "The max amount of messages allowed to be purged in one command" + }, + "rate_limit_commands": { + "datatype": "int", + "description": "The maximum number of commands run before rate limit triggers" + }, + "rate_limit_enabled": { + "datatype": "bool", + "description": "Whether the rate limit feature is enabled or not" + }, + "rate_limit_time": { + "datatype": "int", + "description": "The rolling time window to track for a rate limit" + }, + "report_alert_channel": { + "datatype": "discord.TextChannel", + "description": "The ID of the channel to send reports to" + }, + "report_anonymous": { + "datatype": "bool", + "description": "Whether reports are anonymous" + }, + "report_ping_role": { + "datatype": "discord.Role", + "description": "The ID of the role to ping when a new report is created" + }, + "role_all_assignable_roles": { + "datatype": "list[discord.Role]", + "description": "The list of roles by name that moderators can assign to people" + }, + "role_allow_all_assign": { + "datatype": "list[discord.Role]", + "description": "The list of roles that are allowed to assign others roles" + }, + "role_allow_self_assign": { + "datatype": "list[discord.Role]", + "description": "The list of roles that are allowed to assign themselves roles" + }, + "role_self_assign_map": { + "datatype": "dict[discord.Role, list[discord.Role]]", + "description": "A map in the format target: [use, use]" + }, + "urban_max_responses": { + "datatype": "int", + "description": "The max amount of responses per embed page" + }, + "voting_active_role_id": { + "datatype": "discord.Role", + "description": "User must have this role to start or participate in votes" + }, + "voting_reminders_at": { + "datatype": "list[int]", + "description": "The list of hours remaining in vote to remind non voters" + }, + "voting_votes_channel_roles": { + "datatype": "dict[discord.ForumChannel, list[discord.Role]]", + "description": "Map of forum channel IDs to a list of role IDs. User must have at least one role from the list." + }, + "voting_voting_thresholds": { + "datatype": "list[int]", + "description": "1, % of eligible voters who must vote yes, 2, % of yes/no voters who must have voted yes, 3, % of eligible voters who must have voted anything at all" + }, + "xp_categories_counted": { + "datatype": "list[discord.CategoryChannel]", + "description": "List of category IDs to count for XP" + }, + "xp_excluded_channels": { + "datatype": "list[discord.TextChannel]", + "description": "List of channel IDs to exclude for XP" + }, + "xp_level_roles": { + "datatype": "dict[int, discord.Role]", + "description": "Dict of levels in XP:Role ID" + } +} \ No newline at end of file diff --git a/techsupport_bot/core/extensionconfig.py b/techsupport_bot/core/extensionconfig.py deleted file mode 100644 index 3b667cc71..000000000 --- a/techsupport_bot/core/extensionconfig.py +++ /dev/null @@ -1,42 +0,0 @@ -"""This represents an extension config item when building in the setup function""" - -from __future__ import annotations - -from typing import Self - -import munch - - -class ExtensionConfig: - """Represents the config of an extension.""" - - def __init__(self: Self) -> None: - self.data = munch.DefaultMunch(None) - - def add( - self: Self, - key: str, - datatype: str, - title: str, - description: str, - default: str | bool | int | list[str] | list[int] | dict[str, str], - ) -> None: - """Adds a new entry to the config. - - This is usually used in the extensions's setup function. - - Args: - key (str): the lookup key for the entry - datatype (str): the datatype metadata for the entry - title (str): the title of the entry - description (str): the description of the entry - default (str | bool | int | list[str] | list[int] | dict[str, str]): - the default value to use for the config entry - """ - self.data[key] = { - "datatype": datatype, - "title": title, - "description": description, - "default": default, - "value": default, - } diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index c87859e7c..7ad61ff48 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -3,6 +3,7 @@ import datetime +import configuration import discord import munch @@ -222,10 +223,10 @@ async def send_command_usage_alert( "https://www.iconarchive.com/download/i76061/martz90/circle-addon2/warning.512.png" ) - config = bot_object.guild_configs[str(guild.id)] - try: - alert_channel = guild.get_channel(int(config.moderation.alert_channel)) + alert_channel = guild.get_channel( + int(configuration.get_config_entry(guild.id, "moderation_alert_channel")) + ) except TypeError: alert_channel = None diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index bcc5fba39..9aae2a148 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -12,7 +12,7 @@ import munch from botlogging import LogContext, LogLevel from commands import moderator, modlog -from core import auxiliary, cogs, extensionconfig, moderation +from core import auxiliary, cogs, moderation from discord.ext import commands if TYPE_CHECKING: @@ -25,55 +25,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - config = extensionconfig.ExtensionConfig() - config.add( - key="channels", - datatype="list", - title="Protected channels", - description=( - "The list of channel ID's associated with the channels to auto-protect" - ), - default=[], - ) - config.add( - key="bypass_roles", - datatype="list", - title="Bypassed role names", - description=( - "The list of role names associated with bypassed roles by the auto-protect" - ), - default=[], - ) - config.add( - key="string_map", - datatype="dict", - title="Keyword string map", - description=( - "Mapping of keyword strings to data defining the action taken by" - " auto-protect" - ), - default={}, - ) - config.add( - key="banned_file_extensions", - datatype="dict", - title="List of banned file types", - description=( - "A list of all file extensions to be blocked and have a auto warning issued" - ), - default=[], - ) - config.add( - key="max_mentions", - datatype="int", - title="Max message mentions", - description=( - "Max number of mentions allowed in a message before triggering auto-protect" - ), - default=3, - ) await bot.add_cog(AutoMod(bot=bot, extension_name="automod")) - bot.add_extension_config("automod", config) @dataclass diff --git a/techsupport_bot/functions/events.py b/techsupport_bot/functions/events.py index c53c7f2c0..d22862e05 100644 --- a/techsupport_bot/functions/events.py +++ b/techsupport_bot/functions/events.py @@ -63,8 +63,8 @@ async def on_message_edit( ) embed.set_footer(text=f"Author ID: {before.author.id}") - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + before.author.id, "core_guild_events_channel" ) await self.bot.logger.send_log( @@ -104,9 +104,10 @@ async def on_message_delete(self: Self, message: discord.Message) -> None: embed.add_field(name="Server", value=getattr(guild, "name", "None")) embed.set_footer(text=f"Author ID: {message.author.id}") - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + message.author.id, "core_guild_events_channel" ) + await self.bot.logger.send_log( message=f"Message with ID {message.id} deleted", level=LogLevel.INFO, @@ -139,8 +140,8 @@ async def on_bulk_message_delete( embed.add_field(name="Channels", value=",".join(unique_channels)) embed.add_field(name="Servers", value=",".join(unique_servers)) - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( message=f"{len(messages)} messages bulk deleted!", @@ -184,8 +185,8 @@ async def on_reaction_add( ) embed.add_field(name="Server", value=guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( @@ -233,8 +234,8 @@ async def on_reaction_remove( ) embed.add_field(name="Server", value=guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( @@ -274,8 +275,8 @@ async def on_reaction_clear( embed.add_field(name="Channel", value=getattr(message.channel, "name", "DM")) embed.add_field(name="Server", value=guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( @@ -300,8 +301,8 @@ async def on_guild_channel_delete( embed.add_field(name="Channel Name", value=channel.name) embed.add_field(name="Server", value=channel.guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - channel.guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + channel.guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( @@ -328,8 +329,8 @@ async def on_guild_channel_create( embed = discord.Embed() embed.add_field(name="Channel Name", value=channel.name) embed.add_field(name="Server", value=channel.guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - getattr(channel, "guild", None), key="guild_events_channel" + log_channel = configuration.get_config_entry( + channel.guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( message=( @@ -368,8 +369,8 @@ async def on_guild_channel_update( embed.add_field(name="Channel Name", value=before.name) embed.add_field(name="Server", value=before.guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - before.guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + before.guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( message=( @@ -402,8 +403,8 @@ async def on_guild_channel_pins_update( embed.add_field(name="Channel Name", value=channel.name) embed.add_field(name="Server", value=channel.guild) - log_channel = await self.bot.get_log_channel_from_guild( - channel.guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + channel.guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( @@ -428,8 +429,8 @@ async def on_guild_integrations_update(self: Self, guild: discord.Guild) -> None """ embed = discord.Embed() embed.add_field(name="Server", value=guild) - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( message=f"Integrations updated in guild with ID {guild.id}", @@ -450,8 +451,8 @@ async def on_webhooks_update(self: Self, channel: discord.abc.GuildChannel) -> N embed.add_field(name="Channel", value=channel.name) embed.add_field(name="Server", value=channel.guild) - log_channel = await self.bot.get_log_channel_from_guild( - channel.guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + channel.guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( @@ -486,8 +487,8 @@ async def on_member_update( embed.add_field(name="Roles lost", value=next(iter(changed_role))) embed.add_field(name="Server", value=before.guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - getattr(before, "guild", None), key="member_events_channel" + log_channel = configuration.get_config_entry( + before.guild.id, "core_member_events_channel" ) await self.bot.logger.send_log( @@ -511,8 +512,8 @@ async def on_member_remove(self: Self, member: discord.Member) -> None: embed = discord.Embed() embed.add_field(name="Member", value=member) embed.add_field(name="Server", value=member.guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - getattr(member, "guild", None), key="member_events_channel" + log_channel = configuration.get_config_entry( + member.guild.id, "core_member_events_channel" ) await self.bot.logger.send_log( @@ -551,8 +552,8 @@ async def on_guild_join(self: Self, guild: discord.Guild) -> None: embed = discord.Embed() embed.add_field(name="Server", value=guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( @@ -605,8 +606,8 @@ async def on_guild_update( embed = auxiliary.add_diff_fields(embed, diff) embed.add_field(name="Server", value=before.name) - log_channel = await self.bot.get_log_channel_from_guild( - before, key="guild_events_channel" + log_channel = configuration.get_config_entry( + before.guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( message=f"Guild with ID {before.id} updated", @@ -625,8 +626,8 @@ async def on_guild_role_create(self: Self, role: discord.Role) -> None: """ embed = discord.Embed() embed.add_field(name="Server", value=role.guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - role.guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + role.guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( @@ -648,8 +649,8 @@ async def on_guild_role_delete(self: Self, role: discord.Role) -> None: """ embed = discord.Embed() embed.add_field(name="Server", value=role.guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - role.guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + role.guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( message=( @@ -678,8 +679,8 @@ async def on_guild_role_update( embed = auxiliary.add_diff_fields(embed, diff) embed.add_field(name="Server", value=before.name) - log_channel = await self.bot.get_log_channel_from_guild( - before.guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + before.guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( @@ -709,8 +710,8 @@ async def on_guild_emojis_update( embed = discord.Embed() embed.add_field(name="Server", value=guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="guild_events_channel" + log_channel = configuration.get_config_entry( + guild.id, "core_guild_events_channel" ) await self.bot.logger.send_log( message=f"Emojis updated in guild with ID {guild.id}", @@ -735,8 +736,8 @@ async def on_member_ban( embed.add_field(name="User", value=user) embed.add_field(name="Server", value=guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="member_events_channel" + log_channel = configuration.get_config_entry( + guild.id, "core_member_events_channel" ) await self.bot.logger.send_log( @@ -761,8 +762,8 @@ async def on_member_unban( embed.add_field(name="User", value=user) embed.add_field(name="Server", value=guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - guild, key="member_events_channel" + log_channel = configuration.get_config_entry( + guild.id, "core_member_events_channel" ) await self.bot.logger.send_log( @@ -783,8 +784,8 @@ async def on_member_join(self: Self, member: discord.Member) -> None: embed = discord.Embed() embed.add_field(name="Member", value=member) embed.add_field(name="Server", value=member.guild.name) - log_channel = await self.bot.get_log_channel_from_guild( - getattr(member, "guild", None), key="member_events_channel" + log_channel = configuration.get_config_entry( + member.guild.id, "core_member_events_channel" ) await self.bot.logger.send_log( diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index 4a7ac53f4..ea5373b59 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -9,7 +9,7 @@ import discord import munch from botlogging import LogContext, LogLevel -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: @@ -22,17 +22,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cogs to """ - config = extensionconfig.ExtensionConfig() - config.add( - key="channel_map", - datatype="dict", - title="Mapping of channel ID's", - description="Input Channel ID to Logging Channel ID mapping", - default={}, - ) - await bot.add_cog(Logger(bot=bot, extension_name="logger")) - bot.add_extension_config("logger", config) def get_channel_id(channel: discord.abc.GuildChannel | discord.Thread) -> int: From eaeee57655fecb18b1aa7520d92ec14e9fad6102 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:47:53 -0700 Subject: [PATCH 27/40] A bunch of changes to the config.py system to support future commands --- techsupport_bot/configuration/config.py | 122 ++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 10 deletions(-) diff --git a/techsupport_bot/configuration/config.py b/techsupport_bot/configuration/config.py index 98e5d6add..d6d28d6c2 100644 --- a/techsupport_bot/configuration/config.py +++ b/techsupport_bot/configuration/config.py @@ -15,6 +15,7 @@ """ import json +import os from pathlib import Path from typing import Any @@ -23,6 +24,7 @@ BASE_PATH = "configuration/" +# Publically callable functions def get_config_entry(guild_id: int, key: str) -> Any: # noqa: ANN401 """This searches for a guild specific config entry @@ -37,15 +39,15 @@ def get_config_entry(guild_id: int, key: str) -> Any: # noqa: ANN401 AttributeError: Raised if the passed key is not valid """ - if not check_key_valid(key): + if not _check_key_valid(key): raise AttributeError(f"Key {key} is invalid") default_entry = get_default_config_entry(key) - if not does_guild_config_exist(guild_id): + if not _does_guild_config_exist(guild_id): return default_entry - guild_config = read_json_file(f"guild_configs/{guild_id}.json") + guild_config = _read_guild_json(guild_id) if key in guild_config: return guild_config[key] @@ -64,16 +66,75 @@ def get_default_config_entry(key: str) -> Any: # noqa: ANN401 Raises: AttributeError: Raised if the passed key is not valid """ - if not check_key_valid(key): + if not _check_key_valid(key): raise AttributeError(f"Key {key} is invalid") - default_config = read_json_file("config.default.json") + default_config = _read_json_file("config.default.json") return default_config[key] -# WORKING -def check_key_valid(key: str) -> bool: +def get_default_config_json() -> munch.Munch: + """This gets a munified versions of the default config file + + Returns: + munch.Munch: The default configuration as defined + """ + return _read_json_file("config.default.json") + + +def get_guild_config_json(guild_id: int) -> munch.Munch: + """This gets a munified version of the guild config if it exists + + Args: + guild_id (int): The guild ID to search for + + Raises: + AttributeError: Raised if the specified guild has no config + + Returns: + munch.Munch: The guild config read + """ + if _does_guild_config_exist(guild_id): + return _read_guild_json(guild_id) + raise AttributeError(f"No config found for guild {guild_id}") + + +def write_guild_config_json(guild_id: int, new_config: munch.Munch) -> None: + """A function to write a guild config with a munch.Munch json + + Args: + guild_id (int): The ID of the guild to write to + new_config (munch.Munch): The new config to write + """ + _write_guild_json_file(guild_id, new_config) + + +def edit_config_entry(guild_id: int, key: str, new_value: Any) -> None: # noqa: ANN401 + """This edits a config entry for a specific guild + If there is no guild config for a given guild, a blank config is created + + Args: + guild_id (int): The ID of the guild to change + key (str): The key to write to the config + new_value (Any): The value of the key to write + + Raises: + AttributeError: Raised if the passed key is not valid + """ + if not _check_key_valid(key): + raise AttributeError(f"Key {key} is invalid") + + if not _does_guild_config_exist(guild_id): + _write_blank_guild_config(guild_id) + + guild_config = _read_guild_json(guild_id) + guild_config[key] = new_value + _write_guild_json_file(guild_id, guild_config) + + +# Internal functions only +def _check_key_valid(key: str) -> bool: """This will check if the key is valid and present in default config Args: @@ -82,14 +143,14 @@ def check_key_valid(key: str) -> bool: Returns: bool: True if the key exists, false if it doesn't """ - default_config = read_json_file("config.default.json") + default_config = _read_json_file("config.default.json") if key in default_config: return True return False -def does_guild_config_exist(guild_id: int) -> bool: +def _does_guild_config_exist(guild_id: int) -> bool: """This checks if a guild specific config file exists Args: @@ -105,7 +166,7 @@ def does_guild_config_exist(guild_id: int) -> bool: return False -def read_json_file(path: str) -> munch.Munch: +def _read_json_file(path: str) -> munch.Munch: """This reads a json file from disk and parses it into a munch.Munch This functions assumes the json file exists @@ -117,3 +178,44 @@ def read_json_file(path: str) -> munch.Munch: """ with open(f"{BASE_PATH}{path}", encoding="utf-8") as file: return munch.munchify(json.load(file)) + + +def _read_guild_json(guild_id: int) -> munch.Munch: + """A function to get the json of a specific guild config + + Args: + guild_id (int): The guild ID to get the config for + + Returns: + munch.Munch: The munchified representation of the guild config + """ + return _read_json_file(f"guild_configs/{guild_id}.json") + + +def _write_guild_json_file(guild_id: int, json_data: munch.Munch) -> None: + """Writes a guild configuration file to disk. + This will ensure the guild_configs folder exists as well + + Args: + guild_id (int): The ID of the guild whose configuration should be written. + json_data (munch.Munch): The configuration data to write. + """ + path = f"{BASE_PATH}/guild_configs/{guild_id}.json" + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, "w", encoding="utf-8") as file: + json.dump( + munch.unmunchify(json_data), + file, + indent=4, + ensure_ascii=False, + ) + + +def _write_blank_guild_config(guild_id: int) -> None: + """Creates a blank guild configuration file. + + Args: + guild_id (int): The ID of the guild whose configuration should be created. + """ + _write_guild_json_file(guild_id, munch.Munch()) From 54d0ae5faf59384e82a23733633ccc2407a61c47 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:50:01 -0700 Subject: [PATCH 28/40] Formatting with pylint --- techsupport_bot/functions/automod.py | 1 - techsupport_bot/functions/logger.py | 1 - 2 files changed, 2 deletions(-) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 9aae2a148..f5815e72b 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -9,7 +9,6 @@ import configuration import discord -import munch from botlogging import LogContext, LogLevel from commands import moderator, modlog from core import auxiliary, cogs, moderation diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index ea5373b59..9d6abbe9e 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -7,7 +7,6 @@ import configuration import discord -import munch from botlogging import LogContext, LogLevel from core import auxiliary, cogs from discord.ext import commands From 083933463ab4e56cb17862a0a885d5e17cb16c5c Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:53:42 -0700 Subject: [PATCH 29/40] Formatting with flake8 --- techsupport_bot/commands/voting.py | 2 +- techsupport_bot/functions/automod.py | 3 +-- techsupport_bot/functions/logger.py | 4 +--- techsupport_bot/functions/paste.py | 1 - 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/techsupport_bot/commands/voting.py b/techsupport_bot/commands/voting.py index 175a08896..44e46219b 100644 --- a/techsupport_bot/commands/voting.py +++ b/techsupport_bot/commands/voting.py @@ -712,7 +712,7 @@ def build_vote_pass_embed( Args: vote (munch.Munch): The vote that has ended and needs an embed - config (munch.Munch): The guild config for the guild + guild (discord.Guild): The guild this vote is in Returns: discord.Embed: The embed in a ready to send state diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index f5815e72b..d4230184c 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -412,7 +412,6 @@ async def run_all_checks(message: discord.Message) -> list[AutoModPunishment]: handle_regex_string Args: - config (munch.Munch): The guild config to check with message (discord.Message): The message object to use to search Returns: @@ -437,7 +436,7 @@ def run_only_string_checks( handle_regex_string Args: - config (munch.Munch): The guild config to check with + guild (discord.Guild): The guild the message was sent in content (str): The content of the message to search Returns: diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index 9d6abbe9e..6a62803c2 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -46,7 +46,7 @@ def get_mapped_channel_object( Will return none if the channel doesn't exist in the config Args: - config (munch.Munch): The guild config where the src_channel is + guildd (discord.Guild): The guild where the src_channel is src_channel (int): The ID of the source channel Returns: @@ -77,7 +77,6 @@ async def pre_log_checks( Args: bot (bot.TechSupportBot): The bot object - config (munch.Munch): The config in the guild where the src channel is src_channel (discord.abc.GuildChannel | discord.Thread): The src channel object Returns: @@ -312,7 +311,6 @@ async def build_attachments( Args: bot (bot.TechSupportBot): the bot object - config (munch.Munch): The config from the guild message (discord.Message): The message object to log Returns: diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index 875f9cbc7..083a916d1 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -140,7 +140,6 @@ async def paste_message(self: Self, ctx: commands.Context, content: str) -> None """Moves message into a linx paste if it's too long Args: - config (munch.Munch): The guild config where the too long message was sent ctx (commands.Context): The context where the original message was sent content (str): The string content of the flagged message """ From ec4cb35c4f13f489c706e43deba5a810e03ce3c6 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:55:54 -0700 Subject: [PATCH 30/40] Fix typo in docstring --- techsupport_bot/functions/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index 6a62803c2..fb87248ec 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -46,7 +46,7 @@ def get_mapped_channel_object( Will return none if the channel doesn't exist in the config Args: - guildd (discord.Guild): The guild where the src_channel is + guild (discord.Guild): The guild where the src_channel is src_channel (int): The ID of the source channel Returns: From c29db86a3d81f0728306c4149d7e1a95bd689c98 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:33:27 -0700 Subject: [PATCH 31/40] Rewrite extension.py --- techsupport_bot/commands/extension.py | 254 ++++++++++++-------------- 1 file changed, 117 insertions(+), 137 deletions(-) diff --git a/techsupport_bot/commands/extension.py b/techsupport_bot/commands/extension.py index fb96ca905..643bce883 100644 --- a/techsupport_bot/commands/extension.py +++ b/techsupport_bot/commands/extension.py @@ -12,10 +12,8 @@ from __future__ import annotations -import json from typing import TYPE_CHECKING, Self -import configuration import discord import ui from core import auxiliary, cogs @@ -40,128 +38,45 @@ class ExtensionControl(cogs.BaseCog): The class that holds the extension commands Attributes: - extension_app_command_group (app_commands.Group): The group for the /extension commands + extension_commands (app_commands.Group): The group for the /extension commands """ - extension_app_command_group: app_commands.Group = app_commands.Group( + extension_commands: app_commands.Group = app_commands.Group( name="extension", description="...", extras={"module": "extension"} ) - @extension_app_command_group.command( - name="list_disabled", - description="Lists all disabled extensions in the current server", - extras={"module": "extension"}, - ) - async def list_disabled(self: Self, interaction: discord.Interaction) -> None: - """This will read the current guild config and list all the - extensions that are currently disabled - - Args: - interaction (discord.Interaction): The interaction that triggered the slash command - """ - missing_extensions = [ - item - for item in self.bot.extension_name_list - if item - not in configuration.get_config_entry( - interaction.guild.id, "core_enabled_extensions" - ) - ] - if len(missing_extensions) == 0: - embed = auxiliary.prepare_confirm_embed( - message="No currently loaded extensions are disabled" - ) - else: - embed = auxiliary.prepare_confirm_embed( - message=f"Disabled extensions: {missing_extensions}" - ) - await interaction.response.send_message(embed=embed) - - @app_commands.checks.has_permissions(administrator=True) - @extension_app_command_group.command( - name="enable_all", - description="Enables all loaded but disabled extensions in the guild", - extras={"module": "extension"}, - ) - async def enable_everything(self: Self, interaction: discord.Interaction) -> None: - """This will get all the disabled extensions and enable them for the current - guild. - - Args: - interaction (discord.Interaction): The interaction that triggered the slash command - """ - config = self.bot.guild_configs[str(interaction.guild.id)] - extension_list = configuration.get_config_entry( - interaction.guild.id, "core_enabled_extensions" - ) - missing_extensions = [ - item for item in self.bot.extension_name_list if item not in extension_list - ] - if len(missing_extensions) == 0: - embed = auxiliary.prepare_confirm_embed( - message="No currently loaded extensions are disabled" - ) - else: - for extension in missing_extensions: - extension_list(extension) - - extension_list.sort() - # Modify the database - await self.bot.write_new_config( - str(interaction.guild.id), json.dumps(config) - ) - - # Modify the local cache - self.bot.guild_configs[str(interaction.guild.id)] = config - - embed = auxiliary.prepare_confirm_embed( - f"I have enabled {len(missing_extensions)} for this guild." - ) - await interaction.response.send_message(embed=embed) - - @commands.check(auxiliary.bot_admin_check_context) - @commands.group( - name="extension", - brief="Executes an extension bot command", - description="Executes an extension bot command", - ) - async def extension_group(self: Self, ctx: commands.Context) -> None: - """The bare .extension command. This does nothing but generate the help message - - Args: - ctx (commands.Context): The context in which the command was run in - """ - return - - @auxiliary.with_typing - @extension_group.command( + @app_commands.check(auxiliary.bot_admin_check_interaction) + @extension_commands.command( name="status", description="Gets the status of an extension by name", - usage="[extension-name]", + extras={"module": "extension", "usage": "[extension-name]"}, ) async def extension_status( - self: Self, ctx: commands.Context, *, extension_name: str + self: Self, interaction: discord.Interaction, extension_name: str ) -> None: """Gets the status of an extension. This is a command and should be accessed via Discord. Args: - ctx (commands.Context): the context object for the message + interaction (discord.Interaction): the interaction that called this command extension_name (str): the name of the extension """ extensions_status = ( "loaded" - if ctx.bot.extensions.get( + if self.bot.extensions.get( f"{self.bot.EXTENSIONS_DIR_NAME}.{extension_name}" ) else "unloaded" ) functions_status = ( "loaded" - if ctx.bot.extensions.get(f"{self.bot.FUNCTIONS_DIR_NAME}.{extension_name}") + if self.bot.extensions.get( + f"{self.bot.FUNCTIONS_DIR_NAME}.{extension_name}" + ) else "unloaded" ) + embed = discord.Embed( title=f"Extension status for `{extension_name}`", description=f"Extension: {extensions_status}\nFunction: {functions_status}", @@ -172,64 +87,122 @@ async def extension_status( else: embed.color = discord.Color.gold() - await ctx.send(embed=embed) + await interaction.response.send_message(embed=embed) - @auxiliary.with_typing - @extension_group.command( - name="load", description="Loads an extension by name", usage="[extension-name]" + @app_commands.check(auxiliary.bot_admin_check_interaction) + @extension_commands.command( + name="load", + description="Loads an extension by name", + extras={"module": "extension", "usage": "[extension-name]"}, ) async def load_extension( - self: Self, ctx: commands.Context, *, extension_name: str + self: Self, interaction: discord.Interaction, extension_name: str ) -> None: """Loads an extension by filename. This is a command and should be accessed via Discord. Args: - ctx (commands.Context): the context object for the message + interaction (discord.Interaction): the interaction that called this command extension_name (str): the name of the extension """ + if not self.does_extension_exist: + embed = auxiliary.prepare_deny_embed(f"I could not find {extension_name}") + await interaction.response.send_message(embed=embed) + return + try: - await ctx.bot.load_extension(f"functions.{extension_name}") + await self.bot.load_extension(f"functions.{extension_name}") except (ModuleNotFoundError, commands.errors.ExtensionNotFound): - await ctx.bot.load_extension(f"commands.{extension_name}") - await auxiliary.send_confirm_embed( - message="I've loaded that extension", channel=ctx.channel + await self.bot.load_extension(f"commands.{extension_name}") + + embed = auxiliary.prepare_confirm_embed( + message=f"I've loaded the {extension_name} extension" ) + await interaction.response.send_message(embed=embed) - @auxiliary.with_typing - @extension_group.command( + @app_commands.check(auxiliary.bot_admin_check_interaction) + @extension_commands.command( name="unload", description="Unloads an extension by name", - usage="[extension-name]", + extras={"module": "extension", "usage": "[extension-name]"}, ) async def unload_extension( - self: Self, ctx: commands.Context, *, extension_name: str + self: Self, interaction: discord.Interaction, extension_name: str ) -> None: """Unloads an extension by filename. This is a command and should be accessed via Discord. Args: - ctx (commands.Context): the context object for the message + interaction (discord.Interaction): the interaction that called this command extension_name (str): the name of the extension """ + if not self.does_extension_exist: + embed = auxiliary.prepare_deny_embed(f"I could not find {extension_name}") + await interaction.response.send_message(embed=embed) + return try: - await ctx.bot.unload_extension(f"functions.{extension_name}") + await self.bot.unload_extension(f"functions.{extension_name}") except commands.errors.ExtensionNotLoaded: - await ctx.bot.unload_extension(f"commands.{extension_name}") - await auxiliary.send_confirm_embed( - message="I've unloaded that extension", channel=ctx.channel + await self.bot.unload_extension(f"commands.{extension_name}") + + embed = auxiliary.prepare_confirm_embed( + message=f"I've unloaded the {extension_name} extension" ) + await interaction.response.send_message(embed=embed) - @auxiliary.with_typing - @extension_group.command( + @app_commands.check(auxiliary.bot_admin_check_interaction) + @extension_commands.command( + name="reload", + description="Reloads an extension by name", + extras={"module": "extension", "usage": "[extension-name]"}, + ) + async def reload_extension( + self: Self, interaction: discord.Interaction, extension_name: str + ) -> None: + """Unloads an extension by filename. + + This is a command and should be accessed via Discord. + + Args: + interaction (discord.Interaction): the interaction that called this command + extension_name (str): the name of the extension + """ + if not self.does_extension_exist: + embed = auxiliary.prepare_deny_embed(f"I could not find {extension_name}") + await interaction.response.send_message(embed=embed) + return + try: + await self.bot.unload_extension(f"functions.{extension_name}") + await self.bot.load_extension(f"functions.{extension_name}") + except ( + ModuleNotFoundError, + commands.errors.ExtensionNotFound, + commands.errors.ExtensionNotLoaded, + ): + await self.bot.unload_extension(f"commands.{extension_name}") + await self.bot.load_extension(f"commands.{extension_name}") + + embed = auxiliary.prepare_confirm_embed( + message=f"I've reloaded the {extension_name} extension" + ) + await interaction.response.send_message(embed=embed) + + @app_commands.command( name="register", description="Uploads an extension from Discord to be saved on the bot", - usage="[extension-name] |python-file-upload|", + extras={ + "module": "extension", + "usage": "[extension-name] [python-file-upload]", + }, ) + @app_commands.check(auxiliary.bot_admin_check_interaction) async def register_extension( - self: Self, ctx: commands.Context, extension_name: str + self: Self, + interaction: discord.Interaction, + extension_name: str, + extension_file: discord.Attachment, ) -> None: """Unloads an extension by filename. @@ -239,18 +212,11 @@ async def register_extension( ctx (commands.Context): the context object for the message extension_name (str): the name of the extension """ - if not ctx.message.attachments: - await auxiliary.send_deny_embed( - message="You did not provide a Python file upload", channel=ctx.channel - ) - return - - attachment = ctx.message.attachments[0] - if not attachment.filename.endswith(".py"): - await auxiliary.send_deny_embed( + if not extension_file.filename.endswith(".py"): + embed = auxiliary.prepare_deny_embed( message="I don't recognize your upload as a Python file", - channel=ctx.channel, ) + await interaction.response.send_message(embed=embed) return if extension_name.lower() in await self.bot.get_potential_extensions(): @@ -258,22 +224,36 @@ async def register_extension( await view.send( message=f"Warning! This will replace the current `{extension_name}.py` " + "extension! Are you SURE?", - channel=ctx.channel, - author=ctx.author, + author=interaction.user, + interaction=interaction, ) await view.wait() if view.value is ui.ConfirmResponse.TIMEOUT: return if view.value is ui.ConfirmResponse.DENIED: - await auxiliary.send_deny_embed( - message=f"{extension_name}.py was not replaced", channel=ctx.channel + embed = auxiliary.send_deny_embed( + message=f"{extension_name}.py was not replaced" ) + await interaction.response.send_message(embed=embed) return - fp = await attachment.read() + fp = await extension_file.read() await self.bot.register_file_extension(extension_name, fp) - await auxiliary.send_confirm_embed( + embed = auxiliary.send_confirm_embed( message="I've registered that extension. You can now try loading it", - channel=ctx.channel, ) + await interaction.response.send_message(embed=embed) + return + + async def does_extension_exist(self: Self, extension_name: str) -> bool: + """Checks if a specific extension by name exists + + Args: + self (Self): _description_ + extension_name (str): The name of the extension to check + + Returns: + bool: Whether or not this extensions exists in the bot + """ + return extension_name in self.bot.extension_name_list From 12855f580df9102b5fed7f40f826bc7dbfa1067d Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:49:16 -0700 Subject: [PATCH 32/40] Fix some errors in extension register --- techsupport_bot/commands/extension.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/techsupport_bot/commands/extension.py b/techsupport_bot/commands/extension.py index 643bce883..cf850b135 100644 --- a/techsupport_bot/commands/extension.py +++ b/techsupport_bot/commands/extension.py @@ -189,7 +189,8 @@ async def reload_extension( ) await interaction.response.send_message(embed=embed) - @app_commands.command( + @app_commands.check(auxiliary.bot_admin_check_interaction) + @extension_commands.command( name="register", description="Uploads an extension from Discord to be saved on the bot", extras={ @@ -197,20 +198,18 @@ async def reload_extension( "usage": "[extension-name] [python-file-upload]", }, ) - @app_commands.check(auxiliary.bot_admin_check_interaction) async def register_extension( self: Self, interaction: discord.Interaction, extension_name: str, extension_file: discord.Attachment, ) -> None: - """Unloads an extension by filename. - - This is a command and should be accessed via Discord. + """Registers an extension by filename. Args: ctx (commands.Context): the context object for the message extension_name (str): the name of the extension + extension_file (discord.Attachement): The python file of the extension """ if not extension_file.filename.endswith(".py"): embed = auxiliary.prepare_deny_embed( From 7a4ff3d60b6ae7169a6c5021b2f2c3eb889cadf7 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 12:56:21 -0700 Subject: [PATCH 33/40] Rewrite config commands --- techsupport_bot/commands/config.py | 355 +++++++++++++----------- techsupport_bot/commands/extension.py | 5 +- techsupport_bot/configuration/config.py | 26 +- 3 files changed, 217 insertions(+), 169 deletions(-) diff --git a/techsupport_bot/commands/config.py b/techsupport_bot/commands/config.py index a7571297c..beb5fe156 100644 --- a/techsupport_bot/commands/config.py +++ b/techsupport_bot/commands/config.py @@ -7,10 +7,12 @@ import json from typing import TYPE_CHECKING, Self +import configuration import discord +import munch import ui from core import auxiliary, cogs -from discord.ext import commands +from discord import app_commands if TYPE_CHECKING: import bot @@ -28,228 +30,269 @@ async def setup(bot: bot.TechSupportBot) -> None: class ConfigControl(cogs.BaseCog): """Cog object for per-guild config control.""" - @commands.group( - name="config", - brief="Issues a config command", - description="Issues a config command", + config_commands: app_commands.Group = app_commands.Group( + name="config", description="...", extras={"module": "config"} ) - async def config_command(self: Self, ctx: commands.Context) -> None: - """The parent config command. - This is a command and should be accessed via Discord. - - Args: - ctx (commands.Context): the context object for the message - """ - return + config_extension_commands: app_commands.Group = app_commands.Group( + name="extension", + description="...", + extras={"module": "config"}, + parent=config_commands, + ) - @commands.has_permissions(administrator=True) - @commands.guild_only() - @config_command.command( - name="patch", - brief="Edits guild config", - description="Edits guild config by uploading JSON", - usage="|uploaded-json|", + @app_commands.checks.has_permissions(administrator=True) + @app_commands.guild_only() + @config_extension_commands.command( + name="enable", + description="Enables an extension for the guild by name", + extras={"module": "config", "usage": "[extension-name]"}, ) - async def patch_config(self: Self, ctx: commands.Context) -> None: - """Displays the current config to the user. + async def enable_extension( + self: Self, interaction: discord.Interaction, extension_name: str + ) -> None: + """Enables an extension for the guild. This is a command and should be accessed via Discord. Args: - ctx (commands.Context): the context object for the message + interaction (discord.Interaction): The interaction that called this command + extension_name (str): the extension subname to enable """ - config = self.bot.guild_configs[str(ctx.guild.id)] - - uploaded_data = await auxiliary.get_json_from_attachments(ctx.message) - if uploaded_data: - # server-side check of guild - if str(ctx.guild.id) != str(uploaded_data["guild_id"]): - await auxiliary.send_deny_embed( - message="This config file is not for this guild", - channel=ctx.channel, - ) - return - uploaded_data["guild_id"] = str(ctx.guild.id) - config_difference = auxiliary.config_schema_matches(uploaded_data, config) - if config_difference: - view = ui.Confirm() - await view.send( - message=f"Accept {config_difference} changes to the guild config?", - channel=ctx.channel, - author=ctx.author, - ) - await view.wait() - if view.value is ui.ConfirmResponse.DENIED: - await auxiliary.send_deny_embed( - message="Config was not changed", - channel=ctx.channel, - ) - if view.value is not ui.ConfirmResponse.CONFIRMED: - return - - # Modify the database - await self.bot.write_new_config( - str(ctx.guild.id), json.dumps(uploaded_data) + if extension_name not in self.bot.extension_name_list: + embed = auxiliary.prepare_deny_embed( + message=f"I could not find the extension {extension_name}", ) + await interaction.response.send_message(embed=embed) + return - # Modify the local cache - self.bot.guild_configs[str(ctx.guild.id)] = uploaded_data - - await auxiliary.send_confirm_embed( - message="I've updated that config", channel=ctx.channel + extensions_list: list[str] = configuration.get_config_entry( + interaction.guild.id, "core_enabled_extensions" + ) + if extension_name in extensions_list: + embed = auxiliary.prepare_deny_embed( + message=f"The extension {extension_name} is already enabled for this guild", ) + await interaction.response.send_message(embed=embed) return - json_config = config.copy() + extensions_list.append(extension_name) + extensions_list.sort() - json_config.pop("_id", None) - - json_file = discord.File( - io.StringIO(json.dumps(json_config, indent=4)), - filename=f"{ctx.guild.id}-config-{datetime.datetime.utcnow()}.json", + configuration.edit_config_entry( + interaction.guild.id, "core_enabled_extensions", extensions_list ) - await ctx.send(file=json_file) + embed = auxiliary.prepare_confirm_embed( + message="I've enabled that extension for this guild" + ) + await interaction.response.send_message(embed=embed) - @commands.has_permissions(administrator=True) - @commands.guild_only() - @config_command.command( - name="enable-extension", - brief="Enables an extension", - description="Enables an extension for the guild by name", - usage="[extension-name]", + @app_commands.checks.has_permissions(administrator=True) + @app_commands.guild_only() + @config_extension_commands.command( + name="disable", + description="Disables an extension for the guild by name", + extras={"module": "config", "usage": "[extension-name]"}, ) - async def enable_extension( - self: Self, ctx: commands.Context, extension_name: str + async def disable_extension( + self: Self, interaction: discord.Interaction, extension_name: str ) -> None: - """Enables an extension for the guild. + """Disables an extension for the guild. This is a command and should be accessed via Discord. Args: - ctx (commands.Context): the context object for the message - extension_name (str): the extension subname to enable + interaction (discord.Interaction): The interaction that called this command + extension_name (str): the extension subname to disable """ - if not ( - f"{self.bot.EXTENSIONS_DIR_NAME}.{extension_name}" in self.bot.extensions - or f"{self.bot.FUNCTIONS_DIR_NAME}.{extension_name}" in self.bot.extensions - ): - await auxiliary.send_deny_embed( - message="I could not find that extension, or it's not loaded", - channel=ctx.channel, + if extension_name not in self.bot.extension_name_list: + embed = auxiliary.prepare_deny_embed( + message=f"I could not find the extension {extension_name}", ) + await interaction.response.send_message(embed=embed) return - config = self.bot.guild_configs[str(ctx.guild.id)] - if extension_name in config.enabled_extensions: - await auxiliary.send_deny_embed( - message="That extension is already enabled for this guild", - channel=ctx.channel, + extensions_list: list[str] = configuration.get_config_entry( + interaction.guild.id, "core_enabled_extensions" + ) + if extension_name not in extensions_list: + embed = auxiliary.prepare_deny_embed( + message=f"The extension {extension_name} is already disabled for this guild", ) + await interaction.response.send_message(embed=embed) return - config.enabled_extensions.append(extension_name) - config.enabled_extensions.sort() + extensions_list.remove(extension_name) + extensions_list.sort() + + configuration.edit_config_entry( + interaction.guild.id, "core_enabled_extensions", extensions_list + ) - # Modify the database - await self.bot.write_new_config(str(ctx.guild.id), json.dumps(config)) + embed = auxiliary.prepare_confirm_embed( + message="I've disabled that extension for this guild" + ) + await interaction.response.send_message(embed=embed) - # Modify the local cache - self.bot.guild_configs[str(ctx.guild.id)] = config + @app_commands.checks.has_permissions(administrator=True) + @config_extension_commands.command( + name="list-disabled", + description="Lists all disabled extensions in the current server", + extras={"module": "config"}, + ) + async def list_disabled(self: Self, interaction: discord.Interaction) -> None: + """This will read the current guild config and list all the + extensions that are currently disabled - await auxiliary.send_confirm_embed( - message="I've enabled that extension for this guild", channel=ctx.channel + Args: + interaction (discord.Interaction): The interaction that triggered the slash command + """ + extensions_list: list[str] = configuration.get_config_entry( + interaction.guild.id, "core_enabled_extensions" ) + missing_extensions = [ + item for item in self.bot.extension_name_list if item not in extensions_list + ] + if len(missing_extensions) == 0: + embed = auxiliary.prepare_confirm_embed( + message="No currently loaded extensions are disabled" + ) + else: + embed = auxiliary.prepare_confirm_embed( + message=f"Disabled extensions: {missing_extensions}" + ) + await interaction.response.send_message(embed=embed) - @commands.has_permissions(administrator=True) - @commands.guild_only() - @config_command.command( - name="disable-extension", - brief="Disables an extension", - description="Disables an extension for the guild by name", - usage="[extension-name]", + @app_commands.checks.has_permissions(administrator=True) + @config_extension_commands.command( + name="enable-all", + description="Enables all loaded but disabled extensions in the guild", + extras={"module": "config"}, ) - async def disable_extension( - self: Self, ctx: commands.Context, extension_name: str - ) -> None: - """Disables an extension for the guild. + async def enable_everything(self: Self, interaction: discord.Interaction) -> None: + """This will get all the disabled extensions and enable them for the current + guild. - This is a command and should be accessed via Discord. + Args: + interaction (discord.Interaction): The interaction that triggered the slash command + """ + extensions_list: list[str] = configuration.get_config_entry( + interaction.guild.id, "core_enabled_extensions" + ) + missing_extensions = [ + item for item in self.bot.extension_name_list if item not in extensions_list + ] + if len(missing_extensions) == 0: + embed = auxiliary.prepare_confirm_embed( + message="No currently loaded extensions are disabled" + ) + else: + for extension in missing_extensions: + extensions_list.append(extension) + + extensions_list.sort() + # Modify the config + configuration.edit_config_entry( + interaction.guild.id, "core_enabled_extensions", extensions_list + ) + + embed = auxiliary.prepare_confirm_embed( + f"I have enabled {len(missing_extensions)} extension(s) for this guild." + ) + await interaction.response.send_message(embed=embed) + + @app_commands.checks.has_permissions(administrator=True) + @config_commands.command( + name="json", + description="This gets the guild config json file and sends it as a response", + extras={"module": "config"}, + ) + async def config_json(self: Self, interaction: discord.Interaction): + """This pulls the guild json config and send it to the caller Args: - ctx (commands.Context): the context object for the message - extension_name (str): the extension subname to disable + interaction (discord.Interaction): The interaction that triggered the slash command """ - if not ( - f"{self.bot.EXTENSIONS_DIR_NAME}.{extension_name}" in self.bot.extensions - or f"{self.bot.FUNCTIONS_DIR_NAME}.{extension_name}" in self.bot.extensions - ): - await auxiliary.send_deny_embed( - message="I could not find that extension, or it's not loaded", - channel=ctx.channel, + try: + json_config = configuration.get_guild_config_json(interaction.guild.id) + except AttributeError: + embed = auxiliary.prepare_deny_embed( + "This guild has no current configuration" ) + await interaction.response.send_message(embed=embed) return + json_file = discord.File( + io.StringIO(json.dumps(json_config, indent=4)), + filename=f"{interaction.guild.id}-config-{datetime.datetime.utcnow()}.json", + ) + await interaction.response.send_message(file=json_file) - config = self.bot.guild_configs[str(ctx.guild.id)] - if extension_name not in config.enabled_extensions: - await auxiliary.send_deny_embed( - message="That extension is already disabled for this guild", - channel=ctx.channel, + @app_commands.checks.has_permissions(administrator=True) + @config_commands.command( + name="patch", + description="Edits guild config by uploading JSON", + extras={"module": "config", "usage": "[uploaded-json]"}, + ) + async def patch_config( + self: Self, interaction: discord.Interaction, config_json: discord.Attachment + ) -> None: + """Takes the uploaded json file and writes it to disk + + Args: + interaction (discord.Interaction): The interaction that triggered the slash command + """ + await interaction.response.defer() + + if not config_json.filename.endswith(".json"): + embed = auxiliary.prepare_deny_embed( + message="I don't recognize your upload as a json file", ) + await interaction.followup.send(embed=embed) return - config.enabled_extensions = [ - extension - for extension in config.enabled_extensions - if extension != extension_name - ] - - # Modify the database - await self.bot.write_new_config(str(ctx.guild.id), json.dumps(config)) + json_bytes: bytes = await config_json.read() + json_data: munch.Munch = munch.munchify(json.loads(json_bytes.decode("utf-8"))) - # Modify the local cache - self.bot.guild_configs[str(ctx.guild.id)] = config + configuration.write_guild_config_json(interaction.guild.id, json_data) - await auxiliary.send_confirm_embed( - message="I've disabled that extension for this guild", channel=ctx.channel - ) + embed = auxiliary.prepare_confirm_embed("I have updated this guilds config") + await interaction.followup.send(embed=embed) - @commands.has_permissions(administrator=True) - @commands.guild_only() - @config_command.command( + @app_commands.checks.has_permissions(administrator=True) + @config_commands.command( name="reset", - brief="Resets current guild config", description="Resets config to default for the current guild", + extras={"module": "config"}, ) - async def reset_config(self: Self, ctx: commands.Context) -> None: + async def reset_config(self: Self, interaction: discord.Interaction) -> None: """A function to reset the current guild config to stock Args: - ctx (commands.Context): The context in which the command was run + interaction (discord.Interaction): The interaction that triggered the slash command """ view = ui.Confirm() await view.send( - message=f"Are you sure you want to reset the config for {ctx.guild.name}?", - channel=ctx.channel, - author=ctx.author, + message=f"Are you sure you want to reset the config for {interaction.guild.name}?", + author=interaction.user, + interaction=interaction, ) await view.wait() if view.value == ui.ConfirmResponse.DENIED: - await auxiliary.send_deny_embed( + embed = auxiliary.prepare_deny_embed( message="The config was not reset", - channel=ctx.channel, ) + await view.followup.send(embed=embed) return if view.value == ui.ConfirmResponse.TIMEOUT: return - # Modify the database - await self.bot.write_new_config(str(ctx.guild.id), "false") + default_config = configuration.get_default_config_json() + default_config.core_guild_id = interaction.guild.id + + configuration.write_guild_config_json(interaction.guild.id, default_config) - # Modify the local cache - self.bot.guild_configs[str(ctx.guild.id)] = False - await self.bot.create_new_context_config(guild_id=str(ctx.guild.id)) - await auxiliary.send_confirm_embed( - message="I've reset the config for this guild", channel=ctx.channel + embed = auxiliary.prepare_confirm_embed( + message="I've reset the config for this guild" ) + view.followup.send(embed=embed) diff --git a/techsupport_bot/commands/extension.py b/techsupport_bot/commands/extension.py index cf850b135..0dbe121c4 100644 --- a/techsupport_bot/commands/extension.py +++ b/techsupport_bot/commands/extension.py @@ -234,7 +234,7 @@ async def register_extension( embed = auxiliary.send_deny_embed( message=f"{extension_name}.py was not replaced" ) - await interaction.response.send_message(embed=embed) + await view.followup.send(embed=embed) return fp = await extension_file.read() @@ -242,14 +242,13 @@ async def register_extension( embed = auxiliary.send_confirm_embed( message="I've registered that extension. You can now try loading it", ) - await interaction.response.send_message(embed=embed) + await view.followup.send(embed=embed) return async def does_extension_exist(self: Self, extension_name: str) -> bool: """Checks if a specific extension by name exists Args: - self (Self): _description_ extension_name (str): The name of the extension to check Returns: diff --git a/techsupport_bot/configuration/config.py b/techsupport_bot/configuration/config.py index d6d28d6c2..eee4a9f5c 100644 --- a/techsupport_bot/configuration/config.py +++ b/techsupport_bot/configuration/config.py @@ -126,13 +126,22 @@ def edit_config_entry(guild_id: int, key: str, new_value: Any) -> None: # noqa: raise AttributeError(f"Key {key} is invalid") if not _does_guild_config_exist(guild_id): - _write_blank_guild_config(guild_id) + write_blank_guild_config(guild_id) guild_config = _read_guild_json(guild_id) guild_config[key] = new_value _write_guild_json_file(guild_id, guild_config) +def write_blank_guild_config(guild_id: int) -> None: + """Creates a blank guild configuration file. + + Args: + guild_id (int): The ID of the guild whose configuration should be created. + """ + _write_guild_json_file(guild_id, munch.Munch(core_guild_id=guild_id)) + + # Internal functions only def _check_key_valid(key: str) -> bool: """This will check if the key is valid and present in default config @@ -199,10 +208,16 @@ def _write_guild_json_file(guild_id: int, json_data: munch.Munch) -> None: Args: guild_id (int): The ID of the guild whose configuration should be written. json_data (munch.Munch): The configuration data to write. + + Raises: + AttributeError: If the guild for the passed json data does not equal the passed guild_id """ path = f"{BASE_PATH}/guild_configs/{guild_id}.json" os.makedirs(os.path.dirname(path), exist_ok=True) + if json_data.core_guild_id != guild_id: + raise AttributeError("Guild config for incorrect guild") + with open(path, "w", encoding="utf-8") as file: json.dump( munch.unmunchify(json_data), @@ -210,12 +225,3 @@ def _write_guild_json_file(guild_id: int, json_data: munch.Munch) -> None: indent=4, ensure_ascii=False, ) - - -def _write_blank_guild_config(guild_id: int) -> None: - """Creates a blank guild configuration file. - - Args: - guild_id (int): The ID of the guild whose configuration should be created. - """ - _write_guild_json_file(guild_id, munch.Munch()) From 390ebf71602919ef8b8b1d76b03511a129dd263f Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 12:59:49 -0700 Subject: [PATCH 34/40] Sort extensions on list disabled --- techsupport_bot/commands/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/techsupport_bot/commands/config.py b/techsupport_bot/commands/config.py index beb5fe156..5668e1b1e 100644 --- a/techsupport_bot/commands/config.py +++ b/techsupport_bot/commands/config.py @@ -154,6 +154,7 @@ async def list_disabled(self: Self, interaction: discord.Interaction) -> None: missing_extensions = [ item for item in self.bot.extension_name_list if item not in extensions_list ] + missing_extensions.sort() if len(missing_extensions) == 0: embed = auxiliary.prepare_confirm_embed( message="No currently loaded extensions are disabled" From 9b955c16fda1533a8a2925dd726098a4d6e28bef Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:20:21 -0700 Subject: [PATCH 35/40] Strip old config system out --- techsupport_bot/bot.py | 117 ------------------------------ techsupport_bot/core/databases.py | 20 ----- 2 files changed, 137 deletions(-) diff --git a/techsupport_bot/bot.py b/techsupport_bot/bot.py index ca4879154..de851510f 100644 --- a/techsupport_bot/bot.py +++ b/techsupport_bot/bot.py @@ -7,7 +7,6 @@ import datetime import glob import io -import json import os import threading from typing import Self @@ -69,8 +68,6 @@ def __init__( self.file_config = None # Sets up some dicts and arrays - self.guild_configs: dict[str, munch.Munch] = {} - self.extension_configs = munch.DefaultMunch(None) self.extension_states = munch.DefaultMunch(None) self.command_rate_limit_bans: expiringdict.ExpiringDict[str, bool] = ( expiringdict.ExpiringDict( @@ -178,13 +175,6 @@ async def setup_hook(self: Self) -> None: databases.setup_models(self) await self.db.gino.create_all() - # Load all guild config objects into self.guild_configs object - all_config = await self.models.Config.query.gino.all() - for config in all_config: - self.guild_configs[config.guild_id] = munch.munchify( - json.loads(config.config) - ) - # Adds persistent views to the bot self.add_view(ui.VotingButtonPersistent()) @@ -285,113 +275,6 @@ async def on_message(self: Self, message: discord.Message) -> None: await self.process_commands(message) - # Guild config management functions - - async def register_new_guild_config(self: Self, guild_id: str) -> bool: - """This creates a config for a new guild if needed - - Args: - guild_id (str): The id of the guild to create config for, in string form - - Returns: - bool: True if a config was created, False if a config already existed - """ - async with self.guild_config_lock: - try: - config = self.guild_configs[guild_id] - except KeyError: - config = None - if not config: - await self.create_new_context_config(guild_id) - return True - return False - - async def create_new_context_config(self: Self, guild_id: str) -> munch.Munch: - """Creates a new guild config for a given guild. - - Args: - guild_id (str): The guild ID the config will be for. Only used for storing the config - - Returns: - munch.Munch: The new config object ready to use - """ - extensions_config = munch.DefaultMunch(None) - - for extension_name, extension_config in self.extension_configs.items(): - if extension_config: - # don't attach to guild config if extension isn't configurable - extensions_config[extension_name] = munch.munchify( - extension_config.data - ) - self.extension_name_list.sort() - - config_ = munch.DefaultMunch(None) - - config_.guild_id = str(guild_id) - config_.command_prefix = self.file_config.bot_config.default_prefix - config_.logging_channel = None - config_.member_events_channel = None - config_.guild_events_channel = None - config_.private_channels = [] - config_.enabled_extensions = self.extension_name_list - config_.nickname_filter = False - config_.enable_logging = True - config_.rate_limit = munch.DefaultMunch(None) - config_.rate_limit.enabled = False - config_.rate_limit.commands = 4 - config_.rate_limit.time = 10 - config_.moderation = munch.DefaultMunch(None) - config_.moderation.max_warnings = 3 - config_.moderation.alert_channel = None - - config_.extensions = extensions_config - - try: - await self.logger.send_log( - message=f"Inserting new config for lookup key: {guild_id}", - level=LogLevel.DEBUG, - context=LogContext(guild=self.get_guild(guild_id)), - console_only=True, - ) - # Modify the database - await self.write_new_config(str(guild_id), json.dumps(config_)) - - # Modify the local cache - self.guild_configs[guild_id] = config_ - - except Exception as exception: - # safely finish because the new config is still useful - await self.logger.send_log( - message="Could not insert guild config into Postgres", - level=LogLevel.ERROR, - context=LogContext(guild=self.get_guild(guild_id)), - exception=exception, - ) - - return config_ - - async def write_new_config(self: Self, guild_id: str, config: str) -> None: - """Takes a config and guild and updates the config in the database - This is only needed when a new guild is joined or the config is modifed - - Args: - guild_id (str): The str ID of the guild the config belongs to - config (str): The str representation of the json config - """ - database_config = await self.models.Config.query.where( - self.models.Config.guild_id == guild_id - ).gino.first() - if database_config: - await database_config.update( - config=str(config), update_time=datetime.datetime.utcnow() - ).apply() - else: - new_database_config = self.models.Config( - guild_id=str(guild_id), - config=str(config), - ) - await new_database_config.create() - # File config loading functions def load_file_config(self: Self, validate: bool = True) -> None: diff --git a/techsupport_bot/core/databases.py b/techsupport_bot/core/databases.py index c58c08050..c55276742 100644 --- a/techsupport_bot/core/databases.py +++ b/techsupport_bot/core/databases.py @@ -268,26 +268,6 @@ class Warning(bot.db.Model): time = bot.db.Column(bot.db.DateTime, default=datetime.datetime.utcnow) invoker_id = bot.db.Column(bot.db.String) - class Config(bot.db.Model): - """The postgres table for guild config - Currently used nearly everywhere - - Attributes: - pk (int): The primary key for the database - guild_id (str): The ID of the guild this config is for - config (str): The config text - update_time (datetime.datetime): The time the config was last updated - """ - - __tablename__ = "guild_config" - - pk: int = bot.db.Column(bot.db.Integer, primary_key=True) - guild_id: str = bot.db.Column(bot.db.String) - config: str = bot.db.Column(bot.db.String) - update_time: datetime.datetime = bot.db.Column( - bot.db.DateTime, default=datetime.datetime.utcnow - ) - class Listener(bot.db.Model): """The postgres table for listeners Currently used in listen.py From 674f8e80bae40884109e4a005410bcd147e4535b Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:23:29 -0700 Subject: [PATCH 36/40] Fix some bugs --- techsupport_bot/bot.py | 5 ----- techsupport_bot/core/databases.py | 1 - 2 files changed, 6 deletions(-) diff --git a/techsupport_bot/bot.py b/techsupport_bot/bot.py index de851510f..159e1652f 100644 --- a/techsupport_bot/bot.py +++ b/techsupport_bot/bot.py @@ -193,7 +193,6 @@ async def on_guild_join(self: Self, guild: discord.Guild) -> None: Args: guild (discord.Guild): the guild that was joined """ - self.register_new_guild_config(str(guild.id)) for cog in self.cogs.values(): if getattr(cog, "COG_TYPE", "").lower() == "loop": try: @@ -216,10 +215,6 @@ async def on_ready(self: Self) -> None: ) await self.get_owner() - # Ensure all guilds have a config - for guild in self.guilds: - await self.register_new_guild_config(str(guild.id)) - # DM Logging async def log_DM(self: Self, sent_from: str, source: str, content: str) -> None: diff --git a/techsupport_bot/core/databases.py b/techsupport_bot/core/databases.py index c55276742..57bdc22a6 100644 --- a/techsupport_bot/core/databases.py +++ b/techsupport_bot/core/databases.py @@ -377,7 +377,6 @@ class XP(bot.db.Model): bot.models.ModmailBan = ModmailBan bot.models.UserNote = UserNote bot.models.Warning = Warning - bot.models.Config = Config bot.models.Listener = Listener bot.models.Rule = Rule bot.models.Votes = Votes From ed58bca9820c4602d1feccf2e098c3b7a0100692 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:27:37 -0700 Subject: [PATCH 37/40] Docstring updates --- documentation/core/bot.md | 8 -------- techsupport_bot/commands/config.py | 11 +++++++++-- techsupport_bot/commands/extension.py | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/documentation/core/bot.md b/documentation/core/bot.md index 874c2a4af..ff9314314 100644 --- a/documentation/core/bot.md +++ b/documentation/core/bot.md @@ -19,14 +19,6 @@ The bot.py file is the primary file of the program. It contains all of the core ### on_message -## Guild config management functions - -### register_new_guild_config - -### create_new_context_config - -### write_new_config - ## File config loading functions ### load_file_config diff --git a/techsupport_bot/commands/config.py b/techsupport_bot/commands/config.py index 5668e1b1e..e9f8470e8 100644 --- a/techsupport_bot/commands/config.py +++ b/techsupport_bot/commands/config.py @@ -28,7 +28,13 @@ async def setup(bot: bot.TechSupportBot) -> None: class ConfigControl(cogs.BaseCog): - """Cog object for per-guild config control.""" + """ + Cog object for per-guild config control. + + Attributes: + config_commands (app_commands.Group): The group for the /config commands + config_extension_commands (app_commands.Group): The sub-group for /config extension + """ config_commands: app_commands.Group = app_commands.Group( name="config", description="...", extras={"module": "config"} @@ -209,7 +215,7 @@ async def enable_everything(self: Self, interaction: discord.Interaction) -> Non description="This gets the guild config json file and sends it as a response", extras={"module": "config"}, ) - async def config_json(self: Self, interaction: discord.Interaction): + async def config_json(self: Self, interaction: discord.Interaction) -> None: """This pulls the guild json config and send it to the caller Args: @@ -242,6 +248,7 @@ async def patch_config( Args: interaction (discord.Interaction): The interaction that triggered the slash command + config_json (discord.Attachment): The json file of the new guild config """ await interaction.response.defer() diff --git a/techsupport_bot/commands/extension.py b/techsupport_bot/commands/extension.py index 0dbe121c4..e0092215a 100644 --- a/techsupport_bot/commands/extension.py +++ b/techsupport_bot/commands/extension.py @@ -207,7 +207,7 @@ async def register_extension( """Registers an extension by filename. Args: - ctx (commands.Context): the context object for the message + interaction (discord.Interaction): the interaction that called this command extension_name (str): the name of the extension extension_file (discord.Attachement): The python file of the extension """ From 9b5dcd33e6e75515b22764e34af731655a23de49 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:29:13 -0700 Subject: [PATCH 38/40] Fix typo in docstring --- techsupport_bot/commands/extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/extension.py b/techsupport_bot/commands/extension.py index e0092215a..1cee1d43d 100644 --- a/techsupport_bot/commands/extension.py +++ b/techsupport_bot/commands/extension.py @@ -209,7 +209,7 @@ async def register_extension( Args: interaction (discord.Interaction): the interaction that called this command extension_name (str): the name of the extension - extension_file (discord.Attachement): The python file of the extension + extension_file (discord.Attachment): The python file of the extension """ if not extension_file.filename.endswith(".py"): embed = auxiliary.prepare_deny_embed( From e4fb3491df13e8dc9ac0e4f1ec1eb37db85e8a30 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:37:42 -0700 Subject: [PATCH 39/40] Update docstring --- techsupport_bot/configuration/config.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/techsupport_bot/configuration/config.py b/techsupport_bot/configuration/config.py index eee4a9f5c..516a91a82 100644 --- a/techsupport_bot/configuration/config.py +++ b/techsupport_bot/configuration/config.py @@ -1,17 +1,5 @@ """ -Normal usage: -get_config_entry(guild, key) -get_default_config_entry(key) - - -/config commands: -get_json_config(guild) -update_json_config(guild, config_json) - -Backend commands: -generate_blank_config_file() -check_key_valid(key) - +This file contains all the functions needed to manage the guild config system """ import json From 3eebe004536407c5b56f7911a57f9da8cbe8894a Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:41:38 -0700 Subject: [PATCH 40/40] Move some functions around --- techsupport_bot/configuration/config.py | 62 ++++++++++++------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/techsupport_bot/configuration/config.py b/techsupport_bot/configuration/config.py index 516a91a82..f4e395ec5 100644 --- a/techsupport_bot/configuration/config.py +++ b/techsupport_bot/configuration/config.py @@ -30,7 +30,7 @@ def get_config_entry(guild_id: int, key: str) -> Any: # noqa: ANN401 if not _check_key_valid(key): raise AttributeError(f"Key {key} is invalid") - default_entry = get_default_config_entry(key) + default_entry = _get_default_config_entry(key) if not _does_guild_config_exist(guild_id): return default_entry @@ -42,26 +42,6 @@ def get_config_entry(guild_id: int, key: str) -> Any: # noqa: ANN401 return default_entry -def get_default_config_entry(key: str) -> Any: # noqa: ANN401 - """This gets the value from the default config file for the passed key - - Args: - key (str): The key to search the config file for - - Returns: - Any: The value from the default config file - - Raises: - AttributeError: Raised if the passed key is not valid - """ - if not _check_key_valid(key): - raise AttributeError(f"Key {key} is invalid") - - default_config = _read_json_file("config.default.json") - - return default_config[key] - - def get_default_config_json() -> munch.Munch: """This gets a munified versions of the default config file @@ -114,22 +94,13 @@ def edit_config_entry(guild_id: int, key: str, new_value: Any) -> None: # noqa: raise AttributeError(f"Key {key} is invalid") if not _does_guild_config_exist(guild_id): - write_blank_guild_config(guild_id) + _write_blank_guild_config(guild_id) guild_config = _read_guild_json(guild_id) guild_config[key] = new_value _write_guild_json_file(guild_id, guild_config) -def write_blank_guild_config(guild_id: int) -> None: - """Creates a blank guild configuration file. - - Args: - guild_id (int): The ID of the guild whose configuration should be created. - """ - _write_guild_json_file(guild_id, munch.Munch(core_guild_id=guild_id)) - - # Internal functions only def _check_key_valid(key: str) -> bool: """This will check if the key is valid and present in default config @@ -213,3 +184,32 @@ def _write_guild_json_file(guild_id: int, json_data: munch.Munch) -> None: indent=4, ensure_ascii=False, ) + + +def _write_blank_guild_config(guild_id: int) -> None: + """Creates a blank guild configuration file. + + Args: + guild_id (int): The ID of the guild whose configuration should be created. + """ + _write_guild_json_file(guild_id, munch.Munch(core_guild_id=guild_id)) + + +def _get_default_config_entry(key: str) -> Any: # noqa: ANN401 + """This gets the value from the default config file for the passed key + + Args: + key (str): The key to search the config file for + + Returns: + Any: The value from the default config file + + Raises: + AttributeError: Raised if the passed key is not valid + """ + if not _check_key_valid(key): + raise AttributeError(f"Key {key} is invalid") + + default_config = _read_json_file("config.default.json") + + return default_config[key]