Kuby/commandes/ticket/__init__.py
2026-05-01 16:17:55 +02:00

2862 lines
115 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Système de Tickets Simplifié
=============================
Un système de tickets simple et intuitif avec panel intégré.
Fonctionnalités:
- Une seule commande: /ticket
- Création de tickets via boutons
- Gestion via interface interactive
- Stats et configuration accessibles depuis le même panel
"""
import os
import sys
import io
import asyncio
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from collections import defaultdict
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import discord
from discord.ext import commands, tasks
import traceback
from PIL import Image, ImageDraw, ImageFont
from .data.storage import get_storage
from .data.models import (
GuildTicketConfig,
TicketCategory,
TicketStatus,
Priority,
TicketData,
TicketStats
)
from .utils.formatters import TicketEmbedFormatter
from .staff_analytics import StaffAnalytics, StaffStatsView
from .utils.permissions import is_staff, is_whitelisted, can_access_config
from src.logger import kuby_logger
# 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
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)
self.bot = bot
self.config = config
self.storage = storage or get_storage()
self.PURPLE = discord.Color.blurple()
# Boutons du panel
self._build_panel()
def _build_panel(self):
"""Construit les boutons du panel selon la configuration"""
self.clear_items()
# Créer ticket
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"
)
analytics_btn.callback = self._analytics_callback
self.add_item(analytics_btn)
# 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)
# 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)
async def _create_callback(self, interaction: discord.Interaction):
"""Affiche les catégories pour créer un ticket"""
# Reload config to ensure we have latest permissions/categories
self.config = self.storage.load_config(interaction.guild_id)
# Reload config to ensure we have latest permissions/categories
self.config = self.storage.load_config(interaction.guild_id)
if not self.config.enabled:
await interaction.response.send_message("Le système de tickets est désactivé.", ephemeral=True)
return
if not self.config.categories:
await interaction.response.send_message("Aucune catégorie configurée. Utilisez la configuration.", ephemeral=True)
return
# Créer view avec les catégories
view = CategorySelectionView(self.bot, self.config)
embed = discord.Embed(
title="Créer un Ticket",
description="Sélectionnez une catégorie ci-dessous:",
color=discord.Color.blurple()
)
for cat in self.config.categories[:5]:
embed.add_field(
name=f"{cat.emoji} {cat.name}",
value=cat.description or "Aucune description",
inline=True
)
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"""
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
storage = get_storage()
tickets = storage.get_user_tickets(interaction.guild_id, interaction.user.id, open_only=True)
if not tickets:
await interaction.response.send_message("Vous n'avez aucun ticket ouvert.", ephemeral=True)
return
embed = discord.Embed(
title="Vos Tickets Ouverts",
description=f"Vous avez **{len(tickets)}** ticket(s) ouvert(s):",
color=discord.Color.blue()
)
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}\nStatut: {ticket.status.value}\nCréé: {ticket.created_at.strftime('%d/%m/%Y')}",
inline=True
)
await interaction.response.send_message(embed=embed, ephemeral=True)
async def _analytics_callback(self, interaction: discord.Interaction):
"""Affiche les analytics (staff uniquement)"""
# 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="📊 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)"""
# Reload config
self.config = self.storage.load_config(interaction.guild_id)
if not self._is_whitelisted(interaction):
await interaction.response.send_message("❌ Vous n'êtes pas autorisé à utiliser cette fonctionnalité. Veuillez demander à être ajouté à la whitelist.", ephemeral=True)
return
if not self._can_access_config(interaction):
await interaction.response.send_message("❌ Cette fonctionnalité est réservée aux administrateurs ou aux rôles configurés.", ephemeral=True)
return
storage = get_storage()
config_view = ConfigPanelView(self.bot, storage, self.config)
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)
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)
def _is_staff(self, interaction: discord.Interaction) -> bool:
return is_staff(interaction, self.config)
def _can_access_config(self, interaction: discord.Interaction) -> bool:
return can_access_config(interaction, self.config)
def _is_whitelisted(self, interaction: discord.Interaction) -> bool:
return is_whitelisted(interaction, self.config, self.bot)
def _calculate_stats(self, guild_id: int) -> TicketStats:
"""Calcule les statistiques"""
storage = get_storage()
tickets = storage.get_guild_tickets(guild_id)
stats = TicketStats()
stats.total_tickets = len(tickets)
total_duration = 0
closed_count = 0
rating_sum = 0
rating_count = 0
for ticket in tickets:
if ticket.status == TicketStatus.OPEN:
stats.open_tickets += 1
elif ticket.status == TicketStatus.CLAIMED:
stats.claimed_tickets += 1
stats.open_tickets += 1
elif ticket.status == TicketStatus.CLOSED:
stats.closed_tickets += 1
closed_count += 1
if ticket.duration_minutes:
total_duration += ticket.duration_minutes
if ticket.rating:
rating_sum += ticket.rating
rating_count += 1
if closed_count > 0:
stats.average_duration_minutes = total_duration / closed_count
if rating_count > 0:
stats.average_rating = rating_sum / rating_count
return stats
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)
class CategorySelectionView(discord.ui.View):
"""Sélection de catégorie pour créer un ticket"""
def __init__(self, bot, config: GuildTicketConfig):
super().__init__(timeout=300) # 5 minutes timeout for ephemeral menu
self.bot = bot
self.config = config
self.id_gen = int(datetime.now().timestamp())
# Ajouter boutons de catégories
for category in config.categories[:5]:
self.add_item(CategoryButton(category, self))
def _is_staff(self, interaction: discord.Interaction) -> bool:
return is_staff(interaction, self.config)
def _can_access_config(self, interaction: discord.Interaction) -> bool:
return can_access_config(interaction, self.config)
def _is_whitelisted(self, interaction: discord.Interaction) -> bool:
return is_whitelisted(interaction, self.config, self.bot)
async def _on_category_select(self, interaction: discord.Interaction, category_id: str):
"""Handle category selection"""
try:
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
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
storage = get_storage()
# 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)
}
# 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
# Create channel name using username and category without numbers
safe_username = "".join(c for c in user.display_name if not c.isdigit()).strip()
if not safe_username:
safe_username = "user"
safe_category = "".join(c for c in self.category.name if not c.isdigit()).strip()
channel_name = f"{safe_username}-{safe_category}"
try:
category_channel = None
# Check if category has a specific Discord category
if self.category.discord_category_id:
try:
category_channel = guild.get_channel(int(self.category.discord_category_id))
except (ValueError, TypeError):
pass
# Fallback to default config category if not found or not set
if not category_channel and self.config.default_category:
try:
category_channel = guild.get_channel(int(self.config.default_category))
except (ValueError, TypeError):
pass
ticket_channel = await guild.create_text_channel(
name=channel_name,
category=category_channel,
overwrites=overwrites,
topic=f"Ticket de {user} | Catégorie: {self.category.name}"
)
except Exception as e:
await interaction.response.send_message(f"Erreur: {e}", ephemeral=True)
return
# Create ticket data
ticket_data = TicketData(
channel_id=ticket_channel.id,
guild_id=guild.id,
user_id=user.id,
category_id=self.category.id,
priority=priority,
status=TicketStatus.OPEN,
reason=reason
)
storage.save_ticket(ticket_data)
# Send welcome message
embed = discord.Embed(
title=f" {self.category.emoji} {self.category.name}",
description=f"Bienvenue {user.mention}!\n\nVotre 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)
# 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)
# 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)
await interaction.response.send_message(f"Ticket créé: {ticket_channel.mention}", ephemeral=True)
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)
class TicketManagementView(discord.ui.View):
"""Gestion du ticket (dans le salon du ticket)"""
def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.ticket = ticket
self.category = config.get_category(ticket.category_id)
# Claim (if enabled and ticket is open or claimed)
if config.claim_enabled and self.category and self.category.allow_claims and ticket.status in [TicketStatus.OPEN, TicketStatus.CLAIMED]:
claim_btn = discord.ui.Button(
style=discord.ButtonStyle.primary,
label="Réclamer" if ticket.status == TicketStatus.OPEN else "Transférer",
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)
async def _close_callback(self, interaction: discord.Interaction):
"""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)
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
storage = get_storage()
ticket = storage.load_ticket(interaction.channel.id)
if not ticket:
await interaction.response.send_message("Ticket non trouvé.", ephemeral=True)
return
# Collect messages
messages = []
async for msg in interaction.channel.history(limit=1000, oldest_first=True):
if msg.author.bot:
continue
messages.append({
"id": str(msg.id),
"author_id": msg.author.id,
"author_name": str(msg.author),
"content": msg.content,
"timestamp": msg.created_at.isoformat(),
"is_staff": self._is_staff_member(msg.author)
})
# Generate transcript
from .utils.transcript import get_transcript_generator
transcript_gen = get_transcript_generator()
filepath = transcript_gen.save_transcript(ticket, messages, format="html")
if filepath and os.path.exists(filepath):
file = discord.File(filepath, filename=os.path.basename(filepath))
await interaction.response.send_message("Transcript généré:", file=file, ephemeral=True)
else:
await interaction.response.send_message("Erreur lors de la génération du transcript.", ephemeral=True)
async def _claim_callback(self, interaction: discord.Interaction):
"""Réclame le ticket"""
if not self._is_staff(interaction):
await interaction.response.send_message("Réservé au staff.", ephemeral=True)
return
if self.ticket.claimed_by == interaction.user.id:
await interaction.response.send_message("Vous avez déjà réclamé ce ticket.", ephemeral=True)
return
old_claimer = self.ticket.claimed_by
self.ticket.claimed_by = interaction.user.id
self.ticket.status = TicketStatus.CLAIMED
self.storage.save_ticket(self.ticket)
# Update view (change label)
for item in self.children:
if item.custom_id == "ticket_claim":
item.label = "Transférer"
break
# Force UI update
await interaction.response.edit_message(view=self)
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)
await interaction.channel.send(embed=embed)
async def _reopen_callback(self, interaction: discord.Interaction):
"""Rouvre le ticket"""
if not self._is_staff(interaction):
await interaction.response.send_message("Réservé au staff.", ephemeral=True)
return
self.ticket.status = TicketStatus.OPEN
self.ticket.closed_at = None
self.ticket.claimed_by = None
self.storage.save_ticket(self.ticket)
# Update channel
overwrites = interaction.channel.overwrites.copy()
for target, perms in overwrites.items():
if isinstance(target, discord.Member) and target.id != interaction.guild.me.id:
overwrites[target] = discord.PermissionOverwrite(
read_messages=True,
send_messages=True
)
await interaction.channel.edit(
name=interaction.channel.name.replace("closed-", ""),
overwrites=overwrites
)
# Update view (remove reopen button, add claim if enabled)
for item in self.children:
if item.custom_id == "ticket_reopen":
self.remove_item(item)
break
if self.config.claim_enabled and self.category and self.category.allow_claims:
claim_btn = discord.ui.Button(
style=discord.ButtonStyle.primary,
label="Réclamer",
custom_id="ticket_claim"
)
claim_btn.callback = self._claim_callback
self.add_item(claim_btn)
embed = TicketEmbedFormatter.ticket_reopened(self.ticket, interaction.user)
await interaction.channel.send(embed=embed)
await interaction.response.send_message("Ticket rouvert!", ephemeral=True)
def _is_staff(self, interaction: discord.Interaction) -> bool:
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
# Check category-specific staff roles first
staff_roles = self.category.staff_roles if self.category and self.category.staff_roles else self.config.staff_roles
for role_id in staff_roles:
try:
role = member.guild.get_role(int(role_id))
if role and role in member.roles:
return True
except (ValueError, TypeError, AttributeError):
continue
return False
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
self.category = config.get_category(ticket.category_id)
@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()
def _is_staff(self, interaction: discord.Interaction) -> bool:
return is_staff(interaction, self.config, self.category)
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)
async def _close_ticket(self, interaction: discord.Interaction):
"""Effectue la fermeture réelle du ticket"""
# Collect messages for transcript
transcript_file = None
try:
messages = []
async for msg in interaction.channel.history(limit=1000, oldest_first=True):
if msg.author.bot:
continue
messages.append({
"id": str(msg.id),
"author_id": msg.author.id,
"author_name": str(msg.author),
"content": msg.content,
"timestamp": msg.created_at.isoformat(),
"is_staff": self._is_staff_member(msg.author)
})
from .utils.transcript import get_transcript_generator
transcript_gen = get_transcript_generator()
transcript_file = transcript_gen.save_transcript(self.ticket, messages, format="html")
except Exception as e:
print(f"Error generating transcript: {e}")
# Close ticket
self.ticket.status = TicketStatus.CLOSED
self.ticket.closed_at = datetime.now()
if self.close_reason:
self.ticket.feedback = self.close_reason
self.storage.save_ticket(self.ticket)
duration = self.ticket.duration_minutes
# Send 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é.\nMerci 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)
# Create a special view with a "Noter le staff" button
view = FeedbackView(self.bot, self.storage, self.ticket)
# 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)
await user.send(embed=dm_embed, view=view)
except Exception as e:
print(f"Failed to send feedback DM: {e}")
# Send transcript to log channel
if self.config.log_channel_id:
try:
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)
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)
except Exception as e:
print(f"Error sending transcript to log channel: {e}")
# Delete the ticket channel
try:
await interaction.channel.delete(reason=f"Ticket fermé par {interaction.user}")
except Exception as e:
print(f"Error deleting channel: {e}")
embed = discord.Embed(
title="❌ Erreur",
description="Le ticket a été fermé mais la suppression du salon a échoué.",
color=discord.Color.red()
)
await interaction.channel.send(embed=embed)
async def _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)
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:
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
# Check category-specific staff roles first
staff_roles = self.category.staff_roles if self.category and self.category.staff_roles else self.config.staff_roles
for role_id in staff_roles:
try:
role = member.guild.get_role(int(role_id))
if role and role in member.roles:
return True
except (ValueError, TypeError, AttributeError):
continue
return False
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):
# Check permissions
can_close = (
interaction.user.id == self.ticket.user_id or
self._is_staff(interaction)
)
if not can_close:
await interaction.response.send_message("Vous ne pouvez pas fermer ce ticket.", ephemeral=True)
return
# Handle survey if enabled
if hasattr(self, 'rating') and self.rating.value:
try:
rating = int(self.rating.value)
if 1 <= rating <= 5:
self.ticket.rating = rating
else:
await interaction.response.send_message("La note doit être entre 1 et 5.", ephemeral=True)
return
except ValueError:
await interaction.response.send_message("Note invalide.", ephemeral=True)
return
if hasattr(self, 'feedback') and self.feedback.value:
self.ticket.feedback = self.feedback.value
# Send ephemeral response to user first (required for modal interactions)
await interaction.response.send_message("✅ Fermeture du ticket en cours...", ephemeral=True)
# Send confirmation message in the ticket channel
confirm_embed = discord.Embed(
title="⏳ Fermeture du Ticket",
description="Le ticket va être fermé dans quelques secondes...\n\n"
"📄 Génération du transcript en cours...\n"
"📝 Envoi aux logs...\n"
"🗑️ Suppression du canal...",
color=discord.Color.orange()
)
await interaction.channel.send(embed=confirm_embed)
# Add small delay for user to see the confirmation
import asyncio
await asyncio.sleep(3)
# Generate transcript before closing
transcript_file = None
try:
# Collect messages
messages = []
async for msg in interaction.channel.history(limit=1000, oldest_first=True):
if msg.author.bot:
continue
messages.append({
"id": str(msg.id),
"author_id": msg.author.id,
"author_name": str(msg.author),
"content": msg.content,
"timestamp": msg.created_at.isoformat(),
"is_staff": self._is_staff_member(msg.author)
})
# Generate transcript
from .utils.transcript import get_transcript_generator
transcript_gen = get_transcript_generator()
transcript_file = transcript_gen.save_transcript(self.ticket, messages, format="html")
except Exception as e:
print(f"Error generating transcript: {e}")
# Close ticket
self.ticket.status = TicketStatus.CLOSED
self.ticket.closed_at = datetime.now()
self.storage.save_ticket(self.ticket)
duration = self.ticket.duration_minutes
# Send transcript to log channel if configured
if self.config.log_channel_id:
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
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)
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:
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
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"
)
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"
)
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"
)
max_btn.callback = self._max_callback
self.add_item(max_btn)
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"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(title="Limite de Tickets par Utilisateur")
self.bot = bot
self.storage = storage
self.config = config
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)
# 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)
# Gérer les catégories
self.manage_cat_btn = discord.ui.Button(
style=discord.ButtonStyle.blurple,
label="📂 Gérer les catégories",
custom_id="config_manage_cat"
)
self.manage_cat_btn.callback = self._manage_cat_callback
self.add_item(self.manage_cat_btn)
# Staff permissions
staff_btn = discord.ui.Button(
style=discord.ButtonStyle.secondary,
label="Permissions Staff",
custom_id="config_staff_perms"
)
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)
# Log channel
log_btn = discord.ui.Button(
style=discord.ButtonStyle.secondary,
label="Canal de Logs",
custom_id="config_log_channel"
)
log_btn.callback = self._log_channel_callback
self.add_item(log_btn)
async def _toggle_callback(self, interaction: discord.Interaction):
self.config.enabled = not self.config.enabled
self.storage.save_config(interaction.guild_id, self.config)
status = "activé" if self.config.enabled else "désactivé"
await interaction.response.send_message(f"Système {status}!", ephemeral=True)
# Update button
for item in self.children:
if item.custom_id == "config_toggle":
item.style = discord.ButtonStyle.green if self.config.enabled else discord.ButtonStyle.red
await interaction.message.edit(view=self)
async def _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)
async def _staff_perms_callback(self, interaction: discord.Interaction):
view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "staff")
embed = discord.Embed(
title="Permissions Staff",
description="Configurez qui peut gérer les tickets (répondre, fermer, etc.)",
color=discord.Color.blue()
)
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)
def _add_current_perms_fields(self, embed, roles, users, guild):
role_mentions = []
for role_id in roles:
role = guild.get_role(int(role_id))
if role:
role_mentions.append(role.mention)
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)
async def _category_callback(self, interaction: discord.Interaction):
modal = SetCategoryModal(self.bot, self.storage, self.config)
await interaction.response.send_modal(modal)
async def _log_channel_callback(self, interaction: discord.Interaction):
modal = SetChannelModal(self.bot, self.storage, self.config, "log")
await interaction.response.send_modal(modal)
class AddCategoryModal(discord.ui.Modal):
"""Modal pour ajouter une catégorie"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(title="Nouvelle Catégorie")
self.bot = bot
self.storage = storage
self.config = config
self.name = discord.ui.TextInput(label="Nom", placeholder="Ex: Support", required=True)
self.add_item(self.name)
self.description = discord.ui.TextInput(
label="Description", placeholder="Description...", required=False, style=discord.TextStyle.paragraph
)
self.add_item(self.description)
self.emoji = discord.ui.TextInput(label="Emoji", placeholder="🎫", required=False, max_length=5)
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)
self.storage.save_config(interaction.guild_id, self.config)
await interaction.response.send_message(f"Catégorie **{self.name.value}** créée!", ephemeral=True)
class 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"
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)'}\n"
f"**Catégorie Discord:** {f'<#{self.category.discord_category_id}>' if self.category.discord_category_id else 'Par défaut'}",
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)
@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)
@discord.ui.button(label="📁 Catégorie Discord", style=discord.ButtonStyle.secondary)
async def discord_category_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
view = CategoryDiscordCategoryView(self.bot, self.storage, self.config, self.category, self)
embed = discord.Embed(
title=f"Catégorie Discord: {self.category.name}",
description="Choisissez la catégorie Discord (dossier) dans laquelle les tickets de ce type seront créés.\n\n"
"Si aucune n'est sélectionnée, la catégorie par défaut du serveur sera utilisée.",
color=discord.Color.blue()
)
await interaction.response.edit_message(embed=embed, view=view)
@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)
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)
class CategoryDiscordCategoryView(discord.ui.View):
"""Vue pour configurer la catégorie Discord d'une catégorie de ticket"""
def __init__(self, bot, storage, config, category, parent_view):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.category = category
self.parent_view = parent_view
@discord.ui.select(cls=discord.ui.ChannelSelect, channel_types=[discord.ChannelType.category], placeholder="Sélectionner la catégorie Discord")
async def select_callback(self, interaction: discord.Interaction, select: discord.ui.ChannelSelect):
self.category.discord_category_id = select.values[0].id
self.storage.save_config(interaction.guild_id, self.config)
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
@discord.ui.button(label="❌ Réinitialiser", style=discord.ButtonStyle.danger)
async def reset_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
self.category.discord_category_id = None
self.storage.save_config(interaction.guild_id, self.config)
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
@discord.ui.button(label="⬅️ Retour", 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)
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)
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
# 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
# Update config
self.config.log_channel_id = channel_id
self.storage.save_config(interaction.guild_id, self.config)
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()
)
await interaction.response.send_message(embed=embed, ephemeral=True)
class StaffRolesModal(discord.ui.Modal):
"""Modal pour ajouter/supprimer les rôles staff"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(title="Gérer les Rôles Staff")
self.bot = bot
self.storage = storage
self.config = config
# Champ pour entrer les rôles (mention ou ID)
self.roles = discord.ui.TextInput(
label="Rôles Staff (mention ou ID)",
placeholder="@Staff @Modérateurs ou 123456789 987654321",
required=True,
max_length=500
)
self.add_item(self.roles)
async def on_submit(self, interaction: discord.Interaction):
try:
role_input = self.roles.value.strip()
role_ids = []
# Parse role mentions (@role) or IDs
import re
# Match <@&ROLE_ID> mentions
mention_matches = re.findall(r'<@&(\d+)>', role_input)
# Match raw numbers (IDs)
id_matches = re.findall(r'\b(\d{17,20})\b', role_input)
all_ids = set(mention_matches + id_matches)
if not all_ids:
await interaction.response.send_message(
"❌ Format invalide. Utilisez les mentions (@Rôle) ou les IDs.",
ephemeral=True
)
return
# Validate roles exist
valid_roles = []
for role_id in all_ids:
role = interaction.guild.get_role(int(role_id))
if role:
valid_roles.append(role)
else:
await interaction.response.send_message(
f"❌ Le rôle {role_id} n'existe pas sur ce serveur.",
ephemeral=True
)
return
# Update config
self.config.staff_roles = list(all_ids)
self.storage.save_config(interaction.guild_id, self.config)
# Build success embed
embed = discord.Embed(
title="✅ Rôles Staff Mis à Jour",
description=f"**{len(valid_roles)}** rôle(s) configuré(s) comme staff.",
color=discord.Color.green()
)
role_mentions = [role.mention for role in valid_roles]
embed.add_field(
name="Rôles Staff",
value=", ".join(role_mentions) if role_mentions else "Aucun",
inline=False
)
embed.add_field(
name="Permissions",
value="• Gérer les tickets\n• Accéder aux analytics\n• Générer des transcripts\n• Fermer les tickets des autres",
inline=False
)
await interaction.response.send_message(embed=embed, ephemeral=True)
except Exception as e:
await interaction.response.send_message(f"Erreur lors de la configuration des rôles: {e}", ephemeral=True)
async def on_error(self, interaction: discord.Interaction, error: Exception):
"""Handle errors in the modal"""
await interaction.response.send_message("Une erreur inattendue s'est produite lors de la configuration des rôles.", ephemeral=True)
class PermissionConfigView(discord.ui.View):
"""View to configure permissions (Staff/Config Roles & Users)"""
def __init__(self, bot, storage, config: GuildTicketConfig, guild: discord.Guild, config_type: str = "staff"):
super().__init__(timeout=None)
self.bot = bot
self.storage = storage
self.config = config
self.guild = guild
self.config_type = config_type # "staff" or "config"
# Add components
self._add_role_select()
self._add_user_select()
self._add_remove_select()
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):
try:
# Determine which list to update
if self.config_type == "staff":
target_list = self.config.staff_roles
else:
target_list = self.config.config_roles
# Get new selected IDs
new_ids = interaction.data.get('values', [])
added_count = 0
for new_id in new_ids:
if new_id not in target_list:
target_list.append(new_id)
added_count += 1
self.storage.save_config(interaction.guild_id, self.config)
await 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
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)
async def _refresh_view(self, interaction):
self.clear_items()
self._add_role_select()
self._add_user_select()
self._add_remove_select()
# 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)
def _add_remove_select(self):
# Build options from current config
options = []
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
if options:
select = discord.ui.Select(
placeholder="Sélectionnez pour RETIRER des permissions...",
options=options,
min_values=1,
max_values=min(len(options), 25),
row=2,
custom_id="remove_select"
)
select.callback = self._remove_callback
self.add_item(select)
async def _remove_callback(self, interaction: discord.Interaction):
try:
values = interaction.data.get('values', [])
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)
class TicketCommands(commands.Cog):
"""Cog principal des tickets"""
def __init__(self, bot):
self.bot = bot
self.storage = get_storage()
async def cog_unload(self):
pass
async def cog_load(self):
await self._register_all_views()
async def _register_all_views(self):
"""Enregistre les vues pour tous les serveurs"""
try:
for guild_id in self.storage.get_all_guild_ids():
try:
config = self.storage.load_config(guild_id)
if config and config.enabled:
view = TicketPanelView(self.bot, config, self.storage)
self.bot.add_view(view)
# Register management views for existing tickets
tickets = self.storage.get_guild_tickets(guild_id)
for ticket in tickets:
if ticket.is_open:
mgmt_view = TicketManagementView(self.bot, self.storage, config, ticket)
self.bot.add_view(mgmt_view)
print(f"[Ticket] Views registered for guild {guild_id}")
except Exception as e:
print(f"[Ticket] Error registering views for guild {guild_id}: {e}")
except Exception as e:
print(f"[Ticket] Error in _register_all_views: {e}")
# === COMMANDES ===
@commands.hybrid_command(name="ticket", aliases=["tickets"])
async def ticket_main(self, ctx):
"""Ouvre le panel des tickets"""
await ctx.defer(ephemeral=True)
config = self.storage.load_config(ctx.guild.id)
embed = discord.Embed(
title="Système de Tickets",
description="Gérez vos tickets simplement",
color=discord.Color.blurple()
)
if not config.enabled:
embed.add_field(name="État", value="Le système est actuellement **désactivé**.", inline=False)
else:
embed.add_field(name="Catégories", value=str(len(config.categories)), inline=True)
embed.add_field(name="Total tickets", value=str(len(self.storage.get_guild_tickets(ctx.guild.id))), inline=True)
view = TicketPanelView(self.bot, config, self.storage)
await ctx.send(embed=embed, view=view, ephemeral=True)
@commands.hybrid_command(name="ticketpanel", aliases=["panneau"])
@commands.has_permissions(administrator=True)
async def ticket_panel(self, ctx, channel: discord.TextChannel = None):
"""Envoie le panel de création de tickets dans un canal"""
await ctx.defer(ephemeral=True)
config = self.storage.load_config(ctx.guild.id)
if not config.enabled:
await ctx.send("Le système est désactivé.", ephemeral=True)
return
target = channel or ctx.channel
embed = discord.Embed(
title="Besoin d'aide?",
description="Cliquez sur le bouton ci-dessous pour créer un ticket.\n\nNotre é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)
# Reload config
config = self.storage.load_config(ctx.guild.id)
# Vérifier permissions (admin ou config roles/users)
if not can_access_config(ctx, config):
await ctx.send("❌ Cette commande est réservée aux administrateurs ou aux rôles configurés.", ephemeral=True)
return
embed = discord.Embed(
title="⚙️ Configuration du Système de Tickets",
description="Configurez complètement votre système de tickets depuis cette interface.\n\n"
"**Options disponibles:**\n"
"• Activer/Désactiver le système\n"
"• Configurer les rôles staff\n"
"• Gérer les catégories\n"
"• Définir le canal de logs\n"
"• Et bien plus...",
color=discord.Color.blue()
)
embed.add_field(
name="État actuel",
value=f"Système: {'✅ Activé' if config.enabled else '❌ Désactivé'}\n"
f"Catégories: {len(config.categories)}\n"
f"Rôles staff: {len(config.staff_roles)}",
inline=True
)
embed.add_field(
name="Configuration requise",
value="• **Rôles Staff**: Définissez qui peut gérer les tickets\n"
"• **Catégories**: Créez des catégories pour organiser les tickets\n"
"• **Canal de Logs**: Où envoyer les notifications",
inline=True
)
embed.set_footer(text="Utilisez les boutons ci-dessous pour configurer le système")
view = ConfigPanelView(self.bot, self.storage, config)
await ctx.send(embed=embed, view=view, ephemeral=True)
async def setup(bot):
"""Setup function"""
await bot.add_cog(TicketCommands(bot))