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}") # 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") @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))