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 from utils.premium import check_premium_tier 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}") # Adapter le texte pour éviter les débordements et afficher correctement le nom line1_text = "AU REVOIR" line2_text = username.upper() line3_text = (server_name or "OMEGA KUBE").upper() text_area_left = 300 text_area_right = background.width - 20 text_area_width = text_area_right - text_area_left text_area_center_x = text_area_left + text_area_width // 2 def fit_font(font_path, text, max_size, max_width): size = max_size while size > 10: font = ImageFont.truetype(font_path, size) bbox = draw.textbbox((0, 0), text, font=font) w = bbox[2] - bbox[0] if w <= max_width: h = bbox[3] - bbox[1] return font, w, h size -= 2 font = ImageFont.truetype(font_path, 10) bbox = draw.textbbox((0, 0), text, font=font) return font, bbox[2] - bbox[0], bbox[3] - bbox[1] font1, w1, h1 = fit_font(font_large_path, line1_text, 65, text_area_width) font2, w2, h2 = fit_font(font_small_path, line2_text, 38, text_area_width) font3, w3, h3 = fit_font(font_large_path, line3_text, 65, text_area_width) spacing_1_2 = 12 spacing_2_3 = 12 total_height = h1 + spacing_1_2 + h2 + spacing_2_3 + h3 start_y = (background.height - total_height) // 2 y = start_y draw.text((text_area_center_x - w1 // 2, y), line1_text, font=font1, fill=(255, 255, 255)) y += h1 + spacing_1_2 draw.text((text_area_center_x - w2 // 2, y), line2_text, font=font2, fill=(255, 255, 255)) y += h2 + spacing_2_3 draw.text((text_area_center_x - w3 // 2, y), line3_text, font=font3, 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 " 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}", color=0x9B59B6 ) embed.set_footer(text=f"avait rejoint · {joined_message}") 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}", color=0x9B59B6 ) embed.set_footer(text=f"avait rejoint · {joined_message}") 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") @check_premium_tier() @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))