# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de propriétés à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # hubfav.py : favoris unifiés « Mon univers Ka » — Immo·Ka n'a AUCUN stockage # local de favoris : le hub Groupe KA (groupe-ka.com) est le magasin central. # Chaque ♥ est poussé au hub (POST signé, SYNCHRONE : c'est l'action # utilisateur) et la liste est relue du hub (GET signé, cache mémoire 30 s # par ka_id, [] sur toute erreur). sig = HMAC-SHA256(KA_SSO_SECRET, # "immo-ka..") hex — même secret que le SSO (ts ±5 min). # Config .env : KA_SSO_SECRET, KA_HUB_URL (optionnel). # ----------------------------------------------------------------------------- from __future__ import annotations import hashlib import hmac import os import threading import time import requests CLIENT_ID = "house-ka" KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/") LIST_TTL = 30 # secondes — cache mémoire de hub_list par ka_id _cache: dict[str, tuple[float, list]] = {} _lock = threading.Lock() def _sig(ka_id: str, ts: int) -> str | None: secret = os.environ.get("KA_SSO_SECRET") if not secret: return None return hmac.new(secret.encode(), f"{CLIENT_ID}.{ka_id}.{ts}".encode(), hashlib.sha256).hexdigest() def hub_toggle(ka_id: str, action: str, item: dict) -> bool: """Pousse un ♥ au hub (action « add » ou « remove ») — SYNCHRONE. C'est l'action utilisateur : on attend la réponse du hub (timeout 6 s) pour que le GET qui suit reflète l'état réel. True si le hub a accepté. """ if not ka_id or not str(ka_id).startswith("ka-"): return False # compte legacy non relié au hub ts = int(time.time()) sig = _sig(ka_id, ts) if not sig: return False try: r = requests.post(f"{KA_HUB_URL}/api/sso/favorites", timeout=6, json={ "client_id": CLIENT_ID, "ka_id": ka_id, "ts": str(ts), "sig": sig, "action": action, "item": item, }) return r.status_code == 200 except Exception: return False def hub_list(ka_id: str) -> list: """Favoris immo-ka du membre, lus du hub (GET signé, timeout 5 s). Cache mémoire 30 s par ka_id ; [] sur toute erreur (réseau, 401, 5xx…) — l'erreur n'est PAS mise en cache pour réessayer au prochain appel. """ if not ka_id: return [] now = time.time() with _lock: hit = _cache.get(ka_id) if hit and now - hit[0] < LIST_TTL: return hit[1] ts = int(now) sig = _sig(ka_id, ts) if not sig: return [] try: r = requests.get( f"{KA_HUB_URL}/api/sso/favorites", params={"client_id": CLIENT_ID, "ka_id": ka_id, "ts": ts, "sig": sig}, timeout=5) if r.status_code != 200: return [] favs = r.json().get("favorites") if not isinstance(favs, list): return [] except Exception: return [] with _lock: _cache[ka_id] = (now, favs) return favs def invalidate(ka_id: str) -> None: """Invalide le cache de hub_list après un toggle (état frais au prochain GET).""" with _lock: _cache.pop(ka_id, None)