662 lines
24 KiB
Python
662 lines
24 KiB
Python
|
|
"""
|
|||
|
|
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))
|
|||
|
|
|