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 log = logging.getLogger(__name__) # --- Message Component Views --- class WelcomeMessageView(ui.LayoutView): """An enhanced view for welcome messages with member count and improved styling.""" def __init__(self, member: discord.Member, message: str, member_count: int): super().__init__(timeout=None) # Use member's color or a nice default accent_color = member.color if member.color != discord.Color.default() else discord.Color.green() container = ui.Container(accent_colour=accent_color) # Add a welcome banner/header with avatar header_section = ui.Section( accessory=ui.Thumbnail( media=member.display_avatar.url, description="New Member Avatar", ) ) # Welcome title with emoji header_section.add_item(ui.TextDisplay("๐ŸŽ‰ **Welcome to the Server!** ๐ŸŽ‰")) header_section.add_item(ui.TextDisplay(message)) # Add member info to the header section header_section.add_item(ui.TextDisplay(f"**Member:** {member.display_name}")) header_section.add_item(ui.TextDisplay(f"**Account Created:** {member.created_at.strftime('%B %d, %Y')}")) # Calculate account age account_age = datetime.now(timezone.utc) - member.created_at age_str = f"{account_age.days} days ago" if account_age.days >= 365: years = account_age.days // 365 months = (account_age.days % 365) // 30 age_str = f"{years} year{'s' if years != 1 else ''}, {months} month{'s' if months != 1 else ''} ago" elif account_age.days >= 30: months = account_age.days // 30 age_str = f"{months} month{'s' if months != 1 else ''} ago" header_section.add_item(ui.TextDisplay(f"**Account Age:** {age_str}")) # Add server stats to the header section header_section.add_item(ui.TextDisplay(f"๐Ÿ“Š **Server Statistics**")) header_section.add_item(ui.TextDisplay(f"**Total Members:** {member_count:,}")) header_section.add_item(ui.TextDisplay(f"**You are member #{member_count:,}**")) # Add welcome footer header_section.add_item(ui.TextDisplay("๐Ÿ’ฌ Feel free to introduce yourself and have fun!")) container.add_item(header_section) self.add_item(container) class GoodbyeMessageView(ui.LayoutView): """An enhanced view for goodbye messages with member count and improved styling.""" def __init__(self, member: discord.Member, message: str, member_count: int, reason: str = "left"): super().__init__(timeout=None) # Use darker colors for goodbye messages if reason == "banned": accent_color = discord.Color.red() emoji = "๐Ÿ”จ" title = "Member Banned" elif reason == "kicked": accent_color = discord.Color.orange() emoji = "๐Ÿ‘ข" title = "Member Kicked" else: accent_color = discord.Color.dark_grey() emoji = "๐Ÿ‘‹" title = "Member Left" container = ui.Container(accent_colour=accent_color) # Header section with avatar header_section = ui.Section( accessory=ui.Thumbnail( media=member.display_avatar.url, description="Former Member Avatar", ) ) header_section.add_item(ui.TextDisplay(f"{emoji} **{title}** {emoji}")) header_section.add_item(ui.TextDisplay(message)) # Add member info to the header section header_section.add_item(ui.TextDisplay(f"**Member:** {member.display_name}")) header_section.add_item(ui.TextDisplay(f"**Username:** {member.name}")) # Show join date if available if hasattr(member, 'joined_at') and member.joined_at: join_date = member.joined_at.strftime('%B %d, %Y') time_in_server = datetime.now(timezone.utc) - member.joined_at if time_in_server.days >= 365: years = time_in_server.days // 365 months = (time_in_server.days % 365) // 30 duration_str = f"{years} year{'s' if years != 1 else ''}, {months} month{'s' if months != 1 else ''}" elif time_in_server.days >= 30: months = time_in_server.days // 30 duration_str = f"{months} month{'s' if months != 1 else ''}" else: duration_str = f"{time_in_server.days} day{'s' if time_in_server.days != 1 else ''}" header_section.add_item(ui.TextDisplay(f"**Joined:** {join_date}")) header_section.add_item(ui.TextDisplay(f"**Time in Server:** {duration_str}")) # Add server stats to the header section header_section.add_item(ui.TextDisplay(f"๐Ÿ“Š **Server Statistics**")) header_section.add_item(ui.TextDisplay(f"**Current Members:** {member_count:,}")) 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 self.bot.add_listener(self.on_member_join, "on_member_join") self.bot.add_listener(self.on_member_remove, "on_member_remove") async def on_member_join(self, member: discord.Member): """Sends a welcome message when a new member joins.""" guild = member.guild if not guild: return log.debug(f"Member {member.name} joined guild {guild.name} ({guild.id})") # --- Fetch settings --- 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}!" ) if not welcome_channel_id_str or welcome_channel_id_str == "__NONE__": log.debug(f"Welcome channel not configured for guild {guild.id}") return try: welcome_channel_id = int(welcome_channel_id_str) channel = guild.get_channel(welcome_channel_id) if not channel or not isinstance(channel, discord.TextChannel): log.warning( f"Welcome channel ID {welcome_channel_id} not found or not text channel in guild {guild.id}" ) return # --- Format and send message --- formatted_message = welcome_message_template.format( user=member.mention, username=member.name, server=guild.name ) # Get current member count member_count = guild.member_count or len(guild.members) view = WelcomeMessageView(member, formatted_message, member_count) await channel.send(view=view) log.info(f"Sent welcome message for {member.name} in guild {guild.id}") except ValueError as e: log.error( f"ValueError in WelcomeCog for guild {guild.id}: {e}" ) except discord.Forbidden: log.error( f"Missing permissions to send welcome message in channel {welcome_channel_id} for guild {guild.id}" ) except Exception as e: 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, is kicked, or is banned.""" guild = member.guild if not guild: return log.debug(f"Member {member.name} left guild {guild.name} ({guild.id})") # --- 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__": log.debug(f"Goodbye channel not configured for guild {guild.id}") 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): log.warning( f"Goodbye channel ID {goodbye_channel_id} not found or not text channel in guild {guild.id}" ) return # --- Determine reason for leaving --- reason = "left" entry_user = None # 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 += "." # Get current member count member_count = guild.member_count or len(guild.members) view = GoodbyeMessageView(member, formatted_message, member_count, reason) await channel.send(view=view) log.info( f"Sent goodbye message for {member.name} in guild {guild.id} (Reason: {reason})" ) except ValueError: log.error( f"Invalid goodbye_channel_id '{goodbye_channel_id_str}' configured for guild {guild.id}" ) except discord.Forbidden: log.error( f"Missing permissions to send goodbye message in channel {goodbye_channel_id} for guild {guild.id}" ) except Exception as e: log.exception(f"Error sending goodbye message for guild {guild.id}: {e}") @commands.command( name="setwelcome", help="Sets the welcome message and channel. Usage: `setwelcome #channel [message template]`", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def set_welcome( self, ctx: commands.Context, channel: discord.TextChannel, *, message_template: str = "Welcome {user} to {server}!", ): """Sets the channel and template for welcome messages.""" guild_id = ctx.guild.id key_channel = "welcome_channel_id" key_message = "welcome_message" # Use settings_manager.set_setting success_channel = await settings_manager.set_setting( guild_id, key_channel, str(channel.id) ) success_message = await settings_manager.set_setting( guild_id, key_message, message_template ) if success_channel and success_message: # Both need to succeed embed = discord.Embed( title="โœ… Welcome Messages Configured", description=f"Welcome messages will now be sent to {channel.mention}", color=discord.Color.green() ) embed.add_field( name="Message Template", value=f"```\n{message_template}\n```", inline=False ) embed.add_field( name="Available Variables", value="`{user}` - Mentions the user\n`{username}` - User's name\n`{server}` - Server name", inline=False ) await ctx.send(embed=embed) log.info( f"Welcome settings updated for guild {guild_id} by {ctx.author.name}" ) else: await ctx.send("โŒ Failed to save welcome settings. Check logs.") log.error(f"Failed to save welcome settings for guild {guild_id}") @commands.command( name="disablewelcome", help="Disables welcome messages for this server." ) @commands.has_permissions(administrator=True) @commands.guild_only() async def disable_welcome(self, ctx: commands.Context): """Disables welcome messages by removing the channel setting.""" guild_id = ctx.guild.id key_channel = "welcome_channel_id" 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) if success_channel and success_message: # Both need to succeed embed = discord.Embed( title="โœ… Welcome Messages Disabled", description="Welcome messages have been disabled for this server.", color=discord.Color.orange() ) await ctx.send(embed=embed) log.info( f"Welcome messages disabled for guild {guild_id} by {ctx.author.name}" ) else: await ctx.send("โŒ Failed to disable welcome messages. Check logs.") log.error(f"Failed to disable welcome settings for guild {guild_id}") @commands.command( name="setgoodbye", help="Sets the goodbye message and channel. Usage: `setgoodbye #channel [message template]`", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def set_goodbye( self, ctx: commands.Context, channel: discord.TextChannel, *, message_template: str = "{username} has left the server.", ): """Sets the channel and template for goodbye messages.""" guild_id = ctx.guild.id key_channel = "goodbye_channel_id" key_message = "goodbye_message" # Use settings_manager.set_setting success_channel = await settings_manager.set_setting( guild_id, key_channel, str(channel.id) ) success_message = await settings_manager.set_setting( guild_id, key_message, message_template ) if success_channel and success_message: # Both need to succeed embed = discord.Embed( title="โœ… Goodbye Messages Configured", description=f"Goodbye messages will now be sent to {channel.mention}", color=discord.Color.green() ) embed.add_field( name="Message Template", value=f"```\n{message_template}\n```", inline=False ) embed.add_field( name="Available Variables", value="`{user}` - Mentions the user (may not work after leaving)\n`{username}` - User's name\n`{server}` - Server name", inline=False ) embed.add_field( name="Note", value="Kick and ban messages will override the template with automatic formatting.", inline=False ) await ctx.send(embed=embed) log.info( f"Goodbye settings updated for guild {guild_id} by {ctx.author.name}" ) else: await ctx.send("โŒ Failed to save goodbye settings. Check logs.") log.error(f"Failed to save goodbye settings for guild {guild_id}") @commands.command( name="disablegoodbye", help="Disables goodbye messages for this server." ) @commands.has_permissions(administrator=True) @commands.guild_only() async def disable_goodbye(self, ctx: commands.Context): """Disables goodbye messages by removing the channel setting.""" guild_id = ctx.guild.id key_channel = "goodbye_channel_id" 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) if success_channel and success_message: # Both need to succeed embed = discord.Embed( title="โœ… Goodbye Messages Disabled", description="Goodbye messages have been disabled for this server.", color=discord.Color.orange() ) await ctx.send(embed=embed) log.info( f"Goodbye messages disabled for guild {guild_id} by {ctx.author.name}" ) else: await ctx.send("โŒ Failed to disable goodbye messages. Check logs.") log.error(f"Failed to disable goodbye settings for guild {guild_id}") # --- 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) embed = discord.Embed( title="๐Ÿงช Test Message Sent", description=f"Simulated welcome message for {target_member.mention}. Check the configured welcome channel.", color=discord.Color.blue() ) await ctx.send(embed=embed, 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__": embed = discord.Embed( title="โŒ Configuration Error", description="Goodbye message channel is not configured.", color=discord.Color.red() ) await ctx.send(embed=embed, 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): embed = discord.Embed( title="โŒ Configuration Error", description="Configured goodbye channel not found or is not a text channel.", color=discord.Color.red() ) await ctx.send(embed=embed, 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}**." ) # Get current member count member_count = guild.member_count or len(guild.members) view = GoodbyeMessageView(target_member, formatted_message, member_count, reason) await channel.send(view=view) embed = discord.Embed( title="๐Ÿงช Test Message Sent", description=f"Simulated goodbye message for {target_member.mention} (Reason: {reason}). Check the configured goodbye channel.", color=discord.Color.blue() ) await ctx.send(embed=embed, ephemeral=True) except ValueError: embed = discord.Embed( title="โŒ Configuration Error", description="Invalid goodbye channel ID configured.", color=discord.Color.red() ) await ctx.send(embed=embed, ephemeral=True) except discord.Forbidden: embed = discord.Embed( title="โŒ Permission Error", description="I don't have permissions to send messages in the configured goodbye channel.", color=discord.Color.red() ) await ctx.send(embed=embed, ephemeral=True) except Exception as e: log.exception( f"Error sending test goodbye message for guild {guild.id}: {e}" ) embed = discord.Embed( title="โŒ Unexpected Error", description="An unexpected error occurred. Check the logs for details.", color=discord.Color.red() ) await ctx.send(embed=embed, ephemeral=True) # Error Handling for this Cog async def cog_command_error(self, ctx: commands.Context, error: commands.CommandError): """Handles errors for all commands in this cog.""" if isinstance(error, commands.MissingPermissions): embed = discord.Embed( title="โŒ Missing Permissions", description="You need Administrator permissions to use this command.", color=discord.Color.red() ) await ctx.send(embed=embed, ephemeral=True) elif isinstance(error, commands.BadArgument): embed = discord.Embed( title="โŒ Invalid Argument", description=f"Invalid argument provided. Check the command help: `{ctx.prefix}help {ctx.command.name}`", color=discord.Color.red() ) await ctx.send(embed=embed, ephemeral=True) elif isinstance(error, commands.MissingRequiredArgument): embed = discord.Embed( title="โŒ Missing Argument", description=f"Missing required argument. Check the command help: `{ctx.prefix}help {ctx.command.name}`", color=discord.Color.red() ) await ctx.send(embed=embed, ephemeral=True) elif isinstance(error, commands.NoPrivateMessage): embed = discord.Embed( title="โŒ Server Only", description="This command cannot be used in private messages.", color=discord.Color.red() ) await ctx.send(embed=embed, ephemeral=True) else: original_error = getattr(error, 'original', error) log.error( f"Unhandled error in WelcomeCog command '{ctx.command.name}': {original_error}" ) embed = discord.Embed( title="โŒ Unexpected Error", description="An unexpected error occurred. Please check the logs for details.", color=discord.Color.red() ) await ctx.send(embed=embed, ephemeral=True) async def setup(bot: commands.Bot): # Ensure bot has pools initialized before adding the cog if ( not hasattr(bot, "pg_pool") or not hasattr(bot, "redis") or bot.pg_pool is None or bot.redis is None ): log.warning( "Bot pools not initialized before loading WelcomeCog. Cog will not load." ) return # Prevent loading if pools are missing welcome_cog = WelcomeCog(bot) await bot.add_cog(welcome_cog) log.info("WelcomeCog loaded.")