# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """The injector's control plane. Not part of the real API.""" from __future__ import annotations from typing import TYPE_CHECKING, Any from fastapi import APIRouter, Request if TYPE_CHECKING: from fake_signal.state import State def _state(request: Request) -> State: """Return the State attached to the running app.""" state: State = request.app.state.fake return state def build_control_router() -> APIRouter: """Build the router the scenario injector drives.""" router = APIRouter(prefix="/_control") @router.post("/inject") async def inject(request: Request, frame: dict[str, Any]) -> dict[str, int]: delivered = 0 for socket in list(_state(request).sockets): await socket.send_text(str(frame["raw"])) delivered += 1 return {"delivered": delivered} @router.post("/groups") async def set_groups(request: Request, groups: list[dict[str, Any]]) -> dict[str, int]: """Replace the group list, so the group picker can be rehearsed offline.""" state = _state(request) state.groups.clear() state.groups.extend(groups) return {"groups": len(state.groups)} @router.post("/groups/add") async def add_groups(request: Request, groups: list[dict[str, Any]]) -> dict[str, int]: """Add groups without disturbing the existing ones. Additive on purpose. A scenario that declares only the group it cares about would otherwise silently remove the others -- and with the bot dropping a departed group's rows, that would delete state every later scenario in the same process depends on. """ state = _state(request) known = {group.get("id") for group in state.groups} state.groups.extend(group for group in groups if group.get("id") not in known) return {"groups": len(state.groups)} @router.post("/accounts") async def set_accounts(request: Request, accounts: list[str]) -> dict[str, int]: """Seed the registered accounts, so /v1/groups answers at all.""" state = _state(request) state.accounts.clear() state.accounts.extend(accounts) return {"accounts": len(state.accounts)} @router.get("/sent") async def captured(request: Request) -> list[dict[str, Any]]: return list(_state(request).sent) @router.post("/reset") async def reset(request: Request) -> dict[str, str]: _state(request).reset() return {"status": "ok"} @router.get("/ready") async def ready(request: Request) -> dict[str, int]: return {"sockets": len(_state(request).sockets)} return router