signal-bot/fake_signal/routes_account.py
2026-08-29 15:30:08 +02:00

74 lines
2.6 KiB
Python

# 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