primo commit
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
# Analisi Estensione Chrome — OGame Spy Auto-Importer
|
||||
|
||||
> **Versione:** 2.1.0
|
||||
> **Manifest:** MV3
|
||||
> **File:** `/opt/ogame_db/ogame-importer/`
|
||||
|
||||
---
|
||||
|
||||
## 0. Changelog v2.1.0 — fix auto-import
|
||||
|
||||
L'auto-import **non funzionava** (bisognava sempre premere il bottone). Cause:
|
||||
|
||||
1. **`content.js` leggeva la clipboard con `navigator.clipboard.readText()` da content
|
||||
script.** In un content script valgono i permessi *della pagina*, non
|
||||
`clipboardRead` dell'estensione: sulla pagina OGame la lettura veniva rifiutata e
|
||||
l'errore era inghiottito in silenzio (`catch { return; }`). Il popup funzionava
|
||||
perché è una pagina dell'estensione.
|
||||
2. **Filtro troppo stretto:** `isRelevantPage()` richiedeva `component=messages|spy`
|
||||
nell'URL; il content script girava solo nel frame principale (no `all_frames`).
|
||||
3. **Retry inutile:** il polling usciva subito se il testo non era cambiato, quindi il
|
||||
"secondo tentativo" non avveniva mai.
|
||||
|
||||
**Soluzione:** non si legge più la clipboard. Il testo viene intercettato *quando
|
||||
OGame lo copia*:
|
||||
|
||||
- `inject.js` (nuovo, **MAIN world**, `document_start`) aggancia
|
||||
`navigator.clipboard.writeText` e lo notifica al content script via `postMessage`
|
||||
→ copre il pulsante **API/Copia** di OGame.
|
||||
- `content.js` ascolta l'evento DOM **`copy`** → copre Ctrl+C, tasto destro e
|
||||
`document.execCommand('copy')`.
|
||||
- `manifest.json`: `content.js` + `inject.js` con `all_frames: true`.
|
||||
|
||||
Nessun polling, nessun permesso clipboard richiesto a runtime, funziona anche su Brave.
|
||||
|
||||
### v2.1.1 — fix "Server non raggiungibile" sull'auto-import
|
||||
|
||||
Con il service worker MV3 **freddo**, l'import automatico falliva spesso con
|
||||
"Server non raggiungibile", mentre il bottone manuale funzionava sempre. Causa:
|
||||
il listener `onMessage` è registrato prima che `init()` finisca di leggere
|
||||
`chrome.storage`, quindi `settings` era ancora `{host:'localhost', port:8899}` e
|
||||
la fetch andava all'host sbagliato. Il popup, aprendosi, manda un `getState` che
|
||||
dà tempo al worker di inizializzarsi: per questo il manuale era affidabile.
|
||||
|
||||
Fix: `await settingsReady` all'inizio di ogni handler; l'errore di rete ora
|
||||
ritorna `status:'unreachable'` (ritentabile dal retry già presente in `content.js`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Architettura
|
||||
|
||||
L'estensione segue il pattern standard MV3 con 3 componenti:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Popup (popup.html + popup.js) │
|
||||
│ ── UI impostazioni + import manuale fallback │
|
||||
└────────────────────┬────────────────────────────────────┘
|
||||
│ chrome.runtime.sendMessage
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Background (background.js) — Service Worker │
|
||||
│ ── Logica import, deduplicazione, notifiche, badge │
|
||||
└────────────────────┬────────────────────────────────────┘
|
||||
│ POST /api/reports → Backend Flask
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Content Script (content.js) — isolated world │
|
||||
│ ── Evento DOM 'copy' + messaggi da inject.js │
|
||||
└────────────────────┬────────────────────────────────────┘
|
||||
│ ▲ window.postMessage
|
||||
│ │
|
||||
┌────────────────────▼────────────────────────────────────┐
|
||||
│ inject.js (MAIN world) │
|
||||
│ ── Hook di navigator.clipboard.writeText │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Relazioni tra file
|
||||
|
||||
| File | Dipende da | Parla con |
|
||||
|---|---|---|
|
||||
| `manifest.json` | — | definisce tutto (content scripts: `inject.js` MAIN world + `content.js`) |
|
||||
| `inject.js` | — | `window.postMessage` (a `content.js`) |
|
||||
| `background.js` | `chrome.storage` | `chrome.runtime.sendMessage` (da content/popup), `fetch` (backend) |
|
||||
| `content.js` | `chrome.storage` | evento `copy` (DOM), `window` message (da `inject.js`), `chrome.runtime.sendMessage` (a background) |
|
||||
| `popup.html` | `popup.js` | — |
|
||||
| `popup.js` | `chrome.storage`, `chrome.runtime` | `chrome.runtime.sendMessage` (a background), `fetch` (backend) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Flusso dati dettagliato
|
||||
|
||||
### 2.1 Auto-import (flusso principale)
|
||||
|
||||
```
|
||||
1. Utente su pagina OGame → apre rapporto di spionaggio
|
||||
2. Clicca "API" → writeText(token) (oppure Ctrl+C → evento 'copy')
|
||||
3a. inject.js (MAIN world): writeText agganciato → window.postMessage(token)
|
||||
3b. content.js: listener 'copy' → e.clipboardData.getData('text')
|
||||
4. content.js estrae il token con regex: /(cr|sr|rr|mr)-[a-z]{2}-\d{1,3}-[0-9a-f]{40}/i
|
||||
5. Invia a background: { type: 'capture', token: 'sr-ar-170-...' }
|
||||
6. background.js:
|
||||
a. Controlla SEEN (token già importato?) → se sì, ritorna 'known'
|
||||
b. Controlla debounce (stesso token negli ultimi 6s?) → se sì, ritorna 'dup'
|
||||
c. Controlla se importing già in corso → ritorna 'busy'
|
||||
d. POST http://<host>:<port>/api/reports { token }
|
||||
e. Se OK + created → salva in SEEN, badge ✓ verde, notifica
|
||||
f. Se OK + !created → badge ✓ verde, "già presente" (no notifica)
|
||||
g. Se 429 → badge ! arancione, notifica "proxy occupato"
|
||||
h. Se errore → badge ✗ rosso, notifica errore
|
||||
```
|
||||
|
||||
### 2.2 Import manuale (fallback)
|
||||
|
||||
```
|
||||
1. Utente apre popup → incolla token nel textarea
|
||||
2. Clicca "Importa ora"
|
||||
3. popup.js valida formato token
|
||||
4. Invia a background: { type: 'importManual', token: '...' }
|
||||
5. background.js: stessa logica di cui sopra, ma:
|
||||
- NON salta per SEEN (importa sempre, anche se già visto)
|
||||
- Mostra errore nel popup invece di notifica
|
||||
```
|
||||
|
||||
### 2.3 Verifica connessione
|
||||
|
||||
```
|
||||
1. Utente clicca "Verifica connessione" nel popup
|
||||
2. popup.js: GET http://<host>:<port>/api/bootstrap
|
||||
3. Mostra stato: nome piattaforma, universo, galassie
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Stato e memoria
|
||||
|
||||
### chrome.storage.local — chiavi usate
|
||||
|
||||
| Chiave | Tipo | Contenuto |
|
||||
|---|---|---|
|
||||
| `settings` | object | `{ host, port, autoImport, notifications }` |
|
||||
| `lastImport` | object | Ultimo esito import (badge, messaggio, timestamp) |
|
||||
| `seenTokens` | object | `{ "<rid>": <timestamp>, ... }` — token già importati |
|
||||
|
||||
### Struttura `lastImport`
|
||||
|
||||
```js
|
||||
// Successo:
|
||||
{ ok: true, badge: { t: '✓', c: '#4caf50' }, message: '...', coords: '...', created: true/false, token: '...', at: <ts> }
|
||||
|
||||
// Errore:
|
||||
{ ok: false, badge: { t: '✗', c: '#f44336' }, error: '...', token: '...', at: <ts> }
|
||||
```
|
||||
|
||||
### Variabili di stato in background.js (in memoria)
|
||||
|
||||
| Variabile | Tipo | Scopo |
|
||||
|---|---|---|
|
||||
| `settings` | object | Configurazione corrente (sync da storage) |
|
||||
| `importing` | boolean | Lock: un solo import alla volta |
|
||||
| `lastImport` | object | Ultimo esito (sync con storage) |
|
||||
| `debounce` | object | `{ rid, at }` — anti-doppio invio ravvicinato |
|
||||
| `failNotified` | boolean | Una sola notifica per errore (anti-spam) |
|
||||
| `seen` | object | `{ "<rid>": <timestamp> }` — token già importati |
|
||||
|
||||
### Gestione SEEN (token già importati)
|
||||
|
||||
- **Max voci:** 500
|
||||
- **TTL:** 60 giorni (voce eliminata se `now - timestamp > 60 giorni`)
|
||||
- **Pruning:** quando supera 500, elimina le vecchie (metà di 500 = 250)
|
||||
- **Comportamento:** token già in SEEN → import skip (status `known`), nessuna notifica
|
||||
- **Nota:** per import manuale (`source === 'manual'`) il controllo SEEN è disattivato
|
||||
|
||||
---
|
||||
|
||||
## 4. Regex e formati token
|
||||
|
||||
### Pattern principale (TOKEN_RE)
|
||||
|
||||
```regex
|
||||
^(cr|sr|rr|mr)-([a-z]{2})-(\d{1,3})-([0-9a-f]{40})$
|
||||
```
|
||||
|
||||
| Gruppo | Significato | Esempio |
|
||||
|---|---|---|
|
||||
| 1 | Tipo report | `cr`, `sr`, `rr`, `mr` |
|
||||
| 2 | Community | `ar` |
|
||||
| 3 | Server number | `170` |
|
||||
| 4 | Report ID (40 hex) | `98018ad723cb4fdba047f78728fb831cd28b3ca5` |
|
||||
|
||||
### Pattern fallback (RID_ONLY_RE)
|
||||
|
||||
```regex
|
||||
^[0-9a-f]{40}$
|
||||
```
|
||||
|
||||
Accetta solo l'ID a 40 caratteri esadecimali (senza prefisso tipo-community-server).
|
||||
|
||||
### Regex content.js (TOKEN_SEARCH_RE)
|
||||
|
||||
```regex
|
||||
(cr|sr|rr|mr)-[a-z]{2}-\d{1,3}-[0-9a-f]{40}
|
||||
```
|
||||
|
||||
Senza `^` e `$` — cerca il token **dentro** il testo copiato (la selezione può contenere altro).
|
||||
|
||||
---
|
||||
|
||||
## 5. API Backend usate dall'estensione
|
||||
|
||||
| Endpoint | Metodo | Dati | Scopo |
|
||||
|---|---|---|---|
|
||||
| `/api/reports` | POST | `{ token: "sr-ar-170-..." }` | Importa un rapporto |
|
||||
| `/api/bootstrap` | GET | — | Verifica connessione (solo popup) |
|
||||
|
||||
### Risposte `/api/reports`
|
||||
|
||||
```json
|
||||
// Successo - nuovo:
|
||||
{ ok: true, created: true, coords: "3|456|789", report: { defender_name: "...", defender_planet_name: "..." } }
|
||||
|
||||
// Successo - già presente:
|
||||
{ ok: true, created: false }
|
||||
|
||||
// Errore:
|
||||
{ ok: false, error: "..." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Comportamenti edge case
|
||||
|
||||
### 6.1 Background non pronto / service worker sospeso
|
||||
|
||||
- **Content script:** `chrome.runtime.sendMessage` fallisce → status `'unreachable'` → retry singolo dopo 1s
|
||||
- **Popup:** `chrome.runtime.sendMessage` fallisce → mostra "Ricarica l'estensione"
|
||||
- **MV3:** il service worker può essere ucciso da Chrome dopo inattività → al prossimo messaggio si riattiva
|
||||
|
||||
### 6.2 Doppio invio (stesso token copiato due volte)
|
||||
|
||||
- **Debounce:** 6 secondi — se stesso token in <6s → status `'dup'`, nessun POST
|
||||
- **SEEN:** dopo 6s, se il token è in SEEN → status `'known'`, nessun POST
|
||||
|
||||
### 6.3 Server occupato (HTTP 429)
|
||||
|
||||
- Proxy community: ~10 richieste/minuto
|
||||
- Badge `!` arancione
|
||||
- Notifica una sola volta (`failNotified` flag)
|
||||
- Il token **non** viene aggiunto a SEEN → l'utente può ritentare
|
||||
|
||||
### 6.4 Server irraggiungibile
|
||||
|
||||
- Timeout fetch: 15 secondi
|
||||
- Badge `!` arancione
|
||||
- Notifica una sola volta
|
||||
- Nessuno retry automatico (MV3: il service worker può essere sospeso)
|
||||
|
||||
### 6.5 Auto-import disattivato
|
||||
|
||||
- Se `autoImport === false`, content script ignora tutti gli eventi `copy`
|
||||
- Badge spento (`''`)
|
||||
- Il popup può comunque fare import manuale
|
||||
|
||||
---
|
||||
|
||||
## 7. Considerazioni per future modifiche
|
||||
|
||||
### Punti di forza (da preservare)
|
||||
- Design minimalista: nessun polling, nessun permesso invasivo
|
||||
- Deduplicazione robusta (SEEN + debounce)
|
||||
- Anti-spam notifiche (`failNotified`, solo `created` per notifica)
|
||||
- MV3 compliant, service worker pulito
|
||||
- Separazione chiara dei ruoli (content = cattura, background = import, popup = config)
|
||||
|
||||
### Aree di miglioramento / decisioni da prendere
|
||||
|
||||
1. **SEEN store:** 500 voci / 60 giorni. Con più universi o utenti attivi, potrebbe saturare. Valutare aumento limiti o strategia di pruning più aggressiva.
|
||||
|
||||
2. **Service worker lifecycle:** in MV3 il SW può essere ucciso in qualsiasi momento. Il `importing` lock è in memoria → si resetta. Questo è accettabile perché:
|
||||
- Il content script ha un retry singolo
|
||||
- Il SEEN previene duplicati
|
||||
- L'import manuale nel popup è sempre disponibile
|
||||
|
||||
3. **Configurazione:** host/port di default `localhost:8899`. Se l'utente cambia IP, deve aggiornare manualmente. Potrebbe servire un meccanismo di rilevamento automatico (mDNS, ecc.).
|
||||
|
||||
4. **Content script:** il flag `sentToken` è per pagina (si resetta al reload). Questo significa che se l'utente ricarica la pagina e copia di nuovo lo stesso token, viene reinviato. Il SEEN nel background gestisce la deduplicazione, quindi è accettabile.
|
||||
|
||||
5. **Popup:** non c'è storico degli import — solo l'ultimo. Potrebbe essere utile un log.
|
||||
|
||||
6. **Errori HTTP generici:** il popup mostra `Risposta del server: HTTP 500` ma non il messaggio esatto del backend. Potrebbe essere migliorato.
|
||||
|
||||
7. **Manifest:** nessun `action.default_title` → il popup non ha tooltip. Aggiungere `title` per chiarezza.
|
||||
|
||||
8. **Versione:** hardcoded in manifest e popup. Unificare in un unico punto.
|
||||
|
||||
### Costanti da tenere in sync tra file
|
||||
|
||||
| Costante | background.js | content.js | popup.js |
|
||||
|---|---|---|---|
|
||||
| `TOKEN_RE` | ✓ | ✓ (variante) | ✓ |
|
||||
| `TOKEN_SEARCH_RE` | — | ✓ | — |
|
||||
| `DEFAULTS.host` | ✓ | — | ✓ |
|
||||
| `DEFAULTS.port` | ✓ | — | ✓ |
|
||||
| `DEFAULTS.autoImport` | ✓ | ✓ | ✓ |
|
||||
| `DEFAULTS.notifications` | ✓ | — | ✓ |
|
||||
| `SETTINGS_KEY` | ✓ | — | ✓ |
|
||||
| `SEEN_MAX` | ✓ | — | — |
|
||||
| `SEEN_TTL_MS` | ✓ | — | — |
|
||||
|
||||
### Struttura dati da conoscere
|
||||
|
||||
**Report importato (salvato da backend):**
|
||||
```json
|
||||
{
|
||||
"token": "sr-ar-170-98018ad7...",
|
||||
"rid": "98018ad7...",
|
||||
"type": "sr",
|
||||
"community": "ar",
|
||||
"server": 170,
|
||||
"timestamp": <unix_ts>,
|
||||
"report": { ... dati completi dal proxy ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Riferimenti incrociati
|
||||
|
||||
| Componente | File | Porta |
|
||||
|---|---|---|
|
||||
| Backend Flask | `/opt/ogame_db/app.py` | 8899 |
|
||||
| Aggiornamento dati | `/opt/ogame_db/update_data.py` | — |
|
||||
| Gestione rapporti | `/opt/ogame_db/report_store.py` | — |
|
||||
| Nomi tecnici | `/opt/ogame_db/technames.py` | — |
|
||||
| Frontend web | `/opt/ogame_db/static/` | — |
|
||||
| Dati | `/opt/ogame_db/data/` | — |
|
||||
| Documentazione | `/opt/ogame_db/DOCUMENTAZIONE.md` | — |
|
||||
|
||||
---
|
||||
|
||||
*Documento generato per riferimento sviluppo. Ultimo aggiornamento: v2.1.0.*
|
||||
@@ -0,0 +1,280 @@
|
||||
/* 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
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,97 @@
|
||||
/* OGame Spy Auto-Importer — content script (isolated world)
|
||||
*
|
||||
* Esegue SOLO sulle pagine OGame (vedi manifest: *.ogame.gameforge.com).
|
||||
*
|
||||
* NON legge la clipboard (impossibile da content script: vale il permesso
|
||||
* della pagina, non quello dell'estensione). Intercetta invece il token nel
|
||||
* momento in cui viene copiato, tramite due sorgenti:
|
||||
* 1) evento DOM 'copy' → Ctrl+C, tasto destro, execCommand('copy')
|
||||
* 2) messaggio dal MAIN world → navigator.clipboard.writeText() (pulsante API)
|
||||
*
|
||||
* Il token intercettato viene inviato UNA volta al background che lo importa.
|
||||
* Se il background è occupato o non ancora pronto, si concede un solo retry.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// Stessa forma accettata dal server (ricerca dentro il testo copiato)
|
||||
const TOKEN_SEARCH_RE = /(cr|sr|rr|mr)-[a-z]{2}-\d{1,3}-[0-9a-f]{40}/i;
|
||||
const OGame_HOST_RE = /(^|\.)ogame\.gameforge\.com$/i;
|
||||
const CHANNEL = 'ogame-spy-importer';
|
||||
const RETRY_MS = 1500;
|
||||
|
||||
let autoImport = true;
|
||||
const done = new Set(); // token già inviati con esito definitivo
|
||||
const retried = new Set(); // token che hanno già usato il loro unico retry
|
||||
let inflight = null; // token attualmente in invio
|
||||
|
||||
function isOgame() {
|
||||
return OGame_HOST_RE.test(window.location.hostname);
|
||||
}
|
||||
|
||||
function extractToken(text) {
|
||||
if (!text || typeof text !== 'string') return null;
|
||||
const m = text.match(TOKEN_SEARCH_RE);
|
||||
return m ? m[0].toLowerCase() : null;
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const d = await chrome.storage.local.get({ settings: undefined });
|
||||
autoImport = !d.settings || d.settings.autoImport !== false;
|
||||
}
|
||||
|
||||
async function sendCapture(token) {
|
||||
try {
|
||||
const r = await chrome.runtime.sendMessage({ type: 'capture', token });
|
||||
return (r && r.status) || 'error';
|
||||
} catch {
|
||||
return 'unreachable'; // background non ancora pronto
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToken(token) {
|
||||
if (!token || !isOgame() || !autoImport) return;
|
||||
|
||||
// già gestito (o in corso): niente doppioni se scattano entrambe le sorgenti
|
||||
if (done.has(token) || inflight === token) return;
|
||||
|
||||
inflight = token;
|
||||
const status = await sendCapture(token);
|
||||
inflight = null;
|
||||
|
||||
// 'busy'/'unreachable' = il background non ha risposto: un solo retry.
|
||||
if (status === 'busy' || status === 'unreachable') {
|
||||
if (!retried.has(token)) {
|
||||
retried.add(token);
|
||||
setTimeout(() => handleToken(token), RETRY_MS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Esito definitivo: 'ok'/'dup'/'known' importato o già noto,
|
||||
// 'error' fallito (il background ha già mostrato la notifica d'errore).
|
||||
done.add(token);
|
||||
}
|
||||
|
||||
// ---- sorgente 1: evento 'copy' (Ctrl+C, tasto destro, execCommand) ----
|
||||
document.addEventListener('copy', (e) => {
|
||||
try {
|
||||
const text = e.clipboardData && e.clipboardData.getData('text');
|
||||
const token = extractToken(text);
|
||||
if (token) handleToken(token);
|
||||
} catch (_) { /* noop */ }
|
||||
}, true);
|
||||
|
||||
// ---- sorgente 2: writeText della pagina (main world → postMessage) ----
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.source !== window || !e.data || e.data.__ogameSpyImporter !== CHANNEL) return;
|
||||
const token = extractToken(e.data.text);
|
||||
if (token) handleToken(token);
|
||||
});
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== 'local' || !changes.settings) return;
|
||||
autoImport = changes.settings.newValue.autoImport !== false;
|
||||
});
|
||||
|
||||
(async function init() {
|
||||
await loadSettings();
|
||||
})();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 173 B |
Binary file not shown.
|
After Width: | Height: | Size: 451 B |
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,59 @@
|
||||
/* OGame Spy Auto-Importer — script MAIN world (iniettato nella pagina)
|
||||
*
|
||||
* Ruolo: intercettare il testo che la pagina OGame COPIA negli appunti,
|
||||
* senza mai doverli leggere. Questo evita del tutto il problema dei permessi
|
||||
* clipboard nei content script (dove `navigator.clipboard.readText()` è
|
||||
* soggetta ai permessi della pagina e viene rifiutata → auto-import muto).
|
||||
*
|
||||
* Copre due strade:
|
||||
* 1) navigator.clipboard.writeText() → pulsante "API"/"Copia" di OGame
|
||||
* 2) l'evento DOM 'copy' → Ctrl+C / tasto destro / execCommand
|
||||
* (quest'ultimo è gestito direttamente da content.js, in isolated world)
|
||||
*
|
||||
* Gira a document_start per agganciare writeText prima degli script di gioco.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const CHANNEL = 'ogame-spy-importer';
|
||||
let hooked = false;
|
||||
|
||||
function share(text) {
|
||||
if (!text || typeof text !== 'string') return;
|
||||
try {
|
||||
window.postMessage({ __ogameSpyImporter: CHANNEL, text: text }, '*');
|
||||
} catch (_) { /* noop */ }
|
||||
}
|
||||
|
||||
function hookWriteText(target) {
|
||||
if (!target || typeof target.writeText !== 'function' || target.writeText.__spyHooked) return false;
|
||||
const orig = target.writeText;
|
||||
const wrapper = function (text) {
|
||||
share(text);
|
||||
return orig.apply(this, arguments);
|
||||
};
|
||||
try {
|
||||
wrapper.__spyHooked = true;
|
||||
target.writeText = wrapper;
|
||||
return true;
|
||||
} catch (_) {
|
||||
// proprietà non scrivibile: prova a definire una own property
|
||||
try {
|
||||
Object.defineProperty(target, 'writeText', {
|
||||
configurable: true, writable: true, value: wrapper,
|
||||
});
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Istanza (shadows il prototipo) + prototipo, per essere sicuri.
|
||||
hooked = hookWriteText(navigator.clipboard) || hooked;
|
||||
if (window.Clipboard && Clipboard.prototype) {
|
||||
hooked = hookWriteText(Clipboard.prototype) || hooked;
|
||||
}
|
||||
} catch (_) { /* noop */ }
|
||||
})();
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "OGame Spy Auto-Importer",
|
||||
"version": "2.1.2",
|
||||
"description": "Cattura gli API string di spionaggio copiati mentre sei su una pagina OGame e li carica automaticamente sulla piattaforma (impostabile con IP e porta).",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"notifications",
|
||||
"clipboardRead"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"*://*.ogame.gameforge.com/*"
|
||||
],
|
||||
"js": [
|
||||
"inject.js"
|
||||
],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true,
|
||||
"world": "MAIN"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"*://*.ogame.gameforge.com/*"
|
||||
],
|
||||
"js": [
|
||||
"content.js"
|
||||
],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OGame Spy Auto-Importer</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
width: 340px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #e0e0e0;
|
||||
background: #1a1a2e;
|
||||
padding: 14px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
h1 .icon { font-size: 18px; }
|
||||
.card {
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
padding: 11px 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .6px;
|
||||
color: #8a93b0;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.row { display: flex; gap: 8px; }
|
||||
.field { flex: 1; margin-bottom: 8px; }
|
||||
.field.small { flex: 0 0 90px; }
|
||||
label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #aab;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
input[type="text"], input[type="number"], textarea {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #2a3a5f;
|
||||
border-radius: 4px;
|
||||
background: #0f3460;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
textarea { resize: vertical; min-height: 46px; }
|
||||
input:focus, textarea:focus { border-color: #e94560; }
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #e94560;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background .15s;
|
||||
}
|
||||
.btn:hover { background: #c73652; }
|
||||
.btn:disabled { background: #555; cursor: default; }
|
||||
.btn.ghost { background: #233259; }
|
||||
.btn.ghost:hover { background: #2c3f70; }
|
||||
.target {
|
||||
font-size: 11px;
|
||||
color: #7fd0ff;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
.status { display: flex; align-items: center; gap: 7px; font-size: 12px; margin-top: 8px; }
|
||||
.dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: #555; flex-shrink: 0;
|
||||
}
|
||||
.dot.ok { background: #4caf50; box-shadow: 0 0 5px #4caf50; }
|
||||
.dot.err { background: #f44336; box-shadow: 0 0 5px #f44336; }
|
||||
.dot.wait { background: #ff9800; animation: pulse 1s infinite; }
|
||||
.status small { color: #889; display: block; font-size: 10px; line-height: 1.35; }
|
||||
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
|
||||
.switch-row { display: flex; align-items: center; justify-content: space-between; }
|
||||
.switch-row + .hint { margin-top: 7px; }
|
||||
.switch { position: relative; width: 38px; height: 21px; flex-shrink: 0; cursor: pointer; }
|
||||
.switch input { display: none; }
|
||||
.switch .slider {
|
||||
position: absolute; inset: 0; background: #334; border-radius: 11px; transition: .2s;
|
||||
}
|
||||
.switch .slider::before {
|
||||
content: ''; position: absolute; width: 15px; height: 15px;
|
||||
left: 3px; top: 3px; background: #fff; border-radius: 50%; transition: .2s;
|
||||
}
|
||||
.switch input:checked + .slider { background: #e94560; }
|
||||
.switch input:checked + .slider::before { transform: translateX(17px); }
|
||||
.hint { font-size: 10.5px; color: #778; line-height: 1.45; }
|
||||
.hint code, code {
|
||||
background: #0f3460; padding: 0 4px; border-radius: 3px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 10px;
|
||||
}
|
||||
.result { font-size: 11.5px; margin-top: 8px; word-break: break-word; line-height: 1.4; }
|
||||
.result.ok { color: #6fdc8c; }
|
||||
.result.err { color: #ff8a80; }
|
||||
.last { font-size: 11px; color: #889; margin-top: 2px; word-break: break-word; line-height: 1.4; }
|
||||
.footer { text-align: center; font-size: 10px; color: #556; margin-top: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1><span class="icon">🔭</span> OGame Spy Auto-Importer</h1>
|
||||
|
||||
<!-- Piattaforma -->
|
||||
<div class="card">
|
||||
<div class="card-title">
|
||||
<span>Piattaforma</span>
|
||||
<span class="target" id="targetUrl">–</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="field">
|
||||
<label for="host">Indirizzo / IP</label>
|
||||
<input type="text" id="host" placeholder="localhost" spellcheck="false" autocomplete="off">
|
||||
</div>
|
||||
<div class="field small">
|
||||
<label for="port">Porta</label>
|
||||
<input type="number" id="port" placeholder="8899" min="1" max="65535">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn ghost" id="btnCheck">Verifica connessione</button>
|
||||
<div class="status">
|
||||
<span class="dot" id="statusDot"></span>
|
||||
<span id="statusText">Non verificato</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cattura automatica -->
|
||||
<div class="card">
|
||||
<div class="card-title">Cattura automatica</div>
|
||||
<div class="switch-row">
|
||||
<span>Abilita import dagli appunti</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="autoImport">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hint">
|
||||
Su una pagina OGame (es. <code>s170-ar.ogame.gameforge.com</code>) tieni la
|
||||
scheda attiva e copia l'API string della spia: viene caricato subito.
|
||||
Altrove la clipboard non viene mai letta.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notifiche -->
|
||||
<div class="card">
|
||||
<div class="card-title">Notifiche</div>
|
||||
<div class="switch-row">
|
||||
<span>Mostra notifiche di esito</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="notifications">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import dalla clipboard -->
|
||||
<div class="card">
|
||||
<div class="card-title">Import dalla clipboard</div>
|
||||
<div class="hint" style="margin-bottom:8px">
|
||||
Copia un API string in OGame (pulsante <code>API</code> nel rapporto), poi premi qui.
|
||||
</div>
|
||||
<button class="btn" id="btnImport">Importa ora</button>
|
||||
<div class="result" id="importResult"></div>
|
||||
<div class="last" id="lastImport">Nessun import effettuato.</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">v2.1.2 — intercetta la copia, non legge la clipboard</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,208 @@
|
||||
/* OGame Spy Auto-Importer — popup
|
||||
* Impostazioni piattaforma (IP/porta), toggle cattura/notifiche,
|
||||
* verifica connessione e import manuale di fallback.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const SETTINGS_KEY = 'settings';
|
||||
const DEFAULTS = { host: 'localhost', port: 8899, autoImport: true, notifications: true };
|
||||
|
||||
// Forme accettate per l'import manuale (come lato server)
|
||||
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;
|
||||
|
||||
const refs = {
|
||||
host: document.getElementById('host'),
|
||||
port: document.getElementById('port'),
|
||||
targetUrl: document.getElementById('targetUrl'),
|
||||
btnCheck: document.getElementById('btnCheck'),
|
||||
statusDot: document.getElementById('statusDot'),
|
||||
statusText: document.getElementById('statusText'),
|
||||
autoImport: document.getElementById('autoImport'),
|
||||
notifications: document.getElementById('notifications'),
|
||||
btnImport: document.getElementById('btnImport'),
|
||||
importResult: document.getElementById('importResult'),
|
||||
lastImport: document.getElementById('lastImport'),
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------- utils
|
||||
|
||||
function loadSettings() {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.get({ [SETTINGS_KEY]: DEFAULTS }, (d) => resolve({ ...DEFAULTS, ...(d[SETTINGS_KEY] || {}) }));
|
||||
});
|
||||
}
|
||||
|
||||
function saveSettings(cfg) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.set({ [SETTINGS_KEY]: cfg }, () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
function baseUrl(host, port) {
|
||||
return `http://${String(host || '').trim() || 'localhost'}:${port || 8899}`;
|
||||
}
|
||||
|
||||
function setStatus(cls, html) {
|
||||
refs.statusDot.className = 'dot ' + cls;
|
||||
refs.statusText.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderLast(li) {
|
||||
if (!li) {
|
||||
refs.lastImport.textContent = 'Nessun import effettuato.';
|
||||
return;
|
||||
}
|
||||
if (li.ok) {
|
||||
refs.lastImport.innerHTML = `<span style="color:#6fdc8c">✓</span> ${esc(li.message)}`;
|
||||
} else {
|
||||
refs.lastImport.innerHTML = `<span style="color:#ff8a80">✗</span> ${esc(li.error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = String(s == null ? '' : s);
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function updateTarget() {
|
||||
refs.targetUrl.textContent = `${baseUrl(refs.host.value, refs.port.value)}/api/reports`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- server check
|
||||
|
||||
async function checkServer() {
|
||||
const host = refs.host.value.trim();
|
||||
const port = refs.port.value.trim();
|
||||
setStatus('wait', 'Verifica in corso…');
|
||||
try {
|
||||
const res = await fetch(`${baseUrl(host, port)}/api/bootstrap`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (res.ok && data && data.ok) {
|
||||
const s = data.server || {};
|
||||
const universe = s.number ? `s${s.number}-${s.community || ''}` : (s.domain || '');
|
||||
setStatus('ok',
|
||||
`Connesso — <b>${esc(s.name || 'piattaforma')}</b>` +
|
||||
(universe ? `<small>${esc(universe)} · ${esc(s.galaxies || '?')} galassie</small>` : ''));
|
||||
return true;
|
||||
}
|
||||
setStatus('err', `Risposta del server: ${esc((data && data.error) || 'HTTP ' + res.status)}`);
|
||||
return false;
|
||||
} catch {
|
||||
setStatus('err', `Server non raggiungibile su ${esc(baseUrl(host, port))}.<br><small>Controlla IP/porta e che la piattaforma sia avviata.</small>`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- import dalla clipboard
|
||||
|
||||
async function importFromClipboard() {
|
||||
let text;
|
||||
try {
|
||||
text = await navigator.clipboard.readText();
|
||||
} catch {
|
||||
refs.importResult.className = 'result err';
|
||||
refs.importResult.textContent = '✗ Impossibile leggere la clipboard.';
|
||||
return;
|
||||
}
|
||||
|
||||
const token = text.trim();
|
||||
refs.importResult.textContent = '';
|
||||
|
||||
if (!token) {
|
||||
refs.importResult.className = 'result err';
|
||||
refs.importResult.textContent = 'La clipboard è vuota. Copia un API string da OGame prima.';
|
||||
return;
|
||||
}
|
||||
if (!TOKEN_RE.test(token) && !RID_ONLY_RE.test(token)) {
|
||||
refs.importResult.className = 'result err';
|
||||
refs.importResult.textContent = 'Formato non riconosciuto. Atteso: sr-ar-170-<40 hex> o solo l\'id.';
|
||||
return;
|
||||
}
|
||||
|
||||
refs.btnImport.disabled = true;
|
||||
refs.btnImport.textContent = 'Importazione…';
|
||||
try {
|
||||
const r = await chrome.runtime.sendMessage({ type: 'importManual', token });
|
||||
if (r && r.ok) {
|
||||
refs.importResult.className = 'result ok';
|
||||
refs.importResult.textContent = (r.created ? '✓ Importato — ' : '✓ Già presente — ') + (r.message || '');
|
||||
} else {
|
||||
refs.importResult.className = 'result err';
|
||||
refs.importResult.textContent = '✗ ' + ((r && r.error) || 'Errore sconosciuto');
|
||||
}
|
||||
} catch {
|
||||
refs.importResult.className = 'result err';
|
||||
refs.importResult.textContent = '✗ Impossibile contattare il background. Ricarica l\'estensione.';
|
||||
} finally {
|
||||
refs.btnImport.disabled = false;
|
||||
refs.btnImport.textContent = 'Importa ora';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- init
|
||||
|
||||
async function init() {
|
||||
const cfg = await loadSettings();
|
||||
refs.host.value = cfg.host;
|
||||
refs.port.value = cfg.port;
|
||||
refs.autoImport.checked = cfg.autoImport !== false;
|
||||
refs.notifications.checked = cfg.notifications !== false;
|
||||
updateTarget();
|
||||
|
||||
// ultimo esito import (dal background)
|
||||
try {
|
||||
const st = await chrome.runtime.sendMessage({ type: 'getState' });
|
||||
if (st && st.ok) renderLast(st.lastImport);
|
||||
} catch { /* background non attivo: ignora */ }
|
||||
|
||||
// nessuna verifica automatica: la connessione si controlla solo col
|
||||
// pulsante "Verifica connessione"
|
||||
setStatus('', 'Premi “Verifica connessione” per testare il server');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- eventi
|
||||
|
||||
function onHostPortChanged() {
|
||||
updateTarget();
|
||||
// solo il pulsante "Verifica" interroga il server
|
||||
setStatus('', 'Impostazioni salvate — premi “Verifica connessione”');
|
||||
}
|
||||
|
||||
refs.host.addEventListener('change', async () => {
|
||||
const cfg = await loadSettings();
|
||||
await saveSettings({ ...cfg, host: refs.host.value.trim() || DEFAULTS.host });
|
||||
onHostPortChanged();
|
||||
});
|
||||
|
||||
refs.port.addEventListener('change', async () => {
|
||||
const cfg = await loadSettings();
|
||||
const p = parseInt(refs.port.value, 10);
|
||||
refs.port.value = (p >= 1 && p <= 65535) ? p : DEFAULTS.port;
|
||||
await saveSettings({ ...cfg, port: refs.port.value });
|
||||
onHostPortChanged();
|
||||
});
|
||||
|
||||
refs.autoImport.addEventListener('change', async () => {
|
||||
const cfg = await loadSettings();
|
||||
await saveSettings({ ...cfg, autoImport: refs.autoImport.checked });
|
||||
});
|
||||
|
||||
refs.notifications.addEventListener('change', async () => {
|
||||
const cfg = await loadSettings();
|
||||
await saveSettings({ ...cfg, notifications: refs.notifications.checked });
|
||||
});
|
||||
|
||||
refs.btnCheck.addEventListener('click', checkServer);
|
||||
refs.btnImport.addEventListener('click', importFromClipboard);
|
||||
|
||||
// aggiornamento live mentre il popup è aperto (es. import automatico in corso)
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== 'local' || !changes.lastImport) return;
|
||||
renderLast(changes.lastImport.newValue);
|
||||
});
|
||||
|
||||
init();
|
||||
Reference in New Issue
Block a user