diff --git a/api_server.py b/api_server.py new file mode 100644 index 0000000..8a0c2af --- /dev/null +++ b/api_server.py @@ -0,0 +1,117 @@ +"""Simple HTTP API for the Matrix Bridge. + +Provides endpoints for agents and scheduled tasks to interact +with the bridge without going through the Matrix protocol. + +Endpoints: + GET /session-id — Return the current bridge session ID + POST /send — Send a text message to the configured Matrix room +""" + +from __future__ import annotations + +import json +import logging +import threading +from http.server import HTTPServer, BaseHTTPRequestHandler +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from bridge import Bridge + +log = logging.getLogger("matrix-bridge.api") + + +class _Handler(BaseHTTPRequestHandler): + """HTTP handler that delegates to a shared bridge reference.""" + + # Set once before the server starts — shared across all requests. + bridge: Optional["Bridge"] = None + + # ── Helpers ─────────────────────────────────────────────────────── + + def _send_json(self, status: int, data: dict) -> None: + """Send a JSON response with the given status code.""" + body = json.dumps(data).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _read_body(self) -> Optional[dict]: + """Read and parse the request body as JSON.""" + try: + length = int(self.headers.get("Content-Length", 0)) + except (ValueError, TypeError): + return None + if length <= 0: + return None + raw = self.rfile.read(length) + try: + return json.loads(raw) + except json.JSONDecodeError: + return None + + # ── Routes ──────────────────────────────────────────────────────── + + def do_GET(self) -> None: + if self.path == "/session-id": + if self.bridge is None: + self._send_json(503, {"error": "Bridge not initialized"}) + return + sid = self.bridge.state.session_id + self._send_json(200, {"session_id": sid}) + else: + self._send_json(404, {"error": f"Not found: {self.path}"}) + + def do_POST(self) -> None: + if self.path == "/send": + if self.bridge is None: + self._send_json(503, {"error": "Bridge not initialized"}) + return + + body = self._read_body() + if body is None: + self._send_json(400, {"error": "Invalid or missing JSON body"}) + return + + message = body.get("message") + if not message or not isinstance(message, str): + self._send_json(400, {"error": "Missing or invalid 'message' field"}) + return + + success = self.bridge.matrix.send_message(message) + if success: + self._send_json(200, {"ok": True}) + else: + self._send_json(500, {"error": "Failed to send Matrix message"}) + else: + self._send_json(404, {"error": f"Not found: {self.path}"}) + + # ── Silence default logging ─────────────────────────────────────── + + def log_message(self, format: str, *args) -> None: + log.debug(f"HTTP: {format % args}") + + +def start_api_server( + bridge: "Bridge", host: str = "127.0.0.1", port: int = 8082 +) -> None: + """Start the API HTTP server in a background daemon thread. + + The server runs until the main process exits. It provides a + lightweight JSON API on ``host:port`` for other components to + query the bridge state or trigger Matrix messages. + """ + _Handler.bridge = bridge + server = HTTPServer((host, port), _Handler) + server.timeout = 1.0 # Allows clean shutdown without dedicated stop + + thread = threading.Thread( + target=server.serve_forever, + name=f"api-{host}:{port}", + daemon=True, + ) + thread.start() + log.info(f"API server listening on http://{host}:{port}") diff --git a/config.json b/config.json index 152421d..dc496fa 100644 --- a/config.json +++ b/config.json @@ -16,6 +16,8 @@ "agent_retries": 3, "max_message_length": 40000, "processed_ids_limit": 200, - "agent_response_timeout": 300 + "agent_response_timeout": 300, + "api_host": "127.0.0.1", + "api_port": 8082 } } diff --git a/config.py b/config.py index 92ef16f..049cadf 100644 --- a/config.py +++ b/config.py @@ -37,6 +37,8 @@ class BridgeConfig: max_message_length: int = 40000 processed_ids_limit: int = 200 agent_response_timeout: int = 300 + api_host: str = "127.0.0.1" + api_port: int = 8082 @dataclass diff --git a/main.py b/main.py index 50eab59..0bf8ad9 100644 --- a/main.py +++ b/main.py @@ -20,6 +20,7 @@ import sys from config import Config from bridge import Bridge +from api_server import start_api_server def setup_logging(debug: bool = False) -> None: @@ -61,6 +62,9 @@ def main() -> None: # Create and run the bridge bridge = Bridge(config) + # Start the API server (daemon thread, non-blocking) + start_api_server(bridge, config.bridge.api_host, config.bridge.api_port) + # Verify agent API is reachable if bridge.agent._request("GET", "/api/sessions") is not None: log.info("Agent API is reachable")