# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Registration and group endpoints. Mirrors the shapes scripts/register_bot.sh depends on, so the whole runbook can be rehearsed offline. The real captcha and SMS steps are the only parts that cannot be faked, and both are represented by their exact failure mode. """ from __future__ import annotations from typing import TYPE_CHECKING, Any from fastapi import APIRouter, Request from fastapi.responses import JSONResponse if TYPE_CHECKING: from fake_signal.state import State CAPTCHA_REQUIRED = ( "Captcha required for verification, use --captcha CAPTCHA\n" "To get the token, go to https://signalcaptchas.org/registration/generate.html\n" 'After solving the captcha, right-click on the "Open Signal" link and copy the link.' ) # Mirrors signalapp/registration-service in dev mode, which accepts the last # six digits of the number as the verification code. CODE_LENGTH = 6 def _state(request: Request) -> State: """Return the State attached to the running app.""" state: State = request.app.state.fake return state def expected_code(number: str) -> str: """The verification code the fake accepts for a number.""" digits = "".join(c for c in number if c.isdigit()) return digits[-CODE_LENGTH:] def build_account_router() -> APIRouter: """Build the router covering accounts, registration and groups.""" router = APIRouter() @router.get("/v1/accounts") async def accounts(request: Request) -> list[str]: return list(_state(request).accounts) @router.post("/v1/register/{number}") async def register( request: Request, number: str, payload: dict[str, Any] | None = None ) -> JSONResponse: captcha = (payload or {}).get("captcha") if not captcha: return JSONResponse({"error": CAPTCHA_REQUIRED}, status_code=400) _state(request).pending[number] = expected_code(number) return JSONResponse({}, status_code=201) @router.post("/v1/register/{number}/verify/{token}") async def verify(request: Request, number: str, token: str) -> JSONResponse: state = _state(request) if number not in state.pending: return JSONResponse( {"error": f"{number} has no registration in progress"}, status_code=400 ) if token != state.pending[number]: return JSONResponse({"error": "invalid verification code"}, status_code=400) del state.pending[number] if number not in state.accounts: state.accounts.append(number) return JSONResponse({}, status_code=201) return router