Бот для 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.

Приємною розробки!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.