HTML 82.1%
Python 14.6%
TypeScript 1.9%
CSS 1%
JavaScript 0.5%
1# =============================================================================2# Job·Ka — Groupe KA3# Auteur : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : jobka/hubfav.py6# Rôle : Favoris ♥ « Mon univers Ka » — le hub Groupe KA (groupe-ka.com)7# est le MAGASIN CENTRAL des favoris du groupe : Job·Ka ne stocke rien8# localement. Chaque ♥ est poussé au hub (POST signé HMAC, synchrone : un9# échec remonte à l'appelant) et la liste est lue au hub (GET signé, cache10# mémoire 30 s, invalidé à chaque toggle). Même secret que le SSO.11# Config .env : KA_SSO_SECRET, KA_HUB_URL (optionnel).12# Créé : 2026-08-23 Modifié : 2026-08-2313# =============================================================================14from __future__ import annotations1516import hashlib17import hmac18import os19import threading20import time2122import requests2324CLIENT_ID = "job-ka"25KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")26CACHE_TTL = 30 # secondes — liste des favoris27TIMEOUT = 6 # secondes2829_cache: dict[str, tuple[float, list[dict]]] = {}30_lock = threading.Lock()3132# champs d'item acceptés -> longueur maximale (troncature défensive)33_FIELDS = {"item_id": 120, "title": 200, "subtitle": 200,34 "price_label": 60, "image_url": 500, "url": 500}353637def _sig(ka_id: str, ts: int) -> str | None:38 """Signature HMAC-SHA256 du hub : hex("job-ka.<ka_id>.<ts>")."""39 secret = os.environ.get("KA_SSO_SECRET")40 if not secret:41 return None42 return hmac.new(secret.encode(),43 f"{CLIENT_ID}.{ka_id}.{ts}".encode(),44 hashlib.sha256).hexdigest()454647def linked(ka_id: str | None) -> bool:48 """Vrai si le compte est relié au hub (KA-ID « ka-… » du groupe)."""49 return bool(ka_id) and str(ka_id).startswith("ka-")505152def clean_item(item: dict) -> dict:53 """Ne garde que les champs d'item connus, en chaînes tronquées."""54 return {k: str(item.get(k) or "")[:n]55 for k, n in _FIELDS.items() if item.get(k)}565758def hub_toggle(ka_id: str, action: str, item: dict) -> bool:59 """Pousse un ♥ (« add » / « remove ») au hub — SYNCHRONE, timeout 6 s :60 le hub est le magasin des favoris, l'échec doit remonter à l'appelant."""61 ts = int(time.time())62 sig = _sig(ka_id, ts)63 if not sig or not linked(ka_id) or action not in ("add", "remove"):64 return False65 try:66 r = requests.post(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT,67 json={"client_id": CLIENT_ID, "ka_id": ka_id,68 "ts": str(ts), "sig": sig,69 "action": action, "item": item})70 ok = r.status_code == 20071 except Exception:72 ok = False73 if ok:74 with _lock:75 _cache.pop(ka_id, None) # la prochaine lecture reflète le toggle76 return ok777879def hub_list(ka_id: str) -> list[dict] | None:80 """Favoris Job·Ka du membre, lus au hub (cache mémoire 30 s).81 [] = aucun favori ; None = hub injoignable (erreur, jamais mise en cache)."""82 if not linked(ka_id):83 return [] # compte legacy non relié au hub84 now = time.time()85 with _lock:86 hit = _cache.get(ka_id)87 if hit and now - hit[0] < CACHE_TTL:88 return hit[1]89 sig = _sig(ka_id, int(now))90 if not sig:91 return None92 try:93 r = requests.get(f"{KA_HUB_URL}/api/sso/favorites", timeout=TIMEOUT,94 params={"client_id": CLIENT_ID, "ka_id": ka_id,95 "ts": int(now), "sig": sig})96 if r.status_code != 200:97 return None98 favs = r.json().get("favorites") or []99 if not isinstance(favs, list):100 return None101 except Exception:102 return None103 favs = [f for f in favs if isinstance(f, dict)]104 with _lock:105 _cache[ka_id] = (now, favs)106 return favs107