import discord import re from discord.ext import commands from discord import app_commands, ui import json import os import asyncio from typing import Optional from src.logger import kuby_logger import time # Dictionnaire global pour suivre les renommages de salons # Format: {channel_id: [timestamp1, timestamp2, ...]} channel_rename_timestamps = {} def check_channel_rename_rate_limit(channel_id: int) -> int: """Vérifie si le renommage va atteindre la limite de Discord (2 edits / 10 min).""" now = time.time() if channel_id not in channel_rename_timestamps: channel_rename_timestamps[channel_id] = [] timestamps = [ts for ts in channel_rename_timestamps[channel_id] if now - ts < 600] channel_rename_timestamps[channel_id] = timestamps if len(timestamps) >= 2: return max(0, int(600 - (now - timestamps[0]))) return 0 def log_channel_rename(channel_id: int): """Enregistre un renommage de salon.""" now = time.time() if channel_id not in channel_rename_timestamps: channel_rename_timestamps[channel_id] = [] timestamps = [ts for ts in channel_rename_timestamps[channel_id] if now - ts < 600] timestamps.append(now) channel_rename_timestamps[channel_id] = timestamps TICKET_WHITELIST_SETTINGS_FILE = "data/ticket_whitelist_settings.json" def load_settings(guild_id: int) -> dict: """Charge les paramètres de ticket pour un serveur""" if not os.path.exists(TICKET_WHITELIST_SETTINGS_FILE): return {} try: with open(TICKET_WHITELIST_SETTINGS_FILE, "r", encoding='utf-8') as f: return json.load(f).get(str(guild_id), {}) except (json.JSONDecodeError, FileNotFoundError): return {} def save_settings(guild_id: int, settings: dict) -> None: """Sauvegarde les paramètres de ticket""" data = {} if os.path.exists(TICKET_WHITELIST_SETTINGS_FILE): try: with open(TICKET_WHITELIST_SETTINGS_FILE, "r", encoding='utf-8') as f: data = json.load(f) except (json.JSONDecodeError, FileNotFoundError): pass data[str(guild_id)] = settings os.makedirs(os.path.dirname(TICKET_WHITELIST_SETTINGS_FILE), exist_ok=True) with open(TICKET_WHITELIST_SETTINGS_FILE, "w", encoding='utf-8') as f: json.dump(data, f, indent=4, ensure_ascii=False) async def get_ticket_user_id(channel: discord.TextChannel) -> Optional[int]: """Récupère l'ID du propriétaire du ticket de manière robuste (gère le lag de cache)""" import re topic = getattr(channel, "topic", None) # Tentative d'utilisation du cache d.py (plus rapide) if topic: match = re.search(r"(\d{17,20})", topic) if match: return int(match.group(1)) # Si le topic est vide ou ID introuvable, on tente de forcer un refresh via l'API try: # On utilise fetch_channel pour avoir un objet tout neuf avec le topic à jour refreshed_channel = await channel.guild.fetch_channel(channel.id) topic = getattr(refreshed_channel, "topic", None) if topic: match = re.search(r"(\d{17,20})", topic) if match: return int(match.group(1)) except: pass return None class TicketWhitelistConfigView(ui.View): def __init__(self, guild_id: int): super().__init__(timeout=60) self.guild_id = guild_id async def prompt_for_id(self, interaction: discord.Interaction, prompt_text: str, is_category: bool = False): await interaction.response.send_message(prompt_text, ephemeral=True) def check(m): return m.author == interaction.user and m.channel == interaction.channel try: msg = await interaction.client.wait_for('message', check=check, timeout=30) target = None if is_category: if msg.content.isdigit(): target = discord.utils.get(interaction.guild.categories, id=int(msg.content)) if not target: await interaction.followup.send("❌ Catégorie introuvable avec cet ID.", ephemeral=True) return None else: await interaction.followup.send("❌ Veuillez envoyer l'ID (les chiffres) de la catégorie.", ephemeral=True) return None else: if msg.role_mentions: target = msg.role_mentions[0] elif msg.content.isdigit(): target = interaction.guild.get_role(int(msg.content)) if not target: await interaction.followup.send("❌ ID ou mention invalide.", ephemeral=True) return None try: await msg.delete() except: pass return target except __import__("asyncio").TimeoutError: await interaction.followup.send("❌ Temps écoulé.", ephemeral=True) return None @ui.button(label="Catégorie Tickets", style=discord.ButtonStyle.primary, row=0, emoji="📁") async def set_category(self, interaction: discord.Interaction, button: ui.Button): category = await self.prompt_for_id(interaction, "Envoie l'ID de la **catégorie** où créer les tickets :", is_category=True) if category: settings = load_settings(self.guild_id) settings["ticket_category_id"] = category.id save_settings(self.guild_id, settings) await interaction.followup.send(f"✅ Catégorie configurée sur **{category.name}**", ephemeral=True) @ui.button(label="Rôle Staff", style=discord.ButtonStyle.primary, row=0, emoji="🛡️") async def set_staff_role(self, interaction: discord.Interaction, button: ui.Button): role = await self.prompt_for_id(interaction, "Mentionne le **rôle Staff** ou envoie son ID :") if role: settings = load_settings(self.guild_id) settings["staff_role_id"] = role.id save_settings(self.guild_id, settings) await interaction.followup.send(f"✅ Rôle Staff configuré sur **{role.name}**", ephemeral=True) @ui.button(label="Rôle Équipe Staff", style=discord.ButtonStyle.primary, row=0, emoji="👥") async def set_team_staff_role(self, interaction: discord.Interaction, button: ui.Button): role = await self.prompt_for_id(interaction, "Mentionne le **rôle Équipe Staff** (à ping pour les nouveaux tickets) ou envoie son ID :") if role: settings = load_settings(self.guild_id) settings["team_staff_role_id"] = role.id save_settings(self.guild_id, settings) await interaction.followup.send(f"✅ Rôle Équipe Staff configuré sur **{role.name}**", ephemeral=True) @ui.button(label="Rôle Attente Oral", style=discord.ButtonStyle.secondary, row=1, emoji="🎤") async def set_attente_oral_role(self, interaction: discord.Interaction, button: ui.Button): role = await self.prompt_for_id(interaction, "Mentionne le rôle **Attente Oral** ou envoie son ID :") if role: settings = load_settings(self.guild_id) settings["attente_oral_role_id"] = role.id save_settings(self.guild_id, settings) await interaction.followup.send(f"✅ Rôle Attente Oral configuré sur **{role.name}**", ephemeral=True) @ui.button(label="Rôle Candidature à faire", style=discord.ButtonStyle.secondary, row=1, emoji="📝") async def set_candidature_a_faire_role(self, interaction: discord.Interaction, button: ui.Button): role = await self.prompt_for_id(interaction, "Mentionne le rôle **Candidature à faire** ou envoie son ID :") if role: settings = load_settings(self.guild_id) settings["candidature_a_faire_role_id"] = role.id save_settings(self.guild_id, settings) await interaction.followup.send(f"✅ Rôle Candidature à faire configuré sur **{role.name}**", ephemeral=True) @ui.button(label="Rôle Citoyen", style=discord.ButtonStyle.success, row=1, emoji="🏙️") async def set_citoyen_role(self, interaction: discord.Interaction, button: ui.Button): role = await self.prompt_for_id(interaction, "Mentionne le rôle **Citoyen** ou envoie son ID :") if role: settings = load_settings(self.guild_id) settings["citoyen_role_id"] = role.id save_settings(self.guild_id, settings) await interaction.followup.send(f"✅ Rôle Citoyen configuré sur **{role.name}**", ephemeral=True) class ContinueFormView(discord.ui.View): """Vue avec un bouton pour continuer à l'étape suivante""" def __init__(self, current_step, step1_data=None, step2_part1_data=None, step2_part2_data=None): super().__init__(timeout=180.0) # Définir un timeout pour la vue self.current_step = current_step self.step1_data = step1_data self.step2_part1_data = step2_part1_data self.step2_part2_data = step2_part2_data @discord.ui.button(label="📋 Continuer le formulaire", style=discord.ButtonStyle.green, custom_id="continue_form") async def continue_form(self, interaction: discord.Interaction, button: discord.ui.Button): """Passe à l'étape suivante du formulaire""" if self.current_step == 1: await interaction.response.send_modal(TicketStep2Part1(self.step1_data)) elif self.current_step == 2: await interaction.response.send_modal(TicketStep2Part2(self.step1_data, self.step2_part1_data)) elif self.current_step == 3: await interaction.response.send_modal(TicketStep3(self.step1_data, self.step2_part1_data, self.step2_part2_data)) # Vérifier si le message existe toujours avant de le modifier try: await interaction.message.edit(view=self) except discord.errors.NotFound: # Si le message n'existe plus, nous ne faisons rien pass class TicketStep1(discord.ui.Modal, title="📋 Informations Personnelles"): """Étape 1 du formulaire - Infos personnelles""" pseudo = discord.ui.TextInput(label="Pseudo Epic (Fortnite)", required=True, max_length=16) age = discord.ui.TextInput(label="Âge", required=True, max_length=2) experience = discord.ui.TextInput(label="Expérience RP ?", style=discord.TextStyle.paragraph, required=True, max_length=50) decouverte = discord.ui.TextInput(label="Comment as-tu découvert le serveur ?", required=True, max_length=20) micro = discord.ui.TextInput(label="As-tu un micro de qualité ?", required=True, max_length=3) async def on_submit(self, interaction: discord.Interaction): """Envoie un bouton pour passer à l'étape 2""" view = ContinueFormView(current_step=1, step1_data=self) await interaction.response.send_message("✅ Première étape terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True) class TicketStep2Part1(discord.ui.Modal, title="👤 Infos sur ton personnage (Partie 1)"): """Étape 2 du formulaire - Infos RP (Partie 1)""" def __init__(self, step1_data): super().__init__() self.step1_data = step1_data prenom = discord.ui.TextInput(label="Prénom", required=True) nom = discord.ui.TextInput(label="Nom", required=True) age_perso = discord.ui.TextInput(label="Âge du personnage", required=True) genre = discord.ui.TextInput(label="Genre", required=True, max_length=5) naissance = discord.ui.TextInput(label="Date et lieu de naissance", required=True) async def on_submit(self, interaction: discord.Interaction): """Envoie un bouton pour passer à l'étape suivante""" view = ContinueFormView(current_step=2, step1_data=self.step1_data, step2_part1_data=self) await interaction.response.send_message("✅ Deuxième partie terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True) class TicketStep2Part2(discord.ui.Modal, title="👤 Infos sur ton personnage (Partie 2)"): """Étape 2 du formulaire - Infos RP (Partie 2)""" def __init__(self, step1_data, step2_part1_data): super().__init__() self.step1_data = step1_data self.step2_part1_data = step2_part1_data traits = discord.ui.TextInput(label="Traits de caractère", required=True) nationalite = discord.ui.TextInput(label="Nationalité", required=True) skin_rp = discord.ui.TextInput(label="Skin RP", required=True) metier_souhaite = discord.ui.TextInput(label="Métier souhaité", required=True) async def on_submit(self, interaction: discord.Interaction): """Envoie un bouton pour passer à l'étape 3""" view = ContinueFormView(current_step=3, step1_data=self.step1_data, step2_part1_data=self.step2_part1_data, step2_part2_data=self) await interaction.response.send_message("✅ Deuxième étape terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True) class TicketStep3(discord.ui.Modal, title="📋 Développement du Personnage"): """Étape 3 du formulaire - Histoire du personnage""" def __init__(self, step1_data, step2_part1_data, step2_part2_data): super().__init__() self.step1_data = step1_data self.step2_part1_data = step2_part1_data self.step2_part2_data = step2_part2_data histoire = discord.ui.TextInput( label="Histoire du personnage", style=discord.TextStyle.paragraph, required=True, min_length=425, max_length=500 # Limite de caractères maximum ) objectifs = discord.ui.TextInput(label="Objectifs RP (court et long terme)", style=discord.TextStyle.paragraph, required=True, max_length=100) async def on_submit(self, interaction: discord.Interaction): """Création du ticket après validation""" guild = interaction.guild user = interaction.user settings = load_settings(guild.id) category_id = settings.get("ticket_category_id") staff_role_id = settings.get("staff_role_id") team_staff_role_id = settings.get("team_staff_role_id") if not category_id or not staff_role_id or not team_staff_role_id: return await interaction.response.send_message("❌ Erreur : Le système de tickets n'est pas entièrement configuré sur ce serveur (Catégorie, Rôle Staff ou Rôle Équipe Staff manquant).", ephemeral=True) category = discord.utils.get(guild.categories, id=int(category_id)) if category is None: return await interaction.response.send_message("❌ Erreur : La catégorie des tickets configurée n'existe plus !", ephemeral=True) staff_role = discord.utils.get(guild.roles, id=int(staff_role_id)) team_staff_role = discord.utils.get(guild.roles, id=int(team_staff_role_id)) if staff_role is None or team_staff_role is None: return await interaction.response.send_message("❌ Erreur : L'un des rôles staff configurés n'existe plus !", ephemeral=True) # Vérifier si l'utilisateur a déjà un ticket ouvert for channel in category.text_channels: if channel.topic == f"Ticket de {user.id}": return await interaction.response.send_message(f"❌ Vous avez déjà un ticket ouvert : {channel.mention}", ephemeral=True) # Définir les permissions du salon du ticket overwrites = { guild.default_role: discord.PermissionOverwrite(read_messages=False), user: discord.PermissionOverwrite(read_messages=True, send_messages=True), staff_role: discord.PermissionOverwrite(read_messages=True, send_messages=True), team_staff_role: discord.PermissionOverwrite(read_messages=True, send_messages=False) } # Créer un salon de ticket avec le nom [pseudo]-écrit ticket_channel = await category.create_text_channel( name=f"{user.name}-écrit", topic=f"Ticket de {user.id}", overwrites=overwrites ) # Création de l'embed avec les infos du formulaire embed = discord.Embed(title="📩 Ticket Whitelist", color=0x00ff00) embed.add_field(name="📋 - Informations Personnelles", value=( f"> - Pseudo Epic : {self.step1_data.pseudo.value}\n" f"> - Âge : {self.step1_data.age.value}\n" f"> - Expérience RP : {self.step1_data.experience.value}\n" f"> - Découverte du serveur : {self.step1_data.decouverte.value}\n" f"> - Micro de qualité : {self.step1_data.micro.value}" ), inline=False) embed.add_field(name="👤 - Informations RP", value=( f"> - Prénom : {self.step2_part1_data.prenom.value}\n" f"> - Nom : {self.step2_part1_data.nom.value}\n" f"> - Âge du personnage : {self.step2_part1_data.age_perso.value}\n" f"> - Genre : {self.step2_part1_data.genre.value}\n" f"> - Naissance : {self.step2_part1_data.naissance.value}\n" f"> - Traits de caractère : {self.step2_part2_data.traits.value}\n" f"> - Nationalité : {self.step2_part2_data.nationalite.value}\n" f"> - Skin RP : {self.step2_part2_data.skin_rp.value}\n" f"> - Métier souhaité : {self.step2_part2_data.metier_souhaite.value}" ), inline=False) embed.add_field(name="📋 - Développement du Personnage", value=( f"> - Histoire : {self.histoire.value}\n" f"> - Objectifs : {self.objectifs.value}" ), inline=False) # Envoyer l'embed avec les boutons d'actions pour les staff await ticket_channel.send(embed=embed, view=TicketManagementView()) # Envoyer un message pour mentionner l'équipe staff await ticket_channel.send(f"{team_staff_role.mention} - Nouveau ticket créé !") # Mettre à jour le pseudo du joueur si le bot a les permissions nécessaires if guild.me.guild_permissions.manage_nicknames: new_nickname = f"{self.step2_part1_data.prenom.value} {self.step2_part1_data.nom.value} | {self.step1_data.pseudo.value}" if len(new_nickname) > 32: new_nickname = new_nickname[:32] try: await user.edit(nick=new_nickname) except discord.Forbidden: pass # Envoyer un message de confirmation try: await interaction.response.send_message(f"✅ Ticket ouvert : {ticket_channel.mention}", ephemeral=True) except discord.errors.NotFound: await interaction.followup.send(f"✅ Ticket ouvert : {ticket_channel.mention}", ephemeral=True) class TicketManagementView(discord.ui.View): """Vue pour gérer le ticket avec les boutons Fermer, Réouvrir, Claim et Supprimer""" def __init__(self): super().__init__(timeout=None) async def get_ticket_owner(self, interaction: discord.Interaction): """Récupère le propriétaire du ticket via le helper robuste""" owner_id = await get_ticket_user_id(interaction.channel) if owner_id: try: member = interaction.guild.get_member(owner_id) if not member: member = await interaction.guild.fetch_member(owner_id) return member except: return None return None @discord.ui.button(label="🔒 Fermer le Ticket", style=discord.ButtonStyle.red, custom_id="close_ticket") async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): """Ferme le ticket en désactivant l'envoi de messages pour le membre""" await interaction.response.defer(ephemeral=True) settings = load_settings(interaction.guild.id) staff_role_id = settings.get("staff_role_id") is_staff = any(r.id == int(staff_role_id) for r in interaction.user.roles) if staff_role_id else False if not is_staff and not interaction.user.guild_permissions.administrator: return await interaction.followup.send("❌ Seuls les staffs peuvent fermer un ticket.", ephemeral=True) owner = await self.get_ticket_owner(interaction) if not owner: return await interaction.followup.send("❌ Impossible de trouver le propriétaire du ticket à fermer.", ephemeral=True) try: await interaction.channel.set_permissions(owner, read_messages=False, send_messages=False) log_channel_rename(interaction.channel.id) asyncio.create_task(interaction.channel.edit(name=f"fermé-{owner.display_name}".replace(" ", "-").lower()[:100])) # Envoi du message de fermeture dans le salon new_view = ClosedTicketView() await interaction.channel.send(f"🔒 Ce ticket a été fermé par {interaction.user.mention}.", view=new_view) await interaction.followup.send("✅ Ticket fermé avec succès.", ephemeral=True) except Exception as e: kuby_logger.error(f"Erreur lors de la fermeture du ticket: {e}") await interaction.followup.send(f"❌ Erreur lors de la fermeture : {e}", ephemeral=True) @discord.ui.button(label="👤 Claim le Ticket", style=discord.ButtonStyle.blurple, custom_id="claim_ticket") async def claim_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): """Un staff prend en charge le ticket""" await interaction.response.defer(ephemeral=False) settings = load_settings(interaction.guild.id) staff_role_id = settings.get("staff_role_id") is_staff = any(r.id == int(staff_role_id) for r in interaction.user.roles) if staff_role_id else False if not is_staff and not interaction.user.guild_permissions.administrator: return await interaction.followup.send("❌ Seuls les staffs peuvent claim un ticket.") try: staff_role = interaction.guild.get_role(int(staff_role_id)) if staff_role_id else None # Retrait des permissions du rôle staff pour forcer le claimer à parler seul if staff_role: await interaction.channel.set_permissions(staff_role, send_messages=False, read_messages=True) # Donner les permissions au claimer await interaction.channel.set_permissions(interaction.user, read_messages=True, send_messages=True) await interaction.followup.send(f"👥 {interaction.user.mention} a pris en charge ce ticket.") except Exception as e: kuby_logger.error(f"Erreur lors du claim du ticket: {e}") await interaction.followup.send(f"❌ Erreur lors du claim : {e}") class ClosedTicketView(discord.ui.View): """Vue pour les tickets fermés avec les boutons Réouvrir et Supprimer""" def __init__(self): super().__init__(timeout=None) async def get_ticket_owner(self, interaction: discord.Interaction): """Récupère le propriétaire du ticket via le helper robuste""" owner_id = await get_ticket_user_id(interaction.channel) if owner_id: try: member = interaction.guild.get_member(owner_id) if not member: member = await interaction.guild.fetch_member(owner_id) return member except: return None return None @discord.ui.button(label="♻ Réouvrir le Ticket", style=discord.ButtonStyle.green, custom_id="reopen_ticket") async def reopen_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): """Réouvre le ticket pour le membre""" await interaction.response.defer(ephemeral=True) settings = load_settings(interaction.guild.id) staff_role_id = settings.get("staff_role_id") is_staff = any(r.id == int(staff_role_id) for r in interaction.user.roles) if staff_role_id else False if not is_staff and not interaction.user.guild_permissions.administrator: return await interaction.followup.send("❌ Seuls les staffs peuvent réouvrir un ticket.", ephemeral=True) owner = await self.get_ticket_owner(interaction) if not owner: return await interaction.followup.send("❌ Impossible de trouver le propriétaire du ticket à réouvrir.", ephemeral=True) try: # On demande au staff quel statut remettre embed = discord.Embed( title="♻️ Réouverture du Ticket", description=f"Choisissez le statut à appliquer pour **{owner.display_name}**.", color=discord.Color.blue() ) view = ReopenStatusView(owner) await interaction.channel.send(embed=embed, view=view) await interaction.followup.send("✅ Choisissez le statut ci-dessous.", ephemeral=True) except Exception as e: kuby_logger.error(f"Erreur lors de la préparation de réouverture: {e}") await interaction.followup.send(f"❌ Erreur : {e}", ephemeral=True) class ReopenStatusView(discord.ui.View): """Vue pour choisir le statut lors d'une réouverture""" def __init__(self, owner: discord.Member): super().__init__(timeout=180) self.owner = owner @discord.ui.button(label="Écrit (Ouvert)", style=discord.ButtonStyle.secondary) async def set_written(self, interaction: discord.Interaction, button: discord.ui.Button): await self.apply_status(interaction, "ouvert") @discord.ui.button(label="Oral (À faire)", style=discord.ButtonStyle.primary) async def set_oral(self, interaction: discord.Interaction, button: discord.ui.Button): await self.apply_status(interaction, "oral-à-faire") @discord.ui.button(label="Accepté", style=discord.ButtonStyle.success) async def set_accepted(self, interaction: discord.Interaction, button: discord.ui.Button): await self.apply_status(interaction, "accepté") async def apply_status(self, interaction: discord.Interaction, prefix: str): await interaction.response.defer(ephemeral=True) try: settings = load_settings(interaction.guild.id) candidature_a_faire_role_id = settings.get("candidature_a_faire_role_id") attente_oral_role_id = settings.get("attente_oral_role_id") citoyen_role_id = settings.get("citoyen_role_id") # On récupère les objets rôles role_écrit = interaction.guild.get_role(int(candidature_a_faire_role_id)) if candidature_a_faire_role_id else None role_oral = interaction.guild.get_role(int(attente_oral_role_id)) if attente_oral_role_id else None role_citoyen = interaction.guild.get_role(int(citoyen_role_id)) if citoyen_role_id else None # Synchronisation des RÔLES en fonction du statut choisi roles_to_remove = [] role_to_add = None if prefix == "ouvert": role_to_add = role_écrit roles_to_remove = [r for r in [role_oral, role_citoyen] if r] elif prefix == "oral-à-faire": role_to_add = role_oral roles_to_remove = [r for r in [role_écrit, role_citoyen] if r] elif prefix == "accepté": role_to_add = role_citoyen roles_to_remove = [r for r in [role_écrit, role_oral] if r] # Mise à jour des rôles sur le membre if roles_to_remove: await self.owner.remove_roles(*roles_to_remove, reason="Réouverture du ticket - Maj Statut") if role_to_add: await self.owner.add_roles(role_to_add, reason="Réouverture du ticket - Maj Statut") # Mise à jour du SALON await interaction.channel.set_permissions(self.owner, read_messages=True, send_messages=True) new_name = f"{prefix}-{self.owner.display_name}".replace(" ", "-").lower()[:100] log_channel_rename(interaction.channel.id) asyncio.create_task(interaction.channel.edit(name=new_name, topic=f"Ticket de {self.owner.id}")) new_view = TicketManagementView() await interaction.channel.send(f"✅ Ticket réouvert par {interaction.user.mention} (Statut: **{prefix}**).", view=new_view) await interaction.followup.send(f"✅ Statut **{prefix}** appliqué et rôles synchronisés.", ephemeral=True) self.stop() except Exception as e: kuby_logger.error(f"Erreur lors de la réouverture/synchro: {e}") await interaction.followup.send(f"❌ Erreur système : {e}", ephemeral=True) @discord.ui.button(label="❌ Supprimer le Ticket", style=discord.ButtonStyle.danger, custom_id="delete_ticket") async def delete_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): """Supprime le salon du ticket""" await interaction.response.defer(ephemeral=True) settings = load_settings(interaction.guild.id) staff_role_id = settings.get("staff_role_id") is_staff = any(r.id == int(staff_role_id) for r in interaction.user.roles) if staff_role_id else False if not is_staff and not interaction.user.guild_permissions.administrator: return await interaction.followup.send("❌ Seuls les staffs peuvent supprimer un ticket.", ephemeral=True) try: await interaction.followup.send("❌ Suppression du ticket...", ephemeral=True) await interaction.channel.delete() except Exception as e: kuby_logger.error(f"Erreur lors de la suppression du ticket: {e}") try: await interaction.followup.send(f"❌ Erreur lors de la suppression : {e}", ephemeral=True) except: pass class TicketView(discord.ui.View): """Vue avec le bouton pour ouvrir le ticket""" def __init__(self): super().__init__(timeout=None) @discord.ui.button(label="📑 Ouvrir un Ticket Whitelist", style=discord.ButtonStyle.red, custom_id="open_ticket") async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): """Affiche le premier formulaire""" await interaction.response.send_modal(TicketStep1()) class Ticket(commands.Cog): """Commande pour envoyer l'embed de ticket""" def __init__(self, bot): self.bot = bot async def cog_load(self): """Enregistre les vues persistantes""" self.bot.add_view(TicketView()) self.bot.add_view(TicketManagementView()) self.bot.add_view(ClosedTicketView()) @app_commands.command(name="ticket_whitelist", description="Envoie l'embed pour ouvrir un ticket de whitelist.") @app_commands.checks.has_permissions(administrator=True) async def ticket_whitelist(self, interaction: discord.Interaction): """Envoie l'embed avec le bouton d'ouverture de ticket""" embed = discord.Embed( title="📩 Ticket Whitelist", description="Pour réaliser votre whitelist, ouvrez un ticket et remplissez le formulaire.", color=0x00ff00 ) view = TicketView() await interaction.channel.send(embed=embed, view=view) await interaction.response.send_message("✅ Embed des tickets envoyé.", ephemeral=True) @app_commands.command(name="ticket_whitelist_config", description="Configurer le système de tickets whitelist (Admin)") @app_commands.checks.has_permissions(administrator=True) async def ticket_whitelist_config(self, interaction: discord.Interaction): """Interface de configuration des tickets""" settings = load_settings(interaction.guild.id) embed = discord.Embed( title="⚙️ Configuration Tickets Whitelist", description="Utilisez les boutons ci-dessous pour lier la catégorie et les rôles.", color=discord.Color.blue() ) cat_id = settings.get("ticket_category_id") s_role_id = settings.get("staff_role_id") ts_role_id = settings.get("team_staff_role_id") ao_role_id = settings.get("attente_oral_role_id") caf_role_id = settings.get("candidature_a_faire_role_id") c_role_id = settings.get("citoyen_role_id") embed.add_field(name="📁 Catégorie Tickets", value=f"Id: {cat_id}" if cat_id else "❌ Non configuré", inline=True) embed.add_field(name="🛡️ Staff", value=f"<@&{s_role_id}>" if s_role_id else "❌ Non configuré", inline=True) embed.add_field(name="👥 Équipe Staff", value=f"<@&{ts_role_id}>" if ts_role_id else "❌ Non configuré", inline=True) embed.add_field(name="🎤 Attente Oral", value=f"<@&{ao_role_id}>" if ao_role_id else "❌ Non configuré", inline=True) embed.add_field(name="📝 Candidature à faire", value=f"<@&{caf_role_id}>" if caf_role_id else "❌ Non configuré", inline=True) embed.add_field(name="🏙️ Citoyen", value=f"<@&{c_role_id}>" if c_role_id else "❌ Non configuré", inline=True) await interaction.response.send_message(embed=embed, view=TicketWhitelistConfigView(interaction.guild.id), ephemeral=True) @commands.Cog.listener() async def on_message(self, message): """Surveille les messages des staffs dans les tickets pour validation automatique""" if message.guild is None or message.author.bot: return # On ne traite que si le message contient "validé" if "validé" not in message.content.lower(): return # Détection du format du topic du ticket via helper (robuste) user_id = await get_ticket_user_id(message.channel) if not user_id: await message.channel.send("❌ Debug: Impossible de récupérer l'ID utilisateur via le topic (même après refresh API).") return settings = load_settings(message.guild.id) staff_role_id = settings.get("staff_role_id") # Vérification si l'auteur est staff ou admin is_staff = False if staff_role_id: is_staff = any(r.id == int(staff_role_id) for r in message.author.roles) if not is_staff and not message.author.guild_permissions.administrator: if "validé" in message.content.lower(): await message.channel.send("❌ Debug: Permission refusée. Vous n'êtes ni Staff ni Admin.") return try: # L'ID est déjà trouvé par le helper ci-dessus user_id = user_id try: user = await message.guild.fetch_member(user_id) except (discord.NotFound, discord.HTTPException): await message.channel.send("❌ Utilisateur introuvable. A-t-il quitté le serveur ?", delete_after=10) return attente_oral_role_id = settings.get("attente_oral_role_id") candidature_a_faire_role_id = settings.get("candidature_a_faire_role_id") citoyen_role_id = settings.get("citoyen_role_id") if not attente_oral_role_id or not candidature_a_faire_role_id or not citoyen_role_id: await message.channel.send("❌ Erreur de configuration : Impossible d'attribuer les rôles, ils ne sont pas tous définis dans `/ticket_whitelist_config`.", delete_after=10) return attente_oral_role = message.guild.get_role(int(attente_oral_role_id)) candidature_a_faire_role = message.guild.get_role(int(candidature_a_faire_role_id)) citoyen_role = message.guild.get_role(int(citoyen_role_id)) if (attente_oral_role and attente_oral_role in user.roles) or "oral-à-faire" in message.channel.name.lower(): # DEUXIÈME VALIDATION -> ACCEPTÉ / CITOYEN if attente_oral_role: await user.remove_roles(attente_oral_role) if citoyen_role: await user.add_roles(citoyen_role) # Mise à jour du nom du salon new_name = f"accepté-{user.display_name}".replace(" ", "-").lower()[:100] wait_time = check_channel_rename_rate_limit(message.channel.id) log_channel_rename(message.channel.id) asyncio.create_task(message.channel.edit(name=new_name)) # Confirmation dans le salon (Feedback visuel) embed_val = discord.Embed( title="🎉 Whitelist Validée !", description=f"Félicitations {user.mention}, ton entretien oral a été validé par {message.author.mention} !\nTu es désormais **Citoyen**.", color=discord.Color.green() ) await message.channel.send(embed=embed_val) # DM au joueur (RESTAURATION DESIGN CAPTURE) try: embed_dm = discord.Embed( title="🎉 Félicitations - Citoyenneté Validée !", description=f"Nous avons le plaisir de t'annoncer que ton passage sur **{message.guild.name}** a été validé avec succès !", color=discord.Color.green() ) embed_dm.add_field(name="🏙️ Nouveau Statut", value="Citoyen", inline=True) embed_dm.add_field(name="📍 Serveur", value=message.guild.name, inline=True) embed_dm.add_field(name="📋 Prochaine étape", value="Tu peux désormais te connecter au serveur et commencer ton aventure RP. N'oublie pas de consulter les salons d'informations !", inline=False) if wait_time > 0: m, s = divmod(wait_time, 60) embed_dm.add_field(name="⏳ Nom du ticket", value=f"Le nom de ton ticket sera actualisé d'ici {m}m et {s}s environ (limite de sécurité Discord).", inline=False) embed_dm.add_field(name="🔗 Lien vers ton ticket", value=f"[Clique ici pour voir ton ticket]({message.channel.jump_url})", inline=False) embed_dm.set_footer(text=f"L'équipe {message.guild.name}") if message.guild.icon: embed_dm.set_thumbnail(url=message.guild.icon.url) await user.send(embed=embed_dm) except: pass else: # PREMIÈRE VALIDATION -> ORAL À FAIRE if candidature_a_faire_role: await user.remove_roles(candidature_a_faire_role) if attente_oral_role: await user.add_roles(attente_oral_role) # Mise à jour du nom du salon new_name = f"oral-à-faire-{user.display_name}".replace(" ", "-").lower()[:100] wait_time = check_channel_rename_rate_limit(message.channel.id) log_channel_rename(message.channel.id) asyncio.create_task(message.channel.edit(name=new_name)) # Confirmation dans le salon embed_val = discord.Embed( title="📝 Étape Écrite Validée !", description=f"Ta candidature écrite a été validée par {message.author.mention}.\nTu passes désormais en **Attente Oral**.", color=discord.Color.orange() ) await message.channel.send(embed=embed_val) # DM au joueur (RESTAURATION DESIGN CAPTURE) try: embed_dm = discord.Embed( title="📝 Validation Étape Écrite", description=f"Ta candidature écrite sur **{message.guild.name}** a été examinée et validée par notre équipe !", color=discord.Color.orange() ) embed_dm.add_field(name="🎤 Statut Actuel", value="Attente Oral", inline=True) embed_dm.add_field(name="🛠️ Action Requise", value="Merci de te rendre dans le salon vocal 'Attente Oral' dès que tu es disponible pour passer ton entretien.", inline=False) if wait_time > 0: m, s = divmod(wait_time, 60) embed_dm.add_field(name="⏳ Nom du ticket", value=f"Le nom de ton ticket sera actualisé d'ici {m}m et {s}s environ (limite de sécurité Discord).", inline=False) embed_dm.add_field(name="🔗 Lien vers ton ticket", value=f"[Clique ici pour voir ton ticket]({message.channel.jump_url})", inline=False) embed_dm.set_footer(text=f"L'équipe {message.guild.name}") if message.guild.icon: embed_dm.set_thumbnail(url=message.guild.icon.url) await user.send(embed=embed_dm) except: pass except Exception as e: kuby_logger.error(f"Erreur lors de la validation du ticket: {e}", exc_info=True) await message.channel.send(f"❌ Une erreur système est survenue : {e}") async def setup(bot): await bot.add_cog(Ticket(bot)) bot.add_view(TicketView()) bot.add_view(TicketManagementView()) bot.add_view(ClosedTicketView())