diff --git a/poetry.lock b/poetry.lock index e570c183..df10d2bd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -232,17 +232,17 @@ test = ["asgi-lifespan (>=1.0.1)", "dnspython (>=2.1.0)", "fastapi (>=0.100)", " [[package]] name = "boto3" -version = "1.34.27" +version = "1.34.28" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.8" files = [ - {file = "boto3-1.34.27-py3-none-any.whl", hash = "sha256:3626db4ba9fbb1b58c8fe923da5ed670873b3d881a102956ea19d3b69cd097cc"}, - {file = "boto3-1.34.27.tar.gz", hash = "sha256:ebdd938019f3df2e7b50585353963d4553faf3fbb7b2085c440107fa6caa233b"}, + {file = "boto3-1.34.28-py3-none-any.whl", hash = "sha256:fb56622ce195c06ae0d15ae9472d44529362a869ad52862a5a28b891530969f9"}, + {file = "boto3-1.34.28.tar.gz", hash = "sha256:9e0dcca7bb0567f7b4b84d1d26c19b217abfe149d19106af7f120f09142688cf"}, ] [package.dependencies] -botocore = ">=1.34.27,<1.35.0" +botocore = ">=1.34.28,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -251,13 +251,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.27" +version = "1.34.28" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.8" files = [ - {file = "botocore-1.34.27-py3-none-any.whl", hash = "sha256:1c10f247136ad17b6ef1588c1e043e294dbaebdebe9ce84dc56713029f515c53"}, - {file = "botocore-1.34.27.tar.gz", hash = "sha256:a0e68ba264275b358b8c1cca604161f4d9465cf7847d73e929543a9f30ff22d1"}, + {file = "botocore-1.34.28-py3-none-any.whl", hash = "sha256:03be8209257ab65f3c8be7377cf8d38bff6a6afbe3d36c72924e48959bb694dc"}, + {file = "botocore-1.34.28.tar.gz", hash = "sha256:45c99ccc6389ab1a87e996a7cc8797c7e41d5ecd9a5757d567ba3a57cb7655e7"}, ] [package.dependencies] diff --git a/src/valentina/characters/chargen.py b/src/valentina/characters/chargen.py index 0aa47ee0..d4dfc573 100644 --- a/src/valentina/characters/chargen.py +++ b/src/valentina/characters/chargen.py @@ -527,7 +527,7 @@ async def random_attributes(self, character: Character) -> Character: Args: character (Character): The character for which to generate attributes. """ - logger.debug(f"CHARGEN: Generate attribute values for {character.name}") + logger.debug(f"Generate attribute values for {character.name}") concept = CharacterConcept[character.concept_name] if character.concept_name else None char_class = CharClass[character.char_class_name] @@ -591,7 +591,7 @@ async def random_abilities(self, character: Character) -> Character: Args: character (Character): The character for which to generate abilities. """ - logger.debug(f"CHARGEN: Generate ability values for {character.name}") + logger.debug(f"Generate ability values for {character.name}") concept = CharacterConcept[character.concept_name] if character.concept_name else None @@ -656,7 +656,7 @@ async def random_disciplines(self, character: Character) -> Character: Returns: Character: The updated character. """ - logger.debug(f"CHARGEN: Generate discipline values for {character.name}") + logger.debug(f"Generate discipline values for {character.name}") # TODO: Work with Ghouls which have no clan try: @@ -713,7 +713,7 @@ async def random_virtues(self, character: Character) -> Character: Returns: Character: The updated character. """ - logger.debug(f"CHARGEN: Generate virtue values for {character.name}") + logger.debug(f"Generate virtue values for {character.name}") if not ( virtues := TraitCategory.VIRTUES.get_trait_list(CharClass[character.char_class_name]) @@ -756,7 +756,7 @@ async def random_backgrounds(self, character: Character) -> Character: Returns: Character: The updated character. """ - logger.debug(f"CHARGEN: Generate background values for {character.name}") + logger.debug(f"Generate background values for {character.name}") char_class = CharClass[character.char_class_name] @@ -801,7 +801,7 @@ async def random_willpower(self, character: Character) -> Character: # noqa: PL Returns: Character: The updated character. """ - logger.debug(f"CHARGEN: Generate willpower values for {character.name}") + logger.debug(f"Generate willpower values for {character.name}") if not any(x.name for x in character.traits if x.name == "Self-Control"): # type: ignore [attr-defined] return character @@ -852,7 +852,7 @@ async def random_hunter_traits(self, character: Character) -> Character: if character.char_class != CharClass.HUNTER: return character - logger.debug(f"CHARGEN: Generate hunter trait values for {character.name}") + logger.debug(f"Generate hunter trait values for {character.name}") try: creed = HunterCreed[character.creed_name] @@ -919,7 +919,7 @@ async def concept_special_abilities(self, character: Character) -> Character: # if character.char_class != CharClass.MORTAL: return character - logger.debug(f"CHARGEN: Assign special abilities for {character.name}") + logger.debug(f"Assign special abilities for {character.name}") # Assign Traits for ability in character.concept.value.abilities: @@ -1055,7 +1055,7 @@ async def start(self, restart: bool = False) -> None: Args: restart (bool, optional): Whether to restart the wizard. Defaults to False. """ - logger.debug("CHARGEN: Starting the character generation wizard.") + logger.debug("Starting the character generation wizard.") # Build the instructional embeds embed1 = discord.Embed( @@ -1126,7 +1126,7 @@ async def present_character_choices(self) -> None: Returns: None: This method returns nothing. """ - logger.debug("CHARGEN: Starting the character selection process") + logger.debug("Starting the character selection process") # Generate 3 characters characters = [ @@ -1192,7 +1192,7 @@ async def present_character_choices(self) -> None: campaign_xp, _, _ = self.user.fetch_campaign_xp(self.campaign) # Delete the previously created characters - logger.debug("CHARGEN: Rerolling characters and deleting old ones.") + logger.debug("Rerolling characters and deleting old ones.") for character in characters: await character.delete(link_rule=DeleteRules.DELETE_LINKS) @@ -1271,7 +1271,7 @@ async def spend_freebie_points(self, character: Character) -> Character: Returns: Character: The created character. """ - logger.debug(f"CHARGEN: Spending freebie points for {character.name}") + logger.debug(f"Spending freebie points for {character.name}") # Create the character sheet embed title = f"Spend freebie points on {character.name}\n" diff --git a/src/valentina/cogs/admin.py b/src/valentina/cogs/admin.py index 05ca9052..74c7403d 100644 --- a/src/valentina/cogs/admin.py +++ b/src/valentina/cogs/admin.py @@ -324,7 +324,9 @@ async def emoji_add( # Confirm the action title = f"Add custom emoji :{name}:" - is_confirmed, msg, confirmation_embed = await confirm_action(ctx, title, hidden=hidden) + is_confirmed, msg, confirmation_embed = await confirm_action( + ctx, title, hidden=hidden, audit=True + ) if not is_confirmed: return @@ -332,7 +334,6 @@ async def emoji_add( await ctx.guild.create_custom_emoji(name=name, image=data) # Send confirmation - await ctx.post_to_audit_log(title) await msg.edit_original_response(embed=confirmation_embed, view=None) @guild.command(name="emoji_delete") diff --git a/src/valentina/cogs/campaign.py b/src/valentina/cogs/campaign.py index 89d03a2f..2f321d09 100644 --- a/src/valentina/cogs/campaign.py +++ b/src/valentina/cogs/campaign.py @@ -69,7 +69,7 @@ async def create_campaign( title = f"Create new campaign: `{name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -83,7 +83,6 @@ async def create_campaign( guild.campaigns.append(campaign) await guild.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @campaign.command(name="current_date", description="Set the current date of a campaign") @@ -103,7 +102,6 @@ async def current_date( campaign.date_in_game = date await campaign.save() - await ctx.post_to_audit_log(f"Set date of campaign `{campaign.name}` to `{date:%Y-%m-%d}`") await present_embed( ctx, title=f"Set date of campaign `{campaign.name}` to `{date:%Y-%m-%d}`", @@ -133,7 +131,7 @@ async def delete_campaign( title = f"Delete campaign: {campaign.name}" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -142,7 +140,6 @@ async def delete_campaign( guild = await Guild.get(ctx.guild.id, fetch_links=True) await guild.delete_campaign(campaign) - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @campaign.command(name="view", description="View a campaign") @@ -207,7 +204,7 @@ async def campaign_set_active( title = f"Set campaign `{campaign.name}` as active" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -217,7 +214,6 @@ async def campaign_set_active( guild.active_campaign = campaign await guild.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @campaign.command(name="set_inactive", description="Set a campaign as inactive") @@ -248,7 +244,7 @@ async def campaign_set_inactive( title = f"Set campaign `{active_campaign.name}` as inactive" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -257,7 +253,6 @@ async def campaign_set_inactive( guild.active_campaign = None await guild.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @campaign.command(name="list", description="List all campaigns") @@ -294,7 +289,6 @@ async def campaign_list( ]) await present_embed(ctx, title="Campaigns", fields=fields, level="info") - logger.debug("CAMPAIGN: List all campaigns") ### NPC COMMANDS #################################################################### @@ -476,7 +470,7 @@ async def delete_npc( title = f"Delete NPC: `{npc.name}` in `{active_campaign.name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -485,7 +479,6 @@ async def delete_npc( del active_campaign.npcs[index] await active_campaign.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) ### CHAPTER COMMANDS #################################################################### @@ -641,7 +634,7 @@ async def delete_chapter( title = f"Delete Chapter `{chapter.number}. {chapter.name}` from `{active_campaign.name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -650,7 +643,6 @@ async def delete_chapter( del active_campaign.chapters[index] await active_campaign.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) ### NOTE COMMANDS #################################################################### @@ -802,7 +794,7 @@ async def delete_note( title = f"Delete note: `{note.name}` from `{active_campaign.name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -811,7 +803,6 @@ async def delete_note( del active_campaign.notes[index] await active_campaign.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) diff --git a/src/valentina/cogs/characters.py b/src/valentina/cogs/characters.py index dfd73d07..e52774c8 100644 --- a/src/valentina/cogs/characters.py +++ b/src/valentina/cogs/characters.py @@ -7,7 +7,6 @@ import inflect from discord.commands import Option from discord.ext import commands -from loguru import logger from valentina.characters import AddFromSheetWizard, CharGenWizard from valentina.constants import ( @@ -130,8 +129,7 @@ async def add_character( wizard = AddFromSheetWizard(ctx, character=character, user=user) await wizard.begin_chargen() - await ctx.post_to_audit_log(f"Created player character: `{character.name}`") - logger.info(f"CHARACTER: Create player character {character.name}") + await ctx.post_to_audit_log(f"Create player character: `{character.name}`") @chars.command(name="create", description="Create a new randomized character") async def create_character( @@ -309,7 +307,7 @@ async def transfer_character( title = f"Transfer `{character.name}` from `{ctx.author.display_name}` to `{new_owner.display_name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: return @@ -324,7 +322,6 @@ async def transfer_character( character.user_owner = new_user.id await character.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @chars.command(name="kill", description="Kill a character") @@ -358,7 +355,7 @@ async def kill_character( title = f"Kill `{character.name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: return @@ -366,7 +363,6 @@ async def kill_character( character.is_alive = False await character.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) ### IMAGE COMMANDS #################################################################### @@ -431,14 +427,13 @@ async def add_image( title = f"Add image to `{character.name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden, image=image_url + ctx, title, hidden=hidden, image=image_url, audit=True ) if not is_confirmed: await character.delete_image(image_key) return # Update audit log and original response - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @image.command(name="delete", description="Delete an image from a character") @@ -516,14 +511,13 @@ async def add_trait( title = f"Add trait: `{name.title()}` at `{value}` dots for {character.name}" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: return await character.add_trait(category, name.title(), value, max_value=max_value) - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @trait.command(name="update", description="Update the value of a trait for a character") @@ -576,7 +570,7 @@ async def update_trait( f"Update `{trait.name}` from `{trait.value}` to `{new_value}` for `{character.name}`" ) is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -585,7 +579,6 @@ async def update_trait( trait.value = new_value await trait.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @trait.command(name="delete", description="Delete a trait from a character") @@ -627,6 +620,7 @@ async def delete_trait( title, description="This is a destructive action that can not be undone.", hidden=hidden, + audit=True, ) if not is_confirmed: @@ -637,7 +631,6 @@ async def delete_trait( await trait.delete() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) ### SECTION COMMANDS #################################################################### @@ -752,7 +745,7 @@ async def delete_custom_section( title = f"Delete section `{section.title}` from `{character.name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: return @@ -760,7 +753,6 @@ async def delete_custom_section( character.sheet_sections.pop(section_index) await character.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) ### BIO COMMANDS #################################################################### diff --git a/src/valentina/cogs/developer.py b/src/valentina/cogs/developer.py index 20d66562..f111e54b 100644 --- a/src/valentina/cogs/developer.py +++ b/src/valentina/cogs/developer.py @@ -11,12 +11,12 @@ from beanie import DeleteRules from discord.commands import Option from discord.ext import commands -from loguru import logger from valentina.characters import RNGCharGen -from valentina.constants import PREF_MAX_EMBED_CHARACTERS, EmbedColor +from valentina.constants import PREF_MAX_EMBED_CHARACTERS, EmbedColor, LogLevel from valentina.models import AWSService, Character, GlobalProperty, Guild, RollProbability, User from valentina.models.bot import Valentina, ValentinaContext +from valentina.utils import instantiate_logger from valentina.utils.autocomplete import ( select_aws_object_from_guild, select_changelog_version_1, @@ -110,7 +110,6 @@ async def delete_from_s3_guild( # Delete the object from S3 # TODO: Search for the url in character data and delete it there too so we don't have dead links self.aws_svc.delete_object(key) - logger.info(f"Deleted object with key: {key} from S3") await interaction.edit_original_response(embed=confirmation_embed, view=None) @@ -199,7 +198,7 @@ async def delete_developer_characters( return for c in dev_characters: - logger.debug(f"DEVELOPER: Deleting {c.name}") + ctx.log_command(f"Delete dev character {c.name}") await c.delete(link_rule=DeleteRules.DELETE_LINKS) await interaction.edit_original_response(embed=confirmation_embed, view=None) @@ -302,7 +301,6 @@ async def clear_probability_cache( for result in results: await result.delete() - logger.info(f"DEVELOPER: {ctx.author.display_name} cleared probability data from the db") await interaction.edit_original_response(embed=confirmation_embed, view=None) @server.command(name="reload", description="Reload all cogs") @@ -328,10 +326,8 @@ async def reload( for cog in Path(self.bot.parent_dir / "src" / "valentina" / "cogs").glob("*.py"): if cog.stem[0] != "_": count += 1 - logger.info(f"COGS: Reloading - {cog.stem}") self.bot.reload_extension(f"valentina.cogs.{cog.stem}") - logger.debug("Admin: Reload the bot's cogs") await interaction.edit_original_response(embed=confirmation_embed, view=None) @server.command(name="shutdown", description="Shutdown the bot") @@ -354,7 +350,7 @@ async def shutdown( return await interaction.edit_original_response(embed=confirmation_embed, view=None) - logger.warning(f"DEVELOPER: {ctx.author.display_name} has shut down the bot") + ctx.log_command("Shutdown the bot", LogLevel.WARNING) await self.bot.close() @@ -370,6 +366,7 @@ async def debug_send_log( ), ) -> None: """Send the bot's logs to the user.""" + ctx.log_command("Send the bot's logs", LogLevel.DEBUG) log_file = get_config_value("VALENTINA_LOG_FILE") file = discord.File(log_file) await ctx.respond(file=file, ephemeral=hidden) @@ -386,7 +383,7 @@ async def debug_tail_logs( ), ) -> None: """Tail the bot's logs.""" - logger.debug("ADMIN: Tail bot logs") + ctx.log_command("Tail the bot's logs", LogLevel.DEBUG) max_lines_from_bottom = 20 log_lines = [] @@ -465,6 +462,31 @@ async def status( await ctx.respond(embed=embed, ephemeral=hidden) + ### LOGGING COMMANDS ################################################################ + + @developer.command(name="logging", description="Change log level") + @commands.is_owner() + async def logging( + self, + ctx: ValentinaContext, + log_level: Option(LogLevel), + hidden: Option( + bool, + description="Make the confirmation only visible to you (default True)", + default=True, + ), + ) -> None: + """Change log level.""" + title = f"Set log level to: `{log_level.value}`" + is_confirmed, interaction, confirmation_embed = await confirm_action( + ctx, title, hidden=hidden + ) + if not is_confirmed: + return + + instantiate_logger(log_level) + await interaction.edit_original_response(embed=confirmation_embed, view=None) + def setup(bot: Valentina) -> None: """Add the cog to the bot.""" diff --git a/src/valentina/cogs/experience.py b/src/valentina/cogs/experience.py index d00bdb34..ac18df19 100644 --- a/src/valentina/cogs/experience.py +++ b/src/valentina/cogs/experience.py @@ -66,7 +66,7 @@ async def xp_add( title = f"Add `{amount}` xp to `{user.name}`" description = "View experience with `/user_info`" is_confirmed, msg, confirmation_embed = await confirm_action( - ctx, title, description=description, hidden=hidden + ctx, title, description=description, hidden=hidden, audit=True ) if not is_confirmed: return @@ -75,7 +75,6 @@ async def xp_add( await user.add_campaign_xp(active_campaign, amount) # Send the confirmation message - await ctx.post_to_audit_log(title) await msg.edit_original_response(embed=confirmation_embed, view=None) @xp.command(name="add_cool_point", description="Add a cool point to a user") @@ -116,7 +115,7 @@ async def cp_add( title = f"Add `{amount}` cool {p.plural_noun('point', amount)} to `{user.name}`" description = "View cool points with `/user_info`" is_confirmed, msg, confirmation_embed = await confirm_action( - ctx, title, description=description, hidden=hidden + ctx, title, description=description, hidden=hidden, audit=True ) if not is_confirmed: return @@ -125,7 +124,6 @@ async def cp_add( await user.add_campaign_cool_points(active_campaign, amount) # Send the confirmation message - await ctx.post_to_audit_log(title) await msg.edit_original_response(embed=confirmation_embed, view=None) @xp.command(name="spend", description="Spend experience points") @@ -183,7 +181,9 @@ async def xp_spend( new_trait_value = trait.value + 1 title = f"Upgrade `{trait.name}` from `{trait.value}` {p.plural_noun('dot', trait.value)} to `{trait.value + 1}` {p.plural_noun('dot', trait.value + 1)} for `{upgrade_cost}` xp" - is_confirmed, msg, confirmation_embed = await confirm_action(ctx, title, hidden=hidden) + is_confirmed, msg, confirmation_embed = await confirm_action( + ctx, title, hidden=hidden, audit=True + ) if not is_confirmed: return @@ -196,7 +196,6 @@ async def xp_spend( await trait.save() # Send the confirmation message - await ctx.post_to_audit_log(title) await msg.edit_original_response(embed=confirmation_embed, view=None) diff --git a/src/valentina/cogs/gameplay.py b/src/valentina/cogs/gameplay.py index 5fafc5ca..b79c881f 100644 --- a/src/valentina/cogs/gameplay.py +++ b/src/valentina/cogs/gameplay.py @@ -6,9 +6,8 @@ import discord from discord.commands import Option from discord.ext import commands -from loguru import logger -from valentina.constants import DEFAULT_DIFFICULTY, DiceType +from valentina.constants import DEFAULT_DIFFICULTY, DiceType, LogLevel from valentina.models import User from valentina.models.bot import Valentina, ValentinaContext from valentina.utils import random_num @@ -84,8 +83,9 @@ async def traits( pool = trait_one.value + trait_two.value - logger.debug( - f"ROLL TRAITS: {trait_one.name} ({trait_one.id}) + {trait_two.name} ({trait_two.id}) = {pool}" + ctx.log_command( + f"{trait_one.name} ({trait_one.id}) + {trait_two.name} ({trait_two.id})", + LogLevel.DEBUG, ) await perform_roll( @@ -148,6 +148,11 @@ async def roll_macro( msg = "Macro traits not found on character" raise commands.BadArgument(msg) + ctx.log_command( + f"Macro: {macro.name}: {trait_one.name} ({trait_one.id}) + {trait_two.name} ({trait_two.id})", + LogLevel.DEBUG, + ) + pool = trait_one.value + trait_two.value await perform_roll( diff --git a/src/valentina/cogs/github.py b/src/valentina/cogs/github.py index 8ed5ea2b..3dfa254f 100644 --- a/src/valentina/cogs/github.py +++ b/src/valentina/cogs/github.py @@ -8,7 +8,7 @@ from github.Repository import Repository from loguru import logger -from valentina.constants import GithubIssueLabels +from valentina.constants import GithubIssueLabels, LogLevel from valentina.models.bot import Valentina, ValentinaContext from valentina.utils import errors from valentina.utils.helpers import get_config_value @@ -51,6 +51,7 @@ async def fetch_github_repo(self) -> Repository: @issues.command(name="list", description="List open issues") async def issue_list(self, ctx: ValentinaContext) -> None: """List open Github issues.""" + ctx.log_command("github issues list", LogLevel.DEBUG) repo = await self.fetch_github_repo() open_issues = repo.get_issues(state="open") for issue in open_issues: @@ -77,6 +78,7 @@ async def issue_list(self, ctx: ValentinaContext) -> None: @issues.command(name="get", description="Get details for a specific issue") async def issue_get(self, ctx: ValentinaContext, issue_number: int) -> None: """Get details for a specific Github issue.""" + ctx.log_command(f"github issues get {issue_number}", LogLevel.DEBUG) repo = await self.fetch_github_repo() issue = repo.get_issue(number=issue_number) await present_embed( @@ -103,6 +105,7 @@ async def issue_add( ), ) -> None: """Add a new Github issue.""" + ctx.log_command(f"github issues add {title} {description} {type_of_issue}", LogLevel.DEBUG) repo = await self.fetch_github_repo() issue = repo.create_issue(title=title, body=description, labels=[type_of_issue]) await present_embed( diff --git a/src/valentina/cogs/macros.py b/src/valentina/cogs/macros.py index d185eb84..ac902019 100644 --- a/src/valentina/cogs/macros.py +++ b/src/valentina/cogs/macros.py @@ -161,7 +161,7 @@ async def delete_macro( title = f"Delete macro `{macro.name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden, footer="This action is irreversible." + ctx, title, hidden=hidden, footer="This action is irreversible.", audit=True ) if not is_confirmed: @@ -170,7 +170,6 @@ async def delete_macro( del user.macros[index] await user.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) diff --git a/src/valentina/cogs/misc.py b/src/valentina/cogs/misc.py index 59733e32..34b020cb 100644 --- a/src/valentina/cogs/misc.py +++ b/src/valentina/cogs/misc.py @@ -355,7 +355,7 @@ async def upload_thumbnail( """Add a roll result thumbnail to the bot.""" title = f"Add roll result image for {roll_type.title()}\n{url}" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden, image=url + ctx, title, hidden=hidden, image=url, audit=True ) if not is_confirmed: @@ -364,7 +364,6 @@ async def upload_thumbnail( guild = await Guild.get(ctx.guild.id, fetch_links=True) await guild.add_roll_result_thumbnail(ctx, RollResultType[roll_type.upper()], url) - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) diff --git a/src/valentina/cogs/storyteller.py b/src/valentina/cogs/storyteller.py index e38e3cfa..9e676759 100644 --- a/src/valentina/cogs/storyteller.py +++ b/src/valentina/cogs/storyteller.py @@ -139,7 +139,7 @@ async def create_story_char( wizard = AddFromSheetWizard(ctx, character=character, user=user) await wizard.begin_chargen() - await ctx.post_to_audit_log(f"Created storyteller character: `{character.name}`") + await ctx.post_to_audit_log(f"Create storyteller character: `{character.name}`") logger.info(f"CHARACTER: Create storyteller character {character.name}") @character.command(name="create_rng", description="Create a random new npc character") @@ -238,7 +238,7 @@ async def create_rng_char( color=EmbedColor.SUCCESS.value, ), ) - await ctx.post_to_audit_log(f"Storyteller character created: `{character.full_name}`") + await ctx.post_to_audit_log(f"Create storyteller character: `{character.full_name}`") @character.command(name="list", description="List all storyteller characters") async def list_characters( @@ -310,7 +310,7 @@ async def update_storyteller_character( f"Update `{trait.name}` for `{character.name}` from `{trait.value}` to `{new_value}`" ) is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -320,7 +320,6 @@ async def update_storyteller_character( trait.value = new_value await trait.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @character.command(name="sheet", description="View a character sheet") @@ -361,7 +360,7 @@ async def delete_storyteller_character( """Delete a storyteller character.""" title = f"Delete storyteller character `{character.full_name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -369,7 +368,6 @@ async def delete_storyteller_character( await character.delete(link_rule=DeleteRules.DELETE_LINKS) - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @character.command(name="add_trait", description="Add a trait to a storyteller character") @@ -408,7 +406,7 @@ async def add_trait( """Add a custom trait to a character.""" title = f"Create custom trait: `{name.title()}` at `{value}` dots for {character.full_name}" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -416,7 +414,6 @@ async def add_trait( await character.add_trait(category, name.title(), value, max_value=max_value) - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @character.command(name="image_add", description="Add an image to a storyteller character") @@ -484,7 +481,7 @@ async def add_image( title = f"Add image to `{character.name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden, image=image_url + ctx, title, hidden=hidden, image=image_url, audit=True ) if not is_confirmed: @@ -492,7 +489,6 @@ async def add_image( return # Update audit log and original response - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @character.command( @@ -570,7 +566,7 @@ async def transfer_character( title = f"Transfer `{character.name}` from `{old_owner.name}` to `{new_owner.name}`" is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: return @@ -582,7 +578,6 @@ async def transfer_character( character.user_owner = new_owner.id await character.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) @player.command(name="update", description="Update a player character") @@ -625,7 +620,7 @@ async def update_player_character( f"Update `{trait.name}` from `{trait.value}` to `{new_value}` for `{character.name}`" ) is_confirmed, interaction, confirmation_embed = await confirm_action( - ctx, title, hidden=hidden + ctx, title, hidden=hidden, audit=True ) if not is_confirmed: @@ -634,7 +629,6 @@ async def update_player_character( trait.value = new_value await trait.save() - await ctx.post_to_audit_log(title) await interaction.edit_original_response(embed=confirmation_embed, view=None) ### ROLL COMMANDS #################################################################### diff --git a/src/valentina/constants.py b/src/valentina/constants.py index 3ed4c458..b05b6805 100644 --- a/src/valentina/constants.py +++ b/src/valentina/constants.py @@ -29,6 +29,16 @@ ### ENUMS ### +class LogLevel(str, Enum): + """Enum for logging levels.""" + + TRACE = "TRACE" + DEBUG = "DEBUG" + INFO = "INFO" + WARNING = "WARNING" + ERROR = "ERROR" + + class ChannelPermission(Enum): """Enum for permissions when creating a character. Default is UNRESTRICTED.""" diff --git a/src/valentina/main.py b/src/valentina/main.py index 01bb73fb..0a4394fa 100644 --- a/src/valentina/main.py +++ b/src/valentina/main.py @@ -1,7 +1,5 @@ """Main file which instantiates the bot and runs it.""" -import logging -import sys from pathlib import Path from time import sleep from typing import Optional @@ -11,7 +9,7 @@ from loguru import logger from valentina.models.bot import Valentina -from valentina.utils import InterceptHandler +from valentina.utils import instantiate_logger from valentina.utils.database import test_db_connection from valentina.utils.helpers import get_config_value @@ -29,39 +27,6 @@ def version_callback(value: bool) -> None: raise typer.Exit() -http_log_level = get_config_value("VALENTINA_LOG_LEVEL_HTTP", "INFO") -aws_log_level = get_config_value("VALENTINA_LOG_LEVEL_AWS", "INFO") - -# Instantiate Logging -logging.getLogger("discord.http").setLevel(level=http_log_level.upper()) -logging.getLogger("discord.gateway").setLevel(level=http_log_level.upper()) -logging.getLogger("discord.webhook").setLevel(level=http_log_level.upper()) -logging.getLogger("discord.client").setLevel(level=http_log_level.upper()) -logging.getLogger("faker").setLevel(level="INFO") -for service in ["urllib3", "boto3", "botocore", "s3transfer"]: - logging.getLogger(service).setLevel(level=aws_log_level.upper()) - -logger.remove() -logger.add( - sys.stderr, - level=get_config_value("VALENTINA_LOG_LEVEL", "INFO").upper(), - colorize=True, - format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}: {message}", - enqueue=True, -) -logger.add( - get_config_value("VALENTINA_LOG_FILE", "valentina.log"), - level=get_config_value("VALENTINA_LOG_LEVEL", "INFO").upper(), - rotation="1 week", - retention="2 weeks", - compression="zip", - enqueue=True, -) - -# Intercept standard discord.py logs and redirect to Loguru -logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True) - - @app.command() def main( version: Optional[bool] = typer.Option( # noqa: ARG001 @@ -69,6 +34,9 @@ def main( ), ) -> None: """Run Valentina.""" + # Instantiate the logger + instantiate_logger() + # Ensure the database is available before starting the bot while not test_db_connection(): logger.error("DB: Connection failed. Retrying in 30 seconds...") diff --git a/src/valentina/models/bot.py b/src/valentina/models/bot.py index e3933e76..9a88d691 100644 --- a/src/valentina/models/bot.py +++ b/src/valentina/models/bot.py @@ -17,6 +17,7 @@ from valentina.constants import ( ChannelPermission, EmbedColor, + LogLevel, PermissionManageCampaign, PermissionsGrantXP, PermissionsKillCharacter, @@ -40,6 +41,18 @@ class ValentinaContext(discord.ApplicationContext): """A custom application context for Valentina.""" + # valentina.models.bot: Reload all cogs (@natenate in #general) + + def log_command(self, msg: str, level: LogLevel = LogLevel.INFO) -> None: # pragma: no cover + """Log the command to the console and log file.""" + author = f"@{self.author.display_name}" if hasattr(self, "author") else None + command = f"'/{self.command.qualified_name}'" if hasattr(self, "command") else None + channel = f"#{self.channel.name}" if hasattr(self, "channel") else None + + command_info = [author, command, channel] + + logger.log(level.value, f"{msg} [{', '.join([x for x in command_info if x])}]") + def _message_to_embed(self, message: str) -> discord.Embed: # pragma: no cover """Convert a string message to a discord embed. @@ -132,6 +145,12 @@ async def post_to_audit_log(self, message: str | discord.Embed) -> None: # prag guild = await Guild.get(self.guild.id) audit_log_channel = guild.fetch_audit_log_channel(self.guild) + if isinstance(message, str): + self.log_command(message, LogLevel.INFO) + + if isinstance(message, discord.Embed): + self.log_command(f"{message.title} {message.description}", LogLevel.INFO) + if audit_log_channel: embed = self._message_to_embed(message) if isinstance(message, str) else message diff --git a/src/valentina/models/character.py b/src/valentina/models/character.py index 8379993c..ba45a3c9 100644 --- a/src/valentina/models/character.py +++ b/src/valentina/models/character.py @@ -175,6 +175,7 @@ async def add_image(self, extension: str, data: bytes) -> str: key = f"{key_prefix}/{image_name}" # Upload the image to S3 + logger.debug(f"S3: Uploading {key} to {self.name}") aws_svc.upload_image(data=data, key=key) # Add the image to the character's data @@ -209,7 +210,7 @@ async def delete_image(self, key: str) -> None: # Delete the image from Amazon S3 aws_svc.delete_object(key) - logger.info(f"S3: Deleted {key} from {self.name}") + logger.info(f"S3: Delete {key} from {self.name}") async def add_trait( self, diff --git a/src/valentina/utils/__init__.py b/src/valentina/utils/__init__.py index 9754004b..ebd1e412 100644 --- a/src/valentina/utils/__init__.py +++ b/src/valentina/utils/__init__.py @@ -2,6 +2,6 @@ from .console import console from .helpers import random_num -from .logging import InterceptHandler +from .logging import InterceptHandler, instantiate_logger -__all__ = ["Context", "InterceptHandler", "console", "random_num"] +__all__ = ["Context", "InterceptHandler", "console", "instantiate_logger", "random_num"] diff --git a/src/valentina/utils/logging.py b/src/valentina/utils/logging.py index f1eee950..c5082c45 100644 --- a/src/valentina/utils/logging.py +++ b/src/valentina/utils/logging.py @@ -5,6 +5,58 @@ from loguru import logger +from valentina.constants import LogLevel +from valentina.utils.helpers import get_config_value + + +def instantiate_logger(log_level: LogLevel | None = None) -> None: # pragma: no cover + """Instantiate the Loguru logger for Valentina. + + Configure the logger with the specified verbosity level, log file path, + and whether to log to a file. + + Args: + log_level (LogLevel): The verbosity level for the logger. + + Returns: + None + """ + if log_level: + log_level_name = log_level.value + else: + log_level_name = LogLevel(get_config_value("VALENTINA_LOG_LEVEL", "INFO").upper()) + + http_log_level = get_config_value("VALENTINA_LOG_LEVEL_HTTP", "INFO") + aws_log_level = get_config_value("VALENTINA_LOG_LEVEL_AWS", "INFO") + + # Configure Loguru + logger.remove() + logger.add( + sys.stderr, + level=log_level_name, + colorize=True, + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}: {message}", + enqueue=True, + ) + logger.add( + get_config_value("VALENTINA_LOG_FILE", "valentina.log"), + level=log_level_name, + rotation="10 MB", + retention=3, + compression="zip", + enqueue=True, + ) + + # Intercept standard discord.py logs and redirect to Loguru + logging.getLogger("discord.http").setLevel(level=http_log_level.upper()) + logging.getLogger("discord.gateway").setLevel(level=http_log_level.upper()) + logging.getLogger("discord.webhook").setLevel(level=http_log_level.upper()) + logging.getLogger("discord.client").setLevel(level=http_log_level.upper()) + logging.getLogger("faker").setLevel(level="INFO") + for service in ["urllib3", "boto3", "botocore", "s3transfer"]: + logging.getLogger(service).setLevel(level=aws_log_level.upper()) + logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True) + class InterceptHandler(logging.Handler): """Intercepts standard logging and redirects to Loguru. diff --git a/src/valentina/views/actions.py b/src/valentina/views/actions.py index 4b148b79..6c796bc9 100644 --- a/src/valentina/views/actions.py +++ b/src/valentina/views/actions.py @@ -2,7 +2,7 @@ import discord -from valentina.constants import EmbedColor, Emoji +from valentina.constants import EmbedColor, Emoji, LogLevel from valentina.models.bot import ValentinaContext from valentina.views import ConfirmCancelButtons, present_embed @@ -15,6 +15,7 @@ async def confirm_action( image: str | None = None, thumbnail: str | None = None, footer: str | None = None, + audit: bool = False, ) -> tuple[bool, discord.Interaction, discord.Embed]: """Prompt the user for confirmation. @@ -26,6 +27,7 @@ async def confirm_action( image (str, optional): The image URL for the confirmation embed. Defaults to None. thumbnail (str, optional): The thumbnail URL for the confirmation embed. Defaults to None. footer: str | None = None, + audit (bool): Whether to log the command in the audit log. Returns: tuple(bool, discord.InteractionMessage): A tuple containing the user's response and success response coroutine. @@ -65,4 +67,9 @@ async def confirm_action( if footer is not None: response_embed.set_footer(text=footer) + if audit: + await ctx.post_to_audit_log(title.rstrip("?")) + else: + ctx.log_command(title.rstrip("?"), LogLevel.DEBUG) + return (True, msg, response_embed) diff --git a/src/valentina/views/thumbnail_review.py b/src/valentina/views/thumbnail_review.py index c4f3a4b3..9f06f67b 100644 --- a/src/valentina/views/thumbnail_review.py +++ b/src/valentina/views/thumbnail_review.py @@ -44,9 +44,7 @@ async def confirm_callback(self, button: Button, interaction: discord.Interactio await self.guild.save() # Log to audit log - await self.ctx.post_to_audit_log( - f"Deleted thumbnail id `{self.index}`\n{self.thumbnail.url}" - ) + await self.ctx.post_to_audit_log(f"Delete thumbnail `{self.index}`\n{self.thumbnail.url}") # Respond to user await interaction.response.edit_message( @@ -94,7 +92,7 @@ async def select_callback(self, select, interaction: discord.Interaction) -> Non # Log to audit log await self.ctx.post_to_audit_log( - f"Thumbnail id `{self.index}` categorized to {new_cat} \n{self.thumbnail.url}", + f"Thumbnail `{self.index}` categorized to {new_cat} \n{self.thumbnail.url}", ) # Respond to user