Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)
Python 73.6%
HTML 13.2%
TypeScript 6%
JavaScript 4.5%
CSS 1.7%
Dockerfile 0.6%
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