""" Ticket Creation Views ===================== UI views for creating tickets with categories, priority, and reason. """ from datetime import datetime from typing import Optional, List, Dict, Any import discord from discord.ui import View, Button, Select, Modal, TextInput from discord import Interaction, ButtonStyle, Color from ..data.models import ( TicketData, TicketCategory, TicketStatus, Priority, GuildTicketConfig ) from ..data.storage import get_storage from ..utils.formatters import TicketEmbedFormatter, TicketMessageFormatter from ..utils.transcript import get_transcript_generator class TicketCreationView(View): """ Main view for ticket creation panel. Shows category selection buttons. """ def __init__(self, bot, config: GuildTicketConfig): super().__init__(timeout=None) self.bot = bot self.config = config # Add category buttons for i, category in enumerate(config.categories[:5]): # Max 5 categories btn = Button( style=ButtonStyle.blurple, label=category.name, emoji=category.emoji, custom_id=f"ticket_category_{category.id}" ) btn.callback = self._create_category_callback(category.id) self.add_item(btn) # Add help button help_btn = Button( style=ButtonStyle.secondary, label="Aide", emoji="❓", custom_id="ticket_help" ) help_btn.callback = self._help_callback self.add_item(help_btn) def _create_category_callback(self, category_id: str): """Create callback for category button""" async def callback(interaction: Interaction): await self.on_category_select(interaction, category_id) return callback async def on_category_select(self, interaction: Interaction, category_id: str): """Handle category selection""" await interaction.response.defer(ephemeral=True) category = self.config.get_category(category_id) if not category: await interaction.followup.send("Catégorie non trouvée.", ephemeral=True) return storage = get_storage() # Check if user already has a ticket user_tickets = storage.get_user_tickets( interaction.guild_id, interaction.user.id, open_only=True ) # Check limits user_count = len([t for t in user_tickets if t.category_id == category_id]) if user_count >= category.max_tickets_per_user: await interaction.followup.send( TicketMessageFormatter.ticket_limit_reached(category.max_tickets_per_user), ephemeral=True ) return # Check global limit if len(user_tickets) >= self.config.max_tickets_per_user: await interaction.followup.send( TicketMessageFormatter.ticket_limit_reached(self.config.max_tickets_per_user), ephemeral=True ) return # Show priority selection modal modal = TicketPriorityModal( bot=self.bot, config=self.config, category=category ) await interaction.response.send_modal(modal) async def _help_callback(self, interaction: Interaction): """Handle help button click""" embed = discord.Embed( title="❓ Aide - Création de Ticket", description="Sélectionnez une catégorie ci-dessus correspondant à votre demande:\n\n" "• **Support** - Questions techniques\n" "• **Bug Report** - Signaler un problème\n" "• **Partenariat** - Propositions de partenariat\n" "• **Autre** - Autres demandes", color=Color.blue() ) await interaction.response.send_message(embed=embed, ephemeral=True) class TicketPriorityModal(Modal): """ Modal for selecting priority and reason when creating a ticket. """ def __init__(self, bot, config: GuildTicketConfig, category: TicketCategory): super().__init__(title=f"🎫 {category.name} - Nouveau Ticket") self.bot = bot self.config = config self.category = category # Priority selection priorities = ["Basse", "Normale", "Haute", "Urgente"] priority_placeholder = "Sélectionnez la priorité" if not category.priority_enabled: priorities = ["Normale"] priority_placeholder = "Priorité par défaut" self.priority_select = Select( placeholder=priority_placeholder, options=[ discord.SelectOption(label=p, value=p.lower(), emoji=emoji) for p, emoji in [ ("Basse", "🟢"), ("Normale", "🔵"), ("Haute", "🟠"), ("Urgente", "🔴") ] if p in priorities or p.lower() in ["normale"] ], custom_id="priority_select" ) self.add_item(self.priority_select) # Reason input reason_placeholder = "Décrivez votre demande..." if category.require_reason: reason_placeholder = "Requis - Décrivez votre demande..." self.reason = TextInput( label="Motif de la demande", placeholder=reason_placeholder, required=category.require_reason, style=discord.TextStyle.paragraph, custom_id="reason_input" ) self.add_item(self.reason) async def on_submit(self, interaction: Interaction): """Handle modal submission""" await interaction.response.defer(ephemeral=True) # Map priority priority_map = { "basse": Priority.LOW, "normale": Priority.NORMAL, "haute": Priority.HIGH, "urgente": Priority.URGENT } priority = priority_map.get(self.priority_select.values[0], Priority.NORMAL) await self.create_ticket( interaction=interaction, priority=priority, reason=self.reason.value ) async def create_ticket( self, interaction: Interaction, priority: Priority, reason: str ): """Create the actual ticket""" guild = interaction.guild user = interaction.user # Get category for config category = self.category # Get category channel category_channel = guild.get_channel(int(category.id)) if category.id.isdigit() else None # Build permissions overwrites = { guild.default_role: discord.PermissionOverwrite(read_messages=False), user: discord.PermissionOverwrite(read_messages=True, send_messages=True, attach_files=True), guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True) } # Add staff roles for role_id in category.staff_roles: role = guild.get_role(int(role_id)) if role: overwrites[role] = discord.PermissionOverwrite(read_messages=True, send_messages=True) # Also add global 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 timestamp = datetime.now().strftime("%m%d%H%M") channel_name = f"ticket-{user.id}-{timestamp}" # Create ticket channel try: ticket_channel = await guild.create_text_channel( name=channel_name, category=guild.get_channel(int(self.config.default_category)) if self.config.default_category else None, overwrites=overwrites, topic=f"Ticket de {user} ({user.id}) | Catégorie: {category.name}" ) except Exception as e: await interaction.followup.send(f"❌ Erreur lors de la création du ticket: {e}", ephemeral=True) return # Create ticket data storage = get_storage() ticket_data = TicketData( channel_id=ticket_channel.id, guild_id=guild.id, user_id=user.id, category_id=category.id, priority=priority, status=TicketStatus.OPEN, reason=reason ) storage.save_ticket(ticket_data) # Send welcome message welcome_content = TicketMessageFormatter.welcome_message(user, category) welcome_embed = TicketEmbedFormatter.ticket_created(ticket_data, user, category, guild.name) # Send initial message with management view view = TicketManagementView(self.bot, storage, self.config) await ticket_channel.send( content=welcome_content, embed=welcome_embed, view=view ) # Send confirmation to user await interaction.followup.send( f"✅ Votre ticket a été créé: {ticket_channel.mention}", ephemeral=True ) # Log to log channel if self.config.log_channel_id: log_channel = guild.get_channel(self.config.log_channel_id) if log_channel: log_embed = discord.Embed( title="🎫 Ticket Créé", color=Color.green(), timestamp=datetime.now() ) log_embed.add_field(name="👤 Utilisateur", value=user.mention, inline=True) log_embed.add_field(name="📁 Catégorie", value=category.name, inline=True) log_embed.add_field(name="⚡ Priorité", value=TicketEmbedFormatter.format_priority(priority), inline=True) log_embed.add_field(name="📝 Salon", value=ticket_channel.mention, inline=True) if reason: log_embed.add_field(name="📄 Raison", value=reason[:100], inline=False) await log_channel.send(embed=log_embed) class TicketManagementView(View): """ View shown inside tickets for management actions. """ def __init__(self, bot, storage, config: GuildTicketConfig): super().__init__(timeout=None) self.bot = bot self.storage = storage self.config = config # Close button close_btn = Button( style=ButtonStyle.red, label="Fermer", emoji="🔒", custom_id="ticket_close" ) close_btn.callback = self._close_callback self.add_item(close_btn) # Claim button (if enabled) if config.claim_enabled: claim_btn = Button( style=ButtonStyle.gold, label="Réclamer", emoji="🙋", custom_id="ticket_claim" ) claim_btn.callback = self._claim_callback self.add_item(claim_btn) # Priority button priority_btn = Button( style=ButtonStyle.secondary, label="Priorité", emoji="⚡", custom_id="ticket_priority" ) priority_btn.callback = self._priority_callback self.add_item(priority_btn) # Transcript button transcript_btn = Button( style=ButtonStyle.secondary, label="Transcript", emoji="📄", custom_id="ticket_transcript" ) transcript_btn.callback = self._transcript_callback self.add_item(transcript_btn) async def _close_callback(self, interaction: Interaction): """Handle close button click""" await interaction.response.defer(ephemeral=True) storage = get_storage() ticket = storage.load_ticket(interaction.channel.id) if not ticket: await interaction.followup.send("Ticket non trouvé.", ephemeral=True) return # Check if user can close (owner or staff) can_close = ( interaction.user.id == ticket.user_id or self._is_staff(interaction) ) if not can_close: await interaction.followup.send( "Vous ne pouvez pas fermer ce ticket.", ephemeral=True ) return # Show close modal modal = TicketCloseModal(self.bot, storage, self.config, ticket) await interaction.response.send_modal(modal) async def _claim_callback(self, interaction: Interaction): """Handle claim button click""" await interaction.response.defer(ephemeral=True) if not self._is_staff(interaction): await interaction.followup.send( "Vous n'êtes pas staff.", ephemeral=True ) return storage = get_storage() ticket = storage.load_ticket(interaction.channel.id) if not ticket: await interaction.followup.send("Ticket non trouvé.", ephemeral=True) return if ticket.status == TicketStatus.CLAIMED and ticket.claimed_by == interaction.user.id: # Unclaim ticket.claimed_by = None ticket.status = TicketStatus.OPEN storage.save_ticket(ticket) await interaction.followup.send("Ticket non réclamé.", ephemeral=True) await interaction.channel.send( embed=TicketEmbedFormatter.ticket_unclaimed(ticket, interaction.user) ) elif ticket.status == TicketStatus.CLAIMED: await interaction.followup.send( "Ce ticket est déjà réclamé par quelqu'un d'autre.", ephemeral=True ) else: # Claim ticket.claimed_by = interaction.user.id ticket.status = TicketStatus.CLAIMED storage.save_ticket(ticket) await interaction.followup.send("Ticket réclamé!", ephemeral=True) await interaction.channel.send( embed=TicketEmbedFormatter.ticket_claimed(ticket, interaction.user) ) async def _priority_callback(self, interaction: Interaction): """Handle priority button click""" await interaction.response.defer(ephemeral=True) if not self._is_staff(interaction): await interaction.followup.send( "Vous n'êtes pas staff.", ephemeral=True ) return modal = TicketPriorityChangeModal(self.bot, get_storage(), self.config) await interaction.response.send_modal(modal) async def _transcript_callback(self, interaction: Interaction): """Handle transcript button click""" await interaction.response.defer(ephemeral=True) if not self._is_staff(interaction): await interaction.followup.send( "Vous n'êtes pas staff.", ephemeral=True ) return storage = get_storage() ticket = storage.load_ticket(interaction.channel.id) if not ticket: await interaction.followup.send("Ticket non trouvé.", ephemeral=True) return # Generate transcript transcript_gen = get_transcript_generator() # 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(), "attachments": [a.url for a in msg.attachments], "is_staff": self._is_staff_member(msg.author) }) # Save transcript filepath = transcript_gen.save_transcript(ticket, messages, format="html") if filepath: await interaction.followup.send( f"✅ Transcript sauvegardé: `{filepath}`", ephemeral=True ) else: await interaction.followup.send( "❌ Erreur lors de la génération du transcript.", ephemeral=True ) def _is_staff(self, interaction: Interaction) -> bool: """Check if user is 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 # Also check category-specific roles storage = get_storage() ticket = storage.load_ticket(interaction.channel.id) if ticket: category = self.config.get_category(ticket.category_id) if category: for role_id in category.staff_roles: role = guild.get_role(int(role_id)) if role and role in user.roles: return True return False def _is_staff_member(self, member) -> bool: """Check if member is staff""" 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 TicketCloseModal(Modal): """Modal for closing a ticket with reason""" 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.reason = TextInput( label="Raison de la fermeture", placeholder="Pourquoi fermez-vous ce ticket?", required=False, style=discord.TextStyle.paragraph, custom_id="close_reason" ) self.add_item(self.reason) async def on_submit(self, interaction: Interaction): await interaction.response.defer(ephemeral=True) # Update ticket self.ticket.status = TicketStatus.CLOSED self.ticket.closed_at = datetime.now() self.storage.save_ticket(self.ticket) # Calculate duration duration_minutes = self.ticket.duration_minutes # Send close embed close_embed = TicketEmbedFormatter.ticket_closed( self.ticket, interaction.user, duration_minutes, self.reason.value ) # Update channel permissions 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=False ) await interaction.channel.edit( name=f"closed-{interaction.channel.name}", overwrites=overwrites ) # Send close message await interaction.channel.send( content=TicketMessageFormatter.ticket_closed_notification( interaction.user, self.reason.value ), embed=close_embed ) # Show survey if enabled if self.config.survey_enabled: survey_view = SurveyView(self.bot, self.storage, self.config, self.ticket) survey_embed = TicketEmbedFormatter.survey_embed() await interaction.channel.send(embed=survey_embed, view=survey_view) # Log if self.config.log_channel_id: log_channel = interaction.guild.get_channel(self.config.log_channel_id) if log_channel: log_embed = discord.Embed( title="🔴 Ticket Fermé", color=Color.red(), timestamp=datetime.now() ) log_embed.add_field(name="👤 Fermé par", value=interaction.user.mention, inline=True) log_embed.add_field(name="📝 Salon", value=interaction.channel.mention, inline=True) log_embed.add_field(name="⏱️ Durée", value=TicketEmbedFormatter._format_duration(duration_minutes), inline=True) await log_channel.send(embed=log_embed) await interaction.followup.send("Ticket fermé!", ephemeral=True) class TicketPriorityChangeModal(Modal): """Modal for changing ticket priority""" def __init__(self, bot, storage, config: GuildTicketConfig): super().__init__(title="⚡ Changer la Priorité") self.bot = bot self.storage = storage self.config = config self.priority = Select( placeholder="Sélectionnez la nouvelle priorité", options=[ discord.SelectOption(label="Basse", value="low", emoji="🟢"), discord.SelectOption(label="Normale", value="normal", emoji="🔵"), discord.SelectOption(label="Haute", value="high", emoji="🟠"), discord.SelectOption(label="Urgente", value="urgent", emoji="🔴"), ], custom_id="priority_change" ) self.add_item(self.priority) async def on_submit(self, interaction: Interaction): await interaction.response.defer(ephemeral=True) ticket = self.storage.load_ticket(interaction.channel.id) if not ticket: await interaction.followup.send("Ticket non trouvé.", ephemeral=True) return priority_map = { "low": Priority.LOW, "normal": Priority.NORMAL, "high": Priority.HIGH, "urgent": Priority.URGENT } old_priority = ticket.priority ticket.priority = priority_map[self.priority.values[0]] self.storage.save_ticket(ticket) embed = discord.Embed( title="⚡ Priorité Changée", description=f"La priorité a été changée de **{old_priority.value}** à **{ticket.priority.value}**", color=TicketEmbedFormatter.get_priority_color(ticket.priority) ) await interaction.channel.send(embed=embed) await interaction.followup.send("Priorité mise à jour!", ephemeral=True) class SurveyView(View): """View for satisfaction survey""" def __init__(self, bot, storage, config: GuildTicketConfig, ticket: TicketData): super().__init__(timeout=None) # Persistent view self.bot = bot self.storage = storage self.config = config self.ticket = ticket for i in range(1, 6): btn = Button( style=ButtonStyle.secondary, label=f"{i} ⭐", emoji="⭐" if i == 5 else None, custom_id=f"survey_{i}" ) btn.callback = self._create_rating_callback(i) self.add_item(btn) def _create_rating_callback(self, rating: int): async def callback(interaction: Interaction): await self.on_rating(interaction, rating) return callback async def on_rating(self, interaction: Interaction, rating: int): """Handle rating selection""" # Only allow ticket creator to rate if interaction.user.id != self.ticket.user_id: await interaction.response.send_message( "Seul le créateur du ticket peut voter.", ephemeral=True ) return # Update ticket self.ticket.rating = rating self.storage.save_ticket(self.ticket) # Show thanks embed = TicketEmbedFormatter.survey_thanks_embed(rating) await interaction.response.send_message(embed=embed, ephemeral=True) # Disable buttons for item in self.children: item.disabled = True await interaction.message.edit(view=self)