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