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