2862 lines
115 KiB
Python
2862 lines
115 KiB
Python
"""
|
||
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 disnake
|
||
from disnake.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(disnake.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 = disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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: disnake.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 = disnake.Embed(
|
||
title="Créer un Ticket",
|
||
description="Sélectionnez une catégorie ci-dessous:",
|
||
color=disnake.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: disnake.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 = disnake.Embed(
|
||
title="Vos Tickets Ouverts",
|
||
description=f"Vous avez **{len(tickets)}** ticket(s) ouvert(s):",
|
||
color=disnake.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: disnake.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 = disnake.Embed(
|
||
title="📊 Analytics des Tickets",
|
||
description="Statistiques détaillées des tickets et performances du staff",
|
||
color=disnake.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=disnake.File(rating_chart, 'staff_ratings.png'),
|
||
ephemeral=True
|
||
)
|
||
|
||
# Add individual staff stats view
|
||
stats_view = StaffStatsView(self.bot, staff_list, analytics)
|
||
stats_embed = disnake.Embed(
|
||
title="📈 Statistiques Individuelles",
|
||
description="Cliquez sur un membre du staff pour voir ses statistiques détaillées:",
|
||
color=disnake.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: disnake.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 = disnake.Embed(
|
||
title="Configuration des Tickets",
|
||
description="Gérez la configuration du système de tickets",
|
||
color=disnake.Color.blue()
|
||
)
|
||
|
||
await interaction.response.send_message(embed=embed, view=config_view, ephemeral=True)
|
||
|
||
async def _staff_ratings_callback(self, interaction: disnake.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 = disnake.Embed(
|
||
title="⭐ Notation du Staff",
|
||
description="Performance globale du staff",
|
||
color=disnake.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=disnake.File(rating_chart, 'staff_ratings.png'),
|
||
ephemeral=True
|
||
)
|
||
|
||
# Add individual staff stats view
|
||
stats_view = StaffStatsView(self.bot, staff_list, analytics)
|
||
stats_embed = disnake.Embed(
|
||
title="📈 Statistiques Individuelles",
|
||
description="Cliquez sur un membre du staff pour voir ses statistiques détaillées:",
|
||
color=disnake.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: disnake.Interaction) -> bool:
|
||
return is_staff(interaction, self.config)
|
||
|
||
def _can_access_config(self, interaction: disnake.Interaction) -> bool:
|
||
return can_access_config(interaction, self.config)
|
||
|
||
def _is_whitelisted(self, interaction: disnake.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(disnake.ui.Button):
|
||
def __init__(self, category, view_instance):
|
||
super().__init__(
|
||
style=disnake.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: disnake.Interaction):
|
||
await self.view_instance._on_category_select(interaction, self.category.id)
|
||
|
||
|
||
class CategorySelectionView(disnake.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: disnake.Interaction) -> bool:
|
||
return is_staff(interaction, self.config)
|
||
|
||
def _can_access_config(self, interaction: disnake.Interaction) -> bool:
|
||
return can_access_config(interaction, self.config)
|
||
|
||
def _is_whitelisted(self, interaction: disnake.Interaction) -> bool:
|
||
return is_whitelisted(interaction, self.config, self.bot)
|
||
|
||
async def _on_category_select(self, interaction: disnake.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(disnake.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 = disnake.ui.TextInput(
|
||
label="Priorité",
|
||
placeholder="Normale, Haute ou Urgente",
|
||
default="Normale",
|
||
required=True,
|
||
max_length=10
|
||
)
|
||
self.add_item(self.priority)
|
||
|
||
# Raison
|
||
self.reason = disnake.ui.TextInput(
|
||
label="Motif de votre demande",
|
||
placeholder="Décrivez votre demande...",
|
||
style=disnake.TextStyle.paragraph,
|
||
required=True,
|
||
max_length=500
|
||
)
|
||
self.add_item(self.reason)
|
||
|
||
async def on_submit(self, interaction: disnake.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: disnake.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: disnake.Interaction, priority: Priority, reason: str):
|
||
"""Crée le ticket"""
|
||
guild = interaction.guild
|
||
user = interaction.user
|
||
|
||
storage = get_storage()
|
||
|
||
# Build permissions
|
||
overwrites = {
|
||
guild.default_role: disnake.PermissionOverwrite(read_messages=False),
|
||
user: disnake.PermissionOverwrite(read_messages=True, send_messages=True),
|
||
guild.me: disnake.PermissionOverwrite(read_messages=True, send_messages=True)
|
||
}
|
||
|
||
# Add staff roles (category-specific roles override global ones)
|
||
staff_roles_to_add = self.category.staff_roles if self.category.staff_roles else self.config.staff_roles
|
||
for role_id in staff_roles_to_add:
|
||
try:
|
||
role = guild.get_role(int(role_id))
|
||
if role and role not in overwrites:
|
||
overwrites[role] = disnake.PermissionOverwrite(read_messages=True, send_messages=True)
|
||
except (ValueError, TypeError):
|
||
continue
|
||
|
||
# Create channel name 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 = disnake.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=disnake.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 = disnake.Embed(
|
||
title="Nouveau Ticket",
|
||
color=disnake.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(disnake.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 = disnake.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: disnake.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, disnake.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(disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.ButtonStyle.red,
|
||
label="Fermer",
|
||
custom_id="ticket_close"
|
||
)
|
||
close_btn.callback = self._close_callback
|
||
self.add_item(close_btn)
|
||
|
||
# Transcripts
|
||
transcript_btn = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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: disnake.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 = disnake.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=disnake.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: disnake.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 = disnake.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: disnake.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 = disnake.Embed(
|
||
title="🔄 Ticket Transféré",
|
||
description=f"Le ticket a été transféré de <@{old_claimer}> à {interaction.user.mention}",
|
||
color=disnake.Color.blue()
|
||
)
|
||
else:
|
||
embed = TicketEmbedFormatter.ticket_claimed(self.ticket, interaction.user)
|
||
|
||
await interaction.channel.send(embed=embed)
|
||
|
||
async def _reopen_callback(self, interaction: disnake.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, disnake.Member) and target.id != interaction.guild.me.id:
|
||
overwrites[target] = disnake.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 = disnake.ui.Button(
|
||
style=disnake.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: disnake.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, disnake.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(disnake.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)
|
||
|
||
@disnake.ui.button(label="✅ Confirmer", style=disnake.ButtonStyle.green, custom_id="close_confirm")
|
||
async def confirm_callback(self, interaction: disnake.Interaction, button: disnake.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 = disnake.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=disnake.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)
|
||
|
||
@disnake.ui.button(label="❌ Annuler", style=disnake.ButtonStyle.red, custom_id="close_cancel")
|
||
async def cancel_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button):
|
||
"""L'utilisateur annule la fermeture"""
|
||
await interaction.response.defer()
|
||
await interaction.delete_original_response()
|
||
|
||
def _is_staff(self, interaction: disnake.Interaction) -> bool:
|
||
return is_staff(interaction, self.config, self.category)
|
||
|
||
|
||
class CloseDelayView(disnake.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 = disnake.ui.Button(
|
||
label="❌ Annuler",
|
||
style=disnake.ButtonStyle.red,
|
||
custom_id="delay_cancel"
|
||
)
|
||
cancel_btn.callback = self._cancel_callback
|
||
self.add_item(cancel_btn)
|
||
|
||
async def start_countdown(self, interaction: disnake.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 = disnake.Embed(
|
||
title="🔒 Fermeture du Ticket",
|
||
description=f"**{step_text}**\n\n"
|
||
f"Merci pour votre confiance!",
|
||
color=disnake.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: disnake.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 = disnake.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=disnake.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 = disnake.ui.Button(
|
||
label="⭐ Noter le staff",
|
||
style=disnake.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 = disnake.Embed(
|
||
title="📄 Ticket Fermé - Transcript",
|
||
description=f"Ticket #{self.ticket.channel_id} fermé par {interaction.user.mention}",
|
||
color=disnake.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 = disnake.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 = disnake.Embed(
|
||
title="❌ Erreur",
|
||
description="Le ticket a été fermé mais la suppression du salon a échoué.",
|
||
color=disnake.Color.red()
|
||
)
|
||
await interaction.channel.send(embed=embed)
|
||
|
||
async def _note_staff_callback(self, interaction: disnake.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: disnake.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 = disnake.Embed(
|
||
title="❌ Fermeture Annulée",
|
||
description="La fermeture du ticket a été annulée.",
|
||
color=disnake.Color.red()
|
||
)
|
||
await interaction.channel.send(embed=embed, delete_after=5)
|
||
|
||
def _is_staff(self, interaction: disnake.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, disnake.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(disnake.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 = disnake.ui.TextInput(
|
||
label="Raison de fermeture",
|
||
placeholder="Optionnel...",
|
||
style=disnake.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 = disnake.ui.TextInput(
|
||
label="Note (1-5 étoiles)",
|
||
placeholder="1 à 5",
|
||
required=False,
|
||
max_length=1
|
||
)
|
||
self.add_item(self.rating)
|
||
|
||
self.feedback = disnake.ui.TextInput(
|
||
label="Commentaires",
|
||
placeholder="Vos commentaires sur le support...",
|
||
style=disnake.TextStyle.paragraph,
|
||
required=False,
|
||
max_length=500
|
||
)
|
||
self.add_item(self.feedback)
|
||
|
||
async def on_submit(self, interaction: disnake.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 = disnake.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=disnake.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 = disnake.Embed(
|
||
title="📄 Ticket Fermé - Transcript",
|
||
description=f"Ticket #{self.ticket.channel_id} fermé par {interaction.user.mention}",
|
||
color=disnake.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 = disnake.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: disnake.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, disnake.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(disnake.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 = disnake.ui.Button(
|
||
style=disnake.ButtonStyle.green if config.enabled else disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.ButtonStyle.secondary,
|
||
label="📂 Catégories",
|
||
custom_id="setup_categories"
|
||
)
|
||
cat_btn.callback = self._categories_callback
|
||
self.add_item(cat_btn)
|
||
|
||
# Log channel
|
||
log_btn = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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: disnake.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 = disnake.Embed(
|
||
title="🔄 État du Système",
|
||
description=f"Le système de tickets a été **{status}**!",
|
||
color=disnake.Color.green() if self.config.enabled else disnake.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 = disnake.ButtonStyle.green if self.config.enabled else disnake.ButtonStyle.red
|
||
await interaction.message.edit(view=self)
|
||
|
||
async def _roles_callback(self, interaction: disnake.Interaction):
|
||
view = StaffRolesSetupView(self.bot, self.storage, self.config, interaction.guild)
|
||
|
||
embed = disnake.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=disnake.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: disnake.Interaction):
|
||
view = CategoriesSetupView(self.bot, self.storage, self.config)
|
||
|
||
embed = disnake.Embed(
|
||
title="📂 Gestion des Catégories",
|
||
description="Créez et gérez les catégories de tickets pour organiser vos demandes.",
|
||
color=disnake.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: disnake.Interaction):
|
||
view = ConfigRolesSetupView(self.bot, self.storage, self.config, interaction.guild)
|
||
|
||
embed = disnake.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=disnake.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: disnake.Interaction):
|
||
modal = SetChannelModal(self.bot, self.storage, self.config, "log")
|
||
await interaction.response.send_modal(modal)
|
||
|
||
async def _advanced_callback(self, interaction: disnake.Interaction):
|
||
view = AdvancedSetupView(self.bot, self.storage, self.config)
|
||
|
||
embed = disnake.Embed(
|
||
title="⚙️ Paramètres Avancés",
|
||
description="Configuration fine du système de tickets.",
|
||
color=disnake.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(disnake.ui.View):
|
||
"""Configuration des rôles staff"""
|
||
|
||
def __init__(self, bot, storage, config: GuildTicketConfig, guild: disnake.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(
|
||
disnake.SelectOption(
|
||
label=role.name,
|
||
value=str(role.id),
|
||
default=str(role.id) in self.config.staff_roles
|
||
)
|
||
)
|
||
|
||
if options:
|
||
select = disnake.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: disnake.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 = disnake.Embed(
|
||
title="✅ Rôles Staff Mis à Jour",
|
||
description=f"**{len(selected)}** rôle(s) configuré(s) comme staff.",
|
||
color=disnake.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(disnake.ui.View):
|
||
"""Configuration des rôles config"""
|
||
|
||
def __init__(self, bot, storage, config: GuildTicketConfig, guild: disnake.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(
|
||
disnake.SelectOption(
|
||
label=role.name,
|
||
value=str(role.id),
|
||
default=str(role.id) in self.config.config_roles
|
||
)
|
||
)
|
||
|
||
if options:
|
||
select = disnake.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: disnake.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 = disnake.Embed(
|
||
title="✅ Rôles Config Mis à Jour",
|
||
description=f"**{len(selected)}** rôle(s) configuré(s) pour accéder à la configuration.",
|
||
color=disnake.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(disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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: disnake.Interaction):
|
||
modal = AddCategoryModal(self.bot, self.storage, self.config)
|
||
await interaction.response.send_modal(modal)
|
||
|
||
async def _remove_callback(self, interaction: disnake.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(
|
||
disnake.SelectOption(
|
||
label=cat.name,
|
||
value=cat.id,
|
||
emoji=cat.emoji
|
||
)
|
||
)
|
||
|
||
select = disnake.ui.Select(
|
||
placeholder="Sélectionnez une catégorie à supprimer...",
|
||
options=options
|
||
)
|
||
select.callback = self._remove_select_callback
|
||
|
||
view = disnake.ui.View()
|
||
view.add_item(select)
|
||
|
||
embed = disnake.Embed(
|
||
title="Supprimer une Catégorie",
|
||
description="Sélectionnez la catégorie à supprimer:",
|
||
color=disnake.Color.red()
|
||
)
|
||
|
||
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
|
||
|
||
async def _remove_select_callback(self, interaction: disnake.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(disnake.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 = disnake.ui.Button(
|
||
style=disnake.ButtonStyle.green if config.claim_enabled else disnake.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 = disnake.ui.Button(
|
||
style=disnake.ButtonStyle.green if config.survey_enabled else disnake.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 = disnake.ui.Button(
|
||
style=disnake.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: disnake.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 = disnake.Embed(
|
||
title="🔄 Système de Réclamation",
|
||
description=f"Le système de réclamation a été **{status}**!",
|
||
color=disnake.Color.green() if self.config.claim_enabled else disnake.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 = disnake.ButtonStyle.green if self.config.claim_enabled else disnake.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: disnake.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 = disnake.Embed(
|
||
title="🔄 Système de Sondages",
|
||
description=f"Le système de sondages a été **{status}**!",
|
||
color=disnake.Color.green() if self.config.survey_enabled else disnake.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 = disnake.ButtonStyle.green if self.config.survey_enabled else disnake.ButtonStyle.red
|
||
item.label = f"{'✅' if self.config.survey_enabled else '❌'} Sondages"
|
||
await interaction.message.edit(view=self)
|
||
|
||
async def _max_callback(self, interaction: disnake.Interaction):
|
||
modal = MaxTicketsModal(self.bot, self.storage, self.config)
|
||
await interaction.response.send_modal(modal)
|
||
|
||
|
||
class MaxTicketsModal(disnake.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 = disnake.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: disnake.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 = disnake.Embed(
|
||
title="✅ Limite Mise à Jour",
|
||
description=f"La limite de tickets par utilisateur est maintenant de **{max_tickets}**.",
|
||
color=disnake.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(disnake.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 = disnake.ui.Button(
|
||
style=disnake.ButtonStyle.green if config.enabled else disnake.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 = disnake.ui.Button(
|
||
label="📁 Catégorie par défaut",
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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: disnake.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 = disnake.ButtonStyle.green if self.config.enabled else disnake.ButtonStyle.red
|
||
await interaction.message.edit(view=self)
|
||
|
||
async def _manage_cat_callback(self, interaction: disnake.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: disnake.Interaction):
|
||
view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "staff")
|
||
|
||
embed = disnake.Embed(
|
||
title="Permissions Staff",
|
||
description="Configurez qui peut gérer les tickets (répondre, fermer, etc.)",
|
||
color=disnake.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: disnake.Interaction):
|
||
view = PermissionConfigView(self.bot, self.storage, self.config, interaction.guild, "config")
|
||
|
||
embed = disnake.Embed(
|
||
title="Permissions Configuration",
|
||
description="Configurez qui peut modifier les paramètres du bot (/ticketconfig)",
|
||
color=disnake.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: disnake.Interaction):
|
||
modal = SetCategoryModal(self.bot, self.storage, self.config)
|
||
await interaction.response.send_modal(modal)
|
||
|
||
async def _log_channel_callback(self, interaction: disnake.Interaction):
|
||
modal = SetChannelModal(self.bot, self.storage, self.config, "log")
|
||
await interaction.response.send_modal(modal)
|
||
|
||
|
||
class AddCategoryModal(disnake.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 = disnake.ui.TextInput(label="Nom", placeholder="Ex: Support", required=True)
|
||
self.add_item(self.name)
|
||
|
||
self.description = disnake.ui.TextInput(
|
||
label="Description", placeholder="Description...", required=False, style=disnake.TextStyle.paragraph
|
||
)
|
||
self.add_item(self.description)
|
||
|
||
self.emoji = disnake.ui.TextInput(label="Emoji", placeholder="🎫", required=False, max_length=5)
|
||
self.add_item(self.emoji)
|
||
|
||
async def on_submit(self, interaction: disnake.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(disnake.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 = disnake.ui.TextInput(
|
||
label="Nom",
|
||
default=category.name,
|
||
placeholder="Ex: Support",
|
||
required=True
|
||
)
|
||
self.add_item(self.name)
|
||
|
||
self.description = disnake.ui.TextInput(
|
||
label="Description",
|
||
default=category.description,
|
||
placeholder="Description...",
|
||
required=False,
|
||
style=disnake.TextStyle.paragraph
|
||
)
|
||
self.add_item(self.description)
|
||
|
||
self.emoji = disnake.ui.TextInput(
|
||
label="Emoji",
|
||
default=category.emoji,
|
||
placeholder="🎫",
|
||
required=False,
|
||
max_length=5
|
||
)
|
||
self.add_item(self.emoji)
|
||
|
||
async def on_submit(self, interaction: disnake.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(disnake.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(disnake.SelectOption(
|
||
label=cat.name,
|
||
value=cat.id,
|
||
description=cat.description[:100] if cat.description else "Aucune description",
|
||
emoji=cat.emoji
|
||
))
|
||
|
||
select = disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.ui.Button(
|
||
style=disnake.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 = disnake.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=disnake.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: disnake.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: disnake.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: disnake.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: disnake.Interaction):
|
||
view = ConfigPanelView(self.bot, self.storage, self.config)
|
||
embed = disnake.Embed(
|
||
title="Configuration des Tickets",
|
||
description="Gérez la configuration du système de tickets",
|
||
color=disnake.Color.blue()
|
||
)
|
||
await interaction.response.edit_message(embed=embed, view=view)
|
||
|
||
|
||
class CategoryActionView(disnake.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 = disnake.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=disnake.Color.blue()
|
||
)
|
||
return embed
|
||
|
||
@disnake.ui.button(label="📝 Modifier", style=disnake.ButtonStyle.primary)
|
||
async def edit_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button):
|
||
modal = EditCategoryModal(self.bot, self.storage, self.config, self.category, self)
|
||
await interaction.response.send_modal(modal)
|
||
|
||
@disnake.ui.button(label="🛡️ Permissions", style=disnake.ButtonStyle.success)
|
||
async def permissions_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button):
|
||
view = CategoryPermissionsView(self.bot, self.storage, self.config, self.category, self)
|
||
embed = disnake.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=disnake.Color.green()
|
||
)
|
||
await interaction.response.edit_message(embed=embed, view=view)
|
||
|
||
@disnake.ui.button(label="📁 Catégorie Discord", style=disnake.ButtonStyle.secondary)
|
||
async def discord_category_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button):
|
||
view = CategoryDiscordCategoryView(self.bot, self.storage, self.config, self.category, self)
|
||
embed = disnake.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=disnake.Color.blue()
|
||
)
|
||
await interaction.response.edit_message(embed=embed, view=view)
|
||
|
||
@disnake.ui.button(label="🗑️ Supprimer", style=disnake.ButtonStyle.danger)
|
||
async def delete_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button):
|
||
view = DeleteCategoryConfirmationView(self.bot, self.storage, self.config, self.category, self.parent_view)
|
||
embed = disnake.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=disnake.Color.red()
|
||
)
|
||
await interaction.response.edit_message(embed=embed, view=view)
|
||
|
||
@disnake.ui.button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary)
|
||
async def back_callback(self, interaction: disnake.Interaction, button: disnake.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(disnake.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
|
||
|
||
@disnake.ui.select(cls=disnake.ui.RoleSelect, placeholder="Sélectionner les rôles staff (Gestion)", min_values=0, max_values=10)
|
||
async def role_select_callback(self, interaction: disnake.Interaction, select: disnake.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)
|
||
|
||
@disnake.ui.button(label="⬅️ Annuler", style=disnake.ButtonStyle.secondary)
|
||
async def back_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button):
|
||
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
|
||
|
||
class CategoryDiscordCategoryView(disnake.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
|
||
|
||
@disnake.ui.select(cls=disnake.ui.ChannelSelect, channel_types=[disnake.ChannelType.category], placeholder="Sélectionner la catégorie Discord")
|
||
async def select_callback(self, interaction: disnake.Interaction, select: disnake.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)
|
||
|
||
@disnake.ui.button(label="❌ Réinitialiser", style=disnake.ButtonStyle.danger)
|
||
async def reset_callback(self, interaction: disnake.Interaction, button: disnake.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)
|
||
|
||
@disnake.ui.button(label="⬅️ Retour", style=disnake.ButtonStyle.secondary)
|
||
async def back_callback(self, interaction: disnake.Interaction, button: disnake.ui.Button):
|
||
await interaction.response.edit_message(embed=self.parent_view._get_embed(), view=self.parent_view)
|
||
|
||
|
||
class DeleteCategoryConfirmationView(disnake.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
|
||
|
||
@disnake.ui.button(label="✅ Confirmer", style=disnake.ButtonStyle.danger)
|
||
async def confirm_callback(self, interaction: disnake.Interaction, button: disnake.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)
|
||
|
||
@disnake.ui.button(label="❌ Annuler", style=disnake.ButtonStyle.secondary)
|
||
async def cancel_callback(self, interaction: disnake.Interaction, button: disnake.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(disnake.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 = disnake.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: disnake.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 = disnake.Embed(
|
||
title="✅ Canal de Logs Mis à Jour",
|
||
description=f"Le canal de logs a été défini sur {channel.mention}.",
|
||
color=disnake.Color.green()
|
||
)
|
||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||
|
||
|
||
class StaffRolesModal(disnake.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 = disnake.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: disnake.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 = disnake.Embed(
|
||
title="✅ Rôles Staff Mis à Jour",
|
||
description=f"**{len(valid_roles)}** rôle(s) configuré(s) comme staff.",
|
||
color=disnake.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: disnake.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(disnake.ui.View):
|
||
"""View to configure permissions (Staff/Config Roles & Users)"""
|
||
|
||
def __init__(self, bot, storage, config: GuildTicketConfig, guild: disnake.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 = disnake.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 = disnake.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: disnake.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: disnake.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 = disnake.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 = disnake.Color.orange()
|
||
target_roles = self.config.config_roles
|
||
target_users = self.config.config_users
|
||
|
||
embed = disnake.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(disnake.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(disnake.SelectOption(
|
||
label=f"User: {name[:90]}",
|
||
value=f"u_{u_id}",
|
||
emoji="👤",
|
||
description="Cliquez pour retirer"
|
||
))
|
||
count += 1
|
||
|
||
if options:
|
||
select = disnake.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: disnake.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.slash_command(name="ticket", description="Ouvre le panel des tickets")
|
||
async def ticket_main(self, inter: disnake.ApplicationCommandInteraction):
|
||
"""Ouvre le panel des tickets"""
|
||
await inter.response.defer(ephemeral=True)
|
||
|
||
config = self.storage.load_config(inter.guild.id)
|
||
|
||
embed = disnake.Embed(
|
||
title="Système de Tickets",
|
||
description="Gérez vos tickets simplement",
|
||
color=disnake.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(inter.guild.id))), inline=True)
|
||
|
||
view = TicketPanelView(self.bot, config, self.storage)
|
||
await inter.edit_original_response(embed=embed, view=view)
|
||
|
||
@commands.slash_command(name="ticketpanel", description="Envoie le panel de création de tickets dans un canal")
|
||
@commands.has_permissions(administrator=True)
|
||
async def ticket_panel(self, inter: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel = None):
|
||
"""Envoie le panel de création de tickets dans un canal"""
|
||
await inter.response.defer(ephemeral=True)
|
||
|
||
config = self.storage.load_config(inter.guild.id)
|
||
|
||
if not config.enabled:
|
||
await inter.edit_original_response(content="Le système est désactivé.")
|
||
return
|
||
|
||
target = channel or inter.channel
|
||
|
||
embed = disnake.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=disnake.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 inter.edit_original_response(content=f"Panel envoyé dans {target.mention}")
|
||
|
||
@commands.slash_command(name="ticketconfig", description="Ouvre le panel de configuration complet du système de tickets")
|
||
async def ticket_config(self, inter: disnake.ApplicationCommandInteraction):
|
||
"""Ouvre le panel de configuration complet du système de tickets"""
|
||
await inter.response.defer(ephemeral=True)
|
||
|
||
# Reload config
|
||
config = self.storage.load_config(inter.guild.id)
|
||
|
||
# Vérifier permissions (admin ou config roles/users)
|
||
if not can_access_config(inter, config):
|
||
await inter.edit_original_response(content="❌ Cette commande est réservée aux administrateurs ou aux rôles configurés.")
|
||
return
|
||
|
||
|
||
|
||
embed = disnake.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=disnake.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 inter.edit_original_response(embed=embed, view=view)
|
||
|
||
|
||
async def setup(bot):
|
||
"""Setup function"""
|
||
await bot.add_cog(TicketCommands(bot))
|
||
|