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
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__ ) ) ) ) )
import discord
from discord . ext import commands , tasks
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-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
class TicketPanelView ( discord . ui . View ) :
""" 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 ( )
self . PURPLE = discord . Color . blurple ( )
# 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
create_btn = discord . ui . Button (
style = discord . ButtonStyle . blurple ,
label = " 🎫 Créer un ticket " ,
custom_id = " ticket_create "
)
create_btn . callback = self . _create_callback
self . add_item ( create_btn )
# Mes tickets (si l'utilisateur a des tickets)
my_tickets_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
label = " 📂 Mes tickets " ,
custom_id = " ticket_my_tickets "
)
my_tickets_btn . callback = self . _my_tickets_callback
self . add_item ( my_tickets_btn )
# Analytics (staff only)
analytics_btn = discord . ui . Button (
style = discord . ButtonStyle . primary ,
label = " 📊 Analytics " ,
custom_id = " ticket_analytics "
2026-01-04 00:01:15 +01:00
)
2026-01-10 15:30:51 +01:00
analytics_btn . callback = self . _analytics_callback
self . add_item ( analytics_btn )
# Configuration (admin only)
config_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
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-01-10 15:30:51 +01:00
async def _create_callback ( self , interaction : discord . Interaction ) :
""" Affiche les catégories pour créer un ticket """
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 )
2026-01-04 00:01:15 +01:00
return
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-01-10 15:30:51 +01:00
# Créer view avec les catégories
view = CategorySelectionView ( self . bot , self . config )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
embed = discord . Embed (
title = " Créer un Ticket " ,
description = " Sélectionnez une catégorie ci-dessous: " ,
color = discord . Color . blurple ( )
)
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
for cat in self . config . categories [ : 5 ] :
embed . add_field (
name = f " { cat . emoji } { cat . name } " ,
value = cat . description or " Aucune description " ,
inline = True
)
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
await interaction . response . send_message ( embed = embed , view = view , ephemeral = True )
async def _my_tickets_callback ( self , interaction : discord . Interaction ) :
""" Affiche les tickets de l ' utilisateur """
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
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-01-10 15:30:51 +01:00
embed = discord . Embed (
title = " Vos Tickets Ouverts " ,
description = f " Vous avez ** { len ( tickets ) } ** ticket(s) ouvert(s): " ,
color = discord . Color . blue ( )
)
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é "
embed . add_field (
name = f " Ticket # { ticket . channel_id } " ,
value = f " Salon: { channel_mention } \n Statut: { ticket . status . value } \n Créé: { ticket . created_at . strftime ( ' %d / % m/ % Y ' ) } " ,
inline = True
)
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
await interaction . response . send_message ( embed = embed , ephemeral = True )
async def _analytics_callback ( self , interaction : discord . Interaction ) :
""" Affiche les analytics (staff uniquement) """
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
embed = discord . Embed (
title = " 📊 Analytics des Tickets " ,
description = " Statistiques détaillées des tickets et performances du staff " ,
color = discord . Color . blue ( ) ,
timestamp = datetime . now ( )
)
# Calculate overall stats
tickets = self . storage . get_guild_tickets ( interaction . guild_id )
total_tickets = len ( tickets )
open_tickets = sum ( 1 for t in tickets if t . status in [ TicketStatus . OPEN , TicketStatus . CLAIMED ] )
closed_tickets = sum ( 1 for t in tickets if t . status == TicketStatus . CLOSED )
claimed_tickets = sum ( 1 for t in tickets if t . claimed_by is not None )
# Basic stats
embed . add_field (
name = " 📈 Statistiques Générales " ,
value = f " **Total:** { total_tickets } \n "
f " **Ouverts:** { open_tickets } \n "
f " **Fermés:** { closed_tickets } \n "
f " **Réclamés:** { claimed_tickets } " ,
inline = True
)
# Staff performance summary
if staff_list :
total_ratings = sum ( s [ ' total_ratings ' ] for s in staff_list )
avg_rating = sum ( s [ ' average_rating ' ] for s in staff_list if s [ ' total_ratings ' ] > 0 ) / len ( [ s for s in staff_list if s [ ' total_ratings ' ] > 0 ] ) if any ( s [ ' total_ratings ' ] > 0 for s in staff_list ) else 0
embed . add_field (
name = " 👥 Équipe Staff " ,
value = f " **Membres actifs:** { len ( staff_list ) } \n "
f " **Évaluations totales:** { total_ratings } \n "
f " **Note moyenne globale:** { avg_rating : .1f } ⭐ " if avg_rating > 0 else " **Note moyenne globale:** N/A " ,
inline = True
)
# Send main embed
await interaction . followup . send ( embed = embed , ephemeral = True )
# Create and send staff rating chart
try :
if staff_list :
rating_chart = analytics . create_staff_rating_chart ( staff_list )
await interaction . followup . send (
" 📊 **Évaluations des Membres du Staff** " ,
file = discord . File ( rating_chart , ' staff_ratings.png ' ) ,
ephemeral = True
)
# Add individual staff stats view
stats_view = StaffStatsView ( self . bot , staff_list , analytics )
stats_embed = discord . Embed (
title = " 📈 Statistiques Individuelles " ,
description = " Cliquez sur un membre du staff pour voir ses statistiques détaillées: " ,
color = discord . Color . purple ( )
)
await interaction . followup . send ( embed = stats_embed , view = stats_view , ephemeral = True )
except Exception as e :
await interaction . followup . send ( f " ❌ Erreur lors de la génération des graphiques: { e } " , ephemeral = True )
async def _config_callback ( self , interaction : discord . Interaction ) :
""" Ouvre le panel de configuration (admin ou rôles config uniquement) """
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 )
embed = discord . Embed (
title = " Configuration des Tickets " ,
description = " Gérez la configuration du système de tickets " ,
color = discord . Color . blue ( )
)
await interaction . response . send_message ( embed = embed , view = config_view , ephemeral = True )
def _is_staff ( self , interaction : discord . Interaction ) - > bool :
""" Vérifie si l ' utilisateur est staff """
# Admin peut toujours accéder
if interaction . user . guild_permissions . administrator :
return True
guild = interaction . guild
user = interaction . user
for role_id in self . config . staff_roles :
role = guild . get_role ( int ( role_id ) )
if role and role in user . roles :
return True
return False
def _is_admin ( self , interaction : discord . Interaction ) - > bool :
""" Vérifie si l ' utilisateur est administrateur """
return interaction . user . guild_permissions . administrator
def _can_access_config ( self , interaction : discord . Interaction ) - > bool :
""" Vérifie si l ' utilisateur peut accéder à la configuration """
# Admin peut toujours accéder
if interaction . user . guild_permissions . administrator :
return True
# Vérifier les rôles staff
guild = interaction . guild
user = interaction . user
for role_id in self . config . staff_roles :
role = guild . get_role ( int ( role_id ) )
if role and role in user . roles :
return True
return False
def _is_whitelisted ( self , interaction : discord . Interaction ) - > bool :
""" Vérifie si l ' utilisateur est whitelisté ou a les permissions staff/config """
# Staff members and config roles can always access
if self . _is_staff ( interaction ) or self . _can_access_config ( interaction ) :
return True
# Get whitelist monitor cog
whitelist_cog = self . bot . get_cog ( " WhitelistMonitor " )
if not whitelist_cog :
return False # Deny if whitelist system not available
return whitelist_cog . is_whitelisted ( interaction . guild . id , interaction . user . id )
2026-01-04 00:01:15 +01:00
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
class CategorySelectionView ( discord . ui . View ) :
""" Sélection de catégorie pour créer un ticket """
def __init__ ( self , bot , config : GuildTicketConfig ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . config = config
# Ajouter boutons de catégories
for category in config . categories [ : 5 ] :
btn = discord . ui . Button (
style = discord . ButtonStyle . blurple ,
label = f " { category . emoji } { category . name } " ,
custom_id = f " cat_ { category . id } "
)
btn . callback = self . _category_callback ( category . id )
self . add_item ( btn )
def _is_staff ( self , interaction : discord . Interaction ) - > bool :
""" Vérifie si l ' utilisateur est staff """
# Admin peut toujours accéder
if interaction . user . guild_permissions . administrator :
return True
guild = interaction . guild
user = interaction . user
for role_id in self . config . staff_roles :
role = guild . get_role ( int ( role_id ) )
if role and role in user . roles :
return True
return False
def _can_access_config ( self , interaction : discord . Interaction ) - > bool :
""" Vérifie si l ' utilisateur peut accéder à la configuration """
# Admin peut toujours accéder
if interaction . user . guild_permissions . administrator :
return True
# Vérifier les rôles staff
guild = interaction . guild
user = interaction . user
for role_id in self . config . staff_roles :
role = guild . get_role ( int ( role_id ) )
if role and role in user . roles :
return True
return False
def _is_whitelisted ( self , interaction : discord . Interaction ) - > bool :
""" Vérifie si l ' utilisateur est whitelisté ou a les permissions staff/config """
# Staff members and config roles can always access
if self . _is_staff ( interaction ) or self . _can_access_config ( interaction ) :
return True
# Get whitelist monitor cog
whitelist_cog = self . bot . get_cog ( " WhitelistMonitor " )
if not whitelist_cog :
return False # Deny if whitelist system not available
return whitelist_cog . is_whitelisted ( interaction . guild . id , interaction . user . id )
def _is_staff ( self , interaction : discord . Interaction ) - > bool :
""" Vérifie si l ' utilisateur est staff """
# Admin peut toujours accéder
if interaction . user . guild_permissions . administrator :
return True
guild = interaction . guild
user = interaction . user
for role_id in self . config . staff_roles :
role = guild . get_role ( int ( role_id ) )
if role and role in user . roles :
return True
return False
def _can_access_config ( self , interaction : discord . Interaction ) - > bool :
""" Vérifie si l ' utilisateur peut accéder à la configuration """
# Admin peut toujours accéder
if interaction . user . guild_permissions . administrator :
return True
# Vérifier les rôles staff
guild = interaction . guild
user = interaction . user
for role_id in self . config . staff_roles :
role = guild . get_role ( int ( role_id ) )
if role and role in user . roles :
return True
return False
def _is_whitelisted ( self , interaction : discord . Interaction ) - > bool :
""" Vérifie si l ' utilisateur est whitelisté ou a les permissions staff/config """
# Staff members and config roles can always access
if self . _is_staff ( interaction ) or self . _can_access_config ( interaction ) :
return True
# Get whitelist monitor cog
whitelist_cog = self . bot . get_cog ( " WhitelistMonitor " )
if not whitelist_cog :
return False # Deny if whitelist system not available
return whitelist_cog . is_whitelisted ( interaction . guild . id , interaction . user . id )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
def _category_callback ( self , category_id : str ) :
async def callback ( interaction : discord . Interaction ) :
await self . _on_category_select ( interaction , category_id )
return callback
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
async def _on_category_select ( self , interaction : discord . Interaction , category_id : str ) :
""" Handle category selection """
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
category = self . config . get_category ( category_id )
if not category :
await interaction . response . send_message ( " Catégorie non trouvée. " , ephemeral = True )
return
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
storage = get_storage ( )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
# Check limits
user_tickets = storage . get_user_tickets ( interaction . guild_id , interaction . user . id , open_only = True )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
if len ( user_tickets ) > = self . config . max_tickets_per_user :
await interaction . response . send_message (
f " Vous avez atteint la limite de { self . config . max_tickets_per_user } tickets. " ,
ephemeral = True
)
return
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
# Show priority modal
modal = TicketPriorityModal ( self . bot , self . config , category )
await interaction . response . send_modal ( modal )
class TicketPriorityModal ( discord . ui . Modal ) :
""" Modal pour sélectionner la priorité et la raison """
def __init__ ( self , bot , config : GuildTicketConfig , category : TicketCategory ) :
super ( ) . __init__ ( title = f " { category . name } " )
self . bot = bot
self . config = config
self . category = category
# Priorité
self . priority = discord . ui . TextInput (
label = " Priorité " ,
placeholder = " Normale, Haute ou Urgente " ,
default = " Normale " ,
required = True ,
max_length = 10
)
self . add_item ( self . priority )
# Raison
self . reason = discord . ui . TextInput (
label = " Motif de votre demande " ,
placeholder = " Décrivez votre demande... " ,
style = discord . TextStyle . paragraph ,
required = True ,
max_length = 500
)
self . add_item ( self . reason )
async def on_submit ( self , interaction : discord . Interaction ) :
try :
# Map priority
priority_map = {
" Urgente " : Priority . URGENT ,
" Haute " : Priority . HIGH ,
" Normale " : Priority . NORMAL
}
priority = priority_map . get ( self . priority . value , Priority . NORMAL )
# Create ticket
await self . _create_ticket ( interaction , priority , self . reason . value )
except Exception as e :
await interaction . response . send_message ( f " Erreur lors de la création du ticket: { e } " , ephemeral = True )
async def on_error ( self , interaction : discord . Interaction , error : Exception ) :
""" Handle errors in the modal """
await interaction . response . send_message ( " Une erreur inattendue s ' est produite lors de la création du ticket. " , ephemeral = True )
async def _create_ticket ( self , interaction : discord . Interaction , priority : Priority , reason : str ) :
""" 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 = {
guild . default_role : discord . PermissionOverwrite ( read_messages = False ) ,
user : discord . PermissionOverwrite ( read_messages = True , send_messages = True ) ,
guild . me : discord . PermissionOverwrite ( read_messages = True , send_messages = True )
}
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
# Add staff roles
for role_id in self . config . staff_roles :
role = guild . get_role ( int ( role_id ) )
if role and role not in overwrites :
overwrites [ role ] = discord . PermissionOverwrite ( read_messages = True , send_messages = True )
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
if self . config . default_category :
try :
category_channel = guild . get_channel ( int ( self . config . default_category ) )
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 :
await interaction . response . send_message ( f " Erreur: { e } " , ephemeral = True )
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-01-10 15:30:51 +01:00
# Send welcome message
embed = discord . Embed (
title = f " { self . category . emoji } { self . category . name } " ,
description = f " Bienvenue { user . mention } ! \n \n Votre demande sera traitée sous peu par notre équipe. " ,
color = discord . Color . blurple ( )
)
embed . add_field ( name = " Motif " , value = reason , inline = False )
embed . add_field ( name = " Priorité " , value = priority . value . capitalize ( ) , inline = True )
embed . set_footer ( text = f " Ticket # { ticket_channel . id } " )
# Add management buttons
view = TicketManagementView ( self . bot , storage , self . config , ticket_data )
await ticket_channel . send ( content = user . mention , embed = embed , view = view )
# Log
if self . config . log_channel_id :
log_channel = guild . get_channel ( self . config . log_channel_id )
if log_channel :
log_embed = discord . Embed (
title = " Nouveau Ticket " ,
color = discord . Color . green ( ) ,
timestamp = datetime . now ( )
)
log_embed . add_field ( name = " Utilisateur " , value = user . mention , inline = True )
log_embed . add_field ( name = " Catégorie " , value = self . category . name , inline = True )
log_embed . add_field ( name = " Salon " , value = ticket_channel . mention , inline = True )
await log_channel . send ( embed = log_embed )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
await interaction . response . send_message ( f " Ticket créé: { ticket_channel . mention } " , ephemeral = True )
class TicketManagementView ( discord . ui . View ) :
""" Gestion du ticket (dans le salon du ticket) """
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 )
# Claim (if enabled and ticket is open)
if config . claim_enabled and self . category and self . category . allow_claims and ticket . status == TicketStatus . OPEN :
claim_btn = discord . ui . Button (
style = discord . ButtonStyle . primary ,
label = " Réclamer " ,
custom_id = " ticket_claim "
)
claim_btn . callback = self . _claim_callback
self . add_item ( claim_btn )
# Fermer
close_btn = discord . ui . Button (
style = discord . ButtonStyle . red ,
label = " Fermer " ,
custom_id = " ticket_close "
)
close_btn . callback = self . _close_callback
self . add_item ( close_btn )
# Transcripts
transcript_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
label = " Transcript " ,
custom_id = " ticket_transcript "
)
transcript_btn . callback = self . _transcript_callback
self . add_item ( transcript_btn )
# Reopen (if closed)
if ticket . status == TicketStatus . CLOSED :
reopen_btn = discord . ui . Button (
style = discord . ButtonStyle . green ,
label = " Rouvrir " ,
custom_id = " ticket_reopen "
)
reopen_btn . callback = self . _reopen_callback
self . add_item ( reopen_btn )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
async def _close_callback ( self , interaction : discord . Interaction ) :
""" Ferme le ticket """
modal = CloseTicketModal ( self . bot , self . storage , self . config , self . ticket )
await interaction . response . send_modal ( modal )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
async def _transcript_callback ( self , interaction : discord . Interaction ) :
""" 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 ) :
file = discord . File ( filepath , filename = os . path . basename ( filepath ) )
await interaction . response . send_message ( " Transcript généré: " , file = file , ephemeral = True )
else :
await interaction . response . send_message ( " Erreur lors de la génération du transcript. " , ephemeral = True )
async def _claim_callback ( self , interaction : discord . Interaction ) :
""" Réclame 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 :
await interaction . response . send_message ( " Ce ticket est déjà réclamé. " , ephemeral = True )
return
self . ticket . claimed_by = interaction . user . id
self . ticket . status = TicketStatus . CLAIMED
self . storage . save_ticket ( self . ticket )
# Update view (remove claim button)
for item in self . children :
if item . custom_id == " ticket_claim " :
self . remove_item ( item )
break
# Force UI update
await interaction . response . edit_message ( view = self )
embed = TicketEmbedFormatter . ticket_claimed ( self . ticket , interaction . user )
await interaction . channel . send ( embed = embed )
async def _reopen_callback ( self , interaction : discord . Interaction ) :
""" Rouvre le ticket """
if not self . _is_staff ( interaction ) :
await interaction . response . send_message ( " Réservé au staff. " , ephemeral = True )
return
self . ticket . status = TicketStatus . OPEN
self . ticket . closed_at = None
self . ticket . claimed_by = None
self . storage . save_ticket ( self . ticket )
# Update channel
overwrites = interaction . channel . overwrites . copy ( )
for target , perms in overwrites . items ( ) :
if isinstance ( target , discord . Member ) and target . id != interaction . guild . me . id :
overwrites [ target ] = discord . PermissionOverwrite (
read_messages = True ,
send_messages = True
)
await interaction . channel . edit (
name = interaction . channel . name . replace ( " closed- " , " " ) ,
overwrites = overwrites
)
# Update view (remove reopen button, add claim if enabled)
for item in self . children :
if item . custom_id == " ticket_reopen " :
self . remove_item ( item )
break
if self . config . claim_enabled and self . category and self . category . allow_claims :
claim_btn = discord . ui . Button (
style = discord . ButtonStyle . primary ,
label = " Réclamer " ,
custom_id = " ticket_claim "
)
claim_btn . callback = self . _claim_callback
self . add_item ( claim_btn )
embed = TicketEmbedFormatter . ticket_reopened ( self . ticket , interaction . user )
await interaction . channel . send ( embed = embed )
await interaction . response . send_message ( " Ticket rouvert! " , ephemeral = True )
def _is_staff ( self , interaction : discord . Interaction ) - > bool :
guild = interaction . guild
user = interaction . user
for role_id in self . config . staff_roles :
role = guild . get_role ( int ( role_id ) )
if role and role in user . roles :
return True
return user . guild_permissions . administrator
def _is_staff_member ( self , member ) - > bool :
guild = member . guild
for role_id in self . config . staff_roles :
role = guild . get_role ( int ( role_id ) )
if role and role in member . roles :
return True
return False
class CloseTicketModal ( discord . ui . Modal ) :
""" Modal pour fermer un ticket """
def __init__ ( self , bot , storage , config : GuildTicketConfig , ticket : TicketData ) :
super ( ) . __init__ ( title = " Fermer le Ticket " )
self . bot = bot
self . storage = storage
self . config = config
self . ticket = ticket
self . category = config . get_category ( ticket . category_id )
self . reason = discord . ui . TextInput (
label = " Raison de fermeture " ,
placeholder = " Optionnel... " ,
style = discord . TextStyle . paragraph ,
required = False ,
max_length = 300
)
self . add_item ( self . reason )
# Survey fields if enabled
if config . survey_enabled and self . category and self . category . survey_enabled :
self . rating = discord . ui . TextInput (
label = " Note (1-5 étoiles) " ,
placeholder = " 1 à 5 " ,
required = False ,
max_length = 1
)
self . add_item ( self . rating )
self . feedback = discord . ui . TextInput (
label = " Commentaires " ,
placeholder = " Vos commentaires sur le support... " ,
style = discord . TextStyle . paragraph ,
required = False ,
max_length = 500
)
self . add_item ( self . feedback )
async def on_submit ( self , interaction : discord . Interaction ) :
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
# Handle survey if enabled
if hasattr ( self , ' rating ' ) and self . rating . value :
try :
rating = int ( self . rating . value )
if 1 < = rating < = 5 :
self . ticket . rating = rating
else :
await interaction . response . send_message ( " La note doit être entre 1 et 5. " , ephemeral = True )
return
except ValueError :
await interaction . response . send_message ( " Note invalide. " , ephemeral = True )
return
if hasattr ( self , ' feedback ' ) and self . feedback . value :
self . ticket . feedback = self . feedback . value
# Send ephemeral response to user first (required for modal interactions)
await interaction . response . send_message ( " ✅ Fermeture du ticket en cours... " , ephemeral = True )
# Send confirmation message in the ticket channel
confirm_embed = discord . Embed (
title = " ⏳ Fermeture du Ticket " ,
description = " 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... " ,
color = discord . Color . orange ( )
)
await interaction . channel . send ( embed = confirm_embed )
# 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
if self . config . log_channel_id and transcript_file and os . path . exists ( transcript_file ) :
try :
log_channel = interaction . guild . get_channel ( self . config . log_channel_id )
if log_channel :
# Create log embed
log_embed = discord . Embed (
title = " 📄 Ticket Fermé - Transcript " ,
description = f " Ticket # { self . ticket . channel_id } fermé par { interaction . user . mention } " ,
color = discord . Color . red ( ) ,
timestamp = datetime . now ( )
)
# Get ticket creator
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 . reason . value :
log_embed . add_field ( name = " Raison de fermeture " , value = self . reason . value , inline = False )
# Send transcript file
file = discord . File ( transcript_file , filename = os . path . basename ( transcript_file ) )
await log_channel . send ( embed = log_embed , file = file )
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 )
def _is_staff ( self , interaction : discord . Interaction ) - > bool :
guild = interaction . guild
user = interaction . user
for role_id in self . config . staff_roles :
role = guild . get_role ( int ( role_id ) )
if role and role in user . roles :
return True
return user . guild_permissions . administrator
class TicketSetupView ( discord . ui . View ) :
""" Panel de configuration complet du système de tickets """
def __init__ ( self , bot , storage , config : GuildTicketConfig ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
# Toggle enabled
toggle_btn = discord . ui . Button (
style = discord . ButtonStyle . green if config . enabled else discord . ButtonStyle . red ,
label = " 🔄 Activer/Désactiver " ,
custom_id = " setup_toggle "
)
toggle_btn . callback = self . _toggle_callback
self . add_item ( toggle_btn )
# Staff roles
roles_btn = discord . ui . Button (
style = discord . ButtonStyle . primary ,
label = " 👥 Rôles Staff " ,
custom_id = " setup_roles "
)
roles_btn . callback = self . _roles_callback
self . add_item ( roles_btn )
# Config roles
config_roles_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
label = " 🔑 Rôles Config " ,
custom_id = " setup_config_roles "
)
config_roles_btn . callback = self . _config_roles_callback
self . add_item ( config_roles_btn )
# Categories
cat_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
label = " 📂 Catégories " ,
custom_id = " setup_categories "
)
cat_btn . callback = self . _categories_callback
self . add_item ( cat_btn )
# Log channel
log_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
label = " 📝 Canal de Logs " ,
custom_id = " setup_log_channel "
)
log_btn . callback = self . _log_channel_callback
self . add_item ( log_btn )
# Advanced settings
advanced_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
label = " ⚙️ Paramètres Avancés " ,
custom_id = " setup_advanced "
)
advanced_btn . callback = self . _advanced_callback
self . add_item ( advanced_btn )
async def _toggle_callback ( self , interaction : discord . Interaction ) :
self . config . enabled = not self . config . enabled
self . storage . save_config ( interaction . guild_id , self . config )
status = " activé " if self . config . enabled else " désactivé "
embed = discord . Embed (
title = " 🔄 État du Système " ,
description = f " Le système de tickets a été ** { status } **! " ,
color = discord . Color . green ( ) if self . config . enabled else discord . Color . red ( )
)
await interaction . response . send_message ( embed = embed , ephemeral = True )
# Update button
for item in self . children :
if item . custom_id == " setup_toggle " :
item . style = discord . ButtonStyle . green if self . config . enabled else discord . ButtonStyle . red
await interaction . message . edit ( view = self )
async def _roles_callback ( self , interaction : discord . Interaction ) :
view = StaffRolesSetupView ( self . bot , self . storage , self . config , interaction . guild )
embed = discord . Embed (
title = " 👥 Configuration des Rôles Staff " ,
description = " Définissez les rôles qui peuvent gérer les tickets et accéder aux fonctionnalités avancées. \n \n "
" **Permissions des rôles staff:** \n "
" • Gérer les tickets (réclamer, fermer, etc.) \n "
" • Accéder aux analytics et statistiques \n "
" • Générer des transcripts \n "
" • Fermer les tickets des autres " ,
color = discord . Color . blue ( )
)
role_mentions = [ ]
for role_id in self . config . staff_roles :
role = interaction . guild . get_role ( int ( role_id ) )
if role :
role_mentions . append ( role . mention )
embed . add_field (
name = " Rôles Staff Actuels " ,
value = " , " . join ( role_mentions ) if role_mentions else " ❌ Aucun rôle configuré " ,
inline = False
)
await interaction . response . send_message ( embed = embed , view = view , ephemeral = True )
async def _categories_callback ( self , interaction : discord . Interaction ) :
view = CategoriesSetupView ( self . bot , self . storage , self . config )
embed = discord . Embed (
title = " 📂 Gestion des Catégories " ,
description = " Créez et gérez les catégories de tickets pour organiser vos demandes. " ,
color = discord . Color . blue ( )
)
if self . config . categories :
cat_list = [ ]
for cat in self . config . categories [ : 10 ] : # Limit to 10 for display
cat_list . append ( f " { cat . emoji } { cat . name } " )
embed . add_field (
name = f " Catégories ( { len ( self . config . categories ) } ) " ,
value = " \n " . join ( cat_list ) ,
inline = False
)
else :
embed . add_field (
name = " Aucune catégorie " ,
value = " Créez votre première catégorie pour commencer! " ,
inline = False
)
await interaction . response . send_message ( embed = embed , view = view , ephemeral = True )
async def _config_roles_callback ( self , interaction : discord . Interaction ) :
view = ConfigRolesSetupView ( self . bot , self . storage , self . config , interaction . guild )
embed = discord . Embed (
title = " 🔑 Configuration des Rôles Config " ,
description = " Définissez les rôles qui peuvent accéder au panel de configuration du système de tickets. \n \n "
" **Permissions des rôles config:** \n "
" • Accéder à la commande /ticketconfig \n "
" • Modifier tous les paramètres du système \n "
" • Gérer les catégories et rôles staff " ,
color = discord . Color . purple ( )
)
role_mentions = [ ]
for role_id in self . config . config_roles :
role = interaction . guild . get_role ( int ( role_id ) )
if role :
role_mentions . append ( role . mention )
embed . add_field (
name = " Rôles Config Actuels " ,
value = " , " . join ( role_mentions ) if role_mentions else " ❌ Aucun rôle configuré " ,
inline = False
)
await interaction . response . send_message ( embed = embed , view = view , ephemeral = True )
async def _log_channel_callback ( self , interaction : discord . Interaction ) :
modal = SetChannelModal ( self . bot , self . storage , self . config , " log " )
await interaction . response . send_modal ( modal )
async def _advanced_callback ( self , interaction : discord . Interaction ) :
view = AdvancedSetupView ( self . bot , self . storage , self . config )
embed = discord . Embed (
title = " ⚙️ Paramètres Avancés " ,
description = " Configuration fine du système de tickets. " ,
color = discord . Color . purple ( )
)
embed . add_field (
name = " Paramètres Actuels " ,
value = f " Système de réclamation: { ' ✅ Activé ' if self . config . claim_enabled else ' ❌ Désactivé ' } \n "
f " Sondages: { ' ✅ Activé ' if self . config . survey_enabled else ' ❌ Désactivé ' } \n "
f " Limite par utilisateur: { self . config . max_tickets_per_user } " ,
inline = False
)
await interaction . response . send_message ( embed = embed , view = view , ephemeral = True )
class StaffRolesSetupView ( discord . ui . View ) :
""" Configuration des rôles staff """
def __init__ ( self , bot , storage , config : GuildTicketConfig , guild : discord . Guild ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
self . guild = guild
self . _build_select ( )
def _build_select ( self ) :
""" Build the role select menu """
options = [ ]
for role in self . guild . roles :
if role . id != self . guild . id : # Exclude @everyone
options . append (
discord . SelectOption (
label = role . name ,
value = str ( role . id ) ,
default = str ( role . id ) in self . config . staff_roles
)
)
if options :
select = discord . ui . Select (
placeholder = " Sélectionnez les rôles staff... " ,
options = options [ : 25 ] ,
min_values = 0 ,
max_values = min ( 25 , len ( options ) )
)
select . callback = self . _select_callback
self . add_item ( select )
async def _select_callback ( self , interaction : discord . Interaction ) :
selected = list ( interaction . data . get ( " values " , [ ] ) )
self . config . staff_roles = selected
self . storage . save_config ( interaction . guild_id , self . config )
role_mentions = [ ]
for role_id in selected :
role = interaction . guild . get_role ( int ( role_id ) )
if role :
role_mentions . append ( role . mention )
embed = discord . Embed (
title = " ✅ Rôles Staff Mis à Jour " ,
description = f " ** { len ( selected ) } ** rôle(s) configuré(s) comme staff. " ,
color = discord . Color . green ( )
)
embed . add_field (
name = " Rôles Staff " ,
value = " , " . join ( role_mentions ) if role_mentions else " Aucun " ,
inline = False
)
await interaction . response . send_message ( embed = embed , ephemeral = True )
class ConfigRolesSetupView ( discord . ui . View ) :
""" Configuration des rôles config """
def __init__ ( self , bot , storage , config : GuildTicketConfig , guild : discord . Guild ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
self . guild = guild
self . _build_select ( )
def _build_select ( self ) :
""" Build the role select menu """
options = [ ]
for role in self . guild . roles :
if role . id != self . guild . id : # Exclude @everyone
options . append (
discord . SelectOption (
label = role . name ,
value = str ( role . id ) ,
default = str ( role . id ) in self . config . config_roles
)
)
if options :
select = discord . ui . Select (
placeholder = " Sélectionnez les rôles config... " ,
options = options [ : 25 ] ,
min_values = 0 ,
max_values = min ( 25 , len ( options ) )
)
select . callback = self . _select_callback
self . add_item ( select )
async def _select_callback ( self , interaction : discord . Interaction ) :
selected = list ( interaction . data . get ( " values " , [ ] ) )
self . config . config_roles = selected
self . storage . save_config ( interaction . guild_id , self . config )
role_mentions = [ ]
for role_id in selected :
role = interaction . guild . get_role ( int ( role_id ) )
if role :
role_mentions . append ( role . mention )
embed = discord . Embed (
title = " ✅ Rôles Config Mis à Jour " ,
description = f " ** { len ( selected ) } ** rôle(s) configuré(s) pour accéder à la configuration. " ,
color = discord . Color . green ( )
)
embed . add_field (
name = " Rôles Config " ,
value = " , " . join ( role_mentions ) if role_mentions else " Aucun " ,
inline = False
)
embed . add_field (
name = " Permissions " ,
value = " • Accéder à /ticketconfig \n • Modifier la configuration \n • Gérer les catégories et rôles " ,
inline = False
)
await interaction . response . send_message ( embed = embed , ephemeral = True )
class CategoriesSetupView ( discord . ui . View ) :
""" Gestion des catégories """
def __init__ ( self , bot , storage , config : GuildTicketConfig ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
# Add category
add_btn = discord . ui . Button (
style = discord . ButtonStyle . green ,
label = " ➕ Ajouter" ,
custom_id = " cat_add "
)
add_btn . callback = self . _add_callback
self . add_item ( add_btn )
# Remove category (only if categories exist)
if config . categories :
remove_btn = discord . ui . Button (
style = discord . ButtonStyle . red ,
label = " ➖ Supprimer" ,
custom_id = " cat_remove "
)
remove_btn . callback = self . _remove_callback
self . add_item ( remove_btn )
async def _add_callback ( self , interaction : discord . Interaction ) :
modal = AddCategoryModal ( self . bot , self . storage , self . config )
await interaction . response . send_modal ( modal )
async def _remove_callback ( self , interaction : discord . Interaction ) :
if not self . config . categories :
await interaction . response . send_message ( " Aucune catégorie à supprimer. " , ephemeral = True )
return
# Create select with current categories
options = [ ]
for cat in self . config . categories [ : 25 ] : # Discord limit
options . append (
discord . SelectOption (
label = cat . name ,
value = cat . id ,
emoji = cat . emoji
)
)
select = discord . ui . Select (
placeholder = " Sélectionnez une catégorie à supprimer... " ,
options = options
)
select . callback = self . _remove_select_callback
view = discord . ui . View ( )
view . add_item ( select )
embed = discord . Embed (
title = " Supprimer une Catégorie " ,
description = " Sélectionnez la catégorie à supprimer: " ,
color = discord . Color . red ( )
)
await interaction . response . send_message ( embed = embed , view = view , ephemeral = True )
async def _remove_select_callback ( self , interaction : discord . Interaction ) :
cat_id = interaction . data [ " values " ] [ 0 ]
# Remove category
self . config . categories = [ cat for cat in self . config . categories if cat . id != cat_id ]
self . storage . save_config ( interaction . guild_id , self . config )
await interaction . response . send_message ( " ✅ Catégorie supprimée! " , ephemeral = True )
class AdvancedSetupView ( discord . ui . View ) :
""" Paramètres avancés """
def __init__ ( self , bot , storage , config : GuildTicketConfig ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
# Toggle claim system
claim_btn = discord . ui . Button (
style = discord . ButtonStyle . green if config . claim_enabled else discord . ButtonStyle . red ,
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
survey_btn = discord . ui . Button (
style = discord . ButtonStyle . green if config . survey_enabled else discord . ButtonStyle . red ,
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
max_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
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-01-10 15:30:51 +01:00
async def _claim_callback ( self , interaction : discord . Interaction ) :
self . config . claim_enabled = not self . config . claim_enabled
self . storage . save_config ( interaction . guild_id , self . config )
status = " activé " if self . config . claim_enabled else " désactivé "
embed = discord . Embed (
title = " 🔄 Système de Réclamation " ,
description = f " Le système de réclamation a été ** { status } **! " ,
color = discord . Color . green ( ) if self . config . claim_enabled else discord . Color . red ( )
)
await interaction . response . send_message ( embed = embed , ephemeral = True )
# Update button
for item in self . children :
if item . custom_id == " advanced_claim " :
item . style = discord . ButtonStyle . green if self . config . claim_enabled else discord . ButtonStyle . red
item . label = f " { ' ✅ ' if self . config . claim_enabled else ' ❌ ' } Système de Réclamation "
await interaction . message . edit ( view = self )
async def _survey_callback ( self , interaction : discord . Interaction ) :
self . config . survey_enabled = not self . config . survey_enabled
self . storage . save_config ( interaction . guild_id , self . config )
status = " activé " if self . config . survey_enabled else " désactivé "
embed = discord . Embed (
title = " 🔄 Système de Sondages " ,
description = f " Le système de sondages a été ** { status } **! " ,
color = discord . Color . green ( ) if self . config . survey_enabled else discord . Color . red ( )
)
await interaction . response . send_message ( embed = embed , ephemeral = True )
# Update button
for item in self . children :
if item . custom_id == " advanced_survey " :
item . style = discord . ButtonStyle . green if self . config . survey_enabled else discord . ButtonStyle . red
item . label = f " { ' ✅ ' if self . config . survey_enabled else ' ❌ ' } Sondages "
await interaction . message . edit ( view = self )
async def _max_callback ( self , interaction : discord . Interaction ) :
modal = MaxTicketsModal ( self . bot , self . storage , self . config )
await interaction . response . send_modal ( modal )
class MaxTicketsModal ( discord . ui . Modal ) :
""" 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-01-10 15:30:51 +01:00
super ( ) . __init__ ( title = " Limite de Tickets par Utilisateur " )
2026-01-04 00:01:15 +01:00
self . bot = bot
self . storage = storage
self . config = config
2026-01-10 15:30:51 +01:00
self . max_tickets = discord . ui . TextInput (
label = " Nombre maximum de tickets ouverts par utilisateur " ,
placeholder = " 5 " ,
default = str ( config . max_tickets_per_user ) ,
required = True ,
max_length = 2
)
self . add_item ( self . max_tickets )
async def on_submit ( self , interaction : discord . Interaction ) :
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 )
embed = discord . Embed (
title = " ✅ Limite Mise à Jour " ,
description = f " La limite de tickets par utilisateur est maintenant de ** { max_tickets } **. " ,
color = discord . Color . green ( )
)
await interaction . response . send_message ( embed = embed , ephemeral = True )
except ValueError :
await interaction . response . send_message ( " Veuillez entrer un nombre valide. " , ephemeral = True )
class ConfigPanelView ( discord . ui . View ) :
""" Panel de configuration """
def __init__ ( self , bot , storage , config : GuildTicketConfig ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
# Toggle enabled
toggle_btn = discord . ui . Button (
style = discord . ButtonStyle . green if config . enabled else discord . ButtonStyle . red ,
label = " Activer/Désactiver " ,
custom_id = " config_toggle "
)
toggle_btn . callback = self . _toggle_callback
self . add_item ( toggle_btn )
# Ajouter catégorie
add_cat_btn = discord . ui . Button (
style = discord . ButtonStyle . blurple ,
label = " Ajouter Catégorie " ,
custom_id = " config_add_cat "
)
add_cat_btn . callback = self . _add_cat_callback
self . add_item ( add_cat_btn )
# Staff roles
roles_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
label = " Rôles Staff " ,
custom_id = " config_roles "
)
roles_btn . callback = self . _roles_callback
self . add_item ( roles_btn )
# Log channel
log_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
label = " Canal de Logs " ,
custom_id = " config_log_channel "
)
log_btn . callback = self . _log_channel_callback
self . add_item ( log_btn )
async def _toggle_callback ( self , interaction : discord . Interaction ) :
self . config . enabled = not self . config . enabled
self . storage . save_config ( interaction . guild_id , self . config )
status = " activé " if self . config . enabled else " désactivé "
await interaction . response . send_message ( f " Système { status } ! " , ephemeral = True )
# Update button
for item in self . children :
if item . custom_id == " config_toggle " :
item . style = discord . ButtonStyle . green if self . config . enabled else discord . ButtonStyle . red
await interaction . message . edit ( view = self )
async def _add_cat_callback ( self , interaction : discord . Interaction ) :
2026-01-04 00:01:15 +01:00
modal = AddCategoryModal ( self . bot , self . storage , self . config )
await interaction . response . send_modal ( modal )
2026-01-10 15:30:51 +01:00
async def _roles_callback ( self , interaction : discord . Interaction ) :
view = StaffRolesView ( self . bot , self . storage , self . config , interaction . guild )
embed = discord . Embed (
title = " Rôles Staff " ,
description = " Sélectionnez les rôles qui peuvent gérer les tickets: " ,
color = discord . Color . blue ( )
)
2026-01-04 00:01:15 +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 )
2026-01-10 15:30:51 +01:00
embed . add_field ( name = " Actuels " , value = " , " . join ( role_mentions ) if role_mentions else " Aucun " , inline = False )
2026-01-04 00:01:15 +01:00
await interaction . response . send_message ( embed = embed , view = view , ephemeral = True )
2026-01-10 15:30:51 +01:00
async def _log_channel_callback ( self , interaction : discord . Interaction ) :
modal = SetChannelModal ( self . bot , self . storage , self . config , " log " )
2026-01-04 00:01:15 +01:00
await interaction . response . send_modal ( modal )
class AddCategoryModal ( discord . 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-01-10 15:30:51 +01:00
super ( ) . __init__ ( title = " Nouvelle Catégorie " )
2026-01-04 00:01:15 +01:00
self . bot = bot
self . storage = storage
self . config = config
2026-01-10 15:30:51 +01:00
self . name = discord . ui . TextInput ( label = " Nom " , placeholder = " Ex: Support " , required = True )
2026-01-04 00:01:15 +01:00
self . add_item ( self . name )
self . description = discord . ui . TextInput (
2026-01-10 15:30:51 +01:00
label = " Description " , placeholder = " Description... " , required = False , style = discord . TextStyle . paragraph
2026-01-04 00:01:15 +01:00
)
self . add_item ( self . description )
2026-01-10 15:30:51 +01:00
self . emoji = discord . ui . TextInput ( label = " Emoji " , placeholder = " 🎫 " , required = False , max_length = 5 )
2026-01-04 00:01:15 +01:00
self . add_item ( self . emoji )
async def on_submit ( self , interaction : discord . Interaction ) :
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 )
2026-01-10 15:30:51 +01:00
self . storage . save_config ( interaction . guild_id , self . config )
await interaction . response . send_message ( f " Catégorie ** { self . name . value } ** créée! " , ephemeral = True )
class SetChannelModal ( discord . ui . Modal ) :
""" Modal pour définir le canal de logs """
def __init__ ( self , bot , storage , config : GuildTicketConfig , channel_type : str = " log " ) :
super ( ) . __init__ ( title = " Définir le Canal de Logs " )
self . bot = bot
self . storage = storage
self . config = config
self . channel_type = channel_type
self . channel = discord . ui . TextInput (
label = " Canal de logs " ,
placeholder = " Mentionnez le canal ou entrez l ' ID (ex: #general ou 123456789) " ,
required = True ,
max_length = 100
)
self . add_item ( self . channel )
async def on_submit ( self , interaction : discord . Interaction ) :
channel_input = self . channel . value . strip ( )
# Try to extract channel ID from mention or ID
channel_id = None
# Check for mention format (#channel)
if channel_input . startswith ( ' <# ' ) and channel_input . endswith ( ' > ' ) :
try :
channel_id = int ( channel_input [ 2 : - 1 ] )
except ValueError :
pass
# Check for raw ID
else :
try :
channel_id = int ( channel_input . replace ( ' < ' , ' ' ) . replace ( ' > ' , ' ' ) )
except ValueError :
pass
if not channel_id :
await interaction . response . send_message (
" ❌ Format invalide. Utilisez une mention (#channel) ou un ID de canal. " ,
ephemeral = True
)
return
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
# Verify channel exists
channel = interaction . guild . get_channel ( channel_id )
if not channel :
await interaction . response . send_message (
" ❌ Ce canal n ' existe pas sur ce serveur. " ,
ephemeral = True
)
return
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
# Update config
self . config . log_channel_id = channel_id
self . storage . save_config ( interaction . guild_id , self . config )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
embed = discord . Embed (
title = " ✅ Canal de Logs Mis à Jour " ,
description = f " Le canal de logs a été défini sur { channel . mention } . " ,
color = discord . Color . green ( )
2026-01-04 00:01:15 +01:00
)
2026-01-10 15:30:51 +01:00
await interaction . response . send_message ( embed = embed , ephemeral = True )
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
class StaffRolesModal ( discord . ui . Modal ) :
""" 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)
self . roles = discord . ui . TextInput (
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
async def on_submit ( self , interaction : discord . Interaction ) :
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
embed = discord . Embed (
title = " ✅ Rôles Staff Mis à Jour " ,
description = f " ** { len ( valid_roles ) } ** rôle(s) configuré(s) comme staff. " ,
color = discord . Color . green ( )
)
role_mentions = [ role . mention for role in valid_roles ]
embed . add_field (
name = " Rôles Staff " ,
value = " , " . join ( role_mentions ) if role_mentions else " Aucun " ,
inline = False
)
embed . add_field (
name = " Permissions " ,
value = " • Gérer les tickets \n • Accéder aux analytics \n • Générer des transcripts \n • Fermer les tickets des autres " ,
inline = False
)
await interaction . response . send_message ( embed = embed , ephemeral = True )
except Exception as e :
await interaction . response . send_message ( f " Erreur lors de la configuration des rôles: { e } " , ephemeral = True )
async def on_error ( self , interaction : discord . Interaction , error : Exception ) :
""" Handle errors in the modal """
await interaction . response . send_message ( " Une erreur inattendue s ' est produite lors de la configuration des rôles. " , ephemeral = True )
class StaffRolesView ( discord . ui . View ) :
""" Sélection des rôles staff (alternative avec select menu) """
def __init__ ( self , bot , storage , config : GuildTicketConfig , guild : discord . Guild ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
self . guild = guild
# Le select sera populated dans le callback
self . _build_select ( guild )
def _build_select ( self , guild : discord . Guild ) :
if not guild :
return
options = [ ]
for role in guild . roles :
if role . id != guild . id : # Exclude @everyone
options . append (
discord . SelectOption (
label = role . name ,
value = str ( role . id ) ,
default = str ( role . id ) in self . config . staff_roles
)
)
if options :
select = discord . ui . Select (
placeholder = " Sélectionnez les rôles staff... " ,
options = options [ : 25 ] ,
min_values = 0 ,
max_values = min ( 25 , len ( options ) )
)
select . callback = self . _select_callback
self . add_item ( select )
async def _select_callback ( self , interaction : discord . Interaction ) :
selected = list ( interaction . data . get ( " values " , [ ] ) )
self . config . staff_roles = selected
self . storage . save_config ( interaction . guild_id , self . config )
role_mentions = [ ]
for role_id in selected :
role = interaction . guild . get_role ( int ( role_id ) )
if role :
role_mentions . append ( role . mention )
embed = discord . Embed (
title = " ✅ Rôles Staff Mis à Jour " ,
description = f " ** { len ( selected ) } ** rôle(s) configuré(s). " ,
color = discord . Color . green ( )
)
embed . add_field ( name = " Rôles " , value = " , " . join ( role_mentions ) if role_mentions else " Aucun " , inline = False )
await interaction . response . send_message ( embed = embed , ephemeral = True )
class TicketCommands ( commands . Cog ) :
""" Cog principal des tickets """
def __init__ ( self , bot ) :
self . bot = bot
self . storage = get_storage ( )
def cog_unload ( self ) :
pass
async def cog_load ( self ) :
await self . _register_all_views ( )
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 } " )
# === COMMANDES ===
@commands.hybrid_command ( name = " ticket " , aliases = [ " tickets " ] )
async def ticket_main ( self , ctx ) :
""" Ouvre le panel des tickets """
await ctx . defer ( ephemeral = True )
config = self . storage . load_config ( ctx . guild . id )
embed = discord . Embed (
title = " Système de Tickets " ,
description = " Gérez vos tickets simplement " ,
color = discord . Color . blurple ( )
)
if not config . enabled :
embed . add_field ( name = " État " , value = " Le système est actuellement **désactivé**. " , inline = False )
else :
embed . add_field ( name = " Catégories " , value = str ( len ( config . categories ) ) , inline = True )
embed . add_field ( name = " Total tickets " , value = str ( len ( self . storage . get_guild_tickets ( ctx . guild . id ) ) ) , inline = True )
view = TicketPanelView ( self . bot , config , self . storage )
await ctx . send ( embed = embed , view = view , ephemeral = True )
@commands.hybrid_command ( name = " ticketpanel " , aliases = [ " panneau " ] )
@commands.has_permissions ( administrator = True )
async def ticket_panel ( self , ctx , channel : discord . TextChannel = None ) :
""" Envoie le panel de création de tickets dans un canal """
await ctx . defer ( ephemeral = True )
config = self . storage . load_config ( ctx . guild . id )
if not config . enabled :
await ctx . send ( " Le système est désactivé. " , ephemeral = True )
return
target = channel or ctx . channel
embed = discord . Embed (
title = " Besoin d ' aide? " ,
description = " Cliquez sur le bouton ci-dessous pour créer un ticket. \n \n Notre équipe vous répondra dès que possible! " ,
color = discord . Color . blurple ( )
)
embed . add_field ( name = " Catégories disponibles " , value = str ( len ( config . categories ) ) , inline = False )
view = TicketPanelView ( self . bot , config , self . storage )
await target . send ( embed = embed , view = view )
await ctx . send ( f " Panel envoyé dans { target . mention } " , ephemeral = True )
@commands.hybrid_command ( name = " ticketconfig " , aliases = [ " ticketsetup " , " configticket " ] )
async def ticket_config ( self , ctx ) :
""" Ouvre le panel de configuration complet du système de tickets """
await ctx . defer ( ephemeral = True )
config = self . storage . load_config ( ctx . guild . id )
# Vérifier si l'utilisateur peut accéder à la configuration
can_access = ctx . author . guild_permissions . administrator
if not can_access :
# Vérifier les rôles staff
for role_id in config . staff_roles :
role = ctx . guild . get_role ( int ( role_id ) )
if role and role in ctx . author . roles :
can_access = True
break
if not can_access :
await ctx . send ( " ❌ Cette commande est réservée aux administrateurs ou aux rôles configurés. " , ephemeral = True )
return
embed = discord . Embed (
title = " ⚙️ Configuration du Système de Tickets " ,
description = " Configurez complètement votre système de tickets depuis cette interface. \n \n "
" **Options disponibles:** \n "
" • Activer/Désactiver le système \n "
" • Configurer les rôles staff \n "
" • Gérer les catégories \n "
" • Définir le canal de logs \n "
" • Et bien plus... " ,
color = discord . Color . blue ( )
)
embed . add_field (
name = " État actuel " ,
value = f " Système: { ' ✅ Activé ' if config . enabled else ' ❌ Désactivé ' } \n "
f " Catégories: { len ( config . categories ) } \n "
f " Rôles staff: { len ( config . staff_roles ) } " ,
inline = True
)
embed . add_field (
name = " Configuration requise " ,
value = " • **Rôles Staff**: Définissez qui peut gérer les tickets \n "
" • **Catégories**: Créez des catégories pour organiser les tickets \n "
" • **Canal de Logs**: Où envoyer les notifications " ,
inline = True
)
embed . set_footer ( text = " Utilisez les boutons ci-dessous pour configurer le système " )
view = TicketSetupView ( self . bot , self . storage , config )
await ctx . send ( embed = embed , view = view , ephemeral = True )
2026-01-04 00:01:15 +01:00
async def setup ( bot ) :
2026-01-10 15:30:51 +01:00
""" Setup function """
await bot . add_cog ( TicketCommands ( bot ) )
2026-01-04 00:01:15 +01:00