The Node js Course | PROJECT: TELEGRAM BOT

INTRODUCTION TO THE PROJECT

The goal of this project is to create a Telegram Bot.

If you are not familiar with Telegram, it’s one of the biggest chat networks.

What is a Telegram bot? It’s a program that listens for commands and can interact with you.

Our bot will be called @botranslate .

Its job is to translate every message we send to it .

By default it will try to interpret the origin language, and will translate it t‘o english.

However you can perform some operations through special commands:

  • /from <language> will change the origin language to a specific one
  • /to <language> will translate messages to that language

The preferences will be stored by the bot in its own database.

It will also remember all your commands by default, storing them in a safe place, so you can also see your entire history by running /history .

The bot is privacy-savvy, so you can run /clear to clear all your history, and /dnt (do not track) to stop tracking your messages. /dt (do track) will revert that decision.

Sounds good? Let’s go!

CREATE THE BOT

First thing we must do is to obtain a bot token from Telegram.

How do we do that? We must talk to the Botfather

I’m not joking! This is the official Telegram bot that grants access to other bots.

First thing, install Telegram and get an account there if you don’t have one yet.

Next, inside Telegram search for “botfather”:

Click it, and then press the Start button at the bottom

The bot will reply to the /start command (sent automatically when you click “Start”) with a list of the commands you can send it:

We need to use the /newbot command, here described as “create a new bot”.

You follow the instructions which basically ask for the bot name. I used langbuddybot , you can pick any name you want (you’ll have to choose your unique bot name):

Note down the token. We’ll use that in our Node.js app.

The bot is now reachable at t.me/langbuddybot! We can talk to it, but nothing will happen. It’s just an empty placeholder.

We need to actually create the bot code.

GET STARTED WITH TELEGRAF

Although we could use the raw REST API that Telegram offers, we’re going to use a library called Telegraf .

Read a little about it on its official site and on GitHub.

Why use a library? Because every abstraction that can make things easy for us, the better!

This library in particular is very well done, well maintained and has lots of users (all good signs).

Tip: as a general rule I avoid libraries that have one or more years without any update (sign of possible abandon) or a small user base (like very few stars), unless you know what you are doing.

Install the library using npm i telegraf locally, or add it to the package.json file on Glitch.

Add the Telegram token that Botfather gave you to the .env file, to a variable named TELEGRAM_BOT_TOKEN :

TELEGRAM_BOT_TOKEN="XXXXXXXXXXXXX"

Then create a bot.js file and require telegraf:

const Telegraf = require('telegraf')

and initialize it using new Telegraf() :

const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN)

START UP THE BOT

Remember when we started with Botfather? We had to click the “Start” button.

This is common to all bots. Until you click start, nothing happens.

Using Telegraf we can set our bot to respond to this command by simply adding a callback function to the start() method:

bot.start(ctx => ctx.reply('Welcome'))

Then all we need to do to create a fully functional bot is to call

bot.startPolling()

so that it automatically listens for new messages and chats.

Now we can press the “Start” button on our bot profile in Telegram and it will reply “Welcome”!

If you already pressed it before, right-click the profile and press “Clear History”. You will then start from a clean slate.

The start() command only runs once. Now that we started our interaction with the bot, we can listen for other things that are sent in the chat.

RESPONDING TO /HELP

We can react to the /help command, which is a standard command you can send to any bot. Bots usually explain how to use them.

How do we do that? We add a callback to the help() method:

bot.help(ctx => ctx.reply('Here\'s all the help I can give!'))

THE CONTEXT

What is ctx ? We saw it in start() as well. It’s the context object. Every request gets its own context object, and you can inspect it to gain more information about the message, including

  • ctx.message the received message
  • ctx.inlineQuery the received inline query (more on this later)
  • ctx.from the message sender info

The ctx object also exposes some methods we’ll use to interact with the chat, including:

  • ctx.reply() sends a message to the chat

INTERCEPT ANY MESSAGE

Using the on() method you can intercept any message coming in from the chat, and do something. In this case we reply “Hey!”:

bot.on('message', ctx => ctx.reply('Hey!'))

INTERCEPT A SPECIFIC MESSAGE

You can listen for a specific message in the chat using

bot.hears('hi', ctx => ctx.reply('Hey there'))

Warning: this is case sensitive.

To respond to a command whatever the case is, use a regular expression:

bot.hears('/hi/i', ctx => ctx.reply('Hey there'))

Check out a guide to JavaScript Regular Expressions to know more about regular expressions.

USE THE YANDEX TRANSLATE API

I really wanted to use the Google Translate API. Just because I always use their app.

But, turns out the API is paid, so I looked at an alternative and I found Yandex Translate to be a good one.

Go to https://tech.yandex.com/translate/. If you don’t have a Yandex account, create one for free.

Then create an API key from the interface.

According to the Terms of Use for the Yandex.Translate service, the text Powered by Yandex.Translate must be shown above or below the translation result, with a clickable link to the page http://translate.yandex.com/. As this is just a private sample application, I won’t do it, but just be aware.

Now, store this API key in the .env file as YANDEX_API_KEY .

The Yandex Translate API documentation (https://tech.yandex.com/translate/doc/dg/reference/translate-docpage/) tells us to build an API request in this way:

https://translate.yandex.net/api/v1.5/tr.json/translate
 ?key=<API key>
 &text=<text to translate>
 &lang=<translation direction>

In the project introduction I mentioned we’ll use two commands to set the languages:

  • /from <language> will change the origin language to a specific one
  • /to <language> will translate messages to that language

The preferences will be stored by the bot in the session, so every subsequent message will use those preferences.

The lang in the query parameter can take the form <from>-<to> or just <to> (in this case “from” will be guessed from the message). For example en or it-en .

Let’s try this and translate to english every message that comes in.

We listen to every message using on() :

bot.on('message', ctx => {})

Using Axios ( npm i axios ) we’ll perform a network request to the Yandex Translate API.

See https://flaviocopes.com/axios to get an intro to Axios

We pass in the params object, and we listen for the promise to complete in the then() callback:

const axios = require('axios')

//...
bot.on('message', ctx => {
  axios.get('https://translate.yandex.net/api/v1.5/tr.json/translate', {
    params: {
      key: process.env.YANDEX_API_KEY,
      text: ctx.message.text,
      lang: 'en'
    }
  }).then(res => {
    ctx.reply(res.data.text[0])
  })
})

See, we post the response in the chat by calling ctx.reply() and passing res.data.text[0] , which contains the response from the API.

Try it, it should work!

SET LANGUAGES IN THE SESSION

Right now our bot translates to english the language it detects.

We need to do 2 things now:

  1. manually set the from language 2) change the to language

We will provide the users of our bot these 2 commands:

  • /from <language> will change the origin language to a specific one (if nothing is specified, we’ll get back to guessing the from language)
  • /to <language> will translate messages to that language. In this case we don’t allow an empty option.

Let’s do it!

First we add a from command. We look at the parameter passed, and we determine the language we need to set, doing some rough validation for the language code:

bot.command('from', ctx => {
  const lang = ctx.message.text.substring(6)

  if (lang.length > 2 || lang.length === 1) {
    ctx.reply('🤔 language code must be 2 chars, e.g. "en" or "fr"')
    return
  }

  ctx.reply(lang ? '✅ "from" language set to ' + lang : '✅ autodetect "from" language')
})

Then we add this parameter to the context session, which allows us to store some piece of data to the user chat:

const session = require('telegraf/session')
//...
bot.use(session())
//...

bot.command('from', ctx => {
  const lang = ctx.message.text.substring(6)
  //...validation

  ctx.session.from = lang
})

Now we do the same for to . We detect the language code, by using substring() on ctx.message.text , then we do a little bit of validation and we set it to the ctx.session.to property:

bot.command('to', ctx => {
  const lang = ctx.message.text.substring(4)
  if (lang.length === 0) {
    ctx.reply('🤔 please specify a language code! It must be 2 chars, e.g. "en" or "fr"')
    return
  }

  if (lang.length > 2 || lang.length === 1) {
    ctx.reply('🤔 language code must be 2 chars, e.g. "en" or "fr"')
    return
  }

  ctx.session.to = lang
  ctx.reply('✅ "to" language set to ' + lang)
})

In this case, just to be different, we don’t allow an empty value for the to language. We could have defaulted to english.

Last, we must use this language in the on() method which listens for messages, to translate to the correct language.

I use a ternary operator to determine the lang value in a concise way:

const lang = (ctx.session.from ? ctx.session.from + '-' : '') + (ctx.session.to || 'en')

This line means: if there is a from value, add it and add - . If there is no from value, just use the to value, and fallback to en .

Here it is this line in use:

bot.on('message', ctx => {
  const lang = (ctx.session.from ? ctx.session.from + '-' : '') + (ctx.session.to || 'en')
  console.log(lang)
  axios.get('https://translate.yandex.net/api/v1.5/tr.json/translate', {
    params: {
      key: process.env.YANDEX_API_KEY,
      text: ctx.message.text,
      lang: lang
    }
  }).then(res => {
    ctx.reply(res.data.text[0])
  })
})

STICKY SESSION DATA

There is a problem however, if the program relaunches, the session data is lost.

We can use a session storage middleware to solve this.

There are many options available on npm: https://www.npmjs.com/search?q=telegraf-session

I picked https://www.npmjs.com/package/telegraf-session-local which allows us to save the session to a local file. We’ll store it to .data/session.json in Glitch, as .data is a smart folder that’s not copied over when a project is remixed.

Install telegraf-session-local using npm.

The usage is super simple. Just require it:

const LocalSession = require('telegraf-session-local')

and now instead of calling

bot.use(session())

like we did above, run

bot.use((new LocalSession({ database: '.data/session.json' })).middleware())

This will make sure the session data is stored in the JSON file specified, and now if the app reloads, the session data will stick!

The current bot code is available at https://glitch.com/edit/#!/node-course-project-telegram-bot-a?path=bot.js

THE TRANSLATIONS HISTORY

We’re going to store every translation requested, and the result of the translation, so the user can request it back using the / history command.

We’ll also allow the user to clear the history using /clear .

Plus, calling /dnt we’ll disable storing to the history for this particular user, and using /dt we can restore this functionality.

Let’s start by storing every message in the session, with its translation, right after we get it in the message event handler:

const translation = res.data.text[0]
ctx.reply(translation)

let messages = JSON.parse(ctx.session.messages) || []
messages.push({text: ctx.message.text, translation: translation})
ctx.session.messages = JSON.stringify(messages)

Now we can add a /history command handler:

bot.command('history', ctx => {
  try {
    ctx.reply(JSON.parse(ctx.session.messages).map(message => `${message.text}: ${message.translation}`).join('\n'))
  } catch (err) {
    console.error(err)
  }
})

See how we reply to the person. We parse the messages from the session (since they are stored as JSON), then we create a new array which is formatted as text: translation , and we join the array using new lines as separators, so we are pushing a string to the user.

Let’s also add a command to clear the chat history:

bot.command('clear', ctx => {
  ctx.session.messages = JSON.stringify([])
  ctx.reply('✅ history cleared')
})

We simply assign an empty array (as an array JSON-encoded to a string).

Let’s give the user the “do not track” option by typing the /dnt command. We store a dnt flag in the session:

bot.command('dnt', ctx => {
  ctx.session.dnt = true
  ctx.reply('✅ do not track')
})

and we also add a corresponding /dt command that restores the option to the original state. By default, we do track.

bot.command('dt', ctx => {
  ctx.session.dnt = false
  ctx.reply('✅ do track')
})

Let’s now add this flag in the on('message') callback:

//...
if (ctx.session.dnt === true) {
  return
}
let messages = JSON.parse(ctx.session.messages) || []
//...

The code for the project is available at https://glitch.com/edit/#!/node-course-project-telegram-bot-b?path=bot.js

CHALLENGES

If you got the mentoring package, you can clone the project at https://github.com/flaviocopes/node-course-project-telegram-bot and implement the following coding challenges. Once you are done, email me and I will review your work (if you got the mentoring package!)

  • Add a handler for the /help command. Search in the Telegraf documentation how to respond to this command, and tell the user how they can interact with the bot
  • Make the bot work inline, so that when mentioned in any chat using @botranslate it will translate your message from/to the language you set, and send it instead of the message you wrote. Real-time translation!