← back to Wallco Ai

scripts/cdp_login_and_extract.py

168 lines

#!/usr/bin/env python3
"""
Drive the debug Chrome on :9222 via raw CDP (websocket-client, no Playwright):
  1. Find the Spoonflower tab (or open one if missing)
  2. If not signed in, fill #email/#password and click #submit
  3. Read all Spoonflower cookies via Network.getAllCookies
  4. Save to ~/.spoonflower-cookies.json (Playwright cookie shape)

This sidesteps Playwright's Browser.setDownloadBehavior issue with
externally-launched Chrome.
"""
import json, os, sys, time, urllib.request, pathlib
import websocket  # type: ignore

DEBUG_PORT = 9222
COOKIES_PATH = pathlib.Path.home() / ".spoonflower-cookies.json"


def list_targets():
    with urllib.request.urlopen(f"http://127.0.0.1:{DEBUG_PORT}/json", timeout=4) as r:
        return json.loads(r.read())


def open_new_tab(url):
    with urllib.request.urlopen(
        f"http://127.0.0.1:{DEBUG_PORT}/json/new?{url}", timeout=4
    ) as r:
        return json.loads(r.read())


class CDP:
    def __init__(self, ws_url):
        self.ws = websocket.create_connection(ws_url, timeout=30)
        self._id = 0

    def call(self, method, params=None, timeout=20):
        self._id += 1
        mid = self._id
        self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
        deadline = time.time() + timeout
        while time.time() < deadline:
            self.ws.settimeout(max(0.1, deadline - time.time()))
            try:
                msg = json.loads(self.ws.recv())
            except websocket.WebSocketTimeoutException:
                break
            if msg.get("id") == mid:
                if "error" in msg:
                    raise RuntimeError(f"CDP error {method}: {msg['error']}")
                return msg.get("result", {})
        raise TimeoutError(f"CDP {method} timed out")

    def close(self):
        try:
            self.ws.close()
        except Exception:
            pass


def get_creds():
    sys.path.insert(0, str(pathlib.Path(__file__).parent))
    from spoonflower_lib import creds
    return creds()


def find_spoonflower_target():
    targets = list_targets()
    for t in targets:
        if t.get("type") == "page" and "spoonflower" in t.get("url", ""):
            return t
    # No spoonflower tab — open one
    print("[cdp] no spoonflower tab — opening one", flush=True)
    open_new_tab("https://www.spoonflower.com/login")
    time.sleep(4)
    for t in list_targets():
        if t.get("type") == "page" and "spoonflower" in t.get("url", ""):
            return t
    return None


def main():
    email, password = get_creds()

    target = find_spoonflower_target()
    if not target:
        print(json.dumps({"ok": False, "error": "no spoonflower tab and could not open one"}))
        return 1

    cdp = CDP(target["webSocketDebuggerUrl"])
    try:
        cdp.call("Page.enable")
        cdp.call("Network.enable")

        url = target.get("url", "")
        print(f"[cdp] attached to {url}", flush=True)

        # If we're on /login, drive it
        if "/login" in url:
            print("[cdp] driving login form", flush=True)
            cdp.call("Runtime.evaluate", {
                "expression": (
                    f"document.querySelector('#email').value = {json.dumps(email)};"
                    f"document.querySelector('#password').value = {json.dumps(password)};"
                    "var f = document.querySelector('#email').form;"
                    "if (f) f.submit(); else document.querySelector('#submit').click();"
                ),
                "awaitPromise": False,
            })
            # Wait for navigation
            time.sleep(8)

        # Check signed-in via get-user-stats inline
        stats_js = """
        fetch('https://cart.spoonflower.com/api/spoonflower/get-user-stats', {credentials:'include'})
          .then(r => r.text()).then(t => t).catch(e => 'ERR:'+e)
        """
        r = cdp.call("Runtime.evaluate", {
            "expression": stats_js, "awaitPromise": True, "returnByValue": True,
        })
        body = r.get("result", {}).get("value", "")
        print(f"[cdp] get-user-stats body: {body[:300]}", flush=True)
        try:
            stats = json.loads(body).get("data_layer", {})
        except Exception:
            stats = {}
        user_id = stats.get("userID")
        print(f"[cdp] userID={user_id}  designForSale={stats.get('designForSale')}",
              flush=True)

        # Pull cookies regardless — even partial cookies help debug
        cks = cdp.call("Network.getAllCookies").get("cookies", [])
        # Normalize to Playwright shape
        pw_cookies = []
        for c in cks:
            if "spoonflower" not in c.get("domain", ""):
                continue
            pw_cookies.append({
                "name": c["name"],
                "value": c["value"],
                "domain": c["domain"],
                "path": c.get("path", "/"),
                "secure": bool(c.get("secure", True)),
                "httpOnly": bool(c.get("httpOnly", False)),
                "sameSite": c.get("sameSite", "Lax"),
                "expires": int(c.get("expires") or -1),
            })

        COOKIES_PATH.write_text(json.dumps(pw_cookies, indent=2))
        print(f"[cdp] wrote {len(pw_cookies)} spoonflower cookies to {COOKIES_PATH}",
              flush=True)

        # Print summary as last JSON line for the caller
        print(json.dumps({
            "ok": bool(user_id and user_id != "null"),
            "userID": user_id,
            "designForSale": stats.get("designForSale"),
            "numOrders": stats.get("numOrders"),
            "cookies_saved": len(pw_cookies),
            "cookies_path": str(COOKIES_PATH),
        }))
        return 0 if user_id else 2
    finally:
        cdp.close()


if __name__ == "__main__":
    sys.exit(main())