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__ ) ) ) ) )
import discord
from discord . 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-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 """
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-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 )
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 """
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
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) """
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
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) """
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 )
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 :
2026-01-14 21:32:20 +01:00
return is_staff ( interaction , self . config )
2026-01-10 15:30:51 +01:00
def _can_access_config ( self , interaction : discord . 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
def _is_whitelisted ( self , interaction : discord . 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-01-14 21:32:20 +01:00
class CategoryButton ( discord . ui . Button ) :
def __init__ ( self , category , view_instance ) :
super ( ) . __init__ (
style = discord . ButtonStyle . blurple ,
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
async def callback ( self , interaction : discord . Interaction ) :
await self . view_instance . _on_category_select ( interaction , self . category . id )
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 ) :
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
def _is_staff ( self , interaction : discord . Interaction ) - > bool :
2026-01-14 21:32:20 +01:00
return is_staff ( interaction , self . config )
2026-01-10 15:30:51 +01:00
def _can_access_config ( self , interaction : discord . 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
def _is_whitelisted ( self , interaction : discord . 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-01-10 15:30:51 +01:00
async def _on_category_select ( self , interaction : discord . Interaction , category_id : str ) :
""" Handle category selection """
2026-01-14 21:32:20 +01:00
try :
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-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 )
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
# Show priority modal
modal = TicketPriorityModal ( self . bot , self . config , category )
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
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 )
2026-01-14 21:32:20 +01:00
# Prepare mentions
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 )
# Add management buttons
view = TicketManagementView ( self . bot , storage , self . config , ticket_data )
await ticket_channel . send ( content = mention_content , embed = embed , view = view )
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 :
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 ) :
2026-01-14 21:32:20 +01:00
""" Demande confirmation avant de fermer le ticket """
# 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
# Créer l'embed de confirmation
embed = discord . Embed (
title = " 🔒 Confirmer la Fermeture " ,
description = " Êtes-vous sûr de vouloir fermer ce ticket? \n \n "
" Cette action est irréversible et le salon sera supprimé. " ,
color = discord . Color . orange ( )
)
# Ajouter les informations du ticket
category = self . config . get_category ( self . ticket . category_id )
duration = self . ticket . duration_minutes
embed . add_field ( name = " 📁 Catégorie " , value = category . name if category else " N/A " , inline = True )
embed . add_field ( name = " ⏱️ Durée " , value = f " { duration : .1f } min " if duration else " N/A " , inline = True )
embed . add_field ( name = " 📊 Statut " , value = self . ticket . status . value . capitalize ( ) , inline = True )
embed . set_footer ( text = " Cliquez sur Confirmer pour fermer ou Annuler pour annuler l ' opération " )
# Afficher la vue de confirmation
view = CloseConfirmationView ( self . bot , self . storage , self . config , self . ticket )
await interaction . response . send_message ( embed = embed , view = view , ephemeral = True )
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 )
2026-01-14 21:32:20 +01:00
def _is_staff ( self , interaction : discord . Interaction ) - > bool :
return is_staff ( interaction , self . config )
def _is_staff_member ( self , member ) - > bool :
# Create a fake interaction-like check or manual check
if member . guild_permissions . administrator :
return True
if member . id in self . config . staff_users :
return True
for role_id in self . config . staff_roles :
role = member . guild . get_role ( int ( role_id ) )
if role and role in member . roles :
return True
return False
class CloseConfirmationView ( discord . ui . View ) :
""" 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
@discord.ui.button ( label = " ✅ Confirmer " , style = discord . ButtonStyle . green , custom_id = " close_confirm " )
async def confirm_callback ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
""" L ' utilisateur confirme la fermeture """
# Vérifier les permissions
can_close = (
interaction . user . id == self . ticket . user_id or
self . _is_staff ( interaction )
)
if not can_close :
await interaction . response . send_message ( " Vous ne pouvez pas fermer ce ticket. " , ephemeral = True )
return
# Show the delay view with countdown
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 ( )
)
view = CloseDelayView ( self . bot , self . storage , self . config , self . ticket )
await interaction . response . send_message ( embed = embed , view = view , ephemeral = True )
# Start the countdown
await view . start_countdown ( interaction )
@discord.ui.button ( label = " ❌ Annuler " , style = discord . ButtonStyle . red , custom_id = " close_cancel " )
async def cancel_callback ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
""" L ' utilisateur annule la fermeture """
await interaction . response . defer ( )
await interaction . delete_original_response ( )
2026-01-10 15:30:51 +01:00
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
2026-01-14 21:32:20 +01:00
class CloseDelayView ( discord . ui . View ) :
""" 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
cancel_btn = discord . ui . Button (
label = " ❌ Annuler " ,
style = discord . ButtonStyle . red ,
custom_id = " delay_cancel "
)
cancel_btn . callback = self . _cancel_callback
self . add_item ( cancel_btn )
async def start_countdown ( self , interaction : discord . Interaction ) :
""" 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 :
embed = discord . Embed (
title = " 🔒 Fermeture du Ticket " ,
description = f " ** { step_text } ** \n \n "
f " Merci pour votre confiance! " ,
color = discord . Color . orange ( )
)
embed . set_footer ( text = f " Ticket # { self . ticket . channel_id } " )
await interaction . edit_original_response ( embed = embed )
await asyncio . sleep ( delay )
# Procéder à la fermeture
await self . _close_ticket ( interaction )
2026-01-10 15:30:51 +01:00
2026-01-14 21:32:20 +01:00
async def _close_ticket ( self , interaction : discord . Interaction ) :
""" 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
# Send transcript to log channel
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 :
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 ( )
)
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 )
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 } " )
embed = discord . Embed (
title = " ❌ Erreur " ,
description = " Le ticket a été fermé mais la suppression du salon a échoué. " ,
color = discord . Color . red ( )
)
await interaction . channel . send ( embed = embed )
async def _cancel_callback ( self , interaction : discord . Interaction ) :
""" 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
embed = discord . Embed (
title = " ❌ Fermeture Annulée " ,
description = " La fermeture du ticket a été annulée. " ,
color = discord . Color . red ( )
)
await interaction . channel . send ( embed = embed , delete_after = 5 )
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
2026-01-10 15:30:51 +01:00
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 )
2026-01-14 21:32:20 +01:00
# Staff permissions
staff_btn = discord . ui . Button (
2026-01-10 15:30:51 +01:00
style = discord . 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
config_perms_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
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
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
2026-01-14 21:32:20 +01:00
async def _staff_perms_callback ( self , interaction : discord . Interaction ) :
view = PermissionConfigView ( self . bot , self . storage , self . config , interaction . guild , " staff " )
2026-01-10 15:30:51 +01:00
embed = discord . 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-01-10 15:30:51 +01:00
color = discord . Color . blue ( )
)
2026-01-14 21:32:20 +01:00
self . _add_current_perms_fields ( embed , self . config . staff_roles , self . config . staff_users , interaction . guild )
await interaction . response . send_message ( embed = embed , view = view , ephemeral = True )
async def _config_perms_callback ( self , interaction : discord . Interaction ) :
view = PermissionConfigView ( self . bot , self . storage , self . config , interaction . guild , " config " )
embed = discord . Embed (
title = " Permissions Configuration " ,
description = " Configurez qui peut modifier les paramètres du bot (/ticketconfig) " ,
color = discord . Color . orange ( )
)
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
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 )
2026-01-14 21:32:20 +01:00
class PermissionConfigView ( discord . ui . View ) :
""" View to configure permissions (Staff/Config Roles & Users) """
2026-01-10 15:30:51 +01:00
2026-01-14 21:32:20 +01:00
def __init__ ( self , bot , storage , config : GuildTicketConfig , guild : discord . 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-01-10 15:30:51 +01:00
2026-01-14 21:32:20 +01:00
def _add_role_select ( self ) :
select = discord . ui . RoleSelect (
placeholder = f " Sélectionnez les rôles { self . config_type } " ,
min_values = 0 ,
max_values = 25 ,
row = 0
)
select . callback = self . _role_callback
self . add_item ( select )
def _add_user_select ( self ) :
select = discord . ui . UserSelect (
placeholder = f " Sélectionnez les utilisateurs { self . config_type } " ,
min_values = 0 ,
max_values = 25 ,
row = 1
)
select . callback = self . _user_callback
self . add_item ( select )
async def _role_callback ( self , interaction : discord . Interaction ) :
# Determine which list to update
if self . config_type == " staff " :
target_list = self . config . staff_roles
else :
target_list = self . config . config_roles
# Get new selected IDs
# Note: RoleSelect/UserSelect returns the objects, not IDs directly in values sometimes depending on lib version,
# but interaction.data['values'] is reliable for IDs if we want raw.
# However, interaction.values which is bound to callback by library usually contains the objects.
# Let's use the view item's values.
# Actually standard lib behavior: interaction.data['values'] contains IDs.
# But let's use the argument 'interaction' and the component.
# Better: use the values from the specific component that triggered this
# But since we have separate callbacks we can just read interaction.data
selected_ids = [ str ( r . id ) for r in interaction . data . get ( ' values ' , [ ] ) ] # But wait, select menu interactions return objects in the view item's values property if mapped? NO, interaction.values is for string selects.
# For Entity Select Menus (Role/User), interaction.data['values'] contains IDs.
# BUT, discord.py 2.0 uses select.values for string selects.
# For RoleSelect, values are the list of Role objects.
# Let's try to trust the interaction object's resolution.
# Wait, standard discord.py RoleSelect callback receives interaction.
# The select object itself will have 'values' populated with Role objects after processing.
# But we need to reference the select item.
# Since we added it dynamically, we can't easily reference 'self.role_select'.
# Workaround: Re-read from interaction.data['values'] which are IDs.
values = interaction . data . get ( ' values ' , [ ] )
# WE NEED TO MERGE with existing? No, select menu usually replaces selection if it shows current selection.
# BUT Discord UI Selects don't show "current selection" natively pre-filled unless we set defaults.
# AND we can only set defaults for StringSelect, NOT for RoleSelect/UserSelect (API Limitation).
# So RoleSelect/UserSelect always start empty.
# This implies we can only ADD/REMOVE or we have to handle it as "Add these", "Remove these"?
# OR we treat valid input as "Append these to existing" or "Replace existing"?
# Given we can't show pre-selected items, "Replace" is dangerous because user can't see what was there.
# "Append" is safer but how to remove?
# Modification: Make it "Toggle" or just "Add/Remove" via specific actions?
# A common pattern is: The select menu creates a list. We Add them.
# But how to remove?
# Maybe we should stick to STRING SELECT for removal if we want to show lists.
# But the user issue is "Can't see all roles".
# Better approach for Entity Selects:
# They are usually used for "Add these items".
# To remove, we might need a separate "Remove" workflow or a StringSelect of *current* items to remove.
# Let's implement: "Add selected roles/users to permission".
# And provide a way to "Clear/Remove" via a StringSelect of *existing* configured items.
# So:
# 1. RoleSelect (Add)
# 2. UserSelect (Add)
# 3. StringSelect (Remove) - Populated with currently configured items.
new_ids = interaction . data . get ( ' values ' , [ ] )
added_count = 0
for new_id in new_ids :
if new_id not in target_list :
target_list . append ( new_id )
added_count + = 1
self . storage . save_config ( interaction . guild_id , self . config )
await interaction . response . send_message (
f " ✅ ** { added_count } ** rôle(s) ajouté(s) à la configuration { self . config_type } . " ,
ephemeral = True
)
# Refresh view to update Remove dropdown if we add it
await self . _refresh_view ( interaction )
async def _user_callback ( self , interaction : discord . Interaction ) :
if self . config_type == " staff " :
target_list = self . config . staff_users
else :
target_list = self . config . config_users
2026-01-10 15:30:51 +01:00
2026-01-14 21:32:20 +01:00
new_ids = [ int ( uid ) for uid in interaction . data . get ( ' values ' , [ ] ) ]
added_count = 0
for new_id in new_ids :
if new_id not in target_list :
target_list . append ( new_id )
added_count + = 1
self . storage . save_config ( interaction . guild_id , self . config )
await interaction . response . send_message (
f " ✅ ** { added_count } ** utilisateur(s) ajouté(s) à la configuration { self . config_type } . " ,
ephemeral = True
)
await self . _refresh_view ( interaction )
async def _refresh_view ( self , interaction ) :
# Re-render the view?
# We can't easily edit the ephemeral message with a new view from here without 'response.edit_message'
# But we already responded to interaction.
# So we can followup edit?
# Actually we just want to update the "Remove" select menu.
# Let's add the Remove Select Menu.
self . clear_items ( )
self . _add_role_select ( )
self . _add_user_select ( )
self . _add_remove_select ( )
await interaction . message . edit ( view = self )
def _add_remove_select ( self ) :
# Build options from current config
2026-01-10 15:30:51 +01:00
options = [ ]
2026-01-14 21:32:20 +01:00
if self . config_type == " staff " :
roles = self . config . staff_roles
users = self . config . staff_users
else :
roles = self . config . config_roles
users = self . config . config_users
# Limit to 25 items for removal dropdown (API Limit)
# If more, we might need pagination or just show first 25.
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 } "
options . append ( discord . SelectOption (
label = f " Role: { name [ : 90 ] } " ,
value = f " r_ { r_id } " ,
emoji = " 🛡️ " ,
description = " Cliquez pour retirer "
) )
count + = 1
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 } "
options . append ( discord . SelectOption (
label = f " User: { name [ : 90 ] } " ,
value = f " u_ { u_id } " ,
emoji = " 👤 " ,
description = " Cliquez pour retirer "
) )
count + = 1
2026-01-10 15:30:51 +01:00
if options :
select = discord . ui . Select (
2026-01-14 21:32:20 +01:00
placeholder = " Sélectionnez pour RETIRER des permissions... " ,
options = options ,
min_values = 1 ,
max_values = min ( len ( options ) , 25 ) ,
row = 2 ,
custom_id = " remove_select "
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
async def _remove_callback ( self , interaction : discord . Interaction ) :
values = interaction . data . get ( ' values ' , [ ] )
2026-01-10 15:30:51 +01:00
2026-01-14 21:32:20 +01:00
removed_count = 0
for val in values :
prefix , id_str = val . split ( ' _ ' )
id_val = int ( id_str )
if prefix == ' r ' : # Role
target_list = self . config . staff_roles if self . config_type == " staff " else self . config . config_roles
if str ( id_val ) in target_list :
target_list . remove ( str ( id_val ) )
removed_count + = 1
elif prefix == ' u ' : # User
target_list = self . config . staff_users if self . config_type == " staff " else self . config . config_users
if id_val in target_list :
target_list . remove ( id_val )
removed_count + = 1
2026-01-10 15:30:51 +01:00
self . storage . save_config ( interaction . guild_id , self . config )
2026-01-14 21:32:20 +01:00
await interaction . response . send_message (
f " 🗑️ ** { removed_count } ** permission(s) retirée(s). " ,
ephemeral = True
2026-01-10 15:30:51 +01:00
)
2026-01-14 21:32:20 +01:00
await self . _refresh_view ( interaction )
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 ( )
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 )
2026-01-14 21:32:20 +01:00
# Reload config
2026-01-10 15:30:51 +01:00
config = self . storage . load_config ( ctx . guild . id )
2026-01-14 21:32:20 +01:00
# Vérifier permissions (admin ou config roles/users)
if not can_access_config ( ctx , config ) :
2026-01-10 15:30:51 +01:00
await ctx . send ( " ❌ Cette commande est réservée aux administrateurs ou aux rôles configurés. " , ephemeral = True )
return
2026-01-14 21:32:20 +01:00
2026-01-10 15:30:51 +01:00
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