# Author: Simon-Pierre Boucher # Contact: contact@spboucher.ai # Project: Toit-Ka # ----------------------------------------------------------------------------- # hubfav.py : favoris unifiés « Mon univers Ka » — Toit·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) 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, "toit-ka..") hex (ts ±5 min). # ----------------------------------------------------------------------------- from __future__ import annotations import hashlib import hmac import os import threading import time import requests CLIENT_ID = "toit-ka" KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/") LIST_TTL = 30 _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 (« add » ou « remove ») — synchrone (action utilisateur).""" if not ka_id or not str(ka_id).startswith("ka-"): return False 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 toit-ka du membre, lus du hub (cache 30 s, [] sur erreur).""" 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 après un toggle (état frais au prochain GET).""" with _lock: _cache.pop(ka_id, None)