Kuby/commandes/goodbye.py

295 lines
No EOL
14 KiB
Python

import disnake
from disnake.ext import commands
import sqlite3
import aiohttp
import io
import os
import logging
from PIL import Image, ImageDraw, ImageFont
from datetime import datetime, timezone
kuby_logger = logging.getLogger("KubyBot")
class Goodbye(commands.Cog):
def __init__(self, bot):
self.bot = bot
# Chemin absolu vers la base de données
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()
# On s'assure que la table existe au cas où (normalement gérée par welcome.py)
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,
goodbye_server_name TEXT,
goodbye_message TEXT,
goodbye_banner_background 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 'goodbye_server_name' not in columns:
cursor.execute('ALTER TABLE server_configs ADD COLUMN goodbye_server_name TEXT')
if 'goodbye_message' not in columns:
cursor.execute('ALTER TABLE server_configs ADD COLUMN goodbye_message TEXT')
if 'goodbye_banner_background' not in columns:
cursor.execute('ALTER TABLE server_configs ADD COLUMN goodbye_banner_background TEXT')
conn.commit()
conn.close()
def humanize_time_delta(self, date):
if date is None: return "date inconnue"
if date.tzinfo is None: date = date.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
delta = now - date
total_days = delta.total_seconds() / (3600 * 24)
rounded_days = round(total_days)
years = rounded_days // 365
months = rounded_days // 30
weeks = rounded_days // 7
days = rounded_days
if years > 0: return f"il y a {years} {'an' if years == 1 else 'ans'}"
elif months > 0: return f"il y a {months} mois"
elif weeks > 0: return f"il y a {weeks} {'semaine' if weeks == 1 else 'semaines'}"
elif days > 0: return f"il y a {days} {'jour' if days == 1 else 'jours'}"
elif delta.seconds // 3600 > 0:
hours = delta.seconds // 3600
return f"il y a {hours} {'heure' if hours == 1 else 'heures'}"
elif (delta.seconds % 3600) // 60 > 0:
minutes = (delta.seconds % 3600) // 60
return f"il y a {minutes} {'minute' if minutes == 1 else 'minutes'}"
else:
seconds = delta.seconds % 60
return f"il y a {seconds} {'seconde' if seconds <= 1 else 'secondes'}"
async def create_goodbye_image(self, username, avatar_url, server_name=None, banner_background=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"[Goodbye] 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:
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"[Goodbye] Fond personnalisé trouvé: {bg_path}")
if not bg_path:
default_bg = os.path.join(self.base_dir, 'background_template_goodbye.png')
if not os.path.exists(default_bg):
kuby_logger.error(f"[Goodbye] 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"[Goodbye] Utilisation du fond par défaut: {bg_path}")
# Charger le fond
background = Image.open(bg_path)
draw = ImageDraw.Draw(background)
# Vérifier et charger les polices
font_large_path = os.path.join(self.base_dir, 'custom_font_large.otf')
font_small_path = os.path.join(self.base_dir, 'custom_font_small.otf')
for font_path in [font_large_path, font_small_path]:
if not os.path.exists(font_path):
kuby_logger.error(f"[Goodbye] Police introuvable: {font_path}")
raise FileNotFoundError(f"Police introuvable: {font_path}")
font_large = ImageFont.truetype(font_large_path, 70)
font_small = ImageFont.truetype(font_small_path, 50)
# Dessiner uniquement le nom du serveur (comme pour welcome)
display_name = server_name if server_name else "OMEGA KUBE"
draw.text((325, 135), display_name, font=font_large, fill=(255,255,255))
# 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"[Goodbye] Erreur lors de la création de l'image: {e}", exc_info=True)
return None
@commands.Cog.listener()
async def on_member_remove(self, member):
kuby_logger.info(f"[Goodbye] Départ détecté: {member.name} (ID: {member.id}) du serveur {member.guild.name}")
self._init_db()
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT goodbye_channel, goodbye_server_name, goodbye_message, goodbye_banner_background FROM server_configs WHERE server_id = ?', (member.guild.id,))
result = cursor.fetchone()
conn.close()
if not result or not result[0]:
kuby_logger.debug(f"[Goodbye] Aucune configuration de départ pour le serveur {member.guild.id}")
return
goodbye_channel = result[0]
server_name = result[1]
custom_message = result[2]
banner_background = result[3]
channel = member.guild.get_channel(goodbye_channel)
if not channel:
kuby_logger.warning(f"[Goodbye] Salon de départ introuvable (ID: {goodbye_channel}) pour {member.guild.name}")
return
joined_message = self.humanize_time_delta(member.joined_at)
try:
img = await self.create_goodbye_image(
member.name,
member.avatar.url if member.avatar else member.default_avatar.url,
server_name=server_name,
banner_background=banner_background
)
# Envoyer avec image si disponible
display_name = server_name if server_name else member.guild.name
default_message = "Nous espérons vous revoir bientôt <a:rain:1317973615714504806>"
message_text = custom_message if custom_message else default_message
if img:
embed = disnake.Embed(
description=f"{member.mention} vient de nous quitter.\n\n{message_text}\n\n⌛ **Avait rejoint :** {joined_message}",
color=0x7289DA
)
embed.set_image(url="attachment://goodbye.png")
await channel.send(embed=embed, file=disnake.File(img, filename="goodbye.png"))
kuby_logger.info(f"[Goodbye] Message de départ avec image envoyé pour {member.name}")
else:
# Fallback : image non générée
kuby_logger.warning(f"[Goodbye] Image non générée pour {member.name}, envoi d'un message texte")
embed = disnake.Embed(
description=f"{member.mention} vient de nous quitter.\n\n{message_text}\n\n⌛ **Avait rejoint :** {joined_message}",
color=0x7289DA
)
await channel.send(embed=embed)
except Exception as e:
kuby_logger.error(f"[Goodbye] Erreur lors de la création ou de l'envoi du message: {e}", exc_info=True)
# Fallback avec un message minimal
try:
message_text_fb = custom_message if custom_message else "Nous espérons vous revoir bientôt"
await channel.send(f"{member.mention} vient de nous quitter. {message_text_fb}")
kuby_logger.info(f"[Goodbye] Message de fallback envoyé pour {member.name}")
except Exception as e2:
kuby_logger.error(f"[Goodbye] Échec du message de fallback: {e2}", exc_info=True)
except sqlite3.Error as e:
kuby_logger.error(f"[Goodbye] Erreur SQLite: {e}", exc_info=True)
except Exception as e:
kuby_logger.error(f"[Goodbye] Erreur inattendue dans on_member_remove: {e}", exc_info=True)
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_goodbye_{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="setgoodbye", description="Définit le salon d'au revoir et les paramètres personnalisés")
@commands.has_permissions(administrator=True)
async def set_goodbye(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, server_name: str = None, message: str = None, 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, goodbye_channel, goodbye_server_name, goodbye_message, goodbye_banner_background)
VALUES (?, ?, ?, ?, ?) ON CONFLICT(server_id)
DO UPDATE SET goodbye_channel=excluded.goodbye_channel,
goodbye_server_name=COALESCE(excluded.goodbye_server_name, goodbye_server_name),
goodbye_message=COALESCE(excluded.goodbye_message, goodbye_message),
goodbye_banner_background=COALESCE(excluded.goodbye_banner_background, goodbye_banner_background)''',
(interaction.guild_id, channel.id, server_name, message, banner_path))
conn.commit()
conn.close()
msg = f"✅ Salon d'au revoir défini sur {channel.mention}"
if server_name: msg += f"\nNom du serveur: {server_name}"
if message: msg += f"\nMessage: {message}"
if banner_path: msg += f"\nBannière personnalisée activée."
error_message = msg
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)
def setup(bot):
bot.add_cog(Goodbye(bot))