VIP loop (circle → extract → Translate/Copy/Explain/Share/Vikunja), lasso branding, ship docs, unit tests, and make smoke / launchd emu helpers.
110 lines
3.9 KiB
Python
Executable File
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()
|