This commit is contained in:
2026-03-15 12:22:42 +01:00
parent cd99275933
commit 311ba5e7f3
558 changed files with 55182 additions and 22981 deletions

View File

@@ -1,141 +1,119 @@
const { Events, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
const { Events, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, MessageFlags } = require('discord.js');
const chalk = require('chalk');
const db = require('../functions/database/db.js');
const { colors } = require('../utils/constants');
module.exports = {
name: Events.InteractionCreate,
async execute(interaction) {
// Gérer la soumission des modals (ex: candidature staff)
if (interaction.isModalSubmit()) {
const ticketModule = require('../commands/ticket/ticket.js');
if (interaction.customId === 'candidature_modal') {
await ticketModule.handleCandidatureModalSubmit(interaction);
return;
}
}
name: Events.InteractionCreate,
async execute(interaction) {
try {
// --- 1. GESTION DES MODALS (Soumission) ---
if (interaction.isModalSubmit()) {
const ticketModule = require('../commands/ticket/ticket.js');
// Gérer les boutons
if (interaction.isButton()) {
// Création de ticket via bouton
if (interaction.customId === 'ticket_create_candidature') {
// Sur clic, montrer le modal candidature, sans ticket encore
const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } = require('discord.js');
const questions = require('../commands/ticket/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));
// 1.A - Création de Ticket (Support, Plainte, etc.)
if (interaction.customId.startsWith('ticket_create_modal_')) {
// On récupère le type depuis l'ID (ex: ticket_create_modal_Support)
const type = interaction.customId.replace('ticket_create_modal_', '');
await ticketModule.handleCreate(interaction, type);
return;
}
// 1.B - Candidature Spécifique
if (interaction.customId === 'candidature_modal') {
await ticketModule.handleCandidatureModalSubmit(interaction);
return;
}
// 1.C - Raison Fermeture
if (interaction.customId === 'ticket_close_reason_modal') {
await ticketModule.handleCloseModal(interaction);
return;
}
// 1.D - Raison Réouverture
if (interaction.customId === 'ticket_reopen_modal') {
await ticketModule.handleReopenModal(interaction);
return;
}
// 1.E - Raison Suppression
if (interaction.customId === 'ticket_delete_modal') {
await ticketModule.handleDeleteModal(interaction);
return;
}
}
// --- 2. GESTION DES BOUTONS (Ouverture des Modals) ---
if (interaction.isButton()) {
const ticketModule = require('../commands/ticket/ticket.js');
// 2.A - Bouton Création : On ouvre un MODAL pour tout le monde
if (interaction.customId.startsWith('ticket_create_')) {
// Cas Spécial : Candidature (Déjà géré avec ses questions spécifiques)
if (interaction.customId === 'ticket_create_candidature') {
const questions = require('../commands/ticket/ticket.js').CANDIDATURE_QUESTIONS;
const modal = new ModalBuilder().setCustomId('candidature_modal').setTitle('Recrutement Staff');
for(let i=0; i<Math.min(questions.length,5); i++) {
const input = new TextInputBuilder().setCustomId(`cand_q${i+1}`).setLabel(questions[i].substring(0,45)).setStyle(TextInputStyle.Paragraph).setRequired(true);
modal.addComponents(new ActionRowBuilder().addComponents(input));
}
await interaction.showModal(modal);
return;
}
// Cas Général (Support, Plainte, etc.): Modal simple "Sujet"
// On extrait le type du bouton (ex: ticket_create_support -> support)
let typeKey = interaction.customId.replace('ticket_create_', '');
// On remet en joli format (support -> Support, plainte_staff -> Plainte Staff)
const typeMapping = {
'support': 'Support',
'plainte': 'Plainte',
'plainte_staff': 'Plainte Staff',
'probleme_technique': 'Problème Technique'
};
const prettyType = typeMapping[typeKey] || 'Support';
// On passe le type dans l'ID du modal pour le récupérer au submit
const modal = new ModalBuilder()
.setCustomId(`ticket_create_modal_${prettyType}`)
.setTitle(`Nouveau ticket : ${prettyType}`);
const subjectInput = new TextInputBuilder()
.setCustomId('ticket_subject')
.setLabel("Sujet / Raison de la demande")
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setMaxLength(1000)
.setPlaceholder("Décrivez brièvement votre problème...");
modal.addComponents(new ActionRowBuilder().addComponents(subjectInput));
await interaction.showModal(modal);
return;
}
// 2.B - Autres actions (Fermer, Réouvrir, etc.)
if (interaction.customId.startsWith('ticket_close_')) return await ticketModule.handleClose(interaction);
if (interaction.customId.startsWith('ticket_delete_')) return await ticketModule.handleDelete(interaction);
if (interaction.customId.startsWith('ticket_reopen_')) return await ticketModule.handleReopen(interaction);
if (interaction.customId.startsWith('ticket_claim_')) return await ticketModule.handleClaim(interaction);
}
// --- 3. COMMANDES SLASH ---
if (!interaction.isChatInputCommand()) return;
const command = interaction.client.commands.get(interaction.commandName);
if (!command) {
console.error(chalk.red(`Aucune commande correspondant à ${interaction.commandName} n'a été trouvée.`));
return;
}
await command.execute(interaction);
} catch (error) {
console.error(chalk.red(`Erreur lors de l'exécution d'une interaction :`), error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: 'Une erreur est survenue lors de l\'exécution de cette interaction !', flags: MessageFlags.Ephemeral });
} else {
await interaction.reply({ content: 'Une erreur est survenue lors de l\'exécution de cette interaction !', flags: MessageFlags.Ephemeral });
}
}
await interaction.showModal(modal);
return;
}
if (interaction.customId.startsWith('ticket_create_')) {
const ticketCommand = interaction.client.commands.get('ticket');
if (ticketCommand) {
const ticketModule = require('../commands/ticket/ticket.js');
await ticketModule.handleCreate(interaction);
}
return;
}
// Fermeture de ticket via bouton
if (interaction.customId.startsWith('ticket_close_')) {
try {
const ticketModule = require('../commands/ticket/ticket.js');
await ticketModule.handleClose(interaction);
// Désactiver le bouton après la fermeture
try {
const { ActionRowBuilder, ButtonBuilder } = require('discord.js');
const oldRow = interaction.message.components[0];
if (oldRow && oldRow.components.length > 0) {
const newRow = new ActionRowBuilder();
for (const component of oldRow.components) {
if (component.customId === interaction.customId) {
newRow.addComponents(
ButtonBuilder.from(component).setDisabled(true)
);
} else {
newRow.addComponents(component);
}
}
await interaction.message.edit({ components: [newRow] });
}
} catch (buttonErr) {
console.error('⚠️ Erreur lors de la désactivation du bouton:', buttonErr);
}
} catch (err) {
console.error('Erreur lors de la fermeture du ticket via bouton:', err);
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: '❌ Erreur lors de la fermeture du ticket.',
ephemeral: true
});
} else {
await interaction.editReply({
content: '❌ Erreur lors de la fermeture du ticket.'
});
}
}
return;
}
// Suppression de ticket via bouton
if (interaction.customId.startsWith('ticket_delete_')) {
try {
const ticketModule = require('../commands/ticket/ticket.js');
await ticketModule.handleDelete(interaction);
} catch (err) {
console.error('Erreur lors de la suppression du ticket via bouton:', err);
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: '❌ Erreur lors de la suppression du ticket.',
ephemeral: true
});
} else {
await interaction.editReply({
content: '❌ Erreur lors de la suppression du ticket.'
});
}
}
return;
}
}
// Gérer les commandes slash
if (!interaction.isChatInputCommand()) return;
const command = interaction.client.commands.get(interaction.commandName);
if (!command) {
console.error(chalk.red(`❌ Aucune commande correspondant à ${interaction.commandName} n'a été trouvée.`));
return;
}
try {
await command.execute(interaction);
console.log(chalk.green(`${interaction.user.tag} a utilisé la commande /${interaction.commandName}`));
} catch (error) {
console.error(chalk.red(`❌ Erreur lors de l'exécution de la commande ${interaction.commandName}:`), error);
const errorMessage = {
content: '❌ Une erreur est survenue lors de l\'exécution de cette commande !',
ephemeral: true
};
if (interaction.replied || interaction.deferred) {
await interaction.followUp(errorMessage).catch(() => null);
} else {
await interaction.reply(errorMessage).catch(() => null);
}
}
},
},
};

View File

@@ -1,310 +1,162 @@
const { Events } = require('discord.js');
const { Events, EmbedBuilder } = require('discord.js');
const db = require('../functions/database/db.js');
const { addXP, isChannelExcluded, getUserXP, getXPMultiplier } = require('../functions/xp/xp.js');
const { addXP, isChannelExcluded, getUserXP, getXPMultiplier, getXPProgress } = require('../functions/xp/xp.js');
const { colors } = require('../utils/constants');
// --- FONCTIONS UTILITAIRES ---
// Fonction pour détecter les bumps (Disboard, etc.)
async function detectBump(message) {
// Vérifier si le message vient d'un bot de bump (Disboard, etc.)
// Disboard a l'ID: 302050872383242240
// On peut aussi détecter par le nom du bot ou le contenu du message
const bumpBots = ['302050872383242240']; // Disboard bot ID
if (!message.author.bot) return null;
if (!bumpBots.includes(message.author.id)) return null;
// Détecter les messages de bump (Disboard envoie généralement un embed avec "Bump done!")
const content = message.content?.toLowerCase() || '';
const hasEmbed = message.embeds.length > 0;
// Vérifier si c'est un message de bump
// Disboard envoie généralement un embed avec "Bump done!" ou similaire
if (hasEmbed) {
const embed = message.embeds[0];
const embedDescription = embed.description?.toLowerCase() || '';
const embedTitle = embed.title?.toLowerCase() || '';
// Mots-clés pour détecter un bump
const bumpKeywords = ['bump done', 'bump réussi', 'bump réalisé', 'bump effectué', 'serv bump', 'server bump'];
const isBumpMessage = bumpKeywords.some(keyword =>
embedDescription.includes(keyword) ||
embedTitle.includes(keyword) ||
content.includes(keyword)
);
if (isBumpMessage) {
// Chercher l'utilisateur qui a fait le bump dans les mentions ou dans l'embed
// Disboard mentionne généralement l'utilisateur dans l'embed
let bumpedUserId = null;
// Essayer de trouver l'utilisateur dans les mentions
if (message.mentions.users.size > 0) {
bumpedUserId = message.mentions.users.first().id;
}
// Si pas de mention, chercher dans l'embed (format: "User bumped the server!")
if (!bumpedUserId && embedDescription) {
// Chercher un ID utilisateur dans l'embed (format: <@userId>)
const userIdMatch = embedDescription.match(/<@!?(\d+)>/);
if (userIdMatch) {
bumpedUserId = userIdMatch[1];
}
}
// Si toujours pas trouvé, chercher dans le footer ou les fields
if (!bumpedUserId && embed.footer) {
const footerMatch = embed.footer.text?.match(/<@!?(\d+)>/);
if (footerMatch) {
bumpedUserId = footerMatch[1];
}
}
return bumpedUserId;
}
}
return null;
const bumpBots = ['302050872383242240']; // Disboard bot ID
if (!message.author.bot || !bumpBots.includes(message.author.id)) return null;
const content = message.content?.toLowerCase() || '';
const embed = message.embeds[0];
if (!embed) return null;
const embedDescription = embed.description?.toLowerCase() || '';
const embedTitle = embed.title?.toLowerCase() || '';
const bumpKeywords = ['bump done', 'bump réussi', 'bump réalisé', 'bump effectué', 'serv bump', 'server bump'];
const isBumpMessage = bumpKeywords.some(keyword =>
embedDescription.includes(keyword) ||
embedTitle.includes(keyword) ||
content.includes(keyword)
);
if (!isBumpMessage) return null;
// Tentative de récupération de l'ID utilisateur
if (message.mentions.users.size > 0) return message.mentions.users.first().id;
const userIdMatch = embedDescription.match(/<@!?(\d+)>/);
if (userIdMatch) return userIdMatch[1];
if (embed.footer) {
const footerMatch = embed.footer.text?.match(/<@!?(\d+)>/);
if (footerMatch) return footerMatch[1];
}
return null;
}
// Fonction pour détecter les messages de bienvenue
function detectWelcomeMessage(message) {
// Ignorer les bots
if (message.author.bot) return false;
// Mots-clés pour détecter les messages de bienvenue
const welcomeKeywords = ['bienvenue', 'welcome', 'bvn', 'salut', 'hey', 'bonjour'];
const content = message.content.toLowerCase();
// Vérifier si le message contient un mot de bienvenue
const hasWelcomeKeyword = welcomeKeywords.some(keyword => content.includes(keyword));
// Vérifier si le message mentionne un utilisateur (généralement pour accueillir quelqu'un)
const hasMention = message.mentions.users.size > 0;
// Vérifier si le message est assez court (pour éviter les faux positifs)
const isShortMessage = message.content.length < 100;
return hasWelcomeKeyword && hasMention && isShortMessage;
if (message.author.bot) return false;
const welcomeKeywords = ['bienvenue', 'welcome', 'bvn', 'salut', 'hey', 'bonjour'];
const content = message.content.toLowerCase();
return welcomeKeywords.some(k => content.includes(k)) &&
message.mentions.users.size > 0 &&
message.content.length < 100;
}
async function handleLevelUp(message, result, member) {
if (!result || !result.levelUp) return;
const progress = getXPProgress(result.newXP, result.newLevel);
const embed = new EmbedBuilder()
.setTitle('🎉 Level Up !')
.setDescription(`Félicitations ${member.toString()} ! Tu as atteint le niveau **${result.newLevel}** !`)
.setColor(colors.success)
.addFields(
{ name: '📊 XP', value: `${result.newXP} XP`, inline: true },
{ name: '⭐ Niveau', value: `${result.newLevel}`, inline: true },
{ name: '📈 Progression', value: `${progress.current}/${progress.needed} XP (${progress.percentage}%)`, inline: true }
)
.setThumbnail(member.user.displayAvatarURL({ dynamic: true }))
.setTimestamp();
await message.channel.send({ embeds: [embed] });
}
// --- LOGIQUE PRINCIPALE ---
module.exports = {
name: Events.MessageCreate,
async execute(message) {
// Ignorer les messages qui ne sont pas dans un canal de texte
if (!message.channel.isTextBased() || message.channel.isDMBased()) return;
name: Events.MessageCreate,
async execute(message) {
if (!message.channel.isTextBased() || message.channel.isDMBased()) return;
try {
// Détecter les bumps (AVANT d'ignorer les bots, car Disboard est un bot)
const bumpedUserId = await detectBump(message);
if (bumpedUserId) {
try {
// Enregistrer le bump dans la DB
await db.query(
'INSERT INTO bumps (userId, guildId, bumpTime, reminderSent) VALUES (?, ?, ?, ?)',
[bumpedUserId, message.guild.id, Date.now(), false]
);
// Donner de l'XP pour le bump
const member = await message.guild.members.fetch(bumpedUserId).catch(() => null);
if (member) {
const multiplier = getXPMultiplier(member);
// Gain d'XP pour bump : 50-100 XP (plus que les messages normaux)
const xpGained = Math.floor(Math.random() * 51) + 50; // 50-100 XP
const result = await addXP(bumpedUserId, message.guild.id, xpGained, 'bump', multiplier);
if (result && result.levelUp) {
const { EmbedBuilder } = require('discord.js');
const { colors } = require('../utils/constants');
const { getXPProgress } = require('../functions/xp/xp.js');
const progress = getXPProgress(result.newXP, result.newLevel);
const embed = new EmbedBuilder()
.setTitle('🎉 Level Up !')
.setDescription(`Félicitations ${member.toString()} ! Tu as atteint le niveau **${result.newLevel}** !`)
.setColor(colors.success)
.addFields(
{ name: '📊 XP', value: `${result.newXP} XP`, inline: true },
{ name: '⭐ Niveau', value: `${result.newLevel}`, inline: true },
{ name: '📈 Progression', value: `${progress.current}/${progress.needed} XP (${progress.percentage}%)`, inline: true }
)
.setThumbnail(member.user.displayAvatarURL({ dynamic: true }))
.setTimestamp();
await message.channel.send({ embeds: [embed] });
}
}
// Le reminder sera géré automatiquement par bumpReminder.js (vérifie toutes les minutes)
// Pas besoin de setTimeout ici, le système de reminder s'en occupe
} catch (err) {
console.error('Erreur lors de la détection du bump:', err);
}
// Ne pas continuer le traitement pour les messages de bump
return;
}
try {
// 1. GESTION DES BUMPS
const bumpedUserId = await detectBump(message);
if (bumpedUserId) {
try {
await db.query('INSERT INTO bumps (userId, guildId, bumpTime, reminderSent) VALUES (?, ?, ?, ?)', [bumpedUserId, message.guild.id, Date.now(), false]);
const member = await message.guild.members.fetch(bumpedUserId).catch(() => null);
if (member) {
const xpGained = Math.floor(Math.random() * 51) + 50; // 50-100 XP
const result = await addXP(bumpedUserId, message.guild.id, xpGained, 'bump', getXPMultiplier(member));
await handleLevelUp(message, result, member);
}
} catch (err) { console.error('Erreur bump:', err); }
return;
}
// Ignorer les messages du bot (après la détection des bumps)
if (message.author.bot) return;
// Vérifier si ce canal est un ticket (logique existante pour les tickets)
const [tickets] = await db.query(
'SELECT * FROM tickets WHERE channelId = ? AND status = ?',
[message.channel.id, 'Ouvert']
);
// 2. IGNORER LES BOTS (Sauf pour les bumps traités au-dessus)
if (message.author.bot) return;
if (tickets.length > 0) {
// C'est un ticket, traiter la logique des tickets
const ticket = tickets[0];
// 3. GESTION DES TICKETS
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ? AND status = ?', [message.channel.id, 'Ouvert']);
if (tickets.length > 0) {
const ticket = tickets[0];
const attachments = message.attachments.size > 0 ? message.attachments.map(a => a.url).join(', ') : null;
const maxTag = 100;
const maxText = 65535;
const safeUserTag = (message.author.tag && message.author.tag.length > maxTag) ? message.author.tag.substring(0, maxTag) : message.author.tag;
const safeContent = (message.content && message.content.length > maxText) ? message.content.substring(0, maxText) : message.content;
const safeAttachments = (attachments && attachments.length > maxText) ? attachments.substring(0, maxText) : attachments;
// Enregistrer le message dans la DB
const attachments = message.attachments.size > 0
? message.attachments.map(a => a.url).join(', ')
: null;
await db.query(
`INSERT INTO ticket_messages (ticketId, messageId, userId, userTag, content, attachments, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)`,
[ticket.ticketId, message.id, message.author.id, safeUserTag, safeContent || null, safeAttachments, message.createdTimestamp]
);
return; // Pas d'XP dans les tickets
}
// Troncature des champs pour respecter les limites SQL
const maxTag = 100;
const maxText = 65535;
const safeUserTag = (message.author.tag && message.author.tag.length > maxTag) ? message.author.tag.substring(0, maxTag) : message.author.tag;
const safeContent = (message.content && message.content.length > maxText) ? message.content.substring(0, maxText) : message.content;
const safeAttachments = (attachments && attachments.length > maxText) ? attachments.substring(0, maxText) : attachments;
await db.query(
`INSERT INTO ticket_messages (ticketId, messageId, userId, userTag, content, attachments, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
ticket.ticketId,
message.id,
message.author.id,
safeUserTag,
safeContent || null,
safeAttachments,
message.createdTimestamp
]
);
// 4. SYSTÈME D'XP (Messages normaux)
const excluded = await isChannelExcluded(message.channel.id, message.guild.id);
if (!excluded) {
const userXP = await getUserXP(message.author.id, message.guild.id);
const now = Date.now();
const cooldown = 15000; // 15s
let lastMessageTime = parseInt(userXP.lastMessageTime || 0, 10);
// Reset si date invalide
if (lastMessageTime > now || lastMessageTime < (now - 31536000000)) lastMessageTime = 0;
if (lastMessageTime === 0 || (now - lastMessageTime) >= cooldown) {
const member = await message.guild.members.fetch(message.author.id).catch(() => null);
const xpGained = Math.floor(Math.random() * 11) + 15; // 15-25 XP
const result = await addXP(message.author.id, message.guild.id, xpGained, 'message', member ? getXPMultiplier(member) : 1.0);
if (member) await handleLevelUp(message, result, member);
// Ne pas donner d'XP dans les tickets
return;
}
// Mise à jour stats + timestamp
await db.query('UPDATE user_xp SET lastMessageTime = ?, totalMessages = totalMessages + 1 WHERE userId = ? AND guildId = ?', [now, message.author.id, message.guild.id]);
} else {
// Juste incrémenter le compteur de messages (Cooldown actif)
await db.query('UPDATE user_xp SET totalMessages = totalMessages + 1 WHERE userId = ? AND guildId = ?', [message.author.id, message.guild.id]);
}
}
// Système XP : Gagner de l'XP pour les messages (pas dans les tickets)
// Vérifier si le salon est exclus de l'XP
const excluded = await isChannelExcluded(message.channel.id, message.guild.id);
if (!excluded) {
// Récupérer les données de l'utilisateur
const userXP = await getUserXP(message.author.id, message.guild.id);
// Cooldown : 15 secondes entre chaque gain d'XP par message
const cooldown = 15 * 1000; // 15 secondes
const now = Date.now();
// Convertir lastMessageTime en nombre (peut être string depuis la DB)
let lastMessageTime = 0;
if (userXP.lastMessageTime) {
lastMessageTime = parseInt(userXP.lastMessageTime, 10);
// Si lastMessageTime est dans le futur (erreur de données) ou trop ancien (plus de 1 an), on le réinitialise
if (lastMessageTime > now || lastMessageTime < (now - 365 * 24 * 60 * 60 * 1000)) {
lastMessageTime = 0;
}
}
const timeSinceLastMessage = now - lastMessageTime;
// Si lastMessageTime est 0 (premier message) ou si le cooldown est passé
if (lastMessageTime === 0 || timeSinceLastMessage >= cooldown) {
// Récupérer le membre pour calculer le multiplicateur
const member = await message.guild.members.fetch(message.author.id).catch(() => null);
const multiplier = member ? getXPMultiplier(member) : 1.0;
// Gain d'XP : 15-25 XP aléatoire par message
const xpGained = Math.floor(Math.random() * 11) + 15; // 15-25 XP
// Ajouter l'XP
const result = await addXP(message.author.id, message.guild.id, xpGained, 'message', multiplier);
if (result && result.levelUp) {
// Niveau supérieur atteint !
const { EmbedBuilder } = require('discord.js');
const { colors } = require('../utils/constants');
const { getXPProgress } = require('../functions/xp/xp.js');
const progress = getXPProgress(result.newXP, result.newLevel);
const embed = new EmbedBuilder()
.setTitle('🎉 Level Up !')
.setDescription(`Félicitations ${message.author.toString()} ! Tu as atteint le niveau **${result.newLevel}** !`)
.setColor(colors.success)
.addFields(
{ name: '📊 XP', value: `${result.newXP} XP`, inline: true },
{ name: '⭐ Niveau', value: `${result.newLevel}`, inline: true },
{ name: '📈 Progression', value: `${progress.current}/${progress.needed} XP (${progress.percentage}%)`, inline: true }
)
.setThumbnail(message.author.displayAvatarURL({ dynamic: true }))
.setTimestamp();
await message.channel.send({ embeds: [embed] });
}
// Mettre à jour lastMessageTime et totalMessages (AVANT le gain d'XP pour éviter les problèmes)
await db.query(
'UPDATE user_xp SET lastMessageTime = ?, totalMessages = totalMessages + 1 WHERE userId = ? AND guildId = ?',
[now, message.author.id, message.guild.id]
);
} else {
// Cooldown actif, mettre à jour seulement totalMessages (sans XP)
await db.query(
'UPDATE user_xp SET totalMessages = totalMessages + 1 WHERE userId = ? AND guildId = ?',
[message.author.id, message.guild.id]
);
}
}
// 5. MESSAGES DE BIENVENUE
if (detectWelcomeMessage(message)) {
try {
await message.react('👋').catch(() => null);
await message.react('🎉').catch(() => null);
// Détecter les messages de bienvenue
if (detectWelcomeMessage(message)) {
try {
// Réagir avec un emoji de bienvenue
await message.react('👋').catch(() => null);
await message.react('🎉').catch(() => null);
// Donner de l'XP pour avoir dit bienvenue
const member = await message.guild.members.fetch(message.author.id).catch(() => null);
if (member) {
const multiplier = getXPMultiplier(member);
// Gain d'XP pour bienvenue : 20-40 XP
const xpGained = Math.floor(Math.random() * 21) + 20; // 20-40 XP
const result = await addXP(message.author.id, message.guild.id, xpGained, 'welcome', multiplier);
if (result && result.levelUp) {
const { EmbedBuilder } = require('discord.js');
const { colors } = require('../utils/constants');
const { getXPProgress } = require('../functions/xp/xp.js');
const progress = getXPProgress(result.newXP, result.newLevel);
const embed = new EmbedBuilder()
.setTitle('🎉 Level Up !')
.setDescription(`Félicitations ${message.author.toString()} ! Tu as atteint le niveau **${result.newLevel}** !`)
.setColor(colors.success)
.addFields(
{ name: '📊 XP', value: `${result.newXP} XP`, inline: true },
{ name: '⭐ Niveau', value: `${result.newLevel}`, inline: true },
{ name: '📈 Progression', value: `${progress.current}/${progress.needed} XP (${progress.percentage}%)`, inline: true }
)
.setThumbnail(message.author.displayAvatarURL({ dynamic: true }))
.setTimestamp();
await message.channel.send({ embeds: [embed] });
}
}
} catch (err) {
console.error('Erreur lors de la détection du message de bienvenue:', err);
}
}
const member = await message.guild.members.fetch(message.author.id).catch(() => null);
if (member) {
const xpGained = Math.floor(Math.random() * 21) + 20; // 20-40 XP
const result = await addXP(message.author.id, message.guild.id, xpGained, 'welcome', getXPMultiplier(member));
await handleLevelUp(message, result, member);
}
} catch (err) { console.error('Erreur bienvenue:', err); }
}
} catch (err) {
console.error('Erreur lors de l\'enregistrement du message:', err);
}
},
};
} catch (err) {
console.error('Erreur globale messageCreate:', err);
}
},
};

View File

@@ -10,6 +10,13 @@ module.exports = {
console.log(chalk.blue(`🔧 ${client.commands.size} commande(s) chargée(s)`));
// Statut du bot
client.user.setActivity('France Femboy', { type: ActivityType.Watching });
client.user.setPresence({
activities: [{
name: 'Surveille le discord Femboy Croissant 👀',
type: ActivityType.Streaming,
url: 'https://twitch.tv/femboycroissantoff'
}],
status: 'online'
});
},
};

View File

@@ -1,234 +1,143 @@
/**
* Gestion de l'XP vocal
* Gain d'XP toutes les 10 minutes en vocal (5-25 XP)
* - Timer ne démarre QUE si au moins 2 personnes dans le salon
* - Timer s'arrête si l'utilisateur est mute/deafen
* - totalVoiceTime est incrémenté chaque minute
*/
const { Events } = require('discord.js');
const { Events, EmbedBuilder } = require('discord.js');
const db = require('../functions/database/db.js');
const { addXP, getUserXP, getXPMultiplier } = require('../functions/xp/xp.js');
const { addXP, getXPMultiplier, getXPProgress } = require('../functions/xp/xp.js');
const { colors } = require('../utils/constants');
// Stocker les utilisateurs en vocal par serveur
const voiceUsers = new Map(); // Map<guildId_userId, { joinTime, interval }>
// Map<guildId_userId, { xpInterval, timeInterval }>
const voiceUsers = new Map();
function canGainXP(member) {
if (!member || !member.voice.channelId) return false;
const voiceChannel = member.voice.channel;
if (!voiceChannel) return false;
// Compter les humains
const membersInChannel = voiceChannel.members.filter(m => !m.user.bot).size;
// Conditions : Pas tout seul, pas mute, pas deaf
const isAlone = membersInChannel < 2;
const isMuted = member.voice.serverMute || member.voice.selfMute;
const isDeafened = member.voice.serverDeaf || member.voice.selfDeaf;
return !isAlone && !isMuted && !isDeafened;
}
async function startTimers(key, member, guildId, userId) {
// Si déjà des timers, on ne fait rien
if (voiceUsers.has(key)) return;
// 1. Timer XP (10 min)
const xpInterval = setInterval(async () => {
try {
// Re-vérification dynamique
const currentMember = await member.guild.members.fetch(userId).catch(() => null);
if (!currentMember || !canGainXP(currentMember)) return;
const multiplier = getXPMultiplier(currentMember, 'voice');
const xpGained = Math.floor(Math.random() * 21) + 5; // 5-25 XP
const result = await addXP(userId, guildId, xpGained, 'voice', multiplier);
if (result && result.levelUp) {
const progress = getXPProgress(result.newXP, result.newLevel);
const embed = new EmbedBuilder()
.setTitle('🎉 Level Up !')
.setDescription(`Félicitations ${currentMember.toString()} ! Tu as atteint le niveau **${result.newLevel}** en vocal !`)
.setColor(colors.success)
.addFields(
{ name: '📊 XP', value: `${result.newXP} XP`, inline: true },
{ name: '⭐ Niveau', value: `${result.newLevel}`, inline: true },
{ name: '📈 Progression', value: `${progress.current}/${progress.needed} XP (${progress.percentage}%)`, inline: true }
)
.setThumbnail(currentMember.user.displayAvatarURL({ dynamic: true }))
.setTimestamp();
const systemChannel = member.guild.systemChannel;
if (systemChannel) await systemChannel.send({ embeds: [embed] }).catch(() => null);
}
} catch (err) { console.error('Erreur Timer XP:', err); }
}, 10 * 60 * 1000);
// 2. Timer Temps (1 min)
const timeInterval = setInterval(async () => {
try {
const currentMember = await member.guild.members.fetch(userId).catch(() => null);
if (currentMember && canGainXP(currentMember)) {
await db.query('UPDATE user_xp SET totalVoiceTime = totalVoiceTime + 1 WHERE userId = ? AND guildId = ?', [userId, guildId]);
}
} catch (err) { console.error('Erreur Timer Temps:', err); }
}, 60 * 1000);
voiceUsers.set(key, { xpInterval, timeInterval });
// Mise à jour lastVoiceJoin
await db.query('UPDATE user_xp SET lastVoiceJoin = ? WHERE userId = ? AND guildId = ?', [Date.now(), userId, guildId]).catch(() => {});
}
function stopTimers(key) {
const userData = voiceUsers.get(key);
if (userData) {
clearInterval(userData.xpInterval);
clearInterval(userData.timeInterval);
voiceUsers.delete(key);
}
}
async function updateState(member) {
if (!member || member.user.bot) return;
const guildId = member.guild.id;
const userId = member.id;
const key = `${guildId}_${userId}`;
if (canGainXP(member)) {
await startTimers(key, member, guildId, userId);
} else {
stopTimers(key);
}
}
async function checkChannel(channel) {
if (!channel) return;
// Vérifier tous les membres du salon car l'arrivée/départ de quelqu'un change le statut "isAlone"
for (const [id, member] of channel.members) {
await updateState(member);
}
}
module.exports = {
name: Events.VoiceStateUpdate,
async execute(oldState, newState) {
const userId = newState.member?.id || oldState.member?.id;
const guildId = newState.guild?.id || oldState.guild?.id;
if (!userId || !guildId) return;
// Ignorer les bots
if (newState.member?.user.bot || oldState.member?.user.bot) return;
const key = `${guildId}_${userId}`;
// Utilisateur rejoint un canal vocal
if (!oldState.channelId && newState.channelId) {
// L'utilisateur a rejoint un canal vocal
const joinTime = Date.now();
// Mettre à jour lastVoiceJoin dans la DB
await db.query(
'UPDATE user_xp SET lastVoiceJoin = ? WHERE userId = ? AND guildId = ?',
[joinTime, userId, guildId]
).catch(() => {
// L'utilisateur n'existe pas encore, on le créera lors du premier gain d'XP
});
// Créer un interval pour gagner de l'XP toutes les 10 minutes
const interval = setInterval(async () => {
try {
// Récupérer le serveur et le membre
const guild = newState.guild || oldState.guild;
if (!guild) {
clearInterval(interval);
voiceUsers.delete(key);
return;
}
const member = await guild.members.fetch(userId).catch(() => null);
if (!member || !member.voice.channelId) {
// L'utilisateur n'est plus en vocal, arrêter l'interval
clearInterval(interval);
voiceUsers.delete(key);
return;
}
// Vérifier si l'utilisateur est seul (anti-farming ultra strict)
// Ne pas donner d'XP si l'utilisateur est seul dans le salon (peu importe s'il est muet ou non)
const voiceChannel = member.voice.channel;
if (voiceChannel) {
// Compter le nombre de membres dans le salon (sans les bots)
const membersInChannel = voiceChannel.members.filter(m => !m.user.bot).size;
const isAlone = membersInChannel === 1; // Seul dans le salon
// Si l'utilisateur est seul, ne pas donner d'XP (anti-farming ultra strict)
if (isAlone) {
// Ne pas donner d'XP mais continuer le timer (il pourra gagner de l'XP si quelqu'un le rejoint)
return;
}
}
// Récupérer le multiplicateur (avec source='voice' pour le bonus vocal)
const multiplier = getXPMultiplier(member, 'voice');
// Gain d'XP vocal : 5-25 XP toutes les 10 minutes
const xpGained = Math.floor(Math.random() * 21) + 5; // 5-25 XP
// Ajouter l'XP
const result = await addXP(userId, guildId, xpGained, 'voice', multiplier);
if (result) {
// Mettre à jour totalVoiceTime (en minutes)
await db.query(
'UPDATE user_xp SET totalVoiceTime = totalVoiceTime + 10 WHERE userId = ? AND guildId = ?',
[userId, guildId]
);
// Si niveau supérieur, envoyer un message (optionnel, on peut le désactiver pour éviter le spam)
if (result.levelUp) {
const { EmbedBuilder } = require('discord.js');
const { colors } = require('../utils/constants');
const { getXPProgress } = require('../functions/xp/xp.js');
const progress = getXPProgress(result.newXP, result.newLevel);
const embed = new EmbedBuilder()
.setTitle('🎉 Level Up !')
.setDescription(`Félicitations ${member.toString()} ! Tu as atteint le niveau **${result.newLevel}** en vocal !`)
.setColor(colors.success)
.addFields(
{ name: '📊 XP', value: `${result.newXP} XP`, inline: true },
{ name: '⭐ Niveau', value: `${result.newLevel}`, inline: true },
{ name: '📈 Progression', value: `${progress.current}/${progress.needed} XP (${progress.percentage}%)`, inline: true }
)
.setThumbnail(member.user.displayAvatarURL({ dynamic: true }))
.setTimestamp();
// Envoyer dans le canal système (si disponible)
const systemChannel = guild.systemChannel;
if (systemChannel) {
await systemChannel.send({ embeds: [embed] }).catch(() => null);
}
}
}
} catch (err) {
console.error('Erreur lors du gain d\'XP vocal:', err);
}
}, 10 * 60 * 1000); // 10 minutes
voiceUsers.set(key, { joinTime, interval });
}
// Utilisateur quitte un canal vocal
if (oldState.channelId && !newState.channelId) {
// L'utilisateur a quitté le canal vocal
const userData = voiceUsers.get(key);
if (userData) {
clearInterval(userData.interval);
voiceUsers.delete(key);
// Calculer le temps passé en vocal et mettre à jour totalVoiceTime
const timeSpent = Math.floor((Date.now() - userData.joinTime) / 1000 / 60); // en minutes
if (timeSpent > 0) {
await db.query(
'UPDATE user_xp SET totalVoiceTime = totalVoiceTime + ? WHERE userId = ? AND guildId = ?',
[timeSpent, userId, guildId]
).catch(() => null);
}
}
}
// Utilisateur change de canal vocal
if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) {
// L'utilisateur a changé de canal vocal
// On récupère les données existantes et on met à jour le joinTime
const userData = voiceUsers.get(key);
if (userData) {
// Mettre à jour le joinTime pour le nouveau canal
userData.joinTime = Date.now();
// L'interval continue, pas besoin de le redémarrer
} else {
// Si pour une raison quelconque les données n'existent pas, créer un nouvel interval
// (ce cas ne devrait normalement pas arriver)
const joinTime = Date.now();
await db.query(
'UPDATE user_xp SET lastVoiceJoin = ? WHERE userId = ? AND guildId = ?',
[joinTime, userId, guildId]
).catch(() => null);
const interval = setInterval(async () => {
try {
const guild = newState.guild || oldState.guild;
if (!guild) {
clearInterval(interval);
voiceUsers.delete(key);
return;
}
const member = await guild.members.fetch(userId).catch(() => null);
if (!member || !member.voice.channelId) {
clearInterval(interval);
voiceUsers.delete(key);
return;
}
// Vérifier si l'utilisateur est seul (anti-farming ultra strict)
const voiceChannel = member.voice.channel;
if (voiceChannel) {
const membersInChannel = voiceChannel.members.filter(m => !m.user.bot).size;
const isAlone = membersInChannel === 1;
// Si l'utilisateur est seul, ne pas donner d'XP
if (isAlone) {
return;
}
}
const multiplier = getXPMultiplier(member, 'voice');
const xpGained = Math.floor(Math.random() * 21) + 5;
const result = await addXP(userId, guildId, xpGained, 'voice', multiplier);
if (result) {
await db.query(
'UPDATE user_xp SET totalVoiceTime = totalVoiceTime + 10 WHERE userId = ? AND guildId = ?',
[userId, guildId]
);
if (result.levelUp) {
const { EmbedBuilder } = require('discord.js');
const { colors } = require('../utils/constants');
const { getXPProgress } = require('../functions/xp/xp.js');
const progress = getXPProgress(result.newXP, result.newLevel);
const embed = new EmbedBuilder()
.setTitle('🎉 Level Up !')
.setDescription(`Félicitations ${member.toString()} ! Tu as atteint le niveau **${result.newLevel}** en vocal !`)
.setColor(colors.success)
.addFields(
{ name: '📊 XP', value: `${result.newXP} XP`, inline: true },
{ name: '⭐ Niveau', value: `${result.newLevel}`, inline: true },
{ name: '📈 Progression', value: `${progress.current}/${progress.needed} XP (${progress.percentage}%)`, inline: true }
)
.setThumbnail(member.user.displayAvatarURL({ dynamic: true }))
.setTimestamp();
const systemChannel = guild.systemChannel;
if (systemChannel) {
await systemChannel.send({ embeds: [embed] }).catch(() => null);
}
}
}
} catch (err) {
console.error('Erreur lors du gain d\'XP vocal:', err);
}
}, 10 * 60 * 1000);
voiceUsers.set(key, { joinTime, interval });
}
}
},
};
name: Events.VoiceStateUpdate,
async execute(oldState, newState) {
try {
// Cas 1: Join
if (!oldState.channelId && newState.channelId) {
await checkChannel(newState.channel);
}
// Cas 2: Leave
else if (oldState.channelId && !newState.channelId) {
const member = oldState.member;
const key = `${member.guild.id}_${member.id}`;
stopTimers(key); // Arrêt immédiat pour celui qui part
await checkChannel(oldState.channel); // Vérif pour ceux qui restent
}
// Cas 3: Move
else if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) {
await checkChannel(oldState.channel);
await checkChannel(newState.channel);
}
// Cas 4: Mute/Deaf update (Même channel)
else if (oldState.channelId === newState.channelId) {
await updateState(newState.member);
}
} catch (err) {
console.error('Erreur VoiceStateUpdate:', err);
}
},
};