get_inbox/get_conversation return a sections+references envelope, not a message list. Parser is covered by synthetic Alice/Bob fixtures; CI now runs the nested bin/chats tests. Session check no longer launches Chromium.
156 lines
5.2 KiB
Python
Executable File
156 lines
5.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""chats/refresh-linkedin-session - refresh LinkedIn MCP session from webtop CDP.
|
|
|
|
bin/chats/refresh-linkedin-session [--cdp URL] [--root DIR]
|
|
"""
|
|
|
|
Reads the current LinkedIn cookies out of the running Thorium browser in the
|
|
work-webtop container via CDP (Network.getAllCookies), copies the live browser
|
|
profile onto the source profile directory, and rewrites the portable
|
|
cookies.json + source-state.json that mcp-server-linkedin requires.
|
|
|
|
Usage:
|
|
refresh-linkedin-session [--cdp http://127.0.0.1:9222] [--root /var/tmp/liprofile]
|
|
[--container work-webtop] [--profile thorium-profile]
|
|
|
|
After the headless driver uses a copied profile, LinkedIn rotates the session
|
|
in that copy, so this must run before every sync.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import urllib.request
|
|
|
|
import websockets
|
|
|
|
|
|
def cdp_tab(ws_json):
|
|
for t in ws_json:
|
|
if t.get("webSocketDebuggerUrl"):
|
|
return t["webSocketDebuggerUrl"]
|
|
return None
|
|
|
|
|
|
async def get_cookies(ws_url):
|
|
async with websockets.connect(ws_url, max_size=50_000_000) as ws:
|
|
await ws.send(json.dumps({"id": 1, "method": "Network.getAllCookies", "params": {}}))
|
|
resp = await ws.recv()
|
|
return json.loads(resp).get("result", {}).get("cookies", [])
|
|
|
|
|
|
def write_source_state(root, profile_dir):
|
|
# Reuse the linkedin-mcp-server session_state module to write a valid
|
|
# source-state.json (same schema the daemon reads).
|
|
try:
|
|
from linkedin_mcp_server.session_state import canonical, write_source_state
|
|
|
|
write_source_state(canonical(__import__("pathlib").Path(profile_dir)))
|
|
return
|
|
except Exception:
|
|
pass
|
|
# Fallback: minimal schema-compatible state.
|
|
import uuid
|
|
|
|
state = {
|
|
"version": 1,
|
|
"source_runtime_id": "linux-amd64-host",
|
|
"login_generation": str(uuid.uuid4()),
|
|
"created_at": None,
|
|
"profile_path": profile_dir,
|
|
"cookies_path": os.path.join(root, "cookies.json"),
|
|
}
|
|
from datetime import datetime, timezone
|
|
|
|
state["created_at"] = datetime.now(timezone.utc).isoformat()
|
|
with open(os.path.join(root, "source-state.json"), "w") as f:
|
|
json.dump(state, f, indent=2)
|
|
|
|
|
|
def main():
|
|
args = sys.argv[1:]
|
|
cdp = "http://127.0.0.1:9222"
|
|
root = "/var/tmp/liprofile"
|
|
container = "work-webtop"
|
|
cprofile = "thorium-profile"
|
|
for i in range(0, len(args), 2):
|
|
k = args[i]
|
|
v = args[i + 1] if i + 1 < len(args) else ""
|
|
if k == "--cdp":
|
|
cdp = v
|
|
elif k == "--root":
|
|
root = v
|
|
elif k == "--container":
|
|
container = v
|
|
elif k == "--profile":
|
|
cprofile = v
|
|
|
|
profile_dir = os.path.join(root, "profile")
|
|
os.makedirs(profile_dir, exist_ok=True)
|
|
|
|
# 1. Clear stale daemon/browser locks so the server can claim the profile.
|
|
for lock in ("profile-claim.lock", "profile.lock", "daemon.lock", "lease.lock"):
|
|
p = os.path.join(root, lock)
|
|
if os.path.exists(p):
|
|
os.remove(p)
|
|
for name in os.listdir(profile_dir):
|
|
if name.startswith("Singleton"):
|
|
os.remove(os.path.join(profile_dir, name))
|
|
for name in os.listdir(root):
|
|
if name.startswith("invalid-state-"):
|
|
shutil.rmtree(os.path.join(root, name), ignore_errors=True)
|
|
|
|
# 1. Copy the live browser profile (cookies DB + Local State) so the
|
|
# session the driver launches carries the current login.
|
|
subprocess.run(
|
|
["docker", "cp", f"{container}:/config/{cprofile}/Default", os.path.join(profile_dir, "Default")],
|
|
check=True, capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["docker", "cp", f"{container}:/config/{cprofile}/Local State", os.path.join(profile_dir, "Local State")],
|
|
check=True, capture_output=True,
|
|
)
|
|
for lock in ("SingletonLock", "SingletonCookie", "SingletonSocket"):
|
|
p = os.path.join(profile_dir, lock)
|
|
if os.path.exists(p):
|
|
os.remove(p)
|
|
|
|
# 2. Pull the live cookies out of the running browser.
|
|
with urllib.request.urlopen(f"{cdp}/json", timeout=5) as r:
|
|
tabs = json.loads(r.read())
|
|
ws_url = cdp_tab(tabs)
|
|
if not ws_url:
|
|
sys.stderr.write("refresh-linkedin-session: no CDP tab\n")
|
|
sys.exit(1)
|
|
cookies = asyncio.run(get_cookies(ws_url))
|
|
|
|
li = [c for c in cookies if "linkedin" in c.get("domain", "")]
|
|
out = []
|
|
for c in li:
|
|
domain = c.get("domain", "")
|
|
if domain in (".www.linkedin.com", "www.linkedin.com"):
|
|
domain = ".linkedin.com"
|
|
out.append({
|
|
"name": c["name"],
|
|
"value": c["value"].strip('"'),
|
|
"domain": domain,
|
|
"path": c.get("path", "/"),
|
|
"expires": c.get("expires", -1),
|
|
"httpOnly": c.get("httpOnly", False),
|
|
"secure": c.get("secure", False),
|
|
"sameSite": c.get("sameSite", "None"),
|
|
})
|
|
with open(os.path.join(root, "cookies.json"), "w") as f:
|
|
json.dump(out, f, indent=2)
|
|
|
|
write_source_state(root, profile_dir)
|
|
sys.stderr.write(f"refresh-linkedin-session: {len(out)} cookies, profile refreshed\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|