const { EmbedBuilder, PermissionFlagsBits } = require('discord.js'); const db = require('../db'); // Cache pour le tracking const spamTracker = new Map(); // guildId_oderId -> [timestamps] const dupeTracker = new Map(); // guildId_oderId -> [{content, timestamp}] const joinTracker = new Map(); // guildId -> [timestamps] const warningTracker = new Map(); // guildId_oderId_type -> count /** * Récupère la config anti-raid d'un serveur */ async function getConfig(guildId) { try { return await db.getAsync("SELECT * FROM antiraid_config WHERE guild_id = ?", [guildId]); } catch (err) { console.error('Erreur récupération config antiraid:', err); return null; } } /** * Vérifie si un membre est exclu (rôle ou salon) */ function isExcluded(member, channelId, excludeChannels, excludeRoles) { try { const channels = JSON.parse(excludeChannels || '[]'); const roles = JSON.parse(excludeRoles || '[]'); if (channels.includes(channelId)) return true; if (member && roles.some(roleId => member.roles.cache.has(roleId))) return true; return false; } catch { return false; } } /** * Envoie un log dans le salon de logs anti-raid */ async function sendLog(client, guildId, config, embed) { if (!config.log_channel_id) return; try { const guild = client.guilds.cache.get(guildId); const channel = guild?.channels.cache.get(config.log_channel_id); if (channel) { await channel.send({ embeds: [embed] }); } } catch (err) { console.error('Erreur envoi log antiraid:', err); } } /** * Applique une action sur un membre */ async function applyAction(member, action, reason, duration = 10) { try { switch (action) { case 'delete': // Juste supprimer le message, pas d'action sur le membre return 'deleted'; case 'warn': // Avertissement simple return 'warned'; case 'mute': case 'timeout': await member.timeout(duration * 60 * 1000, reason); return 'muted'; case 'kick': await member.kick(reason); return 'kicked'; case 'ban': await member.ban({ reason, deleteMessageSeconds: 86400 }); return 'banned'; default: return 'none'; } } catch (err) { console.error('Erreur application action antiraid:', err); return 'error'; } } /** * Crée un embed de log */ function createLogEmbed(type, user, reason, action, details = {}) { const colors = { link: 0x3498DB, invite: 0x9B59B6, spam: 0xE74C3C, duplicate: 0xE67E22, mention: 0xF1C40F, emoji: 0x1ABC9C, caps: 0x95A5A6, newline: 0x2ECC71, bot: 0xE91E63, massjoin: 0x9B59B6, badwords: 0xE74C3C }; const titles = { link: '🔗 Anti-Link', invite: '📨 Anti-Invite', spam: '⚡ Anti-Spam', duplicate: '📋 Anti-Duplicate', mention: '📢 Anti-Mention', emoji: '😀 Anti-Emoji', caps: '🔠 Anti-Caps', newline: '📄 Anti-Newline', bot: '🤖 Anti-Bot', massjoin: '👥 Anti-Mass Join', badwords: '🤬 Anti-Gros Mots' }; const embed = new EmbedBuilder() .setColor(colors[type] || 0xED4245) .setTitle(titles[type] || '🛡️ Anti-Raid') .setDescription(reason) .addFields( { name: '👤 Utilisateur', value: `${user} (${user.tag || user.username})`, inline: true }, { name: '⚡ Action', value: action, inline: true } ) .setThumbnail(user.displayAvatarURL?.({ size: 64 }) || null) .setTimestamp(); if (details.content) { embed.addFields({ name: '💬 Contenu', value: details.content.substring(0, 1024), inline: false }); } if (details.channel) { embed.addFields({ name: '📁 Salon', value: `<#${details.channel}>`, inline: true }); } return embed; } // ===== ANTI-LINK ===== async function checkAntiLink(message, config) { if (!config.antilink_enabled) return false; if (isExcluded(message.member, message.channel.id, config.antilink_exclude_channels, config.antilink_exclude_roles)) return false; if (message.member?.permissions.has(PermissionFlagsBits.ManageMessages)) return false; const urlRegex = /https?:\/\/[^\s]+/gi; const links = message.content.match(urlRegex); if (!links) return false; const whitelist = JSON.parse(config.antilink_whitelist_domains || '[]'); const hasBlockedLink = links.some(link => { try { const url = new URL(link); return !whitelist.some(domain => url.hostname.includes(domain)); } catch { return true; } }); if (!hasBlockedLink) return false; await message.delete().catch(() => {}); if (config.antilink_warn_message) { const warnMsg = await message.channel.send(`${message.author} ${config.antilink_warn_message}`); setTimeout(() => warnMsg.delete().catch(() => {}), 5000); } const actionResult = await applyAction(message.member, config.antilink_action, 'Anti-Link'); return { type: 'link', action: actionResult, content: message.content }; } // ===== ANTI-INVITE ===== async function checkAntiInvite(message, config, client) { if (!config.antiinvite_enabled) return false; if (isExcluded(message.member, message.channel.id, config.antiinvite_exclude_channels, config.antiinvite_exclude_roles)) return false; if (message.member?.permissions.has(PermissionFlagsBits.ManageGuild)) return false; const inviteRegex = /(discord\.(gg|io|me|li)|discordapp\.com\/invite|discord\.com\/invite)\/[a-zA-Z0-9]+/gi; const invites = message.content.match(inviteRegex); if (!invites) return false; // Vérifier si c'est une invite de ce serveur if (config.antiinvite_allow_own_server) { try { const guildInvites = await message.guild.invites.fetch(); const ownInviteCodes = guildInvites.map(i => i.code); const hasExternalInvite = invites.some(invite => { const code = invite.split('/').pop(); return !ownInviteCodes.includes(code); }); if (!hasExternalInvite) return false; } catch { // Si on ne peut pas fetch les invites, bloquer par défaut } } await message.delete().catch(() => {}); const warnMsg = await message.channel.send(`${message.author} ⚠️ Les invitations Discord ne sont pas autorisées.`); setTimeout(() => warnMsg.delete().catch(() => {}), 5000); const actionResult = await applyAction(message.member, config.antiinvite_action, 'Anti-Invite'); return { type: 'invite', action: actionResult, content: message.content }; } // ===== ANTI-SPAM ===== async function checkAntiSpam(message, config) { if (!config.antispam_enabled) return false; if (isExcluded(message.member, message.channel.id, config.antispam_exclude_channels, config.antispam_exclude_roles)) return false; if (message.member?.permissions.has(PermissionFlagsBits.ManageMessages)) return false; const key = `${message.guild.id}_${message.author.id}`; const now = Date.now(); const interval = config.antispam_interval_seconds * 1000; if (!spamTracker.has(key)) { spamTracker.set(key, []); } const timestamps = spamTracker.get(key).filter(t => now - t < interval); timestamps.push(now); spamTracker.set(key, timestamps); if (timestamps.length < config.antispam_max_messages) return false; // Spam détecté spamTracker.delete(key); // Supprimer les messages récents de l'utilisateur try { const messages = await message.channel.messages.fetch({ limit: 20 }); const userMessages = messages.filter(m => m.author.id === message.author.id && now - m.createdTimestamp < interval); await message.channel.bulkDelete(userMessages).catch(() => {}); } catch {} const actionResult = await applyAction(message.member, config.antispam_action, 'Anti-Spam', config.antispam_mute_duration_minutes); return { type: 'spam', action: actionResult, content: `${timestamps.length} messages en ${config.antispam_interval_seconds}s` }; } // ===== ANTI-DUPLICATE ===== async function checkAntiDuplicate(message, config) { if (!config.antidupe_enabled) return false; if (isExcluded(message.member, message.channel.id, config.antidupe_exclude_channels, config.antidupe_exclude_roles)) return false; if (message.member?.permissions.has(PermissionFlagsBits.ManageMessages)) return false; if (message.content.length < 5) return false; const key = `${message.guild.id}_${message.author.id}`; const now = Date.now(); const interval = config.antidupe_interval_seconds * 1000; if (!dupeTracker.has(key)) { dupeTracker.set(key, []); } const history = dupeTracker.get(key).filter(h => now - h.timestamp < interval); const duplicates = history.filter(h => h.content === message.content).length; history.push({ content: message.content, timestamp: now }); dupeTracker.set(key, history.slice(-20)); if (duplicates < config.antidupe_max_duplicates - 1) return false; await message.delete().catch(() => {}); const warnMsg = await message.channel.send(`${message.author} ⚠️ Arrêtez de répéter le même message.`); setTimeout(() => warnMsg.delete().catch(() => {}), 5000); const actionResult = await applyAction(message.member, config.antidupe_action, 'Anti-Duplicate'); return { type: 'duplicate', action: actionResult, content: message.content }; } // ===== ANTI-MASS MENTION ===== async function checkAntiMention(message, config) { if (!config.antimention_enabled) return false; if (isExcluded(message.member, message.channel.id, config.antimention_exclude_channels, config.antimention_exclude_roles)) return false; if (message.member?.permissions.has(PermissionFlagsBits.MentionEveryone)) return false; const mentionCount = message.mentions.users.size + message.mentions.roles.size; if (mentionCount < config.antimention_max_mentions) return false; await message.delete().catch(() => {}); const warnMsg = await message.channel.send(`${message.author} ⚠️ Trop de mentions dans votre message.`); setTimeout(() => warnMsg.delete().catch(() => {}), 5000); const actionResult = await applyAction(message.member, config.antimention_action, 'Anti-Mass Mention'); return { type: 'mention', action: actionResult, content: `${mentionCount} mentions` }; } // ===== ANTI-EMOJI ===== async function checkAntiEmoji(message, config) { if (!config.antiemoji_enabled) return false; if (isExcluded(message.member, message.channel.id, config.antiemoji_exclude_channels, config.antiemoji_exclude_roles)) return false; if (message.member?.permissions.has(PermissionFlagsBits.ManageMessages)) return false; const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]|)/g; const emojis = message.content.match(emojiRegex); if (!emojis || emojis.length < config.antiemoji_max_emojis) return false; await message.delete().catch(() => {}); const warnMsg = await message.channel.send(`${message.author} ⚠️ Trop d'emojis dans votre message.`); setTimeout(() => warnMsg.delete().catch(() => {}), 5000); const actionResult = await applyAction(message.member, config.antiemoji_action, 'Anti-Emoji'); return { type: 'emoji', action: actionResult, content: `${emojis.length} emojis` }; } // ===== ANTI-CAPS ===== async function checkAntiCaps(message, config) { if (!config.anticaps_enabled) return false; if (isExcluded(message.member, message.channel.id, config.anticaps_exclude_channels, config.anticaps_exclude_roles)) return false; if (message.member?.permissions.has(PermissionFlagsBits.ManageMessages)) return false; const text = message.content.replace(/[^a-zA-Z]/g, ''); if (text.length < config.anticaps_min_length) return false; const capsCount = (text.match(/[A-Z]/g) || []).length; const capsPercent = (capsCount / text.length) * 100; if (capsPercent < config.anticaps_max_percent) return false; await message.delete().catch(() => {}); const warnMsg = await message.channel.send(`${message.author} ⚠️ Trop de majuscules dans votre message.`); setTimeout(() => warnMsg.delete().catch(() => {}), 5000); const actionResult = await applyAction(message.member, config.anticaps_action, 'Anti-Caps'); return { type: 'caps', action: actionResult, content: `${Math.round(capsPercent)}% majuscules` }; } // ===== ANTI-NEWLINE ===== async function checkAntiNewline(message, config) { if (!config.antinewline_enabled) return false; if (isExcluded(message.member, message.channel.id, config.antinewline_exclude_channels, config.antinewline_exclude_roles)) return false; if (message.member?.permissions.has(PermissionFlagsBits.ManageMessages)) return false; const lines = message.content.split('\n').length; if (lines < config.antinewline_max_lines) return false; await message.delete().catch(() => {}); const warnMsg = await message.channel.send(`${message.author} ⚠️ Trop de lignes dans votre message.`); setTimeout(() => warnMsg.delete().catch(() => {}), 5000); const actionResult = await applyAction(message.member, config.antinewline_action, 'Anti-Newline'); return { type: 'newline', action: actionResult, content: `${lines} lignes` }; } // ===== ANTI-BADWORDS ===== async function checkAntiBadwords(message, config) { if (!config.antibadwords_enabled) return false; if (isExcluded(message.member, message.channel.id, config.antibadwords_exclude_channels, config.antibadwords_exclude_roles)) return false; if (message.member?.permissions.has(PermissionFlagsBits.ManageMessages)) return false; let badwordsList = []; try { badwordsList = JSON.parse(config.antibadwords_words || '[]'); } catch { return false; } if (badwordsList.length === 0) return false; // Normaliser le message (enlever accents, mettre en minuscules) const normalizedContent = message.content .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9\s]/g, ' '); // Vérifier chaque gros mot const foundBadword = badwordsList.find(word => { const normalizedWord = word.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); // Vérifier le mot entier ou avec des variations const regex = new RegExp(`\\b${normalizedWord}\\b|${normalizedWord.split('').join('[^a-z]*')}`, 'i'); return regex.test(normalizedContent); }); if (!foundBadword) return false; await message.delete().catch(() => {}); if (config.antibadwords_warn_message) { const warnMsg = await message.channel.send(`${message.author} ${config.antibadwords_warn_message}`); setTimeout(() => warnMsg.delete().catch(() => {}), 5000); } const actionResult = await applyAction(message.member, config.antibadwords_action, 'Anti-Gros Mots'); return { type: 'badwords', action: actionResult, content: '[Contenu censuré]' }; } // ===== ANTI-BOT JOIN ===== async function checkAntiBot(member, config) { if (!config.antibot_enabled) return false; const now = Date.now(); const accountAge = Math.floor((now - member.user.createdTimestamp) / (1000 * 60 * 60 * 24)); const reasons = []; // Compte trop récent if (accountAge < config.antibot_min_account_age_days) { reasons.push(`Compte créé il y a ${accountAge} jours (min: ${config.antibot_min_account_age_days})`); } // Pas d'avatar if (config.antibot_no_avatar_action && !member.user.avatar) { reasons.push('Pas d\'avatar'); } // Nom suspect (caractères bizarres, pattern de bot) if (config.antibot_suspicious_name_action) { const suspiciousPattern = /^[a-z]{4,8}\d{4}$/i; // Pattern comme "user1234" const weirdChars = /[\u200B-\u200D\uFEFF]/; // Caractères invisibles if (suspiciousPattern.test(member.user.username) || weirdChars.test(member.user.username)) { reasons.push('Nom suspect'); } } if (reasons.length === 0) return false; const actionResult = await applyAction(member, config.antibot_action, 'Anti-Bot: ' + reasons.join(', ')); return { type: 'bot', action: actionResult, reasons }; } // ===== ANTI-MASS JOIN ===== async function checkAntiMassJoin(member, config) { if (!config.antimassj_enabled) return false; const guildId = member.guild.id; const now = Date.now(); const interval = config.antimassj_interval_seconds * 1000; if (!joinTracker.has(guildId)) { joinTracker.set(guildId, []); } const joins = joinTracker.get(guildId).filter(t => now - t < interval); joins.push(now); joinTracker.set(guildId, joins); if (joins.length < config.antimassj_max_joins) return false; // Raid détecté - appliquer l'action sur ce membre const actionResult = await applyAction(member, config.antimassj_action, 'Anti-Mass Join: Raid détecté'); return { type: 'massjoin', action: actionResult, joinCount: joins.length }; } /** * Vérifie tous les filtres anti-raid pour un message */ async function checkMessage(message, client) { if (!message.guild) return; if (message.author.bot) return; const config = await getConfig(message.guild.id); if (!config || !config.enabled) return; // Vérifier chaque filtre const checks = [ checkAntiLink(message, config), checkAntiInvite(message, config, client), checkAntiSpam(message, config), checkAntiDuplicate(message, config), checkAntiMention(message, config), checkAntiEmoji(message, config), checkAntiCaps(message, config), checkAntiNewline(message, config), checkAntiBadwords(message, config) ]; for (const check of checks) { const result = await check; if (result) { // Toujours ajouter un warn automatique lors d'une violation anti-raid await addAutoWarn(message.guild.id, message.author.id, client.user.id, `Anti-Raid: ${result.type}`, result.type, client); const embed = createLogEmbed(result.type, message.author, `Violation détectée`, result.action, { content: result.content, channel: message.channel.id }); await sendLog(client, message.guild.id, config, embed); break; // Une seule action par message } } } /** * Ajoute un warn automatique et vérifie les sanctions */ async function addAutoWarn(guildId, userId, moderatorId, reason, source, client) { try { // Ajouter le warn await new Promise((resolve, reject) => { db.run( "INSERT INTO warnings (guild_id, user_id, moderator_id, reason, source) VALUES (?, ?, ?, ?, ?)", [guildId, userId, moderatorId, reason, source], function(err) { if (err) reject(err); else resolve(this.lastID); } ); }); // Vérifier les sanctions automatiques await checkWarningSanctions(guildId, userId, client); } catch (err) { console.error('Erreur ajout auto warn:', err); } } /** * Vérifie et applique les sanctions automatiques basées sur le nombre de warns */ async function checkWarningSanctions(guildId, userId, client) { try { // Récupérer la config des warns const warnConfig = await db.getAsync("SELECT * FROM warnings_config WHERE guild_id = ?", [guildId]); if (!warnConfig || !warnConfig.enabled) return; // Compter les warns actifs (avec decay si activé) let countQuery = "SELECT COUNT(*) as count FROM warnings WHERE guild_id = ? AND user_id = ?"; const params = [guildId, userId]; if (warnConfig.decay_enabled && warnConfig.decay_days > 0) { const decayTimestamp = Math.floor(Date.now() / 1000) - (warnConfig.decay_days * 86400); countQuery += " AND created_at > ?"; params.push(decayTimestamp); } const result = await db.getAsync(countQuery, params); const warnCount = result.count; // Déterminer l'action à appliquer let action = 'none'; let duration = 0; if (warnCount >= 5 && warnConfig.warn5_action !== 'none') { action = warnConfig.warn5_action; duration = warnConfig.warn5_duration; } else if (warnCount >= 4 && warnConfig.warn4_action !== 'none') { action = warnConfig.warn4_action; duration = warnConfig.warn4_duration; } else if (warnCount >= 3 && warnConfig.warn3_action !== 'none') { action = warnConfig.warn3_action; duration = warnConfig.warn3_duration; } else if (warnCount >= 2 && warnConfig.warn2_action !== 'none') { action = warnConfig.warn2_action; duration = warnConfig.warn2_duration; } else if (warnCount >= 1 && warnConfig.warn1_action !== 'none') { action = warnConfig.warn1_action; duration = warnConfig.warn1_duration; } if (action === 'none') return; // Appliquer la sanction const guild = client.guilds.cache.get(guildId); if (!guild) return; const member = await guild.members.fetch(userId).catch(() => null); if (!member) return; const reason = `Sanction automatique: ${warnCount} avertissement(s)`; switch (action) { case 'mute': await member.timeout(duration * 60 * 1000, reason).catch(() => {}); break; case 'kick': await member.kick(reason).catch(() => {}); break; case 'ban': await member.ban({ reason, deleteMessageSeconds: 86400 }).catch(() => {}); break; } // Notifier si configuré if (warnConfig.notify_channel_id) { const channel = guild.channels.cache.get(warnConfig.notify_channel_id); if (channel) { const embed = new EmbedBuilder() .setColor(0xED4245) .setTitle('⚠️ Sanction automatique') .setDescription(`**${member.user.tag}** a reçu une sanction automatique.`) .addFields( { name: '👤 Utilisateur', value: `${member}`, inline: true }, { name: '📊 Avertissements', value: `${warnCount}`, inline: true }, { name: '⚡ Action', value: action, inline: true } ) .setTimestamp(); await channel.send({ embeds: [embed] }).catch(() => {}); } } } catch (err) { console.error('Erreur check warning sanctions:', err); } } /** * Vérifie les filtres anti-raid pour un nouveau membre */ async function checkMemberJoin(member, client) { if (member.user.bot) return; const config = await getConfig(member.guild.id); if (!config || !config.enabled) return; // Anti-bot const botResult = await checkAntiBot(member, config); if (botResult) { // Ajouter un warn automatique await addAutoWarn(member.guild.id, member.user.id, client.user.id, `Anti-Raid: Compte suspect (${botResult.reasons.join(', ')})`, 'antibot', client); const embed = createLogEmbed('bot', member.user, `Compte suspect détecté: ${botResult.reasons.join(', ')}`, botResult.action); await sendLog(client, member.guild.id, config, embed); } // Anti-mass join const massJoinResult = await checkAntiMassJoin(member, config); if (massJoinResult) { // Ajouter un warn automatique await addAutoWarn(member.guild.id, member.user.id, client.user.id, `Anti-Raid: Mass join détecté`, 'massjoin', client); const embed = createLogEmbed('massjoin', member.user, `Raid potentiel détecté: ${massJoinResult.joinCount} joins rapides`, massJoinResult.action); await sendLog(client, member.guild.id, config, embed); } } // Nettoyage périodique des trackers setInterval(() => { const now = Date.now(); const maxAge = 5 * 60 * 1000; // 5 minutes for (const [key, timestamps] of spamTracker) { const filtered = timestamps.filter(t => now - t < maxAge); if (filtered.length === 0) spamTracker.delete(key); else spamTracker.set(key, filtered); } for (const [key, history] of dupeTracker) { const filtered = history.filter(h => now - h.timestamp < maxAge); if (filtered.length === 0) dupeTracker.delete(key); else dupeTracker.set(key, filtered); } for (const [key, joins] of joinTracker) { const filtered = joins.filter(t => now - t < maxAge); if (filtered.length === 0) joinTracker.delete(key); else joinTracker.set(key, filtered); } }, 60000); module.exports = { getConfig, checkMessage, checkMemberJoin, createLogEmbed, sendLog, addAutoWarn, checkWarningSanctions };