diff --git a/cogs/welcome_cog.py b/cogs/welcome_cog.py index 80679fb..cdd57cc 100644 --- a/cogs/welcome_cog.py +++ b/cogs/welcome_cog.py @@ -1,68 +1,88 @@ import discord from discord.ext import commands +from discord import ui import logging import sys import os +from datetime import datetime, timedelta, timezone +from typing import Literal, Optional # Add the parent directory to sys.path to ensure settings_manager is accessible sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import settings_manager -from global_bot_accessor import get_bot_instance log = logging.getLogger(__name__) +# --- Message Component Views --- + + +class WelcomeMessageView(ui.LayoutView): + """A simple view for welcome messages.""" + + def __init__(self, member: discord.Member, message: str): + super().__init__(timeout=None) + + container = ui.Container(accent_colour=member.color or discord.Color.blurple()) + + header_section = ui.Section( + accessory=ui.Thumbnail( + media=member.display_avatar.url, + description="User Avatar", + ) + ) + header_section.add_item(ui.TextDisplay(message)) + + container.add_item(header_section) + self.add_item(container) + + +class GoodbyeMessageView(ui.LayoutView): + """A simple view for goodbye messages.""" + + def __init__(self, member: discord.Member, message: str): + super().__init__(timeout=None) + + container = ui.Container(accent_colour=discord.Color.dark_grey()) + + header_section = ui.Section( + accessory=ui.Thumbnail( + media=member.display_avatar.url, + description="User Avatar", + ) + ) + header_section.add_item(ui.TextDisplay(message)) + + container.add_item(header_section) + self.add_item(container) + + class WelcomeCog(commands.Cog): """Handles welcome and goodbye messages for guilds.""" def __init__(self, bot: commands.Bot): self.bot = bot - print("WelcomeCog: Initializing and registering event listeners") - - # Check existing event listeners - print( - f"WelcomeCog: Bot event listeners before registration: {self.bot.extra_events}" - ) - - # Register event listeners self.bot.add_listener(self.on_member_join, "on_member_join") self.bot.add_listener(self.on_member_remove, "on_member_remove") - # Check if event listeners were registered - print( - f"WelcomeCog: Bot event listeners after registration: {self.bot.extra_events}" - ) - print("WelcomeCog: Event listeners registered") - async def on_member_join(self, member: discord.Member): """Sends a welcome message when a new member joins.""" - print(f"WelcomeCog: on_member_join event triggered for {member.name}") guild = member.guild if not guild: - print(f"WelcomeCog: Guild not found for member {member.name}") return log.debug(f"Member {member.name} joined guild {guild.name} ({guild.id})") - print( - f"WelcomeCog: Member {member.name} joined guild {guild.name} ({guild.id})" - ) # --- Fetch settings --- - print(f"WelcomeCog: Fetching welcome settings for guild {guild.id}") welcome_channel_id_str = await settings_manager.get_setting( guild.id, "welcome_channel_id" ) welcome_message_template = await settings_manager.get_setting( guild.id, "welcome_message", default="Welcome {user} to {server}!" ) - print( - f"WelcomeCog: Retrieved settings - channel_id: {welcome_channel_id_str}, message: {welcome_message_template}" - ) - # Handle the "__NONE__" marker for potentially unset values if not welcome_channel_id_str or welcome_channel_id_str == "__NONE__": log.debug(f"Welcome channel not configured for guild {guild.id}") - print(f"WelcomeCog: Welcome channel not configured for guild {guild.id}") return try: @@ -72,16 +92,15 @@ class WelcomeCog(commands.Cog): log.warning( f"Welcome channel ID {welcome_channel_id} not found or not text channel in guild {guild.id}" ) - # Maybe remove the setting here if the channel is invalid? return # --- Format and send message --- - # Basic formatting, can be expanded formatted_message = welcome_message_template.format( user=member.mention, username=member.name, server=guild.name ) - await channel.send(formatted_message) + view = WelcomeMessageView(member, formatted_message) + await channel.send(view=view) log.info(f"Sent welcome message for {member.name} in guild {guild.id}") except ValueError: @@ -96,32 +115,23 @@ class WelcomeCog(commands.Cog): log.exception(f"Error sending welcome message for guild {guild.id}: {e}") async def on_member_remove(self, member: discord.Member): - """Sends a goodbye message when a member leaves.""" - print(f"WelcomeCog: on_member_remove event triggered for {member.name}") + """Sends a goodbye message when a member leaves, is kicked, or is banned.""" guild = member.guild if not guild: - print(f"WelcomeCog: Guild not found for member {member.name}") return log.debug(f"Member {member.name} left guild {guild.name} ({guild.id})") - print(f"WelcomeCog: Member {member.name} left guild {guild.name} ({guild.id})") # --- Fetch settings --- - print(f"WelcomeCog: Fetching goodbye settings for guild {guild.id}") goodbye_channel_id_str = await settings_manager.get_setting( guild.id, "goodbye_channel_id" ) goodbye_message_template = await settings_manager.get_setting( guild.id, "goodbye_message", default="{username} has left the server." ) - print( - f"WelcomeCog: Retrieved settings - channel_id: {goodbye_channel_id_str}, message: {goodbye_message_template}" - ) - # Handle the "__NONE__" marker if not goodbye_channel_id_str or goodbye_channel_id_str == "__NONE__": log.debug(f"Goodbye channel not configured for guild {guild.id}") - print(f"WelcomeCog: Goodbye channel not configured for guild {guild.id}") return try: @@ -133,15 +143,66 @@ class WelcomeCog(commands.Cog): ) return - # --- Format and send message --- - formatted_message = goodbye_message_template.format( - user=member.mention, # Might not be mentionable after leaving - username=member.name, - server=guild.name, - ) + # --- Determine reason for leaving --- + reason = "left" + entry_user = None - await channel.send(formatted_message) - log.info(f"Sent goodbye message for {member.name} in guild {guild.id}") + # Check audit log for kick or ban. We check last 2 minutes just in case of delays. + try: + # Check for ban first + async for entry in guild.audit_logs( + limit=1, + action=discord.AuditLogAction.ban, + after=datetime.now(timezone.utc) - timedelta(minutes=2), + ): + if entry.target and entry.target.id == member.id: + reason = "banned" + entry_user = entry.user + break + + # If not banned, check for kick + if reason == "left": + async for entry in guild.audit_logs( + limit=1, + action=discord.AuditLogAction.kick, + after=datetime.now(timezone.utc) - timedelta(minutes=2), + ): + if entry.target and entry.target.id == member.id: + reason = "kicked" + entry_user = entry.user + break + except discord.Forbidden: + log.warning( + f"Missing 'View Audit Log' permissions in guild {guild.id} to determine member remove reason." + ) + except Exception as e: + log.error( + f"Error checking audit log for {member.name} in {guild.id}: {e}" + ) + + # --- Format and send message --- + if reason == "left": + formatted_message = goodbye_message_template.format( + user=member.mention, # Might not be mentionable after leaving + username=member.name, + server=guild.name, + ) + elif reason == "kicked": + formatted_message = f"{member.name} was kicked from the server" + if entry_user and entry_user != self.bot.user: + formatted_message += f" by {entry_user.name}" + formatted_message += "." + else: # banned + formatted_message = f"{member.name} was banned from the server" + if entry_user and entry_user != self.bot.user: + formatted_message += f" by {entry_user.name}" + formatted_message += "." + + view = GoodbyeMessageView(member, formatted_message) + await channel.send(view=view) + log.info( + f"Sent goodbye message for {member.name} in guild {guild.id} (Reason: {reason})" + ) except ValueError: log.error( @@ -203,12 +264,8 @@ class WelcomeCog(commands.Cog): key_message = "welcome_message" # Also clear the message template # Use set_setting with None to delete the settings - success_channel = await settings_manager.set_setting( - guild_id, key_channel, None - ) - success_message = await settings_manager.set_setting( - guild_id, key_message, None - ) + success_channel = await settings_manager.set_setting(guild_id, key_channel, None) + success_message = await settings_manager.set_setting(guild_id, key_message, None) if success_channel and success_message: # Both need to succeed await ctx.send("Welcome messages have been disabled.") @@ -268,12 +325,8 @@ class WelcomeCog(commands.Cog): key_message = "goodbye_message" # Use set_setting with None to delete the settings - success_channel = await settings_manager.set_setting( - guild_id, key_channel, None - ) - success_message = await settings_manager.set_setting( - guild_id, key_message, None - ) + success_channel = await settings_manager.set_setting(guild_id, key_channel, None) + success_message = await settings_manager.set_setting(guild_id, key_message, None) if success_channel and success_message: # Both need to succeed await ctx.send("Goodbye messages have been disabled.") @@ -284,11 +337,109 @@ class WelcomeCog(commands.Cog): await ctx.send("Failed to disable goodbye messages. Check logs.") log.error(f"Failed to disable goodbye settings for guild {guild_id}") + # Error Handling for this Cog + @set_welcome.error + @disable_welcome.error + @set_goodbye.error + # --- Test Commands --- + @commands.group( + name="testmessage", + help="Test the welcome or goodbye messages.", + invoke_without_command=True, + ) + @commands.has_permissions(administrator=True) + @commands.guild_only() + async def testmessage(self, ctx: commands.Context): + """Shows help for the testmessage command group.""" + await ctx.send_help(ctx.command) + + @testmessage.command(name="welcome") + async def test_welcome( + self, ctx: commands.Context, member: Optional[discord.Member] = None + ): + """Simulates a member joining to test the welcome message.""" + target_member = member or ctx.author + await self.on_member_join(target_member) + await ctx.send( + f"Simulated welcome message for {target_member.mention}. Check the configured welcome channel.", + ephemeral=True, + ) + + @testmessage.command(name="goodbye") + async def test_goodbye( + self, + ctx: commands.Context, + reason: Literal["left", "kicked", "banned"] = "left", + member: Optional[discord.Member] = None, + ): + """Simulates a member leaving to test the goodbye message.""" + target_member = member or ctx.author + guild = ctx.guild + + # --- Fetch settings --- + goodbye_channel_id_str = await settings_manager.get_setting( + guild.id, "goodbye_channel_id" + ) + goodbye_message_template = await settings_manager.get_setting( + guild.id, "goodbye_message", default="{username} has left the server." + ) + + if not goodbye_channel_id_str or goodbye_channel_id_str == "__NONE__": + await ctx.send("Goodbye message channel is not configured.", ephemeral=True) + return + + try: + goodbye_channel_id = int(goodbye_channel_id_str) + channel = guild.get_channel(goodbye_channel_id) + if not channel or not isinstance(channel, discord.TextChannel): + await ctx.send( + "Configured goodbye channel not found or is not a text channel.", + ephemeral=True, + ) + return + + # --- Format and send message based on simulated reason --- + if reason == "left": + formatted_message = goodbye_message_template.format( + user=target_member.mention, + username=target_member.name, + server=guild.name, + ) + elif reason == "kicked": + formatted_message = ( + f"{target_member.name} was kicked from the server by {ctx.author.name}." + ) + else: # banned + formatted_message = ( + f"{target_member.name} was banned from the server by {ctx.author.name}." + ) + + view = GoodbyeMessageView(target_member, formatted_message) + await channel.send(view=view) + await ctx.send( + f"Simulated goodbye message for {target_member.mention} (Reason: {reason}). Check the configured goodbye channel.", + ephemeral=True, + ) + + except ValueError: + await ctx.send("Invalid goodbye channel ID configured.", ephemeral=True) + except discord.Forbidden: + await ctx.send( + "I don't have permissions to send messages in the configured goodbye channel.", + ephemeral=True, + ) + except Exception as e: + log.exception( + f"Error sending test goodbye message for guild {guild.id}: {e}" + ) + await ctx.send("An unexpected error occurred.", ephemeral=True) + # Error Handling for this Cog @set_welcome.error @disable_welcome.error @set_goodbye.error @disable_goodbye.error + @testmessage.error async def on_command_error(self, ctx: commands.Context, error): if isinstance(error, commands.MissingPermissions): await ctx.send("You need Administrator permissions to use this command.") @@ -311,7 +462,6 @@ class WelcomeCog(commands.Cog): async def setup(bot: commands.Bot): # Ensure bot has pools initialized before adding the cog - print("WelcomeCog setup function called!") if ( not hasattr(bot, "pg_pool") or not hasattr(bot, "redis") @@ -321,12 +471,8 @@ async def setup(bot: commands.Bot): log.warning( "Bot pools not initialized before loading WelcomeCog. Cog will not load." ) - print("WelcomeCog: Bot pools not initialized. Cannot load cog.") return # Prevent loading if pools are missing welcome_cog = WelcomeCog(bot) await bot.add_cog(welcome_cog) - print( - f"WelcomeCog loaded! Event listeners registered: on_member_join, on_member_remove" - ) log.info("WelcomeCog loaded.")