Update Bot (j'ai plus le repo sur GitHub)
Qui c'est la conne qui a delete le repo sur GitHub? C'EST MOIIIII
This commit is contained in:
673
commands/ticket/ticket.js
Normal file
673
commands/ticket/ticket.js
Normal file
@@ -0,0 +1,673 @@
|
||||
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: 'France Femboy 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: 'France Femboy 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;
|
||||
Reference in New Issue
Block a user