Files
crkl/scripts/gog-bridge.py
T
ilia cbed90db21 Ship Circle 1.16.2: overlay assist, brand, and smoke tooling
VIP loop (circle → extract → Translate/Copy/Explain/Share/Vikunja),
lasso branding, ship docs, unit tests, and make smoke / launchd emu helpers.
2026-08-05 12:26:51 -04:00

110 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Optional lab bridge: Crkl emulator → host `gog` CLI (Gmail send).
make gog-bridge
# Integrations → Prefer gog + URL http://10.0.2.2:8765
Endpoints:
GET /health
POST /v1/gmail/send JSON: {to, subject, body}
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HOST = os.environ.get("CRKL_GOG_BRIDGE_HOST", "0.0.0.0")
PORT = int(os.environ.get("CRKL_GOG_BRIDGE_PORT", "8765"))
GOG = os.environ.get("GOG_BIN") or shutil.which("gog") or "gog"
ACCOUNT = os.environ.get("GOG_ACCOUNT", "")
def run_gog(args: list[str]) -> tuple[int, str, str]:
cmd = [GOG, *args]
if ACCOUNT:
cmd[1:1] = ["-a", ACCOUNT]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt: str, *args) -> None:
sys.stderr.write("gog-bridge: " + (fmt % args) + "\n")
def _json(self, code: int, payload: dict) -> None:
raw = json.dumps(payload).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
def do_GET(self) -> None:
if self.path.startswith("/health"):
code, out, err = run_gog(["auth", "list", "-j"])
accounts: list = []
if code == 0 and out:
try:
parsed = json.loads(out)
raw = parsed.get("accounts") if isinstance(parsed, dict) else parsed
if isinstance(raw, list):
accounts = raw
except json.JSONDecodeError:
accounts = []
ok = code == 0 and len(accounts) > 0
labels = []
for a in accounts[:5]:
if isinstance(a, dict):
labels.append(str(a.get("email") or a.get("account") or a))
else:
labels.append(str(a))
msg = (
"gog ok: " + ", ".join(labels)
if ok
else "No gog tokens. Run: gog auth add (or tunnel Hermes:8765)"
)
self._json(200 if ok else 503, {"ok": ok, "message": msg, "accounts": len(accounts)})
return
self._json(404, {"ok": False, "message": "not found"})
def do_POST(self) -> None:
length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(length) if length else b"{}"
try:
body = json.loads(raw.decode() or "{}")
except json.JSONDecodeError:
self._json(400, {"ok": False, "message": "invalid json"})
return
if self.path.startswith("/v1/gmail/send"):
to = (body.get("to") or "").strip()
subject = (body.get("subject") or "").strip()
text = body.get("body") or ""
if not to or not subject:
self._json(400, {"ok": False, "message": "to and subject required"})
return
args = ["gmail", "send", "--to", to, "--subject", subject, "--body", text]
if body.get("account"):
args = ["-a", body["account"], *args]
code, out, err = run_gog(args)
if code != 0:
self._json(502, {"ok": False, "message": err or out or "send failed"})
return
self._json(200, {"ok": True, "message": f"Sent via gog → {to}\n{out}".strip()})
return
self._json(404, {"ok": False, "message": "not found"})
def main() -> None:
print(f"Crkl gog bridge on http://{HOST}:{PORT} (gog={GOG})")
print("Emulator URL: http://10.0.2.2:8765")
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
if __name__ == "__main__":
main()