SPB Git forge

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

52commits 1branches 0releases
11.3 MBsize
maindefault branch
19 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%
3.8 KB · 104 lines python
Raw Blame History
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File:   creaka/hubfav.py4# Desc:   Favoris « Mon univers Ka » — le hub Groupe KA (groupe-ka.com) est le5#         MAGASIN CENTRAL des favoris du groupe : Créa-Ka ne stocke rien6#         localement. Chaque ♥ est poussé au hub (POST signé HMAC, synchrone :7#         un échec remonte à l'appelant) et la liste est lue au hub (GET signé,8#         cache mémoire 30 s, invalidé à chaque toggle). Même secret que le SSO.9#         Config .env : KA_SSO_SECRET, KA_HUB_URL (optionnel).10# ==============================================================================11from __future__ import annotations1213import hashlib14import hmac15import os16import threading17import time1819import requests2021CLIENT_ID = "crea-ka"22KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")23CACHE_TTL = 30      # secondes — liste des favoris24TIMEOUT = 6         # secondes2526_cache: dict[str, tuple[float, list[dict]]] = {}27_lock = threading.Lock()2829# champs d'item acceptés -> longueur maximale (troncature défensive)30_FIELDS = {"item_id": 120, "title": 200, "subtitle": 200,31           "price_label": 60, "image_url": 500, "url": 500}323334def _sig(ka_id: str, ts: int) -> str | None:35    """Signature HMAC-SHA256 du hub : hex("crea-ka.<ka_id>.<ts>")."""36    secret = os.environ.get("KA_SSO_SECRET")37    if not secret:38        return None39    return hmac.new(secret.encode(),40                    f"{CLIENT_ID}.{ka_id}.{ts}".encode(),41                    hashlib.sha256).hexdigest()424344def linked(ka_id: str | None) -> bool:45    """Vrai si le compte est relié au hub (KA-ID « ka-… » du groupe)."""46    return bool(ka_id) and str(ka_id).startswith("ka-")474849def clean_item(item: dict) -> dict:50    """Ne garde que les champs d'item connus, en chaînes tronquées."""51    return {k: str(item.get(k) or "")[:n]52            for k, n in _FIELDS.items() if item.get(k)}535455def hub_toggle(ka_id: str, action: str, item: dict) -> bool:56    """Pousse un ♥ (« add » / « remove ») au hub — SYNCHRONE, timeout 6 s :57    le hub est le magasin des favoris, l'échec doit remonter à l'appelant."""58    ts = int(time.time())59    sig = _sig(ka_id, ts)60    if not sig or not linked(ka_id) or action not in ("add", "remove"):61        return False62    try:63        r = requests.post(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT,64                          json={"client_id": CLIENT_ID, "ka_id": ka_id,65                                "ts": str(ts), "sig": sig,66                                "action": action, "item": item})67        ok = r.status_code == 20068    except Exception:69        ok = False70    if ok:71        with _lock:72            _cache.pop(ka_id, None)   # la prochaine lecture reflète le toggle73    return ok747576def hub_list(ka_id: str) -> list[dict] | None:77    """Favoris Créa-Ka du membre, lus au hub (cache mémoire 30 s).78    [] = aucun favori ; None = hub injoignable (erreur, jamais mise en cache)."""79    if not linked(ka_id):80        return []                     # compte legacy non relié au hub81    now = time.time()82    with _lock:83        hit = _cache.get(ka_id)84        if hit and now - hit[0] < CACHE_TTL:85            return hit[1]86    sig = _sig(ka_id, int(now))87    if not sig:88        return None89    try:90        r = requests.get(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT,91                         params={"client_id": CLIENT_ID, "ka_id": ka_id,92                                 "ts": int(now), "sig": sig})93        if r.status_code != 200:94            return None95        favs = r.json().get("favorites") or []96        if not isinstance(favs, list):97            return None98    except Exception:99        return None100    favs = [f for f in favs if isinstance(f, dict)]101    with _lock:102        _cache[ka_id] = (now, favs)103    return favs104