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-05-01 16:16:33 +02:00
# Add a task for sending feedback reminders
class FeedbackReminderTask :
def __init__ ( self , bot ) :
self . bot = bot
self . reminders = { } # Store active reminders
async def schedule_feedback_reminder ( self , user_id : int , ticket_id : int , delay_hours : int = 24 ) :
""" Schedule a feedback reminder after a ticket is closed """
# This would be implemented with a proper task scheduler
pass
2026-01-04 00:01:15 +01:00
2026-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 )
2026-05-01 16:16:33 +02:00
# Staff Ratings (staff only)
staff_ratings_btn = discord . ui . Button (
style = discord . ButtonStyle . success ,
label = " ⭐ Notation du Staff " ,
custom_id = " ticket_staff_ratings "
)
staff_ratings_btn . callback = self . _staff_ratings_callback
self . add_item ( staff_ratings_btn )
2026-01-10 15:30:51 +01:00
# 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-02-07 14:42:39 +01:00
# Reload config to ensure we have latest permissions/categories
self . config = self . storage . load_config ( interaction . guild_id )
2026-01-10 15:30:51 +01:00
if not self . config . enabled :
await interaction . response . send_message ( " Le système de tickets est désactivé. " , ephemeral = True )
2026-01-04 00:01:15 +01:00
return
2026-01-10 15:30:51 +01:00
if not self . config . categories :
await interaction . response . send_message ( " Aucune catégorie configurée. Utilisez la configuration. " , ephemeral = True )
2026-01-04 00:01:15 +01:00
return
2026-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-02-07 14:42:39 +01:00
# Reload config
self . config = self . storage . load_config ( interaction . guild_id )
2026-01-10 15:30:51 +01:00
2026-01-04 00:01:15 +01:00
storage = get_storage ( )
2026-01-10 15:30:51 +01:00
tickets = storage . get_user_tickets ( interaction . guild_id , interaction . user . id , open_only = True )
2026-01-04 00:01:15 +01:00
if not tickets :
2026-01-10 15:30:51 +01:00
await interaction . response . send_message ( " Vous n ' avez aucun ticket ouvert. " , ephemeral = True )
2026-01-04 00:01:15 +01:00
return
2026-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 )
2026-05-01 16:16:33 +02:00
async def _staff_ratings_callback ( self , interaction : discord . Interaction ) :
""" Display staff ratings (staff only) """
# Reload config
self . config = self . storage . load_config ( interaction . guild_id )
if not self . _is_staff ( interaction ) :
await interaction . response . send_message ( " ❌ Cette fonctionnalité est réservée au staff. " , ephemeral = True )
return
await interaction . response . defer ( ephemeral = True )
analytics = StaffAnalytics ( self . bot )
# Get staff list for individual stats
staff_list = await analytics . get_staff_list ( interaction . guild_id )
# Create main embed with overall stats
embed = discord . Embed (
title = " ⭐ Notation du Staff " ,
description = " Performance globale 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 )
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-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 :
2026-02-07 14:42:39 +01:00
2026-01-10 15:30:51 +01:00
2026-01-14 21:32:20 +01:00
category = self . config . get_category ( category_id )
if not category :
await interaction . response . send_message ( " Catégorie non trouvée. " , ephemeral = True )
return
storage = get_storage ( )
# Check limits
user_tickets = storage . get_user_tickets ( interaction . guild_id , interaction . user . id , open_only = True )
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-05-01 16:16:33 +02:00
# Add staff roles (category-specific roles override global ones)
staff_roles_to_add = self . category . staff_roles if self . category . staff_roles else self . config . staff_roles
for role_id in staff_roles_to_add :
try :
role = guild . get_role ( int ( role_id ) )
if role and role not in overwrites :
overwrites [ role ] = discord . PermissionOverwrite ( read_messages = True , send_messages = True )
except ( ValueError , TypeError ) :
continue
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
# Create channel name using username and category without numbers
safe_username = " " . join ( c for c in user . display_name if not c . isdigit ( ) ) . strip ( )
if not safe_username :
safe_username = " user "
safe_category = " " . join ( c for c in self . category . name if not c . isdigit ( ) ) . strip ( )
channel_name = f " { safe_username } - { safe_category } "
2026-01-04 00:01:15 +01:00
2026-01-10 15:30:51 +01:00
try :
category_channel = None
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 )
2026-02-07 14:42:39 +01:00
class SetCategoryModal ( discord . ui . Modal ) :
""" Modal pour définir la catégorie Discord où les tickets seront créés """
def __init__ ( self , bot , storage , config : GuildTicketConfig ) :
super ( ) . __init__ ( title = " Définir la Catégorie Discord " )
self . bot = bot
self . storage = storage
self . config = config
self . category = discord . ui . TextInput (
label = " Catégorie Discord " ,
placeholder = " Entrez l ' ID de la catégorie (clic droit -> Copier l ' ID) " ,
required = True ,
max_length = 20
)
self . add_item ( self . category )
async def on_submit ( self , interaction : discord . Interaction ) :
cat_id = self . category . value . strip ( )
# Verify it's a valid ID
if not cat_id . isdigit ( ) :
await interaction . response . send_message ( " ❌ Veuillez entrer un ID de catégorie valide (que des chiffres). " , ephemeral = True )
return
# Check if category exists
category = interaction . guild . get_channel ( int ( cat_id ) )
if not category or not isinstance ( category , discord . CategoryChannel ) :
await interaction . response . send_message ( " ❌ Catégorie introuvable ou ce n ' est pas une catégorie. " , ephemeral = True )
return
self . config . default_category = cat_id
self . storage . save_config ( interaction . guild_id , self . config )
await interaction . response . send_message ( f " 📁 Les tickets seront désormais créés dans la catégorie ** { category . name } **! " , ephemeral = True )
2026-01-10 15:30:51 +01:00
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 )
2026-05-01 16:16:33 +02:00
# Claim (if enabled and ticket is open or claimed)
if config . claim_enabled and self . category and self . category . allow_claims and ticket . status in [ TicketStatus . OPEN , TicketStatus . CLAIMED ] :
2026-01-10 15:30:51 +01:00
claim_btn = discord . ui . Button (
style = discord . ButtonStyle . primary ,
2026-05-01 16:16:33 +02:00
label = " Réclamer " if ticket . status == TicketStatus . OPEN else " Transférer " ,
2026-01-10 15:30:51 +01:00
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
2026-05-01 16:16:33 +02:00
if self . ticket . claimed_by == interaction . user . id :
await interaction . response . send_message ( " Vous avez déjà réclamé ce ticket. " , ephemeral = True )
2026-01-10 15:30:51 +01:00
return
2026-05-01 16:16:33 +02:00
old_claimer = self . ticket . claimed_by
2026-01-10 15:30:51 +01:00
self . ticket . claimed_by = interaction . user . id
self . ticket . status = TicketStatus . CLAIMED
self . storage . save_ticket ( self . ticket )
2026-05-01 16:16:33 +02:00
# Update view (change label)
2026-01-10 15:30:51 +01:00
for item in self . children :
if item . custom_id == " ticket_claim " :
2026-05-01 16:16:33 +02:00
item . label = " Transférer "
2026-01-10 15:30:51 +01:00
break
# Force UI update
await interaction . response . edit_message ( view = self )
2026-05-01 16:16:33 +02:00
if old_claimer :
embed = discord . Embed (
title = " 🔄 Ticket Transféré " ,
description = f " Le ticket a été transféré de <@ { old_claimer } > à { interaction . user . mention } " ,
color = discord . Color . blue ( )
)
else :
embed = TicketEmbedFormatter . ticket_claimed ( self . ticket , interaction . user )
2026-01-10 15:30:51 +01:00
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 :
2026-05-01 16:16:33 +02:00
return is_staff ( interaction , self . config , self . category )
2026-01-14 21:32:20 +01:00
def _is_staff_member ( self , member ) - > bool :
2026-05-01 16:16:33 +02:00
""" Check if a specific member is staff, considering category roles """
if not member :
return False
# Handle cases where member is a User (left the guild)
if not isinstance ( member , discord . Member ) :
return member . id in self . config . staff_users
2026-01-14 21:32:20 +01:00
if member . guild_permissions . administrator :
return True
if member . id in self . config . staff_users :
return True
2026-05-01 16:16:33 +02:00
# Check category-specific staff roles first
staff_roles = self . category . staff_roles if self . category and self . category . staff_roles else self . config . staff_roles
for role_id in staff_roles :
try :
role = member . guild . get_role ( int ( role_id ) )
if role and role in member . roles :
return True
except ( ValueError , TypeError , AttributeError ) :
continue
2026-01-14 21:32:20 +01:00
return False
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
2026-05-01 16:16:33 +02:00
self . category = config . get_category ( ticket . category_id )
2026-01-14 21:32:20 +01:00
@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 :
2026-05-01 16:16:33 +02:00
return is_staff ( interaction , self . config , self . category )
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
2026-02-07 14:42:39 +01:00
# Send Feedback Request to User via DM
try :
user = interaction . guild . get_member ( self . ticket . user_id )
if user :
from . feedback_view import FeedbackView
dm_embed = discord . Embed (
title = " 📝 Votre avis nous intéresse ! " ,
description = f " Votre ticket **# { self . ticket . channel_id } ** a été fermé. \n Merci de prendre un moment pour noter la qualité du support. " ,
color = discord . Color . green ( ) ,
timestamp = datetime . now ( )
)
dm_embed . add_field ( name = " Durée du ticket " , value = f " { duration : .1f } minutes " , inline = True )
if self . close_reason :
dm_embed . add_field ( name = " Raison de fermeture " , value = self . close_reason , inline = False )
2026-05-01 16:16:33 +02:00
# Create a special view with a "Noter le staff" button
2026-02-07 14:42:39 +01:00
view = FeedbackView ( self . bot , self . storage , self . ticket )
2026-05-01 16:16:33 +02:00
# Add a button to directly open the feedback form
note_staff_btn = discord . ui . Button (
label = " ⭐ Noter le staff " ,
style = discord . ButtonStyle . primary ,
custom_id = " note_staff_direct "
)
note_staff_btn . callback = self . _note_staff_callback
view . add_item ( note_staff_btn )
2026-02-07 14:42:39 +01:00
await user . send ( embed = dm_embed , view = view )
except Exception as e :
print ( f " Failed to send feedback DM: { e } " )
2026-01-14 21:32:20 +01:00
# Send transcript to log channel
2026-05-01 16:16:33 +02:00
if self . config . log_channel_id :
2026-01-14 21:32:20 +01:00
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 )
2026-05-01 16:16:33 +02:00
if transcript_file and os . path . exists ( transcript_file ) :
file = discord . File ( transcript_file , filename = os . path . basename ( transcript_file ) )
await log_channel . send ( embed = log_embed , file = file )
else :
log_embed . add_field ( name = " ⚠️ Transcript " , value = " Le transcript n ' a pas pu être généré (l ' utilisateur a peut-être quitté le serveur). " , inline = False )
await log_channel . send ( embed = log_embed )
2026-01-14 21:32:20 +01:00
except Exception as e :
print ( f " Error sending transcript to log channel: { e } " )
# Delete the ticket channel
try :
await interaction . channel . delete ( reason = f " Ticket fermé par { interaction . user } " )
except Exception as e :
print ( f " Error deleting channel: { e } " )
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 )
2026-05-01 16:16:33 +02:00
async def _note_staff_callback ( self , interaction : discord . Interaction ) :
""" Handle direct staff rating button """
# This would open a direct feedback form
await interaction . response . send_message ( " Vous pouvez maintenant noter le staff directement ! " , ephemeral = True )
2026-01-14 21:32:20 +01:00
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 :
2026-05-01 16:16:33 +02:00
return is_staff ( interaction , self . config , self . category )
2026-01-14 21:32:20 +01:00
2026-01-10 15:30:51 +01:00
def _is_staff_member ( self , member ) - > bool :
2026-05-01 16:16:33 +02:00
""" Check if a specific member is staff, considering category roles """
if not member :
return False
# Handle cases where member is a User (left the guild)
if not isinstance ( member , discord . Member ) :
return member . id in self . config . staff_users
if member . guild_permissions . administrator :
return True
if member . id in self . config . staff_users :
return True
# Check category-specific staff roles first
staff_roles = self . category . staff_roles if self . category and self . category . staff_roles else self . config . staff_roles
for role_id in staff_roles :
try :
role = member . guild . get_role ( int ( role_id ) )
if role and role in member . roles :
return True
except ( ValueError , TypeError , AttributeError ) :
continue
2026-01-10 15:30:51 +01:00
return False
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
2026-05-01 16:16:33 +02:00
if self . config . log_channel_id :
2026-01-10 15:30:51 +01:00
try :
log_channel = interaction . guild . get_channel ( self . config . log_channel_id )
if log_channel :
# Create log embed
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
2026-05-01 16:16:33 +02:00
if transcript_file and os . path . exists ( transcript_file ) :
file = discord . File ( transcript_file , filename = os . path . basename ( transcript_file ) )
await log_channel . send ( embed = log_embed , file = file )
else :
log_embed . add_field ( name = " ⚠️ Transcript " , value = " Le transcript n ' a pas pu être généré (l ' utilisateur a peut-être quitté le serveur). " , inline = False )
await log_channel . send ( embed = log_embed )
2026-01-10 15:30:51 +01:00
except Exception as e :
print ( f " Error sending transcript to log channel: { e } " )
# Delete the ticket channel
try :
await interaction . channel . delete ( reason = f " Ticket fermé par { interaction . user } " )
except Exception as e :
print ( f " Error deleting channel: { e } " )
# Fallback: just mark as closed if deletion fails
await interaction . followup . send ( " Erreur lors de la suppression du canal, mais le ticket a été fermé. " , ephemeral = True )
def _is_staff ( self , interaction : discord . Interaction ) - > bool :
2026-05-01 16:16:33 +02:00
return is_staff ( interaction , self . config , self . category )
def _is_staff_member ( self , member ) - > bool :
""" Check if a specific member is staff, considering category roles """
if not member :
return False
# Handle cases where member is a User (left the guild)
if not isinstance ( member , discord . Member ) :
return member . id in self . config . staff_users
if member . guild_permissions . administrator :
return True
if member . id in self . config . staff_users :
return True
staff_roles = self . category . staff_roles if self . category and self . category . staff_roles else self . config . staff_roles
for role_id in staff_roles :
try :
role = member . guild . get_role ( int ( role_id ) )
if role and role in member . roles :
return True
except ( ValueError , TypeError , AttributeError ) :
continue
return False
2026-01-10 15:30:51 +01:00
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 )
2026-02-07 14:42:39 +01:00
# Default category
self . category_btn = discord . ui . Button (
label = " 📁 Catégorie par défaut " ,
style = discord . ButtonStyle . secondary ,
custom_id = " config_category "
)
self . category_btn . callback = self . _category_callback
self . add_item ( self . category_btn )
2026-03-28 21:58:44 +01:00
# Gérer les catégories
self . manage_cat_btn = discord . ui . Button (
2026-01-10 15:30:51 +01:00
style = discord . ButtonStyle . blurple ,
2026-03-28 21:58:44 +01:00
label = " 📂 Gérer les catégories " ,
custom_id = " config_manage_cat "
2026-01-10 15:30:51 +01:00
)
2026-03-28 21:58:44 +01:00
self . manage_cat_btn . callback = self . _manage_cat_callback
self . add_item ( self . manage_cat_btn )
2026-01-10 15:30:51 +01:00
2026-01-14 21:32:20 +01:00
# Staff permissions
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 )
2026-03-28 21:58:44 +01:00
async def _manage_cat_callback ( self , interaction : discord . Interaction ) :
view = CategoryManagerView ( self . bot , self . storage , self . config )
await interaction . response . edit_message ( embed = view . _get_embed ( ) , view = view )
2026-01-10 15:30:51 +01:00
2026-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
2026-02-07 14:42:39 +01:00
async def _category_callback ( self , interaction : discord . Interaction ) :
modal = SetCategoryModal ( self . bot , self . storage , self . config )
await interaction . response . send_modal ( modal )
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 )
2026-03-28 21:58:44 +01:00
class EditCategoryModal ( discord . ui . Modal ) :
""" Modal pour modifier une catégorie existante """
def __init__ ( self , bot , storage , config , category : TicketCategory , parent_view ) :
super ( ) . __init__ ( title = f " Modifier: { category . name } " )
self . bot = bot
self . storage = storage
self . config = config
self . category = category
self . parent_view = parent_view
self . name = discord . ui . TextInput (
label = " Nom " ,
default = category . name ,
placeholder = " Ex: Support " ,
required = True
)
self . add_item ( self . name )
self . description = discord . ui . TextInput (
label = " Description " ,
default = category . description ,
placeholder = " Description... " ,
required = False ,
style = discord . TextStyle . paragraph
)
self . add_item ( self . description )
self . emoji = discord . ui . TextInput (
label = " Emoji " ,
default = category . emoji ,
placeholder = " 🎫 " ,
required = False ,
max_length = 5
)
self . add_item ( self . emoji )
async def on_submit ( self , interaction : discord . Interaction ) :
# Update category object
self . category . name = self . name . value
self . category . description = self . description . value or " "
self . category . emoji = self . emoji . value or " 🎫 "
# Save config
self . storage . save_config ( interaction . guild_id , self . config )
# Refresh the parent view's message
await interaction . response . edit_message ( embed = self . parent_view . _get_embed ( ) , view = self . parent_view )
await interaction . followup . send ( f " ✅ Catégorie ** { self . category . name } ** mise à jour ! " , ephemeral = True )
class CategoryManagerView ( discord . ui . View ) :
""" Vue pour lister et gérer les catégories """
def __init__ ( self , bot , storage , config : GuildTicketConfig ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
self . _add_category_select ( )
def _add_category_select ( self ) :
self . clear_items ( )
if self . config . categories :
options = [ ]
for cat in self . config . categories [ : 25 ] : # Limite Select menu
options . append ( discord . SelectOption (
label = cat . name ,
value = cat . id ,
description = cat . description [ : 100 ] if cat . description else " Aucune description " ,
emoji = cat . emoji
) )
select = discord . ui . Select (
placeholder = " Sélectionnez une catégorie à gérer... " ,
options = options ,
custom_id = " manage_cat_select "
)
select . callback = self . _category_select_callback
self . add_item ( select )
# Bouton Ajouter
add_btn = discord . ui . Button (
style = discord . ButtonStyle . success ,
label = " ➕ Ajouter une catégorie" ,
custom_id = " manage_cat_add "
)
add_btn . callback = self . _add_cat_callback
self . add_item ( add_btn )
# Bouton Retour
back_btn = discord . ui . Button (
style = discord . ButtonStyle . secondary ,
label = " ⬅️ Retour " ,
custom_id = " manage_cat_back "
)
back_btn . callback = self . _back_callback
self . add_item ( back_btn )
def _get_embed ( self ) :
embed = discord . Embed (
title = " 📂 Gestion des Catégories " ,
description = f " Il y a actuellement ** { len ( self . config . categories ) } ** catégorie(s) configurée(s). \n \n "
" Sélectionnez une catégorie dans le menu pour la modifier ou la supprimer. " ,
color = discord . Color . blue ( )
)
if not self . config . categories :
embed . description + = " \n \n ⚠️ **Aucune catégorie configurée.** Commencez par en ajouter une ! "
for cat in self . config . categories [ : 10 ] :
embed . add_field (
name = f " { cat . emoji } { cat . name } " ,
value = cat . description or " Pas de description " ,
inline = False
)
return embed
async def _category_select_callback ( self , interaction : discord . Interaction ) :
# En utilisant interaction.data.get('values') pour obtenir la valeur sélectionnée
cat_id = interaction . data . get ( ' values ' , [ None ] ) [ 0 ]
category = self . config . get_category ( cat_id )
if not category :
await interaction . response . send_message ( " Catégorie introuvable. " , ephemeral = True )
return
view = CategoryActionView ( self . bot , self . storage , self . config , category , self )
await interaction . response . edit_message ( embed = view . _get_embed ( ) , view = view )
async def _add_cat_callback ( self , interaction : discord . Interaction ) :
# Utilisation de la classe AddCategoryModal existante
class AddCategoryManagerModal ( AddCategoryModal ) :
def __init__ ( self , bot , storage , config , parent_view ) :
super ( ) . __init__ ( bot , storage , config )
self . parent_view = parent_view
async def 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 )
self . storage . save_config ( interaction . guild_id , self . config )
# Refresh parent view
self . parent_view . _add_category_select ( )
# On doit utiliser edit_message sur l'interaction d'origine ou renvoyer un embed
await interaction . response . edit_message (
embed = self . parent_view . _get_embed ( ) ,
view = self . parent_view
)
await interaction . followup . send ( f " Catégorie ** { self . name . value } ** créée! " , ephemeral = True )
modal = AddCategoryManagerModal ( self . bot , self . storage , self . config , self )
await interaction . response . send_modal ( modal )
async def _back_callback ( self , interaction : discord . Interaction ) :
view = ConfigPanelView ( self . bot , self . 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 . edit_message ( embed = embed , view = view )
class CategoryActionView ( discord . ui . View ) :
""" Actions pour une catégorie spécifique """
def __init__ ( self , bot , storage , config , category , parent_view ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
self . category = category
self . parent_view = parent_view
def _get_embed ( self ) :
embed = discord . Embed (
title = f " Gestion: { self . category . name } " ,
description = f " **ID:** ` { self . category . id } ` \n "
f " **Emoji:** { self . category . emoji } \n "
f " **Description:** { self . category . description or ' Aucune ' } \n "
2026-05-01 16:16:33 +02:00
f " **Couleur:** ` { self . category . color } ` \n "
f " **Staff Spécifique:** { ' , ' . join ( [ f ' <@& { r } > ' for r in self . category . staff_roles ] ) if self . category . staff_roles else ' Global (Tous les staffs) ' } " ,
2026-03-28 21:58:44 +01:00
color = discord . Color . blue ( )
)
return embed
@discord.ui.button ( label = " 📝 Modifier " , style = discord . ButtonStyle . primary )
async def edit_callback ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
modal = EditCategoryModal ( self . bot , self . storage , self . config , self . category , self )
await interaction . response . send_modal ( modal )
2026-05-01 16:16:33 +02:00
@discord.ui.button ( label = " 🛡️ Permissions " , style = discord . ButtonStyle . success )
async def permissions_callback ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
view = CategoryPermissionsView ( self . bot , self . storage , self . config , self . category , self )
embed = discord . Embed (
title = f " Permissions: { self . category . name } " ,
description = " Choisissez les rôles qui auront accès à cette catégorie de tickets. \n \n "
" ⚠️ **Si vous sélectionnez des rôles ici, seuls ces rôles (et les admins) auront accès aux tickets de cette catégorie.** "
" Les rôles staff globaux n ' y auront plus accès. " ,
color = discord . Color . green ( )
)
await interaction . response . edit_message ( embed = embed , view = view )
2026-03-28 21:58:44 +01:00
@discord.ui.button ( label = " 🗑️ Supprimer " , style = discord . ButtonStyle . danger )
async def delete_callback ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
view = DeleteCategoryConfirmationView ( self . bot , self . storage , self . config , self . category , self . parent_view )
embed = discord . Embed (
title = " ⚠️ Confirmer la suppression " ,
description = f " Êtes-vous sûr de vouloir supprimer la catégorie ** { self . category . name } ** ? \n "
" Cette action est irréversible. " ,
color = discord . Color . red ( )
)
await interaction . response . edit_message ( embed = embed , view = view )
@discord.ui.button ( label = " ⬅️ Retour " , style = discord . ButtonStyle . secondary )
async def back_callback ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
self . parent_view . _add_category_select ( )
await interaction . response . edit_message ( embed = self . parent_view . _get_embed ( ) , view = self . parent_view )
2026-05-01 16:16:33 +02:00
class CategoryPermissionsView ( discord . ui . View ) :
""" Vue pour configurer les rôles staff d ' une catégorie """
def __init__ ( self , bot , storage , config , category , parent_view ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
self . category = category
self . parent_view = parent_view
@discord.ui.select ( cls = discord . ui . RoleSelect , placeholder = " Sélectionner les rôles staff (Gestion) " , min_values = 0 , max_values = 10 )
async def role_select_callback ( self , interaction : discord . Interaction , select : discord . ui . RoleSelect ) :
role_ids = [ str ( role . id ) for role in select . values ]
self . category . staff_roles = role_ids
self . storage . save_config ( interaction . guild_id , self . config )
# Refresh embed
await interaction . response . edit_message ( embed = self . parent_view . _get_embed ( ) , view = self . parent_view )
@discord.ui.button ( label = " ⬅️ Annuler " , style = discord . ButtonStyle . secondary )
async def back_callback ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
await interaction . response . edit_message ( embed = self . parent_view . _get_embed ( ) , view = self . parent_view )
2026-03-28 21:58:44 +01:00
class DeleteCategoryConfirmationView ( discord . ui . View ) :
def __init__ ( self , bot , storage , config , category , parent_view ) :
super ( ) . __init__ ( timeout = None )
self . bot = bot
self . storage = storage
self . config = config
self . category = category
self . parent_view = parent_view
@discord.ui.button ( label = " ✅ Confirmer " , style = discord . ButtonStyle . danger )
async def confirm_callback ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
if self . category in self . config . categories :
self . config . categories . remove ( self . category )
self . storage . save_config ( interaction . guild_id , self . config )
self . parent_view . _add_category_select ( )
await interaction . response . edit_message (
embed = self . parent_view . _get_embed ( ) ,
view = self . parent_view
)
await interaction . followup . send ( f " ✅ Catégorie ** { self . category . name } ** supprimée. " , ephemeral = True )
@discord.ui.button ( label = " ❌ Annuler " , style = discord . ButtonStyle . secondary )
async def cancel_callback ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
view = CategoryActionView ( self . bot , self . storage , self . config , self . category , self . parent_view )
await interaction . response . edit_message ( embed = view . _get_embed ( ) , view = view )
2026-01-10 15:30:51 +01:00
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-02-07 14:42:39 +01:00
self . _add_remove_select ( )
2026-01-10 15:30:51 +01:00
2026-01-14 21:32:20 +01:00
def _add_role_select ( self ) :
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 ) :
2026-02-07 14:42:39 +01:00
try :
# Determine which list to update
if self . config_type == " staff " :
target_list = self . config . staff_roles
else :
target_list = self . config . config_roles
2026-01-14 21:32:20 +01:00
2026-02-07 14:42:39 +01:00
# Get new selected IDs
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 )
2026-01-14 21:32:20 +01:00
2026-01-10 15:30:51 +01:00
2026-02-07 14:42:39 +01:00
await self . _refresh_view ( interaction )
except Exception as e :
# Assuming kuby_logger is defined elsewhere, if not, replace with print or logging.error
# import kuby_logger # Uncomment if kuby_logger is not globally available
# kuby_logger.error(f"Error in _role_callback: {e}")
import traceback
traceback . print_exc ( )
await interaction . response . send_message ( " Une erreur est survenue lors de la mise à jour des rôles. " , ephemeral = True )
async def _user_callback ( self , interaction : discord . Interaction ) :
try :
if self . config_type == " staff " :
target_list = self . config . staff_users
else :
target_list = self . config . config_users
2026-01-14 21:32:20 +01:00
2026-02-07 14:42:39 +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 self . _refresh_view ( interaction )
except Exception as e :
# Assuming kuby_logger is defined elsewhere
# import kuby_logger
# kuby_logger.error(f"Error in _user_callback: {e}")
import traceback
traceback . print_exc ( )
await interaction . response . send_message ( " Une erreur est survenue lors de la mise à jour des utilisateurs. " , ephemeral = True )
2026-01-14 21:32:20 +01:00
async def _refresh_view ( self , interaction ) :
self . clear_items ( )
self . _add_role_select ( )
self . _add_user_select ( )
self . _add_remove_select ( )
2026-02-07 14:42:39 +01:00
# Rebuild Embed
if self . config_type == " staff " :
title = " Permissions Staff "
description = " Configurez qui peut gérer les tickets (répondre, fermer, etc.) "
color = discord . Color . blue ( )
target_roles = self . config . staff_roles
target_users = self . config . staff_users
else :
title = " Permissions Configuration "
description = " Configurez qui peut modifier les paramètres du bot (/ticketconfig) "
color = discord . Color . orange ( )
target_roles = self . config . config_roles
target_users = self . config . config_users
embed = discord . Embed ( title = title , description = description , color = color )
# Add fields logic (inline reuse)
role_mentions = [ ]
for role_id in target_roles :
role = self . guild . get_role ( int ( role_id ) )
if role :
role_mentions . append ( role . mention )
user_mentions = [ ]
for user_id in target_users :
user = self . 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 )
await interaction . response . edit_message ( embed = embed , view = self )
2026-01-14 21:32:20 +01:00
def _add_remove_select ( self ) :
# Build options from current config
2026-01-10 15:30:51 +01:00
options = [ ]
2026-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 ) :
2026-02-07 14:42:39 +01:00
try :
values = interaction . data . get ( ' values ' , [ ] )
2026-01-14 21:32:20 +01:00
2026-02-07 14:42:39 +01:00
removed_count = 0
for val in values :
prefix , id_str = val . split ( ' _ ' )
id_val = int ( id_str )
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
self . storage . save_config ( interaction . guild_id , self . config )
await self . _refresh_view ( interaction )
except Exception as e :
# Assuming kuby_logger is defined elsewhere
# import kuby_logger
# kuby_logger.error(f"Error in _remove_callback: {e}")
import traceback
traceback . print_exc ( )
await interaction . response . send_message ( " Une erreur est survenue lors de la suppression des permissions. " , ephemeral = True )
2026-01-10 15:30:51 +01:00
class TicketCommands ( commands . Cog ) :
""" Cog principal des tickets """
def __init__ ( self , bot ) :
self . bot = bot
self . storage = get_storage ( )
2026-01-14 21:32:20 +01:00
async def cog_unload ( self ) :
2026-01-10 15:30:51 +01:00
pass
async def cog_load ( self ) :
await self . _register_all_views ( )
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 " )
2026-02-07 14:42:39 +01:00
view = ConfigPanelView ( self . bot , self . storage , config )
2026-01-10 15:30:51 +01:00
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