#!/usr/bin/env python3 """Gestione rapporti di spionaggio (SR) importati tramite API string. Formato API string (documentazione ufficiale OGame Origin): sr-ar-170-<40 hex> = tipo(sr) - community(ar) - server(170) - report_id Il recupero avviene attraverso il community proxy ufficiale: https://ogapi.faw-kes.de/v1/report/{api_string}/1 """ import json import re import time import urllib.error import urllib.request from datetime import datetime, timezone from pathlib import Path BASE_DIR = Path(__file__).resolve().parent REPORTS_DIR = BASE_DIR / "data" / "reports" INDEX_PATH = REPORTS_DIR / "index.json" UA = "OGameGalaxyViewer/1.0 (personal galaxy viewer)" def load_config(): with open(BASE_DIR / "config.json", encoding="utf-8") as f: return json.load(f) def _load_index(): if INDEX_PATH.exists(): return json.load(open(INDEX_PATH, encoding="utf-8")) return {"by_coords": {}, "reports": {}} def _save_index(idx): REPORTS_DIR.mkdir(parents=True, exist_ok=True) with open(INDEX_PATH, "w", encoding="utf-8") as f: json.dump(idx, f, ensure_ascii=False, indent=1) def parse_token(token, cfg): """Valida un'API string e ne estrae le parti. Accetta anche solo l'id (40 hex).""" token = (token or "").strip() m = re.fullmatch(r"(cr|sr|rr|mr)-([a-z]{2})-(\d{1,3})-([0-9a-f]{40})", token, re.I) if m: rtype, community, server, rid = m.group(1).lower(), m.group(2).lower(), int(m.group(3)), m.group(4).lower() if community != cfg["community"] or server != cfg["server_number"]: raise ValueError( f"Questo rapporto è dell'universo s{server}-{community}, " f"ma il progetto è configurato su s{cfg['server_number']}-{cfg['community']}." ) return rtype, rid, token m = re.fullmatch(r"([0-9a-f]{40})", token, re.I) if m: return "sr", m.group(1).lower(), f"sr-{cfg['community']}-{cfg['server_number']}-{m.group(1).lower()}" raise ValueError( "Formato non valido. Incolla l'API string del rapporto " "(es. sr-ar-170-98018ad7...b3ca5) o il solo id a 40 caratteri esadecimali." ) def fetch_from_proxy(api_string, cfg): url = f"{cfg['proxy_base']}/v1/report/{api_string}/1" req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"}) try: with urllib.request.urlopen(req, timeout=60) as r: body = json.loads(r.read().decode("utf-8")) except urllib.error.HTTPError as e: if e.code == 429: raise RuntimeError("Proxy occupato (limite 10 richieste/minuto). Riprova tra poco.") raise RuntimeError(f"Errore HTTP {e.code} dal proxy.") code = body.get("RESULT_CODE") if code != 1000: raise RuntimeError( f"Il proxy non ha trovato il rapporto (RESULT_CODE {code}). " "Controlla il token o che il rapporto sia ancora disponibile." ) return body.get("RESULT_DATA") or {} def _defender_info(report): g = report.get("generic", {}) coords = g.get("defender_planet_coordinates", "") ptype = g.get("defender_planet_type") return { "coords": coords, "planet_type": ptype if ptype is not None else 1, "defender_name": g.get("defender_name", ""), "defender_planet_name": g.get("defender_planet_name", ""), "attacker_name": g.get("attacker_name", ""), "event_time": g.get("event_time", ""), "event_timestamp": g.get("event_timestamp", 0), "activity": g.get("activity"), "loot_percentage": g.get("loot_percentage"), # conteggi tenuti anche nell'indice: servono al tab "Top Flotte" "total_ship_count": g.get("total_ship_count"), "total_defense_count": g.get("total_defense_count"), } def import_report(api_string, cfg=None): """Importa un rapporto: lo scarica dal proxy, lo salva su disco e indicizza.""" cfg = cfg or load_config() rtype, rid, api_string = parse_token(api_string, cfg) idx = _load_index() if rid in idx.get("reports", {}): # già presente: ritorna il file esistente return get_report(rid), False data = fetch_from_proxy(api_string, cfg) info = _defender_info(data) if not info["coords"]: raise RuntimeError("Il rapporto non contiene le coordinate del difensore.") REPORTS_DIR.mkdir(parents=True, exist_ok=True) record = { "type": rtype, "sr_id": rid, "api_string": api_string, "saved_at": int(time.time()), **info, } with open(REPORTS_DIR / f"{rid}.json", "w", encoding="utf-8") as f: json.dump({"meta": record, "report": data}, f, ensure_ascii=False, indent=1) idx["reports"][rid] = record key = info["coords"] if info["planet_type"] != 3 else f"{info['coords']}#moon" idx.setdefault("by_coords", {}).setdefault(key, []).append(rid) _save_index(idx) return get_report(rid), True def get_report(rid): p = REPORTS_DIR / f"{rid}.json" if not p.exists(): return None return json.load(open(p, encoding="utf-8")) def delete_report(rid): """Elimina un rapporto: file su disco + voce in index.json. Ritorna True se qualcosa è stato rimosso, False se il rapporto non esiste. """ rid = (rid or "").strip().lower() p = REPORTS_DIR / f"{rid}.json" idx = _load_index() existed = rid in idx.get("reports", {}) if not p.exists() and not existed: return False if p.exists(): p.unlink() idx.get("reports", {}).pop(rid, None) for key in list(idx.get("by_coords", {}).keys()): ids = idx["by_coords"][key] if rid in ids: ids.remove(rid) if not ids: del idx["by_coords"][key] _save_index(idx) return True def list_by_coords(coords, scope="all"): """Elenco sr_id per un pianeta/luna. scope: 'planet' | 'moon' | 'all'.""" idx = _load_index() by = idx.get("by_coords", {}) ids = [] if scope in ("planet", "all"): ids += list(by.get(coords, [])) if scope in ("moon", "all"): ids += list(by.get(f"{coords}#moon", [])) recs = [idx["reports"][r] for r in ids if r in idx["reports"]] recs.sort(key=lambda r: r.get("event_timestamp") or 0, reverse=True) return recs def summary(rec): """Riepilogo compatto per la lista (senza aprire il file completo).""" return rec def stats(cfg=None): cfg = cfg or load_config() idx = _load_index() rep = idx.get("reports", {}) return {"count": len(rep), "planets": len(idx.get("by_coords", {}))}