SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
4 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%
3.9 KB · 107 lines python
Raw Blame History
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