OTP-доставка через мессенджеры

Спецификация доставки одноразовых кодов через Telegram, WhatsApp и другие мессенджеры.

Доставка OTP-кодов через мессенджеры (MAX / Telegram)

Проблема: SMS-коды не работают (провайдер недоступен / отключён). Решение: Отправлять OTP через мессенджеры MAX и Telegram с каскадным fallback. Статус: Проектирование.


1. Ключевое ограничение

Ни MAX API, ни Telegram Bot API не имеют метода поиска пользователя по номеру телефона. Отправить сообщение можно только зная user_id (MAX) или chat_id (Telegram). Эти идентификаторы становятся известны, когда пользователь сам запустил бота.

Единственный способ отправить код без предварительной привязки — Deep Link с параметром ?start=, который передаётся боту при первом запуске.


2. Принципиальная схема

┌─────────────────────────────────────────────────────────┐
                   Пользователь                           
   Вводит телефон  нажимает «Получить код в MAX» / TG   
└────────────────────────┬────────────────────────────────┘
                         
                         
┌─────────────────────────────────────────────────────────┐
                   App / Frontend                         
   Генерирует одноразовый сессионный токен:               
   AUTH_<32 random hex chars>                             
   Сохраняет: session[AUTH_XXX] = {phone, expires: 5min}  
                                                          
   Открывает deep link:                                   
   MAX: https://max.ru/<bot>?start=AUTH_XXX               
   TG:  https://t.me/<bot>?start=AUTH_XXX                 
└────────────────────────┬────────────────────────────────┘
                         
                         
┌─────────────────────────────────────────────────────────┐
                   Мессенджер                              
   Открывает чат с ботом                                   
   Пользователь нажимает Старт                             
└────────────────────────┬────────────────────────────────┘
                         
                         
┌─────────────────────────────────────────────────────────┐
                   Backend (Bot Webhook)                   
                                                          
   MAX: получает bot_started:                              
   { chat_id, user_id, payload: "AUTH_XXX" }              
                                                          
   TG:  получает /start AUTH_XXX                          
   { chat_id, text: "/start AUTH_XXX" }                   
                                                          
    Дёргает App API: POST /auth/link                     
     { token: "AUTH_XXX", messenger: "max", user_id }     
└────────────────────────┬────────────────────────────────┘
                         
                         
┌─────────────────────────────────────────────────────────┐
                   App API                                 
   Находит phone по AUTH_XXX                              
   Сохраняет связку: user_messenger_links[phone] = {      
     max: { user_id, linked_at },                         
     tg:  { chat_id, linked_at }                          
   }                                                      
   Отвечает боту: { status: "ok", otp: "1234" }           
└────────────────────────┬────────────────────────────────┘
                         
                         
┌─────────────────────────────────────────────────────────┐
                   Bot в чате                               
   Отправляет: «Ваш код: 1234»                             
   MAX: POST /messages?user_id=                            
   TG:  sendMessage(chat_id, "Ваш код: 1234")              
└────────────────────────┬────────────────────────────────┘
                         
                         
┌─────────────────────────────────────────────────────────┐
                   Пользователь                             
   Видит код в чате  вводит в приложении  вход           
└─────────────────────────────────────────────────────────┘

3. Каскадная логика (при повторных входах)

После первой авторизации через deep link связка phone → user_id/chat_id сохранена в БД. При повторном входе:

Пользователь вводит телефон
  ↓
Проверка user_messenger_links:
  ┌──────────────────────────────────────────┐
  │ Есть user_id в MAX?                      │
  │   → POST /messages?user_id= → OTP        │
  │   → «Код отправлен в MAX»                │
  ├──────────────────────────────────────────┤
  │ Есть chat_id в TG?                       │
  │   → sendMessage(chat_id) → OTP           │
  │   → «Код отправлен в Telegram»           │
  ├──────────────────────────────────────────┤
  │ Нет привязки                             │
  │   → Deep Link flow (см. схему выше)      │
  │   → Показываем кнопки MAX / TG           │
  └──────────────────────────────────────────┘

4. Сравнение каналов

Параметр MAX Telegram
География 🇷🇺 РФ Global
Deep Link https://max.ru/<bot>?start=<payload> (до 128 симв.) https://t.me/<bot>?start=<payload>
Событие при запуске bot_started с полем payload /start <payload> как текст сообщения
Отправка сообщения POST /messages?user_id= sendMessage(chat_id, text)
Rate limit 30 rps 30 msg/s global, 1 msg/s per chat
Лимит текста 4000 символов 4096 символов
Требуется организация Да (ИП/юрлицо/самозанятый, резиденты РФ) Нет
Модерация бота Да (может занимать дни) Нет
Макс. ботов 5 (юрлицо), 2 (самозанятый) Не ограничено
Получение событий Webhook (HTTPS, порт 443) Webhook или Long Polling
Запрос контакта request_contact кнопка + HMAC-верификация KeyboardButton с request_contact: true

5. Детальные требования

5.1 Сессионный токен

Поле Значение
Формат AUTH_ + 32 символа (hex)
TTL 5 минут
Хранение Redis / in-memory cache
Привязка token → { phone, messenger_choice, created_at }
Инвалидация После успешного получения OTP, по таймауту

MAX: https://max.ru/{bot_username}?start=AUTH_{token}

Telegram: https://t.me/{bot_username}?start=AUTH_{token}

Требования к token: - Только латиница, цифры, _, -, . - Не содержит чувствительных данных (phone, email, user_id)

5.3 Bot Webhook (MAX)

Подписка: POST /subscriptions с update_types: ["bot_started"]

Входящее событие:

{
  "update_type": "bot_started",
  "timestamp": 1748000000000,
  "chat_id": 1234567890,
  "user": {
    "user_id": 543210,
    "name": "Иван",
    "username": "ivan_petrov"
  },
  "payload": "AUTH_abc123def456..."
}

Обработчик webhook: 1. Извлечь payload (если null — ответить приветствием, не обрабатывать) 2. Извлечь user.user_id и chat_id 3. Вызвать POST /api/v1/auth/link вашего приложения: json { "token": "AUTH_abc123def456...", "messenger": "max", "user_id": 543210, "chat_id": 1234567890 } 4. Если App API вернул { status: "ok", otp: "1234" } — отправить сообщение: json POST /messages?user_id=543210 { "text": "Ваш код подтверждения: 1234\nНикому не сообщайте этот код.", "format": "markdown" } 5. Если App API вернул ошибку — отправить: «Срок действия ссылки истёк. Запросите код заново в приложении.»

5.4 Bot Webhook (Telegram)

Входящее сообщение:

{
  "update_id": 123456789,
  "message": {
    "message_id": 1,
    "chat": { "id": 987654321 },
    "text": "/start AUTH_abc123def456..."
  }
}

Обработчик: 1. Проверить, что текст начинается с /start AUTH_ 2. Извлечь token и chat_id 3. Вызвать POST /api/v1/auth/link: json { "token": "AUTH_abc123def456...", "messenger": "telegram", "chat_id": 987654321 } 4. Если App API вернул { status: "ok", otp: "1234" } — отправить: json sendMessage(987654321, { "text": "Ваш код подтверждения: 1234\nНикому не сообщайте этот код." }) 5. Если ошибка — ответ: «Срок действия ссылки истёк.»

5.5 App API — POST /api/v1/auth/request-otp

Вызывается фронтендом при нажатии кнопки «Получить код в MAX/TG».

Запрос:

{
  "phone": "+79001234567",
  "preferred_messenger": "max"    // "max" | "telegram"
}

Логика: 1. Проверить user_messenger_links для phone 2. Если привязка есть → отправить OTP напрямую через бота: - MAX: POST /messages?user_id=... - TG: sendMessage(chat_id, ...) 3. Если привязки нет → сгенерировать AUTH_token, сохранить в Redis: SET auth_session:AUTH_token '{phone, preferred_messenger}' EXPIRE auth_session:AUTH_token 300 4. Вернуть { deep_link: "https://max.ru/bot?start=AUTH_token" }

Ответ:

{
  "success": true,
  "deep_link": "https://max.ru/bot?start=AUTH_abc123..."
}

5.6 App API — POST /api/v1/auth/link

Внутренний эндпоинт, вызывается только ботом (защищён общим секретом).

Запрос:

{
  "token": "AUTH_abc123def...",
  "messenger": "max",
  "user_id": 543210,
  "chat_id": 1234567890
}

Логика: 1. Найти сессию по token в Redis 2. Если не найдена или истекла → { error: "token_expired" } 3. Извлечь phone из сессии 4. Сохранить/обновить user_messenger_links[phone]: sql UPSERT INTO user_messenger_links (phone, max_user_id, max_chat_id, linked_at) VALUES ('+79001234567', 543210, 1234567890, NOW()) 5. Сгенерировать OTP (6 цифр), сохранить: sql UPSERT INTO otp_codes (phone, code, expires_at) VALUES ('+79001234567', '123456', NOW() + INTERVAL 5 MINUTE) 6. Удалить сессионный токен из Redis 7. Вернуть { status: "ok", otp: "123456" }

5.7 App API — POST /api/v1/auth/verify-otp

Запрос:

{
  "phone": "+79001234567",
  "code": "123456"
}

Ответ:

{
  "success": true,
  "token": "jwt_or_session_token..."
}

6. Edge Cases & Обработка ошибок

Сценарий Поведение
Пользователь нажал «Получить код» но не перешёл в мессенджер Токен истечёт через 5 мин. Нажать кнопку ещё раз → новый токен
Пользователь перешёл, нажал Старт, но payload не пришёл Бот отвечает: «Для входа перейдите по ссылке из приложения»
Пользователь уже привязан → нажал MAX / TG Код приходит сразу, deep link не показывается
Бот остановлен / удалён пользователем bot_stopped event → удалить привязку
Ошибка отправки (бот заблокирован) fallback на второй канал, затем предложить перепривязать
MAX/TG API недоступен Время ожидания 5s → ошибка → предложить другой канал
Пользователь удалил приложение, не введя код Токен истекает, связь не сохраняется
payload > 128 символов (MAX) Бот не получит payload. Проверять длину при генерации

7. Затрагиваемые платформы

Изменения коснутся всех клиентских приложений — экран входа (ввод номера, кнопки выбора канала, ввод кода) и экран привязки мессенджера:

Платформа Что меняется
iOS (App Store) Экран входа: +2 кнопки MAX/TG + deep link, поле кода + привязка
Android (RuStore + HUAWEI AppGallery) То же самое
Web (полная версия) Экран входа + deep link (через window.open)
Web (мобильная версия) То же, адаптив под touch

Требования к реализации: - Deep link открывается системно: window.open для web, Linking.openURL / универсальные ссылки для мобильных - После возврата из мессенджера приложение само проверяет статус (polling GET /auth/status?token=AUTH_XXX каждые 3s, max 5 мин) - На Android важен fallback: если MAX не установлен — открыть Telegram; если и TG нет — показать QR


8. Оценка трудозатрат

Этап Зависимости
1 Регистрация организации на MAX, создание бота, модерация Документы организации
2 Создание Telegram-бота, webhook, деплой
3 Redis-сессия auth_session (генерация, TTL, инвалидация)
4 Postgres user_messenger_links (миграция, CRUD)
5 Postgres otp_codes (миграция, CRUD, TTL)
6 Эндпоинт POST /auth/request-otp + deep link generation №3, №4
7 Эндпоинт POST /auth/link (внутренний, с секретом) №4, №5
8 Эндпоинт POST /auth/verify-otp №5
9 MAX Bot: webhook-обработчик bot_started №7
10 MAX Bot: отправка OTP POST /messages №9
11 Telegram Bot: webhook-обработчик /start AUTH_XXX + отправка №7
12 iOS: экран входа (телефон, кнопки MAX/TG, поле кода) №6
13 Android: экран входа (то же самое) №6
14 Web: экран входа №6
15 Web mobile: экран входа (адаптив) №6
16 iOS: экран привязки мессенджера после входа
17 Android: экран привязки мессенджера после входа
18 Web: экран привязки мессенджера после входа
19 Rate limits, retry, таймауты, логирование

9. Схема данных

CREATE TABLE user_messenger_links (
  id            BIGSERIAL PRIMARY KEY,
  phone         VARCHAR(20) NOT NULL UNIQUE,
  max_user_id   BIGINT,        -- NULL, если не привязан
  max_chat_id   BIGINT,        -- NULL, если не привязан
  tg_chat_id    BIGINT,        -- NULL, если не привязан
  tg_username   VARCHAR(255),  -- для отображения
  linked_at     TIMESTAMP NOT NULL DEFAULT NOW(),
  updated_at    TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_phone ON user_messenger_links(phone);

otp_codes

CREATE TABLE otp_codes (
  id          BIGSERIAL PRIMARY KEY,
  phone       VARCHAR(20) NOT NULL,
  code        VARCHAR(6) NOT NULL,
  expires_at  TIMESTAMP NOT NULL,
  used_at     TIMESTAMP,
  created_at  TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_phone_expires ON otp_codes(phone, expires_at);

10. Deployment

Компонент Инфраструктура
MAX Bot Webhook HTTPS-endpoint, публично доступен, порт 443
Telegram Bot Webhook HTTPS-endpoint (может быть тот же)
App API Существующий backend
Redis Сессии auth_session, TTL 5 мин
Postgres Таблицы user_messenger_links, otp_codes

MAX Bot Deployment: 1. Зарегистрировать организацию/ИП на https://business.max.ru 2. Создать бота, пройти модерацию 3. Получить токен бота в Расширенные настройки 4. Настроить webhook: POST /subscriptions с update_types: ["bot_started"]

Telegram Bot Deployment: 1. Создать бота через @BotFather 2. Получить токен 3. Настроить webhook: setWebhook?url=https://your-domain.com/tg-webhook