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.

  1. 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.
  2. 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:

[ruby]`ngrok http 3000`[/ruby]

if everything is going ok you’ll see something like this
telegram

so, since we need an https, we will use this one

telegram

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.

telegram

Write him /newbot to start new bot creation.

telegram

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:
telegram

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

[ruby]post ‘/webhooks/telegram_vbc43edbf1614a075954dvd4bfab34l1’ => ‘webhooks#callback'[/ruby]

Your WebhooksController will now look like this:

[ruby]
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
[/ruby]

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:

  1. Born
  2. Accomplish your very first Rails tutorial
  3. 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

[ruby]
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
[/ruby]

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:

[ruby]
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
[/ruby]

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’.

[ruby]
gem ‘telegram-bot-ruby’
[/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:

[ruby]
development:
bot_token: bot220521163:AAHNZe-njGuJz_6-zwOyR1BKkhyJoGpNPyo
[/ruby]

We are ready to go!

Here is how your BotCommand class will look like:

[ruby]
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
[/ruby]

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.

[ruby]
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
[/ruby]

Next command we need to implement is BotCommand::Born class.

[ruby]
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
[/ruby]

Cool, we have been born, let’s try to accomplish our very first Rails tutorial.

[ruby]
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
[/ruby]

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.

[ruby]
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
[/ruby]

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.

How useful was this post?

Click on a star to rate it!

Average rating 4.8 / 5. Vote count: 14

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?


Author

Daria Stolyar is a Marketing Manager at Rubyroid Labs. You can follow her at Linkedin.

Write A Comment