# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # hubfav.py : favoris unifiés « Mon univers Ka » — chaque ♥ ajouté/retiré sur # Lou-Ka est poussé au hub Groupe KA (magasin central des favoris du # groupe), signé HMAC du secret SSO partagé. Poussée en arrière-plan # (thread), jamais bloquante : un échec réseau n'affecte pas le ♥ local. # Config .env : KA_SSO_SECRET, KA_HUB_URL (optionnel). # ----------------------------------------------------------------------------- from __future__ import annotations import hashlib import hmac import os import threading import time import requests CLIENT_ID = "lou-ka" KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/") def _sig(ka_id: str, ts: int) -> str | None: secret = os.environ.get("KA_SSO_SECRET") if not secret: return None return hmac.new(secret.encode(), f"{CLIENT_ID}.{ka_id}.{ts}".encode(), hashlib.sha256).hexdigest() def _push(ka_id: str, action: str, item: dict) -> None: ts = int(time.time()) sig = _sig(ka_id, ts) if not sig: return try: requests.post(f"{KA_HUB_URL}/api/sso/favorites", timeout=6, json={ "client_id": CLIENT_ID, "ka_id": ka_id, "ts": str(ts), "sig": sig, "action": action, "item": item, }) except Exception: pass # best-effort : le favori local reste la référence d'affichage def push_favorite(ka_id: str | None, action: str, item: dict) -> None: """Pousse un ♥ (action « add » ou « remove ») au hub, sans bloquer.""" if not ka_id or not str(ka_id).startswith("ka-"): return # compte legacy non relié au hub threading.Thread(target=_push, args=(ka_id, action, item), daemon=True).start() def listing_item(row) -> dict: """Transforme une ligne `listings` en item de favori pour le hub.""" import json as _json d = dict(row) try: images = _json.loads(d.get("images") or "[]") except Exception: images = [] title = d.get("title") or d.get("address") or "Logement" bits = [b for b in [d.get("unit_type"), d.get("sector") or d.get("city")] if b] return { "item_id": d["uid"], "title": str(title)[:200], "subtitle": " · ".join(str(b) for b in bits)[:200], "price_label": (d.get("price_label") or (f"{int(d['price'])} $/mois" if d.get("price") else ""))[:60], "image_url": (images[0] if images else "")[:500], "url": f"https://www.lou-ka.com/logement/{d['uid']}", }