📜 ⬆️ ⬇️

Bot for Telegram. Rails way

This post is about a telegram-bot library for writing Telegram bots. Among its main objectives in its creation were ease of development, debugging and testing bots, keeping the interfaces minimal, but extensible, easy to integrate with a Rails application, and providing the necessary tools for writing a bot. Here is what is included:


Interesting? To install, add a telegram-bot to the Gemfile , details under the cat.

Client to bot-api


Creating a client is simple: Telegram::Bot::Client.new(token, username) . The username value is optional and is used for parsing commands with hits ( /cmd@BotName ) and in the session key prefix in the Key-Value store.

The base client method is request(path_suffix, body) , for all commands from the documentation there are shortcuts in the Ruby style - with underscores ( .send_message(body) , answer_inline_query(body) ). All these methods simply perform POST with the passed parameters to the desired URL. The files in the body will be automatically transferred with multipart/form-data , and the nested hashes are encoded in json, as documentation requires.
')
 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) 

Out of the box, the client will return the usual distributed json to each request. You can use the telegram-bot-types and get a virtus model at the output:

 #   Gemfile: gem 'telegram-bot-types', '~> xxx' #  typecasting   : Telegram::Bot::Client.typed_response! #    : bot.extend Telegram::Bot::Client::TypedResponse bot.get_me.class # => Telegram::Bot::Types::User 

Customization


Gem adds methods to the Telegram module to configure and access common clients for the application (they are thread-safe, there will be no problems with multiple threads):

 #   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 

For Rails applications, you can do without manual configuration of bots_config , the config will be read from secrets.yml :

 development: telegram: bots: chat: TOKEN_1 default: token: TOKEN_2 username: ChatBot #     bots.default bot: token: TOKEN username: SomeBot 

Controllers


To handle updates in geme there is a base controller class. As in ActionController , all public methods are used as action methods for processing commands. That is, if the message comes /cmd arg 1 2 , then the method cmd('arg', '1', '2') will be called (if it is defined and public). Unlike ActionController, if an unsupported command arrives, it is simply ignored, without ActionMissing errors.

The controller is able to handle commands with references. If such comes, then the name from the command is compared with the bot's username . In the case of a match, the command is executed, otherwise the message is processed as normal text.

To process other updates (not messages), you must also define public methods with the name from the name of the update type (there are 3 of them available now: `message, inline_query, chosen_inline_result '). These methods take as an argument the corresponding object from the update.

To respond to the incoming notification, there are helpers reply_with(type, params) and answer_inline_query(results, params) , which set the recipient and other fields from the incoming update.

 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 

Most likely the bot will need to remember the chat status between messages. To do this, the controller can use the session. The interface is similar to the session interface in ActionController, the difference in storage method. As an adapter, you can use any ActiveSupport :: Cache-compatible storage ( redis-activesupport , for example).

By default, this value is used as the session ID (it can be changed by overriding the method):

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

Using sessions, you can implement a message context — support for commands sent in several messages: the user sends a command without arguments, the bot specifies which arguments he expects, and the user sends them in the next message (s) (as does BotFather, for example). Such functionality is available in the 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 

Application integration


The controller can be used in several versions:

 #   : ControllerClass.dispatch(bot, update) #   ,  . controller = ControllerClass.new(bot, from: telegram_user, chat: telegram_chat) controller.process(:help, *args) 

There is a Rack-endpoint for handling hooks. Rails applications have route helpers: a bot token will be used as a path suffix. When using a single bot in an application, add:

 # routes.rb telegram_webhooks Telegram::WebhookController 

Using this helper allows you to setWebhook for bots, using the resulting URL, using the task:

 rake telegram:bot:set_webhook RAILS_ENV=production 

Testing


In the game there is a Telegram::Bot::ClientStub to replace the API clients in the tests. Instead of executing requests, it saves them in the #requests hash. To stop all created clients and not to send requests to Telegram during the execution of tests, you can write this:

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

There are helpers for testing controllers in the same way as 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 

Development and debugging


For local debugging, you can run an update poller. To do this, you will most likely need to create a separate bot. rake telegram:bot:poller will launch a poller. It will automatically download code updates when processing updates, there is no need to restart the process.

The source code and more detailed description are available on github .

Pleasant development!

Source: https://habr.com/ru/post/279179/


All Articles