190 lines
6.5 KiB
Python
190 lines
6.5 KiB
Python
#!/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", {}))}
|