Этот пост о библиотеке 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/
Добавить комментарий