# Telegram Hub: контекст проекта для AI

Этот файл предназначен для загрузки в AI-ассистент целиком. Он описывает назначение, архитектуру, публичные контракты и эксплуатацию Telegram Hub без секретов конкретной установки.

Актуальная версия приложения: `2.6.0`.

## Адрес текущего экземпляра

При скачивании этого файла Hub подставляет публичный адрес текущего экземпляра
вместо `https://hub.vitmp.ru`. Для запросов используйте:

```text
Base URL: https://hub.vitmp.ru
```

Полные URL основных endpoints:

```text
GET  https://hub.vitmp.ru/health
GET  https://hub.vitmp.ru/ready
GET  https://hub.vitmp.ru/channels
POST https://hub.vitmp.ru/send
POST https://hub.vitmp.ru/send/photo
POST https://hub.vitmp.ru/send/document
POST https://hub.vitmp.ru/send/media-group
POST https://hub.vitmp.ru/send/batch
POST https://hub.vitmp.ru/telegram/call
POST https://hub.vitmp.ru/telegram/edit-reply-markup
POST https://hub.vitmp.ru/telegram/answer-callback
POST https://hub.vitmp.ru/webhooks/telegram/{webhook_key}
GET  https://hub.vitmp.ru/admin/api/database/download
```

## Кратко о продукте

Telegram Hub является самостоятельным шлюзом между приложениями и Telegram Bot API. Один экземпляр Hub может работать с несколькими Telegram-ботами.

Сервис выполняет четыре основные задачи:

1. Принимает запросы приложений и отправляет сообщения через Telegram Bot API.
2. Создаёт маршрут для каждого бота и регистрирует публичный Telegram webhook.
3. Принимает Telegram update и пересылает исходный JSON в нужное приложение.
4. Сохраняет маршруты, события и попытки доставки в SQLite для администрирования и отладки.

```text
Приложение -> POST /send -> Telegram Hub -> Telegram Bot API
Приложение <- callback POST <- Telegram Hub <- Telegram webhook
```

## Главные требования к изменениям

- Обратная совместимость является обязательным требованием.
- Старые `/send` endpoints, PHP-клиент, `channels.json` и `HUB_BOT_TOKEN` должны продолжать работать.
- Запрос без callback-полей не должен получать новый блок `callback` в успешном ответе.
- Новый маршрут без явного `callback_delivery_mode` должен использовать `sync`.
- Существующий маршрут без переданного режима должен сохранять текущий режим.
- `bot_token` в запросе имеет приоритет над `channel`.
- SQLite schema обновляется добавлением совместимых колонок и индексов без удаления данных.
- API-маршруты должны возвращать JSON-ошибки даже при `Accept: text/html`.
- Нельзя выводить API key, admin password, bot token, Telegram secret или callback secret в публичный API, логи или AI-ответы.
- Ручной режим установки `--external-proxy` не должен изменять конфигурацию reverse proxy или сертификаты.
- Не изменять точные legacy payload и ответы без отдельного плана миграции и регрессионных тестов.

## Технологии

- Python 3.12.
- FastAPI и Uvicorn.
- httpx для Telegram и callback HTTP-запросов.
- SQLite с WAL для постоянных данных.
- Docker Compose для production.
- Caddy или Nginx + Certbot для HTTPS.
- Встроенная админ-панель без отдельного frontend build.
- DataTables, Chart.js и Lucide хранятся локально в `app/static/vendor`.

## Структура кода

| Путь | Ответственность |
|---|---|
| `app/main.py` | FastAPI app, middleware, lifecycle, API key, корневой редирект |
| `app/router/send.py` | Исходящие сообщения и произвольные Telegram Bot API методы |
| `app/router/batch.py` | Пакетная отправка до 100 сообщений |
| `app/router/webhooks.py` | Приём Telegram update и запуск доставки |
| `app/router/admin.py` | Авторизация, admin API, маршруты и события |
| `app/router/docs.py` | Документационный портал, Swagger, ReDoc и AI context |
| `app/services/telegram.py` | Telegram-клиенты, channels и proxy |
| `app/services/routes.py` | Создание маршрута и проверка Telegram webhook |
| `app/services/webhooks.py` | Callback HTTP-запрос, HMAC и запись попытки |
| `app/services/delivery.py` | Durable worker pool, lease, retry и backoff |
| `app/services/storage.py` | SQLite schema, миграции и запросы |
| `app/templates/` | Админка, вход, документация и страницы ошибок |
| `client/` | Совместимый PHP-клиент |
| `start.sh` | Идемпотентная установка и настройка HTTPS |
| `hubctl.sh` | Операторский CLI |
| `tests/` | Контрактные, регрессионные и storage-тесты |

## Авторизация

Исходящий API принимает один из заголовков:

```text
Authorization: Bearer <HUB_API_KEY>
X-API-Key: <HUB_API_KEY>
```

Публичные маршруты:

- `GET /` перенаправляет на `/admin/login`;
- `GET /health`;
- `/docs`, `/docs/*`, `/redoc` и `/openapi.json`;
- `POST /webhooks/telegram/{webhook_key}`, защищённый Telegram secret token;
- `/admin/login` и статические файлы панели.

Панель использует `HUB_ADMIN_USERNAME` и `HUB_ADMIN_PASSWORD`. Браузер получает HTTP-only cookie на 24 часа. Admin API также принимает HTTP Basic с теми же данными.

## Интеграция с Uptime Monitor

Uptime Monitor подключается к Hub через настройки:

```dotenv
TELEGRAM_TRANSPORT=hub
HUB_URL=https://hub.vitmp.ru
HUB_API_KEY=<secret from Hub admin>
HUB_CHANNEL=<bot route name>
HUB_CALLBACK_URL=https://uptime.example.com/hub/telegram
HUB_CALLBACK_SECRET=<same secret as Hub route forward_secret>
HUB_CALLBACK_DELIVERY_MODE=durable
```

Готовый блок для конкретной установки доступен только авторизованному администратору Hub:

```text
GET https://hub.vitmp.ru/admin/api/uptime-integration
```

Endpoint возвращает настоящий `HUB_API_KEY`, route callback URL и callback secret, но не раскрывает bot token. В UI это показано на `/admin/system` → «Подключение Uptime».

## Публичный HTTP API

| Метод | Путь | Назначение |
|---|---|---|
| `GET` | `/health` | Версия, состояние и uptime |
| `GET` | `/channels` | Доступные каналы без bot tokens |
| `POST` | `/send` | Текстовое сообщение |
| `POST` | `/send/photo` | Фото по URL или Telegram `file_id` |
| `POST` | `/send/document` | Документ |
| `POST` | `/send/media-group` | Альбом из 2-10 элементов |
| `POST` | `/send/batch` | До 100 сообщений, максимум 5 одновременно |
| `POST` | `/telegram/call` | Произвольный Telegram Bot API метод |
| `POST` | `/telegram/edit-reply-markup` | Изменение inline keyboard |
| `POST` | `/telegram/answer-callback` | Ответ на callback query |
| `POST` | `/webhooks/telegram/{key}` | Входящий Telegram webhook |

Точная исполняемая схема доступна в `/openapi.json`. Интерактивный интерфейс находится в `/docs/swagger`.

## Общие поля маршрутизации

Все endpoints отправки поддерживают:

| Поле | Значение |
|---|---|
| `channel` | Имя маршрута, по умолчанию `default` |
| `bot_token` | Прямой bot token, имеет приоритет над `channel` |
| `callback_url` | URL приложения для будущих Telegram update |
| `callback_secret` | Секрет HMAC callback-запроса |
| `callback_delivery_mode` | `sync` или `durable` |

Алиасы `webhook_url` и `webhook_secret` эквивалентны callback-полям.

Особый контракт `POST /telegram/call` с `method: "setWebhook"`: `params.url` является конечным callback URL приложения. Hub сохраняет его в маршруте, заменяет на собственный уникальный `/webhooks/telegram/{key}` и заменяет клиентский `secret_token` внутренним. URL приложения не передаётся Telegram напрямую. Остальные параметры `setWebhook` сохраняются. Пустой/некорректный URL и собственный входящий URL Hub отклоняются.

Пример первого запроса:

```json
{
  "bot_token": "123456:REPLACE_ME",
  "chat_id": "123456789",
  "text": "Новая заявка",
  "parse_mode": "HTML",
  "callback_url": "https://app.example.com/hooks/telegram",
  "callback_secret": "REPLACE_WITH_SHARED_SECRET",
  "callback_delivery_mode": "durable"
}
```

Если передан `callback_url`, Hub:

1. Определяет бота по `bot_token` или `channel`.
2. Создаёт или обновляет единственный SQLite-маршрут бота.
3. Генерирует закрытый `/webhooks/telegram/{key}` и Telegram secret token.
4. Вызывает Telegram `getWebhookInfo`.
5. Вызывает `setWebhook`, если адрес отсутствует или отличается.
6. Выполняет исходящий Telegram Bot API запрос.

Callback URL достаточно передать один раз. Следующие запросы могут использовать только сохранённый `channel` или тот же `bot_token`. Hub проверит Telegram webhook и восстановит его при необходимости.

## Формат успешного ответа

Совместимый запрос без callback-полей:

```json
{
  "ok": true,
  "channel": "default",
  "result": {
    "message_id": 17
  }
}
```

Запрос с callback дополнительно получает объект `callback`:

```json
{
  "ok": true,
  "channel": "default",
  "result": {
    "message_id": 17
  },
  "callback": {
    "bot_id": 1,
    "channel": "bot-123456",
    "callback_url": "https://app.example.com/hooks/telegram",
    "delivery_mode": "durable",
    "webhook_url": "https://hub.example.com/webhooks/telegram/opaque-key",
    "created": true,
    "callback_changed": true,
    "webhook_registered": true,
    "webhook_checked": true,
    "webhook_repaired": false
  }
}
```

## Входящий Telegram webhook

Telegram вызывает:

```text
POST /webhooks/telegram/{webhook_key}
X-Telegram-Bot-Api-Secret-Token: <generated-secret>
Content-Type: application/json
```

Hub проверяет маршрут, активность, secret, размер и JSON. Событие дедуплицируется по `bot_id + update_id`.

Callback-получатель получает исходный Telegram JSON и заголовки:

| Заголовок | Значение |
|---|---|
| `X-Hub-Bot` | Стабильное имя маршрута |
| `X-Hub-Event-Id` | ID события в SQLite |
| `X-Telegram-Update-Id` | Telegram `update_id`, если есть |
| `X-Hub-Signature` | `sha256=<HMAC>` от точных байтов JSON |

Получатель должен проверить HMAC через constant-time compare и быстро вернуть любой HTTP `2xx`.

## Режимы доставки

### `sync`

Значение по умолчанию и legacy-поведение. Hub вызывает callback в запросе Telegram webhook. При `2xx` возвращает Telegram `200`; при timeout, сетевой ошибке или не-`2xx` возвращает `502`, чтобы Telegram мог повторить update.

### `durable`

Hub сначала сохраняет событие в SQLite, затем сразу отвечает Telegram `200`. Фоновые воркеры доставляют callback через lease и exponential backoff. После лимита попыток событие получает статус `dead`.

Durable-режим обеспечивает `at least once`, а не `exactly once`. Получатель обязан быть идемпотентным по `X-Hub-Event-Id` или `X-Telegram-Update-Id`.

Состояния очереди:

```text
queued -> forwarding -> delivered
             |
             +-> retrying -> forwarding
                         |
                         +-> dead
```

Ручной retry доступен в панели и через `sudo hub event retry ID`.

## SQLite

Файл по умолчанию внутри контейнера: `/app/data/hub.db`. Он находится в Docker volume `hub_data`.

Основные таблицы:

| Таблица | Содержимое |
|---|---|
| `bots` | Bot token, имя, callback, secrets, режим и состояние webhook |
| `webhook_events` | Telegram payload, metadata, статус, lease и ошибка |
| `webhook_deliveries` | Каждая HTTP-попытка, код, длительность и ответ |
| `outbound_jobs` | Telegram API метод, параметры, lease, попытки, результат и ошибка |
| `outbound_attempts` | Каждая попытка исходящего вызова: код Telegram, длительность, result и полная ошибка |

SQLite использует WAL, foreign keys, busy timeout и совместимые миграции при старте. Backup выполняется через SQLite Online Backup API:

```bash
sudo hub backup
```

CLI сохраняет согласованные копии как `backups/hub-YYYYMMDD-HHMMSS.db`. Каталог исключён из Git и создаётся с правами `0700`, файлы — `0600`.

Авторизованный администратор также может скачать согласованный дамп из раздела «Система» через `GET /admin/api/database/download`.

## Админ-панель

- `/` ведёт на страницу авторизации.
- `/admin/login` выполняет вход.
- `/admin` открывает дашборд;
- `/admin/bots`, `/admin/events` и `/admin/system` открывают отдельные рабочие страницы.
- `/docs` открывает портал документации.

Панель состоит из отдельных серверных страниц: dashboard, маршруты ботов, единый DataTables-журнал webhook/Telegram и системная диагностика. Прямые адреса операций имеют вид `/admin/events/webhook/{id}` и `/admin/events/outbound/{id}`. Ручной retry начинает новый цикл лимита, сохраняя общую историю. API панели расположен под `/admin/api/*` и требует admin cookie или HTTP Basic.

## Исходящая Telegram-очередь

Все старые endpoints отправки сохраняют успешный JSON, но перед вызовом Telegram создают `outbound_jobs`. ID возвращается в `X-Hub-Outbound-Job-Id`. Временные ошибки получают `retrying`, постоянные Telegram `4xx` получают `failed`, исчерпанные временные ошибки получают `dead`. Состояние без bot token возвращает `GET /outbound/{id}`. Гарантия at-least-once допускает дубль после неопределённого сетевого результата.

## Мониторинг

- `/health`: HTTP-процесс и uptime.
- `/ready`: SQLite и оба пула воркеров, HTTP `503` при неготовности.
- `/metrics`: публичный Prometheus format без secrets и payload.
- Ключевые показатели: размеры и возраст очередей, ошибки и latency доставок, недоступные callback, configured/running workers.

Готовые правила: `docs/MONITORING.md`.

## Установка

Production с доменом:

```bash
curl -fsSL https://gitlab.com/valvic/hub/-/raw/main/install.sh \
  | sudo bash -s -- -d hub.example.com
```

Если активный Nginx уже слушает `80/443`, установщик создаёт отдельный управляемый virtual host и использует Certbot. В остальных случаях запускается Caddy.

Явный управляемый Nginx:

```bash
sudo bash install.sh \
  --domain hub.example.com \
  --nginx \
  --tls-email admin@example.com
```

Ручной внешний proxy:

```bash
sudo bash install.sh --domain hub.example.com --external-proxy --port 8080
```

В ручном режиме Hub слушает `127.0.0.1:8080`, а внешний proxy и сертификат остаются ответственностью оператора.

## CLI

```bash
hub -h
sudo hub ps
sudo hub health
sudo hub log 300
sudo hub cert
sudo hub creds
sudo hub doctor
sudo hub bot list
sudo hub event list --status failed
sudo hub event retry 42
sudo hub backup
sudo hub upgrade
```

`sudo hub upgrade` создаёт backup, разрешает только `git pull --ff-only`, пересобирает контейнеры, сохраняет credentials/SQLite/certificates и проверяет health/TLS. Старый `hubctl.sh` остаётся совместимой точкой входа.

## Основные переменные окружения

| Переменная | Назначение |
|---|---|
| `HUB_PUBLIC_URL` | Публичный HTTPS URL Hub |
| `HUB_API_KEY` | Bearer/X-API-Key для исходящего API |
| `HUB_ALLOW_INSECURE_NO_AUTH` | Legacy-разрешение пустого API key; false в новых установках |
| `HUB_ADMIN_USERNAME` | Логин панели |
| `HUB_ADMIN_PASSWORD` | Пароль панели |
| `HUB_DATABASE_PATH` | Путь к SQLite |
| `HUB_EVENT_RETENTION_DAYS` | Срок хранения событий, `0` отключает очистку |
| `HUB_FORWARD_TIMEOUT` | Timeout callback в секундах |
| `HUB_MAX_WEBHOOK_BYTES` | Максимальный Telegram webhook JSON |
| `HUB_DELIVERY_WORKERS` | Число durable workers, `0` приостанавливает очередь |
| `HUB_DELIVERY_MAX_ATTEMPTS` | Лимит автоматических попыток |
| `HUB_DELIVERY_RETRY_BASE_SECONDS` | Начальный backoff |
| `HUB_DELIVERY_RETRY_MAX_SECONDS` | Максимальный backoff |
| `HUB_OUTBOUND_WORKERS` | Число исходящих Telegram workers |
| `HUB_OUTBOUND_MAX_ATTEMPTS` | Лимит исходящих попыток |
| `HUB_OUTBOUND_RETRY_BASE_SECONDS` | Начальный исходящий backoff |
| `HUB_OUTBOUND_RETRY_MAX_SECONDS` | Максимальный исходящий backoff |
| `HUB_CHANNELS_PATH` | Legacy `channels.json` |
| `HUB_BOT_TOKEN` | Legacy default bot token |
| `HUB_PROXY_*` | Общий HTTP/SOCKS proxy к Telegram |
| `HUB_REVERSE_PROXY_MODE` | `direct`, `caddy`, `nginx` или `external` |

## Проверка изменений

Основная команда:

```bash
pytest
```

При изменении публичного API необходимо проверить точные legacy JSON-ответы и Telegram payload. При изменении storage необходимо проверить миграцию старой SQLite schema. При изменении установки необходимо проверить `bash -n install.sh start.sh hubctl.sh run.sh` и сохранение старых режимов запуска.

## Дополнительные источники

- `README.md`: обзор и быстрый старт.
- `docs/API.md`: HTTP API.
- `docs/WEBHOOKS.md`: маршруты и обратные webhook.
- `docs/DELIVERY.md`: durable queue.
- `docs/MONITORING.md`: Prometheus, readiness и alert rules.
- `docs/SDK.md`: Python и TypeScript clients.
- `docs/DEPLOYMENT.md`: установка, HTTPS и обновление.
- `docs/CLI.md`: операторские команды.
- `docs/ADMIN.md`: панель.
- `docs/COMPATIBILITY.md`: обязательные legacy-контракты.
- `/openapi.json`: точная схема запущенной версии.

## Рекомендуемый стартовый запрос к AI

```text
Используй приложенный AI_CONTEXT.md как описание Telegram Hub. Сначала сохрани все перечисленные правила обратной совместимости. Не придумывай отсутствующие endpoints или поля: при наличии репозитория сверяй решения с кодом и OpenAPI. Не включай реальные API keys, bot tokens, пароли или webhook secrets в ответ. Затем выполни следующую задачу: <описание задачи>.
```
