На Хабре тысячи статей про то, как сделать Телеграм-бота под разные языки программирования и платформы. Тема далеко не новая. Но Telegraff – лучший фреймворк для реализации Телеграм ботов и я это под катом докажу. В 2015 году российский рубль лихорадило. У меня были сбережения в долларах и я буквально каждые пять минут проверял курс, чтобы продать валюту по нужному мне курсу. Лихорадка затянулась, я устал и написал Телеграм бота (@TinkoffRatesBot), который оповещает, в случае достижения курса валют порогового (ожидаемого) значения.
Меня очень тронула эта задача. Бота написал довольно быстро, но только вот удовлетворения не получил.  В интеграции с Телеграм нет и не было никаких проблем. Этот вопрос решается за пару часов. И я даже удивлён, что есть целые библиотеки на Java (субъективно, с отвратительным по качеству кодом) по интеграции с Телеграм, заработавшие больше тысячи звезд на Github. Основным вызовом для меня стала система сценариев: пользователь вызывает команду, например, "/taxi", бот задаёт ему ряд вопросов, каждый ответ валидируется и может влиять на порядок последующих вопросов, формируется привычная "форма", отдаётся в конечный метод на обработку для формирования ответа.
Я сделал это, но структура классов, уровни абстракции, все это было так неоднородно, что на это было горько смотреть. Меня мучал вопрос: Как это лаконично и органично перенести в объектно-ориентированную модель? Хотелось иметь что-то простое, удобное, а самое главное — иметь возможность описать весь сценарий в одном изолированном файле, чтобы не нужно было просматривать половину проекта, чтобы понять цепочку взаимодействия с пользователем.  Не сказать, что вопрос стоял очень остро, потому что задача была уже решена. Скорее, иногда я подумывал о нем. В мыслях был Groovy DSL, но когда появился Kotlin, выбор стал очевиден. Так появился Telegraff. Да, конечно, не было никакого соревнования, в котором бы победил Telegraff. И утверждения о том, что Telegraff – лучший, не нужно воспринимать буквально. Но Telegraff – новый, уникальный подход в рамках этой задачи. Легко быть лучшим, будучи единственным. Первым делом нужно указать дополнительный репозиторий для зависимостей. Возможно, на определенном этапе я опубликую Telegraff в Maven Central или в JCenter, но пока так. Осталось дело за малым. Для использования Telegraff нужно указать лишь одну зависимость spring-boot-starter: Конфигурация проекта проста и может ограничиваться первыми двумя-тремя параметрами: Пора писать сценарии! Обработчики (сценарии) – ключевая часть Telegraff. Именно здесь задается цепочка взаимодействия с пользователем. Суть в том, что каждая команда, вроде «/start», «/taxi», «/help» – это отдельный сценарий/скрипт/обработчик/handler.  Сценарий может содержать набор шагов (вопросов), необходимых пройти пользователю, для того, чтобы выполнить команду. Другими словами, пользователь должен заполнить форму. И так, как из интерфейса – мессенджер, надо с пользователем разговаривать, спрашивать.  Надо ли объяснять, что ответы пользователя нужно валидировать? Первым делом пользователь то и сделает, что ответит не так, как вы ожидаете.  Ну и в конце концов, сценарий может ветвиться, т.е. каждый ответ на вопрос может влиять на порядок последующих. К примеру! Для того, чтобы начать, положите файл с расширением .kts в папку c ресурсами handlers: src/main/resources/handlers/ExampleHandler.kts. Ключи степов нарочито небыли вынесены в константы. В продакшене, конечно, такого лучше избегать. Разберемся: Обработчик может и не иметь шагов вовсе. В таком случае, нужно определить лишь поведение обработчика на вызов команды. Для того, чтобы попробовать, форкаем репозиторий, клонируем на локальную машину и заходим в папку telegraff-sample. Конфигурируем, запускаем, трогаем! Вообще telegraff-sample – нарочито независимый проект, который не связан с родительским и имеет даже собственный Gradle Wrapper. Можно оставить только эту папку. Это своего рода архетип. Интеграция с Telegram очень проста и реализована в TelegramApi.  Каждый метод был нарочито реализован индивидуально в силу ряда обстоятельств: начиная от того, что используется спринговский RestTemplate (и тесты под него), заканчивая специфичностью API от Telegram. Как можно было заметить из конфигурации, в Telegraff существуют два типа клиентов этого API: PollingClient, WebhookClient. В зависимости от конфигурации, будет объявлен тот или иной бин. И хотя методы получения обновлений (новых сообщений) от Telegram отличаются, суть неизменна и сводится к одному – публикации события (TelegramUpdateEvent) о новом сообщений через спринговский EventPublisher (паттерн «Наблюдатель»). При желании можно реализовать собственного слушателя, подписавшись на этот тип событий. Логичный, как мне кажется, слой абстракции, ведь абсолютно не имеет значения каким именно образом было получено сообщение.  Как только было получено новое сообщение, требуется его обработать и ответить пользователю. Для этого сообщению нужно пройти через цепочку фильтров. Это похоже на привычные для Java программистов фильтры из Java EE. Разница лишь в том, что так называемые Обработчики (если проводить параллель с Java EE, то это Сервлеты) не являются независимыми от фильтров, а являются их частью.  Итак, фильтры упорядочены и могут пускать сообщения дальше по цепочке, могут нет. LoggingFilter, очевидно, самый приоритетный (первый) фильтр, который будет вызван в рамках процесса обработки нового сообщения. Залогирует информацию по входящему сообщению и отправит его дальше по цепочке. Я нарочно добавил LoggingFilter в пример. На деле же в нем смысла может не быть, т.к. входящие сообщения логируются на уровне клиентов. Следующий фильтр – CancelFilter. Он, по сути, работает в связке с HandlersFilter и является его дополнением. Его задача проста: если пользователь хочет отказаться от текущего сценария, он может написать «/cancel», либо «отмена» и его Состояние (сессия) должно быть очищено. Он может начать любой новый сценарий, не завершив предыдущий. По этой причине CancelFilter «выше» (приоритетнее) HandlersFilter. HandlersFilter – основной фильтр в текущем процессе. Именно этот фильтр хранит состояния пользовательских чатов, находит и вызывает нужный обработчик (сценарий), применяет блоки валидации, определяет порядок шагов и отвечает пользователю.  В случае, если HandlersFilter не нашел ни одного подходящего обработчика для пользовательского сообщения ни в сессии, ни по содержимому, сообщение отправляется дальше по цепочке. Крайним фильтром является UnresolvedFilter. Это фильтр, который знает, что он последний, поэтому его функциональность проста: если дошли до меня, то как отвечать на сообщение – непонятно, скажу, что ничего не понял. Как мне кажется, лучше хоть какие-то сообщения от бота получать, если он не знает как реагировать, чем ничего совсем не получать. Для того, чтобы добавить свой фильтр, нужно объявить Bean класса TelegramFilter и указать аннотацию @TelegramFilterOrder(ORDER_NUMBER). Именно таким образом в @TinkoffRatesBot реализован «калькулятор». Без вызова всякого сценария и команды можно отправить число, например, «1000», или даже целое выражение, например, «4500 * 3 — 12000». Бот посчитает результат выражения, к результату применит актуальные курсы валют и выведет об этом информацию. По сути же, результатом подобных действий является исполнение CalculationFilter, который находится в цепочке ниже HandlersFilter, но выше UnresolvedFilter. Система сценариев (обработчики) Telegraff построена на Kotlin DSL. Если очень вкратце, то это про лямбды и про билдеры.  Отдельно обозревать Kotlin DSL смысла не вижу, т.к. это совсем другой разговор. Есть замечательная документация от JetBrains и исчерпывающий доклад от i_osipov. Этот раздел посвящен присутствующим особенностям. Все они, на мой взгляд, не являются критичными, некоторые из них можно исправить, некоторые нет. Но об этих аспектах необходимо знать. Если у вас есть желание участвовать или знание того, как поправить тот или иной пункт из этого раздела, буду очень благодарен. Вероятно, слой интеграции с Telegram описан не полностью. Реализованы лишь те методы, которые мне были нужны. Если есть чего-то, чего вам лично не хватает, поправьте TelegramApi и засылайте PR! Из важных частей на текущий момент – отсутствие поддержки inline-клавиатуры (это когда клавиатура прям под сообщением в ленте). Задача отягощена тем, что inline-клавиатуры нужно правильно «вписать» в действующую структуру так, чтобы это оставалось просто, удобно, изолированно. Уже есть хорошая идея по реализации этого функционала, но она еще ни в каком виде не реализована и не опробована. К сожалению, некоторые библиотеки, такие как JRuby и, вероятно, Kotlin Embedded Compiler (нужный для компиляции сценариев) могут иметь проблемы, являясь частью Fat JAR. Fat JAR – это когда ваш код и все ваши зависимости упаковываются в один файл (*.jar). Для того, чтобы эту проблему решить, можно распаковывать зависимости в runtime. То есть, когда приложение запускается, JARка зависимости из основного пакета разворачивается где-нибудь на диске и до нее указывается classpath. Сделать это довольно просто через конфигурацию bootJar: Однако, для того, чтобы ссылаться из обработчиков (скриптов) к вашим бинам (сервисам, например), они тоже должны быть распакованы. Что, в принципе, нивелирует пользу такого подхода. Как мне видится, наиболее надежным, простым и удобным методом остается использование Gradle плагина application. Более того, если вы контейнеризуете ваше приложение, то по итогу разницы нет. Обо всем этом я довольно детально писал здесь. Здесь хотелось бы отметить два обстоятельства. Во-первых, если обратить внимание на сценарий вызова такси, можно увидеть, что enum класс определен выше вызова handler(...). Эта необходимость навязана тем, что по факту, handler – вызов функции. Вызов функции, результатом которого должна быть некоторая структура, которую в последующем будет использовать Telegraff. Если по итогу исполнения вашего скрипта фабрика не сможет привести результат к нужному типу, вывалится ошибка на этапе инициализации.  Во-вторых, нужно помнить, что ваши скрипты могут быть проинициализированы раньше, чем все ваше приложение и бины. Если положить, например, ссылку на контекст в статическую переменную и первой же строкой в файле скрипта попытаться достать какой-нибудь сервис, может статься, что его не будет у контекста, т.к. он еще не был проинициализирован. Для того, чтобы на такие проблемы не натыкаться, воспользуйтесь этим методом Telegraff. Он гарантирует, что контекст будет проинициализирован и все нужные бины будут доступны. Пример можно посмотреть здесь. Захотелось попробовать — форкай,
Захотелось поправить — отправляй PR,
Захотелось отблагодарить — поставь звездочку в Github, лайкни пост и расскажи друзьям! Репозиторий проекта Программист