feat(welcome): Enhance welcome and goodbye messages

Integrate `discord.ui.LayoutView` to provide richer, more structured welcome and goodbye messages with user avatars and accent colors.

The `on_member_remove` event now utilizes audit logs to determine if a member was kicked or banned, providing more context in goodbye messages.

Add `testmessage` command group with `welcome` and `goodbye` subcommands to allow administrators to easily test message configurations.

Replace development-time `print` statements with standard `logging` for cleaner output and better debugging.
This commit is contained in:
Slipstreamm 2025-06-14 13:31:35 -06:00
parent 7142c0f370
commit 4c17db72a8

View File

@ -1,68 +1,88 @@
import discord import discord
from discord.ext import commands from discord.ext import commands
from discord import ui
import logging import logging
import sys import sys
import os 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 # 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__)))) sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import settings_manager import settings_manager
from global_bot_accessor import get_bot_instance
log = logging.getLogger(__name__) 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): class WelcomeCog(commands.Cog):
"""Handles welcome and goodbye messages for guilds.""" """Handles welcome and goodbye messages for guilds."""
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = 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_join, "on_member_join")
self.bot.add_listener(self.on_member_remove, "on_member_remove") 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): async def on_member_join(self, member: discord.Member):
"""Sends a welcome message when a new member joins.""" """Sends a welcome message when a new member joins."""
print(f"WelcomeCog: on_member_join event triggered for {member.name}")
guild = member.guild guild = member.guild
if not guild: if not guild:
print(f"WelcomeCog: Guild not found for member {member.name}")
return return
log.debug(f"Member {member.name} joined guild {guild.name} ({guild.id})") 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 --- # --- Fetch settings ---
print(f"WelcomeCog: Fetching welcome settings for guild {guild.id}")
welcome_channel_id_str = await settings_manager.get_setting( welcome_channel_id_str = await settings_manager.get_setting(
guild.id, "welcome_channel_id" guild.id, "welcome_channel_id"
) )
welcome_message_template = await settings_manager.get_setting( welcome_message_template = await settings_manager.get_setting(
guild.id, "welcome_message", default="Welcome {user} to {server}!" 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__": if not welcome_channel_id_str or welcome_channel_id_str == "__NONE__":
log.debug(f"Welcome channel not configured for guild {guild.id}") log.debug(f"Welcome channel not configured for guild {guild.id}")
print(f"WelcomeCog: Welcome channel not configured for guild {guild.id}")
return return
try: try:
@ -72,16 +92,15 @@ class WelcomeCog(commands.Cog):
log.warning( log.warning(
f"Welcome channel ID {welcome_channel_id} not found or not text channel in guild {guild.id}" 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 return
# --- Format and send message --- # --- Format and send message ---
# Basic formatting, can be expanded
formatted_message = welcome_message_template.format( formatted_message = welcome_message_template.format(
user=member.mention, username=member.name, server=guild.name 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}") log.info(f"Sent welcome message for {member.name} in guild {guild.id}")
except ValueError: except ValueError:
@ -96,32 +115,23 @@ class WelcomeCog(commands.Cog):
log.exception(f"Error sending welcome message for guild {guild.id}: {e}") log.exception(f"Error sending welcome message for guild {guild.id}: {e}")
async def on_member_remove(self, member: discord.Member): async def on_member_remove(self, member: discord.Member):
"""Sends a goodbye message when a member leaves.""" """Sends a goodbye message when a member leaves, is kicked, or is banned."""
print(f"WelcomeCog: on_member_remove event triggered for {member.name}")
guild = member.guild guild = member.guild
if not guild: if not guild:
print(f"WelcomeCog: Guild not found for member {member.name}")
return return
log.debug(f"Member {member.name} left guild {guild.name} ({guild.id})") 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 --- # --- Fetch settings ---
print(f"WelcomeCog: Fetching goodbye settings for guild {guild.id}")
goodbye_channel_id_str = await settings_manager.get_setting( goodbye_channel_id_str = await settings_manager.get_setting(
guild.id, "goodbye_channel_id" guild.id, "goodbye_channel_id"
) )
goodbye_message_template = await settings_manager.get_setting( goodbye_message_template = await settings_manager.get_setting(
guild.id, "goodbye_message", default="{username} has left the server." 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__": if not goodbye_channel_id_str or goodbye_channel_id_str == "__NONE__":
log.debug(f"Goodbye channel not configured for guild {guild.id}") log.debug(f"Goodbye channel not configured for guild {guild.id}")
print(f"WelcomeCog: Goodbye channel not configured for guild {guild.id}")
return return
try: try:
@ -133,15 +143,66 @@ class WelcomeCog(commands.Cog):
) )
return 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 --- # --- Format and send message ---
if reason == "left":
formatted_message = goodbye_message_template.format( formatted_message = goodbye_message_template.format(
user=member.mention, # Might not be mentionable after leaving user=member.mention, # Might not be mentionable after leaving
username=member.name, username=member.name,
server=guild.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 += "."
await channel.send(formatted_message) view = GoodbyeMessageView(member, formatted_message)
log.info(f"Sent goodbye message for {member.name} in guild {guild.id}") await channel.send(view=view)
log.info(
f"Sent goodbye message for {member.name} in guild {guild.id} (Reason: {reason})"
)
except ValueError: except ValueError:
log.error( log.error(
@ -203,12 +264,8 @@ class WelcomeCog(commands.Cog):
key_message = "welcome_message" # Also clear the message template key_message = "welcome_message" # Also clear the message template
# Use set_setting with None to delete the settings # Use set_setting with None to delete the settings
success_channel = await settings_manager.set_setting( success_channel = await settings_manager.set_setting(guild_id, key_channel, None)
guild_id, key_channel, None 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.") await ctx.send("Welcome messages have been disabled.")
@ -268,12 +325,8 @@ class WelcomeCog(commands.Cog):
key_message = "goodbye_message" key_message = "goodbye_message"
# Use set_setting with None to delete the settings # Use set_setting with None to delete the settings
success_channel = await settings_manager.set_setting( success_channel = await settings_manager.set_setting(guild_id, key_channel, None)
guild_id, key_channel, None 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.") 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.") 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}")
# 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 # Error Handling for this Cog
@set_welcome.error @set_welcome.error
@disable_welcome.error @disable_welcome.error
@set_goodbye.error @set_goodbye.error
@disable_goodbye.error @disable_goodbye.error
@testmessage.error
async def on_command_error(self, ctx: commands.Context, error): async def on_command_error(self, ctx: commands.Context, error):
if isinstance(error, commands.MissingPermissions): if isinstance(error, commands.MissingPermissions):
await ctx.send("You need Administrator permissions to use this command.") 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): async def setup(bot: commands.Bot):
# Ensure bot has pools initialized before adding the cog # Ensure bot has pools initialized before adding the cog
print("WelcomeCog setup function called!")
if ( if (
not hasattr(bot, "pg_pool") not hasattr(bot, "pg_pool")
or not hasattr(bot, "redis") or not hasattr(bot, "redis")
@ -321,12 +471,8 @@ async def setup(bot: commands.Bot):
log.warning( log.warning(
"Bot pools not initialized before loading WelcomeCog. Cog will not load." "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 return # Prevent loading if pools are missing
welcome_cog = WelcomeCog(bot) welcome_cog = WelcomeCog(bot)
await bot.add_cog(welcome_cog) await bot.add_cog(welcome_cog)
print(
f"WelcomeCog loaded! Event listeners registered: on_member_join, on_member_remove"
)
log.info("WelcomeCog loaded.") log.info("WelcomeCog loaded.")