// Spieler-Log: pro Score-Eintrag ein Datensatz mit IP, grobem Ort, Geraet & Browser. // Persistiert in data/players.json (append, gedeckelt). Nur approximativ (kein Login). import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import UAParser from 'ua-parser-js'; import geoip from 'geoip-lite'; const __dirname = dirname(fileURLToPath(import.meta.url)); const DATA_DIR = join(__dirname, '..', 'data'); const PLAYERS_FILE = join(DATA_DIR, 'players.json'); const MAX_RECORDS = 5000; function readAll() { try { if (!existsSync(PLAYERS_FILE)) return []; const data = JSON.parse(readFileSync(PLAYERS_FILE, 'utf8')); return Array.isArray(data) ? data : []; } catch { return []; } } function writeAll(list) { if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true }); writeFileSync(PLAYERS_FILE, JSON.stringify(list, null, 2)); } // User-Agent -> { device, browser, os } function parseUA(ua) { try { const r = new UAParser(ua || '').getResult(); return { device: r.device && r.device.type ? cap(r.device.type) : 'Desktop', browser: [r.browser.name, r.browser.version].filter(Boolean).join(' ') || 'unbekannt', os: [r.os.name, r.os.version].filter(Boolean).join(' ') || 'unbekannt', }; } catch { return { device: '?', browser: '?', os: '?' }; } } // IP -> { country, city } (offline, grob) function geoLookup(ip, cfCountry) { try { const g = geoip.lookup(cleanIp(ip)); if (g) return { country: g.country || cfCountry || '', city: g.city || '' }; } catch {} return { country: cfCountry || '', city: '' }; } function cleanIp(ip) { return String(ip || '').replace(/^::ffff:/, ''); } const cap = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s); // Einen Spiel-Abschluss protokollieren. export function logGame({ name, score, ip, ua, cfCountry }) { const { device, browser, os } = parseUA(ua); const { country, city } = geoLookup(ip, cfCountry); const list = readAll(); list.push({ name: String(name || 'Anonym'), score: Number(score) || 0, at: new Date().toISOString(), ip: cleanIp(ip), device, browser, os, country, city, }); writeAll(list.slice(-MAX_RECORDS)); } // Aggregiert die Log-Eintraege pro Spielername (case-insensitiv). export function aggregatePlayers() { const byName = new Map(); for (const r of readAll()) { const key = String(r.name || '').trim().toLowerCase(); if (!byName.has(key)) { byName.set(key, { name: r.name, games: 0, bestScore: 0, firstAt: r.at, lastAt: r.at, ips: new Set(), locations: new Set(), devices: new Set(), browsers: new Set(), oses: new Set(), }); } const p = byName.get(key); p.games += 1; p.bestScore = Math.max(p.bestScore, r.score || 0); if (r.at < p.firstAt) p.firstAt = r.at; if (r.at > p.lastAt) p.lastAt = r.at; if (r.ip) p.ips.add(r.ip); const loc = [r.city, r.country].filter(Boolean).join(', '); if (loc) p.locations.add(loc); if (r.device) p.devices.add(r.device); if (r.browser) p.browsers.add(r.browser); if (r.os) p.oses.add(r.os); } return [...byName.values()] .map((p) => ({ name: p.name, games: p.games, bestScore: p.bestScore, firstAt: p.firstAt, lastAt: p.lastAt, ips: [...p.ips], locations: [...p.locations], devices: [...p.devices], browsers: [...p.browsers], oses: [...p.oses], })) .sort((a, b) => new Date(b.lastAt) - new Date(a.lastAt)); }