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:
parent
10173132a2
commit
93e2f9bb44
15 changed files with 3788 additions and 209 deletions
35
commandes/ticket/views/__init__.py
Normal file
35
commandes/ticket/views/__init__.py
Normal 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",
|
||||
]
|
||||
|
||||
522
commandes/ticket/views/admin.py
Normal file
522
commandes/ticket/views/admin.py
Normal 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
|
||||
)
|
||||
|
||||
692
commandes/ticket/views/creation.py
Normal file
692
commandes/ticket/views/creation.py
Normal 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)
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue