From 3f1f3ba40dadbeac9ade7fa9ee0c6d479a4c1a4b Mon Sep 17 00:00:00 2001 From: Arthur Puechberty Date: Sun, 18 Jan 2026 15:08:55 +0100 Subject: [PATCH] add logs --- app/bot.js | 12 +- app/db.js | 22 +++ app/events/channelCreate.js | 53 ++++++ app/events/channelDelete.js | 54 ++++++ app/events/channelUpdate.js | 77 ++++++++ app/events/guildBanAdd.js | 41 +++++ app/events/guildBanRemove.js | 38 ++++ app/events/guildMemberAdd.js | 26 +++ app/events/guildMemberRemove.js | 58 +++++- app/events/guildMemberUpdate.js | 180 ++++++++++++++++++ app/events/guildUpdate.js | 101 ++++++++++ app/events/inviteCreate.js | 41 +++++ app/events/inviteDelete.js | 42 +++++ app/events/messageDelete.js | 38 ++++ app/events/messageDeleteBulk.js | 29 +++ app/events/messageUpdate.js | 38 ++++ app/events/roleCreate.js | 35 ++++ app/events/roleDelete.js | 35 ++++ app/events/roleUpdate.js | 72 ++++++++ app/events/voiceStateUpdate.js | 87 +++++++++ app/fonctions/sendLog.js | 162 ++++++++++++++++ app/public/guild.css | 131 +++++++++++++ app/public/guild.html | 65 +++++++ app/public/guild/logsForm.js | 244 ++++++++++++++++++++++++ app/routes/api.js | 316 ++++++++++++++++++++++++++++++++ 25 files changed, 1994 insertions(+), 3 deletions(-) create mode 100644 app/events/channelCreate.js create mode 100644 app/events/channelDelete.js create mode 100644 app/events/channelUpdate.js create mode 100644 app/events/guildBanAdd.js create mode 100644 app/events/guildBanRemove.js create mode 100644 app/events/guildMemberUpdate.js create mode 100644 app/events/guildUpdate.js create mode 100644 app/events/inviteCreate.js create mode 100644 app/events/inviteDelete.js create mode 100644 app/events/messageDelete.js create mode 100644 app/events/messageDeleteBulk.js create mode 100644 app/events/messageUpdate.js create mode 100644 app/events/roleCreate.js create mode 100644 app/events/roleDelete.js create mode 100644 app/events/roleUpdate.js create mode 100644 app/fonctions/sendLog.js create mode 100644 app/public/guild/logsForm.js diff --git a/app/bot.js b/app/bot.js index 622e6cf..cb69a93 100644 --- a/app/bot.js +++ b/app/bot.js @@ -1,8 +1,16 @@ const db = require("./db"); -const { Client, GatewayIntentBits, Events } = require("discord.js"); +const { Client, GatewayIntentBits, Events, Partials } = require("discord.js"); -const client = new Client({ intents: Object.values(GatewayIntentBits) }); +const client = new Client({ + intents: Object.values(GatewayIntentBits), + partials: [ + Partials.Message, + Partials.Channel, + Partials.GuildMember, + Partials.User + ] +}); require("./loader/events.js")(client); require("./loader/commands.js")(client); diff --git a/app/db.js b/app/db.js index 8f18bb0..2e4502a 100644 --- a/app/db.js +++ b/app/db.js @@ -214,6 +214,28 @@ db.exec(` CREATE INDEX IF NOT EXISTS idx_user_activity_stats_date ON user_activity_stats(guild_id, user_id, date); + CREATE TABLE IF NOT EXISTS logs_config ( + guild_id TEXT PRIMARY KEY, + enabled INTEGER NOT NULL DEFAULT 0, + category_id TEXT, + moderation_enabled INTEGER NOT NULL DEFAULT 0, + moderation_channel_id TEXT, + voice_enabled INTEGER NOT NULL DEFAULT 0, + voice_channel_id TEXT, + messages_enabled INTEGER NOT NULL DEFAULT 0, + messages_channel_id TEXT, + members_enabled INTEGER NOT NULL DEFAULT 0, + members_channel_id TEXT, + channels_enabled INTEGER NOT NULL DEFAULT 0, + channels_channel_id TEXT, + roles_enabled INTEGER NOT NULL DEFAULT 0, + roles_channel_id TEXT, + invites_enabled INTEGER NOT NULL DEFAULT 0, + invites_channel_id TEXT, + server_enabled INTEGER NOT NULL DEFAULT 0, + server_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/channelCreate.js b/app/events/channelCreate.js new file mode 100644 index 0000000..87349ed --- /dev/null +++ b/app/events/channelCreate.js @@ -0,0 +1,53 @@ +const { AuditLogEvent, ChannelType } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +const CHANNEL_TYPE_NAMES = { + [ChannelType.GuildText]: 'Salon textuel', + [ChannelType.GuildVoice]: 'Salon vocal', + [ChannelType.GuildCategory]: 'Catégorie', + [ChannelType.GuildAnnouncement]: 'Salon d\'annonces', + [ChannelType.GuildStageVoice]: 'Salon de conférence', + [ChannelType.GuildForum]: 'Forum', + [ChannelType.GuildMedia]: 'Salon média' +}; + +module.exports = { + name: 'channelCreate', + async execute(client, channel) { + if (!channel.guild) return; + + let executor = null; + + try { + const auditLogs = await channel.guild.fetchAuditLogs({ + type: AuditLogEvent.ChannelCreate, + limit: 1 + }); + + const createLog = auditLogs.entries.first(); + if (createLog && createLog.target.id === channel.id && (Date.now() - createLog.createdTimestamp) < 5000) { + executor = createLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs channel create:', err); + } + + const typeName = CHANNEL_TYPE_NAMES[channel.type] || 'Salon'; + const fields = [ + { name: '📁 Nom', value: channel.name, inline: true }, + { name: '🏷️ Type', value: typeName, inline: true } + ]; + + if (channel.parent) { + fields.push({ name: '📂 Catégorie', value: channel.parent.name, inline: true }); + } + + await sendLog(client, channel.guild.id, 'channels', { + action: 'create', + title: '✅ Salon créé', + description: `Le salon ${channel} a été créé.`, + fields: fields, + executor: executor + }); + } +}; diff --git a/app/events/channelDelete.js b/app/events/channelDelete.js new file mode 100644 index 0000000..de9332c --- /dev/null +++ b/app/events/channelDelete.js @@ -0,0 +1,54 @@ +const { AuditLogEvent, ChannelType } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +const CHANNEL_TYPE_NAMES = { + [ChannelType.GuildText]: 'Salon textuel', + [ChannelType.GuildVoice]: 'Salon vocal', + [ChannelType.GuildCategory]: 'Catégorie', + [ChannelType.GuildAnnouncement]: 'Salon d\'annonces', + [ChannelType.GuildStageVoice]: 'Salon de conférence', + [ChannelType.GuildForum]: 'Forum', + [ChannelType.GuildMedia]: 'Salon média' +}; + +module.exports = { + name: 'channelDelete', + async execute(client, channel) { + if (!channel.guild) return; + + let executor = null; + + try { + const auditLogs = await channel.guild.fetchAuditLogs({ + type: AuditLogEvent.ChannelDelete, + limit: 1 + }); + + const deleteLog = auditLogs.entries.first(); + if (deleteLog && deleteLog.target.id === channel.id && (Date.now() - deleteLog.createdTimestamp) < 5000) { + executor = deleteLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs channel delete:', err); + } + + const typeName = CHANNEL_TYPE_NAMES[channel.type] || 'Salon'; + const fields = [ + { name: '📁 Nom', value: channel.name, inline: true }, + { name: '🏷️ Type', value: typeName, inline: true }, + { name: '🆔 ID', value: channel.id, inline: true } + ]; + + if (channel.parent) { + fields.push({ name: '📂 Catégorie', value: channel.parent.name, inline: true }); + } + + await sendLog(client, channel.guild.id, 'channels', { + action: 'delete', + title: '🗑️ Salon supprimé', + description: `Le salon **#${channel.name}** a été supprimé.`, + fields: fields, + executor: executor + }); + } +}; diff --git a/app/events/channelUpdate.js b/app/events/channelUpdate.js new file mode 100644 index 0000000..a57fbb7 --- /dev/null +++ b/app/events/channelUpdate.js @@ -0,0 +1,77 @@ +const { AuditLogEvent } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'channelUpdate', + async execute(client, oldChannel, newChannel) { + if (!newChannel.guild) return; + + const changes = []; + + // Changement de nom + if (oldChannel.name !== newChannel.name) { + changes.push({ name: '📝 Nom', value: `\`${oldChannel.name}\` → \`${newChannel.name}\``, inline: false }); + } + + // Changement de topic (description) + if (oldChannel.topic !== newChannel.topic) { + const oldTopic = oldChannel.topic || '*Aucun*'; + const newTopic = newChannel.topic || '*Aucun*'; + changes.push({ name: '📄 Description', value: `${oldTopic.substring(0, 100)} → ${newTopic.substring(0, 100)}`, inline: false }); + } + + // Changement de catégorie + if (oldChannel.parentId !== newChannel.parentId) { + const oldParent = oldChannel.parent?.name || '*Aucune*'; + const newParent = newChannel.parent?.name || '*Aucune*'; + changes.push({ name: '📂 Catégorie', value: `${oldParent} → ${newParent}`, inline: false }); + } + + // Changement de slowmode + if (oldChannel.rateLimitPerUser !== newChannel.rateLimitPerUser) { + const formatSlowmode = (seconds) => { + if (seconds === 0) return 'Désactivé'; + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m`; + return `${Math.floor(seconds / 3600)}h`; + }; + changes.push({ + name: '🐌 Slowmode', + value: `${formatSlowmode(oldChannel.rateLimitPerUser)} → ${formatSlowmode(newChannel.rateLimitPerUser)}`, + inline: true + }); + } + + // Changement NSFW + if (oldChannel.nsfw !== newChannel.nsfw) { + changes.push({ name: '🔞 NSFW', value: `${oldChannel.nsfw ? 'Oui' : 'Non'} → ${newChannel.nsfw ? 'Oui' : 'Non'}`, inline: true }); + } + + // Si aucun changement détecté, ignorer + if (changes.length === 0) return; + + let executor = null; + + try { + const auditLogs = await newChannel.guild.fetchAuditLogs({ + type: AuditLogEvent.ChannelUpdate, + limit: 1 + }); + + const updateLog = auditLogs.entries.first(); + if (updateLog && updateLog.target.id === newChannel.id && (Date.now() - updateLog.createdTimestamp) < 5000) { + executor = updateLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs channel update:', err); + } + + await sendLog(client, newChannel.guild.id, 'channels', { + action: 'update', + title: '✏️ Salon modifié', + description: `Le salon ${newChannel} a été modifié.`, + fields: changes, + executor: executor + }); + } +}; diff --git a/app/events/guildBanAdd.js b/app/events/guildBanAdd.js new file mode 100644 index 0000000..7422487 --- /dev/null +++ b/app/events/guildBanAdd.js @@ -0,0 +1,41 @@ +const { AuditLogEvent } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'guildBanAdd', + async execute(client, ban) { + const { guild, user } = ban; + + // Essayer de récupérer l'exécuteur depuis les audit logs + let executor = null; + let reason = 'Aucune raison spécifiée'; + + try { + const auditLogs = await guild.fetchAuditLogs({ + type: AuditLogEvent.MemberBan, + limit: 1 + }); + + const banLog = auditLogs.entries.first(); + if (banLog && banLog.target.id === user.id && (Date.now() - banLog.createdTimestamp) < 5000) { + executor = banLog.executor; + reason = banLog.reason || reason; + } + } catch (err) { + console.error('Erreur récupération audit logs ban:', err); + } + + await sendLog(client, guild.id, 'moderation', { + action: 'ban', + title: '🔨 Membre banni', + description: `**${user.tag}** a été banni du serveur.`, + fields: [ + { name: '👤 Utilisateur', value: `${user} (${user.id})`, inline: true }, + { name: '📝 Raison', value: reason, inline: false } + ], + thumbnail: user.displayAvatarURL({ size: 128 }), + user: user, + executor: executor + }); + } +}; diff --git a/app/events/guildBanRemove.js b/app/events/guildBanRemove.js new file mode 100644 index 0000000..af5b549 --- /dev/null +++ b/app/events/guildBanRemove.js @@ -0,0 +1,38 @@ +const { AuditLogEvent } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'guildBanRemove', + async execute(client, ban) { + const { guild, user } = ban; + + // Essayer de récupérer l'exécuteur depuis les audit logs + let executor = null; + + try { + const auditLogs = await guild.fetchAuditLogs({ + type: AuditLogEvent.MemberBanRemove, + limit: 1 + }); + + const unbanLog = auditLogs.entries.first(); + if (unbanLog && unbanLog.target.id === user.id && (Date.now() - unbanLog.createdTimestamp) < 5000) { + executor = unbanLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs unban:', err); + } + + await sendLog(client, guild.id, 'moderation', { + action: 'unban', + title: '🔓 Membre débanni', + description: `**${user.tag}** a été débanni du serveur.`, + fields: [ + { name: '👤 Utilisateur', value: `${user} (${user.id})`, inline: true } + ], + thumbnail: user.displayAvatarURL({ size: 128 }), + user: user, + executor: executor + }); + } +}; diff --git a/app/events/guildMemberAdd.js b/app/events/guildMemberAdd.js index 8134f6e..cbbce00 100644 --- a/app/events/guildMemberAdd.js +++ b/app/events/guildMemberAdd.js @@ -1,9 +1,33 @@ const { Events, EmbedBuilder } = require("discord.js"); const db = require("../db"); +const { sendLog } = require("../fonctions/sendLog"); module.exports = { name: Events.GuildMemberAdd, async execute(client, member) { + // ===== LOG MEMBRE REJOINT ===== + const accountAge = Math.floor((Date.now() - member.user.createdTimestamp) / (1000 * 60 * 60 * 24)); + const accountAgeStr = accountAge < 1 ? 'Moins d\'un jour' : + accountAge < 7 ? `${accountAge} jours ⚠️` : + accountAge < 30 ? `${accountAge} jours` : + accountAge < 365 ? `${Math.floor(accountAge / 30)} mois` : + `${Math.floor(accountAge / 365)} ans`; + + await sendLog(client, member.guild.id, 'members', { + action: 'join', + title: '📥 Membre rejoint', + description: `**${member.user.tag}** a rejoint le serveur.`, + fields: [ + { name: '👤 Membre', value: `${member} (${member.user.tag})`, inline: true }, + { name: '📊 Membres', value: `${member.guild.memberCount}`, inline: true }, + { name: '📅 Compte créé', value: ``, inline: true }, + { name: '⏳ Âge du compte', value: accountAgeStr, inline: true } + ], + thumbnail: member.user.displayAvatarURL({ size: 128 }), + user: member.user + }); + + // ===== MESSAGE DE BIENVENUE ===== db.get( "SELECT enabled, channel_id, message FROM welcome_config WHERE guild_id = ?", [member.guild.id], @@ -31,6 +55,8 @@ module.exports = { } } ); + + // ===== AUTOROLE ===== db.get( "SELECT enabled, role_id FROM autorole_newuser_config WHERE guild_id = ?", [member.guild.id], diff --git a/app/events/guildMemberRemove.js b/app/events/guildMemberRemove.js index 4546ec0..5f006e6 100644 --- a/app/events/guildMemberRemove.js +++ b/app/events/guildMemberRemove.js @@ -1,9 +1,65 @@ -const { Events, EmbedBuilder } = require("discord.js"); +const { Events, EmbedBuilder, AuditLogEvent } = require("discord.js"); const db = require("../db"); +const { sendLog } = require("../fonctions/sendLog"); module.exports = { name: Events.GuildMemberRemove, async execute(client, member) { + // ===== VÉRIFIER SI C'EST UN KICK ===== + let wasKicked = false; + let kickExecutor = null; + let kickReason = null; + + try { + const auditLogs = await member.guild.fetchAuditLogs({ + type: AuditLogEvent.MemberKick, + limit: 1 + }); + + const kickLog = auditLogs.entries.first(); + if (kickLog && kickLog.target.id === member.id && (Date.now() - kickLog.createdTimestamp) < 5000) { + wasKicked = true; + kickExecutor = kickLog.executor; + kickReason = kickLog.reason || 'Aucune raison spécifiée'; + } + } catch (err) { + // Pas de permission audit logs + } + + if (wasKicked) { + // ===== LOG KICK ===== + await sendLog(client, member.guild.id, 'moderation', { + action: 'kick', + title: '👢 Membre expulsé', + description: `**${member.user.tag}** a été expulsé du serveur.`, + fields: [ + { name: '👤 Membre', value: `${member.user} (${member.user.tag})`, inline: true }, + { name: '📝 Raison', value: kickReason, inline: false } + ], + thumbnail: member.user.displayAvatarURL({ size: 128 }), + user: member.user, + executor: kickExecutor + }); + } else { + // ===== LOG MEMBRE PARTI ===== + const joinedAt = member.joinedTimestamp ? + `` : 'Inconnu'; + + await sendLog(client, member.guild.id, 'members', { + action: 'leave', + title: '📤 Membre parti', + description: `**${member.user.tag}** a quitté le serveur.`, + fields: [ + { name: '👤 Membre', value: `${member.user} (${member.user.tag})`, inline: true }, + { name: '📊 Membres', value: `${member.guild.memberCount}`, inline: true }, + { name: '📅 Avait rejoint', value: joinedAt, inline: true } + ], + thumbnail: member.user.displayAvatarURL({ size: 128 }), + user: member.user + }); + } + + // ===== MESSAGE D'AU REVOIR ===== db.get( "SELECT enabled, channel_id, message FROM goodbye_config WHERE guild_id = ?", [member.guild.id], diff --git a/app/events/guildMemberUpdate.js b/app/events/guildMemberUpdate.js new file mode 100644 index 0000000..fc29725 --- /dev/null +++ b/app/events/guildMemberUpdate.js @@ -0,0 +1,180 @@ +const { AuditLogEvent } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'guildMemberUpdate', + async execute(client, oldMember, newMember) { + const guild = newMember.guild; + + // Vérifier les changements de rôles + const oldRoles = oldMember.roles.cache; + const newRoles = newMember.roles.cache; + + const addedRoles = newRoles.filter(role => !oldRoles.has(role.id)); + const removedRoles = oldRoles.filter(role => !newRoles.has(role.id)); + + // Log des rôles ajoutés + if (addedRoles.size > 0) { + let executor = null; + + try { + const auditLogs = await guild.fetchAuditLogs({ + type: AuditLogEvent.MemberRoleUpdate, + limit: 1 + }); + + const roleLog = auditLogs.entries.first(); + if (roleLog && roleLog.target.id === newMember.id && (Date.now() - roleLog.createdTimestamp) < 5000) { + executor = roleLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs role:', err); + } + + await sendLog(client, guild.id, 'members', { + action: 'add', + title: '✅ Rôle(s) ajouté(s)', + description: `**${newMember.user.tag}** a reçu de nouveaux rôles.`, + fields: [ + { name: '👤 Membre', value: `${newMember} (${newMember.user.tag})`, inline: true }, + { name: '🎭 Rôle(s)', value: addedRoles.map(r => r.toString()).join(', '), inline: false } + ], + thumbnail: newMember.user.displayAvatarURL({ size: 128 }), + user: newMember.user, + executor: executor + }); + } + + // Log des rôles retirés + if (removedRoles.size > 0) { + let executor = null; + + try { + const auditLogs = await guild.fetchAuditLogs({ + type: AuditLogEvent.MemberRoleUpdate, + limit: 1 + }); + + const roleLog = auditLogs.entries.first(); + if (roleLog && roleLog.target.id === newMember.id && (Date.now() - roleLog.createdTimestamp) < 5000) { + executor = roleLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs role:', err); + } + + await sendLog(client, guild.id, 'members', { + action: 'remove', + title: '❌ Rôle(s) retiré(s)', + description: `**${newMember.user.tag}** a perdu des rôles.`, + fields: [ + { name: '👤 Membre', value: `${newMember} (${newMember.user.tag})`, inline: true }, + { name: '🎭 Rôle(s)', value: removedRoles.map(r => r.toString()).join(', '), inline: false } + ], + thumbnail: newMember.user.displayAvatarURL({ size: 128 }), + user: newMember.user, + executor: executor + }); + } + + // Vérifier les changements de pseudo + if (oldMember.nickname !== newMember.nickname) { + let executor = null; + + try { + const auditLogs = await guild.fetchAuditLogs({ + type: AuditLogEvent.MemberUpdate, + limit: 1 + }); + + const nicknameLog = auditLogs.entries.first(); + if (nicknameLog && nicknameLog.target.id === newMember.id && (Date.now() - nicknameLog.createdTimestamp) < 5000) { + executor = nicknameLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs nickname:', err); + } + + await sendLog(client, guild.id, 'members', { + action: 'change', + title: '📝 Pseudo modifié', + fields: [ + { name: '👤 Membre', value: `${newMember} (${newMember.user.tag})`, inline: true }, + { name: '📝 Ancien pseudo', value: oldMember.nickname || '*Aucun*', inline: true }, + { name: '📝 Nouveau pseudo', value: newMember.nickname || '*Aucun*', inline: true } + ], + thumbnail: newMember.user.displayAvatarURL({ size: 128 }), + user: newMember.user, + executor: executor + }); + } + + // Vérifier les timeouts + const oldTimeout = oldMember.communicationDisabledUntil; + const newTimeout = newMember.communicationDisabledUntil; + + if (!oldTimeout && newTimeout) { + // Membre mis en timeout + let executor = null; + let reason = 'Aucune raison spécifiée'; + + try { + const auditLogs = await guild.fetchAuditLogs({ + type: AuditLogEvent.MemberUpdate, + limit: 1 + }); + + const timeoutLog = auditLogs.entries.first(); + if (timeoutLog && timeoutLog.target.id === newMember.id && (Date.now() - timeoutLog.createdTimestamp) < 5000) { + executor = timeoutLog.executor; + reason = timeoutLog.reason || reason; + } + } catch (err) { + console.error('Erreur récupération audit logs timeout:', err); + } + + await sendLog(client, guild.id, 'moderation', { + action: 'timeout', + title: '⏰ Membre mis en timeout', + description: `**${newMember.user.tag}** a été mis en timeout.`, + fields: [ + { name: '👤 Membre', value: `${newMember} (${newMember.user.tag})`, inline: true }, + { name: '⏱️ Expire', value: ``, inline: true }, + { name: '📝 Raison', value: reason, inline: false } + ], + thumbnail: newMember.user.displayAvatarURL({ size: 128 }), + user: newMember.user, + executor: executor + }); + } else if (oldTimeout && !newTimeout) { + // Timeout retiré + let executor = null; + + try { + const auditLogs = await guild.fetchAuditLogs({ + type: AuditLogEvent.MemberUpdate, + limit: 1 + }); + + const timeoutLog = auditLogs.entries.first(); + if (timeoutLog && timeoutLog.target.id === newMember.id && (Date.now() - timeoutLog.createdTimestamp) < 5000) { + executor = timeoutLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs untimeout:', err); + } + + await sendLog(client, guild.id, 'moderation', { + action: 'untimeout', + title: '✅ Timeout retiré', + description: `Le timeout de **${newMember.user.tag}** a été retiré.`, + fields: [ + { name: '👤 Membre', value: `${newMember} (${newMember.user.tag})`, inline: true } + ], + thumbnail: newMember.user.displayAvatarURL({ size: 128 }), + user: newMember.user, + executor: executor + }); + } + } +}; diff --git a/app/events/guildUpdate.js b/app/events/guildUpdate.js new file mode 100644 index 0000000..d2f6caf --- /dev/null +++ b/app/events/guildUpdate.js @@ -0,0 +1,101 @@ +const { AuditLogEvent } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'guildUpdate', + async execute(client, oldGuild, newGuild) { + const changes = []; + + // Changement de nom + if (oldGuild.name !== newGuild.name) { + changes.push({ name: '📝 Nom', value: `\`${oldGuild.name}\` → \`${newGuild.name}\``, inline: false }); + } + + // Changement d'icône + if (oldGuild.icon !== newGuild.icon) { + changes.push({ name: '🖼️ Icône', value: 'L\'icône du serveur a été modifiée', inline: false }); + } + + // Changement de bannière + if (oldGuild.banner !== newGuild.banner) { + changes.push({ name: '🎨 Bannière', value: 'La bannière du serveur a été modifiée', inline: false }); + } + + // Changement de description + if (oldGuild.description !== newGuild.description) { + const oldDesc = oldGuild.description || '*Aucune*'; + const newDesc = newGuild.description || '*Aucune*'; + changes.push({ name: '📄 Description', value: `${oldDesc.substring(0, 100)} → ${newDesc.substring(0, 100)}`, inline: false }); + } + + // Changement de région/locale + if (oldGuild.preferredLocale !== newGuild.preferredLocale) { + changes.push({ name: '🌍 Langue', value: `${oldGuild.preferredLocale} → ${newGuild.preferredLocale}`, inline: true }); + } + + // Changement du salon AFK + if (oldGuild.afkChannelId !== newGuild.afkChannelId) { + const oldChannel = oldGuild.afkChannel?.name || '*Aucun*'; + const newChannel = newGuild.afkChannel?.name || '*Aucun*'; + changes.push({ name: '💤 Salon AFK', value: `${oldChannel} → ${newChannel}`, inline: true }); + } + + // Changement du timeout AFK + if (oldGuild.afkTimeout !== newGuild.afkTimeout) { + const formatTimeout = (seconds) => `${Math.floor(seconds / 60)} minutes`; + changes.push({ name: '⏰ Timeout AFK', value: `${formatTimeout(oldGuild.afkTimeout)} → ${formatTimeout(newGuild.afkTimeout)}`, inline: true }); + } + + // Changement du niveau de vérification + if (oldGuild.verificationLevel !== newGuild.verificationLevel) { + const levels = ['Aucun', 'Faible', 'Moyen', 'Élevé', 'Très élevé']; + changes.push({ name: '🛡️ Niveau de vérification', value: `${levels[oldGuild.verificationLevel]} → ${levels[newGuild.verificationLevel]}`, inline: true }); + } + + // Changement du filtre de contenu explicite + if (oldGuild.explicitContentFilter !== newGuild.explicitContentFilter) { + const filters = ['Désactivé', 'Membres sans rôle', 'Tous les membres']; + changes.push({ name: '🔞 Filtre de contenu', value: `${filters[oldGuild.explicitContentFilter]} → ${filters[newGuild.explicitContentFilter]}`, inline: true }); + } + + // Changement du salon système + if (oldGuild.systemChannelId !== newGuild.systemChannelId) { + const oldChannel = oldGuild.systemChannel?.name || '*Aucun*'; + const newChannel = newGuild.systemChannel?.name || '*Aucun*'; + changes.push({ name: '📢 Salon système', value: `#${oldChannel} → #${newChannel}`, inline: true }); + } + + // Changement du propriétaire + if (oldGuild.ownerId !== newGuild.ownerId) { + changes.push({ name: '👑 Propriétaire', value: `<@${oldGuild.ownerId}> → <@${newGuild.ownerId}>`, inline: false }); + } + + // Si aucun changement détecté, ignorer + if (changes.length === 0) return; + + let executor = null; + + try { + const auditLogs = await newGuild.fetchAuditLogs({ + type: AuditLogEvent.GuildUpdate, + limit: 1 + }); + + const updateLog = auditLogs.entries.first(); + if (updateLog && (Date.now() - updateLog.createdTimestamp) < 5000) { + executor = updateLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs guild update:', err); + } + + await sendLog(client, newGuild.id, 'server', { + action: 'update', + title: '⚙️ Serveur modifié', + description: `Les paramètres du serveur ont été modifiés.`, + fields: changes, + thumbnail: newGuild.iconURL({ size: 128 }), + executor: executor + }); + } +}; diff --git a/app/events/inviteCreate.js b/app/events/inviteCreate.js new file mode 100644 index 0000000..1e88ba6 --- /dev/null +++ b/app/events/inviteCreate.js @@ -0,0 +1,41 @@ +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'inviteCreate', + async execute(client, invite) { + if (!invite.guild) return; + + const fields = [ + { name: '🔗 Code', value: invite.code, inline: true }, + { name: '📁 Salon', value: invite.channel ? `${invite.channel} (#${invite.channel.name})` : 'Inconnu', inline: true } + ]; + + if (invite.maxUses) { + fields.push({ name: '🔢 Utilisations max', value: invite.maxUses.toString(), inline: true }); + } + + if (invite.maxAge) { + const hours = Math.floor(invite.maxAge / 3600); + const minutes = Math.floor((invite.maxAge % 3600) / 60); + let expiration = ''; + if (hours > 0) expiration += `${hours}h `; + if (minutes > 0) expiration += `${minutes}m`; + if (!expiration) expiration = 'Jamais'; + fields.push({ name: '⏰ Expire dans', value: expiration, inline: true }); + } else { + fields.push({ name: '⏰ Expiration', value: 'Jamais', inline: true }); + } + + if (invite.temporary) { + fields.push({ name: '⏳ Temporaire', value: 'Oui (membres expulsés s\'ils quittent)', inline: true }); + } + + await sendLog(client, invite.guild.id, 'invites', { + action: 'create', + title: '🔗 Invitation créée', + description: `Une nouvelle invitation a été créée: **discord.gg/${invite.code}**`, + fields: fields, + executor: invite.inviter + }); + } +}; diff --git a/app/events/inviteDelete.js b/app/events/inviteDelete.js new file mode 100644 index 0000000..c38af7a --- /dev/null +++ b/app/events/inviteDelete.js @@ -0,0 +1,42 @@ +const { AuditLogEvent } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'inviteDelete', + async execute(client, invite) { + if (!invite.guild) return; + + let executor = null; + + try { + const auditLogs = await invite.guild.fetchAuditLogs({ + type: AuditLogEvent.InviteDelete, + limit: 1 + }); + + const deleteLog = auditLogs.entries.first(); + if (deleteLog && deleteLog.target.code === invite.code && (Date.now() - deleteLog.createdTimestamp) < 5000) { + executor = deleteLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs invite delete:', err); + } + + const fields = [ + { name: '🔗 Code', value: invite.code, inline: true }, + { name: '📁 Salon', value: invite.channel ? `#${invite.channel.name}` : 'Inconnu', inline: true } + ]; + + if (invite.uses !== null && invite.uses !== undefined) { + fields.push({ name: '📊 Utilisations', value: invite.uses.toString(), inline: true }); + } + + await sendLog(client, invite.guild.id, 'invites', { + action: 'delete', + title: '🗑️ Invitation supprimée', + description: `L'invitation **discord.gg/${invite.code}** a été supprimée.`, + fields: fields, + executor: executor + }); + } +}; diff --git a/app/events/messageDelete.js b/app/events/messageDelete.js new file mode 100644 index 0000000..2f20341 --- /dev/null +++ b/app/events/messageDelete.js @@ -0,0 +1,38 @@ +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'messageDelete', + async execute(client, message) { + // Ignorer les messages du bot et les messages système + if (!message.guild) return; + if (message.author?.bot) return; + if (!message.content && message.attachments.size === 0 && message.embeds.length === 0) return; + + const fields = [ + { name: '👤 Auteur', value: message.author ? `${message.author} (${message.author.tag})` : 'Inconnu', inline: true }, + { name: '📁 Salon', value: `${message.channel} (#${message.channel.name})`, inline: true } + ]; + + // Ajouter le contenu du message s'il existe + if (message.content) { + const content = message.content.length > 1024 + ? message.content.substring(0, 1021) + '...' + : message.content; + fields.push({ name: '💬 Contenu', value: content, inline: false }); + } + + // Ajouter les pièces jointes + if (message.attachments.size > 0) { + const attachments = message.attachments.map(a => `[${a.name}](${a.url})`).join('\n'); + fields.push({ name: '📎 Pièces jointes', value: attachments.substring(0, 1024), inline: false }); + } + + await sendLog(client, message.guild.id, 'messages', { + action: 'delete', + title: '🗑️ Message supprimé', + fields: fields, + thumbnail: message.author?.displayAvatarURL({ size: 128 }), + user: message.author + }); + } +}; diff --git a/app/events/messageDeleteBulk.js b/app/events/messageDeleteBulk.js new file mode 100644 index 0000000..4f50575 --- /dev/null +++ b/app/events/messageDeleteBulk.js @@ -0,0 +1,29 @@ +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'messageDeleteBulk', + async execute(client, messages, channel) { + if (!channel.guild) return; + + const messageList = messages.map(m => { + const author = m.author ? m.author.tag : 'Inconnu'; + const content = m.content + ? (m.content.length > 50 ? m.content.substring(0, 47) + '...' : m.content) + : '*Pas de contenu*'; + return `**${author}**: ${content}`; + }).slice(0, 10).join('\n'); + + const additionalCount = messages.size > 10 ? `\n... et ${messages.size - 10} autres messages` : ''; + + await sendLog(client, channel.guild.id, 'messages', { + action: 'delete', + title: '🗑️ Suppression en masse', + description: `**${messages.size}** messages ont été supprimés dans ${channel}.`, + fields: [ + { name: '📁 Salon', value: `${channel} (#${channel.name})`, inline: true }, + { name: '📊 Nombre', value: `${messages.size} messages`, inline: true }, + { name: '📝 Aperçu', value: (messageList + additionalCount).substring(0, 1024) || '*Aucun aperçu*', inline: false } + ] + }); + } +}; diff --git a/app/events/messageUpdate.js b/app/events/messageUpdate.js new file mode 100644 index 0000000..50e8284 --- /dev/null +++ b/app/events/messageUpdate.js @@ -0,0 +1,38 @@ +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'messageUpdate', + async execute(client, oldMessage, newMessage) { + // Ignorer si pas de guild ou si c'est un bot + if (!newMessage.guild) return; + if (newMessage.author?.bot) return; + + // Ignorer si le contenu n'a pas changé (peut être un embed qui se charge) + if (oldMessage.content === newMessage.content) return; + + // Ignorer les messages vides + if (!oldMessage.content && !newMessage.content) return; + + const oldContent = oldMessage.content + ? (oldMessage.content.length > 1024 ? oldMessage.content.substring(0, 1021) + '...' : oldMessage.content) + : '*Message vide ou non caché*'; + + const newContent = newMessage.content + ? (newMessage.content.length > 1024 ? newMessage.content.substring(0, 1021) + '...' : newMessage.content) + : '*Message vide*'; + + await sendLog(client, newMessage.guild.id, 'messages', { + action: 'edit', + title: '✏️ Message modifié', + description: `[Aller au message](${newMessage.url})`, + fields: [ + { name: '👤 Auteur', value: `${newMessage.author} (${newMessage.author.tag})`, inline: true }, + { name: '📁 Salon', value: `${newMessage.channel} (#${newMessage.channel.name})`, inline: true }, + { name: '📝 Avant', value: oldContent, inline: false }, + { name: '📝 Après', value: newContent, inline: false } + ], + thumbnail: newMessage.author?.displayAvatarURL({ size: 128 }), + user: newMessage.author + }); + } +}; diff --git a/app/events/roleCreate.js b/app/events/roleCreate.js new file mode 100644 index 0000000..d854afc --- /dev/null +++ b/app/events/roleCreate.js @@ -0,0 +1,35 @@ +const { AuditLogEvent } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'roleCreate', + async execute(client, role) { + let executor = null; + + try { + const auditLogs = await role.guild.fetchAuditLogs({ + type: AuditLogEvent.RoleCreate, + limit: 1 + }); + + const createLog = auditLogs.entries.first(); + if (createLog && createLog.target.id === role.id && (Date.now() - createLog.createdTimestamp) < 5000) { + executor = createLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs role create:', err); + } + + await sendLog(client, role.guild.id, 'roles', { + action: 'create', + title: '✅ Rôle créé', + description: `Le rôle ${role} a été créé.`, + fields: [ + { name: '🎭 Nom', value: role.name, inline: true }, + { name: '🎨 Couleur', value: role.hexColor || '#000000', inline: true }, + { name: '🆔 ID', value: role.id, inline: true } + ], + executor: executor + }); + } +}; diff --git a/app/events/roleDelete.js b/app/events/roleDelete.js new file mode 100644 index 0000000..3e61c7d --- /dev/null +++ b/app/events/roleDelete.js @@ -0,0 +1,35 @@ +const { AuditLogEvent } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'roleDelete', + async execute(client, role) { + let executor = null; + + try { + const auditLogs = await role.guild.fetchAuditLogs({ + type: AuditLogEvent.RoleDelete, + limit: 1 + }); + + const deleteLog = auditLogs.entries.first(); + if (deleteLog && deleteLog.target.id === role.id && (Date.now() - deleteLog.createdTimestamp) < 5000) { + executor = deleteLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs role delete:', err); + } + + await sendLog(client, role.guild.id, 'roles', { + action: 'delete', + title: '🗑️ Rôle supprimé', + description: `Le rôle **@${role.name}** a été supprimé.`, + fields: [ + { name: '🎭 Nom', value: role.name, inline: true }, + { name: '🎨 Couleur', value: role.hexColor || '#000000', inline: true }, + { name: '🆔 ID', value: role.id, inline: true } + ], + executor: executor + }); + } +}; diff --git a/app/events/roleUpdate.js b/app/events/roleUpdate.js new file mode 100644 index 0000000..9420629 --- /dev/null +++ b/app/events/roleUpdate.js @@ -0,0 +1,72 @@ +const { AuditLogEvent } = require('discord.js'); +const { sendLog } = require('../fonctions/sendLog'); + +module.exports = { + name: 'roleUpdate', + async execute(client, oldRole, newRole) { + const changes = []; + + // Changement de nom + if (oldRole.name !== newRole.name) { + changes.push({ name: '📝 Nom', value: `\`${oldRole.name}\` → \`${newRole.name}\``, inline: false }); + } + + // Changement de couleur + if (oldRole.hexColor !== newRole.hexColor) { + changes.push({ name: '🎨 Couleur', value: `${oldRole.hexColor} → ${newRole.hexColor}`, inline: true }); + } + + // Changement hoisted (affiché séparément) + if (oldRole.hoist !== newRole.hoist) { + changes.push({ name: '📊 Affiché séparément', value: `${oldRole.hoist ? 'Oui' : 'Non'} → ${newRole.hoist ? 'Oui' : 'Non'}`, inline: true }); + } + + // Changement mentionnable + if (oldRole.mentionable !== newRole.mentionable) { + changes.push({ name: '🔔 Mentionnable', value: `${oldRole.mentionable ? 'Oui' : 'Non'} → ${newRole.mentionable ? 'Oui' : 'Non'}`, inline: true }); + } + + // Changement de permissions + if (oldRole.permissions.bitfield !== newRole.permissions.bitfield) { + const oldPerms = oldRole.permissions.toArray(); + const newPerms = newRole.permissions.toArray(); + + const addedPerms = newPerms.filter(p => !oldPerms.includes(p)); + const removedPerms = oldPerms.filter(p => !newPerms.includes(p)); + + if (addedPerms.length > 0) { + changes.push({ name: '✅ Permissions ajoutées', value: addedPerms.slice(0, 10).join(', ') + (addedPerms.length > 10 ? '...' : ''), inline: false }); + } + if (removedPerms.length > 0) { + changes.push({ name: '❌ Permissions retirées', value: removedPerms.slice(0, 10).join(', ') + (removedPerms.length > 10 ? '...' : ''), inline: false }); + } + } + + // Si aucun changement détecté, ignorer + if (changes.length === 0) return; + + let executor = null; + + try { + const auditLogs = await newRole.guild.fetchAuditLogs({ + type: AuditLogEvent.RoleUpdate, + limit: 1 + }); + + const updateLog = auditLogs.entries.first(); + if (updateLog && updateLog.target.id === newRole.id && (Date.now() - updateLog.createdTimestamp) < 5000) { + executor = updateLog.executor; + } + } catch (err) { + console.error('Erreur récupération audit logs role update:', err); + } + + await sendLog(client, newRole.guild.id, 'roles', { + action: 'update', + title: '✏️ Rôle modifié', + description: `Le rôle ${newRole} a été modifié.`, + fields: changes, + executor: executor + }); + } +}; diff --git a/app/events/voiceStateUpdate.js b/app/events/voiceStateUpdate.js index 63cf83c..e38b56d 100644 --- a/app/events/voiceStateUpdate.js +++ b/app/events/voiceStateUpdate.js @@ -1,5 +1,6 @@ const { Events, ChannelType, PermissionFlagsBits } = require("discord.js"); const db = require("../db"); +const { sendLog } = require("../fonctions/sendLog"); // Store voice join times and intervals for economy const voiceJoinTimes = new Map(); // guildId_oderId -> timestamp @@ -11,6 +12,9 @@ module.exports = { // ===== PRIVATE ROOM (TEMP VOICE CHANNELS) ===== await handlePrivateRoom(client, oldState, newState); + // ===== VOICE LOGS ===== + await handleVoiceLogs(client, oldState, newState); + if (newState.member.user.bot) return; const guildId = newState.guild.id; @@ -272,3 +276,86 @@ async function trackVoiceTime(guildId, userId, oldState, newState) { // No action needed, session continues } } + +// ===== VOICE LOGS ===== +async function handleVoiceLogs(client, oldState, newState) { + // Ignorer les bots + if (newState.member?.user?.bot) return; + + const guild = newState.guild; + const member = newState.member; + if (!member) return; + + // Utilisateur rejoint un salon vocal + if (newState.channelId && !oldState.channelId) { + await sendLog(client, guild.id, 'voice', { + action: 'join', + title: '🔊 Connexion vocale', + description: `**${member.user.tag}** a rejoint un salon vocal.`, + fields: [ + { name: '👤 Membre', value: `${member} (${member.user.tag})`, inline: true }, + { name: '🔊 Salon', value: `${newState.channel}`, inline: true } + ], + thumbnail: member.user.displayAvatarURL({ size: 128 }), + user: member.user + }); + } + // Utilisateur quitte un salon vocal + else if (!newState.channelId && oldState.channelId) { + await sendLog(client, guild.id, 'voice', { + action: 'leave', + title: '🔇 Déconnexion vocale', + description: `**${member.user.tag}** a quitté un salon vocal.`, + fields: [ + { name: '👤 Membre', value: `${member} (${member.user.tag})`, inline: true }, + { name: '🔊 Salon', value: `${oldState.channel?.name || 'Inconnu'}`, inline: true } + ], + thumbnail: member.user.displayAvatarURL({ size: 128 }), + user: member.user + }); + } + // Utilisateur change de salon + else if (newState.channelId && oldState.channelId && newState.channelId !== oldState.channelId) { + await sendLog(client, guild.id, 'voice', { + action: 'move', + title: '🔀 Changement de salon', + description: `**${member.user.tag}** a changé de salon vocal.`, + fields: [ + { name: '👤 Membre', value: `${member} (${member.user.tag})`, inline: true }, + { name: '📤 Ancien salon', value: `${oldState.channel?.name || 'Inconnu'}`, inline: true }, + { name: '📥 Nouveau salon', value: `${newState.channel}`, inline: true } + ], + thumbnail: member.user.displayAvatarURL({ size: 128 }), + user: member.user + }); + } + + // Vérifier les changements de mute/deafen serveur + if (oldState.serverMute !== newState.serverMute) { + await sendLog(client, guild.id, 'voice', { + action: newState.serverMute ? 'timeout' : 'untimeout', + title: newState.serverMute ? '🔇 Mute serveur activé' : '🔊 Mute serveur désactivé', + description: `**${member.user.tag}** a été ${newState.serverMute ? 'muté' : 'démuté'} par le serveur.`, + fields: [ + { name: '👤 Membre', value: `${member} (${member.user.tag})`, inline: true }, + { name: '🔊 Salon', value: newState.channel ? `${newState.channel}` : 'N/A', inline: true } + ], + thumbnail: member.user.displayAvatarURL({ size: 128 }), + user: member.user + }); + } + + if (oldState.serverDeaf !== newState.serverDeaf) { + await sendLog(client, guild.id, 'voice', { + action: newState.serverDeaf ? 'timeout' : 'untimeout', + title: newState.serverDeaf ? '🔇 Sourd serveur activé' : '🔊 Sourd serveur désactivé', + description: `**${member.user.tag}** a été rendu ${newState.serverDeaf ? 'sourd' : 'non-sourd'} par le serveur.`, + fields: [ + { name: '👤 Membre', value: `${member} (${member.user.tag})`, inline: true }, + { name: '🔊 Salon', value: newState.channel ? `${newState.channel}` : 'N/A', inline: true } + ], + thumbnail: member.user.displayAvatarURL({ size: 128 }), + user: member.user + }); + } +} diff --git a/app/fonctions/sendLog.js b/app/fonctions/sendLog.js new file mode 100644 index 0000000..73f55be --- /dev/null +++ b/app/fonctions/sendLog.js @@ -0,0 +1,162 @@ +const { EmbedBuilder } = require('discord.js'); +const db = require('../db'); + +/** + * Couleurs pour les différents types d'actions + */ +const COLORS = { + // Actions positives (vert) + create: 0x57F287, + join: 0x57F287, + add: 0x57F287, + unban: 0x57F287, + untimeout: 0x57F287, + + // Actions neutres (bleu) + update: 0x5865F2, + edit: 0x5865F2, + move: 0x5865F2, + change: 0x5865F2, + + // Actions négatives (rouge) + delete: 0xED4245, + leave: 0xED4245, + remove: 0xED4245, + ban: 0xED4245, + kick: 0xED4245, + timeout: 0xED4245, + + // Avertissements (orange) + warn: 0xF0B232, + warning: 0xF0B232 +}; + +/** + * Envoie un log dans le salon approprié + * @param {Client} client - Le client Discord + * @param {string} guildId - L'ID du serveur + * @param {string} logType - Le type de log (moderation, voice, messages, members, channels, roles, invites, server) + * @param {object} options - Les options de l'embed + * @param {string} options.action - L'action effectuée (create, delete, update, join, leave, etc.) + * @param {string} options.title - Le titre de l'embed + * @param {string} [options.description] - La description de l'embed + * @param {Array} [options.fields] - Les champs de l'embed + * @param {string} [options.thumbnail] - URL de la miniature + * @param {string} [options.image] - URL de l'image + * @param {User|GuildMember} [options.user] - L'utilisateur concerné + * @param {User|GuildMember} [options.executor] - L'utilisateur qui a effectué l'action + */ +async function sendLog(client, guildId, logType, options) { + try { + // Récupérer la config des logs + const config = await db.getAsync( + "SELECT * FROM logs_config WHERE guild_id = ?", + [guildId] + ); + + // Vérifier si les logs sont activés + if (!config || !config.enabled) return; + + // Vérifier si ce type de log est activé + const typeEnabledField = `${logType}_enabled`; + const typeChannelField = `${logType}_channel_id`; + + if (!config[typeEnabledField]) return; + + const channelId = config[typeChannelField]; + if (!channelId) return; + + // Récupérer le salon + const guild = client.guilds.cache.get(guildId); + if (!guild) return; + + const channel = guild.channels.cache.get(channelId); + if (!channel) return; + + // Déterminer la couleur + const color = COLORS[options.action] || 0x5865F2; + + // Créer l'embed + const embed = new EmbedBuilder() + .setColor(color) + .setTitle(options.title) + .setTimestamp(); + + if (options.description) { + embed.setDescription(options.description); + } + + if (options.fields && options.fields.length > 0) { + embed.addFields(options.fields); + } + + if (options.thumbnail) { + embed.setThumbnail(options.thumbnail); + } + + if (options.image) { + embed.setImage(options.image); + } + + // Ajouter l'utilisateur concerné dans le footer si disponible + if (options.user) { + const user = options.user.user || options.user; + embed.setFooter({ + text: `ID: ${user.id}`, + iconURL: user.displayAvatarURL({ size: 32 }) + }); + } + + // Ajouter l'exécuteur si disponible + if (options.executor) { + const executor = options.executor.user || options.executor; + embed.addFields({ + name: '👤 Exécuté par', + value: `${executor} (${executor.tag || executor.username})`, + inline: true + }); + } + + // Envoyer le log + await channel.send({ embeds: [embed] }); + + } catch (err) { + console.error(`Erreur envoi log ${logType}:`, err); + } +} + +/** + * Récupère la config des logs pour un serveur + * @param {string} guildId - L'ID du serveur + * @returns {Promise} + */ +async function getLogsConfig(guildId) { + try { + return await db.getAsync( + "SELECT * FROM logs_config WHERE guild_id = ?", + [guildId] + ); + } catch (err) { + console.error('Erreur récupération config logs:', err); + return null; + } +} + +/** + * Vérifie si un type de log est activé + * @param {string} guildId - L'ID du serveur + * @param {string} logType - Le type de log + * @returns {Promise} + */ +async function isLogEnabled(guildId, logType) { + const config = await getLogsConfig(guildId); + if (!config || !config.enabled) return false; + return !!config[`${logType}_enabled`]; +} + +module.exports = { + sendLog, + getLogsConfig, + isLogEnabled, + COLORS +}; diff --git a/app/public/guild.css b/app/public/guild.css index 4e074fd..62ee5e7 100644 --- a/app/public/guild.css +++ b/app/public/guild.css @@ -1035,3 +1035,134 @@ body { object-fit: cover; display: none; } + +/* ===== Logs System ===== */ +.logs-types-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--spacing-md); +} + +.log-type-card { + background: var(--bg-dark); + border: 2px solid var(--border-color); + border-radius: var(--radius-md); + padding: var(--spacing-md); + transition: all 0.2s ease; + cursor: pointer; +} + +.log-type-card:hover { + border-color: var(--primary-color); + background: rgba(88, 101, 242, 0.05); +} + +.log-type-card.active { + border-color: var(--primary-color); + background: rgba(88, 101, 242, 0.1); +} + +.log-type-checkbox { + display: flex; + align-items: flex-start; + gap: var(--spacing-sm); + cursor: pointer; +} + +.log-type-checkbox input[type="checkbox"] { + margin-top: 4px; + width: 18px; + height: 18px; + accent-color: var(--primary-color); +} + +.log-type-icon { + font-size: 1.5rem; + line-height: 1; +} + +.log-type-info { + flex: 1; +} + +.log-type-name { + font-weight: 600; + color: var(--text-primary); + margin-bottom: 2px; +} + +.log-type-desc { + font-size: 0.85rem; + color: var(--text-muted); +} + +.log-type-status { + font-size: 1rem; + margin-left: auto; +} + +.logs-channels-preview { + background: var(--bg-dark); + border-radius: var(--radius-md); + padding: var(--spacing-md); +} + +.log-channel-item { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm) 0; + border-bottom: 1px solid var(--border-color); +} + +.log-channel-item:last-child { + border-bottom: none; +} + +.log-channel-icon { + color: var(--text-muted); + font-weight: 500; +} + +.log-channel-name { + flex: 1; + font-family: monospace; + color: var(--text-primary); +} + +.log-channel-status { + font-size: 0.85rem; + padding: 2px 8px; + border-radius: var(--radius-sm); +} + +.log-channel-status.created { + background: rgba(87, 242, 135, 0.15); + color: #57F287; +} + +.log-channel-status.pending { + background: rgba(240, 178, 50, 0.15); + color: #F0B232; +} + +.btn-group { + display: flex; + gap: var(--spacing-sm); +} + +.btn-danger { + background: var(--error-color); + color: white; + border: none; + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-sm); + cursor: pointer; + font-weight: 500; + transition: all 0.2s ease; +} + +.btn-danger:hover { + background: #c0392b; +} + diff --git a/app/public/guild.html b/app/public/guild.html index c9a8954..66eddf4 100644 --- a/app/public/guild.html +++ b/app/public/guild.html @@ -74,6 +74,10 @@ 🤖 Apparence du bot + + 📜 + Logs + @@ -1113,6 +1117,66 @@ + +
+
+
+
+ 📜 +

Système de Logs

+
+ +
+
+

+ Activez les logs pour suivre toutes les actions sur votre serveur. Le bot créera automatiquement les salons de logs dans une catégorie dédiée. +

+ + +
+ + + Sélectionnez une catégorie existante ou laissez vide pour en créer une nouvelle automatiquement. +
+ + +
+ +

Sélectionnez les types de logs que vous souhaitez activer. Un salon sera créé pour chaque type activé.

+ +
+ +
+
+ + + + +
+ +
+
+ @@ -1133,5 +1197,6 @@ + diff --git a/app/public/guild/logsForm.js b/app/public/guild/logsForm.js new file mode 100644 index 0000000..31a4aa1 --- /dev/null +++ b/app/public/guild/logsForm.js @@ -0,0 +1,244 @@ +// ===== LOGS FORM ===== +(function() { + const logsEnabled = document.getElementById('logs-enabled'); + const logsCategory = document.getElementById('logs-category'); + const logsTypesContainer = document.getElementById('logs-types-container'); + const logsPreviewContainer = document.getElementById('logs-preview-container'); + const logsChannelsPreview = document.getElementById('logs-channels-preview'); + const logsSaveBtn = document.getElementById('logs-save-btn'); + const logsDeleteBtn = document.getElementById('logs-delete-btn'); + const statusLogsForm = document.getElementById('status-logs-form'); + + let logTypes = []; + let currentConfig = null; + + // Charger la config des logs + async function loadLogsConfig() { + try { + const res = await fetch(`/api/bot/get-logs-config/${guildId}`); + const data = await res.json(); + + if (data.success) { + logTypes = data.logTypes || []; + currentConfig = data.config || {}; + + // Remplir le select des catégories + logsCategory.innerHTML = ''; + (data.categories || []).forEach(cat => { + const option = document.createElement('option'); + option.value = cat.id; + option.textContent = cat.name; + if (currentConfig.category_id === cat.id) { + option.selected = true; + } + logsCategory.appendChild(option); + }); + + // Activer/désactiver le toggle + logsEnabled.checked = !!currentConfig.enabled; + + // Générer les checkboxes pour les types de logs + renderLogTypes(); + + // Mettre à jour l'aperçu + updatePreview(); + + // Afficher le bouton supprimer si des salons existent + updateDeleteButton(); + } + } catch (err) { + console.error('Erreur chargement config logs:', err); + } + } + + // Générer les checkboxes des types de logs + function renderLogTypes() { + logsTypesContainer.innerHTML = ''; + + logTypes.forEach(logType => { + const isEnabled = currentConfig[`${logType.key}_enabled`]; + const channelId = currentConfig[`${logType.key}_channel_id`]; + + const div = document.createElement('div'); + div.className = 'log-type-card' + (isEnabled ? ' active' : ''); + div.innerHTML = ` + + `; + + const checkbox = div.querySelector('input[type="checkbox"]'); + checkbox.addEventListener('change', () => { + div.classList.toggle('active', checkbox.checked); + updatePreview(); + }); + + logsTypesContainer.appendChild(div); + }); + } + + // Mettre à jour l'aperçu des salons + function updatePreview() { + const checkedTypes = [...document.querySelectorAll('input[name="log-type"]:checked')] + .map(cb => cb.value); + + if (checkedTypes.length === 0 || !logsEnabled.checked) { + logsPreviewContainer.style.display = 'none'; + return; + } + + logsPreviewContainer.style.display = 'block'; + logsChannelsPreview.innerHTML = ''; + + checkedTypes.forEach(key => { + const logType = logTypes.find(lt => lt.key === key); + if (!logType) return; + + const channelId = currentConfig[`${key}_channel_id`]; + const div = document.createElement('div'); + div.className = 'log-channel-item'; + div.innerHTML = ` + # + ${logType.channelName} + + ${channelId ? '✅ Créé' : '⏳ Sera créé'} + + `; + logsChannelsPreview.appendChild(div); + }); + } + + // Mettre à jour le bouton supprimer + function updateDeleteButton() { + const hasChannels = logTypes.some(lt => currentConfig[`${lt.key}_channel_id`]); + logsDeleteBtn.style.display = hasChannels ? 'inline-flex' : 'none'; + } + + // Sauvegarder la config + logsSaveBtn.addEventListener('click', async () => { + const enabledLogs = [...document.querySelectorAll('input[name="log-type"]:checked')] + .map(cb => cb.value); + + logsSaveBtn.disabled = true; + logsSaveBtn.textContent = '⏳ Sauvegarde...'; + statusLogsForm.textContent = ''; + statusLogsForm.className = 'status-message'; + + try { + const res = await fetch('/api/bot/save-logs-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + guildId, + enabled: logsEnabled.checked, + categoryId: logsCategory.value || null, + enabledLogs + }) + }); + + const data = await res.json(); + + if (data.success) { + statusLogsForm.textContent = '✅ Configuration sauvegardée !'; + statusLogsForm.className = 'status-message success'; + + // Mettre à jour la config locale + if (data.categoryId) { + currentConfig.category_id = data.categoryId; + } + if (data.channels) { + for (const [key, channelId] of Object.entries(data.channels)) { + currentConfig[`${key}_channel_id`] = channelId; + if (enabledLogs.includes(key)) { + currentConfig[`${key}_enabled`] = 1; + } + } + } + + // Rafraîchir l'affichage + renderLogTypes(); + updatePreview(); + updateDeleteButton(); + + // Recharger les catégories + await loadLogsConfig(); + } else { + statusLogsForm.textContent = '❌ ' + (data.error || 'Erreur lors de la sauvegarde'); + statusLogsForm.className = 'status-message error'; + } + } catch (err) { + console.error('Erreur sauvegarde logs:', err); + statusLogsForm.textContent = '❌ Erreur de connexion'; + statusLogsForm.className = 'status-message error'; + } + + logsSaveBtn.disabled = false; + logsSaveBtn.textContent = '💾 Sauvegarder'; + }); + + // Supprimer tous les salons + logsDeleteBtn.addEventListener('click', async () => { + if (!confirm('⚠️ Êtes-vous sûr de vouloir supprimer tous les salons de logs ? Cette action est irréversible.')) { + return; + } + + logsDeleteBtn.disabled = true; + logsDeleteBtn.textContent = '⏳ Suppression...'; + statusLogsForm.textContent = ''; + + try { + const res = await fetch('/api/bot/delete-logs-channels', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ guildId }) + }); + + const data = await res.json(); + + if (data.success) { + statusLogsForm.textContent = '✅ Tous les salons de logs ont été supprimés.'; + statusLogsForm.className = 'status-message success'; + + // Reset la config locale + currentConfig = { enabled: false }; + logsEnabled.checked = false; + + // Décocher tous les types + document.querySelectorAll('input[name="log-type"]').forEach(cb => { + cb.checked = false; + cb.closest('.log-type-card')?.classList.remove('active'); + }); + + // Rafraîchir + renderLogTypes(); + updatePreview(); + updateDeleteButton(); + } else { + statusLogsForm.textContent = '❌ ' + (data.error || 'Erreur lors de la suppression'); + statusLogsForm.className = 'status-message error'; + } + } catch (err) { + console.error('Erreur suppression logs:', err); + statusLogsForm.textContent = '❌ Erreur de connexion'; + statusLogsForm.className = 'status-message error'; + } + + logsDeleteBtn.disabled = false; + logsDeleteBtn.textContent = '🗑️ Supprimer tous les salons'; + }); + + // Events toggle + logsEnabled.addEventListener('change', updatePreview); + + // Charger au démarrage + window.addEventListener('guildLoaded', loadLogsConfig); + if (typeof guildId !== 'undefined' && guildId) { + loadLogsConfig(); + } +})(); diff --git a/app/routes/api.js b/app/routes/api.js index 5c0c821..f029845 100644 --- a/app/routes/api.js +++ b/app/routes/api.js @@ -1281,5 +1281,321 @@ module.exports = (app, db, client) => { } }); + // ===== LOGS SYSTEM ===== + + // Types de logs disponibles + const LOG_TYPES = [ + { key: 'moderation', name: '📋 Modération', channelName: '📋・moderation-logs', description: 'Bans, kicks, timeouts, warns' }, + { key: 'voice', name: '🔊 Vocal', channelName: '🔊・voice-logs', description: 'Connexions/déconnexions vocales' }, + { key: 'messages', name: '💬 Messages', channelName: '💬・messages-logs', description: 'Messages édités/supprimés' }, + { key: 'members', name: '👥 Membres', channelName: '👥・members-logs', description: 'Arrivées/départs, rôles, pseudos' }, + { key: 'channels', name: '📁 Salons', channelName: '📁・channels-logs', description: 'Création/suppression de salons' }, + { key: 'roles', name: '🎭 Rôles', channelName: '🎭・roles-logs', description: 'Création/modification de rôles' }, + { key: 'invites', name: '🔗 Invitations', channelName: '🔗・invites-logs', description: 'Création/utilisation d\'invitations' }, + { key: 'server', name: '⚙️ Serveur', channelName: '⚙️・server-logs', description: 'Modifications du serveur' } + ]; + + // Obtenir la config des logs + router.get("/bot/get-logs-config/:guildId", async (req, res) => { + const { guildId } = req.params; + + if (!req.session.guilds) { + return res.status(401).json({ success: false, error: "Non connecté" }); + } + + try { + const row = await db.getAsync( + "SELECT * FROM logs_config WHERE guild_id = ?", + [guildId] + ); + + const guild = client.guilds.cache.get(guildId); + const categories = guild ? guild.channels.cache + .filter(c => c.type === 4) + .map(c => ({ id: c.id, name: c.name })) : []; + + res.json({ + success: true, + config: row || { enabled: false }, + categories, + logTypes: LOG_TYPES + }); + + } catch (err) { + console.error("Erreur get logs config:", err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + // Sauvegarder la config des logs ET créer les salons + router.post("/bot/save-logs-config", express.json(), async (req, res) => { + const { guildId, enabled, categoryId, enabledLogs } = 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 { + const guild = client.guilds.cache.get(guildId); + if (!guild) { + return res.status(404).json({ success: false, error: "Serveur non trouvé" }); + } + + // Récupérer l'ancienne config + let oldConfig = await db.getAsync("SELECT * FROM logs_config WHERE guild_id = ?", [guildId]); + + // Préparer les nouvelles valeurs + const newConfig = { + guild_id: guildId, + enabled: enabled ? 1 : 0, + category_id: categoryId || null, + moderation_enabled: 0, moderation_channel_id: oldConfig?.moderation_channel_id || null, + voice_enabled: 0, voice_channel_id: oldConfig?.voice_channel_id || null, + messages_enabled: 0, messages_channel_id: oldConfig?.messages_channel_id || null, + members_enabled: 0, members_channel_id: oldConfig?.members_channel_id || null, + channels_enabled: 0, channels_channel_id: oldConfig?.channels_channel_id || null, + roles_enabled: 0, roles_channel_id: oldConfig?.roles_channel_id || null, + invites_enabled: 0, invites_channel_id: oldConfig?.invites_channel_id || null, + server_enabled: 0, server_channel_id: oldConfig?.server_channel_id || null + }; + + // Créer/récupérer la catégorie si pas déjà sélectionnée + let category; + if (categoryId) { + category = guild.channels.cache.get(categoryId); + } else if (enabled && enabledLogs && enabledLogs.length > 0) { + // Créer une nouvelle catégorie pour les logs + category = await guild.channels.create({ + name: '📜 LOGS', + type: 4, // CategoryChannel + permissionOverwrites: [ + { + id: guild.id, + deny: ['ViewChannel'] + }, + { + id: client.user.id, + allow: ['ViewChannel', 'SendMessages', 'EmbedLinks'] + } + ] + }); + newConfig.category_id = category.id; + } + + // Pour chaque type de log activé, créer le salon si nécessaire + if (enabled && enabledLogs && category) { + for (const logKey of enabledLogs) { + const logType = LOG_TYPES.find(lt => lt.key === logKey); + if (!logType) continue; + + const enabledField = `${logKey}_enabled`; + const channelField = `${logKey}_channel_id`; + + newConfig[enabledField] = 1; + + // Vérifier si le salon existe déjà + let existingChannel = newConfig[channelField] ? + guild.channels.cache.get(newConfig[channelField]) : null; + + if (!existingChannel) { + // Créer le salon + const newChannel = await guild.channels.create({ + name: logType.channelName, + type: 0, // TextChannel + parent: category.id, + permissionOverwrites: [ + { + id: guild.id, + deny: ['ViewChannel'] + }, + { + id: client.user.id, + allow: ['ViewChannel', 'SendMessages', 'EmbedLinks', 'AttachFiles'] + } + ] + }); + newConfig[channelField] = newChannel.id; + } + } + } + + // Désactiver les logs non sélectionnés (mais garder les salons) + if (enabledLogs) { + for (const logType of LOG_TYPES) { + if (!enabledLogs.includes(logType.key)) { + newConfig[`${logType.key}_enabled`] = 0; + } + } + } + + // Sauvegarder en base + await new Promise((resolve, reject) => { + db.run(` + INSERT INTO logs_config ( + guild_id, enabled, category_id, + moderation_enabled, moderation_channel_id, + voice_enabled, voice_channel_id, + messages_enabled, messages_channel_id, + members_enabled, members_channel_id, + channels_enabled, channels_channel_id, + roles_enabled, roles_channel_id, + invites_enabled, invites_channel_id, + server_enabled, server_channel_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(guild_id) DO UPDATE SET + enabled = ?, category_id = ?, + moderation_enabled = ?, moderation_channel_id = ?, + voice_enabled = ?, voice_channel_id = ?, + messages_enabled = ?, messages_channel_id = ?, + members_enabled = ?, members_channel_id = ?, + channels_enabled = ?, channels_channel_id = ?, + roles_enabled = ?, roles_channel_id = ?, + invites_enabled = ?, invites_channel_id = ?, + server_enabled = ?, server_channel_id = ? + `, [ + newConfig.guild_id, newConfig.enabled, newConfig.category_id, + newConfig.moderation_enabled, newConfig.moderation_channel_id, + newConfig.voice_enabled, newConfig.voice_channel_id, + newConfig.messages_enabled, newConfig.messages_channel_id, + newConfig.members_enabled, newConfig.members_channel_id, + newConfig.channels_enabled, newConfig.channels_channel_id, + newConfig.roles_enabled, newConfig.roles_channel_id, + newConfig.invites_enabled, newConfig.invites_channel_id, + newConfig.server_enabled, newConfig.server_channel_id, + // ON CONFLICT values + newConfig.enabled, newConfig.category_id, + newConfig.moderation_enabled, newConfig.moderation_channel_id, + newConfig.voice_enabled, newConfig.voice_channel_id, + newConfig.messages_enabled, newConfig.messages_channel_id, + newConfig.members_enabled, newConfig.members_channel_id, + newConfig.channels_enabled, newConfig.channels_channel_id, + newConfig.roles_enabled, newConfig.roles_channel_id, + newConfig.invites_enabled, newConfig.invites_channel_id, + newConfig.server_enabled, newConfig.server_channel_id + ], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + + res.json({ + success: true, + categoryId: newConfig.category_id, + channels: { + moderation: newConfig.moderation_channel_id, + voice: newConfig.voice_channel_id, + messages: newConfig.messages_channel_id, + members: newConfig.members_channel_id, + channels: newConfig.channels_channel_id, + roles: newConfig.roles_channel_id, + invites: newConfig.invites_channel_id, + server: newConfig.server_channel_id + } + }); + + } catch (err) { + console.error("Erreur save logs config:", err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + // Supprimer tous les salons de logs + router.post("/bot/delete-logs-channels", express.json(), async (req, res) => { + const { guildId } = 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 { + const guild = client.guilds.cache.get(guildId); + if (!guild) { + return res.status(404).json({ success: false, error: "Serveur non trouvé" }); + } + + const config = await db.getAsync("SELECT * FROM logs_config WHERE guild_id = ?", [guildId]); + if (!config) { + return res.json({ success: true }); + } + + // Supprimer tous les salons de logs + const channelIds = [ + config.moderation_channel_id, + config.voice_channel_id, + config.messages_channel_id, + config.members_channel_id, + config.channels_channel_id, + config.roles_channel_id, + config.invites_channel_id, + config.server_channel_id + ].filter(Boolean); + + for (const channelId of channelIds) { + const channel = guild.channels.cache.get(channelId); + if (channel) { + try { + await channel.delete("Suppression du système de logs"); + } catch (e) { + console.error(`Erreur suppression salon ${channelId}:`, e.message); + } + } + } + + // Supprimer la catégorie si elle a été créée par le bot + if (config.category_id) { + const category = guild.channels.cache.get(config.category_id); + if (category && category.children.cache.size === 0) { + try { + await category.delete("Suppression du système de logs"); + } catch (e) { + console.error(`Erreur suppression catégorie:`, e.message); + } + } + } + + // Reset la config en base + await new Promise((resolve, reject) => { + db.run(` + UPDATE logs_config SET + enabled = 0, category_id = NULL, + moderation_enabled = 0, moderation_channel_id = NULL, + voice_enabled = 0, voice_channel_id = NULL, + messages_enabled = 0, messages_channel_id = NULL, + members_enabled = 0, members_channel_id = NULL, + channels_enabled = 0, channels_channel_id = NULL, + roles_enabled = 0, roles_channel_id = NULL, + invites_enabled = 0, invites_channel_id = NULL, + server_enabled = 0, server_channel_id = NULL + WHERE guild_id = ? + `, [guildId], function(err) { + if (err) reject(err); + else resolve(); + }); + }); + + res.json({ success: true }); + + } catch (err) { + console.error("Erreur delete logs channels:", err); + res.status(500).json({ success: false, error: err.message }); + } + }); + app.use("/api", router); };