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,35 @@
"""
Ticket Views Module
===================
UI views for the ticket system.
"""
from .creation import (
TicketCreationView,
TicketPriorityModal,
TicketManagementView,
TicketCloseModal,
TicketPriorityChangeModal,
SurveyView,
)
from .admin import (
AdminPanelView,
BulkCloseView,
ConfigPanelView,
AddCategoryModal,
RolesManagementView,
)
__all__ = [
"TicketCreationView",
"TicketPriorityModal",
"TicketManagementView",
"TicketCloseModal",
"TicketPriorityChangeModal",
"SurveyView",
"AdminPanelView",
"BulkCloseView",
"ConfigPanelView",
"AddCategoryModal",
"RolesManagementView",
]

View file

@ -0,0 +1,522 @@
"""
Ticket Admin Views
==================
Admin panel views for bulk management and configuration.
"""
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,
TicketStats,
GuildTicketConfig
)
from ..data.storage import get_storage
from ..utils.formatters import TicketEmbedFormatter
from ..utils.transcript import get_transcript_generator
class AdminPanelView(View):
"""
Main admin panel for ticket management.
"""
def __init__(self, bot, config: GuildTicketConfig):
super().__init__(timeout=None)
self.bot = bot
self.config = config
# Stats button
stats_btn = Button(
style=ButtonStyle.blurple,
label="📊 Statistiques",
custom_id="admin_stats"
)
stats_btn.callback = self._stats_callback
self.add_item(stats_btn)
# Bulk close button
bulk_close_btn = Button(
style=ButtonStyle.red,
label="🔴 Fermeture Massive",
custom_id="admin_bulk_close"
)
bulk_close_btn.callback = self._bulk_close_callback
self.add_item(bulk_close_btn)
# Config button
config_btn = Button(
style=ButtonStyle.secondary,
label="⚙️ Configuration",
custom_id="admin_config"
)
config_btn.callback = self._config_callback
self.add_item(config_btn)
# Clean old tickets button
cleanup_btn = Button(
style=ButtonStyle.secondary,
label="🧹 Nettoyer",
custom_id="admin_cleanup"
)
cleanup_btn.callback = self._cleanup_callback
self.add_item(cleanup_btn)
async def _stats_callback(self, interaction: Interaction):
"""Show statistics"""
await interaction.response.defer(ephemeral=True)
storage = get_storage()
stats = self._calculate_stats(interaction.guild_id)
embed = TicketEmbedFormatter.stats_embed(stats, interaction.guild.name)
await interaction.followup.send(embed=embed, ephemeral=True)
async def _bulk_close_callback(self, interaction: Interaction):
"""Show bulk close options"""
await interaction.response.defer(ephemeral=True)
# Get open tickets
storage = get_storage()
tickets = storage.get_guild_tickets(interaction.guild_id, TicketStatus.OPEN)
if not tickets:
await interaction.followup.send("Aucun ticket ouvert à fermer.", ephemeral=True)
return
# Show selection view
view = BulkCloseView(self.bot, self.config, tickets)
embed = discord.Embed(
title="🔴 Fermeture Massive",
description=f"Nombre de tickets ouverts: **{len(tickets)}**\n\n"
"Sélectionnez les tickets à fermer:",
color=Color.red()
)
await interaction.followup.send(embed=embed, view=view, ephemeral=True)
async def _config_callback(self, interaction: Interaction):
"""Show configuration options"""
await interaction.response.defer(ephemeral=True)
view = ConfigPanelView(self.bot, self.config)
embed = discord.Embed(
title="⚙️ Configuration des Tickets",
color=Color.blue()
)
embed.add_field(name="Tickets activés", value="" if self.config.enabled else "", inline=True)
embed.add_field(name="Catégories", value=str(len(self.config.categories)), inline=True)
embed.add_field(name="Rôles Staff", value=str(len(self.config.staff_roles)), inline=True)
embed.add_field(name="Canal Logs", value=f"<#{self.config.log_channel_id}>" if self.config.log_channel_id else "Non configuré", inline=True)
embed.add_field(name="Limite par utilisateur", value=str(self.config.max_tickets_per_user), inline=True)
embed.add_field(name="Auto-close", value="" if self.config.auto_close_enabled else "", inline=True)
await interaction.followup.send(embed=embed, view=view, ephemeral=True)
async def _cleanup_callback(self, interaction: Interaction):
"""Clean up old closed tickets"""
await interaction.response.defer(ephemeral=True)
storage = get_storage()
removed = await self._cleanup_closed_tickets(interaction.guild_id)
await interaction.followup.send(
f"{removed} tickets fermés ont été supprimés.",
ephemeral=True
)
def _calculate_stats(self, guild_id: int) -> TicketStats:
"""Calculate ticket statistics"""
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:
# By status
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
# By category
stats.tickets_by_category[ticket.category_id] = \
stats.tickets_by_category.get(ticket.category_id, 0) + 1
# By priority
stats.tickets_by_priority[ticket.priority.value] = \
stats.tickets_by_priority.get(ticket.priority.value, 0) + 1
# By day
day = ticket.created_at.strftime("%Y-%m-%d")
stats.tickets_by_day[day] = stats.tickets_by_day.get(day, 0) + 1
# Duration
if ticket.duration_minutes:
total_duration += ticket.duration_minutes
# Rating
if ticket.rating:
rating_sum += ticket.rating
rating_count += 1
# Averages
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
async def _cleanup_closed_tickets(self, guild_id: int, days: int = 30) -> int:
"""Remove closed tickets older than X days"""
storage = get_storage()
tickets = storage.get_guild_tickets(guild_id, TicketStatus.CLOSED)
cutoff = datetime.now().timestamp() - (days * 86400)
removed = 0
for ticket in tickets:
if ticket.closed_at and ticket.closed_at.timestamp() < cutoff:
# Delete channel if it exists
channel = self.bot.get_channel(ticket.channel_id)
if channel:
try:
await channel.delete()
except Exception:
pass
# Delete ticket data
storage.delete_ticket(ticket.channel_id)
removed += 1
return removed
class BulkCloseView(View):
"""View for selecting tickets to bulk close"""
def __init__(self, bot, config: GuildTicketConfig, tickets: List[TicketData]):
super().__init__(timeout=None) # Persistent view
self.bot = bot
self.config = config
self.tickets = tickets
self.selected_tickets = set()
# Create select with tickets (max 25 per select)
options = []
for ticket in tickets[:25]:
channel = self.bot.get_channel(ticket.channel_id)
channel_name = channel.name if channel else f"ticket_{ticket.channel_id}"
options.append(
discord.SelectOption(
label=channel_name[:100],
value=str(ticket.channel_id),
description=f"Priorité: {ticket.priority.value}"
)
)
if options:
select = Select(
placeholder="Sélectionnez les tickets à fermer...",
options=options,
min_values=1,
max_values=len(options)
)
select.callback = self._select_callback
self.add_item(select)
# Confirm button
confirm_btn = Button(
style=ButtonStyle.red,
label="Confirmer la fermeture",
custom_id="bulk_confirm"
)
confirm_btn.callback = self._confirm_callback
self.add_item(confirm_btn)
async def _select_callback(self, interaction: Interaction):
"""Handle ticket selection"""
self.selected_tickets = set(int(v) for v in interaction.data["values"])
await interaction.response.send_message(
f"*{len(self.selected_tickets)} ticket(s) sélectionné(s)*",
ephemeral=True
)
async def _confirm_callback(self, interaction: Interaction):
"""Confirm bulk close"""
await interaction.response.defer(ephemeral=True)
if not self.selected_tickets:
await interaction.followup.send("Aucun ticket sélectionné.", ephemeral=True)
return
storage = get_storage()
closed_count = 0
for channel_id in self.selected_tickets:
ticket = storage.load_ticket(channel_id)
if ticket:
ticket.status = TicketStatus.CLOSED
ticket.closed_at = datetime.now()
storage.save_ticket(ticket)
# Update channel
channel = interaction.guild.get_channel(channel_id)
if channel:
# Update permissions
overwrites = 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 channel.edit(
name=f"closed-{channel.name}",
overwrites=overwrites
)
await channel.send(f"Fermé par bulk close (Admin: {interaction.user.mention})")
closed_count += 1
await interaction.followup.send(
f"{closed_count} ticket(s) fermé(s).",
ephemeral=True
)
class ConfigPanelView(View):
"""Configuration panel view"""
def __init__(self, bot, config: GuildTicketConfig):
super().__init__(timeout=None)
self.bot = bot
self.config = config
# Toggle enabled
toggle_btn = Button(
style=ButtonStyle.green if config.enabled else ButtonStyle.red,
label="Activer/Désactiver",
custom_id="config_toggle"
)
toggle_btn.callback = self._toggle_callback
self.add_item(toggle_btn)
# Add category
add_cat_btn = Button(
style=ButtonStyle.blurple,
label=" Ajouter Catégorie",
custom_id="config_add_category"
)
add_cat_btn.callback = self._add_category_callback
self.add_item(add_cat_btn)
# Edit roles
roles_btn = Button(
style=ButtonStyle.secondary,
label="👥 Gérer Rôles Staff",
custom_id="config_roles"
)
roles_btn.callback = self._roles_callback
self.add_item(roles_btn)
# Toggle auto-close
auto_close_btn = Button(
style=ButtonStyle.green if config.auto_close_enabled else ButtonStyle.red,
label="Auto-close",
custom_id="config_auto_close"
)
auto_close_btn.callback = self._auto_close_callback
self.add_item(auto_close_btn)
async def _toggle_callback(self, interaction: Interaction):
"""Toggle ticket system"""
self.config.enabled = not self.config.enabled
storage = get_storage()
storage.save_config(interaction.guild_id, self.config)
status = "activé" if self.config.enabled else "désactivé"
await interaction.response.send_message(
f"Le système de tickets est maintenant **{status}**.",
ephemeral=True
)
async def _add_category_callback(self, interaction: Interaction):
"""Show add category modal"""
modal = AddCategoryModal(self.bot, self.config)
await interaction.response.send_modal(modal)
async def _roles_callback(self, interaction: Interaction):
"""Show role management"""
view = RolesManagementView(self.bot, self.config)
embed = discord.Embed(
title="👥 Gestion des Rôles Staff",
description="Sélectionnez les rôles qui auront accès aux tickets:",
color=Color.blue()
)
# Show current roles
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 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 _auto_close_callback(self, interaction: Interaction):
"""Toggle auto-close"""
self.config.auto_close_enabled = not self.config.auto_close_enabled
storage = get_storage()
storage.save_config(interaction.guild_id, self.config)
status = "activé" if self.config.auto_close_enabled else "désactivé"
await interaction.response.send_message(
f"L'auto-close est maintenant **{status}**.",
ephemeral=True
)
class AddCategoryModal(Modal):
"""Modal for adding a new category"""
def __init__(self, bot, config: GuildTicketConfig):
super().__init__(title=" Nouvelle Catégorie")
self.bot = bot
self.config = config
self.name = TextInput(
label="Nom",
placeholder="Ex: Support",
required=True
)
self.add_item(self.name)
self.description = TextInput(
label="Description",
placeholder="Description de la catégorie...",
required=False,
style=discord.TextStyle.paragraph
)
self.add_item(self.description)
self.emoji = TextInput(
label="Emoji",
placeholder="🎫",
required=False
)
self.add_item(self.emoji)
self.welcome_msg = TextInput(
label="Message de bienvenue",
placeholder="Message affiché quand un ticket est créé...",
required=False,
style=discord.TextStyle.paragraph
)
self.add_item(self.welcome_msg)
async def on_submit(self, interaction: Interaction):
# Generate ID
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 "🎫",
welcome_message=self.welcome_msg.value or "Merci d'avoir créé un ticket."
)
self.config.categories.append(category)
if not self.config.default_category:
self.config.default_category = cat_id
storage = get_storage()
storage.save_config(interaction.guild_id, self.config)
await interaction.response.send_message(
f"Catégorie **{self.name.value}** créée avec succès!",
ephemeral=True
)
class RolesManagementView(View):
"""View for managing staff roles"""
def __init__(self, bot, config: GuildTicketConfig):
super().__init__(timeout=None)
self.bot = bot
self.config = config
# Create select with guild roles
guild = bot.guilds[0] if bot.guilds else None
options = []
if guild:
for role in guild.roles:
if role.id != guild.id: # Skip @everyone
selected = str(role.id) in config.staff_roles
options.append(
discord.SelectOption(
label=role.name,
value=str(role.id),
default=selected
)
)
if options:
select = Select(
placeholder="Sélectionnez les rôles staff...",
options=options[:25]
)
select.callback = self._select_callback
self.add_item(select)
async def _select_callback(self, interaction: Interaction):
"""Handle role selection"""
selected_ids = set(interaction.data["values"])
# Update config
self.config.staff_roles = list(selected_ids)
storage = get_storage()
storage.save_config(interaction.guild_id, self.config)
await interaction.response.send_message(
f"*{len(selected_ids)} rôle(s) mis à jour.*",
ephemeral=True
)

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)