Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.
HTML 82.9%
Python 15.2%
TypeScript 0.9%
JavaScript 0.7%
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