primo commit

This commit is contained in:
2026-09-23 18:59:27 +02:00
parent 029e9f67fb
commit bd900099a0
451 changed files with 143273 additions and 2 deletions
+189
View File
@@ -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", {}))}