Этот пост о библиотеке telegram-bot для написания ботов для Telegram. В числе основных целей при её создании были удобство разработки, отладки и тестирования ботов, сохранение интерфейсов минимальными, но с возможностью расширения, простота интеграции с Rails-приложением, и предоставление необходимых инструментов для написания бота. Вот что входит в состав:
Интересно? Для установки добавьте
Создать клиента просто:
Базовый метод клиента —
Из коробки клиент на каждый запрос будет возвращать обычный распрарсенный json. Можно воспользоваться гемом
Гем добавляет в модуль
Для Rails приложений можно обойтись без ручной настройки
Для обработки обновлений в геме есть базовый класс контроллера. Как и в
Контроллер умеет обрабатывать команды с упоминаниями. Если приходит такая, то имя из команды сравнивается с
Для обработки других обновлений (не сообщений) нужно также определить публичные методы с именем из названия типа обновления (сейчас их доступно 3: `message, inline_query, chosen_inline_result'). Эти методы получают в качестве аргумента соответствующий объект из обновления.
Для ответа на пришедшее уведомление есть хэлперы
Скорее всего боту понадобится запоминать состояние чата между сообщениями. Для этого в контроллере можно воспользоваться сессией. Интерфейс схож с интерфейсом сессии в ActionController, различие в способе хранения. В качестве адаптера можно использовать любое ActiveSupport::Cache-совместимое хранилище (
По-умолчанию в качестве ИД сессии используется такое значение (его можно изменить, переопределив метод):
Используя сессии, можно реализовать контекст сообщений — поддержка команд, пересылаемые в нескольких сообщениях: пользователь отправляет комманду без аргументов, бот уточняет, какие аргументы он ожидает, и пользователь отправляет их в следующем сообщении(-ях) (как это делает BotFather, например). Такой функционал доступен в модуле
Контроллер можно использовать в нескольких вариантах:
Для обработки хуков есть Rack-endpoint. Для Rails приложений есть хэлперы маршрутов: в качестве суффикса пути будет использован токен бота. При использовании единственного бота в приложении достаточно добавить:
Использование этого хэлпера позволяет выполнить
В геме есть
Есть хэлперы для тестирования контроллеров так же, как и ActionController:
Для локальной отладки можно запустить поллер обновлений. Для этого скорее всего понадобится создать отдельного бота.
Исходный код и более подробное описание доступны на github.
Приятной разработки!
- Легковесный клиент для 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.
Приятной разработки!
Cim
Оригинальный README.md выглядит как-то внушительнее даже.