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:
parent
7142c0f370
commit
4c17db72a8
@ -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
|
||||||
|
|
||||||
# --- Format and send message ---
|
# --- Determine reason for leaving ---
|
||||||
formatted_message = goodbye_message_template.format(
|
reason = "left"
|
||||||
user=member.mention, # Might not be mentionable after leaving
|
entry_user = None
|
||||||
username=member.name,
|
|
||||||
server=guild.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
await channel.send(formatted_message)
|
# Check audit log for kick or ban. We check last 2 minutes just in case of delays.
|
||||||
log.info(f"Sent goodbye message for {member.name} in guild {guild.id}")
|
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:
|
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.")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user