From 52e2965f5a2167f296ca31a10fd0c93f7aa36144 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 3 May 2025 17:24:59 -0400 Subject: [PATCH 1/3] De classes logger. Sets up framework to allow bot messages to be passed to logger --- techsupport_bot/commands/echo.py | 25 +- techsupport_bot/commands/factoids.py | 54 +++- techsupport_bot/commands/relay.py | 23 +- techsupport_bot/functions/__init__.py | 1 + techsupport_bot/functions/logger.py | 372 +++++++++++++++++--------- 5 files changed, 345 insertions(+), 130 deletions(-) diff --git a/techsupport_bot/commands/echo.py b/techsupport_bot/commands/echo.py index c212542dd..b83ad8a62 100644 --- a/techsupport_bot/commands/echo.py +++ b/techsupport_bot/commands/echo.py @@ -14,6 +14,7 @@ from core import auxiliary, cogs from discord.ext import commands +from functions import logger as function_logger if TYPE_CHECKING: import bot @@ -72,10 +73,32 @@ async def echo_channel( ) return - await channel.send(content=message) + 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: + return + + target_logging_channel = await function_logger.pre_log_checks( + self.bot, config, channel + ) + if not target_logging_channel: + return + + await function_logger.send_message( + self.bot, + sent_message, + ctx.author, + channel, + target_logging_channel, + content_override=message, + special_flags=["Echo command"], + ) + @auxiliary.with_typing @echo.command( name="user", diff --git a/techsupport_bot/commands/factoids.py b/techsupport_bot/commands/factoids.py index 07ac00e7c..fbcf50947 100644 --- a/techsupport_bot/commands/factoids.py +++ b/techsupport_bot/commands/factoids.py @@ -35,6 +35,7 @@ from croniter import CroniterBadCronError from discord import app_commands from discord.ext import commands +from functions import logger as function_logger if TYPE_CHECKING: import bot @@ -851,7 +852,9 @@ async def response( try: # define the message and send it - await ctx.reply(content=content, embed=embed, mention_author=not mentions) + sent_message = await ctx.reply( + 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") @@ -876,12 +879,15 @@ async def response( exception=exception, ) # Sends the raw factoid instead of the embed as fallback - await ctx.reply( + sent_message = await ctx.reply( f"{mentions+' ' if mentions else ''}{factoid.message}", mention_author=not mentions, ) await self.send_to_irc(ctx.channel, ctx.message, factoid.message) + await self.send_to_logger( + sent_message, ctx.author, ctx.channel, factoid.message + ) async def send_to_irc( self: Self, @@ -907,6 +913,44 @@ async def send_to_irc( factoid_message=factoid_message, ) + async def send_to_logger( + self: Self, + factoid_message_object: discord.Message, + factoid_caller: discord.Member, + channel: discord.abc.GuildChannel | discord.Thread, + factoid_message: str, + ) -> None: + """Send a factoid call to the logger function + + Args: + factoid_message_object (discord.Message): The message that the factoid is sent in + factoid_caller (discord.Member): The person who called the factoid + channel (discord.abc.GuildChannel | discord.Thread): The channel the + 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: + return + + target_logging_channel = await function_logger.pre_log_checks( + self.bot, config, channel + ) + if not target_logging_channel: + return + + await function_logger.send_message( + self.bot, + factoid_message_object, + factoid_caller, + channel, + target_logging_channel, + content_override=factoid_message, + special_flags=["Factoid call"], + ) + @factoid_app_group.command( name="call", description="Calls a factoid from the database and sends it publicy in the channel.", @@ -1019,6 +1063,11 @@ async def factoid_call_command( interaction.channel, interaction.message, factoid.message ) + sent_message = await interaction.original_response() + await self.send_to_logger( + sent_message, interaction.user, interaction.channel, factoid.message + ) + # -- Factoid job related functions -- async def kickoff_jobs(self: Self) -> None: """Gets a list of cron jobs and starts them""" @@ -1182,6 +1231,7 @@ async def cronjob( message = await channel.send(content=factoid.message) await self.send_to_irc(channel, message, factoid.message) + await self.send_to_logger(message, ctx.author, ctx.channel, factoid.message) @commands.group( brief="Executes a factoid command", diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index 36f213392..6e3f059d5 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -11,6 +11,7 @@ from bidict import bidict from core import auxiliary, cogs from discord.ext import commands +from functions import logger as function_logger if TYPE_CHECKING: import bot @@ -415,7 +416,27 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No embed = self.generate_sent_message_embed(split_message=split_message) - await discord_channel.send(content=mentions_string, embed=embed) + 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: + return + target_logging_channel = await function_logger.pre_log_checks( + self.bot, config, discord_channel + ) + if not target_logging_channel: + return + + await function_logger.send_message( + self.bot, + sent_message, + discord_channel.guild.me, + discord_channel, + target_logging_channel, + content_override=split_message["content"], + special_flags=[f"IRC Message from: {split_message['hostmask']}"], + ) def get_mentions( self: Self, message: str, channel: discord.abc.Messageable diff --git a/techsupport_bot/functions/__init__.py b/techsupport_bot/functions/__init__.py index 26928a165..94b10657f 100644 --- a/techsupport_bot/functions/__init__.py +++ b/techsupport_bot/functions/__init__.py @@ -1,3 +1,4 @@ """Functions are commandless cogs""" +from .logger import * from .nickname import * diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index 999a2f928..84012f7e2 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -34,6 +34,91 @@ async def setup(bot: bot.TechSupportBot) -> None: bot.add_extension_config("logger", config) +def get_channel_id(channel: discord.abc.GuildChannel | discord.Thread) -> int: + """A function to get the ID of the channel that should be logged + Will pull the parent ID if a thread is used + + Args: + channel (discord.abc.GuildChannel | discord.Thread): The channel object + + Returns: + int: The ID of the channel that the message was sent in + """ + if isinstance(channel, discord.Thread): + return channel.parent_id + else: + return channel.id + + +def get_mapped_channel_object( + config: munch.Munch, 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 + 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( + str(get_channel_id(src_channel)) + ) + if not mapped_id: + return None + + # Get the channel object associated with the ID + target_logging_channel = src_channel.guild.get_channel(int(mapped_id)) + if not target_logging_channel: + return None + + return target_logging_channel + + +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. + It pulls the ID of the dest channel from the config, makes sure the guilds match + And finds the TextChannel for the dest channel + + 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: + discord.TextChannel: The dest channel object, where the log should be sent + """ + channel_id = get_channel_id(src_channel) + + if not str(channel_id) in config.extensions.logger.channel_map.value: + return None + + target_logging_channel = get_mapped_channel_object(config, 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") + await bot.logger.send_log( + message="Configured channel not in associated guild - aborting log", + level=LogLevel.WARNING, + context=LogContext(guild=src_channel.guild, channel=src_channel), + channel=log_channel, + ) + return None + + return target_logging_channel + + class Logger(cogs.MatchCog): """Class for the logger to make it to discord.""" @@ -49,15 +134,10 @@ async def match( Returns: bool: Whether the message should be logged or not """ - if isinstance(ctx.channel, discord.Thread): - if ( - not str(ctx.channel.parent_id) - in config.extensions.logger.channel_map.value - ): - return False - else: - if not str(ctx.channel.id) in config.extensions.logger.channel_map.value: - return False + channel_id = get_channel_id(ctx.channel) + if not str(channel_id) in config.extensions.logger.channel_map.value: + return False + return True async def response( @@ -69,150 +149,190 @@ async def response( config (munch.Munch): The guild config where the message was sent ctx (commands.Context): The context that was generated when the message was sent """ - # Get the ID of the channel, or parent channel in the case of threads - mapped_id = config.extensions.logger.channel_map.value.get( - str(getattr(ctx.channel, "parent_id", ctx.channel.id)) + target_logging_channel = await pre_log_checks(self.bot, config, ctx.channel) + + await send_message( + self.bot, + ctx.message, + ctx.author, + ctx.channel, + target_logging_channel, ) - if not mapped_id: - return - # Get the channel object associated with the ID - channel = ctx.guild.get_channel(int(mapped_id)) - if not channel: - return - # Don't log stuff cross-guild - if channel.guild.id != ctx.guild.id: - config = self.bot.guild_configs[str(ctx.guild.id)] - log_channel = config.get("logging_channel") - await self.bot.logger.send_log( - message="Configured channel not in associated guild - aborting log", - level=LogLevel.WARNING, - context=LogContext(guild=ctx.guild, channel=ctx.channel), - channel=log_channel, - ) - return +async def send_message( + bot: bot.TechSupportBot, + message: discord.Message, + author: discord.Member, + src_channel: discord.abc.GuildChannel | discord.Thread, + dest_channel: discord.TextChannel, + content_override: str = None, + special_flags: list[str] = [], +) -> None: + """Makes the embed, uploads the attachements, and send a message in the dest_channel + This will make zero checks - # Ensure we have attachments re-uploaded - attachments = await self.build_attachments(ctx, config) + Args: + bot (bot.TechSupportBot): The bot object + message (discord.Message): The message object to log + author (discord.Member): The author of the message + src_channel (discord.abc.GuildChannel | discord.Thread): The source channel where + the initial message was sent to + dest_channel (discord.TextChannel): The destination channel where the + log embed will be sent + content_override (str, optional): If supplied, the content of the message will be + replaced with this. Defaults to None. + 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)] - # Add avatar to attachments to all it to be added to the embed - attachments.insert( - 0, await ctx.author.display_avatar.to_file(filename="avatar.png") - ) + # Ensure we have attachments re-uploaded + attachments = await build_attachments(bot, config, message) - # Make and send the embed and files - embed = self.build_embed(ctx) - await channel.send(embed=embed, files=attachments[:11]) + # Add avatar to attachments to all it to be added to the embed + attachments.insert(0, await author.display_avatar.to_file(filename="avatar.png")) - def build_embed(self: Self, ctx: commands.Context) -> discord.Embed: - """Builds the logged messag embed + # Make and send the embed and files + embed = build_embed( + message, author, src_channel, content_override, special_flags=special_flags + ) + await dest_channel.send(embed=embed, files=attachments[:11]) - Args: - ctx (commands.Context): The context that the message to log was sent in - Returns: - discord.Embed: The prepared embed ready to send to the log channel - """ - embed = discord.Embed() +def build_embed( + message: discord.Message, + author: discord.Member, + src_channel: discord.abc.GuildChannel | discord.Thread, + content_override: str = None, + special_flags: list[str] = [], +) -> discord.Embed: + """Builds the logged messag embed - # Set basic items - embed.color = discord.Color.greyple() - embed.timestamp = datetime.datetime.utcnow() + Args: + message (discord.Message): The message object to log + author (discord.Member): The author of the message + src_channel (discord.abc.GuildChannel | discord.Thread): The source channel where + the initial message was sent to + dest_channel (discord.TextChannel): The destination channel where the + log embed will be sent + content_override (str, optional): If supplied, the content of the message will be + replaced with this. Defaults to None. + special_flags (list[str], optional): If supplied, a new field on the embed will be + added that shows this. Defaults to []. + + Returns: + discord.Embed: The prepared embed ready to send to the log channel + """ + embed = discord.Embed() - # Add the message content - embed.title = "Content" - embed.description = getattr(ctx.message, "clean_content", "No content") - if len(embed.description) == 0: - embed.description = "No content" + # Set basic items + embed.color = discord.Color.greyple() + embed.timestamp = datetime.datetime.utcnow() - # Add the channel/thread name - main_channel = getattr(ctx.channel, "parent", ctx.channel) - embed.add_field( - name="Channel", - value=f"{main_channel.name} ({main_channel.mention})", - ) - if isinstance(ctx.channel, discord.Thread): - embed.add_field( - name="Thread", - value=f"{ctx.channel.name} ({ctx.channel.mention})", - ) + # Add the message content + embed.title = "Content" + print_content = content_override + + if not content_override: + print_content = getattr(message, "clean_content", "No content") + + embed.description = print_content + if len(embed.description) == 0: + embed.description = "No content" - # Add username, display name, and nickname + # Add the channel/thread name + main_channel = getattr(src_channel, "parent", src_channel) + embed.add_field( + name="Channel", + value=f"{main_channel.name} ({main_channel.mention})", + ) + if isinstance(src_channel, discord.Thread): embed.add_field( - name="Display Name", value=getattr(ctx.author, "display_name", "Unknown") + name="Thread", + value=f"{src_channel.name} ({src_channel.mention})", ) - if getattr(ctx.author, "nick", False): - embed.add_field( - name="Global Name", value=getattr(ctx.author, "global_name", "Unknown") - ) - embed.add_field(name="Name", value=getattr(ctx.author, "name", "Unknown")) - # Add roles + # Add username, display name, and nickname + embed.add_field( + name="Display Name", value=getattr(author, "display_name", "Unknown") + ) + if getattr(author, "nick", False): embed.add_field( - name="Roles", - value=", ".join(self.generate_role_list(ctx.author)), + name="Global Name", value=getattr(author, "global_name", "Unknown") ) + embed.add_field(name="Name", value=getattr(author, "name", "Unknown")) - # Add avatar - embed.set_thumbnail(url="attachment://avatar.png") + # Add roles + embed.add_field( + name="Roles", + value=", ".join(generate_role_list(author)), + ) - # Add footer with IDs for better searchings - embed.set_footer( - text=f"Author ID: {ctx.author.id} • Message ID: {ctx.message.id}" - ) + # Flags + if special_flags: + embed.add_field(name="Flags", value=", ".join(special_flags)) - return embed + # Add avatar + embed.set_thumbnail(url="attachment://avatar.png") - def generate_role_list(self: Self, author: discord.Member) -> list[str]: - """Makes a list of role names from the passed member + # Add footer with IDs for better searchings + embed.set_footer(text=f"Author ID: {author.id} • Message ID: {message.id}") - Args: - author (discord.Member): The member to get roles from + return embed - Returns: - list[str]: The list of roles, highest role first - """ - if not hasattr(author, "roles"): - return ["None"] - roles = [role.name for role in author.roles[1:]] - roles.reverse() +def generate_role_list(author: discord.Member) -> list[str]: + """Makes a list of role names from the passed member - if len(roles) == 0: - roles = ["None"] + Args: + author (discord.Member): The member to get roles from - return roles + Returns: + list[str]: The list of roles, highest role first + """ + if not hasattr(author, "roles"): + return ["None"] - async def build_attachments( - self: Self, ctx: commands.Context, config: munch.Munch - ) -> list[discord.File]: - """Reuploads and builds a list of attachments to send along side the embed + roles = [role.name for role in author.roles[1:]] + roles.reverse() - Args: - ctx (commands.Context): The context the original message was sent in - config (munch.Munch): The config from the guild + if len(roles) == 0: + roles = ["None"] - Returns: - list[discord.File]: The list of file objects ready to be sent - """ - attachments: list[discord.File] = [] - if ctx.message.attachments: - total_attachment_size = 0 - for attch in ctx.message.attachments: - if ( - total_attachment_size := total_attachment_size + attch.size - ) <= ctx.filesize_limit: - attachments.append(await attch.to_file()) - if (lf := len(ctx.message.attachments) - len(attachments)) != 0: - log_channel = config.get("logging_channel") - await self.bot.logger.send_log( - message=( - f"Logger did not reupload {lf} file(s) due to file size limit" - f" on message {ctx.message.id} in channel {ctx.channel.name}." - ), - level=LogLevel.WARNING, - channel=log_channel, - context=LogContext(guild=ctx.guild, channel=ctx.channel), - ) - return attachments + return roles + + +async def build_attachments( + bot: bot.TechSupportBot, config: munch.Munch, 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: + list[discord.File]: The list of file objects ready to be sent + """ + attachments: list[discord.File] = [] + if message.attachments: + total_attachment_size = 0 + for attch in message.attachments: + if ( + total_attachment_size := total_attachment_size + attch.size + ) <= message.guild.filesize_limit: + attachments.append(await attch.to_file()) + if (lf := len(message.attachments) - len(attachments)) != 0: + log_channel = config.get("logging_channel") + await bot.logger.send_log( + message=( + f"Logger did not reupload {lf} file(s) due to file size limit" + f" on message {message.id} in channel {message.channel.name}." + ), + level=LogLevel.WARNING, + channel=log_channel, + context=LogContext(guild=message.guild, channel=message.channel), + ) + return attachments From dc358aea70fb3e9831da338c72d5e5173b80e3c4 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 3 May 2025 17:28:44 -0400 Subject: [PATCH 2/3] Formatting --- techsupport_bot/commands/relay.py | 7 +++++-- techsupport_bot/functions/logger.py | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index 6e3f059d5..ddb67acfc 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -428,14 +428,17 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No if not target_logging_channel: return + irc_message_content = split_message["content"] + irc_message_hostmask = split_message["hostmask"] + await function_logger.send_message( self.bot, sent_message, discord_channel.guild.me, discord_channel, target_logging_channel, - content_override=split_message["content"], - special_flags=[f"IRC Message from: {split_message['hostmask']}"], + content_override=irc_message_content, + special_flags=[f"IRC Message from: {irc_message_hostmask}"], ) def get_mentions( diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index 84012f7e2..37b3202a5 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -46,8 +46,7 @@ def get_channel_id(channel: discord.abc.GuildChannel | discord.Thread) -> int: """ if isinstance(channel, discord.Thread): return channel.parent_id - else: - return channel.id + return channel.id def get_mapped_channel_object( From edb6cc6ede57dc1a8d4de939f61a3ebf6129a9eb Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 3 May 2025 17:32:57 -0400 Subject: [PATCH 3/3] Formatting, security fixes --- techsupport_bot/functions/logger.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/functions/logger.py b/techsupport_bot/functions/logger.py index 37b3202a5..97779baa5 100644 --- a/techsupport_bot/functions/logger.py +++ b/techsupport_bot/functions/logger.py @@ -166,7 +166,7 @@ async def send_message( src_channel: discord.abc.GuildChannel | discord.Thread, dest_channel: discord.TextChannel, content_override: str = None, - special_flags: list[str] = [], + special_flags: list[str] = None, ) -> None: """Makes the embed, uploads the attachements, and send a message in the dest_channel This will make zero checks @@ -204,7 +204,7 @@ def build_embed( author: discord.Member, src_channel: discord.abc.GuildChannel | discord.Thread, content_override: str = None, - special_flags: list[str] = [], + special_flags: list[str] = None, ) -> discord.Embed: """Builds the logged messag embed @@ -213,8 +213,6 @@ def build_embed( author (discord.Member): The author of the message src_channel (discord.abc.GuildChannel | discord.Thread): The source channel where the initial message was sent to - dest_channel (discord.TextChannel): The destination channel where the - log embed will be sent content_override (str, optional): If supplied, the content of the message will be replaced with this. Defaults to None. special_flags (list[str], optional): If supplied, a new field on the embed will be