root-me: refactor leaderboard refresh logic

This commit is contained in:
Lopinosaurus 2026-03-13 21:46:11 +01:00
parent 0529f2e4c9
commit 86838b0fb8
1 changed files with 52 additions and 113 deletions

165
server.js
View File

@ -220,17 +220,14 @@ app.get('/api/calendar', async (req, res) => {
}); });
// Root-me ranking // 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 rootmeCache = null;
let rootmePrevScores = {}; // login → last known score 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 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) { function parseRootmeUser(profile, id) {
const profileRaw = Array.isArray(profile) ? profile[0] : profile; 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 }; return { login: user.nom || id, score: Number(user.score) || 0, rank: user.position || null };
} }
async function fetchRootmeRanking(apiKey) { function startRootmePoller() {
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() {
const apiKey = process.env.ROOTME_API_KEY; const apiKey = process.env.ROOTME_API_KEY;
if (!apiKey) return; if (!apiKey) return;
let ids;
try { 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 delayMs = Math.max(ROOTME_MIN_DELAY_MS, Math.floor(ROOTME_TARGET_INTERVAL_MS / ids.length));
const isFirstPoll = Object.keys(rootmePrevScores).length === 0; const headers = { 'Cookie': `api_key=${apiKey}`, 'User-Agent': 'CyberDashboard/1.0' };
if (!isFirstPoll) {
ranking.forEach(user => { rootmeCache = [];
const prev = rootmePrevScores[user.login]; let idx = 0;
if (prev !== undefined && user.score > prev) {
const gained = user.score - prev; async function pollNext() {
console.log(`[rootme] FLAG ! ${user.login} +${gained} pts (${prev} -> ${user.score})`); const id = ids[idx];
broadcast({ type: 'rootme_flag', login: user.login, gained, newScore: user.score }); 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; }); setTimeout(pollNext, delayMs);
rootmeCache = ranking;
broadcast({ type: 'rootme_update', ranking });
console.log(`[rootme] polled — ${ranking.length} joueur(s)`);
} catch (err) {
console.error('[rootme] poll error:', err.message);
} }
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) => { app.get('/api/rootme', (req, res) => {
@ -360,7 +301,5 @@ app.get('/api/rootme', (req, res) => {
server.listen(PORT, () => { server.listen(PORT, () => {
console.log(`Cyber Dashboard running on http://localhost:${PORT}`); console.log(`Cyber Dashboard running on http://localhost:${PORT}`);
pollRootme(); startRootmePoller();
setInterval(pollRootme, ROOTME_POLL_MS);
setInterval(retryRateLimited, 30 * 1000);
}); });