Files
ogame_db/ogame-importer/background.js
T
2026-09-23 18:59:27 +02:00

281 lines
9.4 KiB
JavaScript

/* 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
}
})();