SPB Git forge

spb/sorti-ka

Public

Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%
3.9 KB · 105 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Sorti-Ka — Agrégateur de sorties & événements (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# hubfav.py : favoris « Mon univers Ka » — le hub Groupe KA (groupe-ka.com)5#   est le MAGASIN CENTRAL des favoris du groupe : Sorti·Ka ne stocke rien6#   localement. Chaque ♥ est poussé au hub (POST signé HMAC, synchrone : un7#   échec remonte à l'appelant) et la liste est lue au hub (GET signé, cache8#   mémoire 30 s, invalidé à chaque toggle). Même secret que le SSO.9#   Config .env : KA_SSO_SECRET, KA_HUB_URL (optionnel).10#   (patron Food-Ka : foodka/hubfav.py)11# -----------------------------------------------------------------------------12from __future__ import annotations1314import hashlib15import hmac16import os17import threading18import time1920import requests2122CLIENT_ID = "sorti-ka"23KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")24CACHE_TTL = 30      # secondes — liste des favoris25TIMEOUT = 6         # secondes2627_cache: dict[str, tuple[float, list[dict]]] = {}28_lock = threading.Lock()2930# champs d'item acceptés -> longueur maximale (troncature défensive)31_FIELDS = {"item_id": 120, "title": 200, "subtitle": 200,32           "price_label": 60, "image_url": 500, "url": 500}333435def _sig(ka_id: str, ts: int) -> str | None:36    """Signature HMAC-SHA256 du hub : hex("sorti-ka.<ka_id>.<ts>")."""37    secret = os.environ.get("KA_SSO_SECRET")38    if not secret:39        return None40    return hmac.new(secret.encode(),41                    f"{CLIENT_ID}.{ka_id}.{ts}".encode(),42                    hashlib.sha256).hexdigest()434445def linked(ka_id: str | None) -> bool:46    """Vrai si le compte est relié au hub (KA-ID « ka-… » du groupe)."""47    return bool(ka_id) and str(ka_id).startswith("ka-")484950def clean_item(item: dict) -> dict:51    """Ne garde que les champs d'item connus, en chaînes tronquées."""52    return {k: str(item.get(k) or "")[:n]53            for k, n in _FIELDS.items() if item.get(k)}545556def hub_toggle(ka_id: str, action: str, item: dict) -> bool:57    """Pousse un ♥ (« add » / « remove ») au hub — SYNCHRONE, timeout 6 s :58    le hub est le magasin des favoris, l'échec doit remonter à l'appelant."""59    ts = int(time.time())60    sig = _sig(ka_id, ts)61    if not sig or not linked(ka_id) or action not in ("add", "remove"):62        return False63    try:64        r = requests.post(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT,65                          json={"client_id": CLIENT_ID, "ka_id": ka_id,66                                "ts": str(ts), "sig": sig,67                                "action": action, "item": item})68        ok = r.status_code == 20069    except Exception:70        ok = False71    if ok:72        with _lock:73            _cache.pop(ka_id, None)   # la prochaine lecture reflète le toggle74    return ok757677def hub_list(ka_id: str) -> list[dict] | None:78    """Favoris Sorti·Ka du membre, lus au hub (cache mémoire 30 s).79    [] = aucun favori ; None = hub injoignable (erreur, jamais mise en cache)."""80    if not linked(ka_id):81        return []                     # compte legacy non relié au hub82    now = time.time()83    with _lock:84        hit = _cache.get(ka_id)85        if hit and now - hit[0] < CACHE_TTL:86            return hit[1]87    sig = _sig(ka_id, int(now))88    if not sig:89        return None90    try:91        r = requests.get(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT,92                         params={"client_id": CLIENT_ID, "ka_id": ka_id,93                                 "ts": int(now), "sig": sig})94        if r.status_code != 200:95            return None96        favs = r.json().get("favorites") or []97        if not isinstance(favs, list):98            return None99    except Exception:100        return None101    favs = [f for f in favs if isinstance(f, dict)]102    with _lock:103        _cache[ka_id] = (now, favs)104    return favs105