1373 lines
48 KiB
JavaScript
1373 lines
48 KiB
JavaScript
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||
const db = require('../../functions/database/db.js');
|
||
const { colors, emojis } = require('../../utils/constants');
|
||
|
||
// Questions pré-définies pour les candidatures
|
||
const CANDIDATURE_QUESTIONS = [
|
||
'Pourquoi souhaites-tu rejoindre le staff ?',
|
||
'Quelles sont tes disponibilités ?',
|
||
'As-tu déjà de l\'expérience en modération ?',
|
||
'Comment gérerais-tu un conflit entre membres ?',
|
||
'Qu\'est-ce qui te motive à aider la communauté ?'
|
||
];
|
||
|
||
// Mapping des types de tickets vers des préfixes d'ID
|
||
const TICKET_TYPE_PREFIXES = {
|
||
'Support': 'SUPP',
|
||
'Plainte': 'PLNT',
|
||
'Plainte Staff': 'PLST',
|
||
'Candidature': 'CAND',
|
||
'Problème Technique': 'TECH'
|
||
};
|
||
|
||
function generateTicketId(type = 'Support') {
|
||
const prefix = TICKET_TYPE_PREFIXES[type] || 'TICK';
|
||
const timestamp = Date.now().toString(36).toUpperCase();
|
||
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
||
return `${prefix}-${timestamp}-${random}`;
|
||
}
|
||
|
||
async function getCandidatureResponses(ticketId) {
|
||
const [responses] = await db.query(
|
||
'SELECT * FROM candidature_responses WHERE ticketId = ? ORDER BY questionNumber ASC',
|
||
[ticketId]
|
||
);
|
||
return responses;
|
||
}
|
||
|
||
function generateTranscriptHTML(ticket, messages, candidatureResponses, useRelativeCss = false) {
|
||
const date = new Date(ticket.createdAt);
|
||
const dateStr = date.toLocaleDateString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||
const closedDateStr = ticket.closedAt ? new Date(ticket.closedAt).toLocaleDateString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit' }) : null;
|
||
|
||
const typeEmojis = {
|
||
'Support': '💬',
|
||
'Plainte': '📢',
|
||
'Plainte Staff': '⚠️',
|
||
'Candidature': '📝',
|
||
'Problème Technique': '🔧'
|
||
};
|
||
|
||
const statusColors = {
|
||
'Ouvert': '#10b981',
|
||
'Fermé': '#ef4444',
|
||
'En attente': '#f59e0b'
|
||
};
|
||
|
||
let messagesHTML = '';
|
||
if (messages.length === 0) {
|
||
messagesHTML = '<p class="no-messages">Aucun message enregistré.</p>';
|
||
} else {
|
||
messagesHTML = messages.map(msg => {
|
||
const msgDate = new Date(msg.timestamp).toLocaleString('fr-FR');
|
||
const attachmentsHTML = msg.attachments
|
||
? `<div class="attachments">📎 Pièces jointes: ${msg.attachments.split(', ').map(url => `<a href="${url}" target="_blank">${url}</a>`).join(', ')}</div>`
|
||
: '';
|
||
|
||
return `
|
||
<div class="message">
|
||
<div class="message-header">
|
||
<span class="message-author">${escapeHtml(msg.userTag)}</span>
|
||
<span class="message-date">${msgDate}</span>
|
||
</div>
|
||
<div class="message-content">${escapeHtml(msg.content || '*Message vide*').replace(/\n/g, '<br>')}</div>
|
||
${attachmentsHTML}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
let candidatureHTML = '';
|
||
if (candidatureResponses && candidatureResponses.length > 0) {
|
||
candidatureHTML = `
|
||
<section class="candidature-section">
|
||
<h2>📝 Réponses aux Questions de Candidature</h2>
|
||
${candidatureResponses.map(response => `
|
||
<div class="question-item">
|
||
<div class="question-label">Question ${response.questionNumber}</div>
|
||
<div class="question-text">${escapeHtml(response.question)}</div>
|
||
<div class="response">${escapeHtml(response.response || '*Pas de réponse*').replace(/\n/g, '<br>')}</div>
|
||
</div>
|
||
`).join('')}
|
||
</section>
|
||
`;
|
||
}
|
||
|
||
// Déterminer l'URL du CSS
|
||
let cssUrl;
|
||
if (useRelativeCss) {
|
||
// Utiliser un chemin relatif pour l'ouverture locale
|
||
cssUrl = '../../transcript.css';
|
||
} else {
|
||
// Utiliser l'URL du serveur web
|
||
const webUrl = process.env.TRANSCRIPT_WEB_URL || 'transcript.syxpi.fr';
|
||
cssUrl = `https://${webUrl}/static/transcript.css`;
|
||
}
|
||
|
||
return `<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Transcription - ${ticket.ticketId}</title>
|
||
<link rel="stylesheet" href="${cssUrl}">
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>${typeEmojis[ticket.type] || '🎫'} Transcription du Ticket</h1>
|
||
<div class="ticket-id">${ticket.ticketId}</div>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<div class="info-label">Type</div>
|
||
<div class="info-value">${typeEmojis[ticket.type] || '🎫'} ${ticket.type}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Créé par</div>
|
||
<div class="info-value">${escapeHtml(ticket.userTag)}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Créé le</div>
|
||
<div class="info-value">${dateStr}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Statut</div>
|
||
<div class="info-value">
|
||
<span class="status-badge" style="background: ${statusColors[ticket.status]}; color: white;">
|
||
${ticket.status}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
${closedDateStr ? `
|
||
<div class="info-item">
|
||
<div class="info-label">Fermé le</div>
|
||
<div class="info-value">${closedDateStr}</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="messages-section">
|
||
<h2>💬 Messages</h2>
|
||
${messagesHTML}
|
||
</div>
|
||
|
||
${candidatureHTML}
|
||
|
||
<div class="footer">
|
||
<p>Transcription générée le ${new Date().toLocaleString('fr-FR')}</p>
|
||
<p>France Femboy Bot • Système de Tickets</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const map = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
};
|
||
return text.replace(/[&<>"']/g, m => map[m]);
|
||
}
|
||
|
||
const commandModule = {
|
||
category: 'ticket',
|
||
data: new SlashCommandBuilder()
|
||
.setName('ticket')
|
||
.setDescription('Gérer les tickets')
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName('setup')
|
||
.setDescription('Configurer le système de tickets (Admin uniquement)')
|
||
.addChannelOption(option =>
|
||
option.setName('channel')
|
||
.setDescription('Le salon où afficher le message de création de tickets')
|
||
.setRequired(false)))
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName('close')
|
||
.setDescription('Fermer un ticket')
|
||
.addStringOption(option =>
|
||
option.setName('reason')
|
||
.setDescription('Raison de la fermeture (optionnel)')
|
||
.setRequired(false)))
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName('transcript')
|
||
.setDescription('Générer la transcription d\'un ticket'))
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName('reopen')
|
||
.setDescription('Rouvrir un ticket fermé'))
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName('add')
|
||
.setDescription('Ajouter un utilisateur au ticket')
|
||
.addUserOption(option =>
|
||
option.setName('user')
|
||
.setDescription('L\'utilisateur à ajouter au ticket')
|
||
.setRequired(true)))
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName('claim')
|
||
.setDescription('Réclamer un ticket (modérateurs uniquement)')),
|
||
|
||
async execute(interaction) {
|
||
const subcommand = interaction.options.getSubcommand();
|
||
|
||
if (subcommand === 'setup') {
|
||
await handleSetup(interaction);
|
||
} else if (subcommand === 'close') {
|
||
await handleClose(interaction);
|
||
} else if (subcommand === 'transcript') {
|
||
await handleTranscript(interaction);
|
||
} else if (subcommand === 'reopen') {
|
||
await handleReopen(interaction);
|
||
} else if (subcommand === 'add') {
|
||
await handleAdd(interaction);
|
||
} else if (subcommand === 'claim') {
|
||
await handleClaim(interaction);
|
||
}
|
||
},
|
||
};
|
||
|
||
async function handleSetup(interaction) {
|
||
await interaction.deferReply({ ephemeral: true });
|
||
|
||
// Vérifier les permissions (Admin uniquement)
|
||
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
|
||
return interaction.editReply({
|
||
content: '❌ Tu n\'as pas la permission d\'utiliser cette commande. (Administrateur requis)'
|
||
});
|
||
}
|
||
|
||
const channel = interaction.options.getChannel('channel') || interaction.channel;
|
||
|
||
if (!channel.isTextBased()) {
|
||
return interaction.editReply({
|
||
content: '❌ Le salon doit être un canal texte.'
|
||
});
|
||
}
|
||
|
||
try {
|
||
// Créer l'embed avec les boutons
|
||
const embed = new EmbedBuilder()
|
||
.setTitle('🎫 Créer un Ticket')
|
||
.setColor(colors.primary)
|
||
.setDescription('Clique sur un des boutons ci-dessous pour créer un ticket.\n\n' +
|
||
'**💬 Support** - Pour toute question ou problème\n' +
|
||
'**📢 Plainte** - Pour signaler un problème avec un membre\n' +
|
||
'**⚠️ Plainte Staff** - Pour signaler un problème avec un staff\n' +
|
||
'**📝 Candidature** - Pour postuler au staff\n' +
|
||
'**🔧 Problème Technique** - Pour signaler un bug ou problème technique')
|
||
.setFooter({ text: 'France Femboy Bot • Système de Tickets' })
|
||
.setTimestamp();
|
||
|
||
// Créer les boutons pour chaque type de ticket
|
||
const row = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId('ticket_create_support')
|
||
.setLabel('Support')
|
||
.setStyle(ButtonStyle.Primary)
|
||
.setEmoji('💬'),
|
||
new ButtonBuilder()
|
||
.setCustomId('ticket_create_plainte')
|
||
.setLabel('Plainte')
|
||
.setStyle(ButtonStyle.Secondary)
|
||
.setEmoji('📢'),
|
||
new ButtonBuilder()
|
||
.setCustomId('ticket_create_plainte_staff')
|
||
.setLabel('Plainte Staff')
|
||
.setStyle(ButtonStyle.Danger)
|
||
.setEmoji('⚠️')
|
||
);
|
||
|
||
const row2 = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId('ticket_create_candidature')
|
||
.setLabel('Candidature')
|
||
.setStyle(ButtonStyle.Success)
|
||
.setEmoji('📝'),
|
||
new ButtonBuilder()
|
||
.setCustomId('ticket_create_probleme_technique')
|
||
.setLabel('Problème Technique')
|
||
.setStyle(ButtonStyle.Secondary)
|
||
.setEmoji('🔧')
|
||
);
|
||
|
||
await channel.send({ embeds: [embed], components: [row, row2] });
|
||
|
||
await interaction.editReply({
|
||
content: `✅ Message de création de tickets envoyé dans ${channel.toString()} !`
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('Erreur lors de la configuration du système de tickets:', err);
|
||
await interaction.editReply({
|
||
content: `❌ Erreur lors de la configuration: ${err.message}`
|
||
});
|
||
}
|
||
}
|
||
|
||
async function handleCreate(interaction, ticketType) {
|
||
await interaction.deferReply({ ephemeral: true });
|
||
|
||
// Mapping des customId vers les types
|
||
const typeMapping = {
|
||
'ticket_create_support': 'Support',
|
||
'ticket_create_plainte': 'Plainte',
|
||
'ticket_create_plainte_staff': 'Plainte Staff',
|
||
'ticket_create_candidature': 'Candidature',
|
||
'ticket_create_probleme_technique': 'Problème Technique'
|
||
};
|
||
|
||
const type = ticketType || typeMapping[interaction.customId] || 'Support';
|
||
|
||
try {
|
||
// Vérifier si l'utilisateur a déjà un ticket ouvert
|
||
const [existingTickets] = await db.query(
|
||
'SELECT * FROM tickets WHERE userId = ? AND guildId = ? AND status = ?',
|
||
[interaction.user.id, interaction.guild.id, 'Ouvert']
|
||
);
|
||
|
||
if (existingTickets.length > 0) {
|
||
const existingTicket = existingTickets[0];
|
||
const channel = interaction.guild.channels.cache.get(existingTicket.channelId);
|
||
return interaction.editReply({
|
||
content: `❌ Tu as déjà un ticket ouvert : ${channel ? channel.toString() : `ID: ${existingTicket.ticketId}`}`
|
||
});
|
||
}
|
||
|
||
// Trouver ou créer la catégorie pour les tickets
|
||
let ticketCategory = interaction.guild.channels.cache.find(
|
||
ch => ch.type === ChannelType.GuildCategory && ch.name.toLowerCase().includes('ticket')
|
||
);
|
||
|
||
if (!ticketCategory) {
|
||
// Créer la catégorie si elle n'existe pas
|
||
try {
|
||
ticketCategory = await interaction.guild.channels.create({
|
||
name: '🎫 Tickets',
|
||
type: ChannelType.GuildCategory,
|
||
permissionOverwrites: [
|
||
{
|
||
id: interaction.guild.id,
|
||
deny: [PermissionFlagsBits.ViewChannel],
|
||
},
|
||
{
|
||
id: interaction.client.user.id,
|
||
allow: [
|
||
PermissionFlagsBits.ViewChannel,
|
||
PermissionFlagsBits.ManageChannels,
|
||
PermissionFlagsBits.SendMessages,
|
||
PermissionFlagsBits.ReadMessageHistory,
|
||
],
|
||
},
|
||
],
|
||
});
|
||
} catch (err) {
|
||
console.error('Erreur lors de la création de la catégorie:', err);
|
||
// Si on ne peut pas créer la catégorie, on créera le canal sans parent
|
||
ticketCategory = null;
|
||
}
|
||
} else {
|
||
// Vérifier si le bot a les permissions nécessaires dans la catégorie existante
|
||
try {
|
||
const botMember = await interaction.guild.members.fetch(interaction.client.user.id);
|
||
const categoryPermissions = ticketCategory.permissionsFor(botMember);
|
||
|
||
if (!categoryPermissions || !categoryPermissions.has([
|
||
PermissionFlagsBits.ViewChannel,
|
||
PermissionFlagsBits.ManageChannels,
|
||
])) {
|
||
// Essayer de donner les permissions au bot dans la catégorie
|
||
try {
|
||
await ticketCategory.permissionOverwrites.edit(interaction.client.user.id, {
|
||
ViewChannel: true,
|
||
ManageChannels: true,
|
||
SendMessages: true,
|
||
ReadMessageHistory: true,
|
||
});
|
||
console.log('✅ Permissions données au bot dans la catégorie');
|
||
} catch (permErr) {
|
||
console.warn('⚠️ Impossible de donner les permissions au bot dans la catégorie:', permErr.message);
|
||
// Si on ne peut pas donner les permissions, on créera le canal sans parent
|
||
ticketCategory = null;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('⚠️ Erreur lors de la vérification des permissions:', err.message);
|
||
// En cas d'erreur, on créera le canal sans parent
|
||
ticketCategory = null;
|
||
}
|
||
}
|
||
|
||
// Générer un ID de ticket avec le préfixe du type
|
||
const ticketId = generateTicketId(type);
|
||
|
||
// Nettoyer le nom d'utilisateur pour le nom du canal (Discord limite à 100 caractères, pas de caractères spéciaux)
|
||
const cleanUsername = interaction.user.username
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9-]/g, '')
|
||
.substring(0, 20);
|
||
|
||
const typeName = type.toLowerCase().replace(/\s+/g, '-').substring(0, 20);
|
||
const channelName = `${typeName}-${cleanUsername}`.substring(0, 100);
|
||
|
||
// Préparer les permissions pour le canal
|
||
const permissionOverwrites = [
|
||
{
|
||
id: interaction.guild.id,
|
||
deny: [PermissionFlagsBits.ViewChannel],
|
||
},
|
||
{
|
||
id: interaction.user.id,
|
||
allow: [
|
||
PermissionFlagsBits.ViewChannel,
|
||
PermissionFlagsBits.SendMessages,
|
||
PermissionFlagsBits.ReadMessageHistory,
|
||
],
|
||
},
|
||
{
|
||
id: interaction.client.user.id,
|
||
allow: [
|
||
PermissionFlagsBits.ViewChannel,
|
||
PermissionFlagsBits.SendMessages,
|
||
PermissionFlagsBits.ReadMessageHistory,
|
||
PermissionFlagsBits.EmbedLinks,
|
||
PermissionFlagsBits.AttachFiles,
|
||
PermissionFlagsBits.ManageMessages,
|
||
],
|
||
},
|
||
];
|
||
|
||
// Créer le canal du ticket
|
||
let channel;
|
||
try {
|
||
// Essayer de créer le canal avec la catégorie
|
||
channel = await interaction.guild.channels.create({
|
||
name: channelName,
|
||
type: ChannelType.GuildText,
|
||
parent: ticketCategory?.id,
|
||
permissionOverwrites: permissionOverwrites,
|
||
reason: `Ticket créé par ${interaction.user.tag} (${ticketId})`,
|
||
});
|
||
|
||
// Si le canal a été créé sans catégorie mais qu'on voulait une catégorie, essayer de le déplacer
|
||
if (ticketCategory && !channel.parentId) {
|
||
try {
|
||
await channel.setParent(ticketCategory.id, { reason: `Déplacement du ticket ${ticketId} dans la catégorie` });
|
||
console.log(`✅ Canal ${channel.id} déplacé dans la catégorie ${ticketCategory.name}`);
|
||
} catch (moveErr) {
|
||
console.warn('⚠️ Impossible de déplacer le canal dans la catégorie:', moveErr.message);
|
||
}
|
||
}
|
||
} catch (createErr) {
|
||
// Si la création échoue avec la catégorie, essayer sans catégorie
|
||
if (ticketCategory && createErr.code === 50013) {
|
||
console.warn('⚠️ Impossible de créer le canal dans la catégorie, création sans catégorie...');
|
||
try {
|
||
channel = await interaction.guild.channels.create({
|
||
name: channelName,
|
||
type: ChannelType.GuildText,
|
||
permissionOverwrites: permissionOverwrites,
|
||
reason: `Ticket créé par ${interaction.user.tag} (${ticketId}) - Sans catégorie (permissions insuffisantes)`,
|
||
});
|
||
console.log('✅ Canal créé sans catégorie');
|
||
} catch (retryErr) {
|
||
console.error('❌ Erreur lors de la création du canal (sans catégorie):', retryErr);
|
||
throw retryErr;
|
||
}
|
||
} else {
|
||
throw createErr;
|
||
}
|
||
}
|
||
|
||
// Ajouter les permissions pour les modérateurs (rôles avec ManageMessages)
|
||
const modRoles = interaction.guild.roles.cache.filter(role =>
|
||
role.permissions.has(PermissionFlagsBits.ManageMessages) &&
|
||
!role.managed // Exclure les rôles gérés par des bots
|
||
);
|
||
|
||
for (const role of modRoles.values()) {
|
||
await channel.permissionOverwrites.edit(role.id, {
|
||
ViewChannel: true,
|
||
SendMessages: true,
|
||
ReadMessageHistory: true,
|
||
});
|
||
}
|
||
|
||
// Enregistrer le ticket dans la DB
|
||
await db.query(
|
||
`INSERT INTO tickets (ticketId, channelId, userId, userTag, guildId, type, status, createdAt)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
[ticketId, channel.id, interaction.user.id, interaction.user.tag, interaction.guild.id, type, 'Ouvert', Date.now()]
|
||
);
|
||
|
||
// Créer l'embed de bienvenue
|
||
const typeEmojis = {
|
||
'Support': '💬',
|
||
'Plainte': '📢',
|
||
'Plainte Staff': '⚠️',
|
||
'Candidature': '📝',
|
||
'Problème Technique': '🔧'
|
||
};
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setTitle(`${typeEmojis[type] || '🎫'} Ticket ${type} - ${ticketId}`)
|
||
.setColor(colors.primary)
|
||
.setDescription(`**Créé par:** <@${interaction.user.id}> (${interaction.user.tag})`)
|
||
.addFields(
|
||
{ name: '📋 Instructions', value: 'Utilisez les boutons ci-dessous pour gérer ce ticket.\nLes modérateurs peuvent utiliser `/ticket transcript` pour générer la transcription.', inline: false }
|
||
)
|
||
.setFooter({ text: `Ticket ID: ${ticketId}` })
|
||
.setTimestamp();
|
||
|
||
// Boutons pour fermer et supprimer le ticket (uniquement pour modérateurs/admin)
|
||
const row = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder()
|
||
.setCustomId(`ticket_close_${ticketId}`)
|
||
.setLabel('Fermer')
|
||
.setStyle(ButtonStyle.Danger)
|
||
.setEmoji('🔒'),
|
||
new ButtonBuilder()
|
||
.setCustomId(`ticket_delete_${ticketId}`)
|
||
.setLabel('Supprimer')
|
||
.setStyle(ButtonStyle.Danger)
|
||
.setEmoji('🗑️')
|
||
);
|
||
|
||
await channel.send({ embeds: [embed], components: [row] });
|
||
|
||
// Si c'est une candidature, ouvrir le modal dans le salon
|
||
if (type === 'Candidature') {
|
||
const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } = require('discord.js');
|
||
const questions = require('./ticket.js').CANDIDATURE_QUESTIONS;
|
||
const modal = new ModalBuilder()
|
||
.setCustomId('candidature_modal')
|
||
.setTitle('Candidature Staff (5 max)');
|
||
for(let i = 0; i < Math.min(questions.length,5); i++) {
|
||
const input = new TextInputBuilder()
|
||
.setCustomId(`cand_q${i+1}`)
|
||
.setLabel(questions[i].slice(0, 45))
|
||
.setStyle(TextInputStyle.Paragraph)
|
||
.setRequired(true)
|
||
.setMaxLength(1000);
|
||
modal.addComponents(new ActionRowBuilder().addComponents(input));
|
||
}
|
||
await interaction.showModal(modal);
|
||
return;
|
||
}
|
||
|
||
await interaction.editReply({
|
||
content: `✅ Ticket créé avec succès ! ${channel.toString()}`
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('Erreur lors de la création du ticket:', err);
|
||
await interaction.editReply({
|
||
content: `❌ Erreur lors de la création du ticket: ${err.message}`
|
||
});
|
||
}
|
||
}
|
||
|
||
async function handleClose(interaction) {
|
||
await interaction.deferReply({ ephemeral: true });
|
||
|
||
// Récupérer la raison depuis les options (peut être null si appelé depuis un bouton)
|
||
let reason = 'Aucune raison fournie';
|
||
if (interaction.options && typeof interaction.options.getString === 'function') {
|
||
reason = interaction.options.getString('reason') || 'Aucune raison fournie';
|
||
}
|
||
|
||
try {
|
||
// Trouver le ticket associé à ce canal
|
||
const [tickets] = await db.query(
|
||
'SELECT * FROM tickets WHERE channelId = ?',
|
||
[interaction.channel.id]
|
||
);
|
||
|
||
if (tickets.length === 0) {
|
||
return interaction.editReply({
|
||
content: '❌ Ce canal n\'est pas un ticket.'
|
||
});
|
||
}
|
||
|
||
const ticket = tickets[0];
|
||
|
||
if (ticket.status === 'Fermé') {
|
||
return interaction.editReply({
|
||
content: '❌ Ce ticket est déjà fermé.'
|
||
});
|
||
}
|
||
|
||
// Vérifier les permissions (MODÉRATEURS/ADMIN UNIQUEMENT - les utilisateurs ne peuvent plus fermer)
|
||
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages) &&
|
||
!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
|
||
return interaction.editReply({
|
||
content: '❌ Seuls les modérateurs et administrateurs peuvent fermer un ticket.'
|
||
});
|
||
}
|
||
|
||
// Mettre à jour le ticket
|
||
await db.query(
|
||
'UPDATE tickets SET status = ?, closedAt = ?, closedBy = ? WHERE ticketId = ?',
|
||
['Fermé', Date.now(), interaction.user.id, ticket.ticketId]
|
||
);
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setAuthor({
|
||
name: `${interaction.user.displayName}`,
|
||
iconURL: interaction.user.displayAvatarURL({ dynamic: true })
|
||
})
|
||
.setTitle('🔒 Ticket Fermé')
|
||
.setColor(colors.warning)
|
||
.setDescription(`Ce ticket a été fermé.`)
|
||
.addFields(
|
||
{ name: '👮 Fermé par', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
|
||
{ name: '📝 Raison', value: reason, inline: false }
|
||
)
|
||
.setFooter({ text: `Ticket ID: ${ticket.ticketId} • ${interaction.guild.name}` })
|
||
.setTimestamp();
|
||
|
||
await interaction.channel.send({ embeds: [embed] });
|
||
|
||
// Générer automatiquement le transcript
|
||
try {
|
||
// Récupérer tous les messages du ticket
|
||
const [messages] = await db.query(
|
||
'SELECT * FROM ticket_messages WHERE ticketId = ? ORDER BY timestamp ASC',
|
||
[ticket.ticketId]
|
||
);
|
||
|
||
// Générer la transcription
|
||
let transcript = `# Transcription du Ticket ${ticket.ticketId}\n\n`;
|
||
transcript += `**Type:** ${ticket.type}\n`;
|
||
transcript += `**Créé par:** ${ticket.userTag} (${ticket.userId})\n`;
|
||
transcript += `**Créé le:** <t:${Math.floor(ticket.createdAt / 1000)}:F>\n`;
|
||
if (ticket.closedAt) {
|
||
transcript += `**Fermé le:** <t:${Math.floor(ticket.closedAt / 1000)}:F>\n`;
|
||
transcript += `**Fermé par:** ${ticket.closedBy ? `<@${ticket.closedBy}>` : 'N/A'}\n`;
|
||
}
|
||
transcript += `\n---\n\n`;
|
||
|
||
if (messages.length === 0) {
|
||
transcript += '*Aucun message enregistré.*\n';
|
||
} else {
|
||
for (const msg of messages) {
|
||
const date = new Date(msg.timestamp).toLocaleString('fr-FR');
|
||
transcript += `**[${date}] ${msg.userTag}:**\n${msg.content || '*Message vide*'}\n`;
|
||
if (msg.attachments) {
|
||
transcript += `*Pièces jointes: ${msg.attachments}*\n`;
|
||
}
|
||
transcript += `\n`;
|
||
}
|
||
}
|
||
|
||
// Générer le fichier HTML
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// Organiser par type et date
|
||
const date = new Date(ticket.createdAt);
|
||
const dateFolder = date.toISOString().split('T')[0];
|
||
const typeFolder = ticket.type.toLowerCase().replace(/\s+/g, '-');
|
||
const cleanUsername = ticket.userTag.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
|
||
|
||
const transcriptsBaseDir = path.join(process.cwd(), 'transcripts');
|
||
const typeDir = path.join(transcriptsBaseDir, typeFolder);
|
||
const dateDir = path.join(typeDir, dateFolder);
|
||
|
||
// Créer les dossiers si nécessaire
|
||
if (!fs.existsSync(transcriptsBaseDir)) {
|
||
fs.mkdirSync(transcriptsBaseDir, { recursive: true });
|
||
}
|
||
if (!fs.existsSync(typeDir)) {
|
||
fs.mkdirSync(typeDir, { recursive: true });
|
||
}
|
||
if (!fs.existsSync(dateDir)) {
|
||
fs.mkdirSync(dateDir, { recursive: true });
|
||
}
|
||
|
||
// Copier le CSS dans le dossier transcripts pour l'ouverture locale
|
||
const cssSourcePath = path.join(__dirname, '../../server/public/transcript.css');
|
||
const cssDestPath = path.join(transcriptsBaseDir, 'transcript.css');
|
||
if (fs.existsSync(cssSourcePath) && !fs.existsSync(cssDestPath)) {
|
||
fs.copyFileSync(cssSourcePath, cssDestPath);
|
||
}
|
||
|
||
const candidatureResponses = ticket.type === 'Candidature' ? await getCandidatureResponses(ticket.ticketId) : null;
|
||
const htmlContent = generateTranscriptHTML(ticket, messages, candidatureResponses, true);
|
||
|
||
// Nom du fichier
|
||
const fileName = `${dateFolder}-${cleanUsername}.html`;
|
||
const filePath = path.join(dateDir, fileName);
|
||
|
||
// Sauvegarder le fichier HTML
|
||
fs.writeFileSync(filePath, htmlContent, 'utf8');
|
||
|
||
// Sauvegarder le chemin web dans la DB (utiliser transcripts/ pour correspondre au dossier réel)
|
||
const webPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
|
||
await db.query(
|
||
'UPDATE tickets SET transcript = ?, transcriptPath = ? WHERE ticketId = ?',
|
||
[transcript, webPath, ticket.ticketId]
|
||
);
|
||
|
||
console.log(`✅ Transcript généré automatiquement pour le ticket ${ticket.ticketId}`);
|
||
} catch (transcriptErr) {
|
||
console.error('⚠️ Erreur lors de la génération automatique du transcript:', transcriptErr);
|
||
// On continue même si le transcript échoue
|
||
}
|
||
|
||
// Supprimer les permissions de l'utilisateur
|
||
await interaction.channel.permissionOverwrites.edit(ticket.userId, {
|
||
ViewChannel: false,
|
||
});
|
||
|
||
// Renommer le canal avec le préfixe "🔒-" si ce n'est pas déjà fait
|
||
try {
|
||
const currentName = interaction.channel.name;
|
||
if (!currentName.startsWith('🔒-')) {
|
||
await interaction.channel.setName(`🔒-${currentName}`);
|
||
}
|
||
} catch (renameErr) {
|
||
console.error('⚠️ Erreur lors du renommage du canal:', renameErr);
|
||
// On continue même si le renommage échoue
|
||
}
|
||
|
||
await interaction.editReply({
|
||
content: '✅ Ticket fermé avec succès. Le transcript a été généré automatiquement.'
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('Erreur lors de la fermeture du ticket:', err);
|
||
await interaction.editReply({
|
||
content: `❌ Erreur lors de la fermeture du ticket: ${err.message}`
|
||
});
|
||
}
|
||
}
|
||
|
||
async function handleTranscript(interaction) {
|
||
await interaction.deferReply({ ephemeral: true });
|
||
|
||
try {
|
||
// Trouver le ticket
|
||
const [tickets] = await db.query(
|
||
'SELECT * FROM tickets WHERE channelId = ?',
|
||
[interaction.channel.id]
|
||
);
|
||
|
||
if (tickets.length === 0) {
|
||
return interaction.editReply({
|
||
content: '❌ Ce canal n\'est pas un ticket.'
|
||
});
|
||
}
|
||
|
||
const ticket = tickets[0];
|
||
|
||
// Récupérer tous les messages du ticket
|
||
const [messages] = await db.query(
|
||
'SELECT * FROM ticket_messages WHERE ticketId = ? ORDER BY timestamp ASC',
|
||
[ticket.ticketId]
|
||
);
|
||
|
||
// Générer la transcription
|
||
let transcript = `# Transcription du Ticket ${ticket.ticketId}\n\n`;
|
||
transcript += `**Type:** ${ticket.type}\n`;
|
||
transcript += `**Créé par:** ${ticket.userTag} (${ticket.userId})\n`;
|
||
transcript += `**Créé le:** <t:${Math.floor(ticket.createdAt / 1000)}:F>\n`;
|
||
if (ticket.closedAt) {
|
||
transcript += `**Fermé le:** <t:${Math.floor(ticket.closedAt / 1000)}:F>\n`;
|
||
transcript += `**Fermé par:** ${ticket.closedBy ? `<@${ticket.closedBy}>` : 'N/A'}\n`;
|
||
}
|
||
transcript += `\n---\n\n`;
|
||
|
||
if (messages.length === 0) {
|
||
transcript += '*Aucun message enregistré.*\n';
|
||
} else {
|
||
for (const msg of messages) {
|
||
const date = new Date(msg.timestamp).toLocaleString('fr-FR');
|
||
transcript += `**[${date}] ${msg.userTag}:**\n${msg.content || '*Message vide*'}\n`;
|
||
if (msg.attachments) {
|
||
transcript += `*Pièces jointes: ${msg.attachments}*\n`;
|
||
}
|
||
transcript += `\n`;
|
||
}
|
||
}
|
||
|
||
// Les réponses de candidature seront incluses dans le HTML
|
||
|
||
// Sauvegarder la transcription dans la DB
|
||
await db.query(
|
||
'UPDATE tickets SET transcript = ? WHERE ticketId = ?',
|
||
[transcript, ticket.ticketId]
|
||
);
|
||
|
||
// Générer le fichier HTML
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// Organiser par type et date (ex: transcripts/Support/2025-11-08/)
|
||
const date = new Date(ticket.createdAt);
|
||
const dateFolder = date.toISOString().split('T')[0]; // Format: YYYY-MM-DD
|
||
const typeFolder = ticket.type.toLowerCase().replace(/\s+/g, '-'); // Format: support, plainte-staff, etc.
|
||
const cleanUsername = ticket.userTag.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
|
||
|
||
const transcriptsBaseDir = path.join(process.cwd(), 'transcripts');
|
||
const typeDir = path.join(transcriptsBaseDir, typeFolder);
|
||
const dateDir = path.join(typeDir, dateFolder);
|
||
|
||
// Créer les dossiers si nécessaire
|
||
if (!fs.existsSync(transcriptsBaseDir)) {
|
||
fs.mkdirSync(transcriptsBaseDir, { recursive: true });
|
||
}
|
||
if (!fs.existsSync(typeDir)) {
|
||
fs.mkdirSync(typeDir, { recursive: true });
|
||
}
|
||
if (!fs.existsSync(dateDir)) {
|
||
fs.mkdirSync(dateDir, { recursive: true });
|
||
}
|
||
|
||
// Copier le CSS dans le dossier transcripts pour l'ouverture locale
|
||
const cssSourcePath = path.join(__dirname, '../../server/public/transcript.css');
|
||
const cssDestPath = path.join(transcriptsBaseDir, 'transcript.css');
|
||
if (fs.existsSync(cssSourcePath) && !fs.existsSync(cssDestPath)) {
|
||
fs.copyFileSync(cssSourcePath, cssDestPath);
|
||
}
|
||
|
||
// Générer le HTML
|
||
const htmlContent = generateTranscriptHTML(ticket, messages, ticket.type === 'Candidature' ? await getCandidatureResponses(ticket.ticketId) : null, true);
|
||
|
||
// Nom de fichier : date-username.html (ex: 2025-11-08-syxpi.html)
|
||
const fileName = `${dateFolder}-${cleanUsername}.html`;
|
||
const filePath = path.join(dateDir, fileName);
|
||
|
||
// Sauvegarder le chemin relatif dans la DB pour accès web (utiliser transcripts/ pour correspondre au dossier réel)
|
||
const webPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
|
||
await db.query(
|
||
'UPDATE tickets SET transcript = ?, transcriptPath = ? WHERE ticketId = ?',
|
||
[transcript, webPath, ticket.ticketId]
|
||
);
|
||
|
||
fs.writeFileSync(filePath, htmlContent, 'utf8');
|
||
|
||
// Envoyer le fichier avec info sur le chemin web
|
||
const webUrl = process.env.TRANSCRIPT_WEB_URL || 'transcript.syxpi.fr';
|
||
const fullWebPath = `https://${webUrl}/${webPath}`;
|
||
|
||
await interaction.editReply({
|
||
content: `✅ Transcription générée !\n\n📄 **Fichier local:** \`${fileName}\`\n🌐 **URL web:** ${fullWebPath}\n\n*Note: L'URL web nécessite une authentification Discord.*`,
|
||
files: [{
|
||
attachment: filePath,
|
||
name: fileName
|
||
}]
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('Erreur lors de la génération de la transcription:', err);
|
||
await interaction.editReply({
|
||
content: `❌ Erreur lors de la génération de la transcription: ${err.message}`
|
||
});
|
||
}
|
||
}
|
||
|
||
async function handleReopen(interaction) {
|
||
await interaction.deferReply({ ephemeral: true });
|
||
|
||
// Vérifier les permissions
|
||
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) {
|
||
return interaction.editReply({
|
||
content: '❌ Tu n\'as pas la permission de rouvrir un ticket.'
|
||
});
|
||
}
|
||
|
||
try {
|
||
const [tickets] = await db.query(
|
||
'SELECT * FROM tickets WHERE channelId = ?',
|
||
[interaction.channel.id]
|
||
);
|
||
|
||
if (tickets.length === 0) {
|
||
return interaction.editReply({
|
||
content: '❌ Ce canal n\'est pas un ticket.'
|
||
});
|
||
}
|
||
|
||
const ticket = tickets[0];
|
||
|
||
if (ticket.status !== 'Fermé') {
|
||
return interaction.editReply({
|
||
content: '❌ Ce ticket n\'est pas fermé.'
|
||
});
|
||
}
|
||
|
||
// Rouvrir le ticket
|
||
await db.query(
|
||
'UPDATE tickets SET status = ?, closedAt = NULL, closedBy = NULL WHERE ticketId = ?',
|
||
['Ouvert', ticket.ticketId]
|
||
);
|
||
|
||
// Restaurer les permissions
|
||
await interaction.channel.permissionOverwrites.edit(ticket.userId, {
|
||
ViewChannel: true,
|
||
SendMessages: true,
|
||
ReadMessageHistory: true,
|
||
});
|
||
|
||
// Retirer le préfixe "🔒-" du nom du canal si présent
|
||
try {
|
||
const currentName = interaction.channel.name;
|
||
if (currentName.startsWith('🔒-')) {
|
||
await interaction.channel.setName(currentName.replace('🔒-', ''));
|
||
}
|
||
} catch (renameErr) {
|
||
console.error('⚠️ Erreur lors du renommage du canal:', renameErr);
|
||
// On continue même si le renommage échoue
|
||
}
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setAuthor({
|
||
name: `${interaction.user.displayName}`,
|
||
iconURL: interaction.user.displayAvatarURL({ dynamic: true })
|
||
})
|
||
.setTitle('🔓 Ticket Rouvert')
|
||
.setColor(colors.success)
|
||
.setDescription(`Ce ticket a été rouvert.`)
|
||
.addFields(
|
||
{ name: '👮 Rouvert par', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true }
|
||
)
|
||
.setFooter({ text: `Ticket ID: ${ticket.ticketId} • ${interaction.guild.name}` })
|
||
.setTimestamp();
|
||
|
||
await interaction.channel.send({ embeds: [embed] });
|
||
|
||
await interaction.editReply({
|
||
content: '✅ Ticket rouvert avec succès.'
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('Erreur lors de la réouverture du ticket:', err);
|
||
await interaction.editReply({
|
||
content: `❌ Erreur lors de la réouverture du ticket: ${err.message}`
|
||
});
|
||
}
|
||
}
|
||
|
||
async function handleAdd(interaction) {
|
||
await interaction.deferReply({ ephemeral: true });
|
||
|
||
try {
|
||
// Trouver le ticket associé à ce canal
|
||
const [tickets] = await db.query(
|
||
'SELECT * FROM tickets WHERE channelId = ?',
|
||
[interaction.channel.id]
|
||
);
|
||
|
||
if (tickets.length === 0) {
|
||
return interaction.editReply({
|
||
content: '❌ Ce canal n\'est pas un ticket.'
|
||
});
|
||
}
|
||
|
||
const ticket = tickets[0];
|
||
|
||
// Vérifier que le ticket n'est pas fermé
|
||
if (ticket.status === 'Fermé' || ticket.status === 'Supprimé') {
|
||
return interaction.editReply({
|
||
content: '❌ Vous ne pouvez pas ajouter un utilisateur à un ticket fermé ou supprimé.'
|
||
});
|
||
}
|
||
|
||
// Récupérer l'utilisateur à ajouter
|
||
const userToAdd = interaction.options.getUser('user');
|
||
|
||
if (!userToAdd) {
|
||
return interaction.editReply({
|
||
content: '❌ Utilisateur introuvable.'
|
||
});
|
||
}
|
||
|
||
// Vérifier que l'utilisateur n'est pas un bot (optionnel, mais recommandé)
|
||
if (userToAdd.bot) {
|
||
return interaction.editReply({
|
||
content: '❌ Vous ne pouvez pas ajouter un bot au ticket.'
|
||
});
|
||
}
|
||
|
||
// Vérifier que l'utilisateur n'est pas déjà dans le ticket
|
||
const member = await interaction.guild.members.fetch(userToAdd.id).catch(() => null);
|
||
if (!member) {
|
||
return interaction.editReply({
|
||
content: '❌ Cet utilisateur n\'est pas sur le serveur.'
|
||
});
|
||
}
|
||
|
||
// Vérifier si l'utilisateur a déjà accès au canal
|
||
try {
|
||
const channelPermissions = interaction.channel.permissionsFor(member);
|
||
if (channelPermissions && channelPermissions.has(PermissionFlagsBits.ViewChannel)) {
|
||
return interaction.editReply({
|
||
content: `❌ ${userToAdd.toString()} a déjà accès à ce ticket.`
|
||
});
|
||
}
|
||
} catch (permErr) {
|
||
// Si on ne peut pas vérifier les permissions, on continue quand même
|
||
console.warn('⚠️ Erreur lors de la vérification des permissions:', permErr);
|
||
}
|
||
|
||
// Ajouter les permissions pour l'utilisateur
|
||
await interaction.channel.permissionOverwrites.edit(userToAdd.id, {
|
||
ViewChannel: true,
|
||
SendMessages: true,
|
||
ReadMessageHistory: true,
|
||
});
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setAuthor({
|
||
name: `${interaction.user.displayName}`,
|
||
iconURL: interaction.user.displayAvatarURL({ dynamic: true })
|
||
})
|
||
.setTitle('✅ Utilisateur Ajouté')
|
||
.setColor(colors.success)
|
||
.setDescription(`${userToAdd.toString()} a été ajouté au ticket.`)
|
||
.setThumbnail(userToAdd.displayAvatarURL({ dynamic: true }))
|
||
.addFields(
|
||
{ name: '👤 Utilisateur ajouté', value: `${userToAdd.toString()}\n\`${userToAdd.tag}\``, inline: true },
|
||
{ name: '👮 Ajouté par', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true }
|
||
)
|
||
.setFooter({ text: `Ticket ID: ${ticket.ticketId} • ${interaction.guild.name}` })
|
||
.setTimestamp();
|
||
|
||
await interaction.channel.send({ embeds: [embed] });
|
||
|
||
await interaction.editReply({
|
||
content: `✅ ${userToAdd.toString()} a été ajouté au ticket avec succès.`
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('Erreur lors de l\'ajout d\'un utilisateur au ticket:', err);
|
||
if (err.code === 50013) {
|
||
await interaction.editReply({
|
||
content: '❌ Je n\'ai pas les permissions nécessaires pour ajouter cet utilisateur au ticket.'
|
||
});
|
||
} else {
|
||
await interaction.editReply({
|
||
content: `❌ Erreur lors de l'ajout de l'utilisateur: ${err.message}`
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleClaim(interaction) {
|
||
await interaction.deferReply({ ephemeral: true });
|
||
|
||
// Vérifier les permissions (MODÉRATEURS/ADMIN UNIQUEMENT)
|
||
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages) &&
|
||
!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
|
||
return interaction.editReply({
|
||
content: '❌ Seuls les modérateurs et administrateurs peuvent réclamer un ticket.'
|
||
});
|
||
}
|
||
|
||
try {
|
||
// Trouver le ticket associé à ce canal
|
||
const [tickets] = await db.query(
|
||
'SELECT * FROM tickets WHERE channelId = ?',
|
||
[interaction.channel.id]
|
||
);
|
||
|
||
if (tickets.length === 0) {
|
||
return interaction.editReply({
|
||
content: '❌ Ce canal n\'est pas un ticket.'
|
||
});
|
||
}
|
||
|
||
const ticket = tickets[0];
|
||
|
||
// Vérifier que le ticket n'est pas fermé ou supprimé
|
||
if (ticket.status === 'Fermé' || ticket.status === 'Supprimé') {
|
||
return interaction.editReply({
|
||
content: '❌ Vous ne pouvez pas réclamer un ticket fermé ou supprimé.'
|
||
});
|
||
}
|
||
|
||
// Vérifier si le ticket est déjà claim
|
||
if (ticket.claimedBy && ticket.claimedBy !== interaction.user.id) {
|
||
const claimedByUser = await interaction.client.users.fetch(ticket.claimedBy).catch(() => null);
|
||
return interaction.editReply({
|
||
content: `❌ Ce ticket est déjà réclamé par ${claimedByUser ? claimedByUser.toString() : 'un autre modérateur'}.`
|
||
});
|
||
}
|
||
|
||
// Si l'utilisateur a déjà claim le ticket, lui permettre de le "unclaim"
|
||
if (ticket.claimedBy === interaction.user.id) {
|
||
await db.query(
|
||
'UPDATE tickets SET claimedBy = NULL, claimedAt = NULL, claimedByTag = NULL WHERE ticketId = ?',
|
||
[ticket.ticketId]
|
||
);
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setAuthor({
|
||
name: `${interaction.user.displayName}`,
|
||
iconURL: interaction.user.displayAvatarURL({ dynamic: true })
|
||
})
|
||
.setTitle('✅ Ticket Non Réclamé')
|
||
.setColor(colors.info)
|
||
.setDescription(`Vous avez libéré ce ticket.`)
|
||
.setFooter({ text: `Ticket ID: ${ticket.ticketId} • ${interaction.guild.name}` })
|
||
.setTimestamp();
|
||
|
||
await interaction.channel.send({ embeds: [embed] });
|
||
|
||
return interaction.editReply({
|
||
content: '✅ Ticket libéré avec succès.'
|
||
});
|
||
}
|
||
|
||
// Claim le ticket (on stocke aussi le tag du modérateur pour l'affichage dans la WebUI)
|
||
await db.query(
|
||
'UPDATE tickets SET claimedBy = ?, claimedAt = ?, claimedByTag = ? WHERE ticketId = ?',
|
||
[interaction.user.id, Date.now(), interaction.user.tag, ticket.ticketId]
|
||
);
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setAuthor({
|
||
name: `${interaction.user.displayName}`,
|
||
iconURL: interaction.user.displayAvatarURL({ dynamic: true })
|
||
})
|
||
.setTitle('✅ Ticket Réclamé')
|
||
.setColor(colors.success)
|
||
.setDescription(`Ce ticket a été réclamé par ${interaction.user.toString()}.`)
|
||
.addFields(
|
||
{ name: '👮 Réclamé par', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true }
|
||
)
|
||
.setFooter({ text: `Ticket ID: ${ticket.ticketId} • ${interaction.guild.name}` })
|
||
.setTimestamp();
|
||
|
||
await interaction.channel.send({ embeds: [embed] });
|
||
|
||
await interaction.editReply({
|
||
content: '✅ Ticket réclamé avec succès.'
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error('Erreur lors de la réclamation du ticket:', err);
|
||
await interaction.editReply({
|
||
content: `❌ Erreur lors de la réclamation du ticket: ${err.message}`
|
||
});
|
||
}
|
||
}
|
||
|
||
async function handleDelete(interaction) {
|
||
await interaction.deferReply({ ephemeral: true });
|
||
|
||
try {
|
||
// Trouver le ticket associé à ce canal
|
||
const [tickets] = await db.query(
|
||
'SELECT * FROM tickets WHERE channelId = ?',
|
||
[interaction.channel.id]
|
||
);
|
||
|
||
if (tickets.length === 0) {
|
||
return interaction.editReply({
|
||
content: '❌ Ce canal n\'est pas un ticket.'
|
||
});
|
||
}
|
||
|
||
const ticket = tickets[0];
|
||
|
||
// Vérifier les permissions (MODÉRATEURS/ADMIN UNIQUEMENT)
|
||
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages) &&
|
||
!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
|
||
return interaction.editReply({
|
||
content: '❌ Seuls les modérateurs et administrateurs peuvent supprimer un ticket.'
|
||
});
|
||
}
|
||
|
||
// Générer le transcript avant de supprimer (si pas déjà fait)
|
||
if (ticket.status !== 'Fermé') {
|
||
try {
|
||
// Générer le transcript avant suppression
|
||
const [messages] = await db.query(
|
||
'SELECT * FROM ticket_messages WHERE ticketId = ? ORDER BY timestamp ASC',
|
||
[ticket.ticketId]
|
||
);
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const date = new Date(ticket.createdAt);
|
||
const dateFolder = date.toISOString().split('T')[0];
|
||
const typeFolder = ticket.type.toLowerCase().replace(/\s+/g, '-');
|
||
const cleanUsername = ticket.userTag.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
|
||
|
||
const transcriptsBaseDir = path.join(process.cwd(), 'transcripts');
|
||
const typeDir = path.join(transcriptsBaseDir, typeFolder);
|
||
const dateDir = path.join(typeDir, dateFolder);
|
||
|
||
if (!fs.existsSync(dateDir)) {
|
||
fs.mkdirSync(dateDir, { recursive: true });
|
||
}
|
||
|
||
const candidatureResponses = ticket.type === 'Candidature' ? await getCandidatureResponses(ticket.ticketId) : null;
|
||
const htmlContent = generateTranscriptHTML({ ...ticket, status: 'Fermé', closedAt: Date.now(), closedBy: interaction.user.id }, messages, candidatureResponses, true);
|
||
|
||
const fileName = `${dateFolder}-${cleanUsername}.html`;
|
||
const filePath = path.join(dateDir, fileName);
|
||
fs.writeFileSync(filePath, htmlContent, 'utf8');
|
||
|
||
const webPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
|
||
await db.query(
|
||
'UPDATE tickets SET transcriptPath = ?, status = ?, closedAt = ?, closedBy = ? WHERE ticketId = ?',
|
||
[webPath, 'Fermé', Date.now(), interaction.user.id, ticket.ticketId]
|
||
);
|
||
} catch (transcriptErr) {
|
||
console.error('⚠️ Erreur lors de la génération du transcript avant suppression:', transcriptErr);
|
||
}
|
||
}
|
||
|
||
// Supprimer le canal
|
||
await interaction.channel.delete(`Ticket supprimé par ${interaction.user.tag}`);
|
||
|
||
// Mettre à jour le ticket dans la DB (marquer comme supprimé)
|
||
await db.query(
|
||
'UPDATE tickets SET status = ?, closedAt = ?, closedBy = ? WHERE ticketId = ?',
|
||
['Supprimé', Date.now(), interaction.user.id, ticket.ticketId]
|
||
); // (Assure-toi que la valeur Supprimé est autorisée dans ENUM)
|
||
|
||
// Note: On ne peut pas envoyer de réponse car le canal est supprimé
|
||
// L'utilisateur verra la réponse ephemeral avant la suppression
|
||
|
||
} catch (err) {
|
||
console.error('Erreur lors de la suppression du ticket:', err);
|
||
if (err.code === 50013) {
|
||
await interaction.editReply({
|
||
content: '❌ Je n\'ai pas les permissions nécessaires pour supprimer ce canal.'
|
||
});
|
||
} else {
|
||
await interaction.editReply({
|
||
content: `❌ Erreur lors de la suppression du ticket: ${err.message}`
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Exporter handleCreate, handleClose et handleDelete pour l'utiliser dans interactionCreate
|
||
commandModule.handleCreate = handleCreate;
|
||
commandModule.handleClose = handleClose;
|
||
commandModule.handleDelete = handleDelete;
|
||
module.exports.CANDIDATURE_QUESTIONS = CANDIDATURE_QUESTIONS;
|
||
module.exports.generateTicketId = generateTicketId;
|
||
// Handler pour la soumission du modal de candidature
|
||
commandModule.handleCandidatureModalSubmit = async function(interaction) {
|
||
const db = require('../../functions/database/db.js');
|
||
// Pas de require/référence module ici, tout est local dans ce fichier !
|
||
const { EmbedBuilder, ChannelType, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||
try {
|
||
// Générer ticketId, permissions, etc
|
||
const ticketId = generateTicketId('Candidature');
|
||
const cleanUsername = interaction.user.username.toLowerCase().replace(/[^a-z0-9-]/g, '').substring(0, 20);
|
||
const channelName = `candidature-${cleanUsername}`.substring(0, 100);
|
||
const permissionOverwrites = [
|
||
{
|
||
id: interaction.guild.id,
|
||
deny: [PermissionFlagsBits.ViewChannel],
|
||
},
|
||
{
|
||
id: interaction.user.id,
|
||
allow: [
|
||
PermissionFlagsBits.ViewChannel,
|
||
PermissionFlagsBits.SendMessages,
|
||
PermissionFlagsBits.ReadMessageHistory,
|
||
],
|
||
},
|
||
{
|
||
id: interaction.client.user.id,
|
||
allow: [
|
||
PermissionFlagsBits.ViewChannel,
|
||
PermissionFlagsBits.SendMessages,
|
||
PermissionFlagsBits.ReadMessageHistory,
|
||
PermissionFlagsBits.EmbedLinks,
|
||
PermissionFlagsBits.AttachFiles,
|
||
PermissionFlagsBits.ManageMessages,
|
||
],
|
||
},
|
||
];
|
||
// Chercher ou créer la catégorie "Tickets"
|
||
let ticketCategory = interaction.guild.channels.cache.find(ch => ch.type === ChannelType.GuildCategory && ch.name.toLowerCase().includes('ticket'));
|
||
let channel;
|
||
if (!ticketCategory) {
|
||
ticketCategory = null; // Créer sans catégorie si non trouvée
|
||
}
|
||
channel = await interaction.guild.channels.create({
|
||
name: channelName,
|
||
type: ChannelType.GuildText,
|
||
parent: ticketCategory?.id,
|
||
permissionOverwrites,
|
||
reason: `Ticket candidature créé via Modal by ${interaction.user.tag} (${ticketId})`,
|
||
});
|
||
// Ajout BDD
|
||
await db.query(
|
||
`INSERT INTO tickets (ticketId, channelId, userId, userTag, guildId, type, status, createdAt)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
[ticketId, channel.id, interaction.user.id, interaction.user.tag, interaction.guild.id, 'Candidature', 'Ouvert', Date.now()]
|
||
);
|
||
// Réponses modal -> BDD
|
||
for (let i = 0; i < Math.min(CANDIDATURE_QUESTIONS.length,5); i++) {
|
||
const qLabel = CANDIDATURE_QUESTIONS[i];
|
||
const field = interaction.fields.getTextInputValue(`cand_q${i+1}`);
|
||
await db.query(
|
||
'INSERT INTO candidature_responses (ticketId, questionNumber, question, response, timestamp) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE response = VALUES(response), timestamp = VALUES(timestamp)',
|
||
[ticketId, i+1, qLabel, field, Date.now()]
|
||
);
|
||
}
|
||
// Staff perms
|
||
const modRoles = interaction.guild.roles.cache.filter(role =>
|
||
role.permissions.has(PermissionFlagsBits.ManageMessages) &&
|
||
!role.managed
|
||
);
|
||
for (const role of modRoles.values()) {
|
||
await channel.permissionOverwrites.edit(role.id, {
|
||
ViewChannel: true,
|
||
SendMessages: true,
|
||
ReadMessageHistory: true,
|
||
});
|
||
}
|
||
// Embed recap dans le ticket
|
||
const embed = new EmbedBuilder()
|
||
.setTitle('✅ Nouvelle candidature staff !')
|
||
.setColor(0x10b981)
|
||
.setDescription(`<@${interaction.user.id}> a complété sa candidature. Voici ses réponses :`)
|
||
.addFields(...CANDIDATURE_QUESTIONS.map((q, idx) => ({
|
||
name: `Question ${idx+1}`,
|
||
value: `**${q}**\n${interaction.fields.getTextInputValue(`cand_q${idx+1}`)}`,
|
||
})))
|
||
.setFooter({ text: `Ticket ID: ${ticketId}` })
|
||
.setTimestamp();
|
||
await channel.send({ embeds: [embed] });
|
||
await interaction.reply({
|
||
content: `✅ Salon de candidature créé ! ${channel.toString()}\nLe staff va étudier ta demande.`,
|
||
ephemeral: true
|
||
});
|
||
} catch (err) {
|
||
console.error('Erreur candidature modal:', err);
|
||
try {
|
||
await interaction.reply({
|
||
content: '❌ Une erreur est survenue lors de la soumission de ta candidature.',
|
||
ephemeral: true
|
||
});
|
||
} catch {}
|
||
}
|
||
};
|
||
|
||
module.exports = commandModule;
|
||
module.exports.CANDIDATURE_QUESTIONS = CANDIDATURE_QUESTIONS;
|