281 lines
9.4 KiB
JavaScript
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
|
|
}
|
|
})();
|