diff --git a/cogs/__init__.py b/cogs/__init__.py index bfd6accc..4a924078 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -11,6 +11,7 @@ "ArchiveCommandCog", "GetTokenAuthorisationCommandCog", "CommandErrorCog", + "CommandSuccessCog", "DeleteAllCommandsCog", "EditMessageCommandCog", "EnsureMembersInductedCommandCog", @@ -49,8 +50,10 @@ AnnualYearChannelsIncrementCommandCog, CommitteeHandoverCommandCog, ) + from .archive import ArchiveCommandCog from .command_error import CommandErrorCog +from .command_success import CommandSuccessCog from .delete_all import DeleteAllCommandsCog from .edit_message import EditMessageCommandCog from .get_token_authorisation import GetTokenAuthorisationCommandCog @@ -85,6 +88,7 @@ def setup(bot: TeXBot) -> None: ArchiveCommandCog, GetTokenAuthorisationCommandCog, CommandErrorCog, + CommandSuccessCog, DeleteAllCommandsCog, EditMessageCommandCog, EnsureMembersInductedCommandCog, diff --git a/cogs/archive.py b/cogs/archive.py index 7e348ade..79867725 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -118,6 +118,7 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> ), ephemeral=True, ) + self.log_user_error(message=f"Category {category.name} was already archived") return channel: AllChannelTypes @@ -189,10 +190,6 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> ctx, message=f"Channel {channel.mention} had invalid permissions", ) - logger.error( - "Channel %s had invalid permissions, so could not be archived.", - channel.name, - ) return except discord.Forbidden: @@ -203,14 +200,7 @@ async def archive(self, ctx: TeXBotApplicationContext, str_category_id: str) -> "the channels in the selected category." ), ) - logger.error( # noqa: TRY400 - ( - "TeX-Bot did not have access to " - "the channels in the selected category: " - "%s." - ), - category.name, - ) return await ctx.respond("Category successfully archived", ephemeral=True) + logger.debug("Category %s has been successfully archived.", category.name) diff --git a/cogs/command_success.py b/cogs/command_success.py new file mode 100644 index 00000000..3e04844c --- /dev/null +++ b/cogs/command_success.py @@ -0,0 +1,21 @@ +"""Contains cog classes for any command_success interactions.""" + +from collections.abc import Sequence + +__all__: Sequence[str] = ("CommandSuccessCog",) + + +import logging +from logging import Logger + +from utils import TeXBotApplicationContext, TeXBotBaseCog + +logger: Logger = logging.getLogger("TeX-Bot") + + +class CommandSuccessCog(TeXBotBaseCog): + """Cog class that defines additional code to execute upon a command success.""" + + @TeXBotBaseCog.listener() + async def on_application_command_completion(self, ctx: TeXBotApplicationContext) -> None: + logger.debug("Command execution complete.") # TODO: Pass command name to logger's extra diff --git a/cogs/delete_all.py b/cogs/delete_all.py index fb43de5c..218d70c6 100644 --- a/cogs/delete_all.py +++ b/cogs/delete_all.py @@ -5,12 +5,17 @@ __all__: Sequence[str] = ("DeleteAllCommandsCog",) +import logging +from logging import Logger + import discord from db.core.models import DiscordReminder, GroupMadeMember from db.core.models.utils import AsyncBaseModel from utils import CommandChecks, TeXBotApplicationContext, TeXBotBaseCog +logger: Logger = logging.getLogger("TeX-Bot") + class DeleteAllCommandsCog(TeXBotBaseCog): """Cog class that defines the "/delete-all" command group and command call-back methods.""" @@ -38,6 +43,7 @@ async def _delete_all(ctx: TeXBotApplicationContext, delete_model: type[AsyncBas f"All {delete_model_instances_name_plural} deleted successfully.", ephemeral=True, ) + logger.debug("All %s have been deleted.", delete_model_instances_name_plural) @delete_all.command( name="reminders", diff --git a/cogs/edit_message.py b/cogs/edit_message.py index f6139efb..c1531d34 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -5,8 +5,10 @@ __all__: Sequence[str] = ("EditMessageCommandCog",) +import logging import re from collections.abc import Set +from logging import Logger import discord @@ -19,6 +21,8 @@ TeXBotBaseCog, ) +logger: Logger = logging.getLogger("TeX-Bot") + class EditMessageCommandCog(TeXBotBaseCog): # noinspection SpellCheckingInspection @@ -136,3 +140,4 @@ async def edit_message(self, ctx: TeXBotApplicationContext, str_channel_id: str, return else: await ctx.respond("Message edited successfully.", ephemeral=True) + logger.debug("Message ID %s has been edited successfully.", message_id) diff --git a/cogs/induct.py b/cogs/induct.py index 52cf2b8a..d1ae12a4 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -112,6 +112,7 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) "(You can do this by right-clicking your name in the members-list " "to the right & selecting \"Edit Server Profile\").", ) + logger.debug("Sent welcome message to %s", after) if user_type != "member": await after.send( f"You can also get yourself an annual membership " @@ -122,6 +123,7 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) f"the {self.bot.group_short_name} Discord server:green_square:! " f"Checkout all the perks at {settings["MEMBERSHIP_PERKS_URL"]}", ) + logger.debug("Sent member message to %s", after) except discord.Forbidden: logger.info( "Failed to open DM channel to user %s so no welcome message was sent.", @@ -213,6 +215,12 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb "User has already been inducted. :information_source:" ), ) + self.log_user_error( + message=( + f"User {induction_member} was not inducted " + f"because they already have the guest role." + ), + ) return if not silent: @@ -231,6 +239,10 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb and "grab your roles" in message.content # noqa: COM812 ) if message_already_sent: + logger.debug( + "Welcome message not sent to %s because it's already been sent!", + induction_member, + ) break if not message_already_sent: @@ -239,11 +251,13 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb f"Remember to grab your roles in {roles_channel_mention} " "and say hello to everyone here! :wave:", ) + logger.debug("General induction message for user %s has been sent.") await induction_member.add_roles( guest_role, reason=INDUCT_AUDIT_MESSAGE, ) + logger.debug("Added guest role to %s", induction_member) # noinspection PyUnusedLocal applicant_role: discord.Role | None = None @@ -255,10 +269,12 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb applicant_role, reason=INDUCT_AUDIT_MESSAGE, ) + logger.debug("Removed Applicant role from %s", induction_member) tex_emoji: discord.Emoji | None = self.bot.get_emoji(743218410409820213) if not tex_emoji: tex_emoji = discord.utils.get(main_guild.emojis, name="TeX") + logger.debug("Could not find the TeX emoji!") if intro_channel: recent_message: discord.Message @@ -281,6 +297,10 @@ async def _perform_induction(self, ctx: TeXBotApplicationContext, induction_memb break await initial_response.edit(content=":white_check_mark: User inducted successfully.") + logger.debug( + "Induction completed successfully for user %s", + induction_member, + ) class InductSlashCommandCog(BaseInductCog): @@ -501,3 +521,8 @@ async def ensure_members_inducted(self, ctx: TeXBotApplicationContext) -> None: ), ephemeral=True, ) + logger.debug( + "Successfully inducted members" + if changes_made + else "No members have been inducted" # noqa: COM812 + ) diff --git a/cogs/kill.py b/cogs/kill.py index 3269998b..aeb65aee 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -72,6 +72,7 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: ), view=ConfirmKillView(), ) + logger.debug("Sent kill confirmation message.") confirmation_message: discord.Message = ( response if isinstance(response, discord.Message) @@ -89,6 +90,8 @@ async def kill(self, ctx: TeXBotApplicationContext) -> None: ), ) + logger.debug("Kill confirmation received: %s", button_interaction) + if button_interaction.data["custom_id"] == "shutdown_confirm": # type: ignore[index, typeddict-item] await confirmation_message.edit( content="My battery is low and it's getting dark...", diff --git a/cogs/make_member.py b/cogs/make_member.py index fc9ec509..5c7f0f29 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -129,6 +129,9 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) ), ephemeral=True, ) + self.log_user_error( + message=f"User {interaction_member} already had the member role!", + ) return if not re.fullmatch(r"\A\d{7}\Z", group_member_id): @@ -161,6 +164,7 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) ), ephemeral=True, ) + self.log_user_error(message=f"Student ID {group_member_id} has already been used.") return guild_member_ids: set[str] = set() @@ -249,6 +253,10 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) raise await ctx.respond("Successfully made you a member!", ephemeral=True) + logger.debug( + "User %s used the make member command successfully.", + interaction_member, + ) try: guest_role: discord.Role = await self.bot.guest_role @@ -264,6 +272,11 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) guest_role, reason="TeX Bot slash-command: \"/makemember\"", ) + logger.debug( + "User %s has been given the Guest role as well as the " + "member role as they had not yet been inducted.", + interaction_member, + ) # noinspection PyUnusedLocal applicant_role: discord.Role | None = None @@ -275,3 +288,9 @@ async def make_member(self, ctx: TeXBotApplicationContext, group_member_id: str) applicant_role, reason="TeX Bot slash-command: \"/makemember\"", ) + logger.debug( + ( + "Removed Applicant role from user %s after successful make-member command" + ), + interaction_member, + ) diff --git a/cogs/remind_me.py b/cogs/remind_me.py index 23e18c11..adef8ed6 100644 --- a/cogs/remind_me.py +++ b/cogs/remind_me.py @@ -246,6 +246,11 @@ async def remind_me(self, ctx: TeXBotApplicationContext, delay: str, message: st return await ctx.respond("Reminder set!", ephemeral=True) + logger.debug( + "Reminder '%s' set for user %s", + f"{message[:15]}..." if len(message) > 15 else message, + ctx.interaction.user, + ) await discord.utils.sleep_until(reminder.send_datetime) diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index c9157ad4..6ba5e2ce 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -172,6 +172,7 @@ async def send_get_roles_reminders(self) -> None: "and click on the icons to get optional roles like pronouns " "and year group identifiers.", ) + logger.debug("Role reminder sent to %s", member) except discord.Forbidden: logger.info( "Failed to open DM channel to user, %s, so no role reminder was sent.", diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index f866a36c..df2fa375 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -164,6 +164,7 @@ async def send_introduction_reminders(self) -> None: else None # type: ignore[arg-type] ), ) + logger.debug("Sent introduction reminder to %s", member) except discord.Forbidden: logger.info( "Failed to open DM channel with user, %s, " @@ -274,6 +275,7 @@ async def opt_out_introduction_reminders_button_callback(self, button: discord.B await IntroductionReminderOptOutMember.objects.acreate( discord_id=interaction_member.id, ) + logger.debug("Created opt out object for user %s", interaction_member) except ValidationError as create_introduction_reminder_opt_out_member_error: error_is_already_exists: bool = ( "hashed_member_id" in create_introduction_reminder_opt_out_member_error.message_dict # noqa: E501 diff --git a/cogs/strike.py b/cogs/strike.py index 2010937b..62753885 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -89,9 +89,17 @@ async def perform_moderation_action(strike_user: discord.Member, strikes: int, c elif strikes == 2: await strike_user.kick(reason=MODERATION_ACTION_REASON) + logger.debug( + "User %s has been automatically kicked for having 2 strikes.", + strike_user, + ) elif strikes == 3: await strike_user.ban(reason=MODERATION_ACTION_REASON) + logger.debug( + "User %s has been automatically banned for having 3 strikes.", + strike_user, + ) class ConfirmStrikeMemberView(View): @@ -111,7 +119,7 @@ async def yes_strike_member_button_callback(self, _: discord.Button, interaction The actual handling of the event is done by the command that sent the view, so all that is required is to delete the original message that sent this view. """ - logger.debug("\"Yes\" button pressed. %s", interaction) + logger.debug("\"Yes\" strike button pressed. %s", interaction) await interaction.response.edit_message(view=None) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error @discord.ui.button( # type: ignore[misc] @@ -128,7 +136,7 @@ async def no_strike_member_button_callback(self, _: discord.Button, interaction: The actual handling of the event is done by the command that sent the view, so all that is required is to delete the original message that sent this view. """ - logger.debug("\"No\" button pressed. %s", interaction) + logger.debug("\"No\" strike button pressed. %s", interaction) await interaction.response.edit_message(view=None) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error @@ -150,7 +158,7 @@ async def yes_manual_moderation_action_button_callback(self, _: discord.Button, the manual moderation tracker subroutine that sent the view, so all that is required is to delete the original message that sent this view. """ - logger.debug("\"Yes\" button pressed. %s", interaction) + logger.debug("\"Yes\" manual moderation action button pressed. %s", interaction) await interaction.response.edit_message(view=None) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error @discord.ui.button( # type: ignore[misc] @@ -168,7 +176,7 @@ async def no_manual_moderation_action_button_callback(self, _: discord.Button, i the manual moderation tracker subroutine that sent the view, so all that is required is to delete the original message that sent this view. """ - logger.debug("\"No\" button pressed. %s", interaction) + logger.debug("\"No\" manual moderation action button pressed. %s", interaction) await interaction.response.edit_message(view=None) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error @@ -190,7 +198,7 @@ async def yes_out_of_sync_ban_member_button_callback(self, _: discord.Button, in the manual moderation tracker subroutine that sent the view, so all that is required is to delete the original message that sent this view. """ - logger.debug("\"Yes\" button pressed. %s", interaction) + logger.debug("\"Yes\" out of sync ban member button pressed. %s", interaction) await interaction.response.edit_message(view=None) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error @discord.ui.button( # type: ignore[misc] @@ -208,7 +216,7 @@ async def no_out_of_sync_ban_member_button_callback(self, _: discord.Button, int the manual moderation tracker subroutine that sent the view, so all that is required is to delete the original message that sent this view. """ - logger.debug("\"No\" button pressed. %s", interaction) + logger.debug("\"No\" out of sync ban member button pressed. %s", interaction) await interaction.response.edit_message(view=None) # NOTE: Despite removing the view within the normal command processing loop, the view also needs to be removed here to prevent an Unknown Webhook error @@ -255,6 +263,7 @@ async def _send_strike_user_message(self, strike_user: discord.User | discord.Me f"to them.{includes_ban_message}\n\nA committee member will be in contact " "with you shortly, to discuss this further.", ) + logger.debug("Sent strike message to user %s", strike_user) async def _confirm_perform_moderation_action(self, message_sender_component: MessageSavingSenderComponent, interaction_user: discord.User, strike_user: discord.Member, confirm_strike_message: str, actual_strike_amount: int, button_callback_channel: discord.TextChannel | discord.DMChannel) -> None: # noqa: E501 await message_sender_component.send( @@ -282,6 +291,7 @@ async def _confirm_perform_moderation_action(self, message_sender_component: Mes ), view=None, ) + logger.debug("Cancelled strike action.") return if button_interaction.data["custom_id"] == "yes_strike_member": # type: ignore[index, typeddict-item] @@ -298,6 +308,7 @@ async def _confirm_perform_moderation_action(self, message_sender_component: Mes ), view=None, ) + logger.debug("Strike action against %s completed successfully.", strike_user) return raise ValueError @@ -355,6 +366,7 @@ async def _confirm_increase_strike(self, message_sender_component: MessageSaving }""" ), ) + logger.debug("Sent strike confirmation message.") await asyncio.sleep(118) await message_sender_component.delete() return diff --git a/cogs/write_roles.py b/cogs/write_roles.py index 8c3503c9..b53f9798 100644 --- a/cogs/write_roles.py +++ b/cogs/write_roles.py @@ -5,11 +5,16 @@ __all__: Sequence[str] = ("WriteRolesCommandCog",) +import logging +from logging import Logger + import discord from config import settings from utils import CommandChecks, TeXBotApplicationContext, TeXBotBaseCog +logger: Logger = logging.getLogger("TeX-Bot") + class WriteRolesCommandCog(TeXBotBaseCog): # noinspection SpellCheckingInspection @@ -39,3 +44,4 @@ async def write_roles(self, ctx: TeXBotApplicationContext) -> None: ) await ctx.respond("All messages sent successfully.", ephemeral=True) + logger.debug("Sent role messages!") diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index 60da668c..a512b488 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -151,6 +151,26 @@ async def send_error(cls, bot: TeXBot, interaction: discord.Interaction, interac ).rstrip(": ;"), ) + elif error_code or message: + cls.log_user_error(error_code=error_code, message=message) + + @classmethod + def log_user_error(cls, *, error_code: str | None = None, message: str | None = None) -> None: + if not error_code and not message: + MISSING_ALL_PARAMETERS_MESSAGE: Final[str] = ( + "At least one of 'error_code' or 'message' parameters must be provided " + "to log_user_error()." + ) + raise ValueError(MISSING_ALL_PARAMETERS_MESSAGE) + + logger.debug( + "User error%s", + ( + f"{f" ({error_code})" if error_code else ""}" + f"{f": {message}" if message else ""}" + ), + ) + @staticmethod async def autocomplete_get_text_channels(ctx: TeXBotAutocompleteContext) -> Set[discord.OptionChoice] | Set[str]: # noqa: E501 """