clean
This commit is contained in:
parent
6a2ef3325c
commit
edda341afb
@ -1,247 +0,0 @@
|
||||
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(() => {});
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
{
|
||||
"discordIds": [],
|
||||
"playerIds": []
|
||||
}
|
||||
@ -1,390 +0,0 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const axios = require('axios');
|
||||
const os = require('node:os');
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const { execFile } = require('node:child_process');
|
||||
const { promisify } = require('node:util');
|
||||
|
||||
const { getAllLinks } = require('../../src/core/database.js');
|
||||
const {
|
||||
defaultBaseUrl,
|
||||
defaultServerId,
|
||||
listDirectory,
|
||||
downloadFile,
|
||||
uploadSingleFile,
|
||||
deleteFiles,
|
||||
} = require('../../src/pterodactyl/pterodactylFiles.js');
|
||||
|
||||
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();
|
||||
if (s.length !== 32) return '';
|
||||
if (!/^[0-9a-f]{32}$/.test(s)) return '';
|
||||
return s;
|
||||
};
|
||||
|
||||
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) {
|
||||
const status = await axios.get(`${baseUrl}/api/client/servers/${serverId}/resources`, { headers });
|
||||
const state = status?.data?.attributes?.current_state;
|
||||
if (state === wanted) return true;
|
||||
await sleep(5000);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function detectWorldId({ headers, baseUrl, serverId, saveRoot }) {
|
||||
const envWorldId = (process.env.PALWORLD_WORLD_ID || '').trim();
|
||||
if (envWorldId) return envWorldId;
|
||||
|
||||
const items = await listDirectory({ headers, baseUrl, serverId, directory: saveRoot });
|
||||
const dirs = items.filter((x) => x && x.is_file === false && typeof x.name === 'string');
|
||||
|
||||
if (dirs.length === 1) return dirs[0].name;
|
||||
|
||||
const names = dirs.map((d) => d.name).slice(0, 10).join(', ');
|
||||
throw new Error(
|
||||
`Impossible de déterminer automatiquement le World ID. Définis PALWORLD_WORLD_ID dans .env. Dossiers trouvés dans ${saveRoot}: ${names || '(aucun)'}`
|
||||
);
|
||||
}
|
||||
|
||||
async function runPlayersDeleter({ levelSavPath, uids }) {
|
||||
const execPath = (process.env.PLAYERS_DELETER_PATH || '').trim() || path.resolve(__dirname, '../../players-deleter');
|
||||
|
||||
// The shipped binary is Linux. If you run the bot on Windows, provide an alternate command.
|
||||
if (process.platform === 'win32') {
|
||||
throw new Error(
|
||||
"players-deleter est un exécutable Linux. Lance le bot sous Linux/WSL, ou définis PLAYERS_DELETER_PATH vers une commande compatible (ex: script Python)."
|
||||
);
|
||||
}
|
||||
|
||||
const args = [
|
||||
'--level-sav-path',
|
||||
levelSavPath,
|
||||
'--no-backup',
|
||||
'--keep-player-files',
|
||||
];
|
||||
|
||||
for (const uid of uids) {
|
||||
args.push('--uid', uid);
|
||||
}
|
||||
|
||||
if ((process.env.PLAYERS_DELETER_ALLOW_ZLIB_FALLBACK || '').toLowerCase() === 'true') {
|
||||
args.push('--allow-zlib-fallback');
|
||||
}
|
||||
|
||||
// Best effort: ensure the binary has execute permission on Linux containers.
|
||||
await fs.chmod(execPath, 0o755).catch(() => {});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
try {
|
||||
const result = await execFileAsync(execPath, args, { timeout: 10 * 60 * 1000 });
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} catch (error) {
|
||||
if (error && error.code === 'EACCES') {
|
||||
throw new Error(
|
||||
`Impossible d'exécuter players-deleter (${execPath}): permission refusée (EACCES). ` +
|
||||
`Vérifie que le fichier est exécutable (chmod +x) et que le filesystem n'est pas monté en noexec. ` +
|
||||
`Tu peux aussi définir PLAYERS_DELETER_PATH vers une commande exécutable.`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const out = String(stdout || '').trim();
|
||||
if (out) {
|
||||
try {
|
||||
return JSON.parse(out);
|
||||
} catch {
|
||||
// If not JSON, return raw.
|
||||
return { ok: true, stdout: out, stderr: String(stderr || '').trim() };
|
||||
}
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
return { ok: true, stdout: '', stderr: String(stderr).trim() };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('server-clean')
|
||||
.setDescription('Supprime les joueurs inactifs (3 mois) ou absents du Discord, puis redéploie la sauvegarde'),
|
||||
|
||||
async execute(interaction, headers) {
|
||||
if (!interaction.member.roles.cache.has(ROLE_RYGAINLAND)) {
|
||||
await interaction.reply({
|
||||
content: '❌ Il faut avoir le rôle Rygainland pour pouvoir utiliser cette commande.',
|
||||
flags: 64,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
const baseUrl = defaultBaseUrl();
|
||||
const serverId = defaultServerId();
|
||||
const apiHeaders = headers;
|
||||
|
||||
try {
|
||||
await interaction.editReply('🔎 Recherche des joueurs inactifs...');
|
||||
|
||||
const links = await getAllLinks();
|
||||
|
||||
const threeMonthsAgo = new Date();
|
||||
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
|
||||
|
||||
const targetLinks = [];
|
||||
const reasonsByDiscordId = new Map();
|
||||
|
||||
for (const link of links) {
|
||||
const discordId = String(link.discord_id);
|
||||
const member = await interaction.guild.members.fetch(discordId).catch(() => null);
|
||||
|
||||
const reasons = [];
|
||||
if (!member) {
|
||||
reasons.push('absent_discord');
|
||||
}
|
||||
|
||||
if (link.lastConnection) {
|
||||
const lastConn = new Date(link.lastConnection);
|
||||
if (lastConn < threeMonthsAgo) {
|
||||
reasons.push('inactif_3_mois');
|
||||
}
|
||||
}
|
||||
|
||||
if (reasons.length > 0) {
|
||||
targetLinks.push(link);
|
||||
reasonsByDiscordId.set(discordId, reasons);
|
||||
}
|
||||
}
|
||||
|
||||
// De-dupe by discord_id.
|
||||
const byDiscord = new Map();
|
||||
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 filteredLinks) {
|
||||
const uid = normUid32(link.player_id);
|
||||
if (!uid) {
|
||||
missingPlayerId.push(link);
|
||||
continue;
|
||||
}
|
||||
uidsToDelete.push(uid);
|
||||
}
|
||||
|
||||
if (uidsToDelete.length === 0) {
|
||||
const extra = missingPlayerId.length
|
||||
? `\n⚠️ ${missingPlayerId.length} membre(s) ciblé(s) sans Player ID en DB (impossible à supprimer automatiquement).`
|
||||
: '';
|
||||
const keepText = keepListExcludedCount
|
||||
? `\n🛡️ Exclus via liste blanche: ${keepListExcludedCount}`
|
||||
: '';
|
||||
await interaction.editReply(`✅ Aucun joueur supprimable trouvé.${extra}${keepText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.editReply(
|
||||
`🧹 Joueurs ciblés: **${uidsToDelete.length}** (inactifs/absents).\n` +
|
||||
`💾 Sauvegarde serveur...`
|
||||
);
|
||||
|
||||
// Check current state.
|
||||
const statusResponse = await axios.get(`${baseUrl}/api/client/servers/${serverId}/resources`, { headers: apiHeaders });
|
||||
const currentState = statusResponse.data.attributes.current_state;
|
||||
|
||||
if (currentState === 'running') {
|
||||
await axios.post(
|
||||
`${baseUrl}/api/client/servers/${serverId}/command`,
|
||||
{ command: 'save' },
|
||||
{ headers: apiHeaders }
|
||||
);
|
||||
await axios
|
||||
.post(
|
||||
`${baseUrl}/api/client/servers/${serverId}/command`,
|
||||
{ command: "broadcast 'Nettoyage des joueurs inactifs (arrêt serveur) - ~60s'" },
|
||||
{ headers: apiHeaders }
|
||||
)
|
||||
.catch(() => {});
|
||||
|
||||
await sleep(3000);
|
||||
|
||||
await interaction.editReply('⏹️ Arrêt du serveur...');
|
||||
await axios.post(
|
||||
`${baseUrl}/api/client/servers/${serverId}/power`,
|
||||
{ signal: 'stop' },
|
||||
{ headers: apiHeaders }
|
||||
);
|
||||
|
||||
const stopped = await waitForServerState({
|
||||
headers: apiHeaders,
|
||||
baseUrl,
|
||||
serverId,
|
||||
wanted: 'offline',
|
||||
timeoutMs: 3 * 60 * 1000,
|
||||
});
|
||||
if (!stopped) {
|
||||
await interaction.editReply('⚠️ Le serveur ne passe pas offline, tentative de continuer quand même...');
|
||||
}
|
||||
} else {
|
||||
await interaction.editReply('ℹ️ Serveur déjà arrêté. Passage à la récupération de la sauvegarde...');
|
||||
}
|
||||
|
||||
const saveRoot = (process.env.PALWORLD_SAVE_ROOT || '/Pal/Saved/SaveGames/0').trim();
|
||||
const worldId = await detectWorldId({ headers: apiHeaders, baseUrl, serverId, saveRoot });
|
||||
const worldDir = `${saveRoot}/${worldId}`;
|
||||
const levelSavRemote = `${worldDir}/Level.sav`;
|
||||
|
||||
await interaction.editReply(`📥 Téléchargement de Level.sav (World: ${worldId})...`);
|
||||
const levelSavBuf = await downloadFile({ headers: apiHeaders, baseUrl, serverId, file: levelSavRemote });
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'server-clean-'));
|
||||
const localLevelSav = path.join(tmpDir, 'Level.sav');
|
||||
await fs.writeFile(localLevelSav, levelSavBuf);
|
||||
|
||||
await interaction.editReply('🧬 Suppression des joueurs inactifs dans la sauvegarde...');
|
||||
const deleterReport = await runPlayersDeleter({ levelSavPath: localLevelSav, uids: uidsToDelete });
|
||||
|
||||
const newLevelSavBuf = await fs.readFile(localLevelSav);
|
||||
|
||||
await interaction.editReply('📤 Upload de Level.sav modifié...');
|
||||
await uploadSingleFile({
|
||||
headers: apiHeaders,
|
||||
baseUrl,
|
||||
serverId,
|
||||
directory: worldDir,
|
||||
filename: 'Level.sav',
|
||||
content: newLevelSavBuf,
|
||||
});
|
||||
|
||||
await interaction.editReply('🗑️ Suppression des fichiers Players/<uid>.sav...');
|
||||
const playersDir = `${worldDir}/Players`;
|
||||
|
||||
const filesToDelete = [];
|
||||
for (const uid of uidsToDelete) {
|
||||
filesToDelete.push(`${uid}.sav`);
|
||||
filesToDelete.push(`${uid}_dps.sav`);
|
||||
}
|
||||
|
||||
// Chunk deletes to keep requests reasonable.
|
||||
const CHUNK = 100;
|
||||
for (let i = 0; i < filesToDelete.length; i += CHUNK) {
|
||||
const slice = filesToDelete.slice(i, i + CHUNK);
|
||||
await deleteFiles({ headers: apiHeaders, baseUrl, serverId, root: playersDir, files: slice }).catch(() => {});
|
||||
}
|
||||
|
||||
await interaction.editReply('🚀 Démarrage du serveur...');
|
||||
await axios.post(
|
||||
`${baseUrl}/api/client/servers/${serverId}/power`,
|
||||
{ signal: 'start' },
|
||||
{ headers: apiHeaders }
|
||||
);
|
||||
|
||||
const running = await waitForServerState({
|
||||
headers: apiHeaders,
|
||||
baseUrl,
|
||||
serverId,
|
||||
wanted: 'running',
|
||||
timeoutMs: 4 * 60 * 1000,
|
||||
});
|
||||
|
||||
const missingText = missingPlayerId.length
|
||||
? `\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.
|
||||
const reportSummary = deleterReport && typeof deleterReport === 'object'
|
||||
? JSON.stringify(deleterReport).slice(0, 900)
|
||||
: '';
|
||||
|
||||
await interaction.editReply(
|
||||
`${okText}\n` +
|
||||
`👥 Joueurs supprimés (ciblés): ${uidsToDelete.length}${missingText}${keepText}\n` +
|
||||
(reportSummary ? `📄 players-deleter: ${reportSummary}` : '')
|
||||
);
|
||||
|
||||
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
||||
} catch (error) {
|
||||
console.error('Erreur /server-clean:', error);
|
||||
await interaction.editReply(`❌ Erreur pendant /server-clean: ${error.message || String(error)}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
2
index.js
2
index.js
@ -120,7 +120,7 @@ client.on(Events.InteractionCreate, async interaction => {
|
||||
await command.execute(interaction, process.env.PALWORLD_API_TOKEN);
|
||||
} else if (interaction.commandName === 'trad') {
|
||||
await command.execute(interaction, translator);
|
||||
} else if (interaction.commandName === 'start-server' || interaction.commandName === 'reboot-server' || interaction.commandName === 'server-clean') {
|
||||
} else if (interaction.commandName === 'start-server' || interaction.commandName === 'reboot-server') {
|
||||
await command.execute(interaction, headers);
|
||||
} else {
|
||||
await command.execute(interaction);
|
||||
|
||||
BIN
players-deleter
BIN
players-deleter
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user