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:
2026-02-09 14:36:26 +01:00
parent eab4419e12
commit ad2014b7b2
586 changed files with 58986 additions and 25205 deletions

673
commands/ticket/ticket.js Normal file
View 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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
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;