primo commit
This commit is contained in:
+189
@@ -0,0 +1,189 @@
|
||||
#!/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", {}))}
|
||||
Reference in New Issue
Block a user