806 lines
No EOL
42 KiB
Python
806 lines
No EOL
42 KiB
Python
import disnake
|
|
import re
|
|
from disnake.ext import commands
|
|
from disnake import 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: disnake.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 (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(disnake.ui.View):
|
|
def __init__(self, guild_id: int):
|
|
super().__init__(timeout=60)
|
|
self.guild_id = guild_id
|
|
|
|
async def prompt_for_id(self, interaction: disnake.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 = disnake.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=disnake.ButtonStyle.primary, row=0, emoji="📁")
|
|
async def set_category(self, interaction: disnake.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=disnake.ButtonStyle.primary, row=0, emoji="🛡️")
|
|
async def set_staff_role(self, interaction: disnake.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=disnake.ButtonStyle.primary, row=0, emoji="👥")
|
|
async def set_team_staff_role(self, interaction: disnake.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=disnake.ButtonStyle.secondary, row=1, emoji="🎤")
|
|
async def set_attente_oral_role(self, interaction: disnake.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=disnake.ButtonStyle.secondary, row=1, emoji="📝")
|
|
async def set_candidature_a_faire_role(self, interaction: disnake.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=disnake.ButtonStyle.success, row=1, emoji="🏙️")
|
|
async def set_citoyen_role(self, interaction: disnake.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(disnake.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
|
|
|
|
@disnake.ui.button(label="📋 Continuer le formulaire", style=disnake.ButtonStyle.green, custom_id="continue_form")
|
|
async def continue_form(self, interaction: disnake.Interaction, button: disnake.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 disnake.errors.NotFound:
|
|
# Si le message n'existe plus, nous ne faisons rien
|
|
pass
|
|
|
|
class TicketStep1(disnake.ui.Modal):
|
|
"""Étape 1 du formulaire - Infos personnelles"""
|
|
|
|
def __init__(self):
|
|
super().__init__(
|
|
title="📋 Informations Personnelles",
|
|
custom_id="ticket_step1",
|
|
components=[
|
|
disnake.ui.TextInput(label="Pseudo Epic (Fortnite)", custom_id="pseudo", required=True, max_length=16),
|
|
disnake.ui.TextInput(label="Âge", custom_id="age", required=True, max_length=2),
|
|
disnake.ui.TextInput(label="Expérience RP ?", custom_id="experience", style=disnake.TextInputStyle.paragraph, required=True, max_length=50),
|
|
disnake.ui.TextInput(label="Comment as-tu découvert le serveur ?", custom_id="decouverte", required=True, max_length=20),
|
|
disnake.ui.TextInput(label="As-tu un micro de qualité ?", custom_id="micro", required=True, max_length=3),
|
|
]
|
|
)
|
|
|
|
async def callback(self, interaction: disnake.ModalInteraction):
|
|
"""Envoie un bouton pour passer à l'étape 2"""
|
|
view = ContinueFormView(current_step=1, step1_data=interaction.text_values)
|
|
await interaction.response.send_message("✅ Première étape terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True)
|
|
|
|
class TicketStep2Part1(disnake.ui.Modal):
|
|
"""Étape 2 du formulaire - Infos RP (Partie 1)"""
|
|
|
|
def __init__(self, step1_data):
|
|
self.step1_data = step1_data
|
|
super().__init__(
|
|
title="👤 Infos sur ton personnage (Partie 1)",
|
|
custom_id="ticket_step2_part1",
|
|
components=[
|
|
disnake.ui.TextInput(label="Prénom", custom_id="prenom", required=True),
|
|
disnake.ui.TextInput(label="Nom", custom_id="nom", required=True),
|
|
disnake.ui.TextInput(label="Âge du personnage", custom_id="age_perso", required=True),
|
|
disnake.ui.TextInput(label="Genre", custom_id="genre", required=True, max_length=5),
|
|
disnake.ui.TextInput(label="Date et lieu de naissance", custom_id="naissance", required=True),
|
|
]
|
|
)
|
|
|
|
async def callback(self, interaction: disnake.ModalInteraction):
|
|
"""Envoie un bouton pour passer à l'étape suivante"""
|
|
view = ContinueFormView(current_step=2, step1_data=self.step1_data, step2_part1_data=interaction.text_values)
|
|
await interaction.response.send_message("✅ Deuxième partie terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True)
|
|
|
|
class TicketStep2Part2(disnake.ui.Modal):
|
|
"""Étape 2 du formulaire - Infos RP (Partie 2)"""
|
|
|
|
def __init__(self, step1_data, step2_part1_data):
|
|
self.step1_data = step1_data
|
|
self.step2_part1_data = step2_part1_data
|
|
super().__init__(
|
|
title="👤 Infos sur ton personnage (Partie 2)",
|
|
custom_id="ticket_step2_part2",
|
|
components=[
|
|
disnake.ui.TextInput(label="Traits de caractère", custom_id="traits", required=True),
|
|
disnake.ui.TextInput(label="Nationalité", custom_id="nationalite", required=True),
|
|
disnake.ui.TextInput(label="Skin RP", custom_id="skin_rp", required=True),
|
|
disnake.ui.TextInput(label="Métier souhaité", custom_id="metier_souhaite", required=True),
|
|
]
|
|
)
|
|
|
|
async def callback(self, interaction: disnake.ModalInteraction):
|
|
"""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=interaction.text_values)
|
|
await interaction.response.send_message("✅ Deuxième étape terminée ! Cliquez ci-dessous pour continuer.", view=view, ephemeral=True)
|
|
|
|
class TicketStep3(disnake.ui.Modal):
|
|
"""Étape 3 du formulaire - Histoire du personnage"""
|
|
|
|
def __init__(self, step1_data, step2_part1_data, step2_part2_data):
|
|
self.step1_data = step1_data
|
|
self.step2_part1_data = step2_part1_data
|
|
self.step2_part2_data = step2_part2_data
|
|
super().__init__(
|
|
title="📋 Développement du Personnage",
|
|
custom_id="ticket_step3",
|
|
components=[
|
|
disnake.ui.TextInput(
|
|
label="Histoire du personnage",
|
|
custom_id="histoire",
|
|
style=disnake.TextInputStyle.paragraph,
|
|
required=True,
|
|
min_length=425,
|
|
max_length=500
|
|
),
|
|
disnake.ui.TextInput(label="Objectifs RP (court et long terme)", custom_id="objectifs", style=disnake.TextInputStyle.paragraph, required=True, max_length=100),
|
|
]
|
|
)
|
|
|
|
async def callback(self, interaction: disnake.ModalInteraction):
|
|
"""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 = disnake.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 = disnake.utils.get(guild.roles, id=int(staff_role_id))
|
|
team_staff_role = disnake.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: disnake.PermissionOverwrite(read_messages=False),
|
|
user: disnake.PermissionOverwrite(read_messages=True, send_messages=True),
|
|
staff_role: disnake.PermissionOverwrite(read_messages=True, send_messages=True),
|
|
team_staff_role: disnake.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 = disnake.Embed(title="📩 Ticket Whitelist", color=0x00ff00)
|
|
|
|
# Helper to get value from data
|
|
s1 = self.step1_data
|
|
s2p1 = self.step2_part1_data
|
|
s2p2 = self.step2_part2_data
|
|
s3 = interaction.text_values
|
|
|
|
embed.add_field(name="📋 - Informations Personnelles", value=(
|
|
f"> - Pseudo Epic : {s1['pseudo']}\n"
|
|
f"> - Âge : {s1['age']}\n"
|
|
f"> - Expérience RP : {s1['experience']}\n"
|
|
f"> - Découverte du serveur : {s1['decouverte']}\n"
|
|
f"> - Micro de qualité : {s1['micro']}"
|
|
), inline=False)
|
|
embed.add_field(name="👤 - Informations RP", value=(
|
|
f"> - Prénom : {s2p1['prenom']}\n"
|
|
f"> - Nom : {s2p1['nom']}\n"
|
|
f"> - Âge du personnage : {s2p1['age_perso']}\n"
|
|
f"> - Genre : {s2p1['genre']}\n"
|
|
f"> - Naissance : {s2p1['naissance']}\n"
|
|
f"> - Traits de caractère : {s2p2['traits']}\n"
|
|
f"> - Nationalité : {s2p2['nationalite']}\n"
|
|
f"> - Skin RP : {s2p2['skin_rp']}\n"
|
|
f"> - Métier souhaité : {s2p2['metier_souhaite']}"
|
|
), inline=False)
|
|
embed.add_field(name="📋 - Développement du Personnage", value=(
|
|
f"> - Histoire : {s3['histoire']}\n"
|
|
f"> - Objectifs : {s3['objectifs']}"
|
|
), 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"{s2p1['prenom']} {s2p1['nom']} | {s1['pseudo']}"
|
|
if len(new_nickname) > 32:
|
|
new_nickname = new_nickname[:32]
|
|
try: await user.edit(nick=new_nickname)
|
|
except disnake.Forbidden: pass
|
|
|
|
# Envoyer un message de confirmation
|
|
try:
|
|
await interaction.response.send_message(f"✅ Ticket ouvert : {ticket_channel.mention}", ephemeral=True)
|
|
except disnake.errors.NotFound:
|
|
await interaction.followup.send(f"✅ Ticket ouvert : {ticket_channel.mention}", ephemeral=True)
|
|
|
|
class TicketManagementView(disnake.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: disnake.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
|
|
|
|
@disnake.ui.button(label="🔒 Fermer le Ticket", style=disnake.ButtonStyle.red, custom_id="close_ticket")
|
|
async def close_ticket(self, interaction: disnake.Interaction, button: disnake.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 disnake.errors.RateLimited as e:
|
|
await interaction.followup.send(f"⚠️ Limite de débit Discord : Réessayez dans {e.retry_after:.1f}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)
|
|
|
|
@disnake.ui.button(label="👤 Claim le Ticket", style=disnake.ButtonStyle.blurple, custom_id="claim_ticket")
|
|
async def claim_ticket(self, interaction: disnake.Interaction, button: disnake.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(disnake.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: disnake.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
|
|
|
|
@disnake.ui.button(label="♻ Réouvrir le Ticket", style=disnake.ButtonStyle.green, custom_id="reopen_ticket")
|
|
async def reopen_ticket(self, interaction: disnake.Interaction, button: disnake.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 = disnake.Embed(
|
|
title="♻️ Réouverture du Ticket",
|
|
description=f"Choisissez le statut à appliquer pour **{owner.display_name}**.",
|
|
color=disnake.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(disnake.ui.View):
|
|
"""Vue pour choisir le statut lors d'une réouverture"""
|
|
def __init__(self, owner: disnake.Member):
|
|
super().__init__(timeout=180)
|
|
self.owner = owner
|
|
|
|
@disnake.ui.button(label="Écrit (Ouvert)", style=disnake.ButtonStyle.secondary)
|
|
async def set_written(self, interaction: disnake.Interaction, button: disnake.ui.Button):
|
|
await self.apply_status(interaction, "ouvert")
|
|
|
|
@disnake.ui.button(label="Oral (À faire)", style=disnake.ButtonStyle.primary)
|
|
async def set_oral(self, interaction: disnake.Interaction, button: disnake.ui.Button):
|
|
await self.apply_status(interaction, "oral-à-faire")
|
|
|
|
@disnake.ui.button(label="Accepté", style=disnake.ButtonStyle.success)
|
|
async def set_accepted(self, interaction: disnake.Interaction, button: disnake.ui.Button):
|
|
await self.apply_status(interaction, "accepté")
|
|
|
|
async def apply_status(self, interaction: disnake.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)
|
|
|
|
@disnake.ui.button(label="❌ Supprimer le Ticket", style=disnake.ButtonStyle.danger, custom_id="delete_ticket")
|
|
async def delete_ticket(self, interaction: disnake.Interaction, button: disnake.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(disnake.ui.View):
|
|
"""Vue avec le bouton pour ouvrir le ticket"""
|
|
|
|
def __init__(self):
|
|
super().__init__(timeout=None)
|
|
|
|
@disnake.ui.button(label="📑 Ouvrir un Ticket Whitelist", style=disnake.ButtonStyle.red, custom_id="open_ticket")
|
|
async def open_ticket(self, interaction: disnake.Interaction, button: disnake.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())
|
|
|
|
@commands.slash_command(name="ticket_whitelist", description="Envoie l'embed pour ouvrir un ticket de whitelist.")
|
|
@commands.has_permissions(administrator=True)
|
|
async def ticket_whitelist(self, interaction: disnake.ApplicationCommandInteraction):
|
|
"""Envoie l'embed avec le bouton d'ouverture de ticket"""
|
|
embed = disnake.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)
|
|
|
|
@commands.slash_command(name="ticket_whitelist_config", description="Configurer le système de tickets whitelist (Admin)")
|
|
@commands.has_permissions(administrator=True)
|
|
async def ticket_whitelist_config(self, interaction: disnake.ApplicationCommandInteraction):
|
|
"""Interface de configuration des tickets"""
|
|
settings = load_settings(interaction.guild.id)
|
|
|
|
embed = disnake.Embed(
|
|
title="⚙️ Configuration Tickets Whitelist",
|
|
description="Utilisez les boutons ci-dessous pour lier la catégorie et les rôles.",
|
|
color=disnake.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 (disnake.NotFound, disnake.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 = disnake.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=disnake.Color.green()
|
|
)
|
|
await message.channel.send(embed=embed_val)
|
|
|
|
# DM au joueur (RESTAURATION DESIGN CAPTURE)
|
|
try:
|
|
embed_dm = disnake.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=disnake.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 = disnake.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=disnake.Color.orange()
|
|
)
|
|
await message.channel.send(embed=embed_val)
|
|
|
|
# DM au joueur (RESTAURATION DESIGN CAPTURE)
|
|
try:
|
|
embed_dm = disnake.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=disnake.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()) |