Files
2026-09-23 18:59:27 +02:00

342 lines
14 KiB
Markdown

# 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.*