Перейти к содержанию

P2-013 Telegram Bot интеграция

Метаданные

Поле Значение
Фаза Phase 2: Core
Статус done
Приоритет critical
Связь с roadmap Roadmap - Уведомления

Контекст

Бизнес-контекст

Telegram — единственный канал уведомлений в AqStream. Бот отправляет билеты, напоминания, уведомления об изменениях. Также через бот происходит авторизация и привязка аккаунта.

Технический контекст

  • Notification Service управляет ботом
  • Bot API для отправки сообщений
  • Webhook или long polling для получения команд
  • Deeplinks для приглашений и авторизации

Связанные документы: - Notification Service - Functional Requirements FR-10 - User Service — Telegram auth

Цель

Реализовать Telegram бот для отправки уведомлений, привязки аккаунтов и обработки базовых команд.

Definition of Ready (DoR)

  • [x] Контекст понятен и описан
  • [x] Цель сформулирована
  • [x] Acceptance Criteria определены
  • [x] Технические детали проработаны
  • [x] Зависимости определены и разрешены
  • [x] Нет блокеров

Acceptance Criteria

Создание бота и конфигурация

  • [ ] Бот создан через @BotFather (требуется ручное действие)
  • [x] Настроены commands: /start, /help
  • [x] Webhook или long polling для получения updates (с конфигурируемым timeout)
  • [x] Bot Token безопасно хранится в env variables
  • [x] Документация по получению токена от @BotFather обновлена в docs/operations/environments.md
  • [x] Тестовый bot token настроен для integration тестов (application-test.yaml)
  • [x] Integration тесты для POST /api/v1/auth/telegram написаны (из P2-002)

Команда /start

  • [x] При /start без параметров — welcome сообщение
  • [x] При /start invite_{code} — присоединение к организации
  • [x] При /start link_{token} — привязка Telegram к существующему email-аккаунту
  • [x] При /start reg_{id} — просмотр билета регистрации
  • [ ] При первом входе через Telegram Login Widget — автоматическая привязка chat_id

Привязка аккаунта

  • [ ] После авторизации через Telegram Login Widget — chat_id сохраняется
  • [x] Пользователь с email-аккаунтом может привязать Telegram через /start link_{token}
  • [ ] Генерация link_token через веб-интерфейс (в настройках профиля) — требует frontend
  • [x] Token действителен 15 минут

Отправка сообщений

  • [x] Отправка текстовых сообщений (Markdown)
  • [x] Отправка изображений (билет с QR)
  • [x] Inline кнопки для быстрых действий
  • [x] Обработка ошибок (blocked bot, chat not found)
  • [x] Retry механизм при временных ошибках

Команда /help

  • [x] Показывает доступные команды
  • [x] Ссылка на FAQ или поддержку

Definition of Done (DoD)

  • [x] Все backend Acceptance Criteria выполнены (Frontend AC — отдельная задача)
  • [x] Код написан согласно code style проекта
  • [x] Unit тесты написаны (включая DeeplinkHandlerTest)
  • [x] Integration тесты (mock Telegram API через @MockitoBean)
  • [x] Документация конфигурации
  • [x] Code review пройден
  • [x] CI/CD pipeline проходит

Технические детали

Затрагиваемые компоненты

  • [x] Backend: notification-service (TelegramBotService)
  • [ ] Frontend: кнопка «Привязать Telegram» в настройках
  • [x] Database: telegram_chat_id в users, telegram_link_tokens
  • [x] Infrastructure: webhook endpoint, env variables

Telegram Bot Library

// java-telegram-bot-api (версия в gradle.properties)
implementation 'com.github.pengrad:java-telegram-bot-api:7.11.0'

Bot Service

@Service
@RequiredArgsConstructor
public class TelegramBotService {

    private final TelegramBot bot;
    private final UserClient userClient;

    @PostConstruct
    public void init() {
        // Webhook или Long Polling
        bot.setUpdatesListener(updates -> {
            updates.forEach(this::handleUpdate);
            return UpdatesListener.CONFIRMED_UPDATES_ALL;
        });
    }

    private void handleUpdate(Update update) {
        if (update.message() != null && update.message().text() != null) {
            String text = update.message().text();
            Long chatId = update.message().chat().id();

            if (text.startsWith("/start")) {
                handleStart(chatId, text);
            } else if (text.equals("/help")) {
                handleHelp(chatId);
            }
        }
    }

    public void sendMessage(Long chatId, String text) {
        SendMessage request = new SendMessage(chatId, text)
            .parseMode(ParseMode.Markdown);
        bot.execute(request);
    }

    public void sendPhoto(Long chatId, byte[] photo, String caption) {
        SendPhoto request = new SendPhoto(chatId, photo)
            .caption(caption)
            .parseMode(ParseMode.Markdown);
        bot.execute(request);
    }
}
/start invite_{inviteCode}     — приглашение в организацию
/start link_{linkToken}        — привязка email-аккаунта к Telegram
/start reg_{registrationId}    — просмотр регистрации

Configuration

telegram:
  bot-token: ${TELEGRAM_BOT_TOKEN}
  bot-username: ${TELEGRAM_BOT_USERNAME:AqStreamBot}
  webhook-url: ${TELEGRAM_WEBHOOK_URL:}  # Если пусто — long polling

Error Handling

public void sendMessageSafe(Long chatId, String text) {
    try {
        SendResponse response = bot.execute(new SendMessage(chatId, text));
        if (!response.isOk()) {
            if (response.errorCode() == 403) {
                // Пользователь заблокировал бота
                log.warn("Пользователь заблокировал бота: chatId={}", chatId);
                userClient.clearTelegramChatId(chatId);
            } else {
                log.error("Ошибка отправки: code={}, desc={}",
                    response.errorCode(), response.description());
            }
        }
    } catch (Exception e) {
        log.error("Ошибка Telegram API: chatId={}, error={}", chatId, e.getMessage());
    }
}

Зависимости

Блокирует

  • P2-014 Шаблоны уведомлений
  • P2-002 Telegram авторизация

Зависит от

Out of Scope

  • Интерактивные команды (поиск событий, регистрация через бот)
  • Групповые чаты
  • Inline mode
  • Payments через бот

Заметки

  • Для локальной разработки использовать long polling
  • Для production — webhook (требует HTTPS)
  • ngrok можно использовать для тестирования webhook локально
  • Bot username должен заканчиваться на 'bot' или 'Bot'
  • Rate limits: не более 30 сообщений в секунду на бота