diff --git a/app/commands/🛡️ Modération/clearwarns.js b/app/commands/🛡️ Modération/clearwarns.js new file mode 100644 index 0000000..3e3d0d6 --- /dev/null +++ b/app/commands/🛡️ Modération/clearwarns.js @@ -0,0 +1,123 @@ +const { EmbedBuilder } = require('discord.js'); +const addCommand = require('../../fonctions/addCommand'); +const db = require('../../db'); + +module.exports = addCommand({ + name: 'clearwarns', + description: 'Supprimer tous les avertissements d\'un utilisateur', + aliases: ['resetwarns', 'warnreset', 'warnclear'], + permissions: ['Administrator'], + botOwnerOnly: false, + dm: false, + scope: 'global', + slashOptions: [ + { type: 'USER', name: 'utilisateur', description: 'L\'utilisateur dont vous voulez effacer les warns', required: true } + ], + + executeSlash: async (client, interaction) => { + const user = interaction.options.getUser('utilisateur'); + const guildId = interaction.guild.id; + + try { + // Compter les warns avant suppression + const countResult = await db.getAsync( + "SELECT COUNT(*) as count FROM warnings WHERE guild_id = ? AND user_id = ?", + [guildId, user.id] + ); + + if (countResult.count === 0) { + return interaction.reply({ + content: `✅ **${user.tag}** n'a aucun avertissement.`, + ephemeral: true + }); + } + + // Supprimer tous les warns + await new Promise((resolve, reject) => { + db.run( + "DELETE FROM warnings WHERE guild_id = ? AND user_id = ?", + [guildId, user.id], + function(err) { + if (err) reject(err); + else resolve(); + } + ); + }); + + const embed = new EmbedBuilder() + .setColor(0x57F287) + .setTitle('✅ Avertissements effacés') + .setDescription(`Tous les avertissements de **${user.tag}** ont été supprimés.`) + .addFields( + { name: '👤 Utilisateur', value: `${user}`, inline: true }, + { name: '🗑️ Warns supprimés', value: `${countResult.count}`, inline: true }, + { name: '👮 Par', value: `${interaction.user}`, inline: true } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + } catch (err) { + console.error('Erreur clearwarns:', err); + return interaction.reply({ + content: '❌ Une erreur est survenue.', + ephemeral: true + }); + } + }, + + executePrefix: async (client, message, args) => { + if (args.length < 1) { + return message.reply('❌ Usage: `!clearwarns <@utilisateur>`'); + } + + const userId = args[0].replace(/[<@!>]/g, ''); + const user = await client.users.fetch(userId).catch(() => null); + const guildId = message.guild.id; + + if (!user) { + return message.reply('❌ Utilisateur non trouvé.'); + } + + try { + // Compter les warns avant suppression + const countResult = await db.getAsync( + "SELECT COUNT(*) as count FROM warnings WHERE guild_id = ? AND user_id = ?", + [guildId, user.id] + ); + + if (countResult.count === 0) { + return message.reply(`✅ **${user.tag}** n'a aucun avertissement.`); + } + + // Supprimer tous les warns + await new Promise((resolve, reject) => { + db.run( + "DELETE FROM warnings WHERE guild_id = ? AND user_id = ?", + [guildId, user.id], + function(err) { + if (err) reject(err); + else resolve(); + } + ); + }); + + const embed = new EmbedBuilder() + .setColor(0x57F287) + .setTitle('✅ Avertissements effacés') + .setDescription(`Tous les avertissements de **${user.tag}** ont été supprimés.`) + .addFields( + { name: '👤 Utilisateur', value: `${user}`, inline: true }, + { name: '🗑️ Warns supprimés', value: `${countResult.count}`, inline: true }, + { name: '👮 Par', value: `${message.author}`, inline: true } + ) + .setTimestamp(); + + await message.reply({ embeds: [embed] }); + + } catch (err) { + console.error('Erreur clearwarns:', err); + return message.reply('❌ Une erreur est survenue.'); + } + } +}); diff --git a/app/commands/🛡️ Modération/delwarn.js b/app/commands/🛡️ Modération/delwarn.js new file mode 100644 index 0000000..087c737 --- /dev/null +++ b/app/commands/🛡️ Modération/delwarn.js @@ -0,0 +1,116 @@ +const { EmbedBuilder } = require('discord.js'); +const addCommand = require('../../fonctions/addCommand'); +const db = require('../../db'); + +module.exports = addCommand({ + name: 'delwarn', + description: 'Supprimer un avertissement', + aliases: ['removewarn', 'unwarn', 'clearwarn'], + permissions: ['ModerateMembers'], + botOwnerOnly: false, + dm: false, + scope: 'global', + slashOptions: [ + { type: 'INTEGER', name: 'id', description: 'L\'ID de l\'avertissement à supprimer', required: true } + ], + + executeSlash: async (client, interaction) => { + const warnId = interaction.options.getInteger('id'); + const guildId = interaction.guild.id; + + try { + // Vérifier que le warn existe + const warn = await db.getAsync( + "SELECT * FROM warnings WHERE id = ? AND guild_id = ?", + [warnId, guildId] + ); + + if (!warn) { + return interaction.reply({ + content: '❌ Avertissement non trouvé.', + ephemeral: true + }); + } + + // Supprimer le warn + await new Promise((resolve, reject) => { + db.run("DELETE FROM warnings WHERE id = ?", [warnId], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + + const user = await client.users.fetch(warn.user_id).catch(() => null); + + const embed = new EmbedBuilder() + .setColor(0x57F287) + .setTitle('✅ Avertissement supprimé') + .addFields( + { name: '🆔 Warn ID', value: `#${warnId}`, inline: true }, + { name: '👤 Utilisateur', value: user ? `${user.tag}` : warn.user_id, inline: true }, + { name: '📝 Raison originale', value: warn.reason, inline: false } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + } catch (err) { + console.error('Erreur delwarn:', err); + return interaction.reply({ + content: '❌ Une erreur est survenue.', + ephemeral: true + }); + } + }, + + executePrefix: async (client, message, args) => { + if (args.length < 1) { + return message.reply('❌ Usage: `!delwarn `'); + } + + const warnId = parseInt(args[0]); + const guildId = message.guild.id; + + if (isNaN(warnId)) { + return message.reply('❌ L\'ID doit être un nombre.'); + } + + try { + // Vérifier que le warn existe + const warn = await db.getAsync( + "SELECT * FROM warnings WHERE id = ? AND guild_id = ?", + [warnId, guildId] + ); + + if (!warn) { + return message.reply('❌ Avertissement non trouvé.'); + } + + // Supprimer le warn + await new Promise((resolve, reject) => { + db.run("DELETE FROM warnings WHERE id = ?", [warnId], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + + const user = await client.users.fetch(warn.user_id).catch(() => null); + + const embed = new EmbedBuilder() + .setColor(0x57F287) + .setTitle('✅ Avertissement supprimé') + .addFields( + { name: '🆔 Warn ID', value: `#${warnId}`, inline: true }, + { name: '👤 Utilisateur', value: user ? `${user.tag}` : warn.user_id, inline: true }, + { name: '📝 Raison originale', value: warn.reason, inline: false } + ) + .setTimestamp(); + + await message.reply({ embeds: [embed] }); + + } catch (err) { + console.error('Erreur delwarn:', err); + return message.reply('❌ Une erreur est survenue.'); + } + } +}); diff --git a/app/commands/🛡️ Modération/warn.js b/app/commands/🛡️ Modération/warn.js new file mode 100644 index 0000000..de88e77 --- /dev/null +++ b/app/commands/🛡️ Modération/warn.js @@ -0,0 +1,174 @@ +const { EmbedBuilder, PermissionFlagsBits } = require('discord.js'); +const addCommand = require('../../fonctions/addCommand'); +const db = require('../../db'); +const { checkWarningSanctions } = require('../../fonctions/antiraid'); + +module.exports = addCommand({ + name: 'warn', + description: 'Avertir un utilisateur', + aliases: ['avertir', 'avertissement'], + permissions: ['ModerateMembers'], + botOwnerOnly: false, + dm: false, + scope: 'global', + slashOptions: [ + { type: 'USER', name: 'utilisateur', description: 'L\'utilisateur à avertir', required: true }, + { type: 'STRING', name: 'raison', description: 'La raison de l\'avertissement', required: true } + ], + + executeSlash: async (client, interaction) => { + const user = interaction.options.getUser('utilisateur'); + const reason = interaction.options.getString('raison'); + const moderator = interaction.user; + const guildId = interaction.guild.id; + + // Vérifier qu'on ne peut pas warn un admin/mod + const member = await interaction.guild.members.fetch(user.id).catch(() => null); + if (member && member.permissions.has(PermissionFlagsBits.ModerateMembers)) { + return interaction.reply({ + content: '❌ Vous ne pouvez pas avertir un modérateur.', + ephemeral: true + }); + } + + try { + // Ajouter le warn + const warnId = await new Promise((resolve, reject) => { + db.run( + "INSERT INTO warnings (guild_id, user_id, moderator_id, reason, source) VALUES (?, ?, ?, ?, ?)", + [guildId, user.id, moderator.id, reason, 'manual'], + function(err) { + if (err) reject(err); + else resolve(this.lastID); + } + ); + }); + + // Compter les warns + const countResult = await db.getAsync( + "SELECT COUNT(*) as count FROM warnings WHERE guild_id = ? AND user_id = ?", + [guildId, user.id] + ); + + const embed = new EmbedBuilder() + .setColor(0xFFA500) + .setTitle('⚠️ Avertissement') + .setDescription(`**${user.tag}** a reçu un avertissement.`) + .addFields( + { name: '👤 Utilisateur', value: `${user}`, inline: true }, + { name: '👮 Modérateur', value: `${moderator}`, inline: true }, + { name: '📊 Total warns', value: `${countResult.count}`, inline: true }, + { name: '📝 Raison', value: reason, inline: false }, + { name: '🆔 Warn ID', value: `#${warnId}`, inline: true } + ) + .setThumbnail(user.displayAvatarURL({ size: 64 })) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + + // Notifier l'utilisateur en MP + try { + const dmEmbed = new EmbedBuilder() + .setColor(0xFFA500) + .setTitle('⚠️ Vous avez reçu un avertissement') + .setDescription(`Vous avez été averti sur **${interaction.guild.name}**.`) + .addFields( + { name: '📝 Raison', value: reason }, + { name: '📊 Total avertissements', value: `${countResult.count}` } + ) + .setTimestamp(); + await user.send({ embeds: [dmEmbed] }); + } catch {} + + // Vérifier les sanctions automatiques + await checkWarningSanctions(guildId, user.id, client); + + } catch (err) { + console.error('Erreur warn:', err); + return interaction.reply({ + content: '❌ Une erreur est survenue.', + ephemeral: true + }); + } + }, + + executePrefix: async (client, message, args) => { + if (args.length < 2) { + return message.reply('❌ Usage: `!warn <@utilisateur> `'); + } + + const userMention = args[0]; + const userId = userMention.replace(/[<@!>]/g, ''); + const reason = args.slice(1).join(' '); + const moderator = message.author; + const guildId = message.guild.id; + + const user = await client.users.fetch(userId).catch(() => null); + if (!user) { + return message.reply('❌ Utilisateur non trouvé.'); + } + + // Vérifier qu'on ne peut pas warn un admin/mod + const member = await message.guild.members.fetch(user.id).catch(() => null); + if (member && member.permissions.has(PermissionFlagsBits.ModerateMembers)) { + return message.reply('❌ Vous ne pouvez pas avertir un modérateur.'); + } + + try { + // Ajouter le warn + const warnId = await new Promise((resolve, reject) => { + db.run( + "INSERT INTO warnings (guild_id, user_id, moderator_id, reason, source) VALUES (?, ?, ?, ?, ?)", + [guildId, user.id, moderator.id, reason, 'manual'], + function(err) { + if (err) reject(err); + else resolve(this.lastID); + } + ); + }); + + // Compter les warns + const countResult = await db.getAsync( + "SELECT COUNT(*) as count FROM warnings WHERE guild_id = ? AND user_id = ?", + [guildId, user.id] + ); + + const embed = new EmbedBuilder() + .setColor(0xFFA500) + .setTitle('⚠️ Avertissement') + .setDescription(`**${user.tag}** a reçu un avertissement.`) + .addFields( + { name: '👤 Utilisateur', value: `${user}`, inline: true }, + { name: '👮 Modérateur', value: `${moderator}`, inline: true }, + { name: '📊 Total warns', value: `${countResult.count}`, inline: true }, + { name: '📝 Raison', value: reason, inline: false }, + { name: '🆔 Warn ID', value: `#${warnId}`, inline: true } + ) + .setThumbnail(user.displayAvatarURL({ size: 64 })) + .setTimestamp(); + + await message.reply({ embeds: [embed] }); + + // Notifier l'utilisateur en MP + try { + const dmEmbed = new EmbedBuilder() + .setColor(0xFFA500) + .setTitle('⚠️ Vous avez reçu un avertissement') + .setDescription(`Vous avez été averti sur **${message.guild.name}**.`) + .addFields( + { name: '📝 Raison', value: reason }, + { name: '📊 Total avertissements', value: `${countResult.count}` } + ) + .setTimestamp(); + await user.send({ embeds: [dmEmbed] }); + } catch {} + + // Vérifier les sanctions automatiques + await checkWarningSanctions(guildId, user.id, client); + + } catch (err) { + console.error('Erreur warn:', err); + return message.reply('❌ Une erreur est survenue.'); + } + } +}); diff --git a/app/commands/🛡️ Modération/warnings.js b/app/commands/🛡️ Modération/warnings.js new file mode 100644 index 0000000..0bb7b0c --- /dev/null +++ b/app/commands/🛡️ Modération/warnings.js @@ -0,0 +1,144 @@ +const { EmbedBuilder, PermissionFlagsBits } = require('discord.js'); +const addCommand = require('../../fonctions/addCommand'); +const db = require('../../db'); + +module.exports = addCommand({ + name: 'warnings', + description: 'Voir les avertissements d\'un utilisateur', + aliases: ['warns', 'warninfo', 'warnlist', 'infractions'], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: 'global', + slashOptions: [ + { type: 'USER', name: 'utilisateur', description: 'L\'utilisateur dont vous voulez voir les warns (par défaut: vous)', required: false } + ], + + executeSlash: async (client, interaction) => { + const targetUser = interaction.options.getUser('utilisateur') || interaction.user; + const guildId = interaction.guild.id; + + // Si l'utilisateur veut voir les warns d'un autre, il doit être modo + if (targetUser.id !== interaction.user.id) { + const member = interaction.member; + if (!member.permissions.has(PermissionFlagsBits.ModerateMembers)) { + return interaction.reply({ + content: '❌ Vous ne pouvez voir que vos propres avertissements.', + ephemeral: true + }); + } + } + + try { + const warnings = await db.allAsync( + "SELECT * FROM warnings WHERE guild_id = ? AND user_id = ? ORDER BY created_at DESC LIMIT 25", + [guildId, targetUser.id] + ); + + if (!warnings || warnings.length === 0) { + return interaction.reply({ + content: `✅ **${targetUser.tag}** n'a aucun avertissement sur ce serveur.`, + ephemeral: true + }); + } + + const embed = new EmbedBuilder() + .setColor(0xFFA500) + .setTitle(`⚠️ Avertissements de ${targetUser.tag}`) + .setThumbnail(targetUser.displayAvatarURL({ size: 64 })) + .setDescription(`Total: **${warnings.length}** avertissement(s)`) + .setTimestamp(); + + // Ajouter les 10 derniers warns + const recentWarns = warnings.slice(0, 10); + for (const warn of recentWarns) { + const moderator = await client.users.fetch(warn.moderator_id).catch(() => null); + const date = new Date(warn.created_at * 1000).toLocaleDateString('fr-FR'); + const source = warn.source === 'manual' ? '👮 Manuel' : '🤖 Auto'; + + embed.addFields({ + name: `#${warn.id} - ${date}`, + value: `**Raison:** ${warn.reason}\n**Par:** ${moderator ? moderator.tag : 'Inconnu'} (${source})`, + inline: false + }); + } + + if (warnings.length > 10) { + embed.setFooter({ text: `Affichage des 10 derniers sur ${warnings.length} avertissements` }); + } + + await interaction.reply({ embeds: [embed], ephemeral: targetUser.id === interaction.user.id }); + + } catch (err) { + console.error('Erreur warnings:', err); + return interaction.reply({ + content: '❌ Une erreur est survenue.', + ephemeral: true + }); + } + }, + + executePrefix: async (client, message, args) => { + let targetUser; + + if (args.length > 0) { + const userId = args[0].replace(/[<@!>]/g, ''); + targetUser = await client.users.fetch(userId).catch(() => null); + + // Si l'utilisateur veut voir les warns d'un autre, il doit être modo + if (targetUser && targetUser.id !== message.author.id) { + if (!message.member.permissions.has(PermissionFlagsBits.ModerateMembers)) { + return message.reply('❌ Vous ne pouvez voir que vos propres avertissements.'); + } + } + } + + if (!targetUser) { + targetUser = message.author; + } + + const guildId = message.guild.id; + + try { + const warnings = await db.allAsync( + "SELECT * FROM warnings WHERE guild_id = ? AND user_id = ? ORDER BY created_at DESC LIMIT 25", + [guildId, targetUser.id] + ); + + if (!warnings || warnings.length === 0) { + return message.reply(`✅ **${targetUser.tag}** n'a aucun avertissement sur ce serveur.`); + } + + const embed = new EmbedBuilder() + .setColor(0xFFA500) + .setTitle(`⚠️ Avertissements de ${targetUser.tag}`) + .setThumbnail(targetUser.displayAvatarURL({ size: 64 })) + .setDescription(`Total: **${warnings.length}** avertissement(s)`) + .setTimestamp(); + + // Ajouter les 10 derniers warns + const recentWarns = warnings.slice(0, 10); + for (const warn of recentWarns) { + const moderator = await client.users.fetch(warn.moderator_id).catch(() => null); + const date = new Date(warn.created_at * 1000).toLocaleDateString('fr-FR'); + const source = warn.source === 'manual' ? '👮 Manuel' : '🤖 Auto'; + + embed.addFields({ + name: `#${warn.id} - ${date}`, + value: `**Raison:** ${warn.reason}\n**Par:** ${moderator ? moderator.tag : 'Inconnu'} (${source})`, + inline: false + }); + } + + if (warnings.length > 10) { + embed.setFooter({ text: `Affichage des 10 derniers sur ${warnings.length} avertissements` }); + } + + await message.reply({ embeds: [embed] }); + + } catch (err) { + console.error('Erreur warnings:', err); + return message.reply('❌ Une erreur est survenue.'); + } + } +}); diff --git a/app/db.js b/app/db.js index 736d9d0..6f0f6d2 100644 --- a/app/db.js +++ b/app/db.js @@ -256,6 +256,138 @@ db.exec(` CREATE INDEX IF NOT EXISTS idx_nickname_history_user ON nickname_history(guild_id, user_id, changed_at DESC); + CREATE TABLE IF NOT EXISTS antiraid_config ( + guild_id TEXT PRIMARY KEY, + enabled INTEGER NOT NULL DEFAULT 0, + log_channel_id TEXT, + + -- Anti-link + antilink_enabled INTEGER NOT NULL DEFAULT 0, + antilink_action TEXT NOT NULL DEFAULT 'delete', + antilink_whitelist_domains TEXT DEFAULT '[]', + antilink_exclude_channels TEXT DEFAULT '[]', + antilink_exclude_roles TEXT DEFAULT '[]', + antilink_warn_message TEXT DEFAULT '⚠️ Les liens ne sont pas autorisés ici.', + + -- Anti-invite (liens Discord) + antiinvite_enabled INTEGER NOT NULL DEFAULT 0, + antiinvite_action TEXT NOT NULL DEFAULT 'delete', + antiinvite_allow_own_server INTEGER NOT NULL DEFAULT 1, + antiinvite_exclude_channels TEXT DEFAULT '[]', + antiinvite_exclude_roles TEXT DEFAULT '[]', + + -- Anti-spam + antispam_enabled INTEGER NOT NULL DEFAULT 0, + antispam_action TEXT NOT NULL DEFAULT 'mute', + antispam_max_messages INTEGER NOT NULL DEFAULT 5, + antispam_interval_seconds INTEGER NOT NULL DEFAULT 5, + antispam_mute_duration_minutes INTEGER NOT NULL DEFAULT 10, + antispam_exclude_channels TEXT DEFAULT '[]', + antispam_exclude_roles TEXT DEFAULT '[]', + + -- Anti-duplicate (messages identiques) + antidupe_enabled INTEGER NOT NULL DEFAULT 0, + antidupe_action TEXT NOT NULL DEFAULT 'delete', + antidupe_max_duplicates INTEGER NOT NULL DEFAULT 3, + antidupe_interval_seconds INTEGER NOT NULL DEFAULT 60, + antidupe_exclude_channels TEXT DEFAULT '[]', + antidupe_exclude_roles TEXT DEFAULT '[]', + + -- Anti-mass mention + antimention_enabled INTEGER NOT NULL DEFAULT 0, + antimention_action TEXT NOT NULL DEFAULT 'delete', + antimention_max_mentions INTEGER NOT NULL DEFAULT 5, + antimention_exclude_channels TEXT DEFAULT '[]', + antimention_exclude_roles TEXT DEFAULT '[]', + + -- Anti-mass emoji + antiemoji_enabled INTEGER NOT NULL DEFAULT 0, + antiemoji_action TEXT NOT NULL DEFAULT 'delete', + antiemoji_max_emojis INTEGER NOT NULL DEFAULT 10, + antiemoji_exclude_channels TEXT DEFAULT '[]', + antiemoji_exclude_roles TEXT DEFAULT '[]', + + -- Anti-caps (majuscules) + anticaps_enabled INTEGER NOT NULL DEFAULT 0, + anticaps_action TEXT NOT NULL DEFAULT 'delete', + anticaps_max_percent INTEGER NOT NULL DEFAULT 70, + anticaps_min_length INTEGER NOT NULL DEFAULT 10, + anticaps_exclude_channels TEXT DEFAULT '[]', + anticaps_exclude_roles TEXT DEFAULT '[]', + + -- Anti-bot join + antibot_enabled INTEGER NOT NULL DEFAULT 0, + antibot_action TEXT NOT NULL DEFAULT 'kick', + antibot_min_account_age_days INTEGER NOT NULL DEFAULT 7, + antibot_no_avatar_action INTEGER NOT NULL DEFAULT 0, + antibot_suspicious_name_action INTEGER NOT NULL DEFAULT 0, + + -- Anti-mass join (raid de comptes) + antimassj_enabled INTEGER NOT NULL DEFAULT 0, + antimassj_action TEXT NOT NULL DEFAULT 'kick', + antimassj_max_joins INTEGER NOT NULL DEFAULT 10, + antimassj_interval_seconds INTEGER NOT NULL DEFAULT 10, + antimassj_lockdown_duration_minutes INTEGER NOT NULL DEFAULT 5, + + -- Anti-newline spam + antinewline_enabled INTEGER NOT NULL DEFAULT 0, + antinewline_action TEXT NOT NULL DEFAULT 'delete', + antinewline_max_lines INTEGER NOT NULL DEFAULT 15, + antinewline_exclude_channels TEXT DEFAULT '[]', + antinewline_exclude_roles TEXT DEFAULT '[]', + + -- Anti-badwords (gros mots) + antibadwords_enabled INTEGER NOT NULL DEFAULT 0, + antibadwords_action TEXT NOT NULL DEFAULT 'delete', + antibadwords_words TEXT DEFAULT '[]', + antibadwords_exclude_channels TEXT DEFAULT '[]', + antibadwords_exclude_roles TEXT DEFAULT '[]', + antibadwords_warn_message TEXT DEFAULT '⚠️ Les insultes et gros mots ne sont pas autorisés.' + ); + + CREATE TABLE IF NOT EXISTS antiraid_warnings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + warning_type TEXT NOT NULL, + warned_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) + ); + + CREATE INDEX IF NOT EXISTS idx_antiraid_warnings ON antiraid_warnings(guild_id, user_id, warning_type, warned_at); + + -- Table des warns utilisateurs + CREATE TABLE IF NOT EXISTS warnings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + moderator_id TEXT NOT NULL, + reason TEXT NOT NULL, + source TEXT DEFAULT 'manual', + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) + ); + + CREATE INDEX IF NOT EXISTS idx_warnings ON warnings(guild_id, user_id, created_at DESC); + + -- Configuration des sanctions automatiques par warns + CREATE TABLE IF NOT EXISTS warnings_config ( + guild_id TEXT PRIMARY KEY, + enabled INTEGER NOT NULL DEFAULT 0, + warn1_action TEXT DEFAULT 'none', + warn1_duration INTEGER DEFAULT 10, + warn2_action TEXT DEFAULT 'none', + warn2_duration INTEGER DEFAULT 30, + warn3_action TEXT DEFAULT 'mute', + warn3_duration INTEGER DEFAULT 60, + warn4_action TEXT DEFAULT 'kick', + warn4_duration INTEGER DEFAULT 0, + warn5_action TEXT DEFAULT 'ban', + warn5_duration INTEGER DEFAULT 0, + decay_enabled INTEGER NOT NULL DEFAULT 0, + decay_days INTEGER DEFAULT 30, + notify_user INTEGER NOT NULL DEFAULT 1, + notify_channel_id TEXT + ); + CREATE TABLE IF NOT EXISTS scheduled_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id TEXT NOT NULL, diff --git a/app/events/guildMemberAdd.js b/app/events/guildMemberAdd.js index cbbce00..92e2d3e 100644 --- a/app/events/guildMemberAdd.js +++ b/app/events/guildMemberAdd.js @@ -1,10 +1,14 @@ const { Events, EmbedBuilder } = require("discord.js"); const db = require("../db"); const { sendLog } = require("../fonctions/sendLog"); +const antiraid = require("../fonctions/antiraid"); module.exports = { name: Events.GuildMemberAdd, async execute(client, member) { + // ===== ANTI-RAID CHECKS ===== + await antiraid.checkMemberJoin(member, client); + // ===== LOG MEMBRE REJOINT ===== const accountAge = Math.floor((Date.now() - member.user.createdTimestamp) / (1000 * 60 * 60 * 24)); const accountAgeStr = accountAge < 1 ? 'Moins d\'un jour' : diff --git a/app/events/messageCreate.js b/app/events/messageCreate.js index ba698c9..b2c2bc1 100644 --- a/app/events/messageCreate.js +++ b/app/events/messageCreate.js @@ -1,10 +1,22 @@ const { Events, EmbedBuilder } = require("discord.js"); const db = require("../db"); +const antiraid = require("../fonctions/antiraid"); module.exports = { name: Events.MessageCreate, async execute(client, message) { if (message.author.bot) return; + if (!message.guild) return; + + // ===== ANTI-RAID CHECKS ===== + await antiraid.checkMessage(message, client); + + // Si le message a été supprimé par l'antiraid, ne pas continuer + try { + await message.fetch(); + } catch { + return; // Message supprimé + } const guildId = message.guild.id; const userId = message.author.id; diff --git a/app/fonctions/antiraid.js b/app/fonctions/antiraid.js new file mode 100644 index 0000000..5919031 --- /dev/null +++ b/app/fonctions/antiraid.js @@ -0,0 +1,657 @@ +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 +}; diff --git a/app/public/guild.css b/app/public/guild.css index 62ee5e7..45a8499 100644 --- a/app/public/guild.css +++ b/app/public/guild.css @@ -439,11 +439,11 @@ body { border-radius: var(--border-radius); font-size: 0.9rem; font-weight: 500; - display: none; + min-height: 1.5em; } -.status-message.show { - display: block; +.status-message:empty { + display: none; } .status-message.success { @@ -1166,3 +1166,123 @@ body { background: #c0392b; } +/* ===== Anti-Raid Styles ===== */ +.antiraid-modules { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.antiraid-module { + background: var(--bg-dark); + border-radius: var(--radius-md); + border: 1px solid var(--border-color); + overflow: hidden; + transition: all 0.2s ease; +} + +.antiraid-module:hover { + border-color: var(--primary-color); +} + +.antiraid-module-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-md); + background: rgba(255, 255, 255, 0.02); + cursor: pointer; + user-select: none; +} + +.antiraid-module-header:hover { + background: rgba(255, 255, 255, 0.05); +} + +.antiraid-module-title { + display: flex; + align-items: center; + gap: var(--spacing-sm); + font-weight: 600; + color: var(--text-primary); +} + +.antiraid-icon { + font-size: 1.2rem; +} + +.antiraid-module-body { + padding: var(--spacing-md); + border-top: 1px solid var(--border-color); + display: block; +} + +.antiraid-module-body .text-muted { + margin-bottom: var(--spacing-md); +} + +.form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-md); +} + +.checkbox-group { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.checkbox-label { + display: flex; + align-items: center; + gap: var(--spacing-sm); + cursor: pointer; + color: var(--text-secondary); + transition: color 0.2s; +} + +.checkbox-label:hover { + color: var(--text-primary); +} + +.checkbox-label input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; + accent-color: var(--primary-color); +} + +.multi-select { + min-height: 80px; + padding: var(--spacing-sm); +} + +.multi-select option { + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--radius-sm); + margin-bottom: 2px; +} + +.multi-select option:checked { + background: var(--primary-color); + color: white; +} + +/* Status messages */ +.status-message { + font-size: 0.9rem; + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--radius-sm); + transition: all 0.3s ease; +} + +.status-message.success { + color: #57F287; + background: rgba(87, 242, 135, 0.1); +} + +.status-message.error { + color: var(--error-color); + background: rgba(237, 66, 69, 0.1); +} diff --git a/app/public/guild.html b/app/public/guild.html index 66eddf4..0bb4a41 100644 --- a/app/public/guild.html +++ b/app/public/guild.html @@ -78,6 +78,10 @@ 📜 Logs + + 🛡️ + Anti-Raid + @@ -1177,6 +1181,723 @@ + +
+
+
+
+ 🛡️ +

Système Anti-Raid

+
+ +
+
+

+ Protégez votre serveur contre les raids, le spam et les comportements malveillants. Configurez chaque module individuellement. +

+ + +
+ + + Les actions de l'anti-raid seront envoyées dans ce salon. +
+ + +
+ + + + + +
+
+
+ 📨 + Anti-Invite +
+ +
+
+

Bloque les invitations Discord d'autres serveurs.

+ +
+
+ + +
+
+ +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ + Anti-Spam +
+ +
+
+

Détecte les utilisateurs qui envoient trop de messages rapidement.

+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ 📋 + Anti-Duplicate +
+ +
+
+

Bloque les utilisateurs qui répètent le même message.

+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ 📢 + Anti-Mass Mention +
+ +
+
+

Bloque les messages avec trop de mentions.

+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ 😀 + Anti-Emoji Spam +
+ +
+
+

Bloque les messages avec trop d'emojis.

+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ 🔠 + Anti-Caps +
+ +
+
+

Bloque les messages avec trop de MAJUSCULES.

+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ 📄 + Anti-Newline Spam +
+ +
+
+

Bloque les messages avec trop de sauts de ligne.

+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ 🤖 + Anti-Bot / Compte suspect +
+ +
+
+

Détecte les comptes suspects (nouveaux comptes, sans avatar, noms suspects).

+ +
+
+ + + 0 = désactivé +
+
+ + +
+
+
+ +
+ + +
+
+
+
+ + +
+
+
+ 👥 + Anti-Mass Join (Raid) +
+ +
+
+

Détecte quand trop de membres rejoignent en peu de temps (raid).

+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ 🤬 + Anti-Gros Mots +
+ +
+
+

Bloque les messages contenant des insultes ou gros mots.

+ +
+ + + Le système détecte aussi les variantes avec caractères spéciaux (ex: m3rde) +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ +
+ + +
+
+
+ ⚠️ +

Sanctions automatiques (Warns)

+
+ +
+
+

+ Configurez les sanctions automatiques en fonction du nombre d'avertissements. +

+ +
+ +
+
+ 1 Warn +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ 2 Warns +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ 3 Warns +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ 4 Warns +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ 5+ Warns +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+ +
+ +
+
+ + + +
+ + + Les sanctions automatiques seront notifiées dans ce salon. +
+ + +
+
+ +
+ +
+
+ @@ -1198,5 +1919,6 @@ + diff --git a/app/public/guild/antiraidForm.js b/app/public/guild/antiraidForm.js new file mode 100644 index 0000000..1b4e4fa --- /dev/null +++ b/app/public/guild/antiraidForm.js @@ -0,0 +1,480 @@ +// ============================================= +// ========== ANTI-RAID FORM =================== +// ============================================= + +(function() { + let config = {}; + let warningsConfig = {}; + let channels = []; + let roles = []; + + // Charger la config anti-raid + async function loadAntiraidConfig() { + try { + const res = await fetch(`/api/bot/get-antiraid-config?guildId=${guildId}`); + const data = await res.json(); + + if (data.success) { + config = data.config; + channels = data.channels || []; + roles = data.roles || []; + + populateSelects(); + fillForm(); + } + } catch (err) { + console.error('Erreur chargement config antiraid:', err); + } + + // Charger aussi la config des warnings + try { + const res = await fetch(`/api/bot/get-warnings-config?guildId=${guildId}`); + const data = await res.json(); + + if (data.success) { + warningsConfig = data.config; + fillWarningsForm(); + } + } catch (err) { + console.error('Erreur chargement config warnings:', err); + } + } + + // Remplir les selects avec les salons et rôles + function populateSelects() { + // Salon de logs + const logChannelSelect = document.getElementById('antiraid-log-channel'); + logChannelSelect.innerHTML = ''; + channels.forEach(ch => { + logChannelSelect.innerHTML += ``; + }); + + // Salon notifications warnings + const notifyChannelSelect = document.getElementById('warnings-notify-channel'); + if (notifyChannelSelect) { + notifyChannelSelect.innerHTML = ''; + channels.forEach(ch => { + notifyChannelSelect.innerHTML += ``; + }); + } + + // Multi-selects pour les exclusions + const channelSelects = [ + 'antilink-exclude-channels', + 'antiinvite-exclude-channels', + 'antispam-exclude-channels', + 'antidupe-exclude-channels', + 'antimention-exclude-channels', + 'antiemoji-exclude-channels', + 'anticaps-exclude-channels', + 'antinewline-exclude-channels', + 'antibadwords-exclude-channels' + ]; + + const roleSelects = [ + 'antilink-exclude-roles', + 'antiinvite-exclude-roles', + 'antispam-exclude-roles', + 'antidupe-exclude-roles', + 'antimention-exclude-roles', + 'antiemoji-exclude-roles', + 'anticaps-exclude-roles', + 'antinewline-exclude-roles', + 'antibadwords-exclude-roles' + ]; + + channelSelects.forEach(selectId => { + const select = document.getElementById(selectId); + if (select) { + select.innerHTML = ''; + channels.forEach(ch => { + select.innerHTML += ``; + }); + } + }); + + roleSelects.forEach(selectId => { + const select = document.getElementById(selectId); + if (select) { + select.innerHTML = ''; + roles.forEach(r => { + select.innerHTML += ``; + }); + } + }); + } + + // Remplir le formulaire avec la config + function fillForm() { + // Global + document.getElementById('antiraid-enabled').checked = config.enabled; + document.getElementById('antiraid-log-channel').value = config.log_channel_id || ''; + + // Anti-Link + document.getElementById('antilink-enabled').checked = config.antilink_enabled; + document.getElementById('antilink-action').value = config.antilink_action || 'delete'; + document.getElementById('antilink-warn-message').value = config.antilink_warn_message || '⚠️ Les liens ne sont pas autorisés ici.'; + try { + const whitelist = JSON.parse(config.antilink_whitelist_domains || '[]'); + document.getElementById('antilink-whitelist').value = whitelist.join(', '); + } catch { document.getElementById('antilink-whitelist').value = ''; } + setMultiSelectValues('antilink-exclude-channels', config.antilink_exclude_channels); + setMultiSelectValues('antilink-exclude-roles', config.antilink_exclude_roles); + + // Anti-Invite + document.getElementById('antiinvite-enabled').checked = config.antiinvite_enabled; + document.getElementById('antiinvite-action').value = config.antiinvite_action || 'delete'; + document.getElementById('antiinvite-allow-own').checked = config.antiinvite_allow_own_server; + setMultiSelectValues('antiinvite-exclude-channels', config.antiinvite_exclude_channels); + setMultiSelectValues('antiinvite-exclude-roles', config.antiinvite_exclude_roles); + + // Anti-Spam + document.getElementById('antispam-enabled').checked = config.antispam_enabled; + document.getElementById('antispam-max-messages').value = config.antispam_max_messages || 5; + document.getElementById('antispam-interval').value = config.antispam_interval_seconds || 5; + document.getElementById('antispam-action').value = config.antispam_action || 'mute'; + document.getElementById('antispam-mute-duration').value = config.antispam_mute_duration_minutes || 10; + setMultiSelectValues('antispam-exclude-channels', config.antispam_exclude_channels); + setMultiSelectValues('antispam-exclude-roles', config.antispam_exclude_roles); + + // Anti-Duplicate + document.getElementById('antidupe-enabled').checked = config.antidupe_enabled; + document.getElementById('antidupe-max').value = config.antidupe_max_duplicates || 3; + document.getElementById('antidupe-interval').value = config.antidupe_interval_seconds || 30; + document.getElementById('antidupe-action').value = config.antidupe_action || 'delete'; + setMultiSelectValues('antidupe-exclude-channels', config.antidupe_exclude_channels); + setMultiSelectValues('antidupe-exclude-roles', config.antidupe_exclude_roles); + + // Anti-Mention + document.getElementById('antimention-enabled').checked = config.antimention_enabled; + document.getElementById('antimention-max').value = config.antimention_max_mentions || 5; + document.getElementById('antimention-action').value = config.antimention_action || 'delete'; + setMultiSelectValues('antimention-exclude-channels', config.antimention_exclude_channels); + setMultiSelectValues('antimention-exclude-roles', config.antimention_exclude_roles); + + // Anti-Emoji + document.getElementById('antiemoji-enabled').checked = config.antiemoji_enabled; + document.getElementById('antiemoji-max').value = config.antiemoji_max_emojis || 10; + document.getElementById('antiemoji-action').value = config.antiemoji_action || 'delete'; + setMultiSelectValues('antiemoji-exclude-channels', config.antiemoji_exclude_channels); + setMultiSelectValues('antiemoji-exclude-roles', config.antiemoji_exclude_roles); + + // Anti-Caps + document.getElementById('anticaps-enabled').checked = config.anticaps_enabled; + document.getElementById('anticaps-max-percent').value = config.anticaps_max_percent || 70; + document.getElementById('anticaps-min-length').value = config.anticaps_min_length || 10; + document.getElementById('anticaps-action').value = config.anticaps_action || 'delete'; + setMultiSelectValues('anticaps-exclude-channels', config.anticaps_exclude_channels); + setMultiSelectValues('anticaps-exclude-roles', config.anticaps_exclude_roles); + + // Anti-Newline + document.getElementById('antinewline-enabled').checked = config.antinewline_enabled; + document.getElementById('antinewline-max').value = config.antinewline_max_lines || 15; + document.getElementById('antinewline-action').value = config.antinewline_action || 'delete'; + setMultiSelectValues('antinewline-exclude-channels', config.antinewline_exclude_channels); + setMultiSelectValues('antinewline-exclude-roles', config.antinewline_exclude_roles); + + // Anti-Bot + document.getElementById('antibot-enabled').checked = config.antibot_enabled; + document.getElementById('antibot-min-age').value = config.antibot_min_account_age_days || 7; + document.getElementById('antibot-action').value = config.antibot_action || 'kick'; + document.getElementById('antibot-no-avatar').checked = config.antibot_no_avatar_action; + document.getElementById('antibot-suspicious-name').checked = config.antibot_suspicious_name_action; + + // Anti-Mass Join + document.getElementById('antimassj-enabled').checked = config.antimassj_enabled; + document.getElementById('antimassj-max').value = config.antimassj_max_joins || 10; + document.getElementById('antimassj-interval').value = config.antimassj_interval_seconds || 10; + document.getElementById('antimassj-action').value = config.antimassj_action || 'kick'; + + // Anti-Badwords + document.getElementById('antibadwords-enabled').checked = config.antibadwords_enabled; + document.getElementById('antibadwords-action').value = config.antibadwords_action || 'delete'; + document.getElementById('antibadwords-warn-message').value = config.antibadwords_warn_message || '⚠️ Les insultes et gros mots ne sont pas autorisés.'; + try { + const words = JSON.parse(config.antibadwords_words || '[]'); + document.getElementById('antibadwords-words').value = words.join('\n'); + } catch { document.getElementById('antibadwords-words').value = ''; } + setMultiSelectValues('antibadwords-exclude-channels', config.antibadwords_exclude_channels); + setMultiSelectValues('antibadwords-exclude-roles', config.antibadwords_exclude_roles); + + updateModulesVisibility(); + } + + // Remplir le formulaire des warnings + function fillWarningsForm() { + document.getElementById('warnings-enabled').checked = warningsConfig.enabled; + document.getElementById('warn1-action').value = warningsConfig.warn1_action || 'none'; + document.getElementById('warn1-duration').value = warningsConfig.warn1_duration || 10; + document.getElementById('warn2-action').value = warningsConfig.warn2_action || 'none'; + document.getElementById('warn2-duration').value = warningsConfig.warn2_duration || 30; + document.getElementById('warn3-action').value = warningsConfig.warn3_action || 'mute'; + document.getElementById('warn3-duration').value = warningsConfig.warn3_duration || 60; + document.getElementById('warn4-action').value = warningsConfig.warn4_action || 'kick'; + document.getElementById('warn4-duration').value = warningsConfig.warn4_duration || 0; + document.getElementById('warn5-action').value = warningsConfig.warn5_action || 'ban'; + document.getElementById('warn5-duration').value = warningsConfig.warn5_duration || 0; + document.getElementById('warnings-decay-enabled').checked = warningsConfig.decay_enabled; + document.getElementById('warnings-decay-days').value = warningsConfig.decay_days || 30; + document.getElementById('warnings-notify-channel').value = warningsConfig.notify_channel_id || ''; + + // Afficher/masquer les options de decay + document.getElementById('warnings-decay-options').style.display = + warningsConfig.decay_enabled ? 'grid' : 'none'; + + updateWarningDurationFields(); + } + + // Mettre à jour les champs de durée selon l'action + function updateWarningDurationFields() { + for (let i = 1; i <= 5; i++) { + const action = document.getElementById(`warn${i}-action`).value; + const durationField = document.getElementById(`warn${i}-duration`); + durationField.disabled = action !== 'mute'; + } + } + + // Helper pour les multi-selects + function setMultiSelectValues(selectId, jsonValue) { + const select = document.getElementById(selectId); + if (!select) return; + + try { + const values = JSON.parse(jsonValue || '[]'); + Array.from(select.options).forEach(opt => { + opt.selected = values.includes(opt.value); + }); + } catch {} + } + + function getMultiSelectValues(selectId) { + const select = document.getElementById(selectId); + if (!select) return []; + return Array.from(select.selectedOptions).map(opt => opt.value); + } + + // Mise à jour de la visibilité des modules selon l'état global + function updateModulesVisibility() { + const enabled = document.getElementById('antiraid-enabled').checked; + const modules = document.querySelectorAll('.antiraid-module'); + modules.forEach(m => { + m.style.opacity = enabled ? '1' : '0.5'; + m.style.pointerEvents = enabled ? 'auto' : 'none'; + }); + } + + // Sauvegarder la config + async function saveAntiraidConfig() { + const statusEl = document.getElementById('status-antiraid-form'); + const saveBtn = document.getElementById('antiraid-save-btn'); + + saveBtn.disabled = true; + statusEl.textContent = 'Sauvegarde en cours...'; + statusEl.className = 'status-message'; + + // Parser la whitelist + const whitelistText = document.getElementById('antilink-whitelist').value; + const whitelist = whitelistText.split(',').map(d => d.trim()).filter(d => d); + + const configData = { + enabled: document.getElementById('antiraid-enabled').checked, + log_channel_id: document.getElementById('antiraid-log-channel').value || null, + + // Anti-Link + antilink_enabled: document.getElementById('antilink-enabled').checked, + antilink_action: document.getElementById('antilink-action').value, + antilink_whitelist_domains: whitelist, + antilink_exclude_channels: getMultiSelectValues('antilink-exclude-channels'), + antilink_exclude_roles: getMultiSelectValues('antilink-exclude-roles'), + antilink_warn_message: document.getElementById('antilink-warn-message').value, + + // Anti-Invite + antiinvite_enabled: document.getElementById('antiinvite-enabled').checked, + antiinvite_action: document.getElementById('antiinvite-action').value, + antiinvite_allow_own_server: document.getElementById('antiinvite-allow-own').checked, + antiinvite_exclude_channels: getMultiSelectValues('antiinvite-exclude-channels'), + antiinvite_exclude_roles: getMultiSelectValues('antiinvite-exclude-roles'), + + // Anti-Spam + antispam_enabled: document.getElementById('antispam-enabled').checked, + antispam_max_messages: parseInt(document.getElementById('antispam-max-messages').value) || 5, + antispam_interval_seconds: parseInt(document.getElementById('antispam-interval').value) || 5, + antispam_action: document.getElementById('antispam-action').value, + antispam_mute_duration_minutes: parseInt(document.getElementById('antispam-mute-duration').value) || 10, + antispam_exclude_channels: getMultiSelectValues('antispam-exclude-channels'), + antispam_exclude_roles: getMultiSelectValues('antispam-exclude-roles'), + + // Anti-Duplicate + antidupe_enabled: document.getElementById('antidupe-enabled').checked, + antidupe_max_duplicates: parseInt(document.getElementById('antidupe-max').value) || 3, + antidupe_interval_seconds: parseInt(document.getElementById('antidupe-interval').value) || 30, + antidupe_action: document.getElementById('antidupe-action').value, + antidupe_exclude_channels: getMultiSelectValues('antidupe-exclude-channels'), + antidupe_exclude_roles: getMultiSelectValues('antidupe-exclude-roles'), + + // Anti-Mention + antimention_enabled: document.getElementById('antimention-enabled').checked, + antimention_max_mentions: parseInt(document.getElementById('antimention-max').value) || 5, + antimention_action: document.getElementById('antimention-action').value, + antimention_exclude_channels: getMultiSelectValues('antimention-exclude-channels'), + antimention_exclude_roles: getMultiSelectValues('antimention-exclude-roles'), + + // Anti-Emoji + antiemoji_enabled: document.getElementById('antiemoji-enabled').checked, + antiemoji_max_emojis: parseInt(document.getElementById('antiemoji-max').value) || 10, + antiemoji_action: document.getElementById('antiemoji-action').value, + antiemoji_exclude_channels: getMultiSelectValues('antiemoji-exclude-channels'), + antiemoji_exclude_roles: getMultiSelectValues('antiemoji-exclude-roles'), + + // Anti-Caps + anticaps_enabled: document.getElementById('anticaps-enabled').checked, + anticaps_max_percent: parseInt(document.getElementById('anticaps-max-percent').value) || 70, + anticaps_min_length: parseInt(document.getElementById('anticaps-min-length').value) || 10, + anticaps_action: document.getElementById('anticaps-action').value, + anticaps_exclude_channels: getMultiSelectValues('anticaps-exclude-channels'), + anticaps_exclude_roles: getMultiSelectValues('anticaps-exclude-roles'), + + // Anti-Newline + antinewline_enabled: document.getElementById('antinewline-enabled').checked, + antinewline_max_lines: parseInt(document.getElementById('antinewline-max').value) || 15, + antinewline_action: document.getElementById('antinewline-action').value, + antinewline_exclude_channels: getMultiSelectValues('antinewline-exclude-channels'), + antinewline_exclude_roles: getMultiSelectValues('antinewline-exclude-roles'), + + // Anti-Bot + antibot_enabled: document.getElementById('antibot-enabled').checked, + antibot_min_account_age_days: parseInt(document.getElementById('antibot-min-age').value) || 7, + antibot_no_avatar_action: document.getElementById('antibot-no-avatar').checked, + antibot_suspicious_name_action: document.getElementById('antibot-suspicious-name').checked, + antibot_action: document.getElementById('antibot-action').value, + + // Anti-Mass Join + antimassj_enabled: document.getElementById('antimassj-enabled').checked, + antimassj_max_joins: parseInt(document.getElementById('antimassj-max').value) || 10, + antimassj_interval_seconds: parseInt(document.getElementById('antimassj-interval').value) || 10, + antimassj_action: document.getElementById('antimassj-action').value, + + // Anti-Badwords + antibadwords_enabled: document.getElementById('antibadwords-enabled').checked, + antibadwords_words: document.getElementById('antibadwords-words').value.split('\n').map(w => w.trim()).filter(w => w), + antibadwords_action: document.getElementById('antibadwords-action').value, + antibadwords_warn_message: document.getElementById('antibadwords-warn-message').value, + antibadwords_exclude_channels: getMultiSelectValues('antibadwords-exclude-channels'), + antibadwords_exclude_roles: getMultiSelectValues('antibadwords-exclude-roles') + }; + + try { + const res = await fetch('/api/bot/save-antiraid-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ guildId, config: configData }) + }); + + const data = await res.json(); + + if (data.success) { + statusEl.textContent = '✅ Configuration sauvegardée !'; + statusEl.className = 'status-message success'; + } else { + statusEl.textContent = '❌ ' + (data.error || 'Erreur inconnue'); + statusEl.className = 'status-message error'; + } + } catch (err) { + console.error('Erreur sauvegarde antiraid:', err); + statusEl.textContent = '❌ Erreur de connexion'; + statusEl.className = 'status-message error'; + } + + saveBtn.disabled = false; + setTimeout(() => { + statusEl.textContent = ''; + statusEl.className = 'status-message'; + }, 3000); + } + + // Event Listeners + document.getElementById('antiraid-enabled').addEventListener('change', updateModulesVisibility); + document.getElementById('antiraid-save-btn').addEventListener('click', saveAntiraidConfig); + + // Warnings event listeners + document.getElementById('warnings-decay-enabled').addEventListener('change', (e) => { + document.getElementById('warnings-decay-options').style.display = e.target.checked ? 'grid' : 'none'; + }); + + // Update duration fields when action changes + for (let i = 1; i <= 5; i++) { + document.getElementById(`warn${i}-action`).addEventListener('change', updateWarningDurationFields); + } + + document.getElementById('warnings-save-btn').addEventListener('click', saveWarningsConfig); + + // Sauvegarder la config warnings + async function saveWarningsConfig() { + const statusEl = document.getElementById('status-warnings-form'); + const saveBtn = document.getElementById('warnings-save-btn'); + + saveBtn.disabled = true; + statusEl.textContent = 'Sauvegarde en cours...'; + statusEl.className = 'status-message'; + + const configData = { + enabled: document.getElementById('warnings-enabled').checked, + warn1_action: document.getElementById('warn1-action').value, + warn1_duration: parseInt(document.getElementById('warn1-duration').value) || 10, + warn2_action: document.getElementById('warn2-action').value, + warn2_duration: parseInt(document.getElementById('warn2-duration').value) || 30, + warn3_action: document.getElementById('warn3-action').value, + warn3_duration: parseInt(document.getElementById('warn3-duration').value) || 60, + warn4_action: document.getElementById('warn4-action').value, + warn4_duration: parseInt(document.getElementById('warn4-duration').value) || 0, + warn5_action: document.getElementById('warn5-action').value, + warn5_duration: parseInt(document.getElementById('warn5-duration').value) || 0, + decay_enabled: document.getElementById('warnings-decay-enabled').checked, + decay_days: parseInt(document.getElementById('warnings-decay-days').value) || 30, + notify_channel_id: document.getElementById('warnings-notify-channel').value || null + }; + + try { + const res = await fetch('/api/bot/save-warnings-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ guildId, config: configData }) + }); + + const data = await res.json(); + + if (data.success) { + statusEl.textContent = '✅ Configuration sauvegardée !'; + statusEl.className = 'status-message success'; + } else { + statusEl.textContent = '❌ ' + (data.error || 'Erreur inconnue'); + statusEl.className = 'status-message error'; + } + } catch (err) { + console.error('Erreur sauvegarde warnings:', err); + statusEl.textContent = '❌ Erreur de connexion'; + statusEl.className = 'status-message error'; + } + + saveBtn.disabled = false; + setTimeout(() => { + statusEl.textContent = ''; + statusEl.className = 'status-message'; + }, 3000); + } + + // Toggle modules body visibility when clicking header + document.querySelectorAll('.antiraid-module-header').forEach(header => { + header.addEventListener('click', (e) => { + // Ne pas toggle si on clique sur le switch + if (e.target.closest('.toggle-switch')) return; + + const module = header.closest('.antiraid-module'); + const body = module.querySelector('.antiraid-module-body'); + body.style.display = body.style.display === 'none' ? 'block' : 'none'; + }); + }); + + // Charger au démarrage + loadAntiraidConfig(); +})(); diff --git a/app/routes/api.js b/app/routes/api.js index f029845..f7924a1 100644 --- a/app/routes/api.js +++ b/app/routes/api.js @@ -1597,5 +1597,445 @@ module.exports = (app, db, client) => { } }); + // ============================================= + // ========== ANTI-RAID CONFIG API ============= + // ============================================= + + // Récupérer la config anti-raid + router.get("/bot/get-antiraid-config", async (req, res) => { + const { guildId } = req.query; + + if (!req.session.guilds) { + return res.status(401).json({ success: false, error: "Non connecté" }); + } + + const isAdmin = req.session.guilds.find( + g => g.id === guildId && (BigInt(g.permissions) & 0x8n) === 0x8n + ); + + if (!isAdmin) { + return res.status(403).json({ success: false, error: "Permission refusée" }); + } + + try { + let config = await db.getAsync("SELECT * FROM antiraid_config WHERE guild_id = ?", [guildId]); + + if (!config) { + // Créer config par défaut + await new Promise((resolve, reject) => { + db.run("INSERT INTO antiraid_config (guild_id) VALUES (?)", [guildId], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + config = await db.getAsync("SELECT * FROM antiraid_config WHERE guild_id = ?", [guildId]); + } + + // Récupérer les salons et rôles du serveur + const guild = client.guilds.cache.get(guildId); + const channels = guild ? guild.channels.cache + .filter(c => c.type === 0) + .map(c => ({ id: c.id, name: c.name })) : []; + const roles = guild ? guild.roles.cache + .filter(r => r.id !== guild.id && !r.managed) + .sort((a, b) => b.position - a.position) + .map(r => ({ id: r.id, name: r.name, color: r.hexColor })) : []; + + res.json({ + success: true, + config, + channels, + roles + }); + + } catch (err) { + console.error("Erreur get antiraid config:", err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + // Sauvegarder la config anti-raid + router.post("/bot/save-antiraid-config", express.json(), async (req, res) => { + const { guildId, config } = req.body; + + if (!req.session.guilds) { + return res.status(401).json({ success: false, error: "Non connecté" }); + } + + const isAdmin = req.session.guilds.find( + g => g.id === guildId && (BigInt(g.permissions) & 0x8n) === 0x8n + ); + + if (!isAdmin) { + return res.status(403).json({ success: false, error: "Permission refusée" }); + } + + try { + await new Promise((resolve, reject) => { + db.run(` + INSERT INTO antiraid_config ( + guild_id, enabled, log_channel_id, + antilink_enabled, antilink_action, antilink_whitelist_domains, antilink_exclude_channels, antilink_exclude_roles, antilink_warn_message, + antiinvite_enabled, antiinvite_action, antiinvite_allow_own_server, antiinvite_exclude_channels, antiinvite_exclude_roles, + antispam_enabled, antispam_max_messages, antispam_interval_seconds, antispam_action, antispam_mute_duration_minutes, antispam_exclude_channels, antispam_exclude_roles, + antidupe_enabled, antidupe_max_duplicates, antidupe_interval_seconds, antidupe_action, antidupe_exclude_channels, antidupe_exclude_roles, + antimention_enabled, antimention_max_mentions, antimention_action, antimention_exclude_channels, antimention_exclude_roles, + antiemoji_enabled, antiemoji_max_emojis, antiemoji_action, antiemoji_exclude_channels, antiemoji_exclude_roles, + anticaps_enabled, anticaps_max_percent, anticaps_min_length, anticaps_action, anticaps_exclude_channels, anticaps_exclude_roles, + antinewline_enabled, antinewline_max_lines, antinewline_action, antinewline_exclude_channels, antinewline_exclude_roles, + antibot_enabled, antibot_min_account_age_days, antibot_no_avatar_action, antibot_suspicious_name_action, antibot_action, + antimassj_enabled, antimassj_max_joins, antimassj_interval_seconds, antimassj_action, + antibadwords_enabled, antibadwords_action, antibadwords_words, antibadwords_exclude_channels, antibadwords_exclude_roles, antibadwords_warn_message + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(guild_id) DO UPDATE SET + enabled = excluded.enabled, + log_channel_id = excluded.log_channel_id, + antilink_enabled = excluded.antilink_enabled, + antilink_action = excluded.antilink_action, + antilink_whitelist_domains = excluded.antilink_whitelist_domains, + antilink_exclude_channels = excluded.antilink_exclude_channels, + antilink_exclude_roles = excluded.antilink_exclude_roles, + antilink_warn_message = excluded.antilink_warn_message, + antiinvite_enabled = excluded.antiinvite_enabled, + antiinvite_action = excluded.antiinvite_action, + antiinvite_allow_own_server = excluded.antiinvite_allow_own_server, + antiinvite_exclude_channels = excluded.antiinvite_exclude_channels, + antiinvite_exclude_roles = excluded.antiinvite_exclude_roles, + antispam_enabled = excluded.antispam_enabled, + antispam_max_messages = excluded.antispam_max_messages, + antispam_interval_seconds = excluded.antispam_interval_seconds, + antispam_action = excluded.antispam_action, + antispam_mute_duration_minutes = excluded.antispam_mute_duration_minutes, + antispam_exclude_channels = excluded.antispam_exclude_channels, + antispam_exclude_roles = excluded.antispam_exclude_roles, + antidupe_enabled = excluded.antidupe_enabled, + antidupe_max_duplicates = excluded.antidupe_max_duplicates, + antidupe_interval_seconds = excluded.antidupe_interval_seconds, + antidupe_action = excluded.antidupe_action, + antidupe_exclude_channels = excluded.antidupe_exclude_channels, + antidupe_exclude_roles = excluded.antidupe_exclude_roles, + antimention_enabled = excluded.antimention_enabled, + antimention_max_mentions = excluded.antimention_max_mentions, + antimention_action = excluded.antimention_action, + antimention_exclude_channels = excluded.antimention_exclude_channels, + antimention_exclude_roles = excluded.antimention_exclude_roles, + antiemoji_enabled = excluded.antiemoji_enabled, + antiemoji_max_emojis = excluded.antiemoji_max_emojis, + antiemoji_action = excluded.antiemoji_action, + antiemoji_exclude_channels = excluded.antiemoji_exclude_channels, + antiemoji_exclude_roles = excluded.antiemoji_exclude_roles, + anticaps_enabled = excluded.anticaps_enabled, + anticaps_max_percent = excluded.anticaps_max_percent, + anticaps_min_length = excluded.anticaps_min_length, + anticaps_action = excluded.anticaps_action, + anticaps_exclude_channels = excluded.anticaps_exclude_channels, + anticaps_exclude_roles = excluded.anticaps_exclude_roles, + antinewline_enabled = excluded.antinewline_enabled, + antinewline_max_lines = excluded.antinewline_max_lines, + antinewline_action = excluded.antinewline_action, + antinewline_exclude_channels = excluded.antinewline_exclude_channels, + antinewline_exclude_roles = excluded.antinewline_exclude_roles, + antibot_enabled = excluded.antibot_enabled, + antibot_min_account_age_days = excluded.antibot_min_account_age_days, + antibot_no_avatar_action = excluded.antibot_no_avatar_action, + antibot_suspicious_name_action = excluded.antibot_suspicious_name_action, + antibot_action = excluded.antibot_action, + antimassj_enabled = excluded.antimassj_enabled, + antimassj_max_joins = excluded.antimassj_max_joins, + antimassj_interval_seconds = excluded.antimassj_interval_seconds, + antimassj_action = excluded.antimassj_action, + antibadwords_enabled = excluded.antibadwords_enabled, + antibadwords_action = excluded.antibadwords_action, + antibadwords_words = excluded.antibadwords_words, + antibadwords_exclude_channels = excluded.antibadwords_exclude_channels, + antibadwords_exclude_roles = excluded.antibadwords_exclude_roles, + antibadwords_warn_message = excluded.antibadwords_warn_message + `, [ + guildId, + config.enabled ? 1 : 0, + config.log_channel_id || null, + // Anti-Link + config.antilink_enabled ? 1 : 0, + config.antilink_action || 'delete', + JSON.stringify(config.antilink_whitelist_domains || []), + JSON.stringify(config.antilink_exclude_channels || []), + JSON.stringify(config.antilink_exclude_roles || []), + config.antilink_warn_message || '⚠️ Les liens ne sont pas autorisés ici.', + // Anti-Invite + config.antiinvite_enabled ? 1 : 0, + config.antiinvite_action || 'delete', + config.antiinvite_allow_own_server ? 1 : 0, + JSON.stringify(config.antiinvite_exclude_channels || []), + JSON.stringify(config.antiinvite_exclude_roles || []), + // Anti-Spam + config.antispam_enabled ? 1 : 0, + config.antispam_max_messages || 5, + config.antispam_interval_seconds || 5, + config.antispam_action || 'mute', + config.antispam_mute_duration_minutes || 10, + JSON.stringify(config.antispam_exclude_channels || []), + JSON.stringify(config.antispam_exclude_roles || []), + // Anti-Duplicate + config.antidupe_enabled ? 1 : 0, + config.antidupe_max_duplicates || 3, + config.antidupe_interval_seconds || 30, + config.antidupe_action || 'delete', + JSON.stringify(config.antidupe_exclude_channels || []), + JSON.stringify(config.antidupe_exclude_roles || []), + // Anti-Mention + config.antimention_enabled ? 1 : 0, + config.antimention_max_mentions || 5, + config.antimention_action || 'delete', + JSON.stringify(config.antimention_exclude_channels || []), + JSON.stringify(config.antimention_exclude_roles || []), + // Anti-Emoji + config.antiemoji_enabled ? 1 : 0, + config.antiemoji_max_emojis || 10, + config.antiemoji_action || 'delete', + JSON.stringify(config.antiemoji_exclude_channels || []), + JSON.stringify(config.antiemoji_exclude_roles || []), + // Anti-Caps + config.anticaps_enabled ? 1 : 0, + config.anticaps_max_percent || 70, + config.anticaps_min_length || 10, + config.anticaps_action || 'delete', + JSON.stringify(config.anticaps_exclude_channels || []), + JSON.stringify(config.anticaps_exclude_roles || []), + // Anti-Newline + config.antinewline_enabled ? 1 : 0, + config.antinewline_max_lines || 15, + config.antinewline_action || 'delete', + JSON.stringify(config.antinewline_exclude_channels || []), + JSON.stringify(config.antinewline_exclude_roles || []), + // Anti-Bot + config.antibot_enabled ? 1 : 0, + config.antibot_min_account_age_days || 7, + config.antibot_no_avatar_action ? 1 : 0, + config.antibot_suspicious_name_action ? 1 : 0, + config.antibot_action || 'kick', + // Anti-Mass Join + config.antimassj_enabled ? 1 : 0, + config.antimassj_max_joins || 10, + config.antimassj_interval_seconds || 10, + config.antimassj_action || 'kick', + // Anti-Badwords + config.antibadwords_enabled ? 1 : 0, + config.antibadwords_action || 'delete', + JSON.stringify(config.antibadwords_words || []), + JSON.stringify(config.antibadwords_exclude_channels || []), + JSON.stringify(config.antibadwords_exclude_roles || []), + config.antibadwords_warn_message || '⚠️ Les insultes et gros mots ne sont pas autorisés.' + ], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + + res.json({ success: true }); + + } catch (err) { + console.error("Erreur save antiraid config:", err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + // ============================================= + // ========== WARNINGS CONFIG API ============== + // ============================================= + + // Récupérer la config des warnings + router.get("/bot/get-warnings-config", async (req, res) => { + const { guildId } = req.query; + + if (!req.session.guilds) { + return res.status(401).json({ success: false, error: "Non connecté" }); + } + + const isAdmin = req.session.guilds.find( + g => g.id === guildId && (BigInt(g.permissions) & 0x8n) === 0x8n + ); + + if (!isAdmin) { + return res.status(403).json({ success: false, error: "Permission refusée" }); + } + + try { + let config = await db.getAsync("SELECT * FROM warnings_config WHERE guild_id = ?", [guildId]); + + if (!config) { + // Créer config par défaut + await new Promise((resolve, reject) => { + db.run("INSERT INTO warnings_config (guild_id) VALUES (?)", [guildId], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + config = await db.getAsync("SELECT * FROM warnings_config WHERE guild_id = ?", [guildId]); + } + + // Récupérer les salons du serveur + const guild = client.guilds.cache.get(guildId); + const channels = guild ? guild.channels.cache + .filter(c => c.type === 0) + .map(c => ({ id: c.id, name: c.name })) : []; + + res.json({ + success: true, + config, + channels + }); + + } catch (err) { + console.error("Erreur get warnings config:", err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + // Sauvegarder la config des warnings + router.post("/bot/save-warnings-config", express.json(), async (req, res) => { + const { guildId, config } = req.body; + + if (!req.session.guilds) { + return res.status(401).json({ success: false, error: "Non connecté" }); + } + + const isAdmin = req.session.guilds.find( + g => g.id === guildId && (BigInt(g.permissions) & 0x8n) === 0x8n + ); + + if (!isAdmin) { + return res.status(403).json({ success: false, error: "Permission refusée" }); + } + + try { + await new Promise((resolve, reject) => { + db.run(` + INSERT INTO warnings_config ( + guild_id, enabled, + warn1_action, warn1_duration, + warn2_action, warn2_duration, + warn3_action, warn3_duration, + warn4_action, warn4_duration, + warn5_action, warn5_duration, + decay_enabled, decay_days, + notify_user, notify_channel_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(guild_id) DO UPDATE SET + enabled = excluded.enabled, + warn1_action = excluded.warn1_action, + warn1_duration = excluded.warn1_duration, + warn2_action = excluded.warn2_action, + warn2_duration = excluded.warn2_duration, + warn3_action = excluded.warn3_action, + warn3_duration = excluded.warn3_duration, + warn4_action = excluded.warn4_action, + warn4_duration = excluded.warn4_duration, + warn5_action = excluded.warn5_action, + warn5_duration = excluded.warn5_duration, + decay_enabled = excluded.decay_enabled, + decay_days = excluded.decay_days, + notify_user = excluded.notify_user, + notify_channel_id = excluded.notify_channel_id + `, [ + guildId, + config.enabled ? 1 : 0, + config.warn1_action || 'none', + config.warn1_duration || 10, + config.warn2_action || 'none', + config.warn2_duration || 30, + config.warn3_action || 'mute', + config.warn3_duration || 60, + config.warn4_action || 'kick', + config.warn4_duration || 0, + config.warn5_action || 'ban', + config.warn5_duration || 0, + config.decay_enabled ? 1 : 0, + config.decay_days || 30, + config.notify_user ? 1 : 0, + config.notify_channel_id || null + ], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + + res.json({ success: true }); + + } catch (err) { + console.error("Erreur save warnings config:", err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + // Récupérer les warnings d'un serveur + router.get("/bot/get-warnings-list", async (req, res) => { + const { guildId, userId } = req.query; + + if (!req.session.guilds) { + return res.status(401).json({ success: false, error: "Non connecté" }); + } + + const isAdmin = req.session.guilds.find( + g => g.id === guildId && (BigInt(g.permissions) & 0x8n) === 0x8n + ); + + if (!isAdmin) { + return res.status(403).json({ success: false, error: "Permission refusée" }); + } + + try { + let warnings; + if (userId) { + warnings = await db.allAsync( + "SELECT * FROM warnings WHERE guild_id = ? AND user_id = ? ORDER BY created_at DESC LIMIT 50", + [guildId, userId] + ); + } else { + warnings = await db.allAsync( + "SELECT * FROM warnings WHERE guild_id = ? ORDER BY created_at DESC LIMIT 100", + [guildId] + ); + } + + res.json({ success: true, warnings: warnings || [] }); + + } catch (err) { + console.error("Erreur get warnings list:", err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + // Supprimer un warning + router.post("/bot/delete-warning", express.json(), async (req, res) => { + const { guildId, warnId } = req.body; + + if (!req.session.guilds) { + return res.status(401).json({ success: false, error: "Non connecté" }); + } + + const isAdmin = req.session.guilds.find( + g => g.id === guildId && (BigInt(g.permissions) & 0x8n) === 0x8n + ); + + if (!isAdmin) { + return res.status(403).json({ success: false, error: "Permission refusée" }); + } + + try { + await new Promise((resolve, reject) => { + db.run("DELETE FROM warnings WHERE id = ? AND guild_id = ?", [warnId, guildId], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + + res.json({ success: true }); + + } catch (err) { + console.error("Erreur delete warning:", err); + res.status(500).json({ success: false, error: err.message }); + } + }); + app.use("/api", router); };