baton/docs/backend_spec.md
2026-03-20 20:44:00 +02:00

13 KiB
Raw Blame History

Backend Spec: Baton PWA

Версия: 1.1 Дата: 2026-03-20 Статус: Approved (Architect)


1. API Contracts

POST /api/register

Регистрирует UUID→имя пользователя. Идемпотентен: повторный вызов с тем же UUID возвращает существующую запись.

Request:

POST /api/register
Content-Type: application/json

{
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Алиса"
}
Поле Тип Ограничения
uuid string UUID v4, обязателен
name string 1100 символов, обязателен

Response 200 OK:

{
  "user_id": 42,
  "uuid": "550e8400-e29b-41d4-a716-446655440000"
}

Status codes:

Код Причина
200 Успешно (новый или существующий)
422 Ошибка валидации Pydantic
500 Внутренняя ошибка сервера

POST /api/signal

Принимает сигнал тревоги от PWA. Сохраняет в SQLite, добавляет в очередь агрегатора Telegram.

Request:

POST /api/signal
Content-Type: application/json

{
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": 1742478000000,
  "geo": {
    "lat": 55.7558,
    "lon": 37.6173,
    "accuracy": 15.0
  }
}
Поле Тип Ограничения
user_id string UUID v4 пользователя, обязателен
timestamp int Unix ms, > 0, обязателен
geo object | null Необязателен; если передан — все три поля lat/lon/accuracy обязательны
geo.lat float -90.0 … 90.0
geo.lon float -180.0 … 180.0
geo.accuracy float > 0, метры

Response 200 OK:

{
  "status": "ok",
  "signal_id": 123
}

Status codes:

Код Причина
200 Сигнал принят
422 Ошибка валидации
500 Внутренняя ошибка

POST /api/webhook/telegram

Входящие обновления от Telegram Bot API. Регистрируется через setWebhook при старте сервера.

Headers:

X-Telegram-Bot-Api-Secret-Token: <WEBHOOK_SECRET>

Request body: стандартный Telegram Update object (JSON от Telegram).

Обрабатываемые команды:

Команда Действие
/start Регистрация пользователя через Telegram (INSERT OR IGNORE в users с Telegram user_id как UUID)

Response:

{"ok": true}

Status codes:

Код Причина
200 Обновление обработано
403 Неверный или отсутствующий X-Telegram-Bot-Api-Secret-Token
422 Невалидный JSON

2. DB Schema (SQLite WAL)

-- WAL mode + защита от write-lock (решения #1005, #1002)
PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000;
PRAGMA synchronous=NORMAL;

CREATE TABLE IF NOT EXISTS users (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    uuid       TEXT UNIQUE NOT NULL,
    name       TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS signals (
    id               INTEGER PRIMARY KEY AUTOINCREMENT,
    user_uuid        TEXT NOT NULL REFERENCES users(uuid),
    timestamp        INTEGER NOT NULL,
    lat              REAL DEFAULT NULL,
    lon              REAL DEFAULT NULL,
    accuracy         REAL DEFAULT NULL,
    created_at       TEXT DEFAULT (datetime('now')),
    telegram_batch_id INTEGER DEFAULT NULL
);

CREATE TABLE IF NOT EXISTS telegram_batches (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    message_text  TEXT DEFAULT NULL,
    sent_at       TEXT DEFAULT NULL,
    signals_count INTEGER DEFAULT 0,
    status        TEXT DEFAULT 'pending'
);

-- Индексы
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_uuid         ON users(uuid);
CREATE INDEX IF NOT EXISTS        idx_signals_user_uuid  ON signals(user_uuid);
CREATE INDEX IF NOT EXISTS        idx_signals_created_at ON signals(created_at);
CREATE INDEX IF NOT EXISTS        idx_batches_status     ON telegram_batches(status);

Инвариант: все три PRAGMA применяются при каждом подключении. Пропуск любой — нарушение решений #1005, #1002.


3. Telegram Aggregator Design

Принцип работы

[POST /signal] ──► add_signal(user_name, ts, geo)
                        │
                   [in-memory buffer]
                        │
             asyncio background task (run loop)
                        │
                   sleep(10 sec)
                        │
                   if buffer not empty:
                        │
                   ──► flush()
                        │
                   формат сообщения
                        │
                   send_message()

Буфер и окно

  • Хранение: список list[dict] в памяти, защищённый asyncio.Lock()
  • Интервал сброса: 10 секунд (скользящее окно)
  • Rate limit: максимум 20 сообщений/минуту в группу, 1 сообщение/секунду (#1014)
    • Реализация: asyncio.sleep(1) после каждого успешного send_message

Формат сообщения

🚨 Получено N сигналов [HH:MM:SS—HH:MM:SS]
Пользователи: name1, name2, name3
📍 С геолокацией: K из N

Если имя пользователя неизвестно — показывать первые 8 символов UUID.

Обработка ошибок Telegram

Код ответа Действие
200 OK Успех, обновить telegram_batch status = 'sent'
429 Too Many Requests Прочитать retry_after из тела ответа, await asyncio.sleep(retry_after), повторить
400 Bad Request Логировать ошибку, сигналы не теряются (остаются в SQLite)
5xx Логировать, повторить через 30 сек (1 попытка)

Запись batch в SQLite

После успешной отправки:

INSERT INTO telegram_batches (message_text, sent_at, signals_count, status)
VALUES (?, datetime('now'), ?, 'sent');

UPDATE signals SET telegram_batch_id = ? WHERE id IN (...);

4. Middleware & Security

CORS

CORSMiddleware(
    allow_origins=[config.FRONTEND_ORIGIN],  # env var FRONTEND_ORIGIN
    allow_methods=["POST"],
    allow_headers=["Content-Type"],
)

FRONTEND_ORIGIN обязателен в .env. Не использовать allow_origins=["*"] в продакшне.

Webhook Secret Validation

Middleware проверяет заголовок X-Telegram-Bot-Api-Secret-Token только для маршрута POST /api/webhook/telegram.

Входящий запрос на /api/webhook/telegram
  └── if header != config.WEBHOOK_SECRET → 403 Forbidden (немедленно)
  └── else → передать в обработчик

Реализация: Starlette BaseHTTPMiddleware или dependency в роуте (предпочтительно dependency — проще тестировать).

Input Validation

Все входящие тела запросов валидируются через Pydantic v2 models. FastAPI возвращает 422 автоматически при ошибке валидации.

Secrets Management

Все секреты исключительно через переменные окружения (.env + python-dotenv). Ни один секрет не должен логироваться (особенно WEBHOOK_SECRET и BOT_TOKEN).


5. Startup / Shutdown

Startup (lifespan event)

1. init_db()
   └── PRAGMA journal_mode=WAL
   └── PRAGMA busy_timeout=5000
   └── PRAGMA synchronous=NORMAL
   └── CREATE TABLE IF NOT EXISTS ... (users, signals, telegram_batches)
   └── CREATE INDEX IF NOT EXISTS ...

2. set_webhook(url=config.WEBHOOK_URL, secret=config.WEBHOOK_SECRET)
   └── POST https://api.telegram.org/bot{TOKEN}/setWebhook
   └── WEBHOOK_URL ОБЯЗАН быть HTTPS (#1011)
   └── Если ошибка → залогировать и завершить старт с исключением

3. aggregator = SignalAggregator(interval=10)
   asyncio.create_task(aggregator.run())

Shutdown (lifespan event)

1. aggregator.stop()           — установить флаг остановки
2. await aggregator.flush()    — отправить оставшиеся сигналы из буфера
3. Закрыть все aiosqlite соединения

6. Файловая структура

baton/
├── backend/
│   ├── main.py          # FastAPI app, роуты, lifespan events, CORS
│   ├── db.py            # SQLite init (WAL), CRUD: register_user, save_signal, get_user_name
│   ├── models.py        # Pydantic v2: RegisterRequest/Response, SignalRequest/Response, GeoData
│   ├── telegram.py      # send_message, set_webhook, SignalAggregator
│   ├── config.py        # Settings через python-dotenv: BOT_TOKEN, CHAT_ID, DB_PATH,
│   │                    #   WEBHOOK_SECRET, WEBHOOK_URL, FRONTEND_ORIGIN
│   └── middleware.py    # WebhookSecretValidator (или FastAPI dependency)
├── tests/
│   ├── conftest.py      # Фикстуры: in-memory SQLite, AsyncClient, respx mock
│   ├── test_register.py
│   ├── test_signal.py
│   ├── test_telegram.py
│   ├── test_webhook.py
│   └── test_db.py
├── docs/
│   ├── backend_spec.md  # Этот файл
│   └── backend_review.md  # Создаётся reviewer после имплементации
├── requirements.txt         # fastapi, uvicorn[standard], aiosqlite, httpx, python-dotenv, pydantic>=2.0
├── requirements-dev.txt     # pytest, pytest-asyncio, httpx, respx
└── .env.example

7. .env.example

# Telegram
BOT_TOKEN=your_telegram_bot_token_here
CHAT_ID=-1001234567890
WEBHOOK_SECRET=your_random_secret_here
WEBHOOK_URL=https://yourdomain.com/api/webhook/telegram

# Database
DB_PATH=baton.db

# CORS
FRONTEND_ORIGIN=https://yourdomain.com

8. Соответствие решениям

Решение Статус Реализация
#999: HTTPS для PWA/Geolocation WEBHOOK_URL обязан быть HTTPS; задокументировано в constraints
#1001: Background Sync fallback Не касается бэкенда; offline паттерн — на фронте
#1002: SQLite write-lock busy_timeout=5000 при каждом подключении
#1003: Требования не понижать Все 3 эндпоинта обязательны
#1004: bcrypt через run_in_executor UUID auth — без bcrypt; если добавится — только run_in_executor
#1005: WAL + busy_timeout + synchronous Все три PRAGMA вместе в init_db()
#1009: setWebhook vs getUpdates ТОЛЬКО setWebhook при старте; getUpdates запрещён
#1010: webhook secret validation Middleware/dependency на /api/webhook/telegram
#1011: HTTPS для webhook WEBHOOK_URL в .env.example — HTTPS
#1013: UUID v4 stateless auth register принимает UUID из localStorage
#1014: Telegram rate limit 20/min SignalAggregator: 10-сек окно, sleep(1) между отправками

9. Открытые вопросы (для Director/PM)

  1. CORS origin — какой домен фронтенда? (Определяется Infra-отделом)
  2. Количество uvicorn workers — один процесс (aggregator в памяти) vs несколько (aggregator должен быть shared via Redis/SQLite)
    • Рекомендация: один uvicorn worker в продакшне для MVP (aggregator в памяти корректен)
  3. Команды Telegram-бота — только /start для регистрации, или нужны другие команды?
  4. Telegram message format — показывать реальные имена или UUID?