#!/usr/bin/env python3 """IP geolocation with a provider fallback chain, cached with a TTL. Prints JSON: {lat, lon, city, country, ip, source}. Diagnostics go to stderr, non-zero exit on total failure. Used by services/location.py (and runnable standalone for testing). """ from __future__ import annotations import json import os import sys import time import urllib.request from pathlib import Path CACHE = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "astal-menu" / "location.json" TTL = 1800 # seconds PROVIDERS = [ ("ip-api", "http://ip-api.com/json/?fields=lat,lon,city,country,query", lambda d: {"lat": d["lat"], "lon": d["lon"], "city": d.get("city"), "country": d.get("country"), "ip": d.get("query")}), ("ipapi.co", "https://ipapi.co/json/", lambda d: {"lat": d["latitude"], "lon": d["longitude"], "city": d.get("city"), "country": d.get("country_name"), "ip": d.get("ip")}), ("ipinfo", "https://ipinfo.io/json", lambda d: {"lat": float(d["loc"].split(",")[0]), "lon": float(d["loc"].split(",")[1]), "city": d.get("city"), "country": d.get("country"), "ip": d.get("ip")}), ] def _cached() -> dict | None: try: blob = json.loads(CACHE.read_text()) if time.time() - blob.get("_ts", 0) < TTL: return blob["data"] except (FileNotFoundError, json.JSONDecodeError, KeyError): pass return None def _store(data: dict) -> None: CACHE.parent.mkdir(parents=True, exist_ok=True) CACHE.write_text(json.dumps({"_ts": time.time(), "data": data})) def _fetch(url: str) -> dict: req = urllib.request.Request(url, headers={"User-Agent": "astal-menu/1.0"}) with urllib.request.urlopen(req, timeout=8) as resp: return json.loads(resp.read().decode()) def main() -> int: if "--no-cache" not in sys.argv: hit = _cached() if hit: print(json.dumps(hit)) return 0 for name, url, parse in PROVIDERS: try: data = parse(_fetch(url)) data["source"] = name if data.get("lat") is None or data.get("lon") is None: raise ValueError("no coordinates") _store(data) print(json.dumps(data)) return 0 except Exception as exc: # noqa: BLE001 — try the next provider print(f"{name}: {exc}", file=sys.stderr) print("all geolocation providers failed", file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())