From 6066898dafc2f2260c65447b6fe5e3d3316da4ba Mon Sep 17 00:00:00 2001 From: Slipstreamm Date: Sat, 14 Jun 2025 13:58:24 -0600 Subject: [PATCH] feat(welcome): Enhance welcome and goodbye messages Welcome and goodbye messages now use a more detailed LayoutView with sections for member info, server statistics, and dynamic styling. - Welcome messages display total server members and the new member's account age. - Goodbye messages show current member count, the leaving member's join date and time in server, with dynamic colors and titles for leave, kick, and ban events. - All command feedback for welcome/goodbye configuration, testing, and error handling has been updated to use rich Discord Embeds for improved user experience. --- cogs/welcome_cog.py | 310 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 254 insertions(+), 56 deletions(-) diff --git a/cogs/welcome_cog.py b/cogs/welcome_cog.py index f187316..47d5a7a 100644 --- a/cogs/welcome_cog.py +++ b/cogs/welcome_cog.py @@ -18,42 +18,147 @@ log = logging.getLogger(__name__) class WelcomeMessageView(ui.LayoutView): - """A simple view for welcome messages.""" + """An enhanced view for welcome messages with member count and improved styling.""" - def __init__(self, member: discord.Member, message: str): + def __init__(self, member: discord.Member, message: str, member_count: int): super().__init__(timeout=None) - container = ui.Container(accent_colour=member.color or discord.Color.blurple()) + # 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 header_section = ui.Section( accessory=ui.Thumbnail( media=member.display_avatar.url, - description="User Avatar", + 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)) - + container.add_item(header_section) + + # Add separator + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + + # Member info section + info_section = ui.Section() + info_section.add_item(ui.TextDisplay(f"**Member:** {member.display_name}")) + info_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" + + info_section.add_item(ui.TextDisplay(f"**Account Age:** {age_str}")) + container.add_item(info_section) + + # Add separator + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + + # Server stats section + stats_section = ui.Section() + stats_section.add_item(ui.TextDisplay(f"๐Ÿ“Š **Server Statistics**")) + stats_section.add_item(ui.TextDisplay(f"**Total Members:** {member_count:,}")) + stats_section.add_item(ui.TextDisplay(f"**You are member #{member_count:,}**")) + + container.add_item(stats_section) + + # Add separator + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + + # Welcome footer + footer_section = ui.Section() + footer_section.add_item(ui.TextDisplay("๐Ÿ’ฌ Feel free to introduce yourself and have fun!")) + container.add_item(footer_section) + self.add_item(container) class GoodbyeMessageView(ui.LayoutView): - """A simple view for goodbye messages.""" + """An enhanced view for goodbye messages with member count and improved styling.""" - def __init__(self, member: discord.Member, message: str): + def __init__(self, member: discord.Member, message: str, member_count: int, reason: str = "left"): super().__init__(timeout=None) - container = ui.Container(accent_colour=discord.Color.dark_grey()) + # 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="User Avatar", + description="Former Member Avatar", ) ) + + header_section.add_item(ui.TextDisplay(f"{emoji} **{title}** {emoji}")) header_section.add_item(ui.TextDisplay(message)) - + container.add_item(header_section) + + # Add separator + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + + # Member info section + info_section = ui.Section() + info_section.add_item(ui.TextDisplay(f"**Member:** {member.display_name}")) + info_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 ''}" + + info_section.add_item(ui.TextDisplay(f"**Joined:** {join_date}")) + info_section.add_item(ui.TextDisplay(f"**Time in Server:** {duration_str}")) + + container.add_item(info_section) + + # Add separator + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + + # Server stats section + stats_section = ui.Section() + stats_section.add_item(ui.TextDisplay(f"๐Ÿ“Š **Server Statistics**")) + stats_section.add_item(ui.TextDisplay(f"**Current Members:** {member_count:,}")) + + container.add_item(stats_section) + self.add_item(container) @@ -99,7 +204,10 @@ class WelcomeCog(commands.Cog): user=member.mention, username=member.name, server=guild.name ) - view = WelcomeMessageView(member, formatted_message) + # 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}") @@ -188,17 +296,20 @@ class WelcomeCog(commands.Cog): server=guild.name, ) elif reason == "kicked": - formatted_message = f"{member.name} was kicked from the server" + 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 += f" by **{entry_user.name}**" formatted_message += "." else: # banned - formatted_message = f"{member.name} was banned from the server" + 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 += f" by **{entry_user.name}**" formatted_message += "." - view = GoodbyeMessageView(member, 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})" @@ -242,14 +353,27 @@ class WelcomeCog(commands.Cog): ) if success_channel and success_message: # Both need to succeed - await ctx.send( - f"Welcome messages will now be sent to {channel.mention} with the template:\n```\n{message_template}\n```" + 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.") + await ctx.send("โŒ Failed to save welcome settings. Check logs.") log.error(f"Failed to save welcome settings for guild {guild_id}") @commands.command( @@ -268,12 +392,17 @@ class WelcomeCog(commands.Cog): 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.") + 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.") + await ctx.send("โŒ Failed to disable welcome messages. Check logs.") log.error(f"Failed to disable welcome settings for guild {guild_id}") @commands.command( @@ -303,14 +432,32 @@ class WelcomeCog(commands.Cog): ) if success_channel and success_message: # Both need to succeed - await ctx.send( - f"Goodbye messages will now be sent to {channel.mention} with the template:\n```\n{message_template}\n```" + 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.") + await ctx.send("โŒ Failed to save goodbye settings. Check logs.") log.error(f"Failed to save goodbye settings for guild {guild_id}") @commands.command( @@ -329,12 +476,17 @@ class WelcomeCog(commands.Cog): 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.") + 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.") + await ctx.send("โŒ Failed to disable goodbye messages. Check logs.") log.error(f"Failed to disable goodbye settings for guild {guild_id}") # --- Test Commands --- @@ -356,10 +508,13 @@ class WelcomeCog(commands.Cog): """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, + + 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( @@ -381,17 +536,24 @@ class WelcomeCog(commands.Cog): ) if not goodbye_channel_id_str or goodbye_channel_id_str == "__NONE__": - await ctx.send("Goodbye message channel is not configured.", ephemeral=True) + 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): - await ctx.send( - "Configured goodbye channel not found or is not a text channel.", - ephemeral=True, + 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 --- @@ -403,56 +565,93 @@ class WelcomeCog(commands.Cog): ) elif reason == "kicked": formatted_message = ( - f"{target_member.name} was kicked from the server by {ctx.author.name}." + 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}." + f"**{target_member.name}** was banned from the server by **{ctx.author.name}**." ) - view = GoodbyeMessageView(target_member, formatted_message) + # 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) - await ctx.send( - f"Simulated goodbye message for {target_member.mention} (Reason: {reason}). Check the configured goodbye channel.", - ephemeral=True, + + 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: - 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, + 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}" ) - await ctx.send("An unexpected error occurred.", ephemeral=True) + 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): - await ctx.send("You need Administrator permissions to use this command.", ephemeral=True) + 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): - await ctx.send( - f"Invalid argument provided. Check the command help: `{ctx.prefix}help {ctx.command.name}`", - ephemeral=True + 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): - await ctx.send( - f"Missing required argument. Check the command help: `{ctx.prefix}help {ctx.command.name}`", - ephemeral=True + 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): - await ctx.send("This command cannot be used in private messages.", ephemeral=True) + 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}" ) - await ctx.send("An unexpected error occurred. Please check the logs.", ephemeral=True) + 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): @@ -470,5 +669,4 @@ async def setup(bot: commands.Bot): welcome_cog = WelcomeCog(bot) await bot.add_cog(welcome_cog) - log.info("WelcomeCog loaded.") - \ No newline at end of file + log.info("WelcomeCog loaded.") \ No newline at end of file