# Backend Spec: Baton PWA **Версия:** 1.1 **Дата:** 2026-03-20 **Статус:** Approved (Architect) --- ## 1. API Contracts ### POST /api/register Регистрирует UUID→имя пользователя. **Идемпотентен**: повторный вызов с тем же UUID возвращает существующую запись. **Request:** ```http POST /api/register Content-Type: application/json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "Алиса" } ``` | Поле | Тип | Ограничения | |------|-----|-------------| | uuid | string | UUID v4, обязателен | | name | string | 1–100 символов, обязателен | **Response 200 OK:** ```json { "user_id": 42, "uuid": "550e8400-e29b-41d4-a716-446655440000" } ``` **Status codes:** | Код | Причина | |-----|---------| | 200 | Успешно (новый или существующий) | | 422 | Ошибка валидации Pydantic | | 500 | Внутренняя ошибка сервера | --- ### POST /api/signal Принимает сигнал тревоги от PWA. Сохраняет в SQLite, добавляет в очередь агрегатора Telegram. **Request:** ```http 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:** ```json { "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: ``` **Request body:** стандартный Telegram Update object (JSON от Telegram). **Обрабатываемые команды:** | Команда | Действие | |---------|----------| | `/start` | Регистрация пользователя через Telegram (INSERT OR IGNORE в users с Telegram user_id как UUID) | **Response:** ```json {"ok": true} ``` **Status codes:** | Код | Причина | |-----|---------| | 200 | Обновление обработано | | 403 | Неверный или отсутствующий X-Telegram-Bot-Api-Secret-Token | | 422 | Невалидный JSON | --- ## 2. DB Schema (SQLite WAL) ```sql -- 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 После успешной отправки: ```sql 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 ```python 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 ```env # 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?