"""IP geolocation fallback for default location detection. Uses the free ip-api.com service as an approximate setup/default-location hint. Results are cached in memory for the session to avoid repeated network calls. This cache is not authenticated personal-location authority and must not be used for per-user assistance or disclosure. """ from __future__ import annotations import asyncio import json import urllib.request from rex.logging_utils import get_logger LOGGER = get_logger(__name__) _IP_API_URL = "http://ip-api.com/json/?fields=city,timezone,lat,lon,status,message" _TIMEOUT_SECONDS = 3.0 _location_cache: dict[str, object] | None = None async def detect_location() -> dict[str, object] | None: """Detect approximate location via IP geolocation. Returns: Dict with keys ``city`` (str), ``timezone`` (str), ``lat`` (float), ``lon`` (float), or ``None`` if detection fails or times out. Notes: Result is cached in memory for the session. If ``default_location`` is set in config, callers should skip this function entirely. """ global _location_cache if _location_cache is not None: return _location_cache try: result = await asyncio.wait_for( asyncio.get_running_loop().run_in_executor(None, _fetch_location), timeout=_TIMEOUT_SECONDS, ) if result is not None: _location_cache = result return result except TimeoutError: LOGGER.warning("IP geolocation timed out after %.1fs", _TIMEOUT_SECONDS) return None except Exception as exc: LOGGER.warning("IP geolocation failed: %s", exc) return None def _fetch_location() -> dict[str, object] | None: """Blocking HTTP fetch of ip-api.com. Run in an executor.""" try: req = urllib.request.Request( _IP_API_URL, headers={"User-Agent": "Rex-AI-Assistant/1.0"}, ) with urllib.request.urlopen(req, timeout=_TIMEOUT_SECONDS) as resp: raw = resp.read().decode("utf-8") data = json.loads(raw) if data.get("status") != "success": LOGGER.warning("ip-api returned non-success status: %s", data.get("message")) return None return { "city": str(data["city"]), "timezone": str(data["timezone"]), "lat": float(data["lat"]), "lon": float(data["lon"]), } except Exception as exc: LOGGER.warning("_fetch_location error: %s", exc) return None def get_cached_timezone() -> str | None: """Return the timezone from the in-memory cache, or None if not available.""" if _location_cache is not None: tz = _location_cache.get("timezone") if isinstance(tz, str) and tz: return tz return None def get_cached_city() -> str | None: """Return the city name from the in-memory cache, or None if not available.""" if _location_cache is not None: city = _location_cache.get("city") if isinstance(city, str) and city: return city return None def clear_cache() -> None: """Clear the in-memory location cache (useful for testing).""" global _location_cache _location_cache = None