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/documentation/Extension-howto.md b/documentation/Extension-howto.md index 74641cbbc..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. @@ -262,14 +230,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: -```py -config = self.bot.guild_configs[guild_id] -``` - -Afterwards you can access the values with +To access the config for a given guild, use the "get_config_entry" in the configuration module: ```py -config.extensions.<Ext-name>.<Value-name>.value +import configuration +value = configuration.get_config_entry(guild.id, "config_key") ``` ## Calling an API diff --git a/documentation/core/bot.md b/documentation/core/bot.md index 6cc160286..ff9314314 100644 --- a/documentation/core/bot.md +++ b/documentation/core/bot.md @@ -19,18 +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 - -### 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 eefe70a6e..159e1652f 100644 --- a/techsupport_bot/bot.py +++ b/techsupport_bot/bot.py @@ -7,12 +7,12 @@ import datetime import glob import io -import json import os import threading from typing import Self import botlogging +import configuration import discord import expiringdict import gino @@ -21,7 +21,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 @@ -68,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( @@ -177,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()) @@ -202,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: @@ -225,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: @@ -284,160 +270,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() - - 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: @@ -554,9 +386,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 @@ -845,10 +675,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 @@ -878,7 +705,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 @@ -886,7 +712,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 @@ -894,7 +722,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 @@ -920,8 +750,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 @@ -953,8 +784,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"] @@ -979,7 +808,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, @@ -1008,8 +837,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] @@ -1052,8 +881,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: @@ -1067,7 +894,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/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/application.py b/techsupport_bot/commands/application.py index 70530850e..e02baddfc 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -7,10 +7,10 @@ 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 +40,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 +58,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)) @@ -175,14 +88,15 @@ 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 = 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,18 +104,21 @@ 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: + 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( - config.extensions.application.notification_cron_config.value + configuration.get_config_entry( + guild.id, "application_notification_cron_config" + ) ).next() @@ -686,14 +603,21 @@ 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(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: @@ -719,12 +643,17 @@ 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(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: + 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 @@ -737,7 +666,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 +694,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( @@ -822,9 +758,12 @@ 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(config.extensions.application.management_channel.value) + int( + configuration.get_config_entry( + interaction.guild.id, "application_management_channel" + ) + ) ) embed.description = confirm_message + f"\n{message}" @@ -913,15 +852,18 @@ 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( - int(config.extensions.application.management_channel.value) + int( + configuration.get_config_entry( + guild.id, "application_management_channel" + ) + ) ) if not channel: return @@ -950,7 +892,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 = 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 +916,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 @@ -1014,13 +962,12 @@ 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( - config.extensions.application.reminder_cron_config.value + configuration.get_config_entry(guild.id, "application_reminder_cron_config") ).next() diff --git a/techsupport_bot/commands/chatgpt.py b/techsupport_bot/commands/chatgpt.py index 1784956f3..cacd779f2 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 @@ -133,12 +134,13 @@ 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 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/config.py b/techsupport_bot/commands/config.py index a7571297c..e9f8470e8 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 @@ -26,230 +28,279 @@ 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. - @commands.group( - name="config", - brief="Issues a config command", - description="Issues a config command", - ) - async def config_command(self: Self, ctx: commands.Context) -> None: - """The parent config command. + Attributes: + config_commands (app_commands.Group): The group for the /config commands + config_extension_commands (app_commands.Group): The sub-group for /config extension + """ - This is a command and should be accessed via Discord. + config_commands: app_commands.Group = app_commands.Group( + name="config", description="...", extras={"module": "config"} + ) - 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() - # Modify the database - await self.bot.write_new_config(str(ctx.guild.id), json.dumps(config)) + configuration.edit_config_entry( + interaction.guild.id, "core_enabled_extensions", extensions_list + ) - # Modify the local cache - self.bot.guild_configs[str(ctx.guild.id)] = config + embed = auxiliary.prepare_confirm_embed( + message="I've disabled that extension for this guild" + ) + await interaction.response.send_message(embed=embed) + + @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 + ] + missing_extensions.sort() + 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) -> None: + """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 + config_json (discord.Attachment): The json file of the new guild config + """ + 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/duck.py b/techsupport_bot/commands/duck.py index d59660068..c749aa754 100644 --- a/techsupport_bot/commands/duck.py +++ b/techsupport_bot/commands/duck.py @@ -9,11 +9,11 @@ from datetime import timedelta from typing import TYPE_CHECKING, Self +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 @@ -27,81 +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="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", - title="Success rate (percent %)", - 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) class DuckHunt(cogs.LoopCog): @@ -127,29 +53,28 @@ 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, _: 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( - 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, ) ) async def execute( self: Self, - config: munch.Munch, guild: discord.Guild, channel: discord.TextChannel, banned_user: discord.User = None, @@ -158,14 +83,15 @@ 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. """ 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, @@ -174,7 +100,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,17 +122,18 @@ 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 + 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, @@ -260,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, @@ -333,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, @@ -342,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 @@ -367,38 +291,47 @@ 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..." ) ) 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)) + 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 {config.extensions.duck.cooldown.value} " + f"You missed. Try again in {pause_time} " 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 +343,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" + ) ), ) ) @@ -737,8 +672,7 @@ 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 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" ) @@ -765,7 +699,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() @@ -783,8 +717,7 @@ 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 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" ) @@ -807,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.", @@ -838,8 +771,7 @@ 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 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" ) @@ -880,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.", @@ -959,30 +891,29 @@ 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 = 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) + 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", 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: - 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 """ 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/commands/dumpdbg.py b/techsupport_bot/commands/dumpdbg.py index 1aa4173fd..d8c798a38 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): @@ -66,9 +57,8 @@ 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 = 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( @@ -139,7 +129,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 92f585774..3a8268c92 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 @@ -74,16 +75,13 @@ 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 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( - 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/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/commands/extension.py b/techsupport_bot/commands/extension.py index 7cd53ef11..1cee1d43d 100644 --- a/techsupport_bot/commands/extension.py +++ b/techsupport_bot/commands/extension.py @@ -12,7 +12,6 @@ from __future__ import annotations -import json from typing import TYPE_CHECKING, Self import discord @@ -39,125 +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 - """ - 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 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)] - missing_extensions = [ - item - for item in self.bot.extension_name_list - if item not in config.enabled_extensions - ] - if len(missing_extensions) == 0: - embed = auxiliary.prepare_confirm_embed( - message="No currently loaded extensions are disabled" - ) - else: - for extension in missing_extensions: - config.enabled_extensions.append(extension) - - config.enabled_extensions.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}", @@ -168,85 +87,135 @@ 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( - name="register", - description="Uploads an extension from Discord to be saved on the bot", - usage="[extension-name] |python-file-upload|", + @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 register_extension( - self: Self, ctx: commands.Context, extension_name: str + 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: - 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 ctx.message.attachments: - await auxiliary.send_deny_embed( - message="You did not provide a Python file upload", channel=ctx.channel - ) + 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) - attachment = ctx.message.attachments[0] - if not attachment.filename.endswith(".py"): - await auxiliary.send_deny_embed( + @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={ + "module": "extension", + "usage": "[extension-name] [python-file-upload]", + }, + ) + async def register_extension( + self: Self, + interaction: discord.Interaction, + extension_name: str, + extension_file: discord.Attachment, + ) -> None: + """Registers an extension by filename. + + Args: + interaction (discord.Interaction): the interaction that called this command + extension_name (str): the name 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( 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(): @@ -254,22 +223,35 @@ 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 view.followup.send(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 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: + 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 diff --git a/techsupport_bot/commands/factoids.py b/techsupport_bot/commands/factoids.py index 5490f7f6d..8426cb24d 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 @@ -31,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 @@ -46,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: @@ -103,9 +64,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"), ) @@ -118,9 +80,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"), ) @@ -778,23 +741,24 @@ 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 + ctx (commands.Context): The context of which the message was sent message_contents (str): The message to check Returns: bool: Whether the message starts with the prefix or not """ - return message_contents.startswith(config.extensions.factoids.prefix.value) + if not ctx.guild: + return + return message_contents.startswith( + configuration.get_config_entry(ctx.guild.id, "factoids_prefix") + ) async def response( self: Self, - config: munch.Munch, ctx: commands.Context, message_content: str, _: bool, @@ -802,7 +766,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 @@ -814,7 +777,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 - 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)) @@ -831,20 +794,22 @@ 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: 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, @@ -878,8 +843,9 @@ 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 = 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" @@ -891,8 +857,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, @@ -951,15 +918,13 @@ 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 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( - self.bot, config, channel - ) + target_logging_channel = await function_logger.pre_log_checks(self.bot, channel) if not target_logging_channel: return @@ -994,7 +959,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)) @@ -1020,23 +984,27 @@ 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: 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, @@ -1068,7 +1036,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" @@ -1082,7 +1052,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, @@ -1140,8 +1112,9 @@ async def cronjob( channel = None 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( @@ -1166,8 +1139,9 @@ async def cronjob( log_context = None 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( @@ -1188,8 +1162,9 @@ async def cronjob( log_context = None 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( @@ -1209,8 +1184,9 @@ async def cronjob( log_context = None 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( @@ -1223,28 +1199,27 @@ 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: 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." @@ -1266,8 +1241,9 @@ 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 = 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, @@ -1409,7 +1385,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: @@ -1426,9 +1401,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=( @@ -1779,11 +1753,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: @@ -1950,8 +1925,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, @@ -2473,8 +2449,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/forum.py b/techsupport_bot/commands/forum.py index 7e12a151e..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,21 +555,19 @@ 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."), ) - 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 """ 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,22 +581,19 @@ async def execute(self: Self, config: munch.Munch, 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", ) - 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) @@ -694,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: @@ -718,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, @@ -729,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 @@ -741,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/commands/gate.py b/techsupport_bot/commands/gate.py index 48c921b2c..535366315 100644 --- a/techsupport_bot/commands/gate.py +++ b/techsupport_bot/commands/gate.py @@ -4,10 +4,10 @@ from typing import TYPE_CHECKING, Self +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: @@ -20,90 +20,34 @@ 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): """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 """ - 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, 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,11 +58,14 @@ async def response( await ctx.message.delete() - if content.lower() == config.extensions.gate.verify_text.value: - roles = await self.get_roles(config, ctx) + 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 = 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" @@ -132,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", @@ -148,21 +99,18 @@ 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 """ 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 aafef1522..63869d2e6 100644 --- a/techsupport_bot/commands/hangman.py +++ b/techsupport_bot/commands/hangman.py @@ -6,9 +6,10 @@ import uuid 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 from discord.ext import commands @@ -22,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: @@ -289,9 +280,10 @@ 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 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 @@ -489,18 +481,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/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/joke.py b/techsupport_bot/commands/joke.py index ce2892e91..50e678c75 100644 --- a/techsupport_bot/commands/joke.py +++ b/techsupport_bot/commands/joke.py @@ -4,9 +4,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: @@ -20,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): @@ -52,42 +34,40 @@ 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 """ 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" @@ -122,8 +102,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/kanye.py b/techsupport_bot/commands/kanye.py index 66e21e34d..3cce7a92f 100644 --- a/techsupport_bot/commands/kanye.py +++ b/techsupport_bot/commands/kanye.py @@ -6,9 +6,9 @@ import random 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: @@ -21,31 +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): @@ -92,33 +69,34 @@ 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 Args: - config (munch.Munch): The guild config where the loop is taking place guild (discord.Guild): The guild where the loop is taking place """ 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 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 + guild (discord.Guild): The guild config where the loop is taking place """ 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 7a467b750..d1bc7ae3c 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -5,12 +5,13 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING, Self +import configuration import dateparser import discord 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: @@ -23,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): @@ -105,8 +88,9 @@ async def handle_ban_user( return 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( @@ -476,20 +460,21 @@ 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 ) 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 +493,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 +548,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, @@ -750,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" @@ -765,7 +754,9 @@ 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 475605060..4b6e05cfd 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -6,10 +6,11 @@ from collections import Counter 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 @@ -23,16 +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): @@ -214,8 +206,7 @@ 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(config): + if not self.extension_enabled(guild): return entry = None @@ -250,8 +241,7 @@ 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(config): + if not self.extension_enabled(guild): return entry = None @@ -287,8 +277,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: @@ -312,11 +303,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 @@ -343,8 +332,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: @@ -360,11 +350,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 4f348d2b9..c6a6bff63 100644 --- a/techsupport_bot/commands/modmail.py +++ b/techsupport_bot/commands/modmail.py @@ -18,26 +18,23 @@ from datetime import datetime from typing import TYPE_CHECKING, Self +import configuration import discord import expiringdict -import munch import ui -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: 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 +45,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 +59,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: @@ -302,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]) @@ -350,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 = 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( @@ -370,7 +368,9 @@ 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, ) @@ -426,7 +426,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 @@ -506,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(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}> " @@ -553,7 +556,9 @@ 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( @@ -842,57 +847,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): @@ -959,9 +914,8 @@ 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, config) + await has_modmail_management_role(message) except commands.MissingAnyRole as e: await auxiliary.send_deny_embed(message=f"{e}", channel=message.channel) return @@ -1098,12 +1052,12 @@ 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) - not in config.extensions.factoids.restricted_list.value + not in configuration.get_config_entry( + message.guild.id, "factoids_restricted_list" + ) ): return @@ -1115,7 +1069,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]: @@ -1358,11 +1312,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", @@ -1404,16 +1355,17 @@ 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/commands/news.py b/techsupport_bot/commands/news.py index 957d12629..c3982a1b0 100644 --- a/techsupport_bot/commands/news.py +++ b/techsupport_bot/commands/news.py @@ -7,10 +7,11 @@ from typing import TYPE_CHECKING, Self import aiocron +import configuration 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: @@ -34,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): @@ -172,30 +142,33 @@ 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 Args: - config (munch.Munch): The guild config for the guild looping guild (discord.Guild): The guild where the loop is running """ - 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") 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, @@ -206,13 +179,15 @@ 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 + guild (discord.Guild): The guild where the loop will occur """ - 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", @@ -236,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") @@ -254,7 +229,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 2afe3dd6a..5eaeb3f28 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -4,10 +4,11 @@ from typing import TYPE_CHECKING, Self +import configuration 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 @@ -21,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: @@ -74,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 @@ -106,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 @@ -171,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 @@ -189,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: @@ -259,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( @@ -310,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(config): + 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 @@ -326,7 +300,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/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/relay.py b/techsupport_bot/commands/relay.py index 4aee8d976..91d72ad3d 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -4,9 +4,9 @@ from typing import TYPE_CHECKING, Self +import configuration import discord import irc.client -import munch import ui from bidict import bidict from core import auxiliary, cogs @@ -62,13 +62,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 @@ -97,7 +94,6 @@ async def match( async def response( self: Self, - config: munch.Munch, ctx: commands.Context, content: str, result: str, @@ -105,7 +101,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 @@ -414,10 +409,11 @@ 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 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"] + discord_channel.guild, split_message["content"] ) automod_final = automod.process_automod_violations(automod_actions) if automod_final and automod_final.delete_message: @@ -432,7 +428,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 @@ -450,12 +450,13 @@ 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 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 + self.bot, discord_channel ) if not target_logging_channel: return diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index 1d25c2dfb..959b7ef9d 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): @@ -74,9 +52,9 @@ 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 = 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 +113,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 +129,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/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 774387bcc..44e46219b 100644 --- a/techsupport_bot/commands/voting.py +++ b/techsupport_bot/commands/voting.py @@ -14,10 +14,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: @@ -30,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): @@ -124,13 +88,11 @@ 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( member=interaction.user, channel=channel, - config=config, ): embed = auxiliary.prepare_deny_embed( "You do not have rights to start that vote!" @@ -146,8 +108,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), []) @@ -209,15 +171,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]] = [] @@ -233,7 +193,6 @@ async def vote_channel_autocomplete( if not self.user_can_use_vote_channel( member=member, channel=channel, - config=config, ): continue @@ -250,14 +209,12 @@ 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 @@ -265,10 +222,12 @@ def user_can_use_vote_channel( 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)) @@ -327,11 +286,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: @@ -634,20 +594,15 @@ 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 """ # pylint: disable=C0121 @@ -658,7 +613,7 @@ async def execute(self: Self, config: munch.Munch, 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()) @@ -672,7 +627,7 @@ async def execute(self: Self, config: munch.Munch, 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 @@ -684,13 +639,12 @@ async def execute(self: Self, config: munch.Munch, 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 @@ -699,7 +653,6 @@ 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 """ # Get all eligible voters @@ -725,20 +678,17 @@ 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 """ 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( @@ -755,14 +705,14 @@ async def end_vote( ) 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 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 @@ -775,7 +725,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/commands/whois.py b/techsupport_bot/commands/whois.py index 1e983783a..9beed4d2b 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, xp @@ -66,16 +67,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) @@ -109,7 +112,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( @@ -125,10 +128,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/commands/xp.py b/techsupport_bot/commands/xp.py index 9410e223b..2197d552c 100644 --- a/techsupport_bot/commands/xp.py +++ b/techsupport_bot/commands/xp.py @@ -5,10 +5,10 @@ import random from typing import TYPE_CHECKING, Self +import configuration import discord import expiringdict -import munch -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): @@ -123,13 +99,10 @@ async def top_xp_command(self: Self, interaction: discord.Interaction) -> None: await interaction.followup.send(embed=embed) - 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: @@ -144,11 +117,15 @@ async def match( 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 @@ -162,8 +139,12 @@ async def match( return False # Ignore messages that are factoid calls - if "factoids" in config.enabled_extensions: - factoid_prefix = config.extensions.factoids.prefix.value + if "factoids" in configuration.get_config_entry( + ctx.guild.id, "core_enabled_extensions" + ): + factoid_prefix = configuration.get_config_entry( + ctx.guild.id, "factoids_prefix" + ) if ctx.message.clean_content.startswith(factoid_prefix): return False @@ -179,13 +160,12 @@ 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 """ @@ -205,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 @@ -303,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/__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..f578e87e3 --- /dev/null +++ b/techsupport_bot/configuration/config.default.json @@ -0,0 +1,116 @@ +{ + "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 * * *", + "automod_alert_channel": "", + "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": [], + "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": [], + "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", + "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", + "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": [], + "joke_apply_in_nsfw_channels": false, + "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": "", + "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, + "rate_limit_commands": 4, + "rate_limit_enabled": false, + "rate_limit_time": 10, + "report_alert_channel": "", + "report_anonymous": false, + "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 new file mode 100644 index 000000000..c543e9a65 --- /dev/null +++ b/techsupport_bot/configuration/config.meta.json @@ -0,0 +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" + }, + "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/configuration/config.py b/techsupport_bot/configuration/config.py new file mode 100644 index 000000000..f4e395ec5 --- /dev/null +++ b/techsupport_bot/configuration/config.py @@ -0,0 +1,215 @@ +""" +This file contains all the functions needed to manage the guild config system +""" + +import json +import os +from pathlib import Path +from typing import Any + +import munch + +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 + + 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 + + Raises: + AttributeError: Raised if the passed key is not valid + """ + + 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_guild_json(guild_id) + + if key in guild_config: + return guild_config[key] + return default_entry + + +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: + 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)) + + +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. + + 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), + 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(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] diff --git a/techsupport_bot/core/cogs.py b/techsupport_bot/core/cogs.py index be95a762c..3038a9382 100644 --- a/techsupport_bot/core/cogs.py +++ b/techsupport_bot/core/cogs.py @@ -6,8 +6,8 @@ from collections.abc import Awaitable, Callable from typing import TYPE_CHECKING, Any, Self +import configuration import discord -import munch from botlogging import LogContext, LogLevel from discord.ext import commands @@ -74,19 +74,19 @@ 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: - """Checks if an extension is currently enabled for a given config. + def extension_enabled(self: Self, guild: discord.Guild) -> bool: + """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 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 @@ -115,11 +115,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 ( @@ -128,20 +124,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.id, "core_logging_channel" + ) await self.bot.logger.send_log( message=f"Match cog error: {self.__class__.__name__} {exception}!", level=LogLevel.ERROR, @@ -150,13 +147,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 @@ -168,7 +162,6 @@ async def match( async def response( self: Self, - _config: munch.Munch, _ctx: commands.Context, _content: str, _result: bool, @@ -176,7 +169,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() @@ -217,12 +209,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] = [ @@ -281,12 +272,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( @@ -346,53 +334,57 @@ 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 str(target_channel.id) not in channels_list: # 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: - 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, ) @@ -401,14 +393,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 """ @@ -417,11 +407,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() diff --git a/techsupport_bot/core/databases.py b/techsupport_bot/core/databases.py index c58c08050..57bdc22a6 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 @@ -397,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 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 be57fa4ef..d4230184c 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -7,11 +7,11 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Self +import configuration import discord -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: @@ -24,62 +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="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="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", - 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 @@ -154,20 +99,19 @@ 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 """ - 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, @@ -179,7 +123,9 @@ async def match( 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 @@ -187,7 +133,6 @@ async def match( async def response( self: Self, - config: munch.Munch, ctx: commands.Context, content: str, result: bool, @@ -195,17 +140,15 @@ 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 """ - # 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 @@ -236,13 +179,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" ), ) @@ -261,7 +207,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, ) @@ -295,12 +244,13 @@ 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(config.extensions.automod.alert_channel.value) + int( + configuration.get_config_entry( + ctx.guild.id, "automod_alert_channel" + ) + ) ) except TypeError: alert_channel = None @@ -324,8 +274,7 @@ async def on_raw_message_edit( if not guild: 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) @@ -341,11 +290,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( @@ -455,9 +404,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,48 +412,49 @@ async def run_all_checks( handle_regex_string Args: - config (munch.Munch): The guild config to check with message (discord.Message): The message object to use to search 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 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: 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: @@ -514,9 +462,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( @@ -530,12 +477,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: @@ -545,7 +492,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", @@ -559,18 +508,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", @@ -582,12 +533,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: @@ -597,7 +548,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( @@ -611,12 +562,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: @@ -626,7 +577,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/autoreact.py b/techsupport_bot/functions/autoreact.py index affced92d..d431954dc 100644 --- a/techsupport_bot/functions/autoreact.py +++ b/techsupport_bot/functions/autoreact.py @@ -4,8 +4,8 @@ from typing import TYPE_CHECKING, Self -import munch -from core import auxiliary, cogs, extensionconfig +import configuration +from core import auxiliary, cogs from discord.ext import commands if TYPE_CHECKING: @@ -18,29 +18,17 @@ 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 - ) -> 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 + ctx (commands.Context): The context in which the message was sent content (str): The string content of the message Returns: @@ -48,29 +36,29 @@ 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 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 """ 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/events.py b/techsupport_bot/functions/events.py index 459c7f36e..d22862e05 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 @@ -62,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( @@ -103,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, @@ -138,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!", @@ -183,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( @@ -232,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( @@ -273,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( @@ -299,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( @@ -327,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=( @@ -367,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=( @@ -401,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( @@ -427,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}", @@ -449,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( @@ -485,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( @@ -510,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( @@ -550,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( @@ -604,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", @@ -624,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( @@ -647,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=( @@ -677,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( @@ -708,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}", @@ -734,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( @@ -760,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( @@ -782,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( @@ -811,8 +813,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/honeypot.py b/techsupport_bot/functions/honeypot.py index fc681f348..a42d83e11 100644 --- a/techsupport_bot/functions/honeypot.py +++ b/techsupport_bot/functions/honeypot.py @@ -5,9 +5,9 @@ import datetime from typing import TYPE_CHECKING, Self +import configuration import discord -import munch -from core import cogs, extensionconfig +from core import cogs from discord.ext import commands if TYPE_CHECKING: @@ -20,28 +20,16 @@ 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): """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 @@ -49,13 +37,14 @@ async def match( bool: Whether the author sent in a honeypot channel """ # 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 async def response( self: Self, - config: munch.Munch, ctx: commands.Context, content: str, result: bool, @@ -63,7 +52,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 @@ -72,10 +60,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") - # 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 diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index e9b4b8dde..fb87248ec 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -5,10 +5,10 @@ import datetime from typing import TYPE_CHECKING, Self +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,17 +21,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: @@ -50,20 +40,20 @@ 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 Args: - config (munch.Munch): The guild config 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: 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 +69,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. @@ -88,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: @@ -96,17 +84,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, @@ -121,34 +112,30 @@ 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 """ 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 - 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 """ - 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, @@ -184,10 +171,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: @@ -320,13 +305,12 @@ 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 Args: bot (bot.TechSupportBot): the bot object - config (munch.Munch): The config from the guild message (discord.Message): The message object to log Returns: @@ -341,7 +325,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 b7f84ca63..3b8b12fa0 100644 --- a/techsupport_bot/functions/nickname.py +++ b/techsupport_bot/functions/nickname.py @@ -13,8 +13,8 @@ import string from typing import TYPE_CHECKING, Self +import configuration import discord -import munch from botlogging import LogContext, LogLevel from core import cogs from discord.ext import commands @@ -82,13 +82,10 @@ 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: - config (munch.Munch): The guild config ctx (commands.Context): The context that sent the message content (str): The content of the message @@ -104,7 +101,6 @@ async def match( async def response( self: Self, - config: munch.Munch, ctx: commands.Context, content: str, result: bool, @@ -112,7 +108,6 @@ 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 @@ -132,7 +127,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, @@ -149,10 +146,8 @@ 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 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) @@ -168,7 +163,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 ef8d07447..083a916d1 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -5,10 +5,10 @@ import io from typing import TYPE_CHECKING, Self +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 @@ -22,56 +22,16 @@ 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): """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 @@ -79,7 +39,9 @@ async def match( bool: Whether the message should be inspected for a paste """ # 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, @@ -91,7 +53,9 @@ async def match( 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 @@ -99,7 +63,6 @@ async def match( async def response( self: Self, - config: munch.Munch, ctx: commands.Context, content: str, result: bool, @@ -107,20 +70,24 @@ 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 """ - 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", []): - automod_actions = await automod.run_all_checks(config, ctx.message) + 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" + ): + 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 - 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 @@ -147,8 +114,7 @@ async def on_raw_message_edit( if not guild: 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) @@ -164,23 +130,22 @@ 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 - ) -> 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: - 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 """ - 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=( @@ -193,7 +158,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( @@ -233,13 +198,12 @@ 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 @@ -273,7 +237,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