Files
AntiCoco/tools/attach_capture.py
jerem 051ecb50d8 Écriture de sélection câblée (PUT cart) + auth par cookie storage_state
Découvert via attache CDP au vrai Chrome (contourne le blocage automation) :
- set_selection = PUT /gw/v1/carts/{week}, body {meals:[{index,quantity}], extras:[]}
  sélection par index de course, params (customer/subscription/sku/cutoff) dérivés
  dynamiquement de /subscriptions + /deliveries (aucun id en dur)
- Recipe.course_index conservé depuis le menu pour le mapping id->index
- get_editable_weeks via /deliveries (modèle Delivery: cutoff, status, editable)
- Token lu depuis le cookie apiV2Auth (storage_state) -> auth sans navigateur, headless OK
- hf_confirm_selection: garde-fou coco + dry_run; tool attach_capture.py ajouté
- Dry-run validé: requête identique à l'appel réel capturé
2026-06-15 22:57:36 +02:00

133 lines
4.9 KiB
Python

"""Attache Playwright à TON Chrome (via CDP) au lieu de lancer un navigateur piloté.
Pourquoi : un navigateur lancé par Playwright porte des drapeaux d'automatisation que
HelloFresh peut rejeter au login. Ici, c'est TOI qui lances Chrome (session normale, le
login marche), et on s'y attache en lecture pour :
- capturer le trafic gateway (dont l'appel d'écriture de sélection de recettes) ;
- exporter ta session (cookies) -> .session/storage_state.json, réutilisable en headless.
Prérequis — lance Chrome avec le port de debug et un PROFIL DÉDIÉ (Chrome 149 interdit le
debug sur le profil par défaut). Ta fenêtre Chrome habituelle peut rester ouverte :
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \\
--remote-debugging-port=9222 --user-data-dir="$HOME/.hf-chrome-debug" \\
https://www.hellofresh.fr/my-account/deliveries/menu
Puis, dans cette fenêtre : connecte-toi (email + mot de passe) et change une recette.
Usage :
python tools/attach_capture.py # attend ~5 min puis sauvegarde
ANTICOCO_DISCOVER_WAIT=600 python tools/attach_capture.py
"""
from __future__ import annotations
import json
import os
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from playwright.sync_api import sync_playwright # noqa: E402
from hellofresh import auth # noqa: E402
CDP_URL = os.environ.get("ANTICOCO_CDP_URL", "http://localhost:9222")
LOG_PATH = auth.SESSION_DIR / "discovery_log_cdp.json"
STATE_PATH = auth.SESSION_DIR / "storage_state.json"
DISCOVERED = ROOT / "config" / "endpoints_discovered.json"
def _connect(pw, timeout_s: int = 60):
"""Connexion CDP avec attente que Chrome (port debug) soit disponible."""
deadline = time.time() + timeout_s
last = None
while time.time() < deadline:
try:
return pw.chromium.connect_over_cdp(CDP_URL)
except Exception as e: # Chrome pas encore prêt
last = e
print(f" ...en attente de Chrome sur {CDP_URL}")
time.sleep(3)
raise RuntimeError(f"Impossible de se connecter à {CDP_URL} : {last}")
def main() -> None:
auth.SESSION_DIR.mkdir(parents=True, exist_ok=True)
requests_seen: list[dict] = []
def on_request(req):
if not auth._is_gateway_request(req.url):
return
entry = {"method": req.method, "url": req.url,
"has_auth": bool(req.headers.get("authorization"))}
if req.method in ("POST", "PUT", "PATCH"):
try:
entry["post_data"] = req.post_data
except Exception:
entry["post_data"] = None
requests_seen.append(entry)
print(f" [{req.method}] {req.url}")
with sync_playwright() as pw:
print(f"Connexion à Chrome via {CDP_URL} ...")
browser = _connect(pw)
contexts = browser.contexts
print(f"Connecté. {len(contexts)} contexte(s).")
def wire_page(page):
try:
page.on("request", on_request)
except Exception:
pass
for ctx in contexts:
for p in ctx.pages:
wire_page(p)
ctx.on("page", wire_page)
wait_s = int(os.environ.get("ANTICOCO_DISCOVER_WAIT", "300"))
print(f">>> Connecte-toi et change une recette. Capture {wait_s}s max "
"(Ctrl-C pour finir plus tôt).")
try:
waited = 0
while waited < wait_s and browser.is_connected():
time.sleep(2)
waited += 2
except KeyboardInterrupt:
pass
# Export de la session (cookies) tant que la connexion tient.
try:
if browser.contexts:
browser.contexts[0].storage_state(path=str(STATE_PATH))
print(f"Session exportée -> {STATE_PATH}")
except Exception as e:
print("Export storage_state impossible:", e)
_save_report(requests_seen)
def _save_report(requests_seen: list[dict]) -> None:
LOG_PATH.write_text(json.dumps(requests_seen, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"\n{len(requests_seen)} requêtes gateway -> {LOG_PATH}")
noise = ("otlp/traces", "/login", "auth/email", "translations", "/bot/")
writes = [r for r in requests_seen
if r["method"] in ("POST", "PUT", "PATCH")
and not any(n in r["url"] for n in noise)]
skeleton = {
"_comment": "Brouillon issu de attach_capture (ne remplace pas config/endpoints.json).",
"set_selection_candidates": [f"[{r['method']}] {r['url']}" for r in writes],
}
DISCOVERED.write_text(json.dumps(skeleton, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"Candidats d'écriture ({len(writes)}) -> {DISCOVERED}")
for r in writes:
print(f" [{r['method']}] {r['url']}")
if __name__ == "__main__":
main()