/* OGame Spy Auto-Importer — background (service worker MV3) * * Ruolo: unico punto che parla con la piattaforma. * Riceve i token dal content script (pagine OGame) o dal popup (import * manuale), li carica con POST /api/reports e aggiorna badge/notifiche. * * Politica: un solo tentativo per token. Se la piattaforma non risponde * il token NON viene reinviato: viene mostrata una notifica d'errore e * basta. Nessun timer, nessun ping periodico (MV3: il service worker * può essere sospeso in qualsiasi momento). */ 'use strict'; const SETTINGS_KEY = 'settings'; const LAST_IMPORT_KEY = 'lastImport'; const SEEN_KEY = 'seenTokens'; // rid dei rapporti già importati const SEEN_MAX = 500; // voci tenute in memoria const SEEN_TTL_MS = 60 * 24 * 3600 * 1000; // scadenza voce: 60 giorni const DEFAULTS = { host: 'localhost', port: 8899, autoImport: true, // cattura automatica dagli appunti su OGame notifications: true }; // Stessa forma accettata dal server (report_store.parse_token) const TOKEN_RE = /^(cr|sr|rr|mr)-([a-z]{2})-(\d{1,3})-([0-9a-f]{40})$/i; const RID_ONLY_RE = /^[0-9a-f]{40}$/i; let settings = { ...DEFAULTS }; let importing = false; // un solo import alla volta let lastImport = null; // ultimo esito (ripristino badge/UI) let debounce = { rid: null, at: 0 }; // evita doppioni ravvicinati let failNotified = false; // una sola notifica per down/occupato (no spam) let seen = {}; // rid -> timestamp: rapporti già importati // In MV3 il service worker può essere ucciso e riavviato a ogni messaggio: // il listener viene registrato PRIMA che init() abbia letto le impostazioni, // quindi ogni handler deve attendere il caricamento (altrimenti usa i default // 'localhost:8899' e segnala erroneamente "server non raggiungibile"). let markSettingsReady; const settingsReady = new Promise((resolve) => { markSettingsReady = resolve; }); // ---------------------------------------------------------------- helpers function baseUrl() { const host = String(settings.host || '').trim() || 'localhost'; const port = settings.port || 8899; return `http://${host}:${port}`; } function ridOf(token) { const m = token.match(TOKEN_RE); if (m) return m[4].toLowerCase(); const m2 = token.match(RID_ONLY_RE); return m2 ? token.toLowerCase() : null; } function setBadge(text, color) { chrome.action.setBadgeText({ text }); if (text) chrome.action.setBadgeBackgroundColor({ color }); } function short(s, n = 70) { s = String(s || '').replace(/\s+/g, ' ').trim(); return s.length > n ? s.slice(0, n - 1) + '…' : s; } function notify(title, message) { if (!settings.notifications) return; chrome.notifications.create('imp-' + Date.now(), { type: 'basic', iconUrl: 'icons/notify.png', // glifo con margine: appare più piccolo title, message: short(message) }); } function persistLast(obj) { lastImport = obj; chrome.storage.local.set({ [LAST_IMPORT_KEY]: obj }); } // ---- memoria dei rapporti già importati ---- // Permette di non rinviare al server (e non notificare) i token già visti: // il token resta negli appunti, quindi a ogni reload/navigazione OGame il // content script lo rilegge: senza questa memoria risponderebbe "già // presente" e mostrerebbe una notifica ogni volta. function pruneSeen() { const now = Date.now(); for (const rid of Object.keys(seen)) { if (now - seen[rid] > SEEN_TTL_MS) delete seen[rid]; } const keys = Object.keys(seen); if (keys.length > SEEN_MAX) { keys.sort((a, b) => seen[a] - seen[b]); for (const rid of keys.slice(0, keys.length - SEEN_MAX / 2)) delete seen[rid]; } } function saveSeen() { chrome.storage.local.set({ [SEEN_KEY]: seen }); } function addSeen(rid) { seen[rid] = Date.now(); pruneSeen(); saveSeen(); } async function parseBody(res) { try { return await res.json(); } catch { return { error: `Risposta non valida dal server (HTTP ${res.status})` }; } } // ---------------------------------------------------------------- import async function importToken(token, source) { token = (token || '').trim(); const rid = ridOf(token); if (!rid) { return { ok: false, status: 'error', error: 'Token non riconosciuto.' }; } if (source === 'auto') { if (!settings.autoImport) { return { ok: false, status: 'disabled', error: 'Import automatico disattivato.' }; } // stesso token già gestito negli ultimi secondi (doppio invio) if (rid === debounce.rid && Date.now() - debounce.at < 6000) { return { ok: true, status: 'dup' }; } // già importato in passato (registrato in "seen"): non reinviare if (seen[rid]) { return { ok: true, status: 'known' }; } } if (importing) return { ok: false, status: 'busy' }; // il chiamante riproverà debounce = { rid, at: Date.now() }; importing = true; setBadge('…', '#9e9e9e'); try { let res; try { res = await fetch(`${baseUrl()}/api/reports`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }), signal: AbortSignal.timeout(15000) }); } catch { // server irraggiungibile / timeout: esito ritentabile (il content script, // per l'auto-import, concede un solo retry dopo ~1.5s) setBadge('!', '#ff9800'); const err = `Server non raggiungibile (${baseUrl()})`; if (source === 'manual') { persistLast({ ok: false, badge: { t: '!', c: '#ff9800' }, error: err, token, at: Date.now() }); } else if (!failNotified) { failNotified = true; notify('Import non riuscito', 'Server non raggiungibile'); } return { ok: false, status: 'unreachable', error: err }; } failNotified = false; // server di nuovo raggiungibile if (res.status === 429) { setBadge('!', '#ff9800'); const err = 'Proxy occupato (limite ~10 richieste/min). Riprova tra poco.'; if (source === 'manual') { persistLast({ ok: false, badge: { t: '!', c: '#ff9800' }, error: err, token, at: Date.now() }); } else if (!failNotified) { failNotified = true; notify('Import non riuscito', 'Proxy occupato, riprova tra poco'); } return { ok: false, status: 'error', error: err }; } const data = await parseBody(res); if (res.ok && data.ok) { addSeen(rid); // ricordato: non verrà più rinviato né notificato setBadge('✓', '#4caf50'); const created = !!data.created; const who = (data.report && (data.report.defender_name || data.report.defender_planet_name)) || 'Pianeta'; const message = created ? `${who}${data.coords ? ' · ' + data.coords : ''}` : 'Già presente nel database'; persistLast({ ok: true, badge: { t: '✓', c: '#4caf50' }, message, coords: data.coords || null, created, token, at: Date.now() }); // notifica solo per import realmente nuovi (no spam "già presente") if (created) notify('Spia importata', message); return { ok: true, status: 'ok', created, message }; } // esito terminale (token scaduto, universo errato, 4xx/5xx, …) setBadge('✗', '#f44336'); const err = data.error || `Errore HTTP ${res.status}`; persistLast({ ok: false, badge: { t: '✗', c: '#f44336' }, error: err, token, at: Date.now() }); notify('Import non riuscito', err); return { ok: false, status: 'error', error: err }; } finally { importing = false; } } // ---------------------------------------------------------------- messaggi chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { (async () => { try { await settingsReady; // mai usare i default prima di aver letto lo storage if (!msg || typeof msg !== 'object') { sendResponse({ ok: false, error: 'Messaggio non valido.' }); return; } switch (msg.type) { case 'capture': // dal content script (pagina OGame) sendResponse(await importToken(msg.token, 'auto')); break; case 'importManual': // dal popup (fallback manuale) sendResponse(await importToken(msg.token, 'manual')); break; case 'getState': sendResponse({ ok: true, settings, lastImport }); break; default: sendResponse({ ok: false, error: 'Tipo di messaggio sconosciuto.' }); } } catch (e) { sendResponse({ ok: false, status: 'error', error: String(e && e.message || e) }); } })(); return true; // risposta asincrona }); // ---------------------------------------------------------------- storage chrome.storage.onChanged.addListener((changes, area) => { if (area !== 'local') return; const s = changes[SETTINGS_KEY]; if (s) { settings = { ...DEFAULTS, ...(s.newValue || {}) }; if (!settings.autoImport) setBadge('', '#555'); // spegne il badge "in corso" } }); // ---------------------------------------------------------------- init (async function init() { try { const data = await chrome.storage.local.get({ [SETTINGS_KEY]: DEFAULTS, [LAST_IMPORT_KEY]: null, [SEEN_KEY]: {}, }); settings = { ...DEFAULTS, ...(data[SETTINGS_KEY] || {}) }; lastImport = data[LAST_IMPORT_KEY] || null; seen = data[SEEN_KEY] || {}; pruneSeen(); if (lastImport && lastImport.badge) setBadge(lastImport.badge.t, lastImport.badge.c); } finally { markSettingsReady(); // sblocca gli handler anche in caso di errore } })();