2026-01-04 00:01:15 +01:00
"""
2026-01-10 15:30:51 +01:00
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
2026-01-04 00:01:15 +01:00
"""
import os
import sys
2026-01-10 15:30:51 +01:00
import io
2026-01-14 21:32:20 +01:00
import asyncio
2026-01-10 15:30:51 +01:00
from datetime import datetime , timedelta
from typing import List , Dict , Any , Optional
from collections import defaultdict
2026-01-04 00:01:15 +01:00
# 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
2026-01-10 15:30:51 +01:00
from PIL import Image , ImageDraw , ImageFont
2026-01-04 00:01:15 +01:00
from . data . storage import get_storage
2026-01-10 15:30:51 +01:00
from . data . models import (
GuildTicketConfig ,
TicketCategory ,
TicketStatus ,
Priority ,
TicketData ,
TicketStats
2026-01-04 00:01:15 +01:00
)
2026-01-10 15:30:51 +01:00
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
2026-05-09 18:42:42 +02:00
# V2 RICH COMPONENTS LOADED - 14:10
kuby_logger . info ( " 🎫 [TicketSystem] Chargement de la version V2 Rich Components... " )
2026-01-04 00:01:15 +01:00
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-01-04 00:01:15 +01:00
2026-05-06 17:07:09 +02:00
class TicketPanelView ( disnake . ui . View ) :
2026-01-10 15:30:51 +01:00
""" Panel principal avec toutes les options intégrées """
def __init__ ( self , bot , config : GuildTicketConfig , storage = None ) :
super ( ) . __init__ ( timeout = None )
2026-01-04 00:01:15 +01:00
self . bot = bot
2026-01-10 15:30:51 +01:00
self . config = config
self . storage = storage or get_storage ( )
2026-05-06 17:07:09 +02:00
self . PURPLE = disnake . Color . blurple ( )
2026-01-10 15:30:51 +01:00
# Boutons du panel
self . _build_panel ( )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
label = " 📊 Analytics " ,
custom_id = " ticket_analytics "
2026-01-04 00:01:15 +01:00
)
2026-01-10 15:30:51 +01:00
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 )
2026-05-09 18:42:42 +02:00
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
2026-01-10 15:30:51 +01:00
# Configuration (admin only)
2026-05-06 17:07:09 +02:00
config_btn = disnake . ui . Button (
style = disnake . ButtonStyle . secondary ,
2026-01-10 15:30:51 +01:00
label = " ⚙️ Configuration " ,
custom_id = " ticket_config "
)
config_btn . callback = self . _config_callback
self . add_item ( config_btn )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
2026-01-04 00:01:15 +01:00
2026-05-06 17:07:09 +02:00
async def _create_callback ( self , interaction : disnake . Interaction ) :
2026-01-10 15:30:51 +01:00
""" 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 )
2026-01-10 15:30:51 +01:00
if not self . config . enabled :
await interaction . response . send_message ( " Le système de tickets est désactivé. " , ephemeral = True )
2026-01-04 00:01:15 +01:00
return
2026-01-10 15:30:51 +01:00
if not self . config . categories :
await interaction . response . send_message ( " Aucune catégorie configurée. Utilisez la configuration. " , ephemeral = True )
2026-01-04 00:01:15 +01:00
return
2026-05-09 18:42:42 +02:00
# Build V2 Container for category selection
sections = [
disnake . ui . TextDisplay ( content = " ## 🎫 Créer un Ticket " ) ,
disnake . ui . Separator ( ) ,
]
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
for cat in self . config . categories [ : 5 ] :
2026-05-09 18:42:42 +02:00
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 ,
2026-05-25 17:19:45 +02:00
custom_id = f " cat| { cat . id } "
2026-05-09 18:42:42 +02:00
)
)
2026-01-10 15:30:51 +01:00
)
2026-01-04 00:01:15 +01:00
2026-05-09 18:42:42 +02:00
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
cat_list = " \n " . join ( f " { cat . emoji } ** { cat . name } ** — { cat . description or ' Aucune description ' } " for cat in self . config . categories [ : 5 ] )
if not interaction . response . is_done ( ) :
await interaction . response . send_message (
f " **🎫 Créer un Ticket** \n \n Sélectionnez une catégorie ci-dessous: " ,
view = CategorySelectionView ( self . bot , self . config ) ,
ephemeral = True
)
2026-01-10 15:30:51 +01:00
2026-05-06 17:07:09 +02:00
async def _my_tickets_callback ( self , interaction : disnake . Interaction ) :
2026-01-10 15:30:51 +01:00
""" 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 )
2026-01-10 15:30:51 +01:00
2026-01-04 00:01:15 +01:00
storage = get_storage ( )
2026-01-10 15:30:51 +01:00
tickets = storage . get_user_tickets ( interaction . guild_id , interaction . user . id , open_only = True )
2026-01-04 00:01:15 +01:00
if not tickets :
2026-01-10 15:30:51 +01:00
await interaction . response . send_message ( " Vous n ' avez aucun ticket ouvert. " , ephemeral = True )
2026-01-04 00:01:15 +01:00
return
2026-05-09 18:42:42 +02:00
# 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 ( ) ,
]
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
for ticket in tickets [ : 5 ] :
channel = interaction . guild . get_channel ( ticket . channel_id )
channel_mention = channel . mention if channel else " Salon supprimé "
2026-05-09 18:42:42 +02:00
sections . append ( disnake . ui . TextDisplay (
content = f " **Ticket # { ticket . channel_id } ** \n Salon: { channel_mention } \n Statut: { ticket . status . value } \n Créé: { ticket . created_at . strftime ( ' %d / % m/ % Y ' ) } "
) )
2026-01-04 00:01:15 +01:00
2026-05-09 18:42:42 +02:00
await interaction . response . send_message (
components = [ disnake . ui . Container ( * sections ) ] ,
ephemeral = True
)
2026-01-10 15:30:51 +01:00
2026-05-06 17:07:09 +02:00
async def _analytics_callback ( self , interaction : disnake . Interaction ) :
2026-01-10 15:30:51 +01:00
""" Affiche les analytics (staff uniquement) """
2026-01-14 21:32:20 +01:00
# Reload config
self . config = self . storage . load_config ( interaction . guild_id )
2026-01-10 15:30:51 +01:00
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-01-10 15:30:51 +01:00
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 ( ) ,
2026-01-10 15:30:51 +01: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-01-10 15:30:51 +01: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-01-10 15:30:51 +01: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-01-10 15:30:51 +01: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
async def _config_callback ( self , interaction : disnake . Interaction ) :
2026-01-10 15:30:51 +01:00
""" 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 )
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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-01-10 15:30:51 +01:00
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-01-10 15:30:51 +01:00
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-01-10 15:30:51 +01:00
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-01-10 15:30:51 +01:00
def _calculate_stats ( self , guild_id : int ) - > TicketStats :
""" Calcule les statistiques """
2026-01-04 00:01:15 +01:00
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-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
""" 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
2026-01-10 15:30:51 +01:00
self . bot = bot
self . config = config
2026-01-14 21:32:20 +01:00
self . id_gen = int ( datetime . now ( ) . timestamp ( ) )
2026-01-10 15:30:51 +01:00
# 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-01-10 15:30:51 +01:00
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-01-10 15:30:51 +01:00
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-01-10 15:30:51 +01:00
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-01-04 00:01:15 +01:00
2026-05-06 17:07:09 +02:00
async def _on_category_select ( self , interaction : disnake . Interaction , category_id : str ) :
2026-01-10 15:30:51 +01:00
""" Handle category selection """
2026-01-14 21:32:20 +01:00
try :
2026-02-07 14:42:39 +01:00
2026-01-10 15:30:51 +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 )
2026-05-09 18:42:42 +02:00
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
2026-05-22 14:26:02 +02:00
# 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-01-10 15:30:51 +01:00
2026-05-06 17:07:09 +02:00
class TicketPriorityModal ( disnake . ui . Modal ) :
2026-05-09 18:42:42 +02:00
""" 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 ] )
2026-01-10 15:30:51 +01:00
self . bot = bot
self . config = config
self . category = category
2026-05-09 18:42:42 +02:00
async def callback ( self , interaction : disnake . ModalInteraction ) :
await interaction . response . defer ( ephemeral = True )
2026-01-10 15:30:51 +01:00
try :
# Map priority
priority_map = {
" Urgente " : Priority . URGENT ,
" Haute " : Priority . HIGH ,
" Normale " : Priority . NORMAL
}
2026-05-09 18:42:42 +02:00
# Logic updated to use reason input
priority = Priority . NORMAL
reason = interaction . text_values . get ( " ticket_reason " , " Aucun motif fourni " )
2026-01-10 15:30:51 +01:00
# Create ticket
2026-05-09 18:42:42 +02:00
await self . _create_ticket ( interaction , priority , reason )
2026-01-10 15:30:51 +01:00
except Exception as e :
2026-05-09 18:42:42 +02:00
await interaction . followup . send ( f " Erreur lors de la création du ticket: { e } " , ephemeral = True )
2026-01-10 15:30:51 +01:00
2026-05-06 17:07:09 +02:00
async def on_error ( self , interaction : disnake . Interaction , error : Exception ) :
2026-01-10 15:30:51 +01:00
""" 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-22 14:26:02 +02:00
2026-05-06 17:07:09 +02:00
async def _create_ticket ( self , interaction : disnake . Interaction , priority : Priority , reason : str ) :
2026-01-10 15:30:51 +01:00
""" Crée le ticket """
guild = interaction . guild
user = interaction . user
2026-01-04 00:01:15 +01:00
storage = get_storage ( )
2026-01-10 15:30:51 +01:00
# 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-01-10 15:30:51 +01:00
}
2026-01-04 00:01:15 +01:00
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
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
# 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 } "
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
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 ) )
2026-05-09 18:42:42 +02:00
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 :
2026-01-10 15:30:51 +01:00
try :
category_channel = guild . get_channel ( int ( self . config . default_category ) )
2026-05-09 18:42:42 +02:00
if category_channel and not isinstance ( category_channel , disnake . CategoryChannel ) :
category_channel = None
2026-01-10 15:30:51 +01:00
except ( ValueError , TypeError ) :
pass
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
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 } "
2026-01-04 00:01:15 +01:00
)
2026-01-10 15:30:51 +01:00
except Exception as e :
2026-05-09 18:42:42 +02:00
await interaction . followup . send ( f " Erreur: { e } " , ephemeral = True )
2026-01-10 15:30:51 +01:00
return
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
# 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 )
2026-01-04 00:01:15 +01:00
2026-05-09 18:42:42 +02:00
# 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 )
2026-05-09 18:42:42 +02:00
# 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 ] )
2026-01-10 15:30:51 +01:00
# 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 (
2026-01-10 15:30:51 +01:00
title = " Nouveau Ticket " ,
2026-05-06 17:07:09 +02:00
color = disnake . Color . green ( ) ,
2026-01-10 15:30:51 +01:00
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 )
2026-01-04 00:01:15 +01:00
2026-05-09 18:42:42 +02:00
await interaction . followup . send ( f " Ticket créé: { ticket_channel . mention } " , ephemeral = True )
2026-01-10 15:30:51 +01:00
2026-05-22 14:26:02 +02:00
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 \n Merci 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 ) :
2026-05-09 18:42:42 +02:00
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
2026-05-09 18:42:42 +02:00
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 )
2026-05-09 18:42:42 +02:00
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 \n Votre 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 ) :
2026-05-09 18:42:42 +02:00
""" Gestion du ticket (dans le salon du ticket) - Uniquement pour les callbacks désormais """
2026-01-10 15:30:51 +01:00
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-01-04 00:01:15 +01:00
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 """
2026-05-09 18:42:42 +02:00
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
2026-05-09 18:42:42 +02:00
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-01-04 00:01:15 +01:00
2026-05-06 17:07:09 +02:00
async def _transcript_callback ( self , interaction : disnake . Interaction ) :
2026-01-10 15:30:51 +01:00
""" Génère le transcript """
if not self . _is_staff ( interaction ) :
await interaction . response . send_message ( " Réservé au staff. " , ephemeral = True )
return
2026-01-04 00:01:15 +01:00
storage = get_storage ( )
2026-01-10 15:30:51 +01:00
ticket = storage . load_ticket ( interaction . channel . id )
2026-01-04 00:01:15 +01:00
if not ticket :
2026-01-10 15:30:51 +01:00
await interaction . response . send_message ( " Ticket non trouvé. " , ephemeral = True )
2026-01-04 00:01:15 +01:00
return
2026-01-10 15:30:51 +01:00
# 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 ) )
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
""" 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 )
2026-01-10 15:30:51 +01:00
return
2026-05-01 16:16:33 +02:00
old_claimer = self . ticket . claimed_by
2026-01-10 15:30:51 +01:00
self . ticket . claimed_by = interaction . user . id
self . ticket . status = TicketStatus . CLAIMED
self . storage . save_ticket ( self . ticket )
2026-05-09 18:42:42 +02:00
# 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-01-10 15:30:51 +01:00
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 )
2026-01-10 15:30:51 +01:00
await interaction . channel . send ( embed = embed )
2026-05-06 17:07:09 +02:00
async def _reopen_callback ( self , interaction : disnake . Interaction ) :
2026-01-10 15:30:51 +01:00
""" 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 (
2026-01-10 15:30:51 +01:00
read_messages = True ,
send_messages = True
)
await interaction . channel . edit (
name = interaction . channel . name . replace ( " closed- " , " " ) ,
overwrites = overwrites
)
2026-05-09 18:42:42 +02:00
# Update UI V2
try :
await interaction . response . edit_message ( components = get_ticket_management_container ( self . ticket , self . category , self . config ) )
except disnake . NotFound :
pass
2026-01-10 15:30:51 +01:00
embed = TicketEmbedFormatter . ticket_reopened ( self . ticket , interaction . user )
await interaction . channel . send ( embed = embed )
2026-05-09 18:42:42 +02:00
# 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-01-10 15:30:51 +01:00
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 " )
2026-05-09 18:42:42 +02:00
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
2026-05-09 18:42:42 +02:00
# 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 )
2026-05-09 18:42:42 +02:00
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 " )
2026-05-09 18:42:42 +02:00
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 } " )
2026-05-09 22:11:17 +02:00
# 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 \n Merci 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-01-10 15:30:51 +01:00
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é. \n Merci 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 } " )
2026-05-19 19:51:36 +02:00
# 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
2026-01-10 15:30:51 +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
2026-01-10 15:30:51 +01:00
return False
2026-05-06 17:07:09 +02:00
class CloseTicketModal ( disnake . ui . Modal ) :
2026-01-10 15:30:51 +01:00
""" Modal pour fermer un ticket """
def __init__ ( self , bot , storage , config : GuildTicketConfig , ticket : TicketData ) :
2026-05-09 18:42:42 +02:00
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 ] )
2026-01-10 15:30:51 +01:00
self . bot = bot
self . storage = storage
self . config = config
self . ticket = ticket
self . category = config . get_category ( ticket . category_id )
2026-05-09 18:42:42 +02:00
async def callback ( self , interaction : disnake . ModalInteraction ) :
2026-01-04 00:01:15 +01:00
# Check permissions
can_close = (
2026-01-10 15:30:51 +01:00
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 )
2026-05-09 18:42:42 +02:00
# 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 )
2026-01-10 15:30:51 +01:00
)
# 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 :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +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-10 15:30:51 +01:00
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
2026-01-10 15:30:51 +01:00
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
2026-01-10 15:30:51 +01:00
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 )
2026-05-09 18:42:42 +02:00
if self . reason_input . value :
log_embed . add_field ( name = " Raison de fermeture " , value = self . reason_input . value , inline = False )
2026-01-10 15:30:51 +01:00
# 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 )
2026-01-10 15:30:51 +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 } " )
# 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-01-10 15:30:51 +01:00
2026-05-06 17:07:09 +02:00
class TicketSetupView ( disnake . ui . View ) :
2026-01-10 15:30:51 +01:00
""" 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 ,
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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
2026-01-10 15:30:51 +01:00
await interaction . message . edit ( view = self )
2026-05-06 17:07:09 +02:00
async def _roles_callback ( self , interaction : disnake . Interaction ) :
2026-01-10 15:30:51 +01:00
view = StaffRolesSetupView ( self . bot , self . storage , self . config , interaction . guild )
2026-05-06 17:07:09 +02:00
embed = disnake . Embed (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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 ) :
2026-01-10 15:30:51 +01:00
view = CategoriesSetupView ( self . bot , self . storage , self . config )
2026-05-06 17:07:09 +02:00
embed = disnake . Embed (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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 ) :
2026-01-10 15:30:51 +01:00
view = ConfigRolesSetupView ( self . bot , self . storage , self . config , interaction . guild )
2026-05-06 17:07:09 +02:00
embed = disnake . Embed (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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 ) :
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
view = AdvancedSetupView ( self . bot , self . storage , self . config )
2026-05-06 17:07:09 +02:00
embed = disnake . Embed (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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 ) :
2026-01-10 15:30:51 +01:00
""" Configuration des rôles staff """
2026-05-06 17:07:09 +02:00
def __init__ ( self , bot , storage , config : GuildTicketConfig , guild : disnake . Guild ) :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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 ) :
2026-01-10 15:30:51 +01:00
""" Configuration des rôles config """
2026-05-06 17:07:09 +02:00
def __init__ ( self , bot , storage , config : GuildTicketConfig , guild : disnake . Guild ) :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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 ) :
2026-01-10 15:30:51 +01:00
""" 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 ,
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
label = cat . name ,
value = cat . id ,
emoji = cat . emoji
)
)
2026-05-06 17:07:09 +02:00
select = disnake . ui . Select (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
view . add_item ( select )
2026-05-06 17:07:09 +02:00
embed = disnake . Embed (
2026-01-10 15:30:51 +01:00
title = " Supprimer une Catégorie " ,
description = " Sélectionnez la catégorie à supprimer: " ,
2026-05-06 17:07:09 +02:00
color = disnake . Color . red ( )
2026-01-10 15:30:51 +01:00
)
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 ) :
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
""" 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 ,
2026-01-10 15:30:51 +01:00
label = f " { ' ✅ ' if config . claim_enabled else ' ❌ ' } Système de Réclamation " ,
custom_id = " advanced_claim "
2026-01-04 00:01:15 +01:00
)
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
label = f " { ' ✅ ' if config . survey_enabled else ' ❌ ' } Sondages " ,
custom_id = " advanced_survey "
2026-01-04 00:01:15 +01:00
)
2026-01-10 15:30:51 +01:00
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 ,
2026-01-10 15:30:51 +01:00
label = f " 📊 Limite: { config . max_tickets_per_user } " ,
custom_id = " advanced_max "
2026-01-04 00:01:15 +01:00
)
2026-01-10 15:30:51 +01:00
max_btn . callback = self . _max_callback
self . add_item ( max_btn )
2026-01-04 00:01:15 +01:00
2026-05-06 17:07:09 +02:00
async def _claim_callback ( self , interaction : disnake . Interaction ) :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
""" Modal pour définir la limite de tickets par utilisateur """
2026-01-04 00:01:15 +01:00
def __init__ ( self , bot , storage , config : GuildTicketConfig ) :
2026-05-06 17:07:09 +02:00
self . max_tickets = disnake . ui . TextInput (
2026-01-10 15:30:51 +01:00
label = " Nombre maximum de tickets ouverts par utilisateur " ,
placeholder = " 5 " ,
default = str ( config . max_tickets_per_user ) ,
required = True ,
max_length = 2
)
2026-05-09 18:42:42 +02:00
super ( ) . __init__ ( title = " Limite de Tickets " , components = [ self . max_tickets ] )
self . bot = bot
self . storage = storage
self . config = config
2026-01-10 15:30:51 +01:00
2026-05-09 18:42:42 +02:00
async def callback ( self , interaction : disnake . ModalInteraction ) :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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 ) :
2026-01-10 15:30:51 +01:00
""" 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 ,
2026-01-10 15:30:51 +01:00
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 )
2026-03-28 21:58:44 +01:00
# Gérer les catégories
2026-05-06 17:07:09 +02:00
self . manage_cat_btn = disnake . ui . Button (
style = disnake . ButtonStyle . blurple ,
2026-03-28 21:58:44 +01:00
label = " 📂 Gérer les catégories " ,
custom_id = " config_manage_cat "
2026-01-10 15:30:51 +01:00
)
2026-03-28 21:58:44 +01:00
self . manage_cat_btn . callback = self . _manage_cat_callback
self . add_item ( self . manage_cat_btn )
2026-01-10 15:30:51 +01:00
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-10 15:30:51 +01:00
)
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 )
2026-01-10 15:30:51 +01:00
# Log channel
2026-05-06 17:07:09 +02:00
log_btn = disnake . ui . Button (
style = disnake . ButtonStyle . secondary ,
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
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
2026-01-10 15:30:51 +01:00
await interaction . message . edit ( view = self )
2026-05-06 17:07:09 +02:00
async def _manage_cat_callback ( self , interaction : disnake . Interaction ) :
2026-03-28 21:58:44 +01:00
view = CategoryManagerView ( self . bot , self . storage , self . config )
await interaction . response . edit_message ( embed = view . _get_embed ( ) , view = view )
2026-01-10 15:30:51 +01:00
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-10 15:30:51 +01:00
)
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-10 15:30:51 +01:00
2026-01-14 21:32:20 +01:00
def _add_current_perms_fields ( self , embed , roles , users , guild ) :
2026-01-04 00:01:15 +01:00
role_mentions = [ ]
2026-01-14 21:32:20 +01:00
for role_id in roles :
role = guild . get_role ( int ( role_id ) )
2026-01-04 00:01:15 +01:00
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-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
modal = SetChannelModal ( self . bot , self . storage , self . config , " log " )
2026-01-04 00:01:15 +01:00
await interaction . response . send_modal ( modal )
2026-05-06 17:07:09 +02:00
class AddCategoryModal ( disnake . ui . Modal ) :
2026-01-10 15:30:51 +01:00
""" Modal pour ajouter une catégorie """
2026-01-04 00:01:15 +01:00
def __init__ ( self , bot , storage , config : GuildTicketConfig ) :
2026-05-09 18:42:42 +02:00
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 )
2026-05-22 14:26:02 +02:00
# 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? \n Pourquoi voulez-vous rejoindre? \n Experiences 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
]
)
2026-01-04 00:01:15 +01:00
self . bot = bot
self . storage = storage
self . config = config
2026-05-22 14:26:02 +02:00
2026-05-09 18:42:42 +02:00
async def callback ( self , interaction : disnake . ModalInteraction ) :
2026-01-04 00:01:15 +01:00
import uuid
2026-05-22 14:26:02 +02:00
2026-01-04 00:01:15 +01:00
cat_id = str ( uuid . uuid4 ( ) ) [ : 8 ]
2026-05-22 14:26:02 +02:00
2026-05-09 18:42:42 +02:00
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 " , " 🎫 " )
2026-05-22 14:26:02 +02:00
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 )
2026-01-04 00:01:15 +01:00
category = TicketCategory (
id = cat_id ,
2026-05-09 18:42:42 +02:00
name = name_val ,
description = desc_val ,
2026-05-22 14:26:02 +02:00
emoji = emoji_val ,
is_recruitment = is_recruitment ,
questions = questions ,
responses_channel_id = responses_channel_id
2026-01-04 00:01:15 +01:00
)
2026-05-22 14:26:02 +02:00
2026-01-04 00:01:15 +01:00
self . config . categories . append ( category )
2026-01-10 15:30:51 +01:00
self . storage . save_config ( interaction . guild_id , self . config )
2026-05-22 14:26:02 +02:00
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-01-10 15:30:51 +01:00
2026-05-06 17:07:09 +02:00
class EditCategoryModal ( disnake . ui . Modal ) :
2026-03-28 21:58:44 +01:00
""" Modal pour modifier une catégorie existante """
def __init__ ( self , bot , storage , config , category : TicketCategory , parent_view ) :
2026-05-09 18:42:42 +02:00
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 ] )
2026-03-28 21:58:44 +01:00
self . bot = bot
self . storage = storage
self . config = config
self . category = category
self . parent_view = parent_view
2026-05-09 18:42:42 +02:00
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-03-28 21:58:44 +01:00
2026-05-06 17:07:09 +02:00
class CategoryManagerView ( disnake . ui . View ) :
2026-05-09 18:42:42 +02:00
""" Vue pour lister et gérer les catégories (Modernisée V2) """
2026-03-28 21:58:44 +01:00
def __init__ ( self , bot , storage , config : GuildTicketConfig ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
2026-05-09 18:42:42 +02:00
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 )
]
2026-03-28 21:58:44 +01:00
2026-05-09 18:42:42 +02:00
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 } " )
)
)
2026-03-28 21:58:44 +01:00
2026-05-09 18:42:42 +02:00
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 " )
) )
2026-03-28 21:58:44 +01:00
2026-05-09 18:42:42 +02:00
return [ disnake . ui . Container ( * sections ) ]
2026-03-28 21:58:44 +01:00
def _get_embed ( self ) :
2026-05-06 17:07:09 +02:00
embed = disnake . Embed (
2026-03-28 21:58:44 +01:00
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 ( )
2026-03-28 21:58:44 +01:00
)
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 ) :
2026-03-28 21:58:44 +01:00
# 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 ) :
2026-03-28 21:58:44 +01:00
# 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
2026-05-09 18:42:42 +02:00
async def callback ( self , interaction : disnake . ModalInteraction ) :
2026-03-28 21:58:44 +01:00
import uuid
cat_id = str ( uuid . uuid4 ( ) ) [ : 8 ]
category = TicketCategory (
id = cat_id ,
name = self . name . value ,
description = self . description . value or " " ,
emoji = self . emoji . value or " 🎫 "
)
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 ) :
2026-03-28 21:58:44 +01:00
view = ConfigPanelView ( self . bot , self . storage , self . config )
2026-05-06 17:07:09 +02:00
embed = disnake . Embed (
2026-03-28 21:58:44 +01:00
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 ( )
2026-03-28 21:58:44 +01:00
)
await interaction . response . edit_message ( embed = embed , view = view )
2026-05-06 17:07:09 +02:00
class CategoryActionView ( disnake . ui . View ) :
2026-03-28 21:58:44 +01:00
""" 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 (
2026-03-28 21:58:44 +01:00
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 ( )
2026-03-28 21:58:44 +01:00
)
return embed
2026-05-06 17:07:09 +02:00
@disnake.ui.button ( label = " 📝 Modifier " , style = disnake . ButtonStyle . primary )
2026-05-09 18:42:42 +02:00
async def edit_callback ( self , interaction : disnake . MessageInteraction ) :
2026-03-28 21:58:44 +01:00
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 )
2026-05-09 18:42:42 +02:00
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 )
2026-05-09 18:42:42 +02:00
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 )
2026-05-09 18:42:42 +02:00
async def delete_callback ( self , interaction : disnake . MessageInteraction ) :
2026-03-28 21:58:44 +01:00
view = DeleteCategoryConfirmationView ( self . bot , self . storage , self . config , self . category , self . parent_view )
2026-05-06 17:07:09 +02:00
embed = disnake . Embed (
2026-03-28 21:58:44 +01:00
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 ( )
2026-03-28 21:58:44 +01:00
)
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 )
2026-05-09 18:42:42 +02:00
async def back_callback ( self , interaction : disnake . MessageInteraction ) :
2026-03-28 21:58:44 +01:00
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 )
2026-05-09 18:42:42 +02:00
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 )
2026-05-09 18:42:42 +02:00
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 " )
2026-05-09 18:42:42 +02:00
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 )
2026-05-09 18:42:42 +02:00
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 )
2026-05-09 18:42:42 +02:00
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-03-28 21:58:44 +01:00
2026-05-06 17:07:09 +02:00
class DeleteCategoryConfirmationView ( disnake . ui . View ) :
2026-03-28 21:58:44 +01:00
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 )
2026-05-09 18:42:42 +02:00
async def confirm_callback ( self , interaction : disnake . MessageInteraction ) :
2026-03-28 21:58:44 +01:00
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 )
2026-05-09 18:42:42 +02:00
async def cancel_callback ( self , interaction : disnake . MessageInteraction ) :
2026-03-28 21:58:44 +01:00
view = CategoryActionView ( self . bot , self . storage , self . config , self . category , self . parent_view )
await interaction . response . edit_message ( embed = view . _get_embed ( ) , view = view )
2026-05-09 18:42:42 +02:00
class TicketChannelConfigView ( disnake . ui . View ) :
""" Vue moderne pour configurer un salon via un sélecteur (V2) """
2026-01-10 15:30:51 +01:00
def __init__ ( self , bot , storage , config : GuildTicketConfig , channel_type : str = " log " ) :
2026-05-09 18:42:42 +02:00
super ( ) . __init__ ( timeout = None )
2026-01-10 15:30:51 +01:00
self . bot = bot
self . storage = storage
self . config = config
self . channel_type = channel_type
2026-05-09 18:42:42 +02:00
# Sélecteur de salon
2026-05-09 22:11:17 +02:00
# (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 ]
2026-05-09 18:42:42 +02:00
select = disnake . ui . ChannelSelect (
placeholder = " Sélectionnez la catégorie dans la liste... " ,
2026-05-09 22:11:17 +02:00
channel_types = c_types ,
2026-05-09 18:42:42 +02:00
min_values = 1 ,
max_values = 1 ,
custom_id = " ticket_config_chan_select "
)
2026-05-09 22:11:17 +02:00
# Utiliser uniquement l'instance 'select' (elle sera incluse dans get_components_v2)
self . add_item ( select )
2026-05-09 18:42:42 +02:00
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 " ) )
]
2026-01-10 15:30:51 +01:00
2026-05-09 18:42:42 +02:00
c_types = [ disnake . ChannelType . text ] if self . channel_type == " log " else [ disnake . ChannelType . category ]
2026-01-10 15:30:51 +01:00
2026-05-09 18:42:42 +02:00
# 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 )
2026-01-10 15:30:51 +01:00
try :
2026-05-09 18:42:42 +02:00
await inter . response . send_message ( f " ❌ Erreur lors de la configuration du salon : { e } " , ephemeral = True )
except :
2026-01-10 15:30:51 +01:00
pass
2026-05-09 18:42:42 +02:00
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 ( )
2026-01-10 15:30:51 +01:00
)
2026-05-09 18:42:42 +02:00
await interaction . response . send_message ( embed = embed , ephemeral = True )
except ValueError :
await interaction . response . send_message ( " ❌ ID invalide. " , ephemeral = True )
2026-01-04 00:01:15 +01:00
2026-05-06 17:07:09 +02:00
class StaffRolesModal ( disnake . ui . Modal ) :
2026-01-10 15:30:51 +01:00
""" Modal pour ajouter/supprimer les rôles staff """
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
def __init__ ( self , bot , storage , config : GuildTicketConfig ) :
super ( ) . __init__ ( title = " Gérer les Rôles Staff " )
self . bot = bot
2026-01-04 00:01:15 +01:00
self . storage = storage
self . config = config
2026-01-10 15:30:51 +01:00
# Champ pour entrer les rôles (mention ou ID)
2026-05-06 17:07:09 +02:00
self . roles = disnake . ui . TextInput (
2026-01-10 15:30:51 +01:00
label = " Rôles Staff (mention ou ID) " ,
placeholder = " @Staff @Modérateurs ou 123456789 987654321 " ,
required = True ,
max_length = 500
2026-01-04 00:01:15 +01:00
)
2026-01-10 15:30:51 +01:00
self . add_item ( self . roles )
2026-01-04 00:01:15 +01:00
2026-05-09 18:42:42 +02:00
async def callback ( self , interaction : disnake . ModalInteraction ) :
2026-01-04 00:01:15 +01:00
try :
2026-01-10 15:30:51 +01:00
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 (
2026-01-10 15:30:51 +01:00
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 ( )
2026-01-10 15:30:51 +01:00
)
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 ) :
2026-01-10 15:30:51 +01:00
""" 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-01-10 15:30:51 +01:00
2026-05-06 17:07:09 +02:00
def __init__ ( self , bot , storage , config : GuildTicketConfig , guild : disnake . Guild , config_type : str = " staff " ) :
2026-01-10 15:30:51 +01:00
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-10 15:30:51 +01:00
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 ,
2026-05-09 18:42:42 +02:00
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 ,
2026-05-09 18:42:42 +02:00
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
2026-05-23 12:48:08 +02:00
elif self . config_type == " config " :
2026-02-07 14:42:39 +01:00
target_list = self . config . config_roles
2026-05-23 12:48:08 +02:00
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-05-23 12:48:08 +02:00
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-05-23 12:48:08 +02:00
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-01-10 15:30:51 +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
2026-05-23 12:48:08 +02:00
elif self . config_type == " config " :
2026-02-07 14:42:39 +01:00
target_list = self . config . config_users
2026-05-23 12:48:08 +02:00
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-05-23 12:48:08 +02:00
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-05-23 12:48:08 +02:00
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 ( )
2026-05-09 18:42:42 +02:00
await interaction . response . edit_message ( components = self . get_components_v2 ( ) )
def get_components_v2 ( self ) :
2026-05-23 12:48:08 +02:00
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 } ' . "
2026-05-09 18:42:42 +02:00
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
2026-05-09 18:42:42 +02: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
2026-01-10 15:30:51 +01:00
options = [ ]
2026-05-23 12:48:08 +02:00
2026-01-14 21:32:20 +01:00
if self . config_type == " staff " :
roles = self . config . staff_roles
users = self . config . staff_users
2026-05-23 12:48:08 +02:00
elif self . config_type == " config " :
2026-01-14 21:32:20 +01:00
roles = self . config . config_roles
users = self . config . config_users
2026-05-23 12:48:08 +02:00
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-05-23 12:48:08 +02:00
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-05-23 12:48:08 +02:00
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
2026-05-23 12:48:08 +02:00
2026-01-10 15:30:51 +01:00
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 ,
2026-05-09 18:42:42 +02:00
custom_id = f " ticket_config_perms_remove_ { self . config_type } "
2026-01-10 15:30:51 +01:00
)
2026-01-14 21:32:20 +01:00
select . callback = self . _remove_callback
2026-01-10 15:30:51 +01:00
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-05-23 12:48:08 +02:00
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 )
2026-05-23 12:48:08 +02:00
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
2026-05-23 12:48:08 +02:00
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-05-23 12:48:08 +02:00
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 )
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
pass
async def cog_load ( self ) :
await self . _register_all_views ( )
2026-05-09 18:42:42 +02:00
@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_* / cat_* / close_* custom_ids
if not (
str ( custom_id ) . startswith ( " ticket_ " )
or str ( custom_id ) . startswith ( " cat_ " )
2026-05-22 14:26:02 +02:00
or str ( custom_id ) in { " close_confirm " , " close_cancel " , " delay_cancel " , " recruitment_close " , " recruitment_accept " , " recruitment_refuse " }
2026-05-09 18:42:42 +02:00
) :
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
if custom_id == " ticket_create " :
view = TicketPanelView ( self . bot , config , self . storage )
await view . _create_callback ( inter )
elif custom_id == " ticket_my_tickets " :
view = TicketPanelView ( self . bot , config , self . storage )
await view . _my_tickets_callback ( inter )
elif custom_id == " ticket_analytics " :
view = TicketPanelView ( self . bot , config , self . storage )
await view . _analytics_callback ( inter )
2026-05-23 10:58:21 +02:00
# ✅ BUGFIX: Sélection de catégorie en V2 => custom_id = cat|<catid>|<suffix>
elif str ( custom_id ) . startswith ( " cat| " ) :
2026-05-09 18:42:42 +02:00
try :
2026-05-23 10:58:21 +02:00
# format: cat|<catid>|<suffix>
parts = str ( custom_id ) . split ( " | " )
if len ( parts ) < 3 :
await inter . response . send_message ( " Catégorie non trouvée. " , ephemeral = True )
return
2026-05-09 18:42:42 +02:00
2026-05-23 10:58:21 +02:00
cat_id = parts [ 1 ]
category = next ( ( c for c in config . categories if str ( c . id ) == str ( cat_id ) ) , None )
2026-05-09 18:42:42 +02:00
if not category :
await inter . response . send_message ( " Catégorie non trouvée. " , ephemeral = True )
return
2026-05-22 14:26:02 +02:00
# 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 )
2026-05-23 10:58:21 +02:00
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 )
2026-05-09 18:42:42 +02:00
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 )
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 )
2026-05-22 14:26:02 +02:00
# === 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 )
2026-05-09 18:42:42 +02:00
# === 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 \n Merci 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 \n Merci 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_ " ) :
2026-05-09 22:11:17 +02:00
# 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
)
2026-05-09 18:42:42 +02:00
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 ) ,
2026-05-09 22:11:17 +02:00
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 } "
)
) ,
2026-05-09 18:42:42 +02:00
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 )
2026-05-09 22:11:17 +02:00
# ⚠️ 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 l’ indexation.
# Ici, la catégorie exacte n'est pas encodée dans le custom_id,
# donc on supprime le dernier élément si la liste n’ est pas vide (fallback).
# (Meilleur: corriger l’ encodage 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 )
2026-05-09 18:42:42 +02:00
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
2026-01-10 15:30:51 +01:00
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 } " )
2026-05-22 14:26:02 +02:00
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 )
2026-01-10 15:30:51 +01:00
# === 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 ) :
2026-01-10 15:30:51 +01:00
""" Ouvre le panel des tickets """
2026-05-06 17:07:09 +02:00
config = self . storage . load_config ( inter . guild . id )
2026-01-10 15:30:51 +01:00
view = TicketPanelView ( self . bot , config , self . storage )
2026-05-25 17:19:45 +02:00
await inter . response . send_message ( components = view . get_components_v2 ( ) , view = view , ephemeral = True )
2026-01-10 15:30:51 +01:00
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 " )
2026-01-10 15:30:51 +01:00
@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 ) :
2026-01-10 15:30:51 +01:00
""" 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-01-10 15:30:51 +01:00
2026-05-06 17:07:09 +02:00
config = self . storage . load_config ( inter . guild . id )
2026-01-10 15:30:51 +01:00
if not config . enabled :
2026-05-06 17:07:09 +02:00
await inter . edit_original_response ( content = " Le système est désactivé. " )
2026-01-10 15:30:51 +01:00
return
2026-05-06 17:07:09 +02:00
target = channel or inter . channel
2026-01-10 15:30:51 +01:00
view = TicketPanelView ( self . bot , config , self . storage )
2026-05-09 18:42:42 +02:00
await target . send ( components = view . get_components_v2 ( ) )
2026-01-10 15:30:51 +01:00
2026-05-06 17:07:09 +02:00
await inter . edit_original_response ( content = f " Panel envoyé dans { target . mention } " )
2026-01-10 15:30:51 +01:00
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 ) :
2026-01-10 15:30:51 +01:00
""" 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-10 15:30:51 +01:00
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-10 15:30:51 +01:00
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. " )
2026-01-10 15:30:51 +01:00
return
2026-05-09 18:42:42 +02:00
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
2026-05-09 18:42:42 +02: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 } " )
2026-01-04 00:01:15 +01:00
2026-05-09 18:42:42 +02:00
def setup ( bot ) :
2026-01-10 15:30:51 +01:00
""" Setup function """
2026-05-09 18:42:42 +02:00
bot . add_cog ( TicketCommands ( bot ) )
2026-01-04 00:01:15 +01:00