From 86838b0fb80efd7e77755ed8af652e427c7a75af Mon Sep 17 00:00:00 2001 From: Lopinosaurus Date: Fri, 13 Mar 2026 21:46:11 +0100 Subject: [PATCH] root-me: refactor leaderboard refresh logic --- server.js | 165 +++++++++++++++++------------------------------------- 1 file changed, 52 insertions(+), 113 deletions(-) diff --git a/server.js b/server.js index a4dfd8b..e8561b5 100644 --- a/server.js +++ b/server.js @@ -220,17 +220,14 @@ app.get('/api/calendar', async (req, res) => { }); // Root-me ranking -const ROOTME_POLL_MS = 10 * 60 * 1000; +// Polling rotatif : on poll un joueur à la fois en rotation continue. +// Avec N joueurs et un intervalle cible de 2 min : délai entre chaque = 2min / N. +const ROOTME_TARGET_INTERVAL_MS = 2 * 60 * 1000; // refresh cible par joueur +const ROOTME_MIN_DELAY_MS = 10_000; // plancher anti-429 + let rootmeCache = null; let rootmePrevScores = {}; // login → last known score - -const ROOTME_REQUEST_DELAY_MS = 500; -const ROOTME_RETRY_BASE_MS = 2 * 60 * 1000; // 2 min, doublé à chaque échec -const ROOTME_RETRY_MAX = 3; const rootmePlayerCache = {}; // id → { login, score, rank } -const retryQueue = new Map(); // id → { attempts, nextRetry } - -const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); function parseRootmeUser(profile, id) { const profileRaw = Array.isArray(profile) ? profile[0] : profile; @@ -239,117 +236,61 @@ function parseRootmeUser(profile, id) { return { login: user.nom || id, score: Number(user.score) || 0, rank: user.position || null }; } -async function fetchRootmeRanking(apiKey) { - const raw = fs.readFileSync(path.resolve('logins.txt'), 'utf8'); - const ids = raw.split('\n').map(l => l.trim()).filter(Boolean); - const headers = { 'Cookie': `api_key=${apiKey}`, 'User-Agent': 'CyberDashboard/1.0' }; - - const results = []; - for (const id of ids) { - try { - const resp = await fetch( - `https://api.www.root-me.org/auteurs/${id}`, - { headers, timeout: 10000 } - ); - if (resp.status === 429) { - console.warn(`[rootme] rate-limited on id "${id}", scheduling retry`); - if (rootmePlayerCache[id]) results.push(rootmePlayerCache[id]); - retryQueue.set(id, { attempts: 1, nextRetry: Date.now() + ROOTME_RETRY_BASE_MS }); - } else { - const entry = parseRootmeUser(await resp.json(), id); - if (entry) { rootmePlayerCache[id] = entry; results.push(entry); } - } - } catch (err) { - console.error(`[rootme] fetch error for id "${id}":`, err.message); - if (rootmePlayerCache[id]) results.push(rootmePlayerCache[id]); - } - await sleep(ROOTME_REQUEST_DELAY_MS); - } - - return results.sort((a, b) => b.score - a.score); -} - -async function retryRateLimited() { - const apiKey = process.env.ROOTME_API_KEY; - if (!apiKey || retryQueue.size === 0) return; - - const now = Date.now(); - const headers = { 'Cookie': `api_key=${apiKey}`, 'User-Agent': 'CyberDashboard/1.0' }; - - for (const [id, state] of retryQueue) { - if (now < state.nextRetry) continue; - - try { - const resp = await fetch( - `https://api.www.root-me.org/auteurs/${id}`, - { headers, timeout: 10000 } - ); - - if (resp.status === 429) { - if (state.attempts >= ROOTME_RETRY_MAX) { - console.warn(`[rootme] retry exhausted for id "${id}", giving up until next poll`); - retryQueue.delete(id); - } else { - state.attempts++; - state.nextRetry = Date.now() + ROOTME_RETRY_BASE_MS * Math.pow(2, state.attempts - 1); - console.warn(`[rootme] retry 429 for id "${id}" (attempt ${state.attempts}/${ROOTME_RETRY_MAX}), next in ${Math.round((state.nextRetry - Date.now()) / 60000)} min`); - } - } else { - const entry = parseRootmeUser(await resp.json(), id); - if (entry) { - const prev = rootmePrevScores[entry.login]; - if (prev !== undefined && entry.score > prev) { - const gained = entry.score - prev; - console.log(`[rootme] FLAG (retry) ! ${entry.login} +${gained} pts`); - broadcast({ type: 'rootme_flag', login: entry.login, gained, newScore: entry.score }); - } - rootmePlayerCache[id] = entry; - rootmePrevScores[entry.login] = entry.score; - if (rootmeCache) { - const idx = rootmeCache.findIndex(u => u.login === entry.login); - if (idx !== -1) rootmeCache[idx] = entry; else rootmeCache.push(entry); - rootmeCache.sort((a, b) => b.score - a.score); - broadcast({ type: 'rootme_update', ranking: rootmeCache }); - } - console.log(`[rootme] retry OK for id "${id}" (${entry.login})`); - } - retryQueue.delete(id); - } - } catch (err) { - console.error(`[rootme] retry error for id "${id}":`, err.message); - retryQueue.delete(id); - } - await sleep(ROOTME_REQUEST_DELAY_MS); - } -} - -async function pollRootme() { +function startRootmePoller() { const apiKey = process.env.ROOTME_API_KEY; if (!apiKey) return; + let ids; try { - const ranking = await fetchRootmeRanking(apiKey); + ids = fs.readFileSync(path.resolve('logins.txt'), 'utf8') + .split('\n').map(l => l.trim()).filter(Boolean); + } catch (err) { + console.error('[rootme] cannot read logins.txt:', err.message); + return; + } + if (!ids.length) return; - // Detect score gains and broadcast flag events - const isFirstPoll = Object.keys(rootmePrevScores).length === 0; - if (!isFirstPoll) { - ranking.forEach(user => { - const prev = rootmePrevScores[user.login]; - if (prev !== undefined && user.score > prev) { - const gained = user.score - prev; - console.log(`[rootme] FLAG ! ${user.login} +${gained} pts (${prev} -> ${user.score})`); - broadcast({ type: 'rootme_flag', login: user.login, gained, newScore: user.score }); + const delayMs = Math.max(ROOTME_MIN_DELAY_MS, Math.floor(ROOTME_TARGET_INTERVAL_MS / ids.length)); + const headers = { 'Cookie': `api_key=${apiKey}`, 'User-Agent': 'CyberDashboard/1.0' }; + + rootmeCache = []; + let idx = 0; + + async function pollNext() { + const id = ids[idx]; + idx = (idx + 1) % ids.length; + + try { + const resp = await fetch(`https://api.www.root-me.org/auteurs/${id}`, { headers, timeout: 10000 }); + if (resp.status === 429) { + console.warn(`[rootme] 429 pour id "${id}", prochain tour dans ${delayMs / 1000}s`); + } else { + const entry = parseRootmeUser(await resp.json(), id); + if (entry) { + rootmePlayerCache[id] = entry; + const prev = rootmePrevScores[entry.login]; + if (prev !== undefined && entry.score > prev) { + const gained = entry.score - prev; + console.log(`[rootme] FLAG ! ${entry.login} +${gained} pts (${prev} → ${entry.score})`); + broadcast({ type: 'rootme_flag', login: entry.login, gained, newScore: entry.score }); + } + rootmePrevScores[entry.login] = entry.score; + + const i = rootmeCache.findIndex(u => u.login === entry.login); + if (i !== -1) rootmeCache[i] = entry; else rootmeCache.push(entry); + rootmeCache.sort((a, b) => b.score - a.score); + broadcast({ type: 'rootme_update', ranking: rootmeCache }); } - }); + } + } catch (err) { + console.error(`[rootme] erreur pour id "${id}":`, err.message); } - ranking.forEach(u => { rootmePrevScores[u.login] = u.score; }); - rootmeCache = ranking; - broadcast({ type: 'rootme_update', ranking }); - console.log(`[rootme] polled — ${ranking.length} joueur(s)`); - } catch (err) { - console.error('[rootme] poll error:', err.message); + setTimeout(pollNext, delayMs); } + + console.log(`[rootme] démarrage polling rotatif — ${ids.length} joueur(s), 1 requête toutes les ${delayMs / 1000}s → refresh ~${Math.round(ROOTME_TARGET_INTERVAL_MS / 60000)} min/joueur`); + pollNext(); } app.get('/api/rootme', (req, res) => { @@ -360,7 +301,5 @@ app.get('/api/rootme', (req, res) => { server.listen(PORT, () => { console.log(`Cyber Dashboard running on http://localhost:${PORT}`); - pollRootme(); - setInterval(pollRootme, ROOTME_POLL_MS); - setInterval(retryRateLimited, 30 * 1000); + startRootmePoller(); });