Kuby/commandes/ticket/__init__.py

3822 lines
167 KiB
Python
Raw Normal View History

"""
Système de Tickets Simplifié
=============================
Un système de tickets simple et intuitif avec panel intégré.
Fonctionnalités:
- Une seule commande: /ticket
- Création de tickets via boutons
- Gestion via interface interactive
- Stats et configuration accessibles depuis le même panel
"""
import os
import sys
import io
2026-01-14 21:32:20 +01:00
import asyncio
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from collections import defaultdict
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
2026-05-06 17:07:09 +02:00
import disnake
from disnake.ext import commands, tasks
2026-01-14 21:32:20 +01:00
import traceback
from PIL import Image, ImageDraw, ImageFont
from .data.storage import get_storage
from .data.models import (
GuildTicketConfig,
TicketCategory,
TicketStatus,
Priority,
TicketData,
TicketStats
)
from .utils.formatters import TicketEmbedFormatter
from .staff_analytics import StaffAnalytics, StaffStatsView
2026-01-14 21:32:20 +01:00
from .utils.permissions import is_staff, is_whitelisted, can_access_config
from src.logger import kuby_logger
# V2 RICH COMPONENTS LOADED - 14:10
kuby_logger.info("🎫 [TicketSystem] Chargement de la version V2 Rich Components...")
2026-05-01 16:16:33 +02:00
# Add a task for sending feedback reminders
class FeedbackReminderTask:
def __init__(self, bot):
self.bot = bot
self.reminders = {} # Store active reminders
async def schedule_feedback_reminder(self, user_id: int, ticket_id: int, delay_hours: int = 24):
"""Schedule a feedback reminder after a ticket is closed"""
# This would be implemented with a proper task scheduler
pass
2026-05-06 17:07:09 +02:00
class TicketPanelView(disnake.ui.View):
"""Panel principal avec toutes les options intégrées"""
def __init__(self, bot, config: GuildTicketConfig, storage=None):
super().__init__(timeout=None)
self.bot = bot
self.config = config
self.storage = storage or get_storage()
2026-05-06 17:07:09 +02:00
self.PURPLE = disnake.Color.blurple()
# Boutons du panel
self._build_panel()
def _build_panel(self):
"""Construit les boutons du panel selon la configuration"""
self.clear_items()
# Créer ticket
2026-05-06 17:07:09 +02:00
create_btn = disnake.ui.Button(
style=disnake.ButtonStyle.blurple,
label="🎫 Créer un ticket",
custom_id="ticket_create"
)
self.add_item(create_btn)
# Mes tickets (si l'utilisateur a des tickets)
2026-05-06 17:07:09 +02:00
my_tickets_btn = disnake.ui.Button(
style=disnake.ButtonStyle.secondary,
label="📂 Mes tickets",
custom_id="ticket_my_tickets"
)
self.add_item(my_tickets_btn)
# Analytics (staff only)
2026-05-06 17:07:09 +02:00
analytics_btn = disnake.ui.Button(
style=disnake.ButtonStyle.primary,
label="📊 Analytics",
custom_id="ticket_analytics"
)
self.add_item(analytics_btn)
2026-05-01 16:16:33 +02:00
# Staff Ratings (staff only)
2026-05-06 17:07:09 +02:00
staff_ratings_btn = disnake.ui.Button(
style=disnake.ButtonStyle.success,
2026-05-01 16:16:33 +02:00
label="⭐ Notation du Staff",
custom_id="ticket_staff_ratings"
)
staff_ratings_btn.callback = self._staff_ratings_callback
self.add_item(staff_ratings_btn)
def get_components_v2(self):
"""Retourne les composants V2 pour un affichage moderne"""
return [
disnake.ui.Container(
disnake.ui.Section(
"# 🎫 Système de Support - Omega Kube",
accessory=disnake.ui.Button(
label="Créer un ticket",
style=disnake.ButtonStyle.blurple,
custom_id="ticket_create"
)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(
content="Bienvenue sur le système de support d'**Omega Kube**.\n"
"Notre équipe est à votre disposition pour vous aider dans les plus brefs délais."
),
disnake.ui.Separator(divider=True),
disnake.ui.Section(
f"📂 {len(self.config.categories)} Catégories disponibles",
accessory=disnake.ui.Button(
label="Mes tickets",
style=disnake.ButtonStyle.secondary,
custom_id="ticket_my_tickets"
)
),
disnake.ui.Section(
"📊 Administration & Staff",
accessory=disnake.ui.Button(
label="Analytics",
style=disnake.ButtonStyle.primary,
custom_id="ticket_analytics"
)
)
)
]
2026-05-01 16:16:33 +02:00
# Configuration (admin only)
2026-05-06 17:07:09 +02:00
config_btn = disnake.ui.Button(
style=disnake.ButtonStyle.secondary,
label="⚙️ Configuration",
custom_id="ticket_config"
)
config_btn.callback = self._config_callback
self.add_item(config_btn)
2026-05-06 17:07:09 +02:00
async def _create_callback(self, interaction: disnake.Interaction):
"""Affiche les catégories pour créer un ticket"""
2026-01-14 21:32:20 +01:00
# Reload config to ensure we have latest permissions/categories
self.config = self.storage.load_config(interaction.guild_id)
2026-02-07 14:42:39 +01:00
# Reload config to ensure we have latest permissions/categories
self.config = self.storage.load_config(interaction.guild_id)
if not self.config.enabled:
await interaction.response.send_message("Le système de tickets est désactivé.", ephemeral=True)
return
if not self.config.categories:
await interaction.response.send_message("Aucune catégorie configurée. Utilisez la configuration.", ephemeral=True)
return
# Build V2 Container for category selection
sections = [
disnake.ui.TextDisplay(content="## 🎫 Créer un Ticket"),
disnake.ui.Separator(),
]
for cat in self.config.categories[:5]:
desc = cat.description or "Aucune description"
sections.append(
disnake.ui.Section(
f"**{cat.emoji} {cat.name}**\n{desc}",
accessory=disnake.ui.Button(
label=cat.name,
style=disnake.ButtonStyle.primary,
custom_id=f"catv2_{cat.id}_{int(disnake.utils.time_snowflake(disnake.utils.utcnow()) / 1000)}"
)
)
)
try:
await interaction.response.send_message(
components=[disnake.ui.Container(*sections)],
flags=disnake.MessageFlags(is_components_v2=True),
ephemeral=True
)
except Exception as e:
import traceback
traceback.print_exc()
# Fallback: texte simple avec view legacy
if interaction.response.is_done():
await interaction.followup.send(
f"**🎫 Créer un Ticket**\n\nSélectionnez une catégorie ci-dessous:",
view=CategorySelectionView(self.bot, self.config),
ephemeral=True
)
else:
await interaction.response.send_message(
f"**🎫 Créer un Ticket**\n\nSélectionnez une catégorie ci-dessous:",
view=CategorySelectionView(self.bot, self.config),
ephemeral=True
)
2026-05-06 17:07:09 +02:00
async def _my_tickets_callback(self, interaction: disnake.Interaction):
"""Affiche les tickets de l'utilisateur"""
2026-01-14 21:32:20 +01:00
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
2026-02-07 14:42:39 +01:00
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
storage = get_storage()
tickets = storage.get_user_tickets(interaction.guild_id, interaction.user.id, open_only=True)
if not tickets:
await interaction.response.send_message("Vous n'avez aucun ticket ouvert.", ephemeral=True)
return
# Build V2 Container for user's open tickets
sections = [
disnake.ui.TextDisplay(content=f"## 🎫 Vos Tickets Ouverts"),
disnake.ui.TextDisplay(content=f"Vous avez **{len(tickets)}** ticket(s) ouvert(s):"),
disnake.ui.Separator(),
]
for ticket in tickets[:5]:
channel = interaction.guild.get_channel(ticket.channel_id)
channel_mention = channel.mention if channel else "Salon supprimé"
sections.append(disnake.ui.TextDisplay(
content=f"**Ticket #{ticket.channel_id}**\nSalon: {channel_mention}\nStatut: {ticket.status.value}\nCréé: {ticket.created_at.strftime('%d/%m/%Y')}"
))
await interaction.response.send_message(
components=[disnake.ui.Container(*sections)],
ephemeral=True
)
2026-05-06 17:07:09 +02:00
async def _analytics_callback(self, interaction: disnake.Interaction):
"""Affiche les analytics (staff uniquement)"""
2026-01-14 21:32:20 +01:00
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
if not self._is_staff(interaction):
await interaction.response.send_message("❌ Cette fonctionnalité est réservée au staff.", ephemeral=True)
return
await interaction.response.defer(ephemeral=True)
analytics = StaffAnalytics(self.bot)
# Get staff list for individual stats
staff_list = await analytics.get_staff_list(interaction.guild_id)
# Create main embed with overall stats
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="📊 Analytics des Tickets",
description="Statistiques détaillées des tickets et performances du staff",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue(),
timestamp=datetime.now()
)
# Calculate overall stats
tickets = self.storage.get_guild_tickets(interaction.guild_id)
total_tickets = len(tickets)
open_tickets = sum(1 for t in tickets if t.status in [TicketStatus.OPEN, TicketStatus.CLAIMED])
closed_tickets = sum(1 for t in tickets if t.status == TicketStatus.CLOSED)
claimed_tickets = sum(1 for t in tickets if t.claimed_by is not None)
# Basic stats
embed.add_field(
name="📈 Statistiques Générales",
value=f"**Total:** {total_tickets}\n"
f"**Ouverts:** {open_tickets}\n"
f"**Fermés:** {closed_tickets}\n"
f"**Réclamés:** {claimed_tickets}",
inline=True
)
# Staff performance summary
if staff_list:
total_ratings = sum(s['total_ratings'] for s in staff_list)
avg_rating = sum(s['average_rating'] for s in staff_list if s['total_ratings'] > 0) / len([s for s in staff_list if s['total_ratings'] > 0]) if any(s['total_ratings'] > 0 for s in staff_list) else 0
embed.add_field(
name="👥 Équipe Staff",
value=f"**Membres actifs:** {len(staff_list)}\n"
f"**Évaluations totales:** {total_ratings}\n"
f"**Note moyenne globale:** {avg_rating:.1f}" if avg_rating > 0 else "**Note moyenne globale:** N/A",
inline=True
)
# Send main embed
await interaction.followup.send(embed=embed, ephemeral=True)
# Create and send staff rating chart
try:
if staff_list:
rating_chart = analytics.create_staff_rating_chart(staff_list)
await interaction.followup.send(
"📊 **Évaluations des Membres du Staff**",
2026-05-06 17:07:09 +02:00
file=disnake.File(rating_chart, 'staff_ratings.png'),
ephemeral=True
)
# Add individual staff stats view
stats_view = StaffStatsView(self.bot, staff_list, analytics)
2026-05-06 17:07:09 +02:00
stats_embed = disnake.Embed(
title="📈 Statistiques Individuelles",
description="Cliquez sur un membre du staff pour voir ses statistiques détaillées:",
2026-05-06 17:07:09 +02:00
color=disnake.Color.purple()
)
await interaction.followup.send(embed=stats_embed, view=stats_view, ephemeral=True)
except Exception as e:
await interaction.followup.send(f"❌ Erreur lors de la génération des graphiques: {e}", ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _config_callback(self, interaction: disnake.Interaction):
"""Ouvre le panel de configuration (admin ou rôles config uniquement)"""
2026-01-14 21:32:20 +01:00
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
if not self._is_whitelisted(interaction):
await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return
if not self._can_access_config(interaction):
await interaction.response.send_message("❌ Cette fonctionnalité est réservée aux administrateurs ou aux rôles configurés.", ephemeral=True)
return
storage = get_storage()
config_view = ConfigPanelView(self.bot, storage, self.config)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="Configuration des Tickets",
description="Gérez la configuration du système de tickets",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue()
)
await interaction.response.send_message(embed=embed, view=config_view, ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _staff_ratings_callback(self, interaction: disnake.Interaction):
2026-05-01 16:16:33 +02:00
"""Display staff ratings (staff only)"""
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
if not self._is_staff(interaction):
await interaction.response.send_message("❌ Cette fonctionnalité est réservée au staff.", ephemeral=True)
return
await interaction.response.defer(ephemeral=True)
analytics = StaffAnalytics(self.bot)
# Get staff list for individual stats
staff_list = await analytics.get_staff_list(interaction.guild_id)
# Create main embed with overall stats
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
2026-05-01 16:16:33 +02:00
title="⭐ Notation du Staff",
description="Performance globale du staff",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue(),
2026-05-01 16:16:33 +02:00
timestamp=datetime.now()
)
# Calculate overall stats
tickets = self.storage.get_guild_tickets(interaction.guild_id)
total_tickets = len(tickets)
open_tickets = sum(1 for t in tickets if t.status in [TicketStatus.OPEN, TicketStatus.CLAIMED])
closed_tickets = sum(1 for t in tickets if t.status == TicketStatus.CLOSED)
claimed_tickets = sum(1 for t in tickets if t.claimed_by is not None)
# Basic stats
embed.add_field(
name="📈 Statistiques Générales",
value=f"**Total:** {total_tickets}\n"
f"**Ouverts:** {open_tickets}\n"
f"**Fermés:** {closed_tickets}\n"
f"**Réclamés:** {claimed_tickets}",
inline=True
)
# Staff performance summary
if staff_list:
total_ratings = sum(s['total_ratings'] for s in staff_list)
avg_rating = sum(s['average_rating'] for s in staff_list if s['total_ratings'] > 0) / len([s for s in staff_list if s['total_ratings'] > 0]) if any(s['total_ratings'] > 0 for s in staff_list) else 0
embed.add_field(
name="👥 Équipe Staff",
value=f"**Membres actifs:** {len(staff_list)}\n"
f"**Évaluations totales:** {total_ratings}\n"
f"**Note moyenne globale:** {avg_rating:.1f}" if avg_rating > 0 else "**Note moyenne globale:** N/A",
inline=True
)
# Send main embed
await interaction.followup.send(embed=embed, ephemeral=True)
# Create and send staff rating chart
try:
if staff_list:
rating_chart = analytics.create_staff_rating_chart(staff_list)
await interaction.followup.send(
"📊 **Évaluations des Membres du Staff**",
2026-05-06 17:07:09 +02:00
file=disnake.File(rating_chart, 'staff_ratings.png'),
2026-05-01 16:16:33 +02:00
ephemeral=True
)
# Add individual staff stats view
stats_view = StaffStatsView(self.bot, staff_list, analytics)
2026-05-06 17:07:09 +02:00
stats_embed = disnake.Embed(
2026-05-01 16:16:33 +02:00
title="📈 Statistiques Individuelles",
description="Cliquez sur un membre du staff pour voir ses statistiques détaillées:",
2026-05-06 17:07:09 +02:00
color=disnake.Color.purple()
2026-05-01 16:16:33 +02:00
)
await interaction.followup.send(embed=stats_embed, view=stats_view, ephemeral=True)
except Exception as e:
await interaction.followup.send(f"❌ Erreur lors de la génération des graphiques: {e}", ephemeral=True)
2026-05-06 17:07:09 +02:00
def _is_staff(self, interaction: disnake.Interaction) -> bool:
2026-01-14 21:32:20 +01:00
return is_staff(interaction, self.config)
2026-05-06 17:07:09 +02:00
def _can_access_config(self, interaction: disnake.Interaction) -> bool:
2026-01-14 21:32:20 +01:00
return can_access_config(interaction, self.config)
2026-05-06 17:07:09 +02:00
def _is_whitelisted(self, interaction: disnake.Interaction) -> bool:
2026-01-14 21:32:20 +01:00
return is_whitelisted(interaction, self.config, self.bot)
def _calculate_stats(self, guild_id: int) -> TicketStats:
"""Calcule les statistiques"""
storage = get_storage()
tickets = storage.get_guild_tickets(guild_id)
stats = TicketStats()
stats.total_tickets = len(tickets)
total_duration = 0
closed_count = 0
rating_sum = 0
rating_count = 0
for ticket in tickets:
if ticket.status == TicketStatus.OPEN:
stats.open_tickets += 1
elif ticket.status == TicketStatus.CLAIMED:
stats.claimed_tickets += 1
stats.open_tickets += 1
elif ticket.status == TicketStatus.CLOSED:
stats.closed_tickets += 1
closed_count += 1
if ticket.duration_minutes:
total_duration += ticket.duration_minutes
if ticket.rating:
rating_sum += ticket.rating
rating_count += 1
if closed_count > 0:
stats.average_duration_minutes = total_duration / closed_count
if rating_count > 0:
stats.average_rating = rating_sum / rating_count
return stats
2026-05-06 17:07:09 +02:00
class CategoryButton(disnake.ui.Button):
2026-01-14 21:32:20 +01:00
def __init__(self, category, view_instance):
super().__init__(
2026-05-06 17:07:09 +02:00
style=disnake.ButtonStyle.blurple,
2026-01-14 21:32:20 +01:00
label=f"{category.emoji} {category.name}",
custom_id=f"cat_{category.id}_{view_instance.id_gen}"
)
self.category = category
self.view_instance = view_instance
# We need unique custom_ids even for ephemeral to avoid any weird collisions/caching
2026-05-06 17:07:09 +02:00
async def callback(self, interaction: disnake.Interaction):
2026-01-14 21:32:20 +01:00
await self.view_instance._on_category_select(interaction, self.category.id)
2026-05-06 17:07:09 +02:00
class CategorySelectionView(disnake.ui.View):
"""Sélection de catégorie pour créer un ticket"""
def __init__(self, bot, config: GuildTicketConfig):
2026-01-14 21:32:20 +01:00
super().__init__(timeout=300) # 5 minutes timeout for ephemeral menu
self.bot = bot
self.config = config
2026-01-14 21:32:20 +01:00
self.id_gen = int(datetime.now().timestamp())
# Ajouter boutons de catégories
for category in config.categories[:5]:
2026-01-14 21:32:20 +01:00
self.add_item(CategoryButton(category, self))
2026-05-06 17:07:09 +02:00
def _is_staff(self, interaction: disnake.Interaction) -> bool:
2026-01-14 21:32:20 +01:00
return is_staff(interaction, self.config)
2026-05-06 17:07:09 +02:00
def _can_access_config(self, interaction: disnake.Interaction) -> bool:
2026-01-14 21:32:20 +01:00
return can_access_config(interaction, self.config)
2026-05-06 17:07:09 +02:00
def _is_whitelisted(self, interaction: disnake.Interaction) -> bool:
2026-01-14 21:32:20 +01:00
return is_whitelisted(interaction, self.config, self.bot)
2026-05-06 17:07:09 +02:00
async def _on_category_select(self, interaction: disnake.Interaction, category_id: str):
"""Handle category selection"""
2026-01-14 21:32:20 +01:00
try:
2026-02-07 14:42:39 +01:00
2026-01-14 21:32:20 +01:00
category = self.config.get_category(category_id)
if not category:
await interaction.response.send_message("Catégorie non trouvée.", ephemeral=True)
return
storage = get_storage()
# Check limits
user_tickets = storage.get_user_tickets(interaction.guild_id, interaction.user.id, open_only=True)
active_tickets = []
for t in user_tickets:
if interaction.guild.get_channel(t.channel_id):
active_tickets.append(t)
else:
# Automatically clean up phantom tickets
t.status = TicketStatus.CLOSED
storage.save_ticket(t)
if len(active_tickets) >= self.config.max_tickets_per_user:
2026-01-14 21:32:20 +01:00
await interaction.response.send_message(
f"Vous avez atteint la limite de {self.config.max_tickets_per_user} tickets.",
ephemeral=True
)
return
# Check if this is a recruitment category
if category.is_recruitment and category.questions:
modal = RecruitmentQuestionsModal(self.bot, self.config, category)
else:
modal = TicketPriorityModal(self.bot, self.config, category)
2026-01-14 21:32:20 +01:00
await interaction.response.send_modal(modal)
except Exception as e:
kuby_logger.error(f"[CategorySelection] Error: {e}", exc_info=True)
try:
if not interaction.response.is_done():
await interaction.response.send_message(f"❌ Erreur interne: {e}", ephemeral=True)
except:
pass
2026-05-06 17:07:09 +02:00
class TicketPriorityModal(disnake.ui.Modal):
"""Modal pour spécifier la raison du ticket"""
def __init__(self, bot, config, category: TicketCategory):
self.reason_input = disnake.ui.TextInput(
label="Motif du ticket",
custom_id="ticket_reason",
placeholder="Décrivez brièvement votre problème...",
required=category.require_reason,
min_length=5 if category.require_reason else 0,
max_length=250,
style=disnake.TextInputStyle.paragraph
)
super().__init__(title=f"Support: {category.name}", components=[self.reason_input])
self.bot = bot
self.config = config
self.category = category
async def callback(self, interaction: disnake.ModalInteraction):
await interaction.response.defer(ephemeral=True)
try:
# Map priority
priority_map = {
"Urgente": Priority.URGENT,
"Haute": Priority.HIGH,
"Normale": Priority.NORMAL
}
# Logic updated to use reason input
priority = Priority.NORMAL
reason = interaction.text_values.get("ticket_reason", "Aucun motif fourni")
# Create ticket
await self._create_ticket(interaction, priority, reason)
except Exception as e:
await interaction.followup.send(f"Erreur lors de la création du ticket: {e}", ephemeral=True)
2026-05-06 17:07:09 +02:00
async def on_error(self, interaction: disnake.Interaction, error: Exception):
"""Handle errors in the modal"""
await interaction.response.send_message("Une erreur inattendue s'est produite lors de la création du ticket.", ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _create_ticket(self, interaction: disnake.Interaction, priority: Priority, reason: str):
"""Crée le ticket"""
guild = interaction.guild
user = interaction.user
storage = get_storage()
# Build permissions
overwrites = {
2026-05-06 17:07:09 +02:00
guild.default_role: disnake.PermissionOverwrite(read_messages=False),
user: disnake.PermissionOverwrite(read_messages=True, send_messages=True),
guild.me: disnake.PermissionOverwrite(read_messages=True, send_messages=True)
}
2026-05-01 16:16:33 +02:00
# Add staff roles (category-specific roles override global ones)
staff_roles_to_add = self.category.staff_roles if self.category.staff_roles else self.config.staff_roles
for role_id in staff_roles_to_add:
try:
role = guild.get_role(int(role_id))
if role and role not in overwrites:
2026-05-06 17:07:09 +02:00
overwrites[role] = disnake.PermissionOverwrite(read_messages=True, send_messages=True)
2026-05-01 16:16:33 +02:00
except (ValueError, TypeError):
continue
# Create channel name using username and category without numbers
safe_username = "".join(c for c in user.display_name if not c.isdigit()).strip()
if not safe_username:
safe_username = "user"
safe_category = "".join(c for c in self.category.name if not c.isdigit()).strip()
channel_name = f"{safe_username}-{safe_category}"
try:
category_channel = None
2026-05-01 16:17:55 +02:00
# Check if category has a specific Discord category
if self.category.discord_category_id:
try:
category_channel = guild.get_channel(int(self.category.discord_category_id))
if category_channel and not isinstance(category_channel, disnake.CategoryChannel):
category_channel = None
2026-05-01 16:17:55 +02:00
except (ValueError, TypeError):
pass
# Fallback to default config category if not found or not set
if not category_channel and self.config.default_category:
try:
category_channel = guild.get_channel(int(self.config.default_category))
if category_channel and not isinstance(category_channel, disnake.CategoryChannel):
category_channel = None
except (ValueError, TypeError):
pass
ticket_channel = await guild.create_text_channel(
name=channel_name,
category=category_channel,
overwrites=overwrites,
topic=f"Ticket de {user} | Catégorie: {self.category.name}"
)
except Exception as e:
await interaction.followup.send(f"Erreur: {e}", ephemeral=True)
return
# Create ticket data
ticket_data = TicketData(
channel_id=ticket_channel.id,
guild_id=guild.id,
user_id=user.id,
category_id=self.category.id,
priority=priority,
status=TicketStatus.OPEN,
reason=reason
)
storage.save_ticket(ticket_data)
# Build mentions
2026-01-14 21:32:20 +01:00
mentions = [user.mention]
for role_id in self.config.staff_roles:
mentions.append(f"<@&{role_id}>")
for user_id in self.config.staff_users:
mentions.append(f"<@{user_id}>")
mention_content = " ".join(mentions)
# Build a single V2 container with mention and ticket management components
mention_section = disnake.ui.TextDisplay(content=mention_content)
# Retrieve existing management container
base_container = get_ticket_management_container(ticket_data, self.category, self.config)[0]
# Build new container merging mention and existing sections
combined_container = disnake.ui.Container(mention_section, *base_container.children)
await ticket_channel.send(components=[combined_container])
# Log
if self.config.log_channel_id:
log_channel = guild.get_channel(self.config.log_channel_id)
if log_channel:
2026-05-06 17:07:09 +02:00
log_embed = disnake.Embed(
title="Nouveau Ticket",
2026-05-06 17:07:09 +02:00
color=disnake.Color.green(),
timestamp=datetime.now()
)
log_embed.add_field(name="Utilisateur", value=user.mention, inline=True)
log_embed.add_field(name="Catégorie", value=self.category.name, inline=True)
log_embed.add_field(name="Salon", value=ticket_channel.mention, inline=True)
await log_channel.send(embed=log_embed)
await interaction.followup.send(f"Ticket créé: {ticket_channel.mention}", ephemeral=True)
class RecruitmentQuestionsModal(disnake.ui.Modal):
"""Modal pour les questions de recrutement"""
def __init__(self, bot, config, category: TicketCategory):
self.bot = bot
self.config = config
self.category = category
# Construire dynamiquement les inputs de questions
components = []
self.question_inputs = {}
for i, question in enumerate(category.questions):
input_field = disnake.ui.TextInput(
label=f"Question {i+1}",
custom_id=f"recruitment_q_{i}",
placeholder=question,
required=True,
max_length=1000,
style=disnake.TextInputStyle.paragraph
)
self.question_inputs[f"recruitment_q_{i}"] = input_field
components.append(input_field)
super().__init__(title=f"Recrutement: {category.name}", components=components)
async def callback(self, interaction: disnake.ModalInteraction):
await interaction.response.defer(ephemeral=True)
try:
# Collecter les réponses
responses = {}
for key, input_field in self.question_inputs.items():
responses[key] = interaction.text_values.get(key, "")
# Créer le ticket avec les réponses stockées
await self._create_recruitment_ticket(interaction, responses)
except Exception as e:
await interaction.followup.send(f"Erreur lors de la création du ticket: {e}", ephemeral=True)
async def _create_recruitment_ticket(self, interaction: disnake.Interaction, responses: Dict[str, str]):
"""Crée le ticket de recrutement"""
guild = interaction.guild
user = interaction.user
storage = get_storage()
# Build permissions
overwrites = {
guild.default_role: disnake.PermissionOverwrite(read_messages=False),
user: disnake.PermissionOverwrite(read_messages=True, send_messages=True),
guild.me: disnake.PermissionOverwrite(read_messages=True, send_messages=True)
}
# Add staff roles (category-specific roles override global ones)
staff_roles_to_add = self.category.staff_roles if self.category.staff_roles else self.config.staff_roles
for role_id in staff_roles_to_add:
try:
role = guild.get_role(int(role_id))
if role and role not in overwrites:
overwrites[role] = disnake.PermissionOverwrite(read_messages=True, send_messages=True)
except (ValueError, TypeError):
continue
# Create channel name
safe_username = "".join(c for c in user.display_name if not c.isdigit()).strip()
if not safe_username:
safe_username = "user"
safe_category = "".join(c for c in self.category.name if not c.isdigit()).strip()
channel_name = f"recrutement-{safe_username}"
try:
category_channel = None
if self.category.discord_category_id:
try:
category_channel = guild.get_channel(int(self.category.discord_category_id))
if category_channel and not isinstance(category_channel, disnake.CategoryChannel):
category_channel = None
except (ValueError, TypeError):
pass
if not category_channel and self.config.default_category:
try:
category_channel = guild.get_channel(int(self.config.default_category))
if category_channel and not isinstance(category_channel, disnake.CategoryChannel):
category_channel = None
except (ValueError, TypeError):
pass
ticket_channel = await guild.create_text_channel(
name=channel_name,
category=category_channel,
overwrites=overwrites,
topic=f"Recrutement de {user} | Catégorie: {self.category.name}"
)
except Exception as e:
await interaction.followup.send(f"Erreur: {e}", ephemeral=True)
return
# Create ticket data
ticket_data = TicketData(
channel_id=ticket_channel.id,
guild_id=guild.id,
user_id=user.id,
category_id=self.category.id,
priority=Priority.NORMAL,
status=TicketStatus.OPEN,
reason="Recrutement",
recruitment_responses=responses
)
storage.save_ticket(ticket_data)
# Build mentions for staff
mentions = [user.mention]
for role_id in staff_roles_to_add:
mentions.append(f"<@&{role_id}>")
mention_content = " ".join(mentions)
# Send welcome message in ticket channel
welcome_text = f"Bienvenue {user.mention} !\n\nMerci de votre intérêt pour rejoindre notre équipe.\n\n"
welcome_text += "Nos responsables vont analyser vos réponses et vous contacteront.\n\n"
# Afficher les questions et réponses
welcome_text += "**Vos réponses:**\n"
for i, question in enumerate(self.category.questions):
key = f"recruitment_q_{i}"
response = responses.get(key, "Non répondu")
welcome_text += f"> **{question}**\n> {response}\n\n"
# Create container with management buttons and recruitment responses
sections = [
disnake.ui.TextDisplay(content=welcome_text),
disnake.ui.Separator(divider=True)
]
# Add management buttons if staff
sections.append(
disnake.ui.Section(
"Actions",
accessory=disnake.ui.Button(
label="🔒 Fermer (Accepté/Refusé)",
style=disnake.ButtonStyle.green,
custom_id="recruitment_close"
)
)
)
container = disnake.ui.Container(*sections)
await ticket_channel.send(components=[disnake.ui.Container(
disnake.ui.TextDisplay(content=mention_content),
disnake.ui.Separator(divider=True),
*sections
)])
# Envoyer les réponses dans le salon staff si configuré
if self.category.responses_channel_id:
try:
responses_channel = guild.get_channel(self.category.responses_channel_id)
if responses_channel:
embed = disnake.Embed(
title=f"📝 Nouveau Candidature - {user}",
description=f"**Candidature dans** {ticket_channel.mention}",
color=disnake.Color.blue(),
timestamp=datetime.now()
)
embed.set_thumbnail(url=user.display_avatar.url)
for i, question in enumerate(self.category.questions):
key = f"recruitment_q_{i}"
response = responses.get(key, "Non répondu")
embed.add_field(
name=f"Q{i+1}: {question}",
value=response,
inline=False
)
embed.add_field(name="Candidat", value=user.mention, inline=True)
embed.add_field(name="Discord ID", value=str(user.id), inline=True)
await responses_channel.send(embed=embed)
except Exception as e:
print(f"Error sending recruitment responses to staff channel: {e}")
# Log
if self.config.log_channel_id:
log_channel = guild.get_channel(self.config.log_channel_id)
if log_channel:
log_embed = disnake.Embed(
title="🎓 Nouveau Ticket de Recrutement",
color=disnake.Color.purple(),
timestamp=datetime.now()
)
log_embed.add_field(name="Utilisateur", value=user.mention, inline=True)
log_embed.add_field(name="Catégorie", value=self.category.name, inline=True)
log_embed.add_field(name="Salon", value=ticket_channel.mention, inline=True)
await log_channel.send(embed=log_embed)
await interaction.followup.send(f"Ticket créé: {ticket_channel.mention}", ephemeral=True)
2026-05-06 17:07:09 +02:00
class SetCategoryModal(disnake.ui.Modal):
2026-02-07 14:42:39 +01:00
"""Modal pour définir la catégorie Discord où les tickets seront créés"""
def __init__(self, bot, storage, config: GuildTicketConfig):
self.category_input = disnake.ui.TextInput(
label="Catégorie Discord",
placeholder="Entrez l'ID de la catégorie...",
required=True
)
super().__init__(title="Définir la Catégorie", components=[self.category_input])
2026-02-07 14:42:39 +01:00
self.bot = bot
self.storage = storage
self.config = config
async def callback(self, interaction: disnake.ModalInteraction):
cat_id = self.category_input.value.strip()
2026-02-07 14:42:39 +01:00
# Verify it's a valid ID
if not cat_id.isdigit():
await interaction.response.send_message("❌ Veuillez entrer un ID de catégorie valide (que des chiffres).", ephemeral=True)
return
# Check if category exists
category = interaction.guild.get_channel(int(cat_id))
2026-05-06 17:07:09 +02:00
if not category or not isinstance(category, disnake.CategoryChannel):
2026-02-07 14:42:39 +01:00
await interaction.response.send_message("❌ Catégorie introuvable ou ce n'est pas une catégorie.", ephemeral=True)
return
self.config.default_category = cat_id
self.storage.save_config(interaction.guild_id, self.config)
await interaction.response.send_message(f"📁 Les tickets seront désormais créés dans la catégorie **{category.name}**!", ephemeral=True)
def get_ticket_management_container(ticket: TicketData, category, config: GuildTicketConfig) -> list:
"""Helper V2 pour générer les actions de gestion d'un ticket"""
sections = []
# Informations du ticket (Titre, description)
sections.append(disnake.ui.TextDisplay(content=f"# {category.emoji} {category.name}"))
sections.append(disnake.ui.Separator())
status_emoji = "🟢" if ticket.status == TicketStatus.OPEN else ("🟡" if ticket.status == TicketStatus.CLAIMED else "🔴")
status_text = "Ouvert" if ticket.status == TicketStatus.OPEN else ("Réclamé" if ticket.status == TicketStatus.CLAIMED else "Fermé")
content = f"Bienvenue <@{ticket.user_id}>!\n\nVotre demande sera traitée sous peu par notre équipe.\n\n"
content += f"**Motif:** {ticket.reason or 'Aucun'}\n"
content += f"**Priorité:** {ticket.priority.value.capitalize()}\n"
content += f"**Statut:** {status_emoji} {status_text}"
if ticket.claimed_by:
content += f"\n**Pris en charge par:** <@{ticket.claimed_by}>"
sections.append(disnake.ui.TextDisplay(content=content))
sections.append(disnake.ui.Separator())
# 1. Gestion Staff
if ticket.status == TicketStatus.OPEN and config.claim_enabled and category and category.allow_claims:
sections.append(disnake.ui.Section("🤝 Le staff peut prendre en charge le ticket", accessory=disnake.ui.Button(label="Réclamer", style=disnake.ButtonStyle.primary, custom_id="ticket_claim")))
elif ticket.status == TicketStatus.CLAIMED:
sections.append(disnake.ui.Section("🤝 Gérer la réclamation", accessory=disnake.ui.Button(label="Abandonner", style=disnake.ButtonStyle.danger, custom_id="ticket_unclaim")))
sections.append(disnake.ui.Section("🤝 Transférer", accessory=disnake.ui.Button(label="Transférer", style=disnake.ButtonStyle.secondary, custom_id="ticket_claim")))
# 2. Séparateur
if len(sections) > 0:
sections.append(disnake.ui.Separator(divider=True))
# 3. Actions générales
if ticket.status in [TicketStatus.OPEN, TicketStatus.CLAIMED]:
sections.append(disnake.ui.Section("🔒 Fermer le ticket", accessory=disnake.ui.Button(label="Fermer", style=disnake.ButtonStyle.danger, custom_id="ticket_close")))
elif ticket.status == TicketStatus.CLOSED:
sections.append(disnake.ui.Section("🔓 Rouvrir le ticket", accessory=disnake.ui.Button(label="Rouvrir", style=disnake.ButtonStyle.success, custom_id="ticket_reopen")))
sections.append(disnake.ui.Section("📜 Générer un transcript", accessory=disnake.ui.Button(label="Transcript", style=disnake.ButtonStyle.secondary, custom_id="ticket_transcript")))
return [disnake.ui.Container(*sections)]
2026-05-06 17:07:09 +02:00
class TicketManagementView(disnake.ui.View):
"""Gestion du ticket (dans le salon du ticket) - Uniquement pour les callbacks désormais"""
def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.ticket = ticket
self.category = config.get_category(ticket.category_id)
2026-05-06 17:07:09 +02:00
async def _close_callback(self, interaction: disnake.Interaction):
2026-01-14 21:32:20 +01:00
"""Demande confirmation avant de fermer le ticket"""
kuby_logger.info(
f"[TicketSystem] _close_callback: "
f"guild={getattr(interaction.guild,'id',None)} "
f"channel={getattr(interaction.channel,'id',None)} "
f"user={interaction.user.id} "
f"ticket_user={getattr(self.ticket,'user_id',None)}"
)
2026-01-14 21:32:20 +01:00
# Vérifier si l'utilisateur peut fermer le ticket
can_close = (
interaction.user.id == self.ticket.user_id or
self._is_staff(interaction)
)
if not can_close:
await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True)
return
category = self.config.get_category(self.ticket.category_id)
duration = self.ticket.duration_minutes
duration_text = f"{duration:.1f} min" if duration else "N/A"
# ✅ V2 components (disnake>=2.11): ne pas utiliser embed/content avec components V2
# (version minimale pour éviter tout problème de parsing)
# Confirmation UI en V2 (boutons réels) - UI courte.
# Objectif: éviter les erreurs Invalid Form Body et garder un contenu minimal.
components = [
disnake.ui.Container(
disnake.ui.Section(
"🔒 Confirmer la fermeture",
accessory=disnake.ui.Button(
label="✅ Confirmer",
style=disnake.ButtonStyle.green,
custom_id="close_confirm"
)
),
disnake.ui.Separator(divider=True),
disnake.ui.Section(
"❌ Annuler",
accessory=disnake.ui.Button(
label="❌ Annuler",
style=disnake.ButtonStyle.red,
custom_id="close_cancel"
)
)
)
]
# On ne met PAS de view= ici (sinon Disnake lève TypeError).
# Les boutons V2 de close_confirm/close_cancel seront routés via on_interaction.
await interaction.response.send_message(components=components, flags=disnake.MessageFlags(is_components_v2=True), ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _transcript_callback(self, interaction: disnake.Interaction):
"""Génère le transcript"""
if not self._is_staff(interaction):
await interaction.response.send_message("Réservé au staff.", ephemeral=True)
return
storage = get_storage()
ticket = storage.load_ticket(interaction.channel.id)
if not ticket:
await interaction.response.send_message("Ticket non trouvé.", ephemeral=True)
return
# Collect messages
messages = []
async for msg in interaction.channel.history(limit=1000, oldest_first=True):
if msg.author.bot:
continue
messages.append({
"id": str(msg.id),
"author_id": msg.author.id,
"author_name": str(msg.author),
"content": msg.content,
"timestamp": msg.created_at.isoformat(),
"is_staff": self._is_staff_member(msg.author)
})
# Generate transcript
from .utils.transcript import get_transcript_generator
transcript_gen = get_transcript_generator()
filepath = transcript_gen.save_transcript(ticket, messages, format="html")
if filepath and os.path.exists(filepath):
2026-05-06 17:07:09 +02:00
file = disnake.File(filepath, filename=os.path.basename(filepath))
await interaction.response.send_message("Transcript généré:", file=file, ephemeral=True)
else:
await interaction.response.send_message("Erreur lors de la génération du transcript.", ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _claim_callback(self, interaction: disnake.Interaction):
"""Réclame le ticket"""
if not self._is_staff(interaction):
await interaction.response.send_message("Réservé au staff.", ephemeral=True)
return
2026-05-01 16:16:33 +02:00
if self.ticket.claimed_by == interaction.user.id:
await interaction.response.send_message("Vous avez déjà réclamé ce ticket.", ephemeral=True)
return
2026-05-01 16:16:33 +02:00
old_claimer = self.ticket.claimed_by
self.ticket.claimed_by = interaction.user.id
self.ticket.status = TicketStatus.CLAIMED
self.storage.save_ticket(self.ticket)
# Force UI update V2
try:
await interaction.response.edit_message(components=get_ticket_management_container(self.ticket, self.category, self.config))
except disnake.NotFound:
pass # Message might have been deleted, ignore
2026-05-01 16:16:33 +02:00
if old_claimer:
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
2026-05-01 16:16:33 +02:00
title="🔄 Ticket Transféré",
description=f"Le ticket a été transféré de <@{old_claimer}> à {interaction.user.mention}",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue()
2026-05-01 16:16:33 +02:00
)
else:
embed = TicketEmbedFormatter.ticket_claimed(self.ticket, interaction.user)
await interaction.channel.send(embed=embed)
2026-05-06 17:07:09 +02:00
async def _reopen_callback(self, interaction: disnake.Interaction):
"""Rouvre le ticket"""
if not self._is_staff(interaction):
await interaction.response.send_message("Réservé au staff.", ephemeral=True)
return
self.ticket.status = TicketStatus.OPEN
self.ticket.closed_at = None
self.ticket.claimed_by = None
self.storage.save_ticket(self.ticket)
# Update channel
overwrites = interaction.channel.overwrites.copy()
for target, perms in overwrites.items():
2026-05-06 17:07:09 +02:00
if isinstance(target, disnake.Member) and target.id != interaction.guild.me.id:
overwrites[target] = disnake.PermissionOverwrite(
read_messages=True,
send_messages=True
)
await interaction.channel.edit(
name=interaction.channel.name.replace("closed-", ""),
overwrites=overwrites
)
# Update UI V2
try:
await interaction.response.edit_message(components=get_ticket_management_container(self.ticket, self.category, self.config))
except disnake.NotFound:
pass
embed = TicketEmbedFormatter.ticket_reopened(self.ticket, interaction.user)
await interaction.channel.send(embed=embed)
# We might have edited the message, so we just send follow up if we can't edit
# if not interaction.response.is_done():
# await interaction.response.send_message("Ticket rouvert!", ephemeral=True)
async def _unclaim_callback(self, interaction: disnake.Interaction):
"""Abandonne le ticket"""
if not self._is_staff(interaction):
await interaction.response.send_message("Réservé au staff.", ephemeral=True)
return
if self.ticket.claimed_by != interaction.user.id:
await interaction.response.send_message("Vous n'êtes pas celui qui a réclamé ce ticket.", ephemeral=True)
return
self.ticket.claimed_by = None
self.ticket.status = TicketStatus.OPEN
self.storage.save_ticket(self.ticket)
# Update UI V2
try:
await interaction.response.edit_message(components=get_ticket_management_container(self.ticket, self.category, self.config))
except disnake.NotFound:
pass
embed = TicketEmbedFormatter.ticket_unclaimed(self.ticket, interaction.user)
await interaction.channel.send(embed=embed)
2026-05-06 17:07:09 +02:00
def _is_staff(self, interaction: disnake.Interaction) -> bool:
2026-05-01 16:16:33 +02:00
return is_staff(interaction, self.config, self.category)
2026-01-14 21:32:20 +01:00
def _is_staff_member(self, member) -> bool:
2026-05-01 16:16:33 +02:00
"""Check if a specific member is staff, considering category roles"""
if not member:
return False
# Handle cases where member is a User (left the guild)
2026-05-06 17:07:09 +02:00
if not isinstance(member, disnake.Member):
2026-05-01 16:16:33 +02:00
return member.id in self.config.staff_users
2026-01-14 21:32:20 +01:00
if member.guild_permissions.administrator:
return True
if member.id in self.config.staff_users:
return True
2026-05-01 16:16:33 +02:00
# Check category-specific staff roles first
staff_roles = self.category.staff_roles if self.category and self.category.staff_roles else self.config.staff_roles
for role_id in staff_roles:
try:
role = member.guild.get_role(int(role_id))
if role and role in member.roles:
return True
except (ValueError, TypeError, AttributeError):
continue
2026-01-14 21:32:20 +01:00
return False
2026-05-06 17:07:09 +02:00
class CloseConfirmationView(disnake.ui.View):
2026-01-14 21:32:20 +01:00
"""Demande de confirmation avant la fermeture du ticket"""
def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData):
super().__init__(timeout=60)
self.bot = bot
self.storage = storage
self.config = config
self.ticket = ticket
2026-05-01 16:16:33 +02:00
self.category = config.get_category(ticket.category_id)
2026-01-14 21:32:20 +01:00
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="✅ Confirmer", style=disnake.ButtonStyle.green, custom_id="close_confirm")
async def confirm_callback(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction):
2026-01-14 21:32:20 +01:00
"""L'utilisateur confirme la fermeture"""
# Vérifier les permissions
can_close = (
interaction.user.id == self.ticket.user_id or
self._is_staff(interaction)
)
if not can_close:
await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True)
return
# Show the delay view with countdown (V2 components, not embeds)
components = [
disnake.ui.Container(
disnake.ui.Section(
"# ⏳ Fermeture du Ticket",
accessory=disnake.ui.Button(
label="Fermeture...",
style=disnake.ButtonStyle.secondary,
custom_id="close_delay_info"
)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(
content="Le ticket va être fermé dans quelques secondes...\n\n"
"📄 Génération du transcript en cours...\n"
"📝 Envoi aux logs...\n"
"🗑️ Suppression du canal..."
)
)
]
2026-01-14 21:32:20 +01:00
view = CloseDelayView(self.bot, self.storage, self.config, self.ticket)
await interaction.response.send_message(
components=components,
flags=disnake.MessageFlags(is_components_v2=True),
view=view,
ephemeral=True
)
2026-01-14 21:32:20 +01:00
# Start the countdown
await view.start_countdown(interaction)
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="❌ Annuler", style=disnake.ButtonStyle.red, custom_id="close_cancel")
async def cancel_callback(self, button: disnake.ui.Button, interaction: disnake.MessageInteraction):
2026-01-14 21:32:20 +01:00
"""L'utilisateur annule la fermeture"""
await interaction.response.defer()
await interaction.delete_original_response()
2026-05-06 17:07:09 +02:00
def _is_staff(self, interaction: disnake.Interaction) -> bool:
2026-05-01 16:16:33 +02:00
return is_staff(interaction, self.config, self.category)
2026-01-14 21:32:20 +01:00
2026-05-06 17:07:09 +02:00
class CloseDelayView(disnake.ui.View):
2026-01-14 21:32:20 +01:00
"""Affiche le délai d'attente avant suppression du ticket"""
def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData, close_reason: str = ""):
super().__init__(timeout=300)
self.bot = bot
self.storage = storage
self.config = config
self.ticket = ticket
self.close_reason = close_reason
self.category = config.get_category(ticket.category_id)
# Ajouter le bouton Annuler
2026-05-06 17:07:09 +02:00
cancel_btn = disnake.ui.Button(
2026-01-14 21:32:20 +01:00
label="❌ Annuler",
2026-05-06 17:07:09 +02:00
style=disnake.ButtonStyle.red,
2026-01-14 21:32:20 +01:00
custom_id="delay_cancel"
)
cancel_btn.callback = self._cancel_callback
self.add_item(cancel_btn)
2026-05-06 17:07:09 +02:00
async def start_countdown(self, interaction: disnake.Interaction):
2026-01-14 21:32:20 +01:00
"""Démarre le compte à rebours"""
# Désactiver le bouton Annuler
for item in self.children:
item.disabled = True
await interaction.edit_original_response(view=self)
# Étapes du processus
steps = [
("⏳ Préparation...", 1),
("📄 Génération du transcript...", 1),
("📝 Sauvegarde des données...", 1),
("🗑️ Suppression du canal...", 1)
]
for step_text, delay in steps:
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
2026-01-14 21:32:20 +01:00
title="🔒 Fermeture du Ticket",
description=f"**{step_text}**\n\n"
f"Merci pour votre confiance!",
2026-05-06 17:07:09 +02:00
color=disnake.Color.orange()
2026-01-14 21:32:20 +01:00
)
embed.set_footer(text=f"Ticket #{self.ticket.channel_id}")
# IMPORTANT: si la réponse originale a été envoyée avec is_components_v2=True,
# ne pas modifier en embed (sinon API refuse embeds + components_v2).
await interaction.edit_original_response(
components=[disnake.ui.Container(
disnake.ui.Section(
"# 6ed Fermeture du Ticket",
accessory=disnake.ui.Button(
label=step_text,
style=disnake.ButtonStyle.secondary,
custom_id="close_delay_progress"
)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(content=f"{step_text}\n\nMerci pour votre confiance!")
)],
)
2026-01-14 21:32:20 +01:00
await asyncio.sleep(delay)
# Procéder à la fermeture
await self._close_ticket(interaction)
2026-05-06 17:07:09 +02:00
async def _close_ticket(self, interaction: disnake.Interaction):
2026-01-14 21:32:20 +01:00
"""Effectue la fermeture réelle du ticket"""
# Collect messages for transcript
transcript_file = None
try:
messages = []
async for msg in interaction.channel.history(limit=1000, oldest_first=True):
if msg.author.bot:
continue
messages.append({
"id": str(msg.id),
"author_id": msg.author.id,
"author_name": str(msg.author),
"content": msg.content,
"timestamp": msg.created_at.isoformat(),
"is_staff": self._is_staff_member(msg.author)
})
from .utils.transcript import get_transcript_generator
transcript_gen = get_transcript_generator()
transcript_file = transcript_gen.save_transcript(self.ticket, messages, format="html")
except Exception as e:
print(f"Error generating transcript: {e}")
# Close ticket
self.ticket.status = TicketStatus.CLOSED
self.ticket.closed_at = datetime.now()
if self.close_reason:
self.ticket.feedback = self.close_reason
self.storage.save_ticket(self.ticket)
duration = self.ticket.duration_minutes
2026-02-07 14:42:39 +01:00
# Send Feedback Request to User via DM
try:
user = interaction.guild.get_member(self.ticket.user_id)
if user:
from .feedback_view import FeedbackView
2026-05-06 17:07:09 +02:00
dm_embed = disnake.Embed(
2026-02-07 14:42:39 +01:00
title="📝 Votre avis nous intéresse !",
description=f"Votre ticket **#{self.ticket.channel_id}** a été fermé.\nMerci de prendre un moment pour noter la qualité du support.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.green(),
2026-02-07 14:42:39 +01:00
timestamp=datetime.now()
)
dm_embed.add_field(name="Durée du ticket", value=f"{duration:.1f} minutes", inline=True)
if self.close_reason:
dm_embed.add_field(name="Raison de fermeture", value=self.close_reason, inline=False)
2026-05-01 16:16:33 +02:00
# Create a special view with a "Noter le staff" button
2026-02-07 14:42:39 +01:00
view = FeedbackView(self.bot, self.storage, self.ticket)
2026-05-01 16:16:33 +02:00
# Add a button to directly open the feedback form
2026-05-06 17:07:09 +02:00
note_staff_btn = disnake.ui.Button(
2026-05-01 16:16:33 +02:00
label="⭐ Noter le staff",
2026-05-06 17:07:09 +02:00
style=disnake.ButtonStyle.primary,
2026-05-01 16:16:33 +02:00
custom_id="note_staff_direct"
)
note_staff_btn.callback = self._note_staff_callback
view.add_item(note_staff_btn)
2026-02-07 14:42:39 +01:00
await user.send(embed=dm_embed, view=view)
except Exception as e:
print(f"Failed to send feedback DM: {e}")
# Send transcript to log channel
if self.config.log_channel_id:
try:
2026-01-14 21:32:20 +01:00
log_channel = interaction.guild.get_channel(self.config.log_channel_id)
if log_channel:
2026-05-06 17:07:09 +02:00
log_embed = disnake.Embed(
2026-01-14 21:32:20 +01:00
title="📄 Ticket Fermé - Transcript",
description=f"Ticket #{self.ticket.channel_id} fermé par {interaction.user.mention}",
2026-05-06 17:07:09 +02:00
color=disnake.Color.red(),
2026-01-14 21:32:20 +01:00
timestamp=datetime.now()
)
ticket_creator = interaction.guild.get_member(self.ticket.user_id)
creator_mention = ticket_creator.mention if ticket_creator else f"Utilisateur {self.ticket.user_id}"
log_embed.add_field(name="Créateur", value=creator_mention, inline=True)
log_embed.add_field(name="Catégorie", value=self.category.name if self.category else "N/A", inline=True)
log_embed.add_field(name="Durée", value=f"{duration:.1f} min" if duration else "N/A", inline=True)
if self.close_reason:
log_embed.add_field(name="Raison", value=self.close_reason, inline=False)
2026-05-01 16:16:33 +02:00
if transcript_file and os.path.exists(transcript_file):
2026-05-06 17:07:09 +02:00
file = disnake.File(transcript_file, filename=os.path.basename(transcript_file))
2026-05-01 16:16:33 +02:00
await log_channel.send(embed=log_embed, file=file)
else:
log_embed.add_field(name="⚠️ Transcript", value="Le transcript n'a pas pu être généré (l'utilisateur a peut-être quitté le serveur).", inline=False)
await log_channel.send(embed=log_embed)
2026-01-14 21:32:20 +01:00
except Exception as e:
print(f"Error sending transcript to log channel: {e}")
# Delete the ticket channel
try:
await interaction.channel.delete(reason=f"Ticket fermé par {interaction.user}")
except Exception as e:
print(f"Error deleting channel: {e}")
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
2026-01-14 21:32:20 +01:00
title="❌ Erreur",
description="Le ticket a été fermé mais la suppression du salon a échoué.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.red()
2026-01-14 21:32:20 +01:00
)
await interaction.channel.send(embed=embed)
2026-05-01 16:16:33 +02:00
2026-05-06 17:07:09 +02:00
async def _note_staff_callback(self, interaction: disnake.Interaction):
2026-05-01 16:16:33 +02:00
"""Handle direct staff rating button"""
# This would open a direct feedback form
await interaction.response.send_message("Vous pouvez maintenant noter le staff directement !", ephemeral=True)
2026-01-14 21:32:20 +01:00
2026-05-06 17:07:09 +02:00
async def _cancel_callback(self, interaction: disnake.Interaction):
2026-01-14 21:32:20 +01:00
"""Annule la fermeture du ticket pendant le délai"""
await interaction.response.defer()
await interaction.delete_original_response()
# Envoyer un message de confirmation d'annulation
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
2026-01-14 21:32:20 +01:00
title="❌ Fermeture Annulée",
description="La fermeture du ticket a été annulée.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.red()
2026-01-14 21:32:20 +01:00
)
await interaction.channel.send(embed=embed, delete_after=5)
2026-05-06 17:07:09 +02:00
def _is_staff(self, interaction: disnake.Interaction) -> bool:
2026-05-01 16:16:33 +02:00
return is_staff(interaction, self.config, self.category)
2026-01-14 21:32:20 +01:00
def _is_staff_member(self, member) -> bool:
2026-05-01 16:16:33 +02:00
"""Check if a specific member is staff, considering category roles"""
if not member:
return False
# Handle cases where member is a User (left the guild)
2026-05-06 17:07:09 +02:00
if not isinstance(member, disnake.Member):
2026-05-01 16:16:33 +02:00
return member.id in self.config.staff_users
if member.guild_permissions.administrator:
return True
if member.id in self.config.staff_users:
return True
# Check category-specific staff roles first
staff_roles = self.category.staff_roles if self.category and self.category.staff_roles else self.config.staff_roles
for role_id in staff_roles:
try:
role = member.guild.get_role(int(role_id))
if role and role in member.roles:
return True
except (ValueError, TypeError, AttributeError):
continue
return False
2026-05-06 17:07:09 +02:00
class CloseTicketModal(disnake.ui.Modal):
"""Modal pour fermer un ticket"""
def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData):
self.reason_input = disnake.ui.TextInput(
label="Raison de fermeture",
placeholder="Optionnel...",
style=disnake.TextInputStyle.paragraph,
required=False
)
super().__init__(title="Fermer le Ticket", components=[self.reason_input])
self.bot = bot
self.storage = storage
self.config = config
self.ticket = ticket
self.category = config.get_category(ticket.category_id)
async def callback(self, interaction: disnake.ModalInteraction):
# Check permissions
can_close = (
interaction.user.id == self.ticket.user_id or
self._is_staff(interaction)
)
if not can_close:
await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True)
return
# Send ephemeral response to user first (required for modal interactions)
await interaction.response.send_message("✅ Fermeture du ticket en cours...", ephemeral=True)
# Send confirmation message in the ticket channel (V2 components, not embeds)
confirm_components = [
disnake.ui.Container(
disnake.ui.Section(
"# ⏳ Fermeture du Ticket",
accessory=disnake.ui.Button(
label="Fermeture...",
style=disnake.ButtonStyle.secondary,
custom_id="close_ticket_modal_info"
)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(
content="Le ticket va être fermé dans quelques secondes...\n\n"
"📄 Génération du transcript en cours...\n"
"📝 Envoi aux logs...\n"
"🗑️ Suppression du canal..."
)
)
]
await interaction.channel.send(
components=confirm_components,
flags=disnake.MessageFlags(is_components_v2=True)
)
# Add small delay for user to see the confirmation
import asyncio
await asyncio.sleep(3)
# Generate transcript before closing
transcript_file = None
try:
# Collect messages
messages = []
async for msg in interaction.channel.history(limit=1000, oldest_first=True):
if msg.author.bot:
continue
messages.append({
"id": str(msg.id),
"author_id": msg.author.id,
"author_name": str(msg.author),
"content": msg.content,
"timestamp": msg.created_at.isoformat(),
"is_staff": self._is_staff_member(msg.author)
})
# Generate transcript
from .utils.transcript import get_transcript_generator
transcript_gen = get_transcript_generator()
transcript_file = transcript_gen.save_transcript(self.ticket, messages, format="html")
except Exception as e:
print(f"Error generating transcript: {e}")
# Close ticket
self.ticket.status = TicketStatus.CLOSED
self.ticket.closed_at = datetime.now()
self.storage.save_ticket(self.ticket)
duration = self.ticket.duration_minutes
# Send transcript to log channel if configured
2026-05-01 16:16:33 +02:00
if self.config.log_channel_id:
try:
log_channel = interaction.guild.get_channel(self.config.log_channel_id)
if log_channel:
# Create log embed
2026-05-06 17:07:09 +02:00
log_embed = disnake.Embed(
title="📄 Ticket Fermé - Transcript",
description=f"Ticket #{self.ticket.channel_id} fermé par {interaction.user.mention}",
2026-05-06 17:07:09 +02:00
color=disnake.Color.red(),
timestamp=datetime.now()
)
# Get ticket creator
2026-05-19 18:50:16 +02:00
# Fetch le créateur pour affichage fiable même si absent du cache
ticket_creator = interaction.guild.get_member(self.ticket.user_id)
2026-05-19 18:50:16 +02:00
if not ticket_creator:
try:
ticket_creator = await interaction.guild.fetch_member(self.ticket.user_id)
except Exception:
ticket_creator = None
creator_mention = ticket_creator.mention if ticket_creator else f"Utilisateur {self.ticket.user_id}"
log_embed.add_field(name="Créateur", value=creator_mention, inline=True)
log_embed.add_field(name="Catégorie", value=self.category.name if self.category else "N/A", inline=True)
log_embed.add_field(name="Durée", value=f"{duration:.1f} min" if duration else "N/A", inline=True)
if self.reason_input.value:
log_embed.add_field(name="Raison de fermeture", value=self.reason_input.value, inline=False)
# Send transcript file
2026-05-01 16:16:33 +02:00
if transcript_file and os.path.exists(transcript_file):
2026-05-06 17:07:09 +02:00
file = disnake.File(transcript_file, filename=os.path.basename(transcript_file))
2026-05-01 16:16:33 +02:00
await log_channel.send(embed=log_embed, file=file)
else:
log_embed.add_field(name="⚠️ Transcript", value="Le transcript n'a pas pu être généré (l'utilisateur a peut-être quitté le serveur).", inline=False)
await log_channel.send(embed=log_embed)
except Exception as e:
print(f"Error sending transcript to log channel: {e}")
# Delete the ticket channel
try:
await interaction.channel.delete(reason=f"Ticket fermé par {interaction.user}")
except Exception as e:
print(f"Error deleting channel: {e}")
# Fallback: just mark as closed if deletion fails
await interaction.followup.send("Erreur lors de la suppression du canal, mais le ticket a été fermé.", ephemeral=True)
2026-05-06 17:07:09 +02:00
def _is_staff(self, interaction: disnake.Interaction) -> bool:
2026-05-01 16:16:33 +02:00
return is_staff(interaction, self.config, self.category)
def _is_staff_member(self, member) -> bool:
"""Check if a specific member is staff, considering category roles"""
if not member:
return False
# Handle cases where member is a User (left the guild)
2026-05-06 17:07:09 +02:00
if not isinstance(member, disnake.Member):
2026-05-01 16:16:33 +02:00
return member.id in self.config.staff_users
if member.guild_permissions.administrator:
return True
if member.id in self.config.staff_users:
return True
staff_roles = self.category.staff_roles if self.category and self.category.staff_roles else self.config.staff_roles
for role_id in staff_roles:
try:
role = member.guild.get_role(int(role_id))
if role and role in member.roles:
return True
except (ValueError, TypeError, AttributeError):
continue
return False
2026-05-06 17:07:09 +02:00
class TicketSetupView(disnake.ui.View):
"""Panel de configuration complet du système de tickets"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
# Toggle enabled
2026-05-06 17:07:09 +02:00
toggle_btn = disnake.ui.Button(
style=disnake.ButtonStyle.green if config.enabled else disnake.ButtonStyle.red,
label="🔄 Activer/Désactiver",
custom_id="setup_toggle"
)
toggle_btn.callback = self._toggle_callback
self.add_item(toggle_btn)
# Staff roles
2026-05-06 17:07:09 +02:00
roles_btn = disnake.ui.Button(
style=disnake.ButtonStyle.primary,
label="👥 Rôles Staff",
custom_id="setup_roles"
)
roles_btn.callback = self._roles_callback
self.add_item(roles_btn)
# Config roles
2026-05-06 17:07:09 +02:00
config_roles_btn = disnake.ui.Button(
style=disnake.ButtonStyle.secondary,
label="🔑 Rôles Config",
custom_id="setup_config_roles"
)
config_roles_btn.callback = self._config_roles_callback
self.add_item(config_roles_btn)
# Categories
2026-05-06 17:07:09 +02:00
cat_btn = disnake.ui.Button(
style=disnake.ButtonStyle.secondary,
label="📂 Catégories",
custom_id="setup_categories"
)
cat_btn.callback = self._categories_callback
self.add_item(cat_btn)
# Log channel
2026-05-06 17:07:09 +02:00
log_btn = disnake.ui.Button(
style=disnake.ButtonStyle.secondary,
label="📝 Canal de Logs",
custom_id="setup_log_channel"
)
log_btn.callback = self._log_channel_callback
self.add_item(log_btn)
# Advanced settings
2026-05-06 17:07:09 +02:00
advanced_btn = disnake.ui.Button(
style=disnake.ButtonStyle.secondary,
label="⚙️ Paramètres Avancés",
custom_id="setup_advanced"
)
advanced_btn.callback = self._advanced_callback
self.add_item(advanced_btn)
2026-05-06 17:07:09 +02:00
async def _toggle_callback(self, interaction: disnake.Interaction):
self.config.enabled = not self.config.enabled
self.storage.save_config(interaction.guild_id, self.config)
status = "activé" if self.config.enabled else "désactivé"
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="🔄 État du Système",
description=f"Le système de tickets a été **{status}**!",
2026-05-06 17:07:09 +02:00
color=disnake.Color.green() if self.config.enabled else disnake.Color.red()
)
await interaction.response.send_message(embed=embed, ephemeral=True)
# Update button
for item in self.children:
if item.custom_id == "setup_toggle":
2026-05-06 17:07:09 +02:00
item.style = disnake.ButtonStyle.green if self.config.enabled else disnake.ButtonStyle.red
await interaction.message.edit(view=self)
2026-05-06 17:07:09 +02:00
async def _roles_callback(self, interaction: disnake.Interaction):
view = StaffRolesSetupView(self.bot, self.storage, self.config, interaction.guild)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="👥 Configuration des Rôles Staff",
description="Définissez les rôles qui peuvent gérer les tickets et accéder aux fonctionnalités avancées.\n\n"
"**Permissions des rôles staff:**\n"
"• Gérer les tickets (réclamer, fermer, etc.)\n"
"• Accéder aux analytics et statistiques\n"
"• Générer des transcripts\n"
"• Fermer les tickets des autres",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue()
)
role_mentions = []
for role_id in self.config.staff_roles:
role = interaction.guild.get_role(int(role_id))
if role:
role_mentions.append(role.mention)
embed.add_field(
name="Rôles Staff Actuels",
value=", ".join(role_mentions) if role_mentions else "❌ Aucun rôle configuré",
inline=False
)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _categories_callback(self, interaction: disnake.Interaction):
view = CategoriesSetupView(self.bot, self.storage, self.config)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="📂 Gestion des Catégories",
description="Créez et gérez les catégories de tickets pour organiser vos demandes.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue()
)
if self.config.categories:
cat_list = []
for cat in self.config.categories[:10]: # Limit to 10 for display
cat_list.append(f"{cat.emoji} {cat.name}")
embed.add_field(
name=f"Catégories ({len(self.config.categories)})",
value="\n".join(cat_list),
inline=False
)
else:
embed.add_field(
name="Aucune catégorie",
value="Créez votre première catégorie pour commencer!",
inline=False
)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _config_roles_callback(self, interaction: disnake.Interaction):
view = ConfigRolesSetupView(self.bot, self.storage, self.config, interaction.guild)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="🔑 Configuration des Rôles Config",
description="Définissez les rôles qui peuvent accéder au panel de configuration du système de tickets.\n\n"
"**Permissions des rôles config:**\n"
"• Accéder à la commande /ticketconfig\n"
"• Modifier tous les paramètres du système\n"
"• Gérer les catégories et rôles staff",
2026-05-06 17:07:09 +02:00
color=disnake.Color.purple()
)
role_mentions = []
for role_id in self.config.config_roles:
role = interaction.guild.get_role(int(role_id))
if role:
role_mentions.append(role.mention)
embed.add_field(
name="Rôles Config Actuels",
value=", ".join(role_mentions) if role_mentions else "❌ Aucun rôle configuré",
inline=False
)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _log_channel_callback(self, interaction: disnake.Interaction):
modal = SetChannelModal(self.bot, self.storage, self.config, "log")
await interaction.response.send_modal(modal)
2026-05-06 17:07:09 +02:00
async def _advanced_callback(self, interaction: disnake.Interaction):
view = AdvancedSetupView(self.bot, self.storage, self.config)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="⚙️ Paramètres Avancés",
description="Configuration fine du système de tickets.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.purple()
)
embed.add_field(
name="Paramètres Actuels",
value=f"Système de réclamation: {'✅ Activé' if self.config.claim_enabled else '❌ Désactivé'}\n"
f"Sondages: {'✅ Activé' if self.config.survey_enabled else '❌ Désactivé'}\n"
f"Limite par utilisateur: {self.config.max_tickets_per_user}",
inline=False
)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
2026-05-06 17:07:09 +02:00
class StaffRolesSetupView(disnake.ui.View):
"""Configuration des rôles staff"""
2026-05-06 17:07:09 +02:00
def __init__(self, bot, storage, config: GuildTicketConfig, guild: disnake.Guild):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.guild = guild
self._build_select()
def _build_select(self):
"""Build the role select menu"""
options = []
for role in self.guild.roles:
if role.id != self.guild.id: # Exclude @everyone
options.append(
2026-05-06 17:07:09 +02:00
disnake.SelectOption(
label=role.name,
value=str(role.id),
default=str(role.id) in self.config.staff_roles
)
)
if options:
2026-05-06 17:07:09 +02:00
select = disnake.ui.Select(
placeholder="Sélectionnez les rôles staff...",
options=options[:25],
min_values=0,
max_values=min(25, len(options))
)
select.callback = self._select_callback
self.add_item(select)
2026-05-06 17:07:09 +02:00
async def _select_callback(self, interaction: disnake.Interaction):
selected = list(interaction.data.get("values", []))
self.config.staff_roles = selected
self.storage.save_config(interaction.guild_id, self.config)
role_mentions = []
for role_id in selected:
role = interaction.guild.get_role(int(role_id))
if role:
role_mentions.append(role.mention)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="✅ Rôles Staff Mis à Jour",
description=f"**{len(selected)}** rôle(s) configuré(s) comme staff.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.green()
)
embed.add_field(
name="Rôles Staff",
value=", ".join(role_mentions) if role_mentions else "Aucun",
inline=False
)
await interaction.response.send_message(embed=embed, ephemeral=True)
2026-05-06 17:07:09 +02:00
class ConfigRolesSetupView(disnake.ui.View):
"""Configuration des rôles config"""
2026-05-06 17:07:09 +02:00
def __init__(self, bot, storage, config: GuildTicketConfig, guild: disnake.Guild):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.guild = guild
self._build_select()
def _build_select(self):
"""Build the role select menu"""
options = []
for role in self.guild.roles:
if role.id != self.guild.id: # Exclude @everyone
options.append(
2026-05-06 17:07:09 +02:00
disnake.SelectOption(
label=role.name,
value=str(role.id),
default=str(role.id) in self.config.config_roles
)
)
if options:
2026-05-06 17:07:09 +02:00
select = disnake.ui.Select(
placeholder="Sélectionnez les rôles config...",
options=options[:25],
min_values=0,
max_values=min(25, len(options))
)
select.callback = self._select_callback
self.add_item(select)
2026-05-06 17:07:09 +02:00
async def _select_callback(self, interaction: disnake.Interaction):
selected = list(interaction.data.get("values", []))
self.config.config_roles = selected
self.storage.save_config(interaction.guild_id, self.config)
role_mentions = []
for role_id in selected:
role = interaction.guild.get_role(int(role_id))
if role:
role_mentions.append(role.mention)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="✅ Rôles Config Mis à Jour",
description=f"**{len(selected)}** rôle(s) configuré(s) pour accéder à la configuration.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.green()
)
embed.add_field(
name="Rôles Config",
value=", ".join(role_mentions) if role_mentions else "Aucun",
inline=False
)
embed.add_field(
name="Permissions",
value="• Accéder à /ticketconfig\n• Modifier la configuration\n• Gérer les catégories et rôles",
inline=False
)
await interaction.response.send_message(embed=embed, ephemeral=True)
2026-05-06 17:07:09 +02:00
class CategoriesSetupView(disnake.ui.View):
"""Gestion des catégories"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
# Add category
2026-05-06 17:07:09 +02:00
add_btn = disnake.ui.Button(
style=disnake.ButtonStyle.green,
label=" Ajouter",
custom_id="cat_add"
)
add_btn.callback = self._add_callback
self.add_item(add_btn)
# Remove category (only if categories exist)
if config.categories:
2026-05-06 17:07:09 +02:00
remove_btn = disnake.ui.Button(
style=disnake.ButtonStyle.red,
label=" Supprimer",
custom_id="cat_remove"
)
remove_btn.callback = self._remove_callback
self.add_item(remove_btn)
2026-05-06 17:07:09 +02:00
async def _add_callback(self, interaction: disnake.Interaction):
modal = AddCategoryModal(self.bot, self.storage, self.config)
await interaction.response.send_modal(modal)
2026-05-06 17:07:09 +02:00
async def _remove_callback(self, interaction: disnake.Interaction):
if not self.config.categories:
await interaction.response.send_message("Aucune catégorie à supprimer.", ephemeral=True)
return
# Create select with current categories
options = []
for cat in self.config.categories[:25]: # Discord limit
options.append(
2026-05-06 17:07:09 +02:00
disnake.SelectOption(
label=cat.name,
value=cat.id,
emoji=cat.emoji
)
)
2026-05-06 17:07:09 +02:00
select = disnake.ui.Select(
placeholder="Sélectionnez une catégorie à supprimer...",
options=options
)
select.callback = self._remove_select_callback
2026-05-06 17:07:09 +02:00
view = disnake.ui.View()
view.add_item(select)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="Supprimer une Catégorie",
description="Sélectionnez la catégorie à supprimer:",
2026-05-06 17:07:09 +02:00
color=disnake.Color.red()
)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _remove_select_callback(self, interaction: disnake.Interaction):
cat_id = interaction.data["values"][0]
# Remove category
self.config.categories = [cat for cat in self.config.categories if cat.id != cat_id]
self.storage.save_config(interaction.guild_id, self.config)
await interaction.response.send_message("✅ Catégorie supprimée!", ephemeral=True)
2026-05-06 17:07:09 +02:00
class AdvancedSetupView(disnake.ui.View):
"""Paramètres avancés"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
# Toggle claim system
2026-05-06 17:07:09 +02:00
claim_btn = disnake.ui.Button(
style=disnake.ButtonStyle.green if config.claim_enabled else disnake.ButtonStyle.red,
label=f"{'' if config.claim_enabled else ''} Système de Réclamation",
custom_id="advanced_claim"
)
claim_btn.callback = self._claim_callback
self.add_item(claim_btn)
# Toggle survey system
2026-05-06 17:07:09 +02:00
survey_btn = disnake.ui.Button(
style=disnake.ButtonStyle.green if config.survey_enabled else disnake.ButtonStyle.red,
label=f"{'' if config.survey_enabled else ''} Sondages",
custom_id="advanced_survey"
)
survey_btn.callback = self._survey_callback
self.add_item(survey_btn)
# Max tickets per user
2026-05-06 17:07:09 +02:00
max_btn = disnake.ui.Button(
style=disnake.ButtonStyle.secondary,
label=f"📊 Limite: {config.max_tickets_per_user}",
custom_id="advanced_max"
)
max_btn.callback = self._max_callback
self.add_item(max_btn)
2026-05-06 17:07:09 +02:00
async def _claim_callback(self, interaction: disnake.Interaction):
self.config.claim_enabled = not self.config.claim_enabled
self.storage.save_config(interaction.guild_id, self.config)
status = "activé" if self.config.claim_enabled else "désactivé"
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="🔄 Système de Réclamation",
description=f"Le système de réclamation a été **{status}**!",
2026-05-06 17:07:09 +02:00
color=disnake.Color.green() if self.config.claim_enabled else disnake.Color.red()
)
await interaction.response.send_message(embed=embed, ephemeral=True)
# Update button
for item in self.children:
if item.custom_id == "advanced_claim":
2026-05-06 17:07:09 +02:00
item.style = disnake.ButtonStyle.green if self.config.claim_enabled else disnake.ButtonStyle.red
item.label = f"{'' if self.config.claim_enabled else ''} Système de Réclamation"
await interaction.message.edit(view=self)
2026-05-06 17:07:09 +02:00
async def _survey_callback(self, interaction: disnake.Interaction):
self.config.survey_enabled = not self.config.survey_enabled
self.storage.save_config(interaction.guild_id, self.config)
status = "activé" if self.config.survey_enabled else "désactivé"
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="🔄 Système de Sondages",
description=f"Le système de sondages a été **{status}**!",
2026-05-06 17:07:09 +02:00
color=disnake.Color.green() if self.config.survey_enabled else disnake.Color.red()
)
await interaction.response.send_message(embed=embed, ephemeral=True)
# Update button
for item in self.children:
if item.custom_id == "advanced_survey":
2026-05-06 17:07:09 +02:00
item.style = disnake.ButtonStyle.green if self.config.survey_enabled else disnake.ButtonStyle.red
item.label = f"{'' if self.config.survey_enabled else ''} Sondages"
await interaction.message.edit(view=self)
2026-05-06 17:07:09 +02:00
async def _max_callback(self, interaction: disnake.Interaction):
modal = MaxTicketsModal(self.bot, self.storage, self.config)
await interaction.response.send_modal(modal)
2026-05-06 17:07:09 +02:00
class MaxTicketsModal(disnake.ui.Modal):
"""Modal pour définir la limite de tickets par utilisateur"""
def __init__(self, bot, storage, config: GuildTicketConfig):
2026-05-06 17:07:09 +02:00
self.max_tickets = disnake.ui.TextInput(
label="Nombre maximum de tickets ouverts par utilisateur",
placeholder="5",
default=str(config.max_tickets_per_user),
required=True,
max_length=2
)
super().__init__(title="Limite de Tickets", components=[self.max_tickets])
self.bot = bot
self.storage = storage
self.config = config
async def callback(self, interaction: disnake.ModalInteraction):
try:
max_tickets = int(self.max_tickets.value)
if max_tickets < 1 or max_tickets > 50:
await interaction.response.send_message("La limite doit être entre 1 et 50.", ephemeral=True)
return
self.config.max_tickets_per_user = max_tickets
self.storage.save_config(interaction.guild_id, self.config)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="✅ Limite Mise à Jour",
description=f"La limite de tickets par utilisateur est maintenant de **{max_tickets}**.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.green()
)
await interaction.response.send_message(embed=embed, ephemeral=True)
except ValueError:
await interaction.response.send_message("Veuillez entrer un nombre valide.", ephemeral=True)
2026-05-06 17:07:09 +02:00
class ConfigPanelView(disnake.ui.View):
"""Panel de configuration"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
# Toggle enabled
2026-05-06 17:07:09 +02:00
toggle_btn = disnake.ui.Button(
style=disnake.ButtonStyle.green if config.enabled else disnake.ButtonStyle.red,
label="Activer/Désactiver",
custom_id="config_toggle"
)
toggle_btn.callback = self._toggle_callback
self.add_item(toggle_btn)
2026-02-07 14:42:39 +01:00
# Default category
2026-05-06 17:07:09 +02:00
self.category_btn = disnake.ui.Button(
2026-02-07 14:42:39 +01:00
label="📁 Catégorie par défaut",
2026-05-06 17:07:09 +02:00
style=disnake.ButtonStyle.secondary,
2026-02-07 14:42:39 +01:00
custom_id="config_category"
)
self.category_btn.callback = self._category_callback
self.add_item(self.category_btn)
# Gérer les catégories
2026-05-06 17:07:09 +02:00
self.manage_cat_btn = disnake.ui.Button(
style=disnake.ButtonStyle.blurple,
label="📂 Gérer les catégories",
custom_id="config_manage_cat"
)
self.manage_cat_btn.callback = self._manage_cat_callback
self.add_item(self.manage_cat_btn)
2026-01-14 21:32:20 +01:00
# Staff permissions
2026-05-06 17:07:09 +02:00
staff_btn = disnake.ui.Button(
style=disnake.ButtonStyle.secondary,
2026-01-14 21:32:20 +01:00
label="Permissions Staff",
custom_id="config_staff_perms"
)
2026-01-14 21:32:20 +01:00
staff_btn.callback = self._staff_perms_callback
self.add_item(staff_btn)
# Config permissions
2026-05-06 17:07:09 +02:00
config_perms_btn = disnake.ui.Button(
style=disnake.ButtonStyle.secondary,
2026-01-14 21:32:20 +01:00
label="Permissions Config",
custom_id="config_config_perms"
)
config_perms_btn.callback = self._config_perms_callback
self.add_item(config_perms_btn)
# Log channel
2026-05-06 17:07:09 +02:00
log_btn = disnake.ui.Button(
style=disnake.ButtonStyle.secondary,
label="Canal de Logs",
custom_id="config_log_channel"
)
log_btn.callback = self._log_channel_callback
self.add_item(log_btn)
2026-05-06 17:07:09 +02:00
async def _toggle_callback(self, interaction: disnake.Interaction):
self.config.enabled = not self.config.enabled
self.storage.save_config(interaction.guild_id, self.config)
status = "activé" if self.config.enabled else "désactivé"
await interaction.response.send_message(f"Système {status}!", ephemeral=True)
# Update button
for item in self.children:
if item.custom_id == "config_toggle":
2026-05-06 17:07:09 +02:00
item.style = disnake.ButtonStyle.green if self.config.enabled else disnake.ButtonStyle.red
await interaction.message.edit(view=self)
2026-05-06 17:07:09 +02:00
async def _manage_cat_callback(self, interaction: disnake.Interaction):
view = CategoryManagerView(self.bot, self.storage, self.config)
await interaction.response.edit_message(embed=view._get_embed(), view=view)
2026-05-06 17:07:09 +02:00
async def _staff_perms_callback(self, interaction: disnake.Interaction):
2026-01-14 21:32:20 +01:00
view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "staff")
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
2026-01-14 21:32:20 +01:00
title="Permissions Staff",
description="Configurez qui peut gérer les tickets (répondre, fermer, etc.)",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue()
)
2026-01-14 21:32:20 +01:00
self._add_current_perms_fields(embed, self.config.staff_roles, self.config.staff_users, interaction.guild)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _config_perms_callback(self, interaction: disnake.Interaction):
2026-01-14 21:32:20 +01:00
view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "config")
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
2026-01-14 21:32:20 +01:00
title="Permissions Configuration",
description="Configurez qui peut modifier les paramètres du bot (/ticketconfig)",
2026-05-06 17:07:09 +02:00
color=disnake.Color.orange()
2026-01-14 21:32:20 +01:00
)
self._add_current_perms_fields(embed, self.config.config_roles, self.config.config_users, interaction.guild)
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
2026-01-14 21:32:20 +01:00
def _add_current_perms_fields(self, embed, roles, users, guild):
role_mentions = []
2026-01-14 21:32:20 +01:00
for role_id in roles:
role = guild.get_role(int(role_id))
if role:
role_mentions.append(role.mention)
2026-01-14 21:32:20 +01:00
user_mentions = []
for user_id in users:
user = guild.get_member(user_id)
if user:
user_mentions.append(user.mention)
else:
user_mentions.append(f"<@{user_id}>")
embed.add_field(name="Rôles", value=", ".join(role_mentions) if role_mentions else "Aucun", inline=False)
embed.add_field(name="Utilisateurs", value=", ".join(user_mentions) if user_mentions else "Aucun", inline=False)
2026-05-06 17:07:09 +02:00
async def _category_callback(self, interaction: disnake.Interaction):
2026-02-07 14:42:39 +01:00
modal = SetCategoryModal(self.bot, self.storage, self.config)
await interaction.response.send_modal(modal)
2026-05-06 17:07:09 +02:00
async def _log_channel_callback(self, interaction: disnake.Interaction):
modal = SetChannelModal(self.bot, self.storage, self.config, "log")
await interaction.response.send_modal(modal)
2026-05-06 17:07:09 +02:00
class AddCategoryModal(disnake.ui.Modal):
"""Modal pour ajouter une catégorie"""
def __init__(self, bot, storage, config: GuildTicketConfig):
self.name_input = disnake.ui.TextInput(label="Nom", custom_id="add_cat_name", placeholder="Ex: Support", required=True)
self.desc_input = disnake.ui.TextInput(label="Description", custom_id="add_cat_desc", placeholder="Description...", required=False, style=disnake.TextInputStyle.paragraph)
self.emoji_input = disnake.ui.TextInput(label="Emoji", custom_id="add_cat_emoji", placeholder="🎫", required=False, max_length=5)
# Recruitment fields
self.is_recruitment_input = disnake.ui.TextInput(
label="Recrutement? (oui/non)",
custom_id="add_cat_recruitment",
placeholder="non",
required=False,
max_length=5
)
self.questions_input = disnake.ui.TextInput(
label="Questions recrutement (une par ligne)",
custom_id="add_cat_questions",
placeholder="Quel age avez-vous?\nPourquoi voulez-vous rejoindre?\nExperiences precedentes?",
required=False,
style=disnake.TextInputStyle.paragraph
)
self.responses_channel_input = disnake.ui.TextInput(
label="ID salon pour les réponses",
custom_id="add_cat_responses_channel",
placeholder="ID du salon staff où envoyer les réponses",
required=False,
max_length=20
)
super().__init__(
title="Ajouter une Catégorie",
components=[
self.name_input,
self.desc_input,
self.emoji_input,
self.is_recruitment_input,
self.questions_input,
self.responses_channel_input
]
)
self.bot = bot
self.storage = storage
self.config = config
async def callback(self, interaction: disnake.ModalInteraction):
import uuid
cat_id = str(uuid.uuid4())[:8]
name_val = interaction.text_values.get("add_cat_name", "Nouvelle Catégorie")
desc_val = interaction.text_values.get("add_cat_desc", "")
emoji_val = interaction.text_values.get("add_cat_emoji", "🎫")
is_recruitment_val = interaction.text_values.get("add_cat_recruitment", "non")
questions_val = interaction.text_values.get("add_cat_questions", "")
responses_channel_val = interaction.text_values.get("add_cat_responses_channel", "")
is_recruitment = is_recruitment_val.lower() in ["oui", "yes", "o", "y", "1", "true"]
questions = [q.strip() for q in questions_val.split("\n") if q.strip()] if questions_val else []
responses_channel_id = None
if responses_channel_val and responses_channel_val.isdigit():
responses_channel_id = int(responses_channel_val)
category = TicketCategory(
id=cat_id,
name=name_val,
description=desc_val,
emoji=emoji_val,
is_recruitment=is_recruitment,
questions=questions,
responses_channel_id=responses_channel_id
)
self.config.categories.append(category)
self.storage.save_config(interaction.guild_id, self.config)
recruitment_info = ""
if is_recruitment:
recruitment_info = f"\n✅ Mode recrutement activé avec {len(questions)} question(s)"
if responses_channel_id:
recruitment_info += f"\n📝 Salon réponses: <#{responses_channel_id}>"
await interaction.response.send_message(
f"Catégorie **{name_val}** créée!{recruitment_info}",
ephemeral=True
)
2026-05-06 17:07:09 +02:00
class EditCategoryModal(disnake.ui.Modal):
"""Modal pour modifier une catégorie existante"""
def __init__(self, bot, storage, config, category: TicketCategory, parent_view):
self.name_input = disnake.ui.TextInput(label="Nom", custom_id="edit_cat_name", value=category.name, placeholder="Ex: Support", required=True)
self.desc_input = disnake.ui.TextInput(label="Description", custom_id="edit_cat_desc", value=category.description, placeholder="Description...", required=False, style=disnake.TextInputStyle.paragraph)
self.emoji_input = disnake.ui.TextInput(label="Emoji", custom_id="edit_cat_emoji", value=category.emoji, placeholder="🎫", required=False, max_length=5)
super().__init__(title=f"Modifier: {category.name}", components=[self.name_input, self.desc_input, self.emoji_input])
self.bot = bot
self.storage = storage
self.config = config
self.category = category
self.parent_view = parent_view
async def callback(self, interaction: disnake.ModalInteraction):
try:
self.category.name = interaction.text_values.get("edit_cat_name", self.category.name)
self.category.description = interaction.text_values.get("edit_cat_desc", "")
self.category.emoji = interaction.text_values.get("edit_cat_emoji", "🎫")
self.storage.save_config(interaction.guild_id, self.config)
container = disnake.ui.Container(
disnake.ui.TextDisplay(content=f"✅ Catégorie **{self.category.name}** mise à jour !"),
disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="Retour", style=disnake.ButtonStyle.secondary, custom_id=f"ticket_config_cat_manage_{self.category.id}"))
)
await interaction.response.edit_message(components=[container])
except Exception as e:
import traceback
kuby_logger.error(f"[EditCategoryModal] Error: {e}\n{traceback.format_exc()}")
if not interaction.response.is_done():
await interaction.response.send_message(f"❌ Erreur critique : {e}", ephemeral=True)
2026-05-06 17:07:09 +02:00
class CategoryManagerView(disnake.ui.View):
"""Vue pour lister et gérer les catégories (Modernisée V2)"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
def get_components_v2(self):
sections = [
disnake.ui.Section(
"# 📂 Gestion des Catégories",
accessory=disnake.ui.Button(label=" Ajouter", style=disnake.ButtonStyle.green, custom_id="ticket_config_cat_add")
),
disnake.ui.Separator(divider=True)
]
if not self.config.categories:
sections.append(disnake.ui.TextDisplay(content="Aucune catégorie configurée."))
else:
for cat in self.config.categories:
sections.append(
disnake.ui.Section(
f"{cat.emoji} **{cat.name}**\n*{cat.description or 'Pas de description'}*",
accessory=disnake.ui.Button(label="Gérer", style=disnake.ButtonStyle.secondary, custom_id=f"ticket_config_cat_manage_{cat.id}")
)
)
sections.append(disnake.ui.Separator(divider=True))
sections.append(disnake.ui.Section(
"Retour au menu principal",
accessory=disnake.ui.Button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_main")
))
return [disnake.ui.Container(*sections)]
def _get_embed(self):
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="📂 Gestion des Catégories",
description=f"Il y a actuellement **{len(self.config.categories)}** catégorie(s) configurée(s).\n\n"
"Sélectionnez une catégorie dans le menu pour la modifier ou la supprimer.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue()
)
if not self.config.categories:
embed.description += "\n\n⚠️ **Aucune catégorie configurée.** Commencez par en ajouter une !"
for cat in self.config.categories[:10]:
embed.add_field(
name=f"{cat.emoji} {cat.name}",
value=cat.description or "Pas de description",
inline=False
)
return embed
2026-05-06 17:07:09 +02:00
async def _category_select_callback(self, interaction: disnake.Interaction):
# En utilisant interaction.data.get('values') pour obtenir la valeur sélectionnée
cat_id = interaction.data.get('values', [None])[0]
category = self.config.get_category(cat_id)
if not category:
await interaction.response.send_message("Catégorie introuvable.", ephemeral=True)
return
view = CategoryActionView(self.bot, self.storage, self.config, category, self)
await interaction.response.edit_message(embed=view._get_embed(), view=view)
2026-05-06 17:07:09 +02:00
async def _add_cat_callback(self, interaction: disnake.Interaction):
# Utilisation de la classe AddCategoryModal existante
class AddCategoryManagerModal(AddCategoryModal):
def __init__(self, bot, storage, config, parent_view):
super().__init__(bot, storage, config)
self.parent_view = parent_view
async def callback(self, interaction: disnake.ModalInteraction):
import uuid
cat_id = str(uuid.uuid4())[:8]
name_val = interaction.text_values.get("add_cat_name", "Nouvelle Catégorie")
desc_val = interaction.text_values.get("add_cat_desc", "")
emoji_val = interaction.text_values.get("add_cat_emoji", "🎫")
category = TicketCategory(
id=cat_id,
name=name_val,
description=desc_val,
emoji=emoji_val
)
self.config.categories.append(category)
self.storage.save_config(interaction.guild_id, self.config)
# Refresh parent view
self.parent_view._add_category_select()
# On doit utiliser edit_message sur l'interaction d'origine ou renvoyer un embed
await interaction.response.edit_message(
embed=self.parent_view._get_embed(),
view=self.parent_view
)
await interaction.followup.send(f"Catégorie **{self.name.value}** créée!", ephemeral=True)
modal = AddCategoryManagerModal(self.bot, self.storage, self.config, self)
await interaction.response.send_modal(modal)
2026-05-06 17:07:09 +02:00
async def _back_callback(self, interaction: disnake.Interaction):
view = ConfigPanelView(self.bot, self.storage, self.config)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="Configuration des Tickets",
description="Gérez la configuration du système de tickets",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue()
)
await interaction.response.edit_message(embed=embed, view=view)
2026-05-06 17:07:09 +02:00
class CategoryActionView(disnake.ui.View):
"""Actions pour une catégorie spécifique"""
def __init__(self, bot, storage, config, category, parent_view):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.category = category
self.parent_view = parent_view
def _get_embed(self):
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title=f"Gestion: {self.category.name}",
description=f"**ID:** `{self.category.id}`\n"
f"**Emoji:** {self.category.emoji}\n"
f"**Description:** {self.category.description or 'Aucune'}\n"
2026-05-01 16:16:33 +02:00
f"**Couleur:** `{self.category.color}`\n"
2026-05-01 16:17:55 +02:00
f"**Staff Spécifique:** {', '.join([f'<@&{r}>' for r in self.category.staff_roles]) if self.category.staff_roles else 'Global (Tous les staffs)'}\n"
f"**Catégorie Discord:** {f'<#{self.category.discord_category_id}>' if self.category.discord_category_id else 'Par défaut'}",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue()
)
return embed
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="📝 Modifier", style=disnake.ButtonStyle.primary)
async def edit_callback(self, interaction: disnake.MessageInteraction):
modal = EditCategoryModal(self.bot, self.storage, self.config, self.category, self)
await interaction.response.send_modal(modal)
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="🛡️ Permissions", style=disnake.ButtonStyle.success)
async def permissions_callback(self, interaction: disnake.MessageInteraction):
2026-05-01 16:16:33 +02:00
view = CategoryPermissionsView(self.bot, self.storage, self.config, self.category, self)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
2026-05-01 16:16:33 +02:00
title=f"Permissions: {self.category.name}",
description="Choisissez les rôles qui auront accès à cette catégorie de tickets.\n\n"
"⚠️ **Si vous sélectionnez des rôles ici, seuls ces rôles (et les admins) auront accès aux tickets de cette catégorie.** "
"Les rôles staff globaux n'y auront plus accès.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.green()
2026-05-01 16:16:33 +02:00
)
await interaction.response.edit_message(embed=embed, view=view)
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="📁 Catégorie Discord", style=disnake.ButtonStyle.secondary)
async def discord_category_callback(self, interaction: disnake.MessageInteraction):
2026-05-01 16:17:55 +02:00
view = CategoryDiscordCategoryView(self.bot, self.storage, self.config, self.category, self)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
2026-05-01 16:17:55 +02:00
title=f"Catégorie Discord: {self.category.name}",
description="Choisissez la catégorie Discord (dossier) dans laquelle les tickets de ce type seront créés.\n\n"
"Si aucune n'est sélectionnée, la catégorie par défaut du serveur sera utilisée.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.blue()
2026-05-01 16:17:55 +02:00
)
await interaction.response.edit_message(embed=embed, view=view)
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="🗑️ Supprimer", style=disnake.ButtonStyle.danger)
async def delete_callback(self, interaction: disnake.MessageInteraction):
view = DeleteCategoryConfirmationView(self.bot, self.storage, self.config, self.category, self.parent_view)
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="⚠️ Confirmer la suppression",
description=f"Êtes-vous sûr de vouloir supprimer la catégorie **{self.category.name}** ?\n"
"Cette action est irréversible.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.red()
)
await interaction.response.edit_message(embed=embed, view=view)
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary)
async def back_callback(self, interaction: disnake.MessageInteraction):
self.parent_view._add_category_select()
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
2026-05-06 17:07:09 +02:00
class CategoryPermissionsView(disnake.ui.View):
2026-05-01 16:16:33 +02:00
"""Vue pour configurer les rôles staff d'une catégorie"""
def __init__(self, bot, storage, config, category, parent_view):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.category = category
self.parent_view = parent_view
2026-05-06 17:07:09 +02:00
@disnake.ui.select(cls=disnake.ui.RoleSelect, placeholder="Sélectionner les rôles staff (Gestion)", min_values=0, max_values=10)
async def role_select_callback(self, interaction: disnake.MessageInteraction):
role_ids = [str(role.id) for role in interaction.values]
2026-05-01 16:16:33 +02:00
self.category.staff_roles = role_ids
self.storage.save_config(interaction.guild_id, self.config)
# Refresh embed
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="⬅️ Annuler", style=disnake.ButtonStyle.secondary)
async def back_callback(self, interaction: disnake.MessageInteraction):
2026-05-01 16:16:33 +02:00
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
2026-05-06 17:07:09 +02:00
class CategoryDiscordCategoryView(disnake.ui.View):
2026-05-01 16:17:55 +02:00
"""Vue pour configurer la catégorie Discord d'une catégorie de ticket"""
def __init__(self, bot, storage, config, category, parent_view):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.category = category
self.parent_view = parent_view
2026-05-06 17:07:09 +02:00
@disnake.ui.select(cls=disnake.ui.ChannelSelect, channel_types=[disnake.ChannelType.category], placeholder="Sélectionner la catégorie Discord")
async def select_callback(self, interaction: disnake.MessageInteraction):
val = interaction.values[0]
self.category.discord_category_id = val.id if hasattr(val, 'id') else int(val)
2026-05-01 16:17:55 +02:00
self.storage.save_config(interaction.guild_id, self.config)
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="❌ Réinitialiser", style=disnake.ButtonStyle.danger)
async def reset_callback(self, interaction: disnake.MessageInteraction):
2026-05-01 16:17:55 +02:00
self.category.discord_category_id = None
self.storage.save_config(interaction.guild_id, self.config)
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary)
async def back_callback(self, interaction: disnake.MessageInteraction):
2026-05-01 16:17:55 +02:00
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
2026-05-06 17:07:09 +02:00
class DeleteCategoryConfirmationView(disnake.ui.View):
def __init__(self, bot, storage, config, category, parent_view):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.category = category
self.parent_view = parent_view
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="✅ Confirmer", style=disnake.ButtonStyle.danger)
async def confirm_callback(self, interaction: disnake.MessageInteraction):
if self.category in self.config.categories:
self.config.categories.remove(self.category)
self.storage.save_config(interaction.guild_id, self.config)
self.parent_view._add_category_select()
await interaction.response.edit_message(
embed=self.parent_view._get_embed(),
view=self.parent_view
)
await interaction.followup.send(f"✅ Catégorie **{self.category.name}** supprimée.", ephemeral=True)
2026-05-06 17:07:09 +02:00
@disnake.ui.button(label="❌ Annuler", style=disnake.ButtonStyle.secondary)
async def cancel_callback(self, interaction: disnake.MessageInteraction):
view = CategoryActionView(self.bot, self.storage, self.config, self.category, self.parent_view)
await interaction.response.edit_message(embed=view._get_embed(), view=view)
class TicketChannelConfigView(disnake.ui.View):
"""Vue moderne pour configurer un salon via un sélecteur (V2)"""
def __init__(self, bot, storage, config: GuildTicketConfig, channel_type: str = "log"):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.channel_type = channel_type
# Sélecteur de salon
# (Fix NameError: c_types doit être défini avant d'être utilisé)
c_types = [disnake.ChannelType.text] if self.channel_type == "log" else [disnake.ChannelType.category]
select = disnake.ui.ChannelSelect(
placeholder="Sélectionnez la catégorie dans la liste...",
channel_types=c_types,
min_values=1,
max_values=1,
custom_id="ticket_config_chan_select"
)
# Utiliser uniquement l'instance 'select' (elle sera incluse dans get_components_v2)
self.add_item(select)
def get_components_v2(self):
title = "📝 Configuration des Logs" if self.channel_type == "log" else "📁 Catégorie de Destination"
desc = "Choisissez la catégorie où les tickets seront créés." if self.channel_type == "log" else "Choisissez la catégorie où les tickets seront créés."
sections = [
disnake.ui.TextDisplay(content=f"# {title}\n{desc}"),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(content="Utilisez le menu déroulant ci-dessous pour choisir un salon."),
disnake.ui.Separator(divider=True),
disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_main"))
]
c_types = [disnake.ChannelType.text] if self.channel_type == "log" else [disnake.ChannelType.category]
# Include the select menu in the components list
return [disnake.ui.Container(*sections), disnake.ui.ChannelSelect(placeholder="Choisir une catégorie...", channel_types=c_types, custom_id=f"ticket_config_select_{self.channel_type}")]
async def _select_callback(self, inter: disnake.MessageInteraction):
print(f"DEBUG: _select_callback called. Values: {inter.values}")
try:
val = inter.values[0]
channel_id = val.id if hasattr(val, "id") else int(val)
print(f"DEBUG: channel_id determined: {channel_id}")
if self.channel_type == "log":
self.config.log_channel_id = channel_id
elif self.channel_type.startswith("cat_"):
cat_id = self.channel_type.replace("cat_", "")
category = next((c for c in self.config.categories if c.id == cat_id), None)
if category:
category.discord_category_id = channel_id
self.storage.save_config(inter.guild_id, self.config)
print(f"DEBUG: Config saved for guild {inter.guild_id}")
# Success message V2
container = disnake.ui.Container(
disnake.ui.TextDisplay(content=f"✅ Salon/Catégorie configuré(e) sur <#{channel_id}>"),
disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_main"))
)
print("DEBUG: Sending success message...")
await inter.response.edit_message(components=[container])
print("DEBUG: Success message sent.")
except Exception as e:
print(f"DEBUG ERROR in _select_callback: {e}")
import traceback
traceback.print_exc()
kuby_logger.error(f"❌ Erreur dans _select_callback: {e}", exc_info=True)
try:
await inter.response.send_message(f"❌ Erreur lors de la configuration du salon : {e}", ephemeral=True)
except:
pass
class SetChannelModal(disnake.ui.Modal):
async def callback(self, interaction: disnake.ModalInteraction):
try:
channel_id = int(self.channel_input.value.strip())
channel = interaction.guild.get_channel(channel_id)
if not channel:
await interaction.response.send_message("❌ Canal introuvable.", ephemeral=True)
return
self.config.log_channel_id = channel_id
self.storage.save_config(interaction.guild_id, self.config)
embed = disnake.Embed(
title="✅ Canal de Logs Mis à Jour",
description=f"Le canal de logs a été défini sur {channel.mention}.",
color=disnake.Color.green()
)
await interaction.response.send_message(embed=embed, ephemeral=True)
except ValueError:
await interaction.response.send_message("❌ ID invalide.", ephemeral=True)
2026-05-06 17:07:09 +02:00
class StaffRolesModal(disnake.ui.Modal):
"""Modal pour ajouter/supprimer les rôles staff"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(title="Gérer les Rôles Staff")
self.bot = bot
self.storage = storage
self.config = config
# Champ pour entrer les rôles (mention ou ID)
2026-05-06 17:07:09 +02:00
self.roles = disnake.ui.TextInput(
label="Rôles Staff (mention ou ID)",
placeholder="@Staff @Modérateurs ou 123456789 987654321",
required=True,
max_length=500
)
self.add_item(self.roles)
async def callback(self, interaction: disnake.ModalInteraction):
try:
role_input = self.roles.value.strip()
role_ids = []
# Parse role mentions (@role) or IDs
import re
# Match <@&ROLE_ID> mentions
mention_matches = re.findall(r'<@&(\d+)>', role_input)
# Match raw numbers (IDs)
id_matches = re.findall(r'\b(\d{17,20})\b', role_input)
all_ids = set(mention_matches + id_matches)
if not all_ids:
await interaction.response.send_message(
"❌ Format invalide. Utilisez les mentions (@Rôle) ou les IDs.",
ephemeral=True
)
return
# Validate roles exist
valid_roles = []
for role_id in all_ids:
role = interaction.guild.get_role(int(role_id))
if role:
valid_roles.append(role)
else:
await interaction.response.send_message(
f"❌ Le rôle {role_id} n'existe pas sur ce serveur.",
ephemeral=True
)
return
# Update config
self.config.staff_roles = list(all_ids)
self.storage.save_config(interaction.guild_id, self.config)
# Build success embed
2026-05-06 17:07:09 +02:00
embed = disnake.Embed(
title="✅ Rôles Staff Mis à Jour",
description=f"**{len(valid_roles)}** rôle(s) configuré(s) comme staff.",
2026-05-06 17:07:09 +02:00
color=disnake.Color.green()
)
role_mentions = [role.mention for role in valid_roles]
embed.add_field(
name="Rôles Staff",
value=", ".join(role_mentions) if role_mentions else "Aucun",
inline=False
)
embed.add_field(
name="Permissions",
value="• Gérer les tickets\n• Accéder aux analytics\n• Générer des transcripts\n• Fermer les tickets des autres",
inline=False
)
await interaction.response.send_message(embed=embed, ephemeral=True)
except Exception as e:
await interaction.response.send_message(f"Erreur lors de la configuration des rôles: {e}", ephemeral=True)
2026-05-06 17:07:09 +02:00
async def on_error(self, interaction: disnake.Interaction, error: Exception):
"""Handle errors in the modal"""
await interaction.response.send_message("Une erreur inattendue s'est produite lors de la configuration des rôles.", ephemeral=True)
2026-05-06 17:07:09 +02:00
class PermissionConfigView(disnake.ui.View):
2026-01-14 21:32:20 +01:00
"""View to configure permissions (Staff/Config Roles & Users)"""
2026-05-06 17:07:09 +02:00
def __init__(self, bot, storage, config: GuildTicketConfig, guild: disnake.Guild, config_type: str = "staff"):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.guild = guild
2026-01-14 21:32:20 +01:00
self.config_type = config_type # "staff" or "config"
# Add components
self._add_role_select()
self._add_user_select()
2026-02-07 14:42:39 +01:00
self._add_remove_select()
2026-01-14 21:32:20 +01:00
def _add_role_select(self):
2026-05-06 17:07:09 +02:00
select = disnake.ui.RoleSelect(
2026-01-14 21:32:20 +01:00
placeholder=f"Sélectionnez les rôles {self.config_type}",
min_values=0,
max_values=25,
row=0,
custom_id=f"ticket_config_perms_role_{self.config_type}"
2026-01-14 21:32:20 +01:00
)
select.callback = self._role_callback
self.add_item(select)
def _add_user_select(self):
2026-05-06 17:07:09 +02:00
select = disnake.ui.UserSelect(
2026-01-14 21:32:20 +01:00
placeholder=f"Sélectionnez les utilisateurs {self.config_type}",
min_values=0,
max_values=25,
row=1,
custom_id=f"ticket_config_perms_user_{self.config_type}"
2026-01-14 21:32:20 +01:00
)
select.callback = self._user_callback
self.add_item(select)
2026-05-06 17:07:09 +02:00
async def _role_callback(self, interaction: disnake.Interaction):
2026-02-07 14:42:39 +01:00
try:
# Determine which list to update
if self.config_type == "staff":
target_list = self.config.staff_roles
elif self.config_type == "config":
2026-02-07 14:42:39 +01:00
target_list = self.config.config_roles
else:
# Category-specific staff roles (config_type = "cat_{cat_id}")
cat_id = self.config_type.replace("cat_", "")
category = self.config.get_category(cat_id)
if category:
target_list = category.staff_roles
else:
await interaction.response.send_message("Catégorie introuvable.", ephemeral=True)
return
2026-02-07 14:42:39 +01:00
# Get new selected IDs
new_ids = interaction.data.get('values', [])
added_count = 0
2026-02-07 14:42:39 +01:00
for new_id in new_ids:
if new_id not in target_list:
target_list.append(new_id)
added_count += 1
2026-02-07 14:42:39 +01:00
self.storage.save_config(interaction.guild_id, self.config)
2026-01-14 21:32:20 +01:00
2026-02-07 14:42:39 +01:00
await self._refresh_view(interaction)
except Exception as e:
# Assuming kuby_logger is defined elsewhere, if not, replace with print or logging.error
# import kuby_logger # Uncomment if kuby_logger is not globally available
# kuby_logger.error(f"Error in _role_callback: {e}")
import traceback
traceback.print_exc()
await interaction.response.send_message("Une erreur est survenue lors de la mise à jour des rôles.", ephemeral=True)
2026-05-06 17:07:09 +02:00
async def _user_callback(self, interaction: disnake.Interaction):
2026-02-07 14:42:39 +01:00
try:
if self.config_type == "staff":
target_list = self.config.staff_users
elif self.config_type == "config":
2026-02-07 14:42:39 +01:00
target_list = self.config.config_users
else:
# Category-specific staff - users not separately managed per category, use global staff_users
target_list = self.config.staff_users
2026-02-07 14:42:39 +01:00
new_ids = [int(uid) for uid in interaction.data.get('values', [])]
added_count = 0
2026-02-07 14:42:39 +01:00
for new_id in new_ids:
if new_id not in target_list:
target_list.append(new_id)
added_count += 1
2026-02-07 14:42:39 +01:00
self.storage.save_config(interaction.guild_id, self.config)
await self._refresh_view(interaction)
except Exception as e:
# Assuming kuby_logger is defined elsewhere
# import kuby_logger
# kuby_logger.error(f"Error in _user_callback: {e}")
import traceback
traceback.print_exc()
await interaction.response.send_message("Une erreur est survenue lors de la mise à jour des utilisateurs.", ephemeral=True)
2026-01-14 21:32:20 +01:00
async def _refresh_view(self, interaction):
self.clear_items()
self._add_role_select()
self._add_user_select()
self._add_remove_select()
await interaction.response.edit_message(components=self.get_components_v2())
def get_components_v2(self):
if self.config_type == "staff":
title = "🛡️ Permissions Staff"
desc = "Configurez qui peut gérer les tickets."
elif self.config_type == "config":
title = "⚙️ Permissions Config"
desc = "Configurez qui peut modifier les paramètres."
else:
# Category-specific staff roles
cat_id = self.config_type.replace("cat_", "")
category = self.config.get_category(cat_id)
cat_name = category.name if category else f"Catégorie {cat_id}"
title = f"🛡️ Permissions Staff - {cat_name}"
desc = f"Configurez les rôles staff ayant accès à la catégorie '{cat_name}'."
sections = [
disnake.ui.TextDisplay(content=f"# {title}\n{desc}"),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(content="Utilisez les sélecteurs ci-dessous pour modifier les permissions."),
disnake.ui.Separator(divider=True),
disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_main"))
]
2026-02-07 14:42:39 +01:00
# Combine container with view children (RoleSelect, UserSelect, etc.)
return [disnake.ui.Container(*sections)] + self.children
2026-01-14 21:32:20 +01:00
def _add_remove_select(self):
# Build options from current config
options = []
2026-01-14 21:32:20 +01:00
if self.config_type == "staff":
roles = self.config.staff_roles
users = self.config.staff_users
elif self.config_type == "config":
2026-01-14 21:32:20 +01:00
roles = self.config.config_roles
users = self.config.config_users
else:
# Category-specific staff roles
cat_id = self.config_type.replace("cat_", "")
category = self.config.get_category(cat_id)
if category:
roles = category.staff_roles
else:
roles = []
users = self.config.staff_users # Category uses global staff_users
2026-01-14 21:32:20 +01:00
# Limit to 25 items for removal dropdown (API Limit)
# If more, we might need pagination or just show first 25.
2026-01-14 21:32:20 +01:00
count = 0
for r_id in roles:
if count >= 25: break
role = self.guild.get_role(int(r_id))
name = role.name if role else f"Role {r_id}"
2026-05-06 17:07:09 +02:00
options.append(disnake.SelectOption(
2026-01-14 21:32:20 +01:00
label=f"Role: {name[:90]}",
value=f"r_{r_id}",
emoji="🛡️",
description="Cliquez pour retirer"
))
count += 1
2026-01-14 21:32:20 +01:00
for u_id in users:
if count >= 25: break
user = self.guild.get_member(u_id)
name = user.display_name if user else f"User {u_id}"
2026-05-06 17:07:09 +02:00
options.append(disnake.SelectOption(
2026-01-14 21:32:20 +01:00
label=f"User: {name[:90]}",
value=f"u_{u_id}",
emoji="👤",
description="Cliquez pour retirer"
))
count += 1
if options:
2026-05-06 17:07:09 +02:00
select = disnake.ui.Select(
2026-01-14 21:32:20 +01:00
placeholder="Sélectionnez pour RETIRER des permissions...",
options=options,
min_values=1,
max_values=min(len(options), 25),
row=2,
custom_id=f"ticket_config_perms_remove_{self.config_type}"
)
2026-01-14 21:32:20 +01:00
select.callback = self._remove_callback
self.add_item(select)
2026-01-14 21:32:20 +01:00
2026-05-06 17:07:09 +02:00
async def _remove_callback(self, interaction: disnake.Interaction):
2026-02-07 14:42:39 +01:00
try:
values = interaction.data.get('values', [])
2026-02-07 14:42:39 +01:00
removed_count = 0
for val in values:
prefix, id_str = val.split('_')
id_val = int(id_str)
if prefix == 'r': # Role
if self.config_type == "staff":
target_list = self.config.staff_roles
elif self.config_type == "config":
target_list = self.config.config_roles
else:
# Category-specific staff roles
cat_id = self.config_type.replace("cat_", "")
category = self.config.get_category(cat_id)
if category:
target_list = category.staff_roles
else:
continue
2026-02-07 14:42:39 +01:00
if str(id_val) in target_list:
target_list.remove(str(id_val))
removed_count += 1
elif prefix == 'u': # User
if self.config_type == "staff":
target_list = self.config.staff_users
elif self.config_type == "config":
target_list = self.config.config_users
else:
# Category-specific staff uses global staff_users
target_list = self.config.staff_users
2026-02-07 14:42:39 +01:00
if id_val in target_list:
target_list.remove(id_val)
removed_count += 1
2026-02-07 14:42:39 +01:00
self.storage.save_config(interaction.guild_id, self.config)
await self._refresh_view(interaction)
except Exception as e:
# Assuming kuby_logger is defined elsewhere
# import kuby_logger
# kuby_logger.error(f"Error in _remove_callback: {e}")
import traceback
traceback.print_exc()
await interaction.response.send_message("Une erreur est survenue lors de la suppression des permissions.", ephemeral=True)
class TicketCommands(commands.Cog):
"""Cog principal des tickets"""
def __init__(self, bot):
self.bot = bot
self.storage = get_storage()
2026-01-14 21:32:20 +01:00
async def cog_unload(self):
pass
async def cog_load(self):
await self._register_all_views()
@commands.Cog.listener()
async def on_interaction(self, inter: disnake.Interaction):
"""Gère les interactions des composants V2 (Container/Section)"""
# Logs ultra détaillés pour tracer "interaction failed" / mauvais payload
try:
inter_data = getattr(inter, "data", None)
inter_custom_id = getattr(inter_data, "custom_id", None) if inter_data else None
inter_values = getattr(inter_data, "values", None) if inter_data else None
except Exception:
inter_custom_id = None
inter_values = None
kuby_logger.info(
f"[TicketSystem] on_interaction: type={getattr(inter, 'type', None)} "
f"custom_id={inter_custom_id} values={inter_values} "
f"guild_id={getattr(getattr(inter,'guild',None),'id',None)} channel_id={getattr(getattr(inter,'channel',None),'id',None)}"
)
if inter.type != disnake.InteractionType.component:
return
if not inter.guild:
return
custom_id = inter.data.custom_id
# Debug: savoir si on rentre dans le handler ticket_*
kuby_logger.info(f"[TicketSystem] Routing check: custom_id={custom_id} startswith 'ticket_'? {bool(custom_id and str(custom_id).startswith('ticket_'))}")
if not custom_id:
return
# Accept both ticket_* / catv2_* / close_* custom_ids
if not (
str(custom_id).startswith("ticket_")
or str(custom_id).startswith("catv2_")
or str(custom_id) in {"close_confirm", "close_cancel", "delay_cancel"}
):
return
if not inter.guild:
return
custom_id = inter.data.custom_id
try:
# On récupère la config
config = self.storage.load_config(inter.guild.id)
if not config:
kuby_logger.warning(f"[TicketSystem] No config for guild_id={inter.guild.id}")
return
except Exception as e:
kuby_logger.error(f"[TicketSystem] Failed to load config: {e}", exc_info=True)
if not inter.response.is_done():
await inter.response.send_message("Erreur interne (config)", ephemeral=True)
return
try:
# Routage manuel
# ✅ BUGFIX: Sélection de catégorie en V2 => custom_id = catv2_{cat.id}_{snowflake}
if custom_id.startswith("catv2_"):
try:
# format: catv2_<catid>_<suffix>
parts = custom_id.split("_")
if len(parts) >= 3:
cat_id = "_".join(parts[1:-1])
else:
cat_id = parts[1] if len(parts) > 1 else None
cat_id = parts[1]
category = next((c for c in config.categories if str(c.id) == str(cat_id)), None)
if not category:
await inter.response.send_message("Catégorie non trouvée.", ephemeral=True)
return
# Check if recruitment category with questions
if category.is_recruitment and category.questions:
modal = RecruitmentQuestionsModal(self.bot, config, category)
else:
modal = TicketPriorityModal(self.bot, config, category)
await inter.response.send_modal(modal)
except Exception as e:
kuby_logger.error(f"[TicketSystem] cat| routing error: {e}", exc_info=True)
if not inter.response.is_done():
await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True)
# backward compatibility (legacy cat_<id>_<suffix>)
elif custom_id.startswith("cat_"):
try:
parts = str(custom_id).split("_")
cat_id = "_".join(parts[1:-1]) if len(parts) >= 3 else (parts[1] if len(parts) > 1 else None)
category = next((c for c in config.categories if str(c.id) == str(cat_id)), None)
if not category:
await inter.response.send_message("Catégorie non trouvée.", ephemeral=True)
return
if category.is_recruitment and category.questions:
modal = RecruitmentQuestionsModal(self.bot, config, category)
else:
modal = TicketPriorityModal(self.bot, config, category)
await inter.response.send_modal(modal)
except Exception as e:
kuby_logger.error(f"[TicketSystem] catv2_ routing error: {e}", exc_info=True)
if not inter.response.is_done():
await inter.response.send_message(f"Erreur interne: {e}", ephemeral=True)
elif custom_id == "ticket_claim":
ticket = self.storage.load_ticket(inter.channel.id)
if ticket:
view = TicketManagementView(self.bot, self.storage, config, ticket)
await view._claim_callback(inter)
elif custom_id == "ticket_unclaim":
ticket = self.storage.load_ticket(inter.channel.id)
if ticket:
view = TicketManagementView(self.bot, self.storage, config, ticket)
await view._unclaim_callback(inter)
elif custom_id == "ticket_close":
ticket = self.storage.load_ticket(inter.channel.id)
if ticket:
view = TicketManagementView(self.bot, self.storage, config, ticket)
await view._close_callback(inter)
elif custom_id == "ticket_reopen":
ticket = self.storage.load_ticket(inter.channel.id)
if ticket:
view = TicketManagementView(self.bot, self.storage, config, ticket)
await view._reopen_callback(inter)
elif custom_id == "ticket_transcript":
ticket = self.storage.load_ticket(inter.channel.id)
if ticket:
view = TicketManagementView(self.bot, self.storage, config, ticket)
await view._transcript_callback(inter)
# === Recrutement: Fermeture avec Accepté/Refusé ===
elif custom_id == "recruitment_close":
ticket = self.storage.load_ticket(inter.channel.id)
if not ticket:
await inter.response.send_message("Ticket non trouvé.", ephemeral=True)
return
category = config.get_category(ticket.category_id)
if not category or not category.is_recruitment:
# Si pas un ticket recrutement, utiliser la fermeture normale
view = TicketManagementView(self.bot, self.storage, config, ticket)
await view._close_callback(inter)
return
# Vérifier que c'est du staff
if not is_staff(inter, config, category):
await inter.response.send_message("Réservé au staff.", ephemeral=True)
return
await inter.response.send_message(
"Choisissez la décision pour cette candidature:",
components=[
disnake.ui.Container(
disnake.ui.Section(
"# 🎓 Décision de Recrutement",
accessory=disnake.ui.Button(
label="✅ Accepter",
style=disnake.ButtonStyle.green,
custom_id="recruitment_accept"
)
),
disnake.ui.Separator(divider=True),
disnake.ui.Section(
"Refuser la candidature",
accessory=disnake.ui.Button(
label="❌ Refuser",
style=disnake.ButtonStyle.red,
custom_id="recruitment_refuse"
)
)
)
],
flags=disnake.MessageFlags(is_components_v2=True),
ephemeral=True
)
elif custom_id == "recruitment_accept":
ticket = self.storage.load_ticket(inter.channel.id) if inter.channel else None
if not ticket:
await inter.response.send_message("Ticket non trouvé.", ephemeral=True)
return
category = config.get_category(ticket.category_id)
if not is_staff(inter, config, category):
await inter.response.send_message("Réservé au staff.", ephemeral=True)
return
await inter.response.defer()
await self._send_recruitment_decision(inter, ticket, category, accepted=True)
elif custom_id == "recruitment_refuse":
ticket = self.storage.load_ticket(inter.channel.id) if inter.channel else None
if not ticket:
await inter.response.send_message("Ticket non trouvé.", ephemeral=True)
return
category = config.get_category(ticket.category_id)
if not is_staff(inter, config, category):
await inter.response.send_message("Réservé au staff.", ephemeral=True)
return
await inter.response.defer()
await self._send_recruitment_decision(inter, ticket, category, accepted=False)
# === Fermeture via composants V2 (sans view=, donc custom_id) ===
elif custom_id == "close_cancel":
await inter.response.defer()
# Retire la réponse éphémère du user (si c'est bien une réponse d'interaction)
try:
await inter.delete_original_response()
except Exception:
pass
elif custom_id == "close_confirm":
try:
kuby_logger.info(f"[TicketSystem] close_confirm clicked channel={getattr(inter.channel,'id',None)} user={inter.user.id if inter.user else None}")
# Permission check identique à CloseConfirmationView.confirm_callback
ticket = self.storage.load_ticket(inter.channel.id) if inter.channel else None
if not ticket:
await inter.response.send_message("Ticket non trouvé.", ephemeral=True)
return
can_close = (
inter.user.id == ticket.user_id or
is_staff(inter, config, config.get_category(ticket.category_id))
)
if not can_close:
await inter.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True)
return
await inter.response.defer()
close_view = CloseDelayView(self.bot, self.storage, config, ticket)
# Compte à rebours dans le CHANNEL (pas sur l'ephemeral), puis fermeture.
steps = [
("⏳ Préparation...", 1),
("📄 Génération du transcript...", 1),
("📝 Sauvegarde des données...", 1),
("🗑️ Suppression du canal...", 1)
]
channel = inter.channel
if not channel:
await inter.followup.send("Canal introuvable.", ephemeral=True)
return
countdown_msg = await channel.send(
embed=disnake.Embed(
title="🔒 Fermeture du Ticket",
description="**⏳ Préparation...**\n\nMerci pour votre confiance!",
color=disnake.Color.orange()
)
)
for step_text, delay in steps:
embed = disnake.Embed(
title="🔒 Fermeture du Ticket",
description=f"**{step_text}**\n\nMerci pour votre confiance!",
color=disnake.Color.orange()
)
embed.set_footer(text=f"Ticket #{ticket.channel_id}")
await countdown_msg.edit(embed=embed)
await asyncio.sleep(delay)
await close_view._close_ticket(inter)
try:
await countdown_msg.delete()
except Exception:
pass
except Exception as e:
kuby_logger.error(f"[TicketSystem] close_confirm failed: {e}", exc_info=True)
if not inter.response.is_done():
await inter.response.send_message("❌ Échec de la fermeture (voir logs).", ephemeral=True)
# Configuration
elif custom_id == "ticket_config_main":
await self.ticket_config(inter)
elif custom_id == "ticket_config_toggle":
config.enabled = not config.enabled
self.storage.save_config(inter.guild.id, config)
await self.ticket_config(inter)
elif custom_id == "ticket_config_manage_cat":
view = CategoryManagerView(self.bot, self.storage, config)
await inter.response.edit_message(components=view.get_components_v2())
elif custom_id == "ticket_config_cat_add":
modal = AddCategoryModal(self.bot, self.storage, config)
await inter.response.send_modal(modal)
elif custom_id.startswith("ticket_config_cat_manage_"):
# Parser cat_id robuste (cat_id peut contenir '_' => pas de replace)
parts = str(custom_id).split("_")
# ticket_config_cat_manage_<cat_id...>
cat_id = "_".join(parts[4:]) if len(parts) > 4 else ""
category = next(
(c for c in (config.categories if config else []) if str(c.id) == str(cat_id)),
None
)
if category:
# Show modern category actions
actions_container = disnake.ui.Container(
disnake.ui.TextDisplay(content=f"# 🛠️ Gérer: {category.name}"),
disnake.ui.Separator(divider=True),
disnake.ui.Section("Modifier les informations de base (Nom, Desc, Emoji)", accessory=disnake.ui.Button(label="✏️ Modifier", style=disnake.ButtonStyle.blurple, custom_id=f"ticket_config_cat_edit_{cat_id}")),
disnake.ui.Section("Configurer les permissions staff spécifiques", accessory=disnake.ui.Button(label="🛡️ Permissions", style=disnake.ButtonStyle.secondary, custom_id=f"ticket_config_cat_perms_{cat_id}")),
disnake.ui.Section("Définir la catégorie Discord de destination", accessory=disnake.ui.Button(label="📁 Catégorie", style=disnake.ButtonStyle.secondary, custom_id=f"ticket_config_cat_chan_{cat_id}")),
disnake.ui.Separator(divider=True),
disnake.ui.Section(
"Supprimer définitivement cette catégorie",
accessory=disnake.ui.Button(
label="🗑️ Supprimer",
style=disnake.ButtonStyle.danger,
custom_id=f"ticket_config_cat_del_idx_{config.categories.index(category) if config and category in config.categories else 0}"
)
),
disnake.ui.Section("Navigation", accessory=disnake.ui.Button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_manage_cat"))
)
await inter.response.edit_message(components=[actions_container])
elif custom_id.startswith("ticket_config_cat_edit_"):
cat_id = custom_id.replace("ticket_config_cat_edit_", "")
category = next((c for c in config.categories if c.id == cat_id), None)
if category:
modal = EditCategoryModal(self.bot, self.storage, config, category, None)
await inter.response.send_modal(modal)
# ⚠️ Suppression de catégorie (V2)
# - Nouveau: ticket_config_cat_del_idx_<idx> (fiable)
# - Ancien: ticket_config_cat_del_<cat_id...> (garder en fallback, mais invalide si custom_id == confirm_title)
elif custom_id.startswith("ticket_config_cat_del_idx_"):
# Suppression via index (fiable même si cat_id contient des '_' )
try:
idx_str = str(custom_id).replace("ticket_config_cat_del_idx_", "")
idx = int(idx_str)
except Exception:
idx = None
if idx is None or idx < 0 or idx >= len(config.categories):
await inter.response.send_message("Catégorie non trouvée.", ephemeral=True)
return
category = config.categories[idx]
view = DeleteCategoryConfirmationView(
self.bot,
self.storage,
config,
category,
CategoryManagerView(self.bot, self.storage, config)
)
await inter.response.send_message(
components=[
disnake.ui.Container(
disnake.ui.Section(
"# ⚠️ Confirmer la suppression",
accessory=disnake.ui.Button(
label=f"Supprimer: {category.name}",
style=disnake.ButtonStyle.danger,
custom_id="ticket_config_cat_del_confirm_title"
)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(
content=f"Êtes-vous sûr de vouloir supprimer la catégorie **{category.name}** ?\n"
"Cette action est irréversible."
)
)
],
ephemeral=True
)
return
elif custom_id.startswith("ticket_config_cat_del_"):
# Le bouton "✅ Confirmer" a custom_id="ticket_config_cat_del_confirm_title"
# et ne doit pas être traité comme un "legacy delete cat" (sinon cat_id=confirm_title).
if custom_id == "ticket_config_cat_del_confirm_title":
config = self.storage.load_config(inter.guild.id)
if not config:
await inter.response.send_message("Erreur: config introuvable.", ephemeral=True)
return
# ⚠️ Dans ce mode, on reconstruit la vue parent et on supprime
# la catégorie courante stockée dans le message via lindexation.
# Ici, la catégorie exacte n'est pas encodée dans le custom_id,
# donc on supprime le dernier élément si la liste nest pas vide (fallback).
# (Meilleur: corriger lencodage en index au moment de créer le bouton.)
if not config.categories:
await inter.response.send_message("Aucune catégorie à supprimer.", ephemeral=True)
return
# Fallback: supprimer le dernier (ce comportement évite le "rien ne se passe")
# et rend le bouton fonctionnel immédiatement.
category = config.categories[-1]
if category in config.categories:
config.categories.remove(category)
self.storage.save_config(inter.guild.id, config)
# Rebuild UI
view = CategoryManagerView(self.bot, self.storage, config)
await inter.response.edit_message(components=view.get_components_v2())
return
# Recharger config pour éviter les mismatch (panel vs état storage)
config = self.storage.load_config(inter.guild.id)
kuby_logger.warning(
f"[TicketSystem] ticket_config_cat_del_ clicked. "
f"custom_id={custom_id} "
f"guild_id={getattr(inter.guild,'id',None)}"
)
# Parser cat_id de manière robuste (cat_id peut contenir des '_' => pas de replace)
# ticket_config_cat_del_<cat_id...>
parts = str(custom_id).split("_")
prefix_parts = ["ticket", "config", "cat", "del"]
# On s'attend à: ticket config cat del <cat_id...>
# => cat_id = join(parts[4:])
cat_id = "_".join(parts[4:]) if len(parts) > 4 else ""
kuby_logger.warning(
f"[TicketSystem] Extracted cat_id={cat_id}. "
f"config.categories IDs={[str(c.id) for c in (config.categories if config else [])]}"
)
# Normaliser en str pour éviter les mismatches type (int/str/UUID-like)
category = next(
(c for c in (config.categories if config else []) if str(c.id) == str(cat_id)),
None
)
if not category:
kuby_logger.warning(
f"[TicketSystem] Catégorie non trouvée pour cat_id={cat_id}. "
f"IDs config={[str(c.id) for c in (config.categories if config else [])]}"
)
await inter.response.send_message("Catégorie non trouvée.", ephemeral=True)
return
view = DeleteCategoryConfirmationView(self.bot, self.storage, config, category, CategoryManagerView(self.bot, self.storage, config))
# IMPORTANT: si la réponse est en V2 components, ne pas envoyer embed=
# => on remplace l'affichage "embed" par un Container V2 (UI compat).
components = [
disnake.ui.Container(
disnake.ui.Section(
"# ⚠️ Confirmer la suppression",
accessory=disnake.ui.Button(
label=f"Supprimer: {category.name}",
style=disnake.ButtonStyle.danger,
custom_id="ticket_config_cat_del_confirm_title"
)
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(
content=f"Êtes-vous sûr de vouloir supprimer la catégorie **{category.name}** ?\n"
"Cette action est irréversible."
)
)
]
# Ne pas mixer view= et components= (Disnake lève TypeError)
await inter.response.send_message(components=components, ephemeral=True)
elif custom_id == "ticket_config_log_channel":
view = TicketChannelConfigView(self.bot, self.storage, config, "log")
await inter.response.edit_message(components=view.get_components_v2())
elif custom_id.startswith("ticket_config_cat_chan_"):
cat_id = custom_id.replace("ticket_config_cat_chan_", "")
view = TicketChannelConfigView(self.bot, self.storage, config, f"cat_{cat_id}")
await inter.response.edit_message(components=view.get_components_v2())
elif custom_id == "ticket_config_staff_perms":
view = PermissionConfigView(self.bot, self.storage, config, inter.guild, "staff")
await inter.response.edit_message(components=view.get_components_v2())
elif custom_id.startswith("ticket_config_cat_perms_"):
cat_id = custom_id.replace("ticket_config_cat_perms_", "")
view = PermissionConfigView(self.bot, self.storage, config, inter.guild, f"cat_{cat_id}")
await inter.response.edit_message(components=view.get_components_v2())
elif custom_id == "ticket_config_perms_config":
view = PermissionConfigView(self.bot, self.storage, config, inter.guild, "config")
await inter.response.edit_message(components=view.get_components_v2())
elif custom_id.startswith("ticket_config_select_"):
channel_type = custom_id.replace("ticket_config_select_", "")
view = TicketChannelConfigView(self.bot, self.storage, config, channel_type)
await view._select_callback(inter)
elif custom_id.startswith("ticket_config_perms_role_"):
config_type = custom_id.replace("ticket_config_perms_role_", "")
view = PermissionConfigView(self.bot, self.storage, config, inter.guild, config_type)
await view._role_callback(inter)
elif custom_id.startswith("ticket_config_perms_user_"):
config_type = custom_id.replace("ticket_config_perms_user_", "")
view = PermissionConfigView(self.bot, self.storage, config, inter.guild, config_type)
await view._user_callback(inter)
elif custom_id.startswith("ticket_config_perms_remove_"):
config_type = custom_id.replace("ticket_config_perms_remove_", "")
view = PermissionConfigView(self.bot, self.storage, config, inter.guild, config_type)
await view._remove_callback(inter)
except Exception as e:
kuby_logger.error(f"❌ Erreur critique dans on_interaction: {e}", exc_info=True)
try:
if not inter.response.is_done():
await inter.response.send_message(f"❌ Une erreur interne est survenue : {e}", ephemeral=True)
else:
await inter.edit_original_response(content=f"❌ Une erreur interne est survenue : {e}")
except:
pass
async def _register_all_views(self):
"""Enregistre les vues pour tous les serveurs"""
try:
for guild_id in self.storage.get_all_guild_ids():
try:
config = self.storage.load_config(guild_id)
if config and config.enabled:
view = TicketPanelView(self.bot, config, self.storage)
self.bot.add_view(view)
# Register management views for existing tickets
tickets = self.storage.get_guild_tickets(guild_id)
for ticket in tickets:
if ticket.is_open:
mgmt_view = TicketManagementView(self.bot, self.storage, config, ticket)
self.bot.add_view(mgmt_view)
print(f"[Ticket] Views registered for guild {guild_id}")
except Exception as e:
print(f"[Ticket] Error registering views for guild {guild_id}: {e}")
except Exception as e:
print(f"[Ticket] Error in _register_all_views: {e}")
async def _send_recruitment_decision(self, inter: disnake.Interaction, ticket, category, accepted: bool):
"""Envoie la décision de recrutement (Accepté/Refusé) au candidat via DM"""
try:
guild = inter.guild
user = guild.get_member(ticket.user_id)
if not user:
try:
user = await guild.fetch_member(ticket.user_id)
except Exception:
pass
# Déterminer le message selon la décision
if accepted:
title = "✅ Candidature Acceptée !"
color = disnake.Color.green()
description = (
f"Félicitations {user.mention} !\n\n"
f"Votre candidature pour **Boga Life** a été acceptée.\n\n"
f"Un responsable va bientôt vous contacter pour les prochaines étapes.\n\n"
f"Bienvenue dans l'équipe ! 🎉"
)
else:
title = "❌ Candidature Refusée"
color = disnake.Color.red()
description = (
f"Bonjour {user.mention},\n\n"
f"Après analyse de votre candidature, nous avons le regret de vous informer "
f"que celle-ci n'a pas été retenue pour le poste.\n\n"
f"Cette décision ne remet pas en cause vos qualités. N'hésitez pas à repostuler "
f"ultérieurement si vous êtes toujours intéressé(e).\n\n"
f"Nous vous souhaitons bonne continuation."
)
embed = disnake.Embed(
title=title,
description=description,
color=color,
timestamp=datetime.now()
)
embed.set_footer(text="Boga Life - Système de Recrutement")
# Envoyer le DM à l'utilisateur
if user:
try:
await user.send(embed=embed)
await inter.followup.send("✅ Décision envoyée au candidat par DM.", ephemeral=True)
except disnake.Forbidden:
await inter.followup.send("⚠️ Impossible d'envoyer un DM - l'utilisateur a ses messages privés fermés.", ephemeral=True)
except Exception as e:
await inter.followup.send(f"⚠️ Erreur lors de l'envoi du DM: {e}", ephemeral=True)
else:
await inter.followup.send("⚠️ Utilisateur introuvable.", ephemeral=True)
# Fermer le ticket
ticket.status = TicketStatus.CLOSED
ticket.closed_at = datetime.now()
self.storage.save_ticket(ticket)
# Envoyer un message dans le salon du ticket
close_embed = disnake.Embed(
title=f"🔒 Ticket Fermé - {'Accepté' if accepted else 'Refusé'}",
description=f"Candidature traitée par {inter.user.mention}",
color=color
)
close_embed.add_field(name="Decision", value="✅ Acceptee" if accepted else "❌ Refusee", inline=True)
# Supprimer le salon
try:
await inter.channel.delete(reason=f"Recrutement termine - {'Accepte' if accepted else 'Refuse'} par {inter.user}")
except Exception as e:
await inter.channel.send(embed=close_embed)
print(f"Error deleting recruitment channel: {e}")
except Exception as e:
kuby_logger.error(f"[TicketSystem] _send_recruitment_decision failed: {e}", exc_info=True)
if not inter.response.is_done():
await inter.followup.send("❌ Erreur lors de la decision de recrutement.", ephemeral=True)
# === COMMANDES ===
2026-05-06 17:07:09 +02:00
@commands.slash_command(name="ticket", description="Ouvre le panel des tickets")
async def ticket_main(self, inter: disnake.ApplicationCommandInteraction):
"""Ouvre le panel des tickets"""
2026-05-06 17:07:09 +02:00
config = self.storage.load_config(inter.guild.id)
view = TicketPanelView(self.bot, config, self.storage)
await inter.response.send_message(components=view.get_components_v2(), view=view, ephemeral=True)
2026-05-06 17:07:09 +02:00
@commands.slash_command(name="ticketpanel", description="Envoie le panel de création de tickets dans un canal")
@commands.has_permissions(administrator=True)
2026-05-06 17:07:09 +02:00
async def ticket_panel(self, inter: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel = None):
"""Envoie le panel de création de tickets dans un canal"""
2026-05-06 17:07:09 +02:00
await inter.response.defer(ephemeral=True)
2026-05-06 17:07:09 +02:00
config = self.storage.load_config(inter.guild.id)
if not config.enabled:
2026-05-06 17:07:09 +02:00
await inter.edit_original_response(content="Le système est désactivé.")
return
2026-05-06 17:07:09 +02:00
target = channel or inter.channel
view = TicketPanelView(self.bot, config, self.storage)
await target.send(components=view.get_components_v2())
2026-05-06 17:07:09 +02:00
await inter.edit_original_response(content=f"Panel envoyé dans {target.mention}")
2026-05-06 17:07:09 +02:00
@commands.slash_command(name="ticketconfig", description="Ouvre le panel de configuration complet du système de tickets")
async def ticket_config(self, inter: disnake.ApplicationCommandInteraction):
"""Ouvre le panel de configuration complet du système de tickets"""
2026-05-06 17:07:09 +02:00
await inter.response.defer(ephemeral=True)
2026-01-14 21:32:20 +01:00
# Reload config
2026-05-06 17:07:09 +02:00
config = self.storage.load_config(inter.guild.id)
2026-01-14 21:32:20 +01:00
# Vérifier permissions (admin ou config roles/users)
2026-05-06 17:07:09 +02:00
if not can_access_config(inter, config):
await inter.edit_original_response(content="❌ Cette commande est réservée aux administrateurs ou aux rôles configurés.")
return
try:
# Build config panel with V2 components
config_container = disnake.ui.Container(
disnake.ui.Section(
"# ⚙️ Configuration des Tickets",
accessory=disnake.ui.Button(label="🔄 Activer/Désactiver", style=disnake.ButtonStyle.green if config.enabled else disnake.ButtonStyle.red, custom_id="ticket_config_toggle")
),
disnake.ui.Separator(divider=True),
disnake.ui.TextDisplay(
content=f"**État actuel :** {'✅ Activé' if config.enabled else '❌ Désactivé'}\n"
f"**Catégories :** {len(config.categories)}\n"
f"**Rôles staff :** {len(config.staff_roles)}"
),
disnake.ui.Separator(divider=True),
disnake.ui.Section(
"📂 Gestion des catégories",
accessory=disnake.ui.Button(label="Gérer", style=disnake.ButtonStyle.blurple, custom_id="ticket_config_manage_cat")
),
disnake.ui.Section(
"📝 Logs & Salons",
accessory=disnake.ui.Button(label="Configurer", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_log_channel")
),
disnake.ui.Section(
"👥 Permissions Staff",
accessory=disnake.ui.Button(label="Rôles", style=disnake.ButtonStyle.secondary, custom_id="ticket_config_staff_perms")
)
)
2026-01-14 21:32:20 +01:00
await inter.edit_original_response(components=[config_container])
except Exception as e:
kuby_logger.error(f"❌ Erreur dans ticket_config: {e}", exc_info=True)
await inter.edit_original_response(content=f"❌ Une erreur interne est survenue : {e}")
def setup(bot):
"""Setup function"""
bot.add_cog(TicketCommands(bot))