Telegram bots are becoming the next big thing. With Pavel Durov’s promise to give away $1 million to the best Telegram bot developers, even those who were not very much interested in this topic, got some appetite. Luckily, among Rubyroid Labs Team we also have some developers interested in this topic.
We have asked our Team member Maxim Abramchuk to shade some light and make a guide on how to create a Telegram Bot that stores state. You could have already seen Maxim’s article about code style.
Let’s have a look at Maxim decided to share with us. As he says, he has a couple of reasons to write this article.
- At first, he has a repository called ruby-telegram-bot-starter-kit, which contains a boilerplate for creating simple Telegram bots. It’s pretty cool, it allows you to save data to database, translate your messages via i18n, use custom keyboards and etc., but as he realised later he did miss a couple of things that are pretty important.
He has been asked a lot via Github issues about the right/possible way to store state of a Telegram bot vs user communication. - The technic of communicating with user he has used in this repo was a plain old long-polling, which sucks, because actually Telegram has Webhook API which is a much more convenient way to setup user vs bot communication.
So, today we are going to write a little quest game bot in Telegram using Ruby on Rails, Webhook API and some architecture which will help you to store your user-bot state like a charm. Let’s imagine that you already have a Rails app created and Rails server started on localhost:3000.
Webhooks API is working with HTTPS resources only, so we need to use ngrok to proxy our localhost:3000 with some server accessible via HTTPS. So, let’s download ngrok (or install it via brew on OS X) and do a:
1 |
`ngrok http 3000` |
if everything is going ok you’ll see something like this
so, since we need an https, we will use this one
Our next step is to create a bot in Telegram and get it’s token
To do that you need to find a Botfather in Telegram.
Write him /newbot to start new bot creation.
Cool, now we have a rails server running and a Telegram bot available to communicate with our users.
Our next step is to setup a communication with our user via webhooks API. Let’s do that.
We need to visit URL, which can be constructed like this one
[plain]https://api.telegram.org/bot<telegram_bot_token_from_botfather>/setWebhook?url=<your_https_ngrok_url>/webhooks/telegram_<place_some_big_random_token_here>[/plain]
so, in my case it will be something like this
[plain]https://api.telegram.org/bot220521163:AAHNZe-njGuJz_6-zwOyR1BKkhyJoGpNPyo/setWebhook?url=https://bb157435.ngrok.io/webhooks/telegram_vbc43edbf1614a075954dvd4bfab34l1[/plain]
We need to place a random string at the end to make sure that nobody will guess it and get access to bot-vs-user communication.
If you’ve done everything ok, when visiting this URL you’ll see a success message which looks something like this:
So, this message means that all the message users send to your bot will go through your Rails application’s WebhooksController, which we are going to implement in a next step.
Open existing or create a new webhooks_controller.rb file and implement a method called telegram_vbc43edbf1614a075954dvd4bfab34l1
do not forget to add mapping to the routes.rb
1 |
post ‘/webhooks/telegram_vbc43edbf1614a075954dvd4bfab34l1’ => 'webhooks#callback' |
Your WebhooksController will now look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class WebhooksController < ApplicationController def telegram_vbc43edbf1614a075954dvd4bfab34l1 dispatcher.new(webhook, user).process render nothing: true, head: :ok end def webhook params['webhook'] end def dispatcher BotMessageDispatcher end def from webhook[:message][:from] end def user @user ||= (::User.find_by(telegram_id: from[:id]) || register_user) end def register_user @user = User.find_or_initialize_by(telegram_id: from[:id]) @user.update_attributes!(first_name: from[:first_name], last_name: from[:last_name]) end end |
So, right now we have a controller action for receving incoming messages. Next step is to implement a plain Ruby class that will differentiate our messages and answer them correctly accordingly to the step user is now on. So, for these purposes we need to create a class called BotMessageDispatcher which will have a list of commands available for user.
Since we are writing a very simple quest game we need to define a command for each particular step of our game. One more thing we need to do to make our game work is to remember on which step is user now and not to jump across and between independent steps.
So, let’s decide what we will do in our quest game. I decided to go with ‘Becoming a Rails rockstar’ game which will have 3 simple steps:
- Born
- Accomplish your very first Rails tutorial
- Write a blog in 15 minutes
You are a real Rails rockstar!
So, to you can not accomplish any Rails tutorials without been born and also you can not write blog in 15 minutes without accomplishing any Rails tutorials. So, we need to take care about this and do not allow user to jump to step 2 without accomplishing step 1 and etc.
There where state storing is coming. We gonna create some methods for our User model to get current step, get next step, save current step and etc.
To do that let’s add jsonb field called ‘bot_command_data’ to our user model
Not to make this article really long here is the final implementation of these helper methods for User model
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class User < ActiveRecord::Base def set_next_bot_command(command) self.bot_command_data['command'] = command save end def get_next_bot_command bot_command_data['command'] end def set_next_bot_command_method(method) self.bot_command_data['method'] = method save end def get_next_bot_command_method bot_command_data['method'] end def set_next_bot_command_data(data) self.bot_command_data['data'] = data save end def get_next_bot_command_data bot_command_data['data'] end def reset_next_bot_command self.bot_command_data = {} save end end |
So, as you can see we implemented some very useful helpers which will help us to store and retrieve user state from database. Here you can see a word ‘bot_command’, so, we will build our user-bot communication with abstractions represented in a plain Ruby class called BotCommand.
We will create a base class with basic functionality and a class inherited from it for each command we need.
So, at first let’s implement our BotMessageDispatcher that will know which particular command to apply at any moment of bot-user communication.
Here it is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class BotMessageDispatcher attr_reader :message, :user AVAILABLE_COMMANDS = [ BotCommand::Start, BotCommand::Born, BotCommand::AccomplishTutorial, BotCommand::WriteBlog ] def initialize(message, user) @message = message @user = user end def process if command = AVAILABLE_COMMANDS.find { |command_class| command_class.new(@user, @message).should_start? } command.new(@user, @message).start elsif @user.get_next_bot_command bot_command = @user.get_next_bot_command.safe_constantize bot_command.new(@user, @message).public_send @user.get_next_bot_command_method else BotCommand::Undefined.new(@user, @message).start end end end |
Here you can see a constant array of all the commands we need and a method called ‘process’ which will figure out which command is user able to process now, save it, and then prepare user for processing next command.
So, we have a mechanism for dispatching messages, storing state, figuring out which command at each moment of user-bot communication. So, the last step of our implementation is to implement BotCommand class and separate class for each command. Let’s do it.
Our BotCommand class will actually send messages to users, so we need to install a Ruby gem for communication with Telegram to do that.
Just add this line to your Gemfile and do ‘bundle install’.
1 |
gem 'telegram-bot-ruby' |
One more thing we need to send messages is our Telegram bot token, so let’s add it to the secrets.yml file in a key called ‘bot_token’.
For me it will be:
1 2 |
development: bot_token: bot220521163:AAHNZe-njGuJz_6-zwOyR1BKkhyJoGpNPyo |
We are ready to go!
Here is how your BotCommand class will look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
require 'telegram/bot' module BotCommand class Base attr_reader :user, :message, :api def initialize(user, message) @user = user @message = message token = Rails.application.secrets.bot_token @api = ::Telegram::Bot::Api.new(token) end def should_start? raise NotImplementedError end def start raise NotImplementedError end protected def send_message(text, options={}) @api.call('sendMessage', chat_id: @user.telegram_id, text: text) end def text @message[:message][:text] end def from @message[:message][:from] end end end |
Here it is. We have a base class for our abstract BotCommand. Here we have a method for sending messages and 2 methods which need to be implemented in all the other command classes which will be inherited from the Base class.
So, let’s do that. The very first command is called Start and used to trigger Telegram bot to start conversation with our user.
1 2 3 4 5 6 7 8 9 10 11 12 |
module BotCommand class Start < Base def should_start? text =~ /\A\/start/ end def start send_message(‘Hello! Here is a simple quest game! Type /born to start your interesting journey to the Rails rockstar position!') user.set_next_bot_command('BotCommand::Born') end end end |
Next command we need to implement is BotCommand::Born class.
1 2 3 4 5 6 7 8 9 10 11 12 |
module BotCommand class Born < Base def should_start? text =~ /\A\/born/ end def start send_message(‘You have been just born! It’s time to learn some programming stuff. Type /accomplish_tutorial to start learning Rails from simple tutorial!') user.set_next_bot_command('BotCommand::AccomplishTutorial') end end end |
Cool, we have been born, let’s try to accomplish our very first Rails tutorial.
1 2 3 4 5 6 7 8 9 10 11 12 |
module BotCommand class AccomplishTutorial < Base def should_start? text =~ /\A\/accomplish_tutorial/ end def start send_message(‘It was hard, but it’s over! Models, controllers, views, wow, a lot stuff! Let’s practice now. What do you think about writing a Rails blog? Type /write_blog to continue.') user.set_next_bot_command('BotCommand::WriteBlog') end end end |
Tutorial accomplished, your user knows a lot of stuff about Rails already. Let’s give him ability to write a blog and become a Rails rockstar.
1 2 3 4 5 6 7 8 9 10 11 12 |
module BotCommand class WriteBlog < Base def should_start? text =~ /\A\/write_blog/ end def start send_message(‘Hmm, looks cool! Seems like you really know Rails! A real rockstar!') user.reset_next_bot_command end end end |
Looks like we’ve done it. As you can see, our architecture is pretty simple and very flexible, so you are welcome to change it according to your needs.
Good luck in creating useful bots!
Rubyroid Labs is an advanced mobile development company. Learn more about our services.