Kuby/commandes/goodbye.py

332 lines
16 KiB
Python
Raw Permalink Normal View History

2026-05-06 17:07:09 +02:00
import disnake
from disnake.ext import commands
2026-01-01 18:48:25 +01:00
import sqlite3
import aiohttp
import io
import os
2026-05-25 19:08:45 +02:00
import logging
2026-01-01 18:48:25 +01:00
from PIL import Image, ImageDraw, ImageFont
from datetime import datetime, timezone
2026-07-06 16:08:14 +02:00
from utils.premium import check_premium_tier
2026-01-01 18:48:25 +01:00
2026-05-25 19:08:45 +02:00
kuby_logger = logging.getLogger("KubyBot")
2026-01-01 18:48:25 +01:00
class Goodbye(commands.Cog):
def __init__(self, bot):
self.bot = bot
2026-05-25 19:08:45 +02:00
# 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,
2026-05-01 16:17:55 +02:00
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')
2026-05-01 16:17:55 +02:00
if 'goodbye_banner_background' not in columns:
cursor.execute('ALTER TABLE server_configs ADD COLUMN goodbye_banner_background TEXT')
conn.commit()
conn.close()
2026-01-01 18:48:25 +01:00
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'}"
2026-05-01 16:17:55 +02:00
async def create_goodbye_image(self, username, avatar_url, server_name=None, banner_background=None):
2026-05-25 19:08:45 +02:00
# 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()
2026-01-01 18:48:25 +01:00
2026-05-25 19:08:45 +02:00
# 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}")
2026-06-18 17:40:55 +02:00
# 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))
2026-01-01 18:48:25 +01:00
2026-05-25 19:08:45 +02:00
# 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)
2026-01-01 18:48:25 +01:00
2026-05-25 19:08:45 +02:00
# 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
2026-01-01 18:48:25 +01:00
@commands.Cog.listener()
async def on_member_remove(self, member):
2026-05-25 19:08:45 +02:00
kuby_logger.info(f"[Goodbye] Départ détecté: {member.name} (ID: {member.id}) du serveur {member.guild.name}")
2026-06-03 15:52:43 +02:00
self._init_db()
2026-05-25 19:08:45 +02:00
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
2026-01-01 18:48:25 +01:00
goodbye_channel = result[0]
server_name = result[1]
custom_message = result[2]
2026-05-01 16:17:55 +02:00
banner_background = result[3]
channel = member.guild.get_channel(goodbye_channel)
2026-05-25 19:08:45 +02:00
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,
2026-05-01 16:17:55 +02:00
server_name=server_name,
banner_background=banner_background
)
2026-01-01 18:48:25 +01:00
2026-05-25 19:08:45 +02:00
# 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
2026-05-25 19:08:45 +02:00
if img:
embed = disnake.Embed(
2026-06-18 17:40:55 +02:00
description=f"{member.mention} vient de nous quitter.\n\n{message_text}",
color=0x9B59B6
2026-05-25 19:08:45 +02:00
)
2026-06-18 17:40:55 +02:00
embed.set_footer(text=f"avait rejoint · {joined_message}")
2026-05-25 19:08:45 +02:00
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(
2026-06-18 17:40:55 +02:00
description=f"{member.mention} vient de nous quitter.\n\n{message_text}",
color=0x9B59B6
)
2026-06-18 17:40:55 +02:00
embed.set_footer(text=f"avait rejoint · {joined_message}")
2026-05-25 19:08:45 +02:00
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)
2026-01-01 18:48:25 +01:00
2026-05-06 17:07:09 +02:00
async def save_banner_attachment(self, attachment: disnake.Attachment, guild_id: int) -> str:
2026-05-01 16:17:55 +02:00
"""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)
2026-05-25 19:08:45 +02:00
@commands.slash_command(name="setgoodbye", description="Définit le salon d'au revoir et les paramètres personnalisés")
2026-07-06 16:08:14 +02:00
@check_premium_tier()
2026-05-06 17:07:09 +02:00
@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):
2026-05-25 19:08:45 +02:00
# ✅ DÉFÉRER LA RÉPONSE (le traitement peut prendre > 3s avec les images)
await interaction.response.defer(ephemeral=True)
2026-05-01 16:17:55 +02:00
banner_path = None
2026-05-25 19:08:45 +02:00
error_message = None
2026-05-01 16:17:55 +02:00
# 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:
2026-05-25 19:08:45 +02:00
error_message = f"❌ Erreur avec le fichier: {e}"
2026-05-25 19:08:45 +02:00
# 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}"
2026-05-25 19:08:45 +02:00
# Envoyer la réponse finale (déferrée)
await interaction.followup.send(error_message, ephemeral=True)
2026-01-01 18:48:25 +01:00
def setup(bot):
bot.add_cog(Goodbye(bot))