Ctrl K
ComunidadGuías…

rex/geolocation.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""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