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}") 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}\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))