Fix système arriver départ V1
This commit is contained in:
parent
01dd4979d8
commit
d9afa32828
18 changed files with 1367 additions and 291 deletions
|
|
@ -13,8 +13,14 @@ from typing import Optional, Dict, Any
|
|||
PARIS_TZ = ZoneInfo("Europe/Paris")
|
||||
DATE_FORMAT = "%d/%m/%Y %H:%M"
|
||||
|
||||
ABSENCE_SETTINGS_FILE = "data/absence_settings.json"
|
||||
ABSENCE_DATA_FILE = "data/absences.json"
|
||||
# Chemins ABSOLUS
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
|
||||
ABSENCE_SETTINGS_FILE = os.path.join(DATA_DIR, "absence_settings.json")
|
||||
ABSENCE_DATA_FILE = os.path.join(DATA_DIR, "absences.json")
|
||||
|
||||
# Créer le dossier data/ s'il n'existe pas
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
file_lock = asyncio.Lock()
|
||||
|
||||
def load_settings(guild_id: int) -> dict:
|
||||
|
|
|
|||
|
|
@ -167,40 +167,17 @@ class BugReport(commands.Cog):
|
|||
self.bot = bot
|
||||
|
||||
async def cog_load(self):
|
||||
# Disnake appelle automatiquement cette méthode dès que la boucle d'événements est active.
|
||||
self.bot.loop.create_task(self._start_webhook_server())
|
||||
# ❌ DÉSACTIVÉ : Le webhook est géré par serveur_flask.py (port 5000)
|
||||
# Le serveur Flask proxyfie déjà les webhooks GitLab vers ce cog via /bot_event
|
||||
kuby_logger.info("BugReport cog chargé (webhook géré par serveur_flask.py)")
|
||||
# Créer le dossier pour REPORTS_FILE s'il n'existe pas
|
||||
REPORTS_DIR = os.path.dirname(REPORTS_FILE)
|
||||
os.makedirs(REPORTS_DIR, exist_ok=True)
|
||||
|
||||
async def _start_webhook_server(self):
|
||||
await self.bot.wait_until_ready()
|
||||
|
||||
self.web_app = web.Application()
|
||||
self.web_app.router.add_post('/bot_event', self.handle_bot_event)
|
||||
self.runner = web.AppRunner(self.web_app)
|
||||
await self.runner.setup()
|
||||
|
||||
max_retries = 10
|
||||
retry_delay = 5
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
self.site = web.TCPSite(self.runner, '127.0.0.1', 5001)
|
||||
await self.site.start()
|
||||
kuby_logger.info(
|
||||
f"Internal Bot Webhook Server started on 127.0.0.1:5001 (Attempt {attempt})"
|
||||
)
|
||||
return
|
||||
except OSError as e:
|
||||
if getattr(e, 'errno', None) == 98: # Address already in use
|
||||
if attempt < max_retries:
|
||||
kuby_logger.warning(
|
||||
f"Port 5001 already in use, retrying in {retry_delay}s... ({attempt}/{max_retries})"
|
||||
)
|
||||
await asyncio.sleep(retry_delay)
|
||||
else:
|
||||
kuby_logger.error(f"Failed to bind to port 5001 after {max_retries} attempts.")
|
||||
raise e
|
||||
else:
|
||||
raise e
|
||||
# ❌ FONCTION DÉSACTIVÉE (voir cog_load)
|
||||
kuby_logger.warning("Tentative d'appel à _start_webhook_server() - cette fonction est désactivée")
|
||||
return
|
||||
|
||||
def cog_unload(self):
|
||||
if hasattr(self, 'runner'):
|
||||
|
|
|
|||
|
|
@ -9,9 +9,14 @@ import logging
|
|||
|
||||
kuby_logger = logging.getLogger("KubyBot")
|
||||
|
||||
# --- CONFIGURATION ---
|
||||
CONV_SETTINGS_FILE = "data/convocation_settings.json"
|
||||
CONV_BACKUPS_FILE = "data/convocation_backups.json"
|
||||
# Chemins ABSOLUS
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
|
||||
CONV_SETTINGS_FILE = os.path.join(DATA_DIR, "convocation_settings.json")
|
||||
CONV_BACKUPS_FILE = os.path.join(DATA_DIR, "convocation_backups.json")
|
||||
|
||||
# Créer le dossier data/ s'il n'existe pas
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
def load_json(filepath: str) -> Dict:
|
||||
if not os.path.exists(filepath):
|
||||
|
|
|
|||
|
|
@ -4,13 +4,18 @@ 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
|
||||
self.db_path = os.path.join(os.path.dirname(__file__), "..", "config.db")
|
||||
# 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):
|
||||
|
|
@ -77,86 +82,139 @@ class Goodbye(commands.Cog):
|
|||
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):
|
||||
current_dir = os.path.dirname(__file__)
|
||||
base_dir = os.path.abspath(os.path.join(current_dir, ".."))
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(avatar_url) as resp:
|
||||
avatar_bytes = await resp.read()
|
||||
# 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()
|
||||
|
||||
# Utiliser le fond personnalisé ou celui par défaut
|
||||
bg_path = None
|
||||
if banner_background:
|
||||
# On cherche dans le dossier banners/
|
||||
potential_path = os.path.join(base_dir, "banners", banner_background)
|
||||
if os.path.exists(potential_path):
|
||||
bg_path = potential_path
|
||||
|
||||
if bg_path:
|
||||
# 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)
|
||||
else:
|
||||
background = Image.open(os.path.join(base_dir, 'background_template_goodbye.png'))
|
||||
draw = ImageDraw.Draw(background)
|
||||
|
||||
font_large = ImageFont.truetype(os.path.join(base_dir, 'custom_font_large.otf'), 70)
|
||||
font_small = ImageFont.truetype(os.path.join(base_dir, 'custom_font_small.otf'), 50)
|
||||
font_large_omega = ImageFont.truetype(os.path.join(base_dir, 'custom_font_large.otf'), 55)
|
||||
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))
|
||||
|
||||
draw.text((325, 70), "A Bientot", font=font_large, fill=(255,255,255))
|
||||
draw.text((325, 135), f"{username} chez", font=font_small, fill=(255,255,255))
|
||||
|
||||
# Afficher le nom du serveur personnalisé s'il existe, sinon OMEGA KUBE
|
||||
display_name = server_name if server_name else "OMEGA KUBE"
|
||||
draw.text((325, 195), display_name, font=font_large_omega, 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)
|
||||
|
||||
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)
|
||||
|
||||
img_byte_arr = io.BytesIO()
|
||||
background.save(img_byte_arr, format='PNG')
|
||||
img_byte_arr.seek(0)
|
||||
return img_byte_arr
|
||||
# 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):
|
||||
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()
|
||||
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
|
||||
|
||||
if result and result[0]:
|
||||
goodbye_channel = result[0]
|
||||
server_name = result[1]
|
||||
custom_message = result[2]
|
||||
banner_background = result[3]
|
||||
|
||||
channel = member.guild.get_channel(goodbye_channel)
|
||||
if 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
|
||||
)
|
||||
joined_message = self.humanize_time_delta(member.joined_at)
|
||||
|
||||
# Utiliser le message personnalisé ou un par défaut
|
||||
description = custom_message if custom_message else "Nous espérons vous revoir bientôt <a:rain:1317973615714504806>"
|
||||
# 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
|
||||
|
||||
components = [
|
||||
disnake.ui.Container(
|
||||
disnake.ui.TextDisplay(f"{member} viens de nous quitter."),
|
||||
disnake.ui.Separator(divider=True),
|
||||
disnake.ui.TextDisplay(description),
|
||||
disnake.ui.TextDisplay(f"⌛ **Avait rejoint :** {joined_message}"),
|
||||
disnake.ui.MediaGallery(disnake.MediaGalleryItem(url="attachment://goodbye.png"))
|
||||
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
|
||||
)
|
||||
]
|
||||
await channel.send(components=components, file=disnake.File(img, filename="goodbye.png"))
|
||||
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"""
|
||||
|
|
@ -190,42 +248,47 @@ class Goodbye(commands.Cog):
|
|||
|
||||
return os.path.basename(banner_path)
|
||||
|
||||
@commands.slash_command(name="setgoodbye", description="Définit le salon d'aurevoir et les paramètres personnalisés")
|
||||
@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:
|
||||
await interaction.response.send_message(f"❌ Erreur avec le fichier: {e}", ephemeral=True)
|
||||
return
|
||||
|
||||
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()
|
||||
error_message = f"❌ Erreur avec le fichier: {e}"
|
||||
|
||||
msg = f"✅ Salon d'aurevoir 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."
|
||||
# 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}"
|
||||
|
||||
components = [
|
||||
disnake.ui.Container(
|
||||
disnake.ui.TextDisplay(msg)
|
||||
)
|
||||
]
|
||||
await interaction.response.send_message(components=components, ephemeral=True)
|
||||
# Envoyer la réponse finale (déferrée)
|
||||
await interaction.followup.send(error_message, ephemeral=True)
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Goodbye(bot))
|
||||
|
|
@ -4,6 +4,7 @@ import sqlite3
|
|||
import os
|
||||
import logging
|
||||
from typing import Dict, Optional
|
||||
import asyncio
|
||||
|
||||
kuby_logger = logging.getLogger("KubyBot")
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ class Invites(commands.Cog):
|
|||
self.bot = bot
|
||||
self.db_path = os.path.join(os.path.dirname(__file__), "..", "config.db")
|
||||
self.invite_cache = {} # {guild_id: {code: uses}}
|
||||
self.cache_lock = asyncio.Lock() # Verrou pour éviter les problèmes de concurrence
|
||||
self._init_db()
|
||||
|
||||
def _init_db(self):
|
||||
|
|
@ -38,9 +40,12 @@ class Invites(commands.Cog):
|
|||
async def update_invite_cache(self, guild):
|
||||
try:
|
||||
invites = await guild.invites()
|
||||
self.invite_cache[guild.id] = {invite.code: invite.uses for invite in invites}
|
||||
async with self.cache_lock:
|
||||
self.invite_cache[guild.id] = {invite.code: invite.uses for invite in invites}
|
||||
except disnake.Forbidden:
|
||||
pass
|
||||
kuby_logger.warning(f"[Invites] Impossible de mettre à jour le cache des invitations pour {guild.name} (permissions manquantes)")
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Invites] Erreur lors de la mise à jour du cache des invitations pour {guild.name}: {e}")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self):
|
||||
|
|
@ -54,24 +59,30 @@ class Invites(commands.Cog):
|
|||
|
||||
@commands.Cog.listener()
|
||||
async def on_invite_create(self, invite):
|
||||
if invite.guild.id not in self.invite_cache:
|
||||
self.invite_cache[invite.guild.id] = {}
|
||||
self.invite_cache[invite.guild.id][invite.code] = invite.uses
|
||||
async with self.cache_lock:
|
||||
if invite.guild.id not in self.invite_cache:
|
||||
self.invite_cache[invite.guild.id] = {}
|
||||
self.invite_cache[invite.guild.id][invite.code] = invite.uses
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_invite_delete(self, invite):
|
||||
if invite.guild.id in self.invite_cache:
|
||||
self.invite_cache[invite.guild.id].pop(invite.code, None)
|
||||
async with self.cache_lock:
|
||||
if invite.guild.id in self.invite_cache:
|
||||
self.invite_cache[invite.guild.id].pop(invite.code, None)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_join(self, member):
|
||||
if member.bot: return
|
||||
|
||||
guild = member.guild
|
||||
old_invites = self.invite_cache.get(guild.id, {})
|
||||
try:
|
||||
old_invites = self.invite_cache.get(guild.id, {})
|
||||
new_invites = await guild.invites()
|
||||
except disnake.Forbidden:
|
||||
kuby_logger.warning(f"[Invites] Impossible de récupérer les invitations pour {guild.name} (permissions manquantes)")
|
||||
return
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Invites] Erreur lors de la récupération des invitations pour {guild.name}: {e}")
|
||||
return
|
||||
|
||||
used_invite = None
|
||||
|
|
@ -86,104 +97,159 @@ class Invites(commands.Cog):
|
|||
break
|
||||
|
||||
# Mise à jour du cache
|
||||
self.invite_cache[guild.id] = {invite.code: invite.uses for invite in new_invites}
|
||||
async with self.cache_lock:
|
||||
self.invite_cache[guild.id] = {invite.code: invite.uses for invite in new_invites}
|
||||
|
||||
if used_invite:
|
||||
inviter = used_invite.inviter
|
||||
if inviter:
|
||||
self.record_invite(guild.id, member.id, inviter.id, used_invite.code)
|
||||
kuby_logger.info(f"📥 [Invites] {member} invité par {inviter} ({used_invite.code})")
|
||||
try:
|
||||
self.record_invite(guild.id, member.id, inviter.id, used_invite.code)
|
||||
kuby_logger.info(f"📥 [Invites] {member} invité par {inviter} ({used_invite.code})")
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Invites] Erreur lors de l'enregistrement de l'invitation pour {member}: {e}")
|
||||
|
||||
# Modular join message
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT log_channel_id FROM invite_configs WHERE guild_id = ?', (guild.id,))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if result:
|
||||
log_channel = guild.get_channel(result[0])
|
||||
if log_channel:
|
||||
# Determine method
|
||||
method = "invitation standard"
|
||||
if guild.vanity_url_code and used_invite.code == guild.vanity_url_code:
|
||||
method = "lien personnalisé"
|
||||
elif used_invite.max_age == 0:
|
||||
method = "invitation permanente"
|
||||
elif used_invite.temporary:
|
||||
method = "invitation temporaire"
|
||||
elif used_invite.max_uses == 1:
|
||||
method = "invitation à usage unique"
|
||||
|
||||
components = [
|
||||
disnake.ui.Container(
|
||||
disnake.ui.Section(
|
||||
f"Nouveau membre via invitation",
|
||||
accessory=disnake.ui.Thumbnail(member.display_avatar.url)
|
||||
),
|
||||
disnake.ui.Separator(divider=True),
|
||||
disnake.ui.TextDisplay(f"👤 {member.mention} vient de nous rejoindre !"),
|
||||
disnake.ui.TextDisplay(f"🤝 Grâce à {inviter.mention}"),
|
||||
disnake.ui.TextDisplay(f"🔗 Via **{method}**")
|
||||
)
|
||||
]
|
||||
await log_channel.send(components=components)
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT log_channel_id FROM invite_configs WHERE guild_id = ?', (guild.id,))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if result:
|
||||
log_channel = guild.get_channel(result[0])
|
||||
if log_channel:
|
||||
# Determine method
|
||||
method = "invitation standard"
|
||||
if guild.vanity_url_code and used_invite.code == guild.vanity_url_code:
|
||||
method = "lien personnalisé"
|
||||
elif used_invite.max_age == 0:
|
||||
method = "invitation permanente"
|
||||
elif used_invite.temporary:
|
||||
method = "invitation temporaire"
|
||||
elif used_invite.max_uses == 1:
|
||||
method = "invitation à usage unique"
|
||||
|
||||
# Récupérer les statistiques de l'inviteur
|
||||
total_invites, current_invites = self.get_invite_stats(guild.id, inviter.id)
|
||||
|
||||
components = [
|
||||
disnake.ui.Container(
|
||||
disnake.ui.Section(
|
||||
f"Nouveau membre via invitation",
|
||||
accessory=disnake.ui.Thumbnail(member.display_avatar.url)
|
||||
),
|
||||
disnake.ui.Separator(divider=True),
|
||||
disnake.ui.TextDisplay(f"👤 {member.mention} vient de nous rejoindre !"),
|
||||
disnake.ui.TextDisplay(f"🤝 Grâce à {inviter.mention}"),
|
||||
disnake.ui.TextDisplay(f"📊 Invitations : {current_invites} membres présents / {total_invites} total"),
|
||||
disnake.ui.TextDisplay(f"🔗 Via **{method}**")
|
||||
)
|
||||
]
|
||||
await log_channel.send(components=components)
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Invites] Erreur lors de l'envoi du message de log pour {member}: {e}")
|
||||
|
||||
def record_invite(self, guild_id, member_id, inviter_id, code):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO member_invites (guild_id, member_id, inviter_id, invite_code)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(guild_id, member_id) DO UPDATE SET inviter_id=excluded.inviter_id, invite_code=excluded.invite_code
|
||||
''', (guild_id, member_id, inviter_id, code))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO member_invites (guild_id, member_id, inviter_id, invite_code)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(guild_id, member_id) DO UPDATE SET inviter_id=excluded.inviter_id, invite_code=excluded.invite_code
|
||||
''', (guild_id, member_id, inviter_id, code))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Invites] Erreur lors de l'enregistrement de l'invitation dans la base de données: {e}")
|
||||
|
||||
def get_inviter_info(self, guild_id, member_id):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT inviter_id FROM member_invites WHERE guild_id = ? AND member_id = ?', (guild_id, member_id))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
return result[0] if result else None
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT inviter_id FROM member_invites WHERE guild_id = ? AND member_id = ?', (guild_id, member_id))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
return result[0] if result else None
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Invites] Erreur lors de la récupération de l'inviteur pour le membre {member_id}: {e}")
|
||||
return None
|
||||
|
||||
def get_invite_stats(self, guild_id, user_id):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT member_id FROM member_invites WHERE guild_id = ? AND inviter_id = ?', (guild_id, user_id))
|
||||
invited_ids = [row[0] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT member_id FROM member_invites WHERE guild_id = ? AND inviter_id = ?', (guild_id, user_id))
|
||||
invited_ids = [row[0] for row in cursor.fetchall()]
|
||||
conn.close()
|
||||
|
||||
guild = self.bot.get_guild(guild_id)
|
||||
if not guild: return 0, 0
|
||||
guild = self.bot.get_guild(guild_id)
|
||||
if not guild: return 0, 0
|
||||
|
||||
current_present = 0
|
||||
for mid in invited_ids:
|
||||
if guild.get_member(mid):
|
||||
current_present += 1
|
||||
current_present = 0
|
||||
for mid in invited_ids:
|
||||
if guild.get_member(mid):
|
||||
current_present += 1
|
||||
else:
|
||||
kuby_logger.debug(f"[Invites] Membre invité {mid} non trouvé dans le serveur {guild_id}")
|
||||
|
||||
return len(invited_ids), current_present
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Invites] Erreur lors de la récupération des statistiques d'invitations pour {user_id}: {e}")
|
||||
return 0, 0
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_remove(self, member):
|
||||
if member.bot: return
|
||||
|
||||
return len(invited_ids), current_present
|
||||
guild = member.guild
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DELETE FROM member_invites WHERE guild_id = ? AND member_id = ?', (guild.id, member.id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
kuby_logger.info(f"🗑️ [Invites] Enregistrement de l'invitation supprimé pour {member.name} (ID: {member.id}) car il a quitté le serveur.")
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Invites] Erreur lors de la suppression de l'enregistrement de l'invitation pour {member.name}: {e}")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_ban(self, guild, user):
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DELETE FROM member_invites WHERE guild_id = ? AND member_id = ?', (guild.id, user.id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
kuby_logger.info(f"🗑️ [Invites] Enregistrement de l'invitation supprimé pour {user.name} (ID: {user.id}) car il a été banni.")
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Invites] Erreur lors de la suppression de l'enregistrement de l'invitation pour {user.name}: {e}")
|
||||
|
||||
@commands.slash_command(name="setinvitechannel", description="Définit le salon des logs d'invitations")
|
||||
@commands.has_permissions(administrator=True)
|
||||
async def set_invite_channel(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO invite_configs (guild_id, log_channel_id)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(guild_id) DO UPDATE SET log_channel_id=excluded.log_channel_id
|
||||
''', (interaction.guild_id, channel.id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
components = [
|
||||
disnake.ui.Container(
|
||||
disnake.ui.TextDisplay(f"✅ Salon des logs d'invitations défini sur {channel.mention}")
|
||||
)
|
||||
]
|
||||
await interaction.response.send_message(components=components, ephemeral=True)
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO invite_configs (guild_id, log_channel_id)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(guild_id) DO UPDATE SET log_channel_id=excluded.log_channel_id
|
||||
''', (interaction.guild_id, channel.id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
components = [
|
||||
disnake.ui.Container(
|
||||
disnake.ui.TextDisplay(f"✅ Salon des logs d'invitations défini sur {channel.mention}")
|
||||
)
|
||||
]
|
||||
await interaction.response.send_message(components=components, ephemeral=True)
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Invites] Erreur lors de la configuration du salon des logs d'invitations: {e}")
|
||||
await interaction.response.send_message("❌ Une erreur est survenue lors de la configuration du salon des logs d'invitations.", ephemeral=True)
|
||||
|
||||
@commands.command(name="invitations_debug")
|
||||
async def invites_prefix(self, ctx, user: disnake.Member = None):
|
||||
|
|
|
|||
|
|
@ -4,8 +4,14 @@ import json
|
|||
import os
|
||||
from datetime import datetime
|
||||
|
||||
SETTINGS_FILE = "data/security_settings.json"
|
||||
WHITELIST_FILE = "data/whitelist.json"
|
||||
# Chemins ABSOLUS
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "..", "..", "data")
|
||||
SETTINGS_FILE = os.path.join(DATA_DIR, "security_settings.json")
|
||||
WHITELIST_FILE = os.path.join(DATA_DIR, "whitelist.json")
|
||||
|
||||
# Créer le dossier data/ s'il n'existe pas
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
def is_whitelisted(guild_id, user_id):
|
||||
if not os.path.exists(WHITELIST_FILE): return False
|
||||
|
|
|
|||
|
|
@ -5,9 +5,14 @@ import os
|
|||
from collections import deque
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Chemins des fichiers
|
||||
SETTINGS_FILE = "data/security_settings.json"
|
||||
WHITELIST_FILE = "data/whitelist.json"
|
||||
# Chemins ABSOLUS
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "..", "..", "data")
|
||||
SETTINGS_FILE = os.path.join(DATA_DIR, "security_settings.json")
|
||||
WHITELIST_FILE = os.path.join(DATA_DIR, "whitelist.json")
|
||||
|
||||
# Créer le dossier data/ s'il n'existe pas
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
# Cache TTL en secondes (5 secondes pour avoir une réactivité acceptable)
|
||||
CACHE_TTL = 5
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@ import json
|
|||
import os
|
||||
from datetime import datetime
|
||||
|
||||
SETTINGS_FILE = "data/security_settings.json"
|
||||
# Chemins ABSOLUS
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "..", "..", "data")
|
||||
SETTINGS_FILE = os.path.join(DATA_DIR, "security_settings.json")
|
||||
|
||||
# Créer le dossier data/ s'il n'existe pas
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
def get_log_channel(guild, settings):
|
||||
channel_id = settings.get("log_channel_id")
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@ import json
|
|||
import os
|
||||
from typing import Optional
|
||||
|
||||
# Chemins des fichiers
|
||||
SETTINGS_FILE = "data/security_settings.json"
|
||||
# Chemins ABSOLUS
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "..", "..", "data")
|
||||
SETTINGS_FILE = os.path.join(DATA_DIR, "security_settings.json")
|
||||
|
||||
# Créer le dossier data/ s'il n'existe pas
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
def load_settings(guild_id: int) -> dict:
|
||||
"""Charge les paramètres de sécurité pour un serveur"""
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
# utils/no_link.py
|
||||
# commandes/no_link.py
|
||||
import re
|
||||
import disnake
|
||||
from disnake.ext import commands
|
||||
import json
|
||||
import os
|
||||
|
||||
# Fichier JSON de configuration security
|
||||
SECURITY_FILE = "security.json"
|
||||
# Chemins ABSOLUS
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
|
||||
SECURITY_FILE = os.path.join(DATA_DIR, "security.json")
|
||||
|
||||
# Créer le dossier data/ s'il n'existe pas
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
def load_guild_security(guild_id: int) -> dict:
|
||||
"""Retourne la configuration d'un serveur."""
|
||||
|
|
|
|||
|
|
@ -4,9 +4,14 @@ import json
|
|||
import os
|
||||
from typing import Optional
|
||||
|
||||
# Chemins des fichiers
|
||||
SETTINGS_FILE = "data/security_settings.json"
|
||||
WHITELIST_FILE = "data/whitelist.json"
|
||||
# Chemins ABSOLUS des fichiers (basés sur le fichier courant)
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
|
||||
SETTINGS_FILE = os.path.join(DATA_DIR, "security_settings.json")
|
||||
WHITELIST_FILE = os.path.join(DATA_DIR, "whitelist.json")
|
||||
|
||||
# Créer le dossier data/ s'il n'existe pas
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
# --- UTILITAIRES ---
|
||||
def load_settings(guild_id: int) -> dict:
|
||||
|
|
@ -31,7 +36,7 @@ def save_settings(guild_id: int, settings: dict) -> None:
|
|||
|
||||
data[str(guild_id)] = settings
|
||||
|
||||
os.makedirs(os.path.dirname(SETTINGS_FILE), exist_ok=True)
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
with open(SETTINGS_FILE, "w", encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,13 @@ def log_channel_rename(channel_id: int):
|
|||
timestamps.append(now)
|
||||
channel_rename_timestamps[channel_id] = timestamps
|
||||
|
||||
TICKET_WHITELIST_SETTINGS_FILE = "data/ticket_whitelist_settings.json"
|
||||
# Chemins ABSOLUS
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
|
||||
TICKET_WHITELIST_SETTINGS_FILE = os.path.join(DATA_DIR, "ticket_whitelist_settings.json")
|
||||
|
||||
# Créer le dossier data/ s'il n'existe pas
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
def load_settings(guild_id: int) -> dict:
|
||||
"""Charge les paramètres de ticket pour un serveur"""
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ kuby_logger = logging.getLogger("KubyBot")
|
|||
class Welcome(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
# Base de données à la racine
|
||||
self.db_path = os.path.join(os.path.dirname(__file__), "..", "config.db")
|
||||
# Base de données à la racine - Chemin absolu
|
||||
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):
|
||||
|
|
@ -56,16 +57,18 @@ class Welcome(commands.Cog):
|
|||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
async def create_welcome_image(self, username, avatar_url, server_name=None, banner_background=None):
|
||||
current_dir = os.path.dirname(__file__)
|
||||
base_dir = os.path.abspath(os.path.join(current_dir, ".."))
|
||||
|
||||
async def create_welcome_image(self, username, avatar_url, server_name=None, banner_background=None, custom_message=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"[Welcome] 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()
|
||||
|
||||
# Utiliser le fond personnalisé ou celui par défaut
|
||||
# Vérifier et utiliser le fond personnalisé ou celui par défaut
|
||||
bg_path = None
|
||||
if banner_background:
|
||||
# Si c'est un chemin absolu (pour compatibilité temporaire)
|
||||
|
|
@ -73,40 +76,97 @@ class Welcome(commands.Cog):
|
|||
bg_path = banner_background
|
||||
else:
|
||||
# Sinon on cherche dans le dossier banners/
|
||||
potential_path = os.path.join(base_dir, "banners", 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"[Welcome] Fond personnalisé trouvé: {bg_path}")
|
||||
|
||||
if bg_path:
|
||||
background = Image.open(bg_path)
|
||||
else:
|
||||
background = Image.open(os.path.join(base_dir, 'background_template.png'))
|
||||
if not bg_path:
|
||||
default_bg = os.path.join(self.base_dir, 'background_template.png')
|
||||
if not os.path.exists(default_bg):
|
||||
kuby_logger.error(f"[Welcome] 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"[Welcome] Utilisation du fond par défaut: {bg_path}")
|
||||
|
||||
# Charger le fond
|
||||
background = Image.open(bg_path)
|
||||
draw = ImageDraw.Draw(background)
|
||||
|
||||
font_large = ImageFont.truetype(os.path.join(base_dir, 'custom_font_large.otf'), 70)
|
||||
font_small = ImageFont.truetype(os.path.join(base_dir, 'custom_font_small.otf'), 50)
|
||||
# Charger la police avec support des accents
|
||||
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-Bold.ttf"
|
||||
if not os.path.exists(font_path):
|
||||
kuby_logger.warning(f"[Welcome] Police DejaVu introuvable, fallback sur police custom")
|
||||
font_path = os.path.join(self.base_dir, 'custom_font_large.otf')
|
||||
|
||||
font = ImageFont.truetype(font_path, 55)
|
||||
|
||||
# Construire le message à afficher sur la bannière
|
||||
display_message = custom_message if custom_message else "Bienvenue {membre} chez {server}"
|
||||
|
||||
# Remplacer les placeholders
|
||||
try:
|
||||
display_message = display_message.format(
|
||||
membre=username,
|
||||
member=username,
|
||||
server=server_name or "",
|
||||
serveur=server_name or "",
|
||||
username=username,
|
||||
servername=server_name or "",
|
||||
)
|
||||
except Exception:
|
||||
pass # Si placeholders inconnus, garder tel quel
|
||||
|
||||
# Zone de texte disponible (à droite de l'avatar)
|
||||
text_area_left = 280
|
||||
text_area_right = background.width - 20
|
||||
text_area_width = text_area_right - text_area_left
|
||||
|
||||
# Découper le texte en lignes qui tiennent dans la zone
|
||||
words = display_message.split()
|
||||
lines = []
|
||||
current_line = ""
|
||||
for word in words:
|
||||
test_line = f"{current_line} {word}".strip()
|
||||
bbox = draw.textbbox((0, 0), test_line, font=font)
|
||||
if bbox[2] - bbox[0] <= text_area_width:
|
||||
current_line = test_line
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
current_line = word
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
wrapped_text = "\n".join(lines)
|
||||
|
||||
# Mesurer le bloc de texte complet
|
||||
bbox = draw.textbbox((0, 0), wrapped_text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
# Centrer le bloc dans la zone disponible
|
||||
text_x = text_area_left + (text_area_width - text_width) // 2
|
||||
text_y = (background.height - text_height) // 2
|
||||
|
||||
draw.text((text_x, text_y), wrapped_text, font=font, fill=(255, 255, 255), align="center")
|
||||
|
||||
draw.text((325, 70), "Bienvenue", font=font_large, fill=(255,255,255))
|
||||
draw.text((325, 135), f"{username}", font=font_small, fill=(255,255,255))
|
||||
|
||||
# Afficher le nom du serveur uniquement si configuré
|
||||
if server_name:
|
||||
font_server = ImageFont.truetype(os.path.join(base_dir, 'custom_font_large.otf'), 55)
|
||||
draw.text((325, 195), server_name, font=font_server, 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:
|
||||
logging.error(f"Erreur image: {e}")
|
||||
kuby_logger.error(f"[Welcome] Erreur lors de la création de l'image: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
@commands.Cog.listener()
|
||||
|
|
@ -121,7 +181,7 @@ class Welcome(commands.Cog):
|
|||
conn.close()
|
||||
|
||||
if not result:
|
||||
kuby_logger.warning(f"⚠️ [Welcome] Aucune configuration trouvée dans la base de données pour le serveur {member.guild.id}")
|
||||
kuby_logger.debug(f"⚠️ [Welcome] Aucune configuration trouvée dans la base de données pour le serveur {member.guild.id}")
|
||||
return
|
||||
|
||||
welcome_channel = result[0]
|
||||
|
|
@ -170,40 +230,76 @@ class Welcome(commands.Cog):
|
|||
if welcome_channel:
|
||||
channel = member.guild.get_channel(welcome_channel)
|
||||
if channel:
|
||||
img = await self.create_welcome_image(
|
||||
member.name,
|
||||
member.avatar.url if member.avatar else member.default_avatar.url,
|
||||
server_name=server_name,
|
||||
banner_background=banner_background
|
||||
)
|
||||
if img:
|
||||
try:
|
||||
# Créer l'image de bienvenue
|
||||
server_display_name = server_name if server_name else member.guild.name
|
||||
img = await self.create_welcome_image(
|
||||
member.name,
|
||||
member.avatar.url if member.avatar else member.default_avatar.url,
|
||||
server_name=server_display_name,
|
||||
banner_background=banner_background,
|
||||
custom_message=welcome_channel_message
|
||||
)
|
||||
|
||||
# Attendre un peu que le Cog d'invitations traite le join
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Récupérer l'inviteur
|
||||
invites_cog = self.bot.get_cog("Invites")
|
||||
inviter_text = ""
|
||||
if invites_cog:
|
||||
inviter_id = invites_cog.get_inviter_info(member.guild.id, member.id)
|
||||
if inviter_id:
|
||||
inviter = member.guild.get_member(inviter_id) or await self.bot.fetch_user(inviter_id)
|
||||
total, current = invites_cog.get_invite_stats(member.guild.id, inviter_id)
|
||||
inviter_text = f"\n\n**Invité par :** {inviter.mention} ({current} invitations)"
|
||||
try:
|
||||
inviter_id = invites_cog.get_inviter_info(member.guild.id, member.id)
|
||||
if inviter_id:
|
||||
inviter = member.guild.get_member(inviter_id)
|
||||
if not inviter:
|
||||
try:
|
||||
inviter = await self.bot.fetch_user(inviter_id)
|
||||
except (disnake.NotFound, disnake.HTTPException) as e:
|
||||
kuby_logger.warning(f"[Welcome] Impossible de récupérer l'inviteur {inviter_id}: {e}")
|
||||
inviter = None
|
||||
|
||||
if inviter:
|
||||
total, current = invites_cog.get_invite_stats(member.guild.id, inviter_id)
|
||||
inviter_text = f"\n\n**Invité par :** {inviter.mention} ({current} invitations)"
|
||||
else:
|
||||
inviter_text = "\n\n**Invité par :** [Utilisateur introuvable]"
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Welcome] Erreur lors de la récupération de l'inviteur: {e}", exc_info=True)
|
||||
inviter_text = ""
|
||||
|
||||
# Utiliser le message personnalisé ou un message par défaut
|
||||
channel_description = (welcome_channel_message.format(
|
||||
username=member.name,
|
||||
servername=member.guild.name,
|
||||
member_count=member.guild.member_count
|
||||
) if welcome_channel_message else "Bienvenue dans le serveur !\nSi vous cherchez des RolePlay de qualité,\nVous êtes au bon endroit. <a:sip:1316891821858619452>") + inviter_text
|
||||
|
||||
components = [
|
||||
disnake.ui.Container(
|
||||
disnake.ui.TextDisplay(f"Merci d'accueillir {member} 🤝"),
|
||||
disnake.ui.Separator(divider=True),
|
||||
disnake.ui.TextDisplay(channel_description),
|
||||
disnake.ui.MediaGallery(disnake.MediaGalleryItem(url="attachment://welcome.png"))
|
||||
# Envoyer le message avec le format exact requis
|
||||
if img:
|
||||
server_display_name = server_name if server_name else member.guild.name
|
||||
# IMPORTANT: transmettre aussi le nom réel du serveur à Pillow pour les placeholders {server}
|
||||
# (utile quand welcome_server_name n'est pas configuré)
|
||||
# (server_name est utilisé par create_welcome_image)
|
||||
embed = disnake.Embed(
|
||||
description=f"Bienvenue sur {server_display_name} {member.name} ! Grâce à toi, nous avons atteint les {member.guild.member_count} membres !{inviter_text}",
|
||||
color=0x9B59B6
|
||||
)
|
||||
]
|
||||
await channel.send(content=member.mention, components=components, file=disnake.File(img, filename="welcome.png"))
|
||||
|
||||
embed.set_image(url="attachment://welcome.png")
|
||||
await channel.send(embed=embed, file=disnake.File(img, filename="welcome.png"))
|
||||
kuby_logger.info(f"[Welcome] Message de bienvenue avec image envoyé pour {member.name}")
|
||||
else:
|
||||
# Fallback : image non générée, envoyer un message texte
|
||||
kuby_logger.warning(f"[Welcome] Image non générée pour {member.name}, envoi d'un message texte")
|
||||
server_display_name = server_name if server_name else member.guild.name
|
||||
await channel.send(f"Bienvenue sur {server_display_name} {member.mention} ! Grâce à toi, nous avons atteint les {member.guild.member_count} membres !{inviter_text}")
|
||||
|
||||
except Exception as e:
|
||||
kuby_logger.error(f"[Welcome] Erreur lors de l'envoi du message dans le salon: {e}", exc_info=True)
|
||||
# Fallback avec un message minimal
|
||||
try:
|
||||
await channel.send(f"{member.mention} Bienvenue dans le serveur !")
|
||||
kuby_logger.info(f"[Welcome] Message de fallback envoyé pour {member.name}")
|
||||
except Exception as e2:
|
||||
kuby_logger.error(f"[Welcome] Échec du message de fallback: {e2}", exc_info=True)
|
||||
else:
|
||||
kuby_logger.warning(f"[Welcome] Salon de bienvenue introuvable (ID: {welcome_channel}) pour {member.guild.name}")
|
||||
else:
|
||||
kuby_logger.debug(f"[Welcome] Aucun salon de bienvenue configuré pour {member.guild.name}")
|
||||
|
||||
async def save_banner_attachment(self, attachment: disnake.Attachment, guild_id: int) -> str:
|
||||
"""Sauvegarde l'attachment et retourne le chemin du fichier"""
|
||||
|
|
@ -240,31 +336,42 @@ class Welcome(commands.Cog):
|
|||
@commands.slash_command(name="setwelcome", description="Définit le salon, les messages de bienvenue et le fond de la bannière")
|
||||
@commands.has_permissions(administrator=True)
|
||||
async def set_welcome(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, message_dm: str, message_salon: str, server_name: str, 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:
|
||||
await interaction.response.send_message(f"❌ Erreur avec le fichier: {e}", ephemeral=True)
|
||||
return
|
||||
error_message = f"❌ Erreur avec le fichier: {e}"
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''INSERT INTO server_configs (server_id, welcome_channel, welcome_dm_message, welcome_server_name, welcome_banner_background, welcome_channel_message)
|
||||
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(server_id)
|
||||
DO UPDATE SET welcome_channel=excluded.welcome_channel,
|
||||
welcome_dm_message=excluded.welcome_dm_message,
|
||||
welcome_server_name=excluded.welcome_server_name,
|
||||
welcome_banner_background=COALESCE(excluded.welcome_banner_background, welcome_banner_background),
|
||||
welcome_channel_message=excluded.welcome_channel_message''',
|
||||
(interaction.guild_id, channel.id, message_dm, server_name, banner_path, message_salon))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# 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, welcome_channel, welcome_dm_message, welcome_server_name, welcome_banner_background, welcome_channel_message)
|
||||
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(server_id)
|
||||
DO UPDATE SET welcome_channel=excluded.welcome_channel,
|
||||
welcome_dm_message=excluded.welcome_dm_message,
|
||||
welcome_server_name=excluded.welcome_server_name,
|
||||
welcome_banner_background=COALESCE(excluded.welcome_banner_background, welcome_banner_background),
|
||||
welcome_channel_message=excluded.welcome_channel_message''',
|
||||
(interaction.guild_id, channel.id, message_dm, server_name, banner_path, message_salon))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
bg_used = "background_template.png" if not banner_path else (banner_path.split('/')[-1])
|
||||
error_message = f"✅ Configuration de bienvenue enregistrée !\nFond utilisé: {bg_used}"
|
||||
except Exception as e:
|
||||
error_message = f"❌ Erreur lors de la sauvegarde en base de données: {e}"
|
||||
|
||||
bg_used = "background_template.png" if not banner_path else (banner_path.split('/')[-1])
|
||||
await interaction.response.send_message(f"✅ Configuration de bienvenida enregistrée !\nFond utilisé: {bg_used}", ephemeral=True)
|
||||
# Envoyer la réponse finale (déferrée)
|
||||
await interaction.followup.send(error_message, ephemeral=True)
|
||||
|
||||
@commands.slash_command(name="invitations", description="Affiche vos statistiques d'invitations")
|
||||
async def invitations_stats(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.Member = None):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@ import json
|
|||
import os
|
||||
from typing import List
|
||||
|
||||
WHITELIST_FILE = "data/whitelist.json"
|
||||
# Chemins ABSOLUS des fichiers
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
|
||||
WHITELIST_FILE = os.path.join(DATA_DIR, "whitelist.json")
|
||||
|
||||
# Créer le dossier data/ s'il n'existe pas
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
class Whitelist(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue