Première version non terminer du nouveau système de ticket § ajout du système de boutons persistant (non tester)

This commit is contained in:
Mathis 2026-01-04 00:01:15 +01:00
parent 10173132a2
commit 93e2f9bb44
15 changed files with 3788 additions and 209 deletions

View file

@ -0,0 +1,692 @@
"""
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)