diff --git a/commands/server/server-clean-keep.js b/commands/server/server-clean-keep.js new file mode 100644 index 0000000..db6873d --- /dev/null +++ b/commands/server/server-clean-keep.js @@ -0,0 +1,247 @@ +const { SlashCommandBuilder, MessageFlags } = require('discord.js'); +const fs = require('node:fs/promises'); +const path = require('node:path'); + +const ROLE_RYGAINLAND = '1444684935632912394'; +const KEEP_LIST_FILE_PATH = path.resolve(__dirname, 'server-clean-keep.json'); + +const normUid32 = (val) => { + if (!val) return ''; + const s = String(val).replace(/-/g, '').trim().toLowerCase(); + if (s.length !== 32) return ''; + if (!/^[0-9a-f]{32}$/.test(s)) return ''; + return s; +}; + +async function readKeepList() { + try { + const raw = await fs.readFile(KEEP_LIST_FILE_PATH, 'utf8'); + const parsed = JSON.parse(raw); + + const discordIds = Array.isArray(parsed.discordIds) + ? parsed.discordIds.map((x) => String(x || '').trim()).filter(Boolean) + : []; + + const playerIds = Array.isArray(parsed.playerIds) + ? parsed.playerIds.map(normUid32).filter(Boolean) + : []; + + return { + discordIds: [...new Set(discordIds)], + playerIds: [...new Set(playerIds)], + }; + } catch (error) { + if (error && error.code === 'ENOENT') { + return { discordIds: [], playerIds: [] }; + } + throw error; + } +} + +async function writeKeepList(keepList) { + const payload = { + discordIds: [...new Set((keepList.discordIds || []).map((x) => String(x || '').trim()).filter(Boolean))], + playerIds: [...new Set((keepList.playerIds || []).map(normUid32).filter(Boolean))], + }; + + await fs.writeFile(KEEP_LIST_FILE_PATH, JSON.stringify(payload, null, 2), 'utf8'); +} + +module.exports = { + data: new SlashCommandBuilder() + .setName('server-clean-keep') + .setDescription('Gérer la liste blanche des joueurs protégés du /server-clean') + .addSubcommand((sub) => + sub + .setName('list') + .setDescription('Afficher la liste blanche actuelle') + ) + .addSubcommand((sub) => + sub + .setName('add-discord') + .setDescription('Ajouter un compte Discord à la liste blanche') + .addUserOption((opt) => + opt + .setName('membre') + .setDescription('Compte Discord à protéger') + .setRequired(true) + ) + ) + .addSubcommand((sub) => + sub + .setName('remove-discord') + .setDescription('Retirer un compte Discord de la liste blanche') + .addUserOption((opt) => + opt + .setName('membre') + .setDescription('Compte Discord à retirer') + .setRequired(true) + ) + ) + .addSubcommand((sub) => + sub + .setName('add-player') + .setDescription('Ajouter un Player ID Palworld à la liste blanche') + .addStringOption((opt) => + opt + .setName('player-id') + .setDescription('UID hexadécimal (32 caractères)') + .setRequired(true) + ) + ) + .addSubcommand((sub) => + sub + .setName('remove-player') + .setDescription('Retirer un Player ID Palworld de la liste blanche') + .addStringOption((opt) => + opt + .setName('player-id') + .setDescription('UID hexadécimal (32 caractères)') + .setRequired(true) + ) + ), + + async execute(interaction) { + if (!interaction.member.roles.cache.has(ROLE_RYGAINLAND)) { + await interaction.reply({ + content: '❌ Il faut avoir le rôle Rygainland pour utiliser cette commande.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + const sub = interaction.options.getSubcommand(); + + try { + const keepList = await readKeepList(); + + if (sub === 'list') { + const discordText = keepList.discordIds.length + ? keepList.discordIds.map((id) => `- <@${id}> (${id})`).join('\n') + : '- (vide)'; + + const playerText = keepList.playerIds.length + ? keepList.playerIds.map((id) => `- ${id}`).join('\n') + : '- (vide)'; + + await interaction.reply({ + content: + '🛡️ Liste blanche /server-clean\n\n' + + `Discord IDs (${keepList.discordIds.length})\n${discordText}\n\n` + + `Player IDs (${keepList.playerIds.length})\n${playerText}`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (sub === 'add-discord') { + const user = interaction.options.getUser('membre', true); + if (!keepList.discordIds.includes(user.id)) { + keepList.discordIds.push(user.id); + await writeKeepList(keepList); + await interaction.reply({ + content: `✅ Ajouté à la liste blanche Discord: <@${user.id}> (${user.id})`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + await interaction.reply({ + content: `ℹ️ Déjà présent dans la liste blanche Discord: <@${user.id}>`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (sub === 'remove-discord') { + const user = interaction.options.getUser('membre', true); + const before = keepList.discordIds.length; + keepList.discordIds = keepList.discordIds.filter((id) => id !== user.id); + + if (keepList.discordIds.length !== before) { + await writeKeepList(keepList); + await interaction.reply({ + content: `✅ Retiré de la liste blanche Discord: <@${user.id}> (${user.id})`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + await interaction.reply({ + content: `ℹ️ Ce compte n'était pas dans la liste blanche Discord: <@${user.id}>`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (sub === 'add-player') { + const raw = interaction.options.getString('player-id', true); + const uid = normUid32(raw); + if (!uid) { + await interaction.reply({ + content: '❌ Player ID invalide. Format attendu: 32 caractères hexadécimaux.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (!keepList.playerIds.includes(uid)) { + keepList.playerIds.push(uid); + await writeKeepList(keepList); + await interaction.reply({ + content: `✅ Player ID ajouté à la liste blanche: ${uid}`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + await interaction.reply({ + content: `ℹ️ Player ID déjà présent dans la liste blanche: ${uid}`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (sub === 'remove-player') { + const raw = interaction.options.getString('player-id', true); + const uid = normUid32(raw); + if (!uid) { + await interaction.reply({ + content: '❌ Player ID invalide. Format attendu: 32 caractères hexadécimaux.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + const before = keepList.playerIds.length; + keepList.playerIds = keepList.playerIds.filter((id) => id !== uid); + + if (keepList.playerIds.length !== before) { + await writeKeepList(keepList); + await interaction.reply({ + content: `✅ Player ID retiré de la liste blanche: ${uid}`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + await interaction.reply({ + content: `ℹ️ Player ID non trouvé dans la liste blanche: ${uid}`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + await interaction.reply({ + content: '❌ Sous-commande inconnue.', + flags: MessageFlags.Ephemeral, + }); + } catch (error) { + console.error('Erreur server-clean-keep:', error); + await interaction.reply({ + content: `❌ Erreur: ${error.message || String(error)}`, + flags: MessageFlags.Ephemeral, + }).catch(() => {}); + } + }, +}; diff --git a/commands/server/server-clean-keep.json b/commands/server/server-clean-keep.json new file mode 100644 index 0000000..1c42c3a --- /dev/null +++ b/commands/server/server-clean-keep.json @@ -0,0 +1,4 @@ +{ + "discordIds": [], + "playerIds": [] +} diff --git a/commands/server/server-clean.js b/commands/server/server-clean.js index 397b009..1992530 100644 --- a/commands/server/server-clean.js +++ b/commands/server/server-clean.js @@ -20,6 +20,8 @@ const execFileAsync = promisify(execFile); const ROLE_RYGAINLAND = '1444684935632912394'; +const keepListFilePath = path.resolve(__dirname, 'server-clean-keep.json'); + const normUid32 = (val) => { if (!val) return ''; const s = String(val).replace(/-/g, '').trim().toLowerCase(); @@ -30,6 +32,45 @@ const normUid32 = (val) => { const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const parseCsv = (val) => + String(val || '') + .split(',') + .map((x) => x.trim()) + .filter(Boolean); + +async function loadKeepList() { + const keepDiscordIds = new Set(parseCsv(process.env.SERVER_CLEAN_KEEP_DISCORD_IDS)); + const keepPlayerIds = new Set(parseCsv(process.env.SERVER_CLEAN_KEEP_PLAYER_IDS).map(normUid32).filter(Boolean)); + + try { + const raw = await fs.readFile(keepListFilePath, 'utf8'); + const parsed = JSON.parse(raw); + + if (Array.isArray(parsed.discordIds)) { + for (const id of parsed.discordIds) { + const clean = String(id || '').trim(); + if (clean) keepDiscordIds.add(clean); + } + } + + if (Array.isArray(parsed.playerIds)) { + for (const uid of parsed.playerIds) { + const clean = normUid32(uid); + if (clean) keepPlayerIds.add(clean); + } + } + } catch (error) { + if (error && error.code !== 'ENOENT') { + throw new Error(`Impossible de lire ${path.basename(keepListFilePath)}: ${error.message || String(error)}`); + } + } + + return { + keepDiscordIds, + keepPlayerIds, + }; +} + async function waitForServerState({ headers, baseUrl, serverId, wanted, timeoutMs }) { const started = Date.now(); while (Date.now() - started < timeoutMs) { @@ -158,10 +199,26 @@ module.exports = { for (const l of targetLinks) byDiscord.set(String(l.discord_id), l); const deduped = [...byDiscord.values()]; + const keepList = await loadKeepList(); + const filteredLinks = []; + let keepListExcludedCount = 0; + + for (const link of deduped) { + const discordId = String(link.discord_id || '').trim(); + const uid = normUid32(link.player_id); + + if (keepList.keepDiscordIds.has(discordId) || (uid && keepList.keepPlayerIds.has(uid))) { + keepListExcludedCount += 1; + continue; + } + + filteredLinks.push(link); + } + const uidsToDelete = []; const missingPlayerId = []; - for (const link of deduped) { + for (const link of filteredLinks) { const uid = normUid32(link.player_id); if (!uid) { missingPlayerId.push(link); @@ -174,7 +231,10 @@ module.exports = { const extra = missingPlayerId.length ? `\n⚠️ ${missingPlayerId.length} membre(s) ciblé(s) sans Player ID en DB (impossible à supprimer automatiquement).` : ''; - await interaction.editReply(`✅ Aucun joueur supprimable trouvé.${extra}`); + const keepText = keepListExcludedCount + ? `\n🛡️ Exclus via liste blanche: ${keepListExcludedCount}` + : ''; + await interaction.editReply(`✅ Aucun joueur supprimable trouvé.${extra}${keepText}`); return; } @@ -286,6 +346,10 @@ module.exports = { ? `\n⚠️ Non supprimés (pas de Player ID en DB): ${missingPlayerId.length}` : ''; + const keepText = keepListExcludedCount + ? `\n🛡️ Exclus via liste blanche: ${keepListExcludedCount}` + : ''; + const okText = running ? '✅ Nettoyage terminé et serveur redémarré.' : '⚠️ Nettoyage terminé, mais le serveur n\'a pas confirmé son état running.'; // Keep output short; deleterReport can be long. @@ -295,7 +359,7 @@ module.exports = { await interaction.editReply( `${okText}\n` + - `👥 Joueurs supprimés (ciblés): ${uidsToDelete.length}${missingText}\n` + + `👥 Joueurs supprimés (ciblés): ${uidsToDelete.length}${missingText}${keepText}\n` + (reportSummary ? `📄 players-deleter: ${reportSummary}` : '') );