discordbot/cogs/welcome_cog.py
Slipstreamm 4778237089 refactor(welcome): Remove disable_goodbye error handler
Removes the dedicated error handler for the `disable_goodbye` command.
This allows for more centralized or general error handling to take over.
Adds a log message when the WelcomeCog is successfully loaded.
2025-06-14 13:40:54 -06:00

474 lines
19 KiB
Python

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):
"""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):
"""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
)
view = WelcomeMessageView(member, formatted_message)
await channel.send(view=view)
log.info(f"Sent welcome message for {member.name} in guild {guild.id}")
except ValueError:
log.error(
f"Invalid welcome_channel_id '{welcome_channel_id_str}' configured for guild {guild.id}"
)
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 += "."
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:
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
await ctx.send(
f"Welcome messages will now be sent to {channel.mention} with the template:\n```\n{message_template}\n```"
)
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
await ctx.send("Welcome messages have been disabled.")
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
await ctx.send(
f"Goodbye messages will now be sent to {channel.mention} with the template:\n```\n{message_template}\n```"
)
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
await ctx.send("Goodbye messages have been disabled.")
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)
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
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)
elif isinstance(error, commands.BadArgument):
await ctx.send(
f"Invalid argument provided. Check the command help: `{ctx.prefix}help {ctx.command.name}`",
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
)
elif isinstance(error, commands.NoPrivateMessage):
await ctx.send("This command cannot be used in private messages.", 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)
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.")