Kuby/commandes/welcome.py

408 lines
21 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import disnake
from disnake.ext import commands
import sqlite3
import aiohttp
import io
import os
import logging
import asyncio
from PIL import Image, ImageDraw, ImageFont
kuby_logger = logging.getLogger("KubyBot")
class Welcome(commands.Cog):
def __init__(self, bot):
self.bot = bot
# Base de données à la racine - Chemin absolu
self.base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
self.db_path = os.path.join(self.base_dir, "config.db")
self._init_db()
def _init_db(self):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS server_configs (
server_id INTEGER PRIMARY KEY,
welcome_channel INTEGER,
goodbye_channel INTEGER,
welcome_dm_message TEXT,
welcome_server_name TEXT,
welcome_banner_background TEXT,
welcome_channel_message TEXT
)
''')
# Migration pour ajouter les colonnes si elles n'existent pas
cursor.execute("PRAGMA table_info(server_configs)")
columns = [col[1] for col in cursor.fetchall()]
if 'welcome_server_name' not in columns:
cursor.execute('ALTER TABLE server_configs ADD COLUMN welcome_server_name TEXT')
if 'welcome_banner_background' not in columns:
cursor.execute('ALTER TABLE server_configs ADD COLUMN welcome_banner_background TEXT')
if 'welcome_channel_message' not in columns:
cursor.execute('ALTER TABLE server_configs ADD COLUMN welcome_channel_message TEXT')
# Migration: Conversion des chemins absolus en chemins relatifs pour les bannières
cursor.execute("SELECT server_id, welcome_banner_background FROM server_configs WHERE welcome_banner_background IS NOT NULL")
for server_id, path in cursor.fetchall():
if path and os.path.isabs(path):
filename = os.path.basename(path)
# Vérifier si c'est bien une bannière dans le dossier banners
if "banner_" in filename:
cursor.execute("UPDATE server_configs SET welcome_banner_background = ? WHERE server_id = ?", (filename, server_id))
conn.commit()
conn.close()
async def create_welcome_image(self, username, avatar_url, server_name=None, banner_background=None, custom_message=None):
# Utiliser base_dir déjà calculé dans __init__
try:
# Télécharger l'avatar
async with aiohttp.ClientSession() as session:
async with session.get(avatar_url) as resp:
if resp.status != 200:
kuby_logger.error(f"[Welcome] Impossible de télécharger l'avatar: {resp.status}")
raise ValueError(f"Impossible de télécharger l'avatar (code: {resp.status})")
avatar_bytes = await resp.read()
# Vérifier et utiliser le fond personnalisé ou celui par défaut
bg_path = None
if banner_background:
# Si c'est un chemin absolu (pour compatibilité temporaire)
if os.path.isabs(banner_background) and os.path.exists(banner_background):
bg_path = banner_background
else:
# Sinon on cherche dans le dossier banners/
potential_path = os.path.join(self.base_dir, "banners", banner_background)
if os.path.exists(potential_path):
bg_path = potential_path
kuby_logger.debug(f"[Welcome] Fond personnalisé trouvé: {bg_path}")
if not bg_path:
default_bg = os.path.join(self.base_dir, 'background_template.png')
if not os.path.exists(default_bg):
kuby_logger.error(f"[Welcome] Fond par défaut introuvable: {default_bg}")
raise FileNotFoundError(f"Fond par défaut introuvable: {default_bg}")
bg_path = default_bg
kuby_logger.debug(f"[Welcome] Utilisation du fond par défaut: {bg_path}")
# Charger le fond
background = Image.open(bg_path)
draw = ImageDraw.Draw(background)
# Charger la police avec support des accents
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-Bold.ttf"
if not os.path.exists(font_path):
kuby_logger.warning(f"[Welcome] Police DejaVu introuvable, fallback sur police custom")
font_path = os.path.join(self.base_dir, 'custom_font_large.otf')
font = ImageFont.truetype(font_path, 55)
# Construire le message à afficher sur la bannière
display_message = custom_message if custom_message else "Bienvenue {membre} chez {server}"
# Remplacer les placeholders
try:
display_message = display_message.format(
membre=username,
member=username,
server=server_name or "",
serveur=server_name or "",
username=username,
servername=server_name or "",
)
except Exception:
pass # Si placeholders inconnus, garder tel quel
# Zone de texte disponible (à droite de l'avatar)
text_area_left = 280
text_area_right = background.width - 20
text_area_width = text_area_right - text_area_left
# Découper le texte en lignes qui tiennent dans la zone
words = display_message.split()
lines = []
current_line = ""
for word in words:
test_line = f"{current_line} {word}".strip()
bbox = draw.textbbox((0, 0), test_line, font=font)
if bbox[2] - bbox[0] <= text_area_width:
current_line = test_line
else:
if current_line:
lines.append(current_line)
current_line = word
if current_line:
lines.append(current_line)
wrapped_text = "\n".join(lines)
# Mesurer le bloc de texte complet
bbox = draw.textbbox((0, 0), wrapped_text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Centrer le bloc dans la zone disponible
text_x = text_area_left + (text_area_width - text_width) // 2
text_y = (background.height - text_height) // 2
draw.text((text_x, text_y), wrapped_text, font=font, fill=(255, 255, 255), align="center")
# Ajouter l'avatar
avatar = Image.open(io.BytesIO(avatar_bytes)).convert("RGBA").resize((224, 224), Image.LANCZOS)
mask = Image.new('L', avatar.size, 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.ellipse((12, 12, 212, 212), fill=255)
background.paste(avatar, (40, 40), mask)
# Retourner l'image
img_byte_arr = io.BytesIO()
background.save(img_byte_arr, format='PNG')
img_byte_arr.seek(0)
return img_byte_arr
except Exception as e:
kuby_logger.error(f"[Welcome] Erreur lors de la création de l'image: {e}", exc_info=True)
return None
@commands.Cog.listener()
async def on_member_join(self, member):
kuby_logger.info(f"📥 [Welcome] Événement on_member_join reçu pour {member.name} (ID: {member.id}) sur {member.guild.name}")
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT welcome_channel, welcome_dm_message, welcome_server_name, welcome_banner_background, welcome_channel_message FROM server_configs WHERE server_id = ?', (member.guild.id,))
result = cursor.fetchone()
conn.close()
if not result:
kuby_logger.debug(f"⚠️ [Welcome] Aucune configuration trouvée dans la base de données pour le serveur {member.guild.id}")
return
welcome_channel = result[0]
welcome_dm_text = result[1]
server_name = result[2]
banner_background = result[3]
welcome_channel_message = result[4]
kuby_logger.debug(f"🔍 [Welcome] Config récupérée: DM={bool(welcome_dm_text)}, Channel={welcome_channel}")
# 1. Gestion du DM
if welcome_dm_text and not member.bot:
kuby_logger.info(f"✉️ [Welcome] Tentative d'envoi de DM à {member.name}")
try:
formatted_message = welcome_dm_text.format(
username=member.name,
servername=member.guild.name,
member_count=member.guild.member_count
)
# On combine le texte et le GIF dans un Embed élégant pour masquer le lien brut
gif_url = "https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXV1NW02NTk0bGF2ZHcyMGFna2F6amRrbGpobGpsMHowOW5xa2E5eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZtFuW4rsUbfrRzPauJ/giphy.gif"
embed = disnake.Embed(
description=formatted_message,
color=0x2b2d31 # Couleur sombre moderne et épurée
)
embed.set_image(url=gif_url)
await member.send(embed=embed)
kuby_logger.info(f"✅ [Welcome] DM envoyé avec succès à {member.name} sous forme d'Embed")
except disnake.Forbidden:
kuby_logger.warning(f"🚫 [Welcome] Impossible d'envoyer le DM à {member.name} (DMs fermés)")
except disnake.HTTPException as e:
if e.code == 40003: # Rate limited (Opening DMs too fast)
kuby_logger.warning(f"⏳ [Welcome] Limite de débit Discord atteinte pour les DMs : {member.name} n'a pas reçu son message.")
elif e.code == 50007: # Cannot send messages to this user
kuby_logger.warning(f"🚫 [Welcome] Impossible d'envoyer le DM à {member.name} (DMs fermés ou utilisateur non joignable)")
else:
kuby_logger.error(f"❌ [Welcome] Erreur HTTP lors de l'envoi du DM à {member.name}: {e}")
except Exception as e:
kuby_logger.error(f"❌ [Welcome] Erreur lors de l'envoi du DM à {member.name}: {e}", exc_info=True)
else:
kuby_logger.info(f" [Welcome] Aucun message de DM configuré pour le serveur {member.guild.id}")
except Exception as e:
kuby_logger.error(f"❌ [Welcome] Erreur critique dans on_member_join: {e}", exc_info=True)
# 2. Gestion du salon de bienvenue
if welcome_channel:
channel = member.guild.get_channel(welcome_channel)
if channel:
try:
# Créer l'image de bienvenue
server_display_name = server_name if server_name else member.guild.name
img = await self.create_welcome_image(
member.name,
member.avatar.url if member.avatar else member.default_avatar.url,
server_name=server_display_name,
banner_background=banner_background,
custom_message=welcome_channel_message
)
# Attendre un peu que le Cog d'invitations traite le join
await asyncio.sleep(1)
# Récupérer l'inviteur
invites_cog = self.bot.get_cog("Invites")
inviter_text = ""
if invites_cog:
try:
inviter_id = invites_cog.get_inviter_info(member.guild.id, member.id)
if inviter_id:
inviter = member.guild.get_member(inviter_id)
if not inviter:
try:
inviter = await self.bot.fetch_user(inviter_id)
except (disnake.NotFound, disnake.HTTPException) as e:
kuby_logger.warning(f"[Welcome] Impossible de récupérer l'inviteur {inviter_id}: {e}")
inviter = None
if inviter:
total, current = invites_cog.get_invite_stats(member.guild.id, inviter_id)
inviter_text = f"\n\n**Invité par :** {inviter.mention} ({current} invitations)"
else:
inviter_text = "\n\n**Invité par :** [Utilisateur introuvable]"
except Exception as e:
kuby_logger.error(f"[Welcome] Erreur lors de la récupération de l'inviteur: {e}", exc_info=True)
inviter_text = ""
# Envoyer le message avec le format exact requis
if img:
server_display_name = server_name if server_name else member.guild.name
# IMPORTANT: transmettre aussi le nom réel du serveur à Pillow pour les placeholders {server}
# (utile quand welcome_server_name n'est pas configuré)
# (server_name est utilisé par create_welcome_image)
embed = disnake.Embed(
description=f"Bienvenue sur {server_display_name} {member.name} ! Grâce à toi, nous avons atteint les {member.guild.member_count} membres !{inviter_text}",
color=0x9B59B6
)
embed.set_image(url="attachment://welcome.png")
await channel.send(embed=embed, file=disnake.File(img, filename="welcome.png"))
kuby_logger.info(f"[Welcome] Message de bienvenue avec image envoyé pour {member.name}")
else:
# Fallback : image non générée, envoyer un message texte
kuby_logger.warning(f"[Welcome] Image non générée pour {member.name}, envoi d'un message texte")
server_display_name = server_name if server_name else member.guild.name
await channel.send(f"Bienvenue sur {server_display_name} {member.mention} ! Grâce à toi, nous avons atteint les {member.guild.member_count} membres !{inviter_text}")
except Exception as e:
kuby_logger.error(f"[Welcome] Erreur lors de l'envoi du message dans le salon: {e}", exc_info=True)
# Fallback avec un message minimal
try:
await channel.send(f"{member.mention} Bienvenue dans le serveur !")
kuby_logger.info(f"[Welcome] Message de fallback envoyé pour {member.name}")
except Exception as e2:
kuby_logger.error(f"[Welcome] Échec du message de fallback: {e2}", exc_info=True)
else:
kuby_logger.warning(f"[Welcome] Salon de bienvenue introuvable (ID: {welcome_channel}) pour {member.guild.name}")
else:
kuby_logger.debug(f"[Welcome] Aucun salon de bienvenue configuré pour {member.guild.name}")
async def save_banner_attachment(self, attachment: disnake.Attachment, guild_id: int) -> str:
"""Sauvegarde l'attachment et retourne le chemin du fichier"""
base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "banners"))
os.makedirs(base_dir, exist_ok=True)
# Vérifier que c'est une image
if not attachment.content_type or not attachment.content_type.startswith('image/'):
raise ValueError("Le fichier doit être une image.")
# Vérifier le format PNG
if not attachment.filename.lower().endswith('.png'):
raise ValueError("Le fichier doit être au format PNG.")
# Vérifier les dimensions
banner_path = os.path.join(base_dir, f"banner_{guild_id}.png")
async with aiohttp.ClientSession() as session:
async with session.get(attachment.url) as resp:
if resp.status == 200:
# Vérifier les dimensions avant de sauvegarder
img_data = await resp.read()
from io import BytesIO
with Image.open(BytesIO(img_data)) as img:
width, height = img.size
if width != 1000 or height != 300:
raise ValueError(f"Les dimensions doivent être 1000x300 pixels (votre image: {width}x{height})")
with open(banner_path, 'wb') as f:
f.write(img_data)
return os.path.basename(banner_path)
@commands.slash_command(name="setwelcome", description="Définit le salon, les messages de bienvenue et le fond de la bannière")
@commands.has_permissions(administrator=True)
async def set_welcome(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, message_dm: str, message_salon: str, server_name: str, banner_file: disnake.Attachment = None):
# ✅ DÉFÉRER LA RÉPONSE (le traitement peut prendre > 3s avec les images)
await interaction.response.defer(ephemeral=True)
banner_path = None
error_message = None
# Si un fichier est uploadé, le sauvegarder
if banner_file:
try:
banner_path = await self.save_banner_attachment(banner_file, interaction.guild_id)
except Exception as e:
error_message = f"❌ Erreur avec le fichier: {e}"
# Si pas d'erreur, sauvegarder en base de données
if not error_message:
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''INSERT INTO server_configs (server_id, welcome_channel, welcome_dm_message, welcome_server_name, welcome_banner_background, welcome_channel_message)
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(server_id)
DO UPDATE SET welcome_channel=excluded.welcome_channel,
welcome_dm_message=excluded.welcome_dm_message,
welcome_server_name=excluded.welcome_server_name,
welcome_banner_background=COALESCE(excluded.welcome_banner_background, welcome_banner_background),
welcome_channel_message=excluded.welcome_channel_message''',
(interaction.guild_id, channel.id, message_dm, server_name, banner_path, message_salon))
conn.commit()
conn.close()
bg_used = "background_template.png" if not banner_path else (banner_path.split('/')[-1])
error_message = f"✅ Configuration de bienvenue enregistrée !\nFond utilisé: {bg_used}"
except Exception as e:
error_message = f"❌ Erreur lors de la sauvegarde en base de données: {e}"
# Envoyer la réponse finale (déferrée)
await interaction.followup.send(error_message, ephemeral=True)
@commands.slash_command(name="invitations", description="Affiche vos statistiques d'invitations")
async def invitations_stats(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.Member = None):
user = user or interaction.author
await self._send_invitation_stats(interaction, user)
@commands.user_command(name="Stats Invitations")
async def invitations_user(self, interaction: disnake.UserCommandInteraction, user: disnake.Member):
await self._send_invitation_stats(interaction, user)
async def _send_invitation_stats(self, interaction, user):
invites_cog = self.bot.get_cog("Invites")
if not invites_cog:
return await interaction.response.send_message("❌ Le module d'invitations est désactivé.", ephemeral=True)
total, current = invites_cog.get_invite_stats(interaction.guild.id, user.id)
components = [
disnake.ui.Container(
disnake.ui.Section(
f"Statistiques d'invitations - {user.display_name}",
accessory=disnake.ui.Thumbnail(user.display_avatar.url)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(f"➜ Total d'invitations : **{total}**"),
disnake.ui.TextDisplay(f"➜ Membres présents : **{current}**")
)
]
await interaction.response.send_message(components=components)
def setup(bot):
bot.add_cog(Welcome(bot))