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.
This commit is contained in:
Slipstreamm 2025-06-14 13:58:24 -06:00
parent 4778237089
commit 6066898daf

View File

@ -18,42 +18,147 @@ log = logging.getLogger(__name__)
class WelcomeMessageView(ui.LayoutView): 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) 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( header_section = ui.Section(
accessory=ui.Thumbnail( accessory=ui.Thumbnail(
media=member.display_avatar.url, 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)) header_section.add_item(ui.TextDisplay(message))
container.add_item(header_section) 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) self.add_item(container)
class GoodbyeMessageView(ui.LayoutView): 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) 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( header_section = ui.Section(
accessory=ui.Thumbnail( accessory=ui.Thumbnail(
media=member.display_avatar.url, 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)) header_section.add_item(ui.TextDisplay(message))
container.add_item(header_section) 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) self.add_item(container)
@ -99,7 +204,10 @@ class WelcomeCog(commands.Cog):
user=member.mention, username=member.name, server=guild.name 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) await channel.send(view=view)
log.info(f"Sent welcome message for {member.name} in guild {guild.id}") log.info(f"Sent welcome message for {member.name} in guild {guild.id}")
@ -188,17 +296,20 @@ class WelcomeCog(commands.Cog):
server=guild.name, server=guild.name,
) )
elif reason == "kicked": 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: 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 += "." formatted_message += "."
else: # banned 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: 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 += "." 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) await channel.send(view=view)
log.info( log.info(
f"Sent goodbye message for {member.name} in guild {guild.id} (Reason: {reason})" 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 if success_channel and success_message: # Both need to succeed
await ctx.send( embed = discord.Embed(
f"Welcome messages will now be sent to {channel.mention} with the template:\n```\n{message_template}\n```" 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( log.info(
f"Welcome settings updated for guild {guild_id} by {ctx.author.name}" f"Welcome settings updated for guild {guild_id} by {ctx.author.name}"
) )
else: 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}") log.error(f"Failed to save welcome settings for guild {guild_id}")
@commands.command( @commands.command(
@ -268,12 +392,17 @@ class WelcomeCog(commands.Cog):
success_message = await settings_manager.set_setting(guild_id, key_message, None) success_message = await settings_manager.set_setting(guild_id, key_message, None)
if success_channel and success_message: # Both need to succeed 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( log.info(
f"Welcome messages disabled for guild {guild_id} by {ctx.author.name}" f"Welcome messages disabled for guild {guild_id} by {ctx.author.name}"
) )
else: 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}") log.error(f"Failed to disable welcome settings for guild {guild_id}")
@commands.command( @commands.command(
@ -303,14 +432,32 @@ class WelcomeCog(commands.Cog):
) )
if success_channel and success_message: # Both need to succeed if success_channel and success_message: # Both need to succeed
await ctx.send( embed = discord.Embed(
f"Goodbye messages will now be sent to {channel.mention} with the template:\n```\n{message_template}\n```" 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( log.info(
f"Goodbye settings updated for guild {guild_id} by {ctx.author.name}" f"Goodbye settings updated for guild {guild_id} by {ctx.author.name}"
) )
else: 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}") log.error(f"Failed to save goodbye settings for guild {guild_id}")
@commands.command( @commands.command(
@ -329,12 +476,17 @@ class WelcomeCog(commands.Cog):
success_message = await settings_manager.set_setting(guild_id, key_message, None) success_message = await settings_manager.set_setting(guild_id, key_message, None)
if success_channel and success_message: # Both need to succeed 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( log.info(
f"Goodbye messages disabled for guild {guild_id} by {ctx.author.name}" f"Goodbye messages disabled for guild {guild_id} by {ctx.author.name}"
) )
else: 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}") log.error(f"Failed to disable goodbye settings for guild {guild_id}")
# --- Test Commands --- # --- Test Commands ---
@ -356,10 +508,13 @@ class WelcomeCog(commands.Cog):
"""Simulates a member joining to test the welcome message.""" """Simulates a member joining to test the welcome message."""
target_member = member or ctx.author target_member = member or ctx.author
await self.on_member_join(target_member) await self.on_member_join(target_member)
await ctx.send(
f"Simulated welcome message for {target_member.mention}. Check the configured welcome channel.", embed = discord.Embed(
ephemeral=True, 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") @testmessage.command(name="goodbye")
async def test_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__": 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 return
try: try:
goodbye_channel_id = int(goodbye_channel_id_str) goodbye_channel_id = int(goodbye_channel_id_str)
channel = guild.get_channel(goodbye_channel_id) channel = guild.get_channel(goodbye_channel_id)
if not channel or not isinstance(channel, discord.TextChannel): if not channel or not isinstance(channel, discord.TextChannel):
await ctx.send( embed = discord.Embed(
"Configured goodbye channel not found or is not a text channel.", title="❌ Configuration Error",
ephemeral=True, description="Configured goodbye channel not found or is not a text channel.",
color=discord.Color.red()
) )
await ctx.send(embed=embed, ephemeral=True)
return return
# --- Format and send message based on simulated reason --- # --- Format and send message based on simulated reason ---
@ -403,56 +565,93 @@ class WelcomeCog(commands.Cog):
) )
elif reason == "kicked": elif reason == "kicked":
formatted_message = ( 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 else: # banned
formatted_message = ( 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 channel.send(view=view)
await ctx.send(
f"Simulated goodbye message for {target_member.mention} (Reason: {reason}). Check the configured goodbye channel.", embed = discord.Embed(
ephemeral=True, 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: except ValueError:
await ctx.send("Invalid goodbye channel ID configured.", ephemeral=True) embed = discord.Embed(
except discord.Forbidden: title="❌ Configuration Error",
await ctx.send( description="Invalid goodbye channel ID configured.",
"I don't have permissions to send messages in the configured goodbye channel.", color=discord.Color.red()
ephemeral=True,
) )
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: except Exception as e:
log.exception( log.exception(
f"Error sending test goodbye message for guild {guild.id}: {e}" 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 # Error Handling for this Cog
async def cog_command_error(self, ctx: commands.Context, error: commands.CommandError): async def cog_command_error(self, ctx: commands.Context, error: commands.CommandError):
"""Handles errors for all commands in this cog.""" """Handles errors for all commands in this cog."""
if isinstance(error, commands.MissingPermissions): 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): elif isinstance(error, commands.BadArgument):
await ctx.send( embed = discord.Embed(
f"Invalid argument provided. Check the command help: `{ctx.prefix}help {ctx.command.name}`", title="❌ Invalid Argument",
ephemeral=True 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): elif isinstance(error, commands.MissingRequiredArgument):
await ctx.send( embed = discord.Embed(
f"Missing required argument. Check the command help: `{ctx.prefix}help {ctx.command.name}`", title="❌ Missing Argument",
ephemeral=True 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): 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: else:
original_error = getattr(error, 'original', error) original_error = getattr(error, 'original', error)
log.error( log.error(
f"Unhandled error in WelcomeCog command '{ctx.command.name}': {original_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): async def setup(bot: commands.Bot):
@ -471,4 +670,3 @@ async def setup(bot: commands.Bot):
welcome_cog = WelcomeCog(bot) welcome_cog = WelcomeCog(bot)
await bot.add_cog(welcome_cog) await bot.add_cog(welcome_cog)
log.info("WelcomeCog loaded.") log.info("WelcomeCog loaded.")