В предыдущей статье «Краткий обзор подсистемы команд в Symfony», мы рассмотрели возможности «команд» и на «синтетических примерах» рассказали об удобстве применения.
В этой статье покажем, как ещё могут быть использованы команды. Для живого примера возьмём задачу создания telegram-бота. Функционал бота будет простой, всего на пару команд: отреагировать на команду /start (Запустить), запросив код подтверждения, и обработать ввод кода подтверждения, проверив корректность формата и актуальность кода.
Пример сценария:
> (пользователь) /start (Запустить)
> (бот) Привет! Введите ваш код подтверждения в формате XXX-XXX.
> (пользователь) 123-0
> (бот) Неверный формат, введите код в формате XXX-XXX.
> (пользователь) 123-123
> (бот) Спасибо! Проверяю ваш код!
> (бот) Код верный!
Главная цель – показать, как используется Symfony Framework и его компоненты (подсистемы).
Этап № 1 – получаем API token для телеграм бота
Для этого нам надо зайти в чат с @BotFather и выполнить команду /newbot.
Затем ввести имя бота и его никнэйм (username).
После этого в сообщении получим HTTP API token вида
Этап № 2 – решаем, как хранить HTTP API token внутри Symfony
Если хотим в будущем менять токен или задавать его через интерфейс (веб-интерфейс или интерфейс командой строки), то лучше хранить токен в базе данных или в локальном файле (если ваш проект не является распределённой системой и представляет собой единственный экземпляр на сервере). Если токен будет очень редко меняться, и при этом за его изменение ответственным будет назначаться программист, и при этом проект не является распределённой системой, то хранить переменные окружения нужно в .env файле.
Для упрощения будем использовать .env и .env.local. Их различие в том, что в нашем случае .env будет попадать в git-репозиторий, а вот .env.local будет прописан в .gitignore и не будет попадать в git-репозиторий. Поэтому локально у нас может быть один токен, а когда выложим проект на сервер, в нём в .env.local пропишем другой токен.
Итого: в любом случае начинаем с хранения токена в .env.local, а дальше действуем по обстоятельствам, при этом в .env прописываем пустое значение.
Доставать токен в самом простом случае будем через ParameterBagInterface, для этого в config/services.yaml пропишем параметр:
Этап № 3 – создаём отправитель сообщений и проверяем отправку сообщений в Telegram.
Теперь, когда у нас есть токен, следует его проверить и отправить сообщение куда-либо, например себе в личные сообщения. Но как узнать свой chat_id?
В самом простом случае можно вместо chat_id указать свой username (никнэйм) и сообщение будет доставлено. Но если хотим всё унифицировать по integer chat_id, то пробуем узнать этот параметр из API телеграма через HTTP-запрос.
- Запускаем общение с ботом через кнопку «Запустить» и отправляем ему любое текстовое сообщение.
- Идём по HTTP API ссылке – можно через браузер, но я в примере буду использовать curl + jq и терминал.
И ищу своё сообщение (text: «моё сообщение»), в данном примере это будет слово «test».
Отсюда получаем нужный нам «chat.id», где полный json-путь до него это - «message.chat.id».
Этот chat.id будем использовать для тестирования, а в самом телеграме будем проверять получение сообщения и его формат.
Теперь идём в Symfony и создаём «сервис отправки сообщений в телеграм».
Создаём файл src/Service/TelegramSender.php с кодом:
Представленный код умеет логировать ошибки настройки проекта или ошибку аргумента метода , а также ошибки получения ответа != 200 ОК. Такое логирование полезно в режиме production – можно быстрее понять, где ошибка, и что она вообще есть. Исключительные и непредвиденные ошибки тоже будем отслеживать, но чуть ниже.
На данном этапе нужно убедиться только в возможности отправки сообщений.
Создаём код («Symfony команду»), в которой протестируем отправку сообщений и затем будем использовать эту команду для обработки запуска бота (/start).
Идём в терминал и вызываем команду:
bin/console app:telegram:command:start {message.chat.id}
Если сообщение отправляется, то необходимо «допилить код» так, чтобы при передаче «параметра эмуляции отправки сообщения» сообщение не уходило по-настоящему, а лишь выводило результат в терминал. Это поможет нам отлаживать и тестировать приложение на различных условиях без спама в настоящего бота.
Для этого допишем код так:
src/Service/TelegramSender.php – изменения отмечены красными точками:
И дорабатываем команду:
Теперь выполняем команду и проверяем что сообщение не отправляется в телеграм, а выводится в терминал.
bin/console app:telegram:command:start --simulate=1 {message.chat.id}
Мы должны увидеть одно жёлтое сообщение и строку из константы MESSAGE_START.
Если всё это происходит, то переходим к следующему этапу.
Этап № 4 – создаём и задаём webhook для обработки диалогов с телеграм ботом
Для начала надо зарегистрировать URL, на который телеграм будет «перенаправлять сообщения к боту» (или «сообщения чата в котором состоит бот») на этот URL.
Картинка для понимания:
Предположим, что наш URL будет такой /api/pub/telegram/webhook/
Значит вызываем метод:
https://api.telegram.org/bot{HTTP_API_TOKEN}/setWebhook?url=/api/pub/telegram/webhook/
Рекомендуется обратить внимание на параметры этого метода: https://core.telegram.org/bots/api#setwebhook
Там есть полезные параметры, такие как: secret_token (позволяет защитить обращения к URL секретным словом, которое будет приходить в заголовках запроса), а также max_connections (снижает нагрузку на сервер) и ip_address (защищает от подмены DNS).
Теперь в Symfony создаём маршрут (роут) /api/pub/telegram/webhook/ , который должен принимать POST-запросы.
В данном коде мы используем возможность Symfony вызывать команду из контроллера, передавая параметры, сформированные из HTTP-запроса. В данном примере параметры мы получаем из JSON объекта «Update»:
https://core.telegram.org/bots/api#update
С помощью десериализации JSON→PPO читаем параметры JSON. Если вам не нужна десериалзиация в PHP-объекты, то можно воспользоваться обычным json_decode. Вот так:
Минус такого подхода в том, что не получится провести валидацию каких-либо значений параметров запроса при десериализации (точнее получится, но это будет набор if else isset и т. д.).
На этом этапе наш бот сможет уже отвечать на команду /start , и при этом мы сможем тестировать наш код локально без взаимодействия с API телеграм.
Этап № 5 – обработка ввода и контекст общения
Теперь добавим команду обработки ввода кода, проверки его формата и проверки его актуальности. Для этого создадим Symfony-команду, чтобы её можно было вызывать вот так:
bin/console app:telegram:command:dialog {chat_id} {Текст_сообщения}
Команду назовём DialogCommand, потому что она будет реагировать на любой ввод пользователя. Для этого добавим обработку этой команды в контроллере-обработчике веб-хука /api/pub/telegram/webhook/.
Теперь любое сообщение попадает в app:telegram:command:dialog.
* Стоит уделить внимание тому, что сообщения в телеграме могут быть в виде изображений, тогда в getText() будет пусто. Т.к это лишь пример, то тут пропускается обработка медиа-сообщений и т.д.
Основной код DialogCommand:
На что тут следует обратить внимание?
- В switch идёт обработка «контекста диалога», то есть его состояния
- По умолчанию на любой ввод пользователя, если «контекст диалога» прошёл проверку кода, выводится сообщение «Реакция на сообщение {исходное_сообщение}»
- Появился TelegramDialogContext – это сущность (запись в таблице БД), которую мы создаём для chatId, если её нет, и, если есть, вытягиваем по chatId. Создаём мы её в состоянии STATE_START
В этой сущности можно хранить состояние диалога. Она должна быть уникальна на chatId. С помощью этой сущности (записи в БД) можно отслеживать и менять состояние диалога.
Итоги
С помощью представленного выше подхода создания ботов можно покрывать различные сценарии и различные мессенджеры, а также API внешних систем. Какие могут быть альтернативы?
В некоторых случаях, когда нужно набросать «MVP-версию бота» под разные мессенджеры (именно мессенджеры), то можно воспользоваться библиотекой «BotMan» ( https://botman.io/ ) . Но нужно понимать, что если у вас уже есть Symfony-проект, то интеграция библиотеки и работа с ней потребует знаний и времени на её изучение и построение архитектуры внутри вашего проекта конкретно под библиотеку BotMan.
Поэтому представленный подход через Symfony-команды имеет преимущества в быстром старте и понятном коде. Вам понадобится обладать знаниями только для Symfony Framework и API-нужного мессенджера, который, как правило, представлен в отдельных библиотеках. К тому же когда API обновляется или функции внутри мессенджера поменяются, то BotMan придёться форкать и дорабатывать отдельно.
P.S. Приходите к нам в NJ Soft за автоматизацией ваших проектов!