# Local HTTP API — handoff ## Summary Paarrot exposes a localhost HTTP API (port **33384**) so external tools — Stream Deck, scripts, macros — can control the running desktop app: mute/deafen, switch rooms, send messages, and read status. The Electron main process runs an Express server; actions are forwarded to the renderer via IPC and executed against the live Matrix client and call service. ## User-facing behavior - API starts automatically when the Electron app launches (no user setting). - Only binds to `127.0.0.1` — not reachable from the network. - No authentication today; security relies on localhost-only binding. ## Architecture ``` External client (curl, Stream Deck, script) → GET/POST http://127.0.0.1:33384/... → electron/api-server.js (Express) → IPC to renderer (executeAction) → cinny/src/app/paarrot-api.ts (initPaarrotAPI) → Matrix client / CallService / router navigate ``` Navigation for room changes uses `setPaarrotNavigate()` registered from the React router in `ClientNonUIFeatures.tsx`. ## Key files | Path | Role | |------|------| | `electron/api-server.js` | Express routes, IPC bridge to renderer | | `electron/main.js` | Starts `PaarrotAPIServer` with the main window | | `cinny/src/app/paarrot-api.ts` | Action handlers (mute, deafen, channel, message, status) | | `cinny/src/app/pages/client/ClientNonUIFeatures.tsx` | Calls `initPaarrotAPI()` and `setPaarrotNavigate()` | | `test-api.js` / `test-api.sh` | Smoke tests for endpoints | | `paarrot-api.postman_collection.json` | Postman collection (generated) | ## Data model No persistent storage. Actions read/write ephemeral app state (current room, call mute/deafen). ### IPC actions (renderer) | Action | Params | Returns | |--------|--------|---------| | `get-status` | — | mute, deafen, currentRoom, connected, userId | | `toggle-mute` / `set-mute` | `{ muted? }` | `{ muted }` | | `toggle-deafen` / `set-deafen` | `{ deafened? }` | `{ deafened }` | | `change-channel` | `{ roomId }` | navigation result | | `get-channels` | — | room list | | `send-message` | `{ roomId, message }` | send result | | `send-message-current` | `{ message }` | send to active room | | `get-current-room` | — | current room id | ## Dependencies - `express`, `cors`, `body-parser` (Electron) - `matrix-js-sdk` (renderer) - Call feature: `getCallService()` from `features/call/useCall.ts` ## Integration points - **Voice calls**: mute/deafen actions delegate to `CallService` - **Stream Deck**: `streamdeck-plugin/` consumes this API - **Room navigation**: uses `getHomeRoomPath`, `getDirectRoomPath`, `getSpaceRoomPath` - **Space detection**: `isSpaceLikeRoom()` checks `im.paarrot.room.kind` and `m.forum` type ## Testing ### Manual 1. Start Paarrot desktop (`npm run dev` or built app). 2. `curl http://127.0.0.1:33384/health` → `{ "status": "ok" }` 3. `curl http://127.0.0.1:33384/status` while logged in. 4. Join a voice call; `POST /mute/toggle` and verify UI state. 5. `POST /channel` with a room id; verify navigation. ### Automated - `node test-api.js` from repo root ## Known issues & gotchas - Mute/deafen only work when `CallService` has an active call; otherwise handlers return a message, not an error. - API is Electron-only; `initPaarrotAPI` no-ops when `window.electron.api` is missing (web/Tauri builds). - Adding endpoints requires changes in **both** `api-server.js` and `paarrot-api.ts`. ## Future work - Optional auth token for local API - WebSocket or SSE for status push (Stream Deck polling today) - Document all endpoints in Postman collection generator (`scripts/generate-postman-collection.js`) ## Related docs - [API.md](../../API.md) — full endpoint reference - [API-QUICKSTART.md](../../API-QUICKSTART.md) - [streamdeck HANDOFF](../streamdeck/HANDOFF.md) ## Add your extra things here - Default port: **33384** (`PaarrotAPIServer` constructor in `api-server.js`) - `isSpaceLikeRoom` treats `forum_space` room kind and `m.forum` create type as spaces for channel listing