bot better then phone header header image

Bot Dev: Write a stateful Telegram bot – Part 1

Why do we need all these bots? Why people are talking about them so much? What the bot is good for? Look around, it’s 2017 outside and nobody uses phones for calls only. Even more: today a lot of people prefer chatting to calling somebody.

But, you are still forced to make calls to get a taxi or to understand why your LAN connection is down (oh man, you again forgot to pay for it!). How often are you listening to the music, when you are on hold, instead of doing real business? That’s fine when it’s Bach, but what if it’s Justin Bieber instead? They are just wasting your time!

Chat is a better medium for a lot of people. But, yeah, you still will wait until there is a free person to respond you. Ok, finally somebody responded to you, but what if that person is in bad mood? So it still looks too unreliable and complicated. That is where bots come into play.

What We’ll Be Building

In this series of articles, we will be exploring the world of chatbots. In particular, we will build an app which allows end users to submit requests for fixing their stuff. We will call this application FixMyStuff and we are starting with implementing bot. For development, we will use npm, Node.js, Babel (as ES6 transpiler), MongoDB as data storage and some libs. This tutorial assumes at least some prior knowledge of Node.js and MongoDB. If you are new to them, you better go through some tutorial for beginner.

Application Requirements

As first steps we are going to have next:

  • Telegram bot

  • Bot’s backend part written in Node.js with MongoDB

  • End users are able to submit their fix requests

  • Users are able to check their requests and their status

Telegram Bot

Even though Telegram is not that popular as facebook messenger (especially in the USA and Western Europe) I’ve decided to start with it. Why? Just because I am using it.

What should we start with? Obviously, creating the bot and getting it’s token so we can rule it. This step is pretty easy:

  1. go to https://telegram.me/BotFather – telegram’s bot for creating bots

  2. type /newbot

  3. BotFather will guide you through the steps. Generally, it will look like this:

    create telegram bot

Bellow last message you will find your token. Let’s create our project dir.

The next step is to initialize our Node.js project. npm init and go through steps (usually default values are ok for us). Now let’s add some dependencies we need. That is how you package.json should look like (ok, you can change ‘author’ field to your name).

Currently, it’s enough to have there only one script – start. There we set environment mode and run nodemon with preprocessing our code with babel.

Let’s quickly go through dependencies:

  • lodash – contains some useful methods, like object merge.
  • mongoose – ORM for MongoDB
  • node-telegram-bot-api – library which wraps Telegram API into a nice set of methods
  • npmlog – lib for logging our messages. This will make our life easier
  • babel-* – we are going to write our project in ES6 and for this we will use transpiler (es6 is supported on 99% by latest nodejs versions, but we can be not aware of nodejs version we will run our app on in production, so we will keep  it safe)
  • nodemon – lib for restarting our project on code change

Now we should install dependencies:

For babel we need to enable presets. That is why we create .babelrc  file next to package json. This is its content:

Starting from babel 6, if you have no presets enabled, your code will be just parsed and generated again (unchanged). But we are writing in ECMAScript 2015 and want to transpile it to ES5. And yes, for using babel in our start script (where we use command line interface), we should install babel cli globally

App Setup

Here’s our file structure:

 

As you can see, we put Telegram specific logic into the separate folder. Thought out this article, you will see that I am willing to reach a higher level of abstraction. In case you wanna to reuse existing logic with another bot (and I will cover this in one of next articles), you should keep bot-related logic away from your business logic.

Now let’s get closer to our code. First, we will look into our main script:

There we establish a connection to MongoDB with mongoose, force mongoose to use bluebird promises and set log level for npmlog. Then we create an instance of our Bot and start it.

Before going to the logic of our bot, let’s check our configuration:

So there we have the configuration, which is common for all environments and several specific – development and production. At the end we merge common and current environment configurations. Telegram specific properties we will discuss later on. For now, we are interested in envs.development.mongo.uri. You should change it to match your local MongoDB instance (usually it’s the same).

Bot brains

We are ready to look into our bot’s brains:

As you can see, this file is not final. In method listen, we bind method handleText to this and set it as a callback on event ‘text’. And we return resolved Promise. Nothing interesting there so far.

The constructor looks more complicated. Let’s check what we have there.There are 2 available ways of getting data (in our case – messages) from Telegram server:

  1. webhook – you give your url to the Telegram server, and when a new message arrives – message will be sent to you
  2. polling – every n seconds you make a request to the server and it responds to you with available data.

With webhook your bot will respond to users much faster but it requires SSL certificate. Polling, on the other hand – is slower (because of intervals between requests). So in our case, we use webhook for the production environment and polling – for development,  because in last case we do not need it to respond fast and we are not forced to mess with  SSL certificate.

It’s time to write our first command:

There we just send a message to the user, we got this command from. We do not have any commands yet (yes, except help) so we tell user about this. sendMessage is one of the base methods in the library.

Take a look at handle method’s signature. It will be our contract for all command handlers – they should implement method handle(message, bot), which returns Promise.

Route me

Next, we need somehow to transport message from the messenger to our help command.

Here we create a dictionary with available commands. And method, where we remove ‘/’ from the command, getting handler from the dictionary and return it.

Now it’s a time to connect our messenger and Handler Router. First, we should wrap message object from telegram with our own object to keep external dependencies away from our logic.

Here we have one static method, where we split message on command itself and any other text which can go after it.

Let’s get back to our messenger. We should add there 2 more imports and implement method handleText.

In method handleText we wrap message, find requested command and handle the message with it.

Test it

Finally, we are ready to test our bot. Next command is used to run the project (make sure, that your MongoDB instance is up and running).

In you logs, you should see something similar to output bellow:

run bot

How about going to telegram to check if our bot works? Go to https://telegram.me/yourBotName. Type help and check output. I’ve got next:

first test of bot help command

If you got no answer – check your logs.

“Fix” Command

Now it’s time to create more useful command – /fix. With it the user will be able to submit his requests for fixing his broken things. First, we will try to send this command before we, actually, implement it. You will get nothing from the bot, but in logs you will see:

Oops. Let’s check what has happened. In our HandlerRouter we return value from the dictionary. Usually, it’s handler instance, but what If there is no such handler? In such case we get undefined and, as a result, error. We need to fix it.

We changed method getCommandHandler  to return default handler if there is no such command. We are using Help command handler for this.

Bot abstraction

In one of the previous sections, I was talking about abstractions. And we have applied this principle in several places. But we forgot to apply this principle for Bot itself. Let’s fix it. We start with our wrapper for bot object. Here’s what the code looks like.

Currently, we just call method from API of the library we used. But in future, when you will want to adapt this code for another library(which works with, e.g. Facebook Messenger Bot API), your commands will not be affected by this change (ok, probably will be, but not that much). The biggest thing you will need to do – is to create an object with another implementation of sendMessage method.

Also here are some changes in messenger:

Here we added import for our fixMyStuffBot.js, created wrapper and passed it to handleText method of our command handler.

“Fix” Command (attempt #2)

Finally, it’s time to get to really useful command – Fix Request. First, we should create model, which we will store in our MongoDB:

It has 2 fields – userId and request, both are strings. Code of the command handler:

Here we create model, with userId from the message and with request description. Then we save this model, and if promise from mongoose resolves (so it’s saved) – we send message back to the user. If error occurs – we log this, and send error message to the user. And yes, we return promise from handle method, in case we need to do some extra steps outside of the command. After implementing command, we need to add it to our handler router src/command/index.js:

  • new import import FixRequest from './fixRequest';
  • constructor

Also, we should update our help command:

Let’s run our application and test it:

telegram bot help command execute

And submit new request:

execute bot fix command

 

Additionally, we can check if request has been saved in our DB:

check bot state in mongodb

So everything looks good so far.

“My requests” command

After user submitted request, he, probably, will wanna to check it’s status. Let’s give him this feature. First, we should adapt our model.

Here we added status field, with enum type. Actually, it’s String type, but we provide the array of possible values, so it behaves like enum. Default value would be NEW.

Now go ahead to the command:

Let’s go through handle method:

  1. We search for all requests from user
  2. Then if there are any requests, we format them. Otherwise – we tell user, that we found no requests.
  3. We send message back to the user. In case of error, as usually, we log it and tell user to try later.

Method  formatRequests is quite simple: we add each user request and it’s status to the message body with several line separators.

Finally, we will test this handler.

test my fix requests command

We got our requests back (output, obviously, depends on submitted requests). That is what we expected. Command seems to be finished now. The last thing would be to update help command.

Conclusion

At this point, we’ve learned some basics of chatbots. Why they are good and how to write a simple bot. For now, we have only 2 commands. Standalone they are not really useful, but we will fix this in our next topic.

You can find the complete code in this GitHub repo.

 

P.S.: will be happy to get questions from you and see you soon.

P.P.S.: after this article has been finished and next article was started I came across another library for creating Telegram Bot with Node.js. It has pretty nice OOB routing and some other features. So you should give it a try as well.

 

3 thoughts on “Bot Dev: Write a stateful Telegram bot – Part 1

Your thoughts are welcome