# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """The endpoints the real signal-cli-rest-api exposes.""" from __future__ import annotations from typing import Any from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect from fastapi.responses import JSONResponse, Response from fake_signal.state import PIXEL_PNG, State def _state(request: Request) -> State: """Return the State attached to the running app.""" state: State = request.app.state.fake return state def build_api_router() -> APIRouter: """Build the router mirroring the real container's API.""" router = APIRouter() @router.get("/v1/about") async def about(request: Request) -> dict[str, Any]: return {"build": 2, "mode": _state(request).mode, "versions": ["v1", "v2"]} @router.get("/v1/qrcodelink") async def qrcodelink(device_name: str = "signal-api") -> Response: # The real endpoint returns a PNG, which is why the live runbook has # to open an image rather than print a string. return Response( content=PIXEL_PNG, media_type="image/png", headers={"X-Device-Name": device_name}, ) @router.post("/v2/send") async def send(request: Request, payload: dict[str, Any]) -> JSONResponse: _state(request).record_send(payload) return JSONResponse({"timestamp": 1719177917717}, status_code=201) @router.get("/v1/attachments") async def list_attachments(request: Request) -> list[str]: return list(_state(request).attachments) @router.delete("/v1/attachments/{attachment_id}") async def delete_attachment(request: Request, attachment_id: str) -> Response: attachments = _state(request).attachments if attachment_id in attachments: attachments.remove(attachment_id) return Response(status_code=204) _add_receive_route(router) return router def _add_receive_route(router: APIRouter) -> None: """Attach the WebSocket receive endpoint. Split out so the router builder stays under the complexity cap. """ @router.websocket("/v1/receive/{number}") async def receive(websocket: WebSocket, number: str) -> None: state: State = websocket.app.state.fake await websocket.accept() await websocket.send_json({"envelope": {"subscribed": number}}) state.sockets.append(websocket) try: while True: # Blocks until the client goes away. The bot never sends. await websocket.receive_text() except WebSocketDisconnect: pass finally: # Branchless: a conditional remove leaves an arc no test can reach. state.sockets[:] = [s for s in state.sockets if s is not websocket]