Бот для Telegram. Rails way

от автора

Этот пост о библиотеке telegram-bot для написания ботов для Telegram. В числе основных целей при её создании были удобство разработки, отладки и тестирования ботов, сохранение интерфейсов минимальными, но с возможностью расширения, простота интеграции с Rails-приложением, и предоставление необходимых инструментов для написания бота. Вот что входит в состав:

  • Легковесный клиент для API ботов.
  • Базовый класс для контроллера обновлений с парсером сообщений. Сделан на основе AbstractController из ActionDispatch, предоставляет колбэки, сессии, сохранение контекста сообщений и прочее.
  • Rack-middleware для продакшена, чтобы принимать update-хуки, и поллер с автоматической загрузкой обновленного кода для удобной разработки.
  • Rake таски, хэлперы для рельсовых маршрутов и тестов.

Интересно? Для установки добавьте telegram-bot в Gemfile, подробности под катом.

Клиент к bot-API

Создать клиента просто: Telegram::Bot::Client.new(token, username). Значение username опционально и используется для парсинга команд с обращениями (/cmd@BotName) и в префиксе ключа сессии в Key-Value хранилище.

Базовый метод клиента — request(path_suffix, body), для всех команд из документации есть шорткаты в стиле Ruby — с подчеркиваниями (.send_message(body), answer_inline_query(body)). Все эти методы просто выполняют POST с переданными параметрами на нужный URL. Файлы в body будут автоматически переданы с multipart/form-data, а вложенные хэши закодированны в json, как требует документация.

bot.request(:getMe) or bot.get_me bot.request(:getupdates, offset: 1) or bot.get_updates(offset: 1) bot.send_message chat_id: chat_id, text: 'Test' bot.send_photo chat_id: chat_id, photo: File.open(photo_filename)

Из коробки клиент на каждый запрос будет возвращать обычный распрарсенный json. Можно воспользоваться гемом telegram-bot-types и получать на выходе virtus-модели:

# Добавьте в Gemfile: gem 'telegram-bot-types', '~> x.x.x' # Включите typecasting для всех ботов: Telegram::Bot::Client.typed_response! # или для отдельного клиента: bot.extend Telegram::Bot::Client::TypedResponse  bot.get_me.class # => Telegram::Bot::Types::User

Настройка

Гем добавляет в модуль Telegram методы для настройки и доступа к общим для приложения клиентам (они потокобезопасны, проблем с несколькими потоками не возникнет):

# Добавьте настройки Telegram.bots_config = {   # Можно указать только токен   default: 'bot_token',   # или вместе с username   chat: {     token: 'other_token',     username   } }  # Теперь боты будут доступны так: Telegram.bots[:chat].send_message(params) Telegram.bots[:default].send_message(params)  # Для :default бота есть шорткат (удобно, если он единственный): Telegram.bot.get_me

Для Rails приложений можно обойтись без ручной настройки bots_config, конфиг будет прочитан из secrets.yml:

development:   telegram:     bots:       chat: TOKEN_1       default:         token: TOKEN_2         username: ChatBot     # Это будет вмержено как bots.default     bot:       token: TOKEN       username: SomeBot

Контроллеры

Для обработки обновлений в геме есть базовый класс контроллера. Как и в ActionController, все публичные методы используются в качестве action-методов для обработки команд. То есть, если приходит сообщение /cmd arg 1 2, то будет вызван метод cmd('arg', '1', '2') (если он определён и публичный). В отличии от ActionController, если приходит неподдерживаемая команда, то она просто игнорируется, без ошибок ActionMissing.

Контроллер умеет обрабатывать команды с упоминаниями. Если приходит такая, то имя из команды сравнивается с username бота. В случае совпадения выполняется команда, иначе сообщение обрабатывается как обычное текстовое.

Для обработки других обновлений (не сообщений) нужно также определить публичные методы с именем из названия типа обновления (сейчас их доступно 3: `message, inline_query, chosen_inline_result’). Эти методы получают в качестве аргумента соответствующий объект из обновления.

Для ответа на пришедшее уведомление есть хэлперы reply_with(type, params) и answer_inline_query(results, params), которые выставляют получателя и другие поля из пришедшего обновления.

class TelegramWebhookController < Telegram::Bot::UpdatesController   def message(message)     reply_with text: "Echo: #{message['text']}"   end    def start(*)     # Есть хэлперы для chat и from:     reply_with text: "Hello #{from['username']}!" if from     # Доступ к самому сообщению можно получить через payload:     log { "Started at: #{payload['date']}" }   end    # При объявлении команд следует обязательно использовать splat-аргументы и   # значения по-умолчанию, потому что пользователи могут написать команду   # как с лишними параметрами, так и без них вообще.   def help(cmd = nil, *)     message =       if cmd         help_for_cmd?(cmd) ? t(".cmd.#{cmd}") : t('.no_help')       else         t('.help')       end     reply_with text: message   end end

Скорее всего боту понадобится запоминать состояние чата между сообщениями. Для этого в контроллере можно воспользоваться сессией. Интерфейс схож с интерфейсом сессии в ActionController, различие в способе хранения. В качестве адаптера можно использовать любое ActiveSupport::Cache-совместимое хранилище (redis-activesupport, например).

По-умолчанию в качестве ИД сессии используется такое значение (его можно изменить, переопределив метод):

def session_key   "#{bot.username}:#{from ? "from:#{from['id']}" : "chat:#{chat['id']}"}" end

Используя сессии, можно реализовать контекст сообщений — поддержка команд, пересылаемые в нескольких сообщениях: пользователь отправляет комманду без аргументов, бот уточняет, какие аргументы он ожидает, и пользователь отправляет их в следующем сообщении(-ях) (как это делает BotFather, например). Такой функционал доступен в модуле Telegram::Bot::UpdatesController::MessageContext:

class TelegramWebhookController < Telegram::Bot::UpdatesController   include Telegram::Bot::UpdatesController::MessageContext    def rename(*)     # Сохраним контекст для следующего сообщения:     save_context :rename     reply_with :message, text: 'What name do you like?'   end    # Зададим хэндлер для этого контекста:   context_handler :rename do |message|     update_name message[:text]     reply_with :message, text: 'Renamed!'   end    # Можно сделать по-другому. Определим rename, чтобы он мог обрабатывать команды   # с переданным аргументом.   def rename(name = nil, *)     if name       update_name name       reply_with :message, text: 'Renamed!'     else       # Если аргумент не указан, то сохраняем контекст:       save_context :rename       reply_with :message, text: 'What name do you like?'     end   end    # Без блока для обработки контекста будет использован тот же метод, что и название контекста.   # Экшн будет выполнен со всеми колбэками, точно так же, как если бы пришло   # сообщение '/rename %text%'   context_handler :rename    # Если таких контекстов много, можно использовать:   context_to_action!   # При этом для всех явно не заданных контекстов будет использован экшн по его названию. end

Интеграция в приложение

Контроллер можно использовать в нескольких вариантах:

# Для обработки обновления: ControllerClass.dispatch(bot, update)  # Вызвать экшн вручную, без обновления. controller = ControllerClass.new(bot, from: telegram_user, chat: telegram_chat) controller.process(:help, *args)

Для обработки хуков есть Rack-endpoint. Для Rails приложений есть хэлперы маршрутов: в качестве суффикса пути будет использован токен бота. При использовании единственного бота в приложении достаточно добавить:

# routes.rb telegram_webhooks Telegram::WebhookController

Использование этого хэлпера позволяет выполнить setWebhook для ботов, использую получившиеся URL, с помощью таски:

rake telegram:bot:set_webhook RAILS_ENV=production

Тестирование

В геме есть Telegram::Bot::ClientStub, чтобы заменить клиентов API в тестах. Вместо выполнения запросов он сохраняет их в хэше #requests. Чтобы застабить всех создаваемых клиентов и не отправлять запросы к Telegram во время выполнения тестов, можно написать так:

RSpec.configure do |config|   # ...   Telegram.reset_bots   Telegram::Bot::ClientStub.stub_all!   config.after { Telegram.bot.reset }   # ... end

Есть хэлперы для тестирования контроллеров так же, как и ActionController:

require 'telegram/bot/updates_controller/rspec_helpers'  RSpec.describe TelegramWebhookController do   include_context 'telegram/bot/updates_controller'    describe '#rename' do     subject { -> { dispatch_message "/rename #{new_name}" } }     let(:new_name) { 'new_name' }     it { should change { resource.reload.name }.to(new_name) }   end end

Разработка и отладка

Для локальной отладки можно запустить поллер обновлений. Для этого скорее всего понадобится создать отдельного бота. rake telegram:bot:poller запустит поллер. Он автоматически будет загружать обновления кода при обработке обновлений, нет необходимости перезапускать процесс.

Исходный код и более подробное описание доступны на github.

Приятной разработки!

ссылка на оригинал статьи https://habrahabr.ru/post/279179/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *