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,661 @@
"""
Modular Ticket System
=====================
A complete, beautiful, and modular ticket system for Discord bots.
Features:
- Multiple ticket categories with custom settings
- Priority levels (Low/Normal/High/Urgent)
- Claim system for staff
- Transcript generation (TXT, JSON, HTML)
- Satisfaction surveys
- Statistics and reporting
- Bulk management
- Auto-close inactive tickets
- Rich embeds and beautiful UI
"""
import os
import sys
# 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 datetime import datetime
from typing import Optional
from .data.storage import get_storage
from .data.models import GuildTicketConfig, TicketCategory, TicketStatus, Priority
from .utils.formatters import TicketEmbedFormatter
from .utils.transcript import get_transcript_generator
from .views.creation import (
TicketCreationView,
TicketManagementView,
SurveyView,
)
from .views.admin import (
AdminPanelView,
ConfigPanelView,
RolesManagementView,
)
class Ticket(commands.Cog):
"""
Main ticket cog with all functionality.
"""
def __init__(self, bot):
self.bot = bot
self.storage = get_storage()
self._register_views.start()
self._auto_close_check.start()
def cog_unload(self):
self._register_views.cancel()
self._auto_close_check.cancel()
async def cog_load(self):
"""Register persistent views when cog is loaded"""
await self._register_all_views()
async def _register_all_views(self):
"""Register all persistent views for all configured guilds"""
try:
for guild_id in self.storage.get_all_guild_ids():
try:
config = self.storage.load_config(guild_id)
if config and config.enabled:
# Register creation view (with categories)
creation_view = TicketCreationView(self.bot, config)
self.bot.add_view(creation_view)
# Register management view (no config needed for persistence)
management_view = TicketManagementView(self.bot, self.storage, config)
self.bot.add_view(management_view)
# Register admin views
admin_view = AdminPanelView(self.bot, config)
self.bot.add_view(admin_view)
config_view = ConfigPanelView(self.bot, config)
self.bot.add_view(config_view)
roles_view = RolesManagementView(self.bot, config)
self.bot.add_view(roles_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}")
@tasks.loop(seconds=30)
async def _register_views(self):
"""Register persistent views periodically"""
await self._register_all_views()
@tasks.loop(minutes=5)
async def _auto_close_check(self):
"""Check for tickets to auto-close"""
try:
for guild_id in self.storage.get_all_guild_ids():
config = self.storage.load_config(guild_id)
if not config.auto_close_enabled:
continue
auto_close_days = config.auto_close_days or 7
auto_close_threshold = datetime.now().timestamp() - (auto_close_days * 86400)
tickets = self.storage.get_guild_tickets(guild_id, TicketStatus.OPEN)
for ticket in tickets:
if ticket.created_at.timestamp() < auto_close_threshold:
# Auto-close ticket
ticket.status = TicketStatus.CLOSED
ticket.closed_at = datetime.now()
self.storage.save_ticket(ticket)
# Log if configured
if config.log_channel_id:
log_channel = self.bot.get_guild(guild_id).get_channel(config.log_channel_id)
if log_channel:
await log_channel.send(
f"🔒 Ticket auto-fermé (inactif depuis {auto_close_days} jours)"
)
except Exception as e:
print(f"[Ticket] Auto-close error: {e}")
@commands.Cog.listener()
async def on_ready(self):
print("🎫 Module Ticket chargé avec succès!")
# --- Main Commands ---
@commands.hybrid_group(name="ticket", aliases=["tickets", "ticketing"])
async def ticket_group(self, ctx):
"""Système de tickets complet"""
if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command)
# --- Setup Commands ---
@ticket_group.command(name="setup", aliases=["configurer"])
@commands.has_permissions(administrator=True)
async def ticket_setup(self, ctx):
"""Configurer le système de tickets"""
await ctx.defer(ephemeral=True)
config = self.storage.load_config(ctx.guild.id)
# Send setup panel
embed = discord.Embed(
title="⚙️ Configuration des Tickets",
description="Bienvenue dans le panneau de configuration des tickets!",
color=discord.Color.blue()
)
embed.add_field(name="📁 Catégories", value=str(len(config.categories)), inline=True)
embed.add_field(name="👥 Rôles Staff", value=str(len(config.staff_roles)), inline=True)
embed.add_field(name="📊 Canal Logs", value=f"<#{config.log_channel_id}>" if config.log_channel_id else "Non configuré", inline=True)
view = TicketSetupView(self.bot, self.storage, config)
await ctx.send(embed=embed, view=view, ephemeral=True)
@ticket_group.command(name="panel", aliases=["panneau"])
async def ticket_panel(self, ctx, channel: discord.TextChannel = None):
"""Créer un panneau de création de tickets"""
await ctx.defer(ephemeral=True)
config = self.storage.load_config(ctx.guild.id)
if not config.enabled:
await ctx.send("Le système de tickets est désactivé.", ephemeral=True)
return
if not config.categories:
await ctx.send("Aucune catégorie configurée. Utilisez `/ticket setup` d'abord.", ephemeral=True)
return
target = channel or ctx.channel
embed = TicketEmbedFormatter.panel_embed(config, ctx.guild.name)
view = TicketCreationView(self.bot, config)
await target.send(embed=embed, view=view)
await ctx.send(f"Panneau envoyé dans {target.mention}", ephemeral=True)
# --- Info Commands ---
@ticket_group.command(name="info", aliases=["informations"])
async def ticket_info(self, ctx, channel: discord.TextChannel = None):
"""Voir les informations d'un ticket"""
await ctx.defer(ephemeral=True)
target = channel or ctx.channel
storage = get_storage()
ticket = storage.load_ticket(target.id)
if not ticket:
await ctx.send("Ce salon n'est pas un ticket.", ephemeral=True)
return
user = self.bot.get_user(ticket.user_id)
category = storage.load_config(ctx.guild.id).get_category(ticket.category_id)
embed = TicketEmbedFormatter.ticket_info_embed(ticket, user, category)
await ctx.send(embed=embed, ephemeral=True)
@ticket_group.command(name="list", aliases=["liste"])
async def ticket_list(self, ctx, user: discord.Member = None):
"""Liste des tickets"""
await ctx.defer(ephemeral=True)
storage = get_storage()
if user:
tickets = storage.get_user_tickets(ctx.guild.id, user.id, open_only=False)
else:
tickets = storage.get_guild_tickets(ctx.guild.id)
if not tickets:
await ctx.send("Aucun ticket trouvé.", ephemeral=True)
return
embed = TicketEmbedFormatter.ticket_list_embed(tickets, user)
await ctx.send(embed=embed, ephemeral=True)
# --- Stats Command ---
@ticket_group.command(name="stats", aliases=["statistiques"])
async def ticket_stats(self, ctx):
"""Voir les statistiques des tickets"""
await ctx.defer(ephemeral=True)
config = self.storage.load_config(ctx.guild.id)
stats = self._calculate_stats(ctx.guild.id)
embed = TicketEmbedFormatter.stats_embed(stats, ctx.guild.name)
await ctx.send(embed=embed, ephemeral=True)
def _calculate_stats(self, guild_id):
"""Calculate ticket statistics"""
from .data.models import TicketStats
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
stats.tickets_by_category[ticket.category_id] = \
stats.tickets_by_category.get(ticket.category_id, 0) + 1
stats.tickets_by_priority[ticket.priority.value] = \
stats.tickets_by_priority.get(ticket.priority.value, 0) + 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
# --- Admin Commands ---
@ticket_group.command(name="admin", aliases=["administration"])
@commands.has_permissions(administrator=True)
async def ticket_admin(self, ctx):
"""Panneau d'administration des tickets"""
await ctx.defer(ephemeral=True)
config = self.storage.load_config(ctx.guild.id)
view = AdminPanelView(self.bot, config)
embed = discord.Embed(
title="🎛️ Administration des Tickets",
description="Gérez votre système de tickets ici:",
color=discord.Color.blue()
)
embed.add_field(name="📊 Statistiques", value="Voir les stats", inline=True)
embed.add_field(name="🔴 Fermeture Massive", value="Fermer plusieurs tickets", inline=True)
embed.add_field(name="⚙️ Configuration", value="Configurer le système", inline=True)
embed.add_field(name="🧹 Nettoyer", value="Supprimer les vieux tickets", inline=True)
await ctx.send(embed=embed, view=view, ephemeral=True)
@ticket_group.command(name="transcript", aliases=["transcription"])
async def ticket_transcript(self, ctx, channel: discord.TextChannel = None):
"""Générer un transcript pour un ticket"""
await ctx.defer(ephemeral=True)
target = channel or ctx.channel
storage = get_storage()
ticket = storage.load_ticket(target.id)
if not ticket:
await ctx.send("Ce salon n'est pas un ticket.", ephemeral=True)
return
# Collect messages
messages = []
async for msg in target.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(msg.author, ctx.guild)
})
# Generate transcript
transcript_gen = get_transcript_generator()
filepath = transcript_gen.save_transcript(ticket, messages, format="html")
if filepath:
await ctx.send(f"✅ Transcript sauvegardé: `{filepath}`", ephemeral=True)
else:
await ctx.send("❌ Erreur lors de la génération.", ephemeral=True)
def _is_staff(self, member, guild) -> bool:
"""Check if member is staff"""
config = self.storage.load_config(guild.id)
for role_id in config.staff_roles:
role = guild.get_role(int(role_id))
if role and role in member.roles:
return True
return False
# --- Category Management ---
@ticket_group.command(name="category", aliases=["categorie"])
@commands.has_permissions(administrator=True)
async def ticket_category(self, ctx, action: str, category_name: str = None):
"""Gérer les catégories de tickets (add/remove/list)"""
await ctx.defer(ephemeral=True)
config = self.storage.load_config(ctx.guild.id)
if action == "list":
if not config.categories:
await ctx.send("Aucune catégorie configurée.", ephemeral=True)
return
embed = discord.Embed(
title="📁 Catégories de Tickets",
color=discord.Color.blue()
)
for cat in config.categories:
embed.add_field(
name=f"{cat.emoji} {cat.name}",
value=f"ID: `{cat.id}`\n{cat.description or 'Aucune description'}",
inline=False
)
await ctx.send(embed=embed, ephemeral=True)
elif action == "add":
if not category_name:
await ctx.send("Nom de catégorie requis.", ephemeral=True)
return
import uuid
cat_id = str(uuid.uuid4())[:8]
category = TicketCategory(
id=cat_id,
name=category_name,
description="Nouvelle catégorie",
emoji="🎫"
)
config.categories.append(category)
self.storage.save_config(ctx.guild.id, config)
await ctx.send(f"Catégorie **{category_name}** créée!", ephemeral=True)
elif action == "remove":
if not category_name:
await ctx.send("Nom de catégorie requis.", ephemeral=True)
return
config.categories = [c for c in config.categories if c.name.lower() != category_name.lower()]
self.storage.save_config(ctx.guild.id, config)
await ctx.send(f"Catégorie **{category_name}** supprimée.", ephemeral=True)
else:
await ctx.send("Action invalide. Utilisez: list, add, remove", ephemeral=True)
# --- Close/Reopen ---
@ticket_group.command(name="close", aliases=["fermer"])
async def ticket_close(self, ctx, channel: discord.TextChannel = None, *, reason: str = None):
"""Fermer un ticket"""
await ctx.defer(ephemeral=True)
target = channel or ctx.channel
storage = get_storage()
ticket = storage.load_ticket(target.id)
if not ticket:
await ctx.send("Ce salon n'est pas un ticket.", ephemeral=True)
return
# Check permissions
can_close = (
ctx.author.id == ticket.user_id or
self._is_staff(ctx.author, ctx.guild)
)
if not can_close:
await ctx.send("Vous ne pouvez pas fermer ce ticket.", ephemeral=True)
return
# Close ticket
ticket.status = TicketStatus.CLOSED
ticket.closed_at = datetime.now()
storage.save_ticket(ticket)
# Update channel
overwrites = target.overwrites.copy()
for target_perm, perms in overwrites.items():
if isinstance(target_perm, discord.Member) and target_perm.id != ctx.guild.me.id:
overwrites[target_perm] = discord.PermissionOverwrite(
read_messages=True,
send_messages=False
)
await target.edit(
name=f"closed-{target.name}",
overwrites=overwrites
)
embed = TicketEmbedFormatter.ticket_closed(ticket, ctx.author, ticket.duration_minutes, reason)
await target.send(embed=embed)
await ctx.send("Ticket fermé!", ephemeral=True)
@ticket_group.command(name="reopen", aliases=["rouvrir"])
async def ticket_reopen(self, ctx, channel: discord.TextChannel = None):
"""Rouvrir un ticket fermé"""
await ctx.defer(ephemeral=True)
target = channel or ctx.channel
storage = get_storage()
ticket = storage.load_ticket(target.id)
if not ticket:
await ctx.send("Ce salon n'est pas un ticket.", ephemeral=True)
return
if ticket.status != TicketStatus.CLOSED:
await ctx.send("Ce ticket n'est pas fermé.", ephemeral=True)
return
# Check permissions
can_reopen = (
ctx.author.id == ticket.user_id or
self._is_staff(ctx.author, ctx.guild)
)
if not can_reopen:
await ctx.send("Vous ne pouvez pas rouvrir ce ticket.", ephemeral=True)
return
# Reopen ticket
ticket.status = TicketStatus.OPEN
storage.save_ticket(ticket)
# Update channel name
new_name = target.name.replace("closed-", "")
await target.edit(name=new_name)
embed = TicketEmbedFormatter.ticket_reopened(ticket, ctx.author)
await target.send(embed=embed)
await ctx.send("Ticket rouvert!", ephemeral=True)
class TicketSetupView(discord.ui.View):
"""View for initial ticket setup"""
def __init__(self, bot, storage, config: GuildTicketConfig):
super().__init__(timeout=None) # Persistent view
self.bot = bot
self.storage = storage
self.config = config
@discord.ui.button(label=" Ajouter Catégorie", style=discord.ButtonStyle.blurple, custom_id="ticket_setup_add_category")
async def add_category(self, interaction: discord.Interaction, button: discord.ui.Button):
modal = AddCategoryModal(self.bot, self.storage, self.config)
await interaction.response.send_modal(modal)
@discord.ui.button(label="👥 Configurer Rôles Staff", style=discord.ButtonStyle.secondary, custom_id="ticket_setup_config_roles")
async def config_roles(self, interaction: discord.Interaction, button: discord.ui.Button):
from .views.admin import RolesManagementView
view = RolesManagementView(self.bot, self.config)
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 = discord.Embed(
title="👥 Gestion des Rôles Staff",
description="Sélectionnez les rôles qui auront accès aux tickets:",
color=discord.Color.blue()
)
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)
@discord.ui.button(label="📊 Définir Canal Logs", style=discord.ButtonStyle.secondary, custom_id="ticket_setup_config_logs")
async def config_logs(self, interaction: discord.Interaction, button: discord.ui.Button):
modal = LogChannelModal(self.storage, self.config)
await interaction.response.send_modal(modal)
@discord.ui.button(label="✅ Système Activé", style=discord.ButtonStyle.green, custom_id="ticket_setup_toggle_system")
async def enable_system(self, interaction: discord.Interaction, button: discord.ui.Button):
self.config.enabled = not self.config.enabled
self.storage.save_config(interaction.guild.id, self.config)
if self.config.enabled:
button.style = discord.ButtonStyle.green
button.label = "✅ Système Activé"
status_msg = "Système de tickets activé!"
else:
button.style = discord.ButtonStyle.red
button.label = "❌ Système Désactivé"
status_msg = "Système de tickets désactivé!"
await interaction.response.edit_message(view=self)
await interaction.followup.send(status_msg, ephemeral=True)
class AddCategoryModal(discord.ui.Modal):
"""Modal for adding a category"""
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 de la catégorie...",
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)
if not self.config.default_category:
self.config.default_category = cat_id
self.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 LogChannelModal(discord.ui.Modal):
"""Modal for setting log channel"""
def __init__(self, storage, config: GuildTicketConfig):
super().__init__(title="📊 Canal Logs")
self.storage = storage
self.config = config
self.channel_id = discord.ui.TextInput(
label="ID du Canal",
placeholder="Ex: 123456789012345678",
required=True
)
self.add_item(self.channel_id)
async def on_submit(self, interaction: discord.Interaction):
try:
channel_id = int(self.channel_id.value)
self.config.log_channel_id = channel_id
self.storage.save_config(interaction.guild.id, self.config)
await interaction.response.send_message(f"Canal logs défini: <#{channel_id}>", ephemeral=True)
except ValueError:
await interaction.response.send_message("ID de canal invalide.", ephemeral=True)
async def setup(bot):
"""Setup function for the cog"""
await bot.add_cog(Ticket(bot))