673 lines
31 KiB
JavaScript
673 lines
31 KiB
JavaScript
const {
|
||
SlashCommandBuilder,
|
||
PermissionFlagsBits,
|
||
EmbedBuilder,
|
||
ChannelType,
|
||
ActionRowBuilder,
|
||
ButtonBuilder,
|
||
ButtonStyle,
|
||
ModalBuilder,
|
||
TextInputBuilder,
|
||
TextInputStyle,
|
||
MessageFlags
|
||
} = require('discord.js');
|
||
const db = require('../../functions/database/db.js');
|
||
const { colors } = require('../../utils/constants');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// 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) {
|
||
cssUrl = '../../transcript.css';
|
||
} else {
|
||
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>Femboy Croissant 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 (Admin)').addChannelOption(o => o.setName('channel').setDescription('Salon')))
|
||
.addSubcommand(subcommand => subcommand.setName('close').setDescription('Fermer un ticket').addStringOption(o => o.setName('reason').setDescription('Raison')))
|
||
.addSubcommand(subcommand => subcommand.setName('transcript').setDescription('Générer transcript'))
|
||
.addSubcommand(subcommand => subcommand.setName('reopen').setDescription('Rouvrir un ticket'))
|
||
.addSubcommand(subcommand => subcommand.setName('add').setDescription('Ajouter utilisateur').addUserOption(o => o.setName('user').setDescription('Utilisateur').setRequired(true)))
|
||
.addSubcommand(subcommand => subcommand.setName('claim').setDescription('Réclamer un ticket')),
|
||
|
||
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 handleCreate(interaction, ticketType) {
|
||
// Si l'interaction est un ModalSubmit, on a déjà répondu dans interactionCreate ? Non, on va defer ici.
|
||
// Attention : handleCreate est appelé après le ModalSubmit.
|
||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||
|
||
const type = ticketType || 'Support';
|
||
|
||
// Récupérer le sujet s'il vient d'un modal
|
||
let subject = 'Aucun sujet précisé';
|
||
if (interaction.isModalSubmit() && interaction.fields) {
|
||
try {
|
||
subject = interaction.fields.getTextInputValue('ticket_subject');
|
||
} catch (e) { /* Pas de champ subject (ex: commande slash sans modal) */ }
|
||
}
|
||
|
||
try {
|
||
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}`}`
|
||
});
|
||
}
|
||
|
||
let ticketCategory = interaction.guild.channels.cache.find(
|
||
ch => ch.type === ChannelType.GuildCategory && ch.name.toLowerCase().includes('ticket')
|
||
);
|
||
|
||
if (!ticketCategory) {
|
||
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] },
|
||
],
|
||
});
|
||
} catch (err) { ticketCategory = null; }
|
||
}
|
||
|
||
const ticketId = generateTicketId(type);
|
||
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);
|
||
|
||
let channel;
|
||
// Permissions initiales : Le user voit le salon
|
||
const userPerms = [
|
||
{ id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel] },
|
||
{ id: interaction.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles] },
|
||
{ id: interaction.client.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ManageMessages, PermissionFlagsBits.EmbedLinks, PermissionFlagsBits.AttachFiles] },
|
||
];
|
||
|
||
try {
|
||
channel = await interaction.guild.channels.create({
|
||
name: channelName,
|
||
type: ChannelType.GuildText,
|
||
parent: ticketCategory?.id,
|
||
permissionOverwrites: userPerms,
|
||
});
|
||
} catch (createErr) {
|
||
channel = await interaction.guild.channels.create({
|
||
name: channelName,
|
||
type: ChannelType.GuildText,
|
||
permissionOverwrites: userPerms,
|
||
});
|
||
}
|
||
|
||
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 });
|
||
}
|
||
|
||
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()]
|
||
);
|
||
|
||
const typeEmojis = { 'Support': '💬', 'Plainte': '📢', 'Plainte Staff': '⚠️', 'Candidature': '📝', 'Problème Technique': '🔧' };
|
||
|
||
const welcomeEmbed = new EmbedBuilder()
|
||
.setColor(colors.success)
|
||
.setTitle(`${typeEmojis[type] || '🎫'} Ticket Ouvert : ${type}`)
|
||
.setDescription(`👋 Bonjour ${interaction.user.toString()} !\n\nUn membre de l'équipe va prendre en charge votre demande.`)
|
||
.addFields(
|
||
{ name: '📋 Sujet de la demande', value: `\`\`\`${subject}\`\`\``, inline: false },
|
||
{ name: '🆔 ID', value: `\`${ticketId}\``, inline: true },
|
||
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:R>`, inline: true }
|
||
)
|
||
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
|
||
.setFooter({ text: 'Femboy Croissant Bot • Support', iconURL: interaction.client.user.displayAvatarURL() })
|
||
.setTimestamp();
|
||
|
||
const row = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder().setCustomId(`ticket_close_${ticketId}`).setLabel('Fermer').setStyle(ButtonStyle.Danger).setEmoji('🔒'),
|
||
new ButtonBuilder().setCustomId(`ticket_claim_${ticketId}`).setLabel('Réclamer').setStyle(ButtonStyle.Secondary).setEmoji('👋')
|
||
);
|
||
|
||
await channel.send({ content: `${interaction.user} <@&${modRoles.first()?.id || interaction.user.id}>`, embeds: [welcomeEmbed], components: [row] });
|
||
await interaction.editReply({ content: `✅ Ticket créé avec succès ! ${channel.toString()}` });
|
||
|
||
} catch (err) {
|
||
console.error('Erreur creation ticket:', err);
|
||
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
|
||
}
|
||
}
|
||
|
||
// 1. Point d'entrée pour la fermeture (Bouton ou Slash)
|
||
async function handleClose(interaction) {
|
||
// 1. Vérification Permissions (User ne doit pas pouvoir fermer)
|
||
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) {
|
||
return interaction.reply({ content: '❌ Seuls les membres du staff peuvent fermer un ticket.', flags: MessageFlags.Ephemeral });
|
||
}
|
||
|
||
if (interaction.isButton()) {
|
||
const modal = new ModalBuilder().setCustomId('ticket_close_reason_modal').setTitle('Fermeture du Ticket');
|
||
const input = new TextInputBuilder().setCustomId('reason').setLabel('Raison').setStyle(TextInputStyle.Paragraph).setRequired(true);
|
||
modal.addComponents(new ActionRowBuilder().addComponents(input));
|
||
await interaction.showModal(modal);
|
||
return;
|
||
}
|
||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||
let reason = interaction.options?.getString('reason') || 'Aucune raison fournie';
|
||
await processTicketClose(interaction, reason);
|
||
}
|
||
|
||
async function handleCloseModal(interaction) {
|
||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||
// Check permission again for security
|
||
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) {
|
||
return interaction.editReply({ content: '❌ Action non autorisée.' });
|
||
}
|
||
const reason = interaction.fields.getTextInputValue('reason') || 'Aucune raison fournie';
|
||
await processTicketClose(interaction, reason);
|
||
}
|
||
|
||
// 3. Logique commune de fermeture
|
||
async function processTicketClose(interaction, reason) {
|
||
try {
|
||
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
|
||
if (!tickets.length) return interaction.editReply({ content: '❌ Erreur ticket.' });
|
||
const ticket = tickets[0];
|
||
|
||
await db.query('UPDATE tickets SET status = ?, closedAt = ?, closedBy = ? WHERE ticketId = ?', ['Fermé', Date.now(), interaction.user.id, ticket.ticketId]);
|
||
|
||
// --- GENERATION TRANSCRIPT AUTO ---
|
||
try {
|
||
const [messages] = await db.query('SELECT * FROM ticket_messages WHERE ticketId = ? ORDER BY timestamp', [ticket.ticketId]);
|
||
const candidatureResponses = ticket.type === 'Candidature' ? await getCandidatureResponses(ticket.ticketId) : null;
|
||
|
||
const htmlContent = generateTranscriptHTML(ticket, messages, candidatureResponses, true);
|
||
|
||
// Structure dossiers
|
||
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 dateDir = path.join(transcriptsBaseDir, typeFolder, dateFolder);
|
||
if (!fs.existsSync(dateDir)) fs.mkdirSync(dateDir, { recursive: true });
|
||
|
||
// Nom de fichier unique avec ID du ticket
|
||
const fileName = `${dateFolder}-${cleanUsername}-${ticket.ticketId}.html`;
|
||
const filePath = path.join(dateDir, fileName);
|
||
fs.writeFileSync(filePath, htmlContent, 'utf8');
|
||
|
||
// Chemin relatif pour la DB (ex: transcripts/support/2025-01-01/file.html)
|
||
const dbPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
|
||
|
||
await db.query('UPDATE tickets SET transcriptPath = ? WHERE ticketId = ?', [dbPath, ticket.ticketId]);
|
||
|
||
} catch (transcriptErr) {
|
||
console.error('Erreur génération transcript auto:', transcriptErr);
|
||
}
|
||
// ----------------------------------
|
||
|
||
const closeEmbed = new EmbedBuilder()
|
||
.setTitle('🔒 Ticket Fermé')
|
||
.setColor(colors.error)
|
||
.setDescription(`Ticket clôturé par ${interaction.user}.`)
|
||
.addFields(
|
||
{ name: '📝 Raison', value: `\`\`\`${reason}\`\`\``, inline: false },
|
||
{ name: '👮 Géré par', value: ticket.claimedBy ? `<@${ticket.claimedBy}>` : '> *Personne*', inline: true }
|
||
);
|
||
|
||
// BOUTONS DE GESTION APRES FERMETURE
|
||
const row = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder().setCustomId(`ticket_reopen_${ticket.ticketId}`).setLabel('Rouvrir').setStyle(ButtonStyle.Success).setEmoji('🔓'),
|
||
new ButtonBuilder().setCustomId(`ticket_delete_${ticket.ticketId}`).setLabel('Supprimer').setStyle(ButtonStyle.Danger).setEmoji('🗑️')
|
||
);
|
||
|
||
const closeMsg = await interaction.channel.send({ embeds: [closeEmbed], components: [row] });
|
||
|
||
if (!interaction.channel.name.startsWith('🔒-')) {
|
||
await interaction.channel.setName(`🔒-${interaction.channel.name}`).catch(() => {});
|
||
}
|
||
|
||
// VISIBILITE : On retire la vue au membre
|
||
await interaction.channel.permissionOverwrites.edit(ticket.userId, { ViewChannel: false });
|
||
|
||
// Désactivation des ANCIENS boutons (Sauf le nouveau message de fermeture)
|
||
try {
|
||
const messages = await interaction.channel.messages.fetch({ limit: 20 });
|
||
const oldMessages = messages.filter(m => m.author.id === interaction.client.user.id && m.components.length > 0 && m.id !== closeMsg.id);
|
||
for (const oldMsg of oldMessages.values()) {
|
||
const newRows = oldMsg.components.map(row => {
|
||
const newRow = new ActionRowBuilder();
|
||
row.components.forEach(c => newRow.addComponents(ButtonBuilder.from(c).setDisabled(true)));
|
||
return newRow;
|
||
});
|
||
await oldMsg.edit({ components: newRows });
|
||
}
|
||
} catch (e) {}
|
||
|
||
await interaction.editReply({ content: '✅ Ticket fermé.' });
|
||
} catch (err) { await interaction.editReply({ content: `❌ Erreur: ${err.message}` }); }
|
||
}
|
||
|
||
async function handleTranscript(interaction) {
|
||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||
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];
|
||
|
||
const [messages] = await db.query('SELECT * FROM ticket_messages WHERE ticketId = ? ORDER BY timestamp', [ticket.ticketId]);
|
||
|
||
// Structure dossiers
|
||
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 dateDir = path.join(transcriptsBaseDir, typeFolder, dateFolder);
|
||
if (!fs.existsSync(dateDir)) fs.mkdirSync(dateDir, { recursive: true });
|
||
|
||
// Génération
|
||
const candidatureResponses = ticket.type === 'Candidature' ? await getCandidatureResponses(ticket.ticketId) : null;
|
||
const htmlContent = generateTranscriptHTML(ticket, messages, candidatureResponses, true);
|
||
|
||
// Nom de fichier unique avec ID du ticket
|
||
const fileName = `${dateFolder}-${cleanUsername}-${ticket.ticketId}.html`;
|
||
const filePath = path.join(dateDir, fileName);
|
||
fs.writeFileSync(filePath, htmlContent, 'utf8');
|
||
|
||
const webPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
|
||
const webUrl = process.env.TRANSCRIPT_WEB_URL || 'transcript.syxpi.fr';
|
||
|
||
// Mise à jour DB
|
||
const dbPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
|
||
await db.query('UPDATE tickets SET transcriptPath = ? WHERE ticketId = ?', [dbPath, ticket.ticketId]);
|
||
|
||
await interaction.editReply({
|
||
content: `✅ **Transcription générée !**\n\n📄 **Fichier local:** \`${fileName}\`\n🌐 **Lien Web:** https://${webUrl}/${webPath}`,
|
||
files: [{ attachment: filePath, name: fileName }]
|
||
});
|
||
} catch (err) {
|
||
console.error(err);
|
||
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
|
||
}
|
||
}
|
||
|
||
async function handleReopen(interaction) {
|
||
if (interaction.isButton()) {
|
||
const modal = new ModalBuilder().setCustomId('ticket_reopen_modal').setTitle('Réouverture du Ticket');
|
||
const input = new TextInputBuilder().setCustomId('reason').setLabel('Raison').setStyle(TextInputStyle.Paragraph).setRequired(true);
|
||
modal.addComponents(new ActionRowBuilder().addComponents(input));
|
||
await interaction.showModal(modal);
|
||
return;
|
||
}
|
||
await handleReopenModal(interaction, "Commande Slash");
|
||
}
|
||
|
||
async function handleReopenModal(interaction, slashReason = null) {
|
||
if (!slashReason) await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||
const reason = slashReason || interaction.fields.getTextInputValue('reason');
|
||
|
||
try {
|
||
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
|
||
const ticket = tickets[0];
|
||
|
||
await db.query('UPDATE tickets SET status = ?, closedAt = NULL, closedBy = NULL WHERE ticketId = ?', ['Ouvert', ticket.ticketId]);
|
||
|
||
// Restituer la vue
|
||
await interaction.channel.permissionOverwrites.edit(ticket.userId, { ViewChannel: true, SendMessages: true });
|
||
|
||
if (interaction.channel.name.startsWith('🔒-')) {
|
||
await interaction.channel.setName(interaction.channel.name.replace('🔒-', ''));
|
||
}
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setTitle('🔓 Ticket Réouvert')
|
||
.setColor(colors.success)
|
||
.setDescription(`Le ticket a été rouvert par ${interaction.user}.`)
|
||
.addFields({ name: '📝 Raison', value: `\`\`\`${reason}\`\`\`` });
|
||
|
||
// IMPORTANT : On remet le bouton FERMER
|
||
const row = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder().setCustomId(`ticket_close_${ticket.ticketId}`).setLabel('Fermer').setStyle(ButtonStyle.Danger).setEmoji('🔒')
|
||
);
|
||
|
||
await interaction.channel.send({ embeds: [embed], components: [row] });
|
||
|
||
// Supprimer le message de fermeture précédent
|
||
try {
|
||
const messages = await interaction.channel.messages.fetch({ limit: 10 });
|
||
const closeMsg = messages.find(m => m.embeds[0]?.title === '🔒 Ticket Fermé');
|
||
if (closeMsg) await closeMsg.delete();
|
||
} catch (e) {}
|
||
|
||
const msg = { content: '✅ Ticket rouvert.' };
|
||
if (interaction.replied || interaction.deferred) await interaction.editReply(msg);
|
||
else await interaction.reply({ ...msg, flags: MessageFlags.Ephemeral });
|
||
|
||
} catch (err) { console.error(err); }
|
||
}
|
||
|
||
async function handleAdd(interaction) {
|
||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||
try {
|
||
const user = interaction.options.getUser('user');
|
||
await interaction.channel.permissionOverwrites.edit(user.id, { ViewChannel: true, SendMessages: true, ReadMessageHistory: true });
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setTitle('👤 Utilisateur Ajouté')
|
||
.setColor(colors.info)
|
||
.setDescription(`${user.toString()} a été ajouté au ticket par ${interaction.user.toString()}.`)
|
||
.setTimestamp();
|
||
|
||
await interaction.channel.send({ embeds: [embed] });
|
||
await interaction.editReply({ content: `✅ ${user.tag} ajouté.` });
|
||
} catch (err) {
|
||
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
|
||
}
|
||
}
|
||
|
||
async function handleClaim(interaction) {
|
||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) return interaction.editReply({ content: '❌ Staff uniquement.' });
|
||
|
||
try {
|
||
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
|
||
if (!tickets.length) return interaction.editReply({ content: '❌ Pas un ticket.' });
|
||
const ticket = tickets[0];
|
||
|
||
if (ticket.claimedBy === interaction.user.id) {
|
||
await db.query('UPDATE tickets SET claimedBy = NULL, claimedByTag = NULL WHERE ticketId = ?', [ticket.ticketId]);
|
||
await interaction.channel.send({ embeds: [new EmbedBuilder().setColor(colors.info).setDescription(`🛑 **Ticket libéré** par ${interaction.user.toString()}.`)] });
|
||
return interaction.editReply({ content: '✅ Ticket libéré.' });
|
||
}
|
||
|
||
if (ticket.claimedBy) return interaction.editReply({ content: `❌ Déjà réclamé par quelqu'un d'autre.` });
|
||
|
||
await db.query('UPDATE tickets SET claimedBy = ?, claimedByTag = ?, claimedAt = ? WHERE ticketId = ?', [interaction.user.id, interaction.user.tag, Date.now(), ticket.ticketId]);
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setColor(colors.success)
|
||
.setTitle('👋 Ticket Pris en Charge')
|
||
.setDescription(`Ce ticket est désormais géré par ${interaction.user.toString()}.`)
|
||
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
|
||
.setTimestamp();
|
||
|
||
await interaction.channel.send({ embeds: [embed] });
|
||
await interaction.editReply({ content: '✅ Ticket réclamé.' });
|
||
} catch (err) {
|
||
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
|
||
}
|
||
}
|
||
|
||
async function handleDelete(interaction) {
|
||
// Suppression directe sans modal
|
||
await handleDeleteModal(interaction, "Suppression demandée par l'utilisateur");
|
||
}
|
||
|
||
async function handleDeleteModal(interaction, slashReason = null) {
|
||
// Si l'interaction n'a pas été différée ou répondue, on le fait
|
||
if (!interaction.deferred && !interaction.replied) {
|
||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||
}
|
||
|
||
try {
|
||
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
|
||
if (tickets.length) {
|
||
await db.query('UPDATE tickets SET status = ?, closedAt = ?, closedBy = ? WHERE ticketId = ?', ['Supprimé', Date.now(), interaction.user.id, tickets[0].ticketId]);
|
||
}
|
||
await interaction.channel.delete();
|
||
} catch (err) { console.error(err); }
|
||
}
|
||
|
||
// Fonction spécifique pour le submit du modal
|
||
async function handleCandidatureModalSubmit(interaction) {
|
||
const ticketId = generateTicketId('Candidature');
|
||
const channelName = `candidature-${interaction.user.username.replace(/[^a-z0-9]/gi, '').substring(0,15)}`;
|
||
|
||
try {
|
||
let category = interaction.guild.channels.cache.find(c => c.type === ChannelType.GuildCategory && c.name.toLowerCase().includes('ticket'));
|
||
const channel = await interaction.guild.channels.create({
|
||
name: channelName, type: ChannelType.GuildText, parent: category?.id,
|
||
permissionOverwrites: [
|
||
{ id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel] },
|
||
{ id: interaction.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] },
|
||
{ id: interaction.client.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels] }
|
||
]
|
||
});
|
||
|
||
await db.query(`INSERT INTO tickets (ticketId, channelId, userId, userTag, guildId, type, status, createdAt) VALUES (?, ?, ?, ?, ?, 'Candidature', 'Ouvert', ?)`, [ticketId, channel.id, interaction.user.id, interaction.user.tag, interaction.guild.id, Date.now()]);
|
||
|
||
const fields = [];
|
||
for (let i = 0; i < Math.min(CANDIDATURE_QUESTIONS.length, 5); i++) {
|
||
const val = interaction.fields.getTextInputValue(`cand_q${i+1}`);
|
||
// Correction: Utilisation de Date.now() au lieu de new Date() pour le timestamp
|
||
await db.query('INSERT INTO candidature_responses (ticketId, questionNumber, question, response, timestamp) VALUES (?, ?, ?, ?, ?)', [ticketId, i+1, CANDIDATURE_QUESTIONS[i], val, Date.now()]);
|
||
// Utiliser la question complète comme nom de champ
|
||
fields.push({ name: CANDIDATURE_QUESTIONS[i], value: val.substring(0, 1024) });
|
||
}
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setTitle('📝 Nouvelle Candidature Staff')
|
||
.setColor(colors.warning)
|
||
.setDescription(`Candidature soumise par ${interaction.user.toString()} (\`${interaction.user.tag}\`)`)
|
||
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
|
||
.addFields(fields)
|
||
.addFields(
|
||
{ name: '━━━━━━━━━━━━━━━━', value: 'ℹ️ **Informations**' },
|
||
{ name: '🆔 ID Ticket', value: `\`${ticketId}\``, inline: true },
|
||
{ name: '📅 Soumis le', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
|
||
)
|
||
.setFooter({ text: 'Femboy Croissant Bot • Recrutement', iconURL: interaction.client.user.displayAvatarURL() })
|
||
.setTimestamp();
|
||
|
||
const row = new ActionRowBuilder().addComponents(
|
||
new ButtonBuilder().setCustomId(`ticket_close_${ticketId}`).setLabel('Fermer').setStyle(ButtonStyle.Danger).setEmoji('🔒'),
|
||
new ButtonBuilder().setCustomId(`ticket_claim_${ticketId}`).setLabel('Traiter').setStyle(ButtonStyle.Success).setEmoji('✅')
|
||
);
|
||
|
||
await channel.send({ content: '@here Nouvelle candidature', embeds: [embed], components: [row] });
|
||
await interaction.reply({ content: `✅ Candidature envoyée : ${channel.toString()}`, flags: MessageFlags.Ephemeral });
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
// Exports
|
||
commandModule.handleCreate = handleCreate;
|
||
commandModule.handleClose = handleClose;
|
||
commandModule.handleCloseModal = handleCloseModal;
|
||
commandModule.handleReopen = handleReopen;
|
||
commandModule.handleReopenModal = handleReopenModal;
|
||
commandModule.handleDelete = handleDelete;
|
||
commandModule.handleDeleteModal = handleDeleteModal;
|
||
commandModule.handleAdd = handleAdd;
|
||
commandModule.handleClaim = handleClaim;
|
||
commandModule.handleTranscript = handleTranscript;
|
||
commandModule.handleCandidatureModalSubmit = handleCandidatureModalSubmit;
|
||
|
||
module.exports = commandModule;
|
||
module.exports.CANDIDATURE_QUESTIONS = CANDIDATURE_QUESTIONS;
|
||
module.exports.generateTicketId = generateTicketId; |