#!/usr/bin/env python3 """Chrome Native Messaging host: append browser visits to local SQLite.""" import json, os, sqlite3, struct, sys from datetime import datetime, timezone from pathlib import Path ARCHIVE_DIR = Path(os.environ.get("LOCAL_HISTORY_ARCHIVE_DIR", Path.home() / "ChromeHistoryArchive")) DB_PATH = ARCHIVE_DIR / "history.sqlite3" def read_message(): raw = sys.stdin.buffer.read(4) if not raw: return None length = struct.unpack("=I", raw)[0] if length > 20_000_000: raise ValueError("Message too large") return json.loads(sys.stdin.buffer.read(length).decode("utf-8")) def send_message(value): body = json.dumps(value, ensure_ascii=False).encode("utf-8") sys.stdout.buffer.write(struct.pack("=I", len(body))) sys.stdout.buffer.write(body) sys.stdout.buffer.flush() def db(): ARCHIVE_DIR.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(DB_PATH) conn.execute("PRAGMA journal_mode=WAL") conn.execute("""CREATE TABLE IF NOT EXISTS visits ( id INTEGER PRIMARY KEY, url TEXT NOT NULL, title TEXT NOT NULL DEFAULT '', visit_time TEXT NOT NULL, transition TEXT, source TEXT NOT NULL, archived_at TEXT NOT NULL, UNIQUE(url, visit_time, source))""") conn.execute("CREATE INDEX IF NOT EXISTS idx_visits_time ON visits(visit_time)") return conn def handle(message): if message.get("type") != "visits" or not isinstance(message.get("visits"), list): return {"ok": False, "error": "Expected a visits message"} now = datetime.now(timezone.utc).isoformat() rows = [] for visit in message["visits"]: url, when = visit.get("url"), visit.get("visit_time") if isinstance(url, str) and url and isinstance(when, str) and when: rows.append((url, str(visit.get("title") or ""), when, str(visit.get("transition") or "observed"), str(visit.get("source") or "chrome-extension"), now)) with db() as conn: conn.executemany("INSERT OR IGNORE INTO visits (url,title,visit_time,transition,source,archived_at) VALUES (?,?,?,?,?,?)", rows) return {"ok": True, "archived": len(rows), "database": str(DB_PATH)} try: while message := read_message(): try: send_message(handle(message)) except Exception as exc: send_message({"ok": False, "error": str(exc)}) except Exception: sys.exit(1)