Today the most popular ways to build telegram chat bots in node js are Telegraf and Node-Telegram-Bot-Api. The second is more popular by weekly downloads(250.000), but in this post I don't want to equalize them by stats. I want to talk about my experience with them, in which cases I used them and show you really nice guide, especially for Telegraf, cause they haven't readable docs for newbies or people who don't use typescript.
Node Telegram Bot API
So, let's start with this package. It is really easy, but due to it we can't use it in really big projects, cause it will be hard to understand structure of bot without comments or million javascript files.
To download type:
npm i node-telegram-bot-api
After this, as said in documentation, you need to initialize it with your telegram token, which you received from Bot Father
const TelegramBot = require('node-telegram-bot-api');
const token = 'YOUR_TELEGRAM_BOT_TOKEN';
// Create a bot that uses 'polling' to fetch new updates
const bot = new TelegramBot(token, {polling: true});
And there are two main methods in this package - bot.sendMessage(chatId, message, options)
and bot.on("one of params from api like message or text", function which contains message param and returns response)
bot.on('message', (msg) => {
const chatId = msg.chat.id;
// send a message to the chat acknowledging receipt of their message
bot.sendMessage(chatId, 'Received your message');
});
Here is message object with all fields. So, if we need to react on /start
command we need to make something like this:
bot.on("message", async function (msg) {
const text = msg.text;
const chatId = msg.chat.id;
if (text === "/start") {
bot.sendMessage(chatId, `Hello ${msg.from.username}! Choose your language...`,
reply_markup: {
one_time_keyboard: true,
keyboard: [["ua"], ["en"]],
},);
}
});
We checked if our text is equal to our command and responded with message. It is all main logic from this package, more sendMessage methods or params you can see here. Also we used reply_markup
option in which we can use our keyboard, which will be sent with our message. And then we can check if text equal to "en" we will change our language to english and response with message.
In addition, if you have a question like can you send message without getting message from user - you can do it. All that you need is to save your user to database like MongoDB with chatId field and use something like express to make CRUD methods to send your message. Here is my example of this 'bot mailer':
app.get("/", async (req, res) => {
await TelegramUser.find()
.cursor()
.eachAsync(async (user) => {
bot.sendMessage(user.chatId, "<b>Hi!</b>", {
parse_mode: "HTML",
disable_web_page_preview: false,
});
});
});
So here we sent our message "Hi" in async forEach mongodb method when we got request to our app. TelegramUser
is a mongodb model, where I store user chatId. Also you can see two the most useful options from this package - parse_mode, which parses our message to HTML(but can't parse all tags, like img, video, cause there are methods like sendPhoto, sendVideo) and disable_web_page_preview, which you can use when in message you send link and you don't want to see preview of it.
I hope you got it, let's move to telegraf.
Telegraf
Let's talk about telegram bot library which I prefer to use in big projects.
To download:
npm i telegraf
In this library we can see similar methods with node-telegram-bot-api:
const { Telegraf } = require('telegraf')
const bot = new Telegraf(process.env.BOT_TOKEN)
bot.start((ctx) => ctx.reply('Welcome'))
bot.help((ctx) => ctx.reply('Send me a sticker'))
bot.on('sticker', (ctx) => ctx.reply('👍'))
bot.hears('hi', (ctx) => ctx.reply('Hey there'))
bot.launch()
To initialize we use new Telegraf(token)
. To react on /start command we type bot.start
which is more readable than in previous package. Here instead of message object we have ctx - context object, which almost doesn't have big difference from first one but has many another methods.
ctx.reply
is function with response message, here if we want to parse HTML or reply with audio or photo etc. we can use methods like ctx.replyWithHTML
or ctx.replyWithPhoto
, all methods you can see in previous link.
bot.on
is the same as in node-telegram-bot-api, all params which can receive this function you can see here.
bot.hears
is just a function which reacts on user's message.
And to launch our bot we use bot.launch()
It would seem that it is the end, but in fact it is one part of opportunities which this library gives. Here you can use scenes.
Telegraf Scenes
Let's imagine that telegram bot is a theatre with one viewer - our user. When performance starts, the first scene is beginning, let's name it start scene. There are some actions take place in this scene for some time and then the scene ends, in another words we left the scene. So this interval from beginning of our scene and ending is our scene.
Telegraf Scene is the same, but let's display it in code.
This library has two types of scene: Base Scene and Wizard Scene.
To set up our scenes we need to register them with Stage. Let's do it in our main file.
app.js
const { Scenes, Telegraf } = require("telegraf");
const start = require("./controllers/start"); // scene file
const about = require("./controllers/about"); // scene file
const settings = require("./controllers/settings"); // scene file
const contact = require("./controllers/contact"); // scene file
const search = require("./controllers/search"); // scene file
const bot = new Telegraf(process.env.TELEGRAM_TOKEN);
const stage = new Scenes.Stage([start, about, settings, contact, search]); // Register our scenes
bot.use(stage.middleware()); // Stage middleware
bot.hears("settings", Scenes.Stage.enter("settings")); // Entering the settings scene when listener worked
Here we imported our scenes and registered them in array. Also we added a stage middleware without which our scenes won't work. And to enter our scene we set bot listener with bot.hears
and then as a first param we type our scene id(see in next paragraph) and then we enter the scene with Scenes.Stage.enter
.
Base Scene
Here is an example of this scene:
settings.js
const { Scenes } = require("telegraf");
const Scene = Scenes.BaseScene;
const { getMainKeyboard, getBackKeyboard } = require("../../util/keyboards");
const { leave } = Scenes.Stage;
const settings = new Scene("settings");
settings.enter(async (ctx) => {
const { backKeyboard } = getBackKeyboard();
await ctx.reply("settings.what_to_change", backKeyboard);
});
settings.leave(async (ctx) => {
const { mainKeyboard } = getMainKeyboard();
await ctx.reply("shared.what_next", mainKeyboard);
await ctx.scene.leave();
});
settings.hears("Back", leave());
settings.action("backButtonId", console.log("Back button was clicked"));
module.exports = settings;
../../util/keyboards
const { Markup } = require("telegraf");
exports.getMainKeyboard = () => {
let mainKeyboard = Markup.keyboard([
["Movies", "Settings"],
]);
mainKeyboard = mainKeyboard.oneTime();
return mainKeyboard;
};
exports.getBackKeyboard = () => {
let backKeyboard = Markup.keyboard(["Back"]);
backKeyboard = backKeyboard.oneTime();
return backKeyboard;
};
It is file with our settings scene. To initialize it, we use new Scene("scene id")
. When user entered our scene he will receive response from settings.enter(async function(ctx))
, in which we make our first step of scene. Then all actions in our scene will be in settings.on
or in settings.hears
. Then, when scene is ending we use settings.leave
in which we have function with moving back to default scene(start scene) and ctx.scene.leave()
, which is required to leave scene. Without it you have a risk to stay in this scene forever.
Also, if we have keyboard, which can be created by Markup.keyboard
, all our buttons there are strings in array. If we want to make many rows in our keyboard, we can make many arrays, like if we have two arrays with two strings(buttons), we will get keyboard with 2 rows with 2 buttons in each.
When we tap on button, we can handle this tap in scene.action(buttonId)
, where buttonId is our button's second param in Markup.button.callback
. Also we can create inline keyboard with Markup.inlineKeyboard
Markup.inlineKeyboard(
[
Markup.button.callback("Back", "backButtonId"),
],
{}
)
.oneTime()
.resize();
Wizard Scene
All logic of this scene is the same as in previous, but here we have a chain with scene's steps. To go to our next step we use ctx.wizard.next
and ctx.wizard.steps[ctx.wizard.cursor](ctx)
too, cause in last versions of Telegraf without it our step won't change. To leave from scene we use ctx.scene.leave
const { Scenes } = require("telegraf");
const { getMainKeyboard } = require("../../util/keyboards");
const Wizard = Scenes.WizardScene;
const about = new Wizard(
"about", // Our wizard scene id, which we will use to enter the scene
async (ctx) => {
await ctx.reply(ctx.i18n.t("scenes.about.main"));
ctx.wizard.next();
return ctx.wizard.steps[ctx.wizard.cursor](ctx);
},
async (ctx) => {
const { mainKeyboard } = getMainKeyboard(ctx);
await ctx.reply(ctx.i18n.t("shared.what_next"), mainKeyboard);
return ctx.scene.leave();
}
);
module.exports = about;
Session
And the last main thing in this library is session. It is like local storage in web but for telegram bots. Here we can save params that we need to save and use them in future. To set up, let's change our main file:
const { Scenes, session, Telegraf } = require("telegraf");
const start = require("./controllers/start"); // scene file
const about = require("./controllers/about"); // scene file
const settings = require("./controllers/settings"); // scene file
const contact = require("./controllers/contact"); // scene file
const search = require("./controllers/search"); // scene file
const bot = new Telegraf(process.env.TELEGRAM_TOKEN);
const stage = new Scenes.Stage([start, about, settings, contact, search]); // Register our scenes
bot.use(session()); // Session middleware
bot.use(stage.middleware()); // Stage middleware
bot.hears("settings", Scenes.Stage.enter("settings")); // Entering the settings scene when listener worked
Here we imported our session from telegraf and made a session middleware. Then we can use it in our scenes. For example:
const { Scenes, session } = require("telegraf");
const { getMainKeyboard, getBackKeyboard } = require("../../util/keyboards");
const { User } = require("../../models");
const { getMoviesBySearch, getMovieKeyboard } = require("./helpers");
const Base = Scenes.BaseScene;
const search = new Base("search");
const { leave } = Scenes.Stage;
search.enter(async (ctx) => {
const backKeyboard = getBackKeyboard(ctx);
await ctx.replyWithHTML("scenes.search.welcome_to_search",
backKeyboard
);
});
search.hears("Back"), leave());
search.on("text", async (ctx) => {
const user = await User.findById(ctx.from.id);
const movies = getMoviesBySearch(user.language, ctx.message.text);
const movieKeyboard = getMovieKeyboard(ctx);
ctx.session.movies = movies; // Set session field
ctx.session.index = 0; // Set session field
ctx.replyWithHTML(``, movieKeyboard);
});
search.leave(async (ctx) => {
const { mainKeyboard } = getMainKeyboard(ctx);
await ctx.reply("shared.what_next", mainKeyboard);
delete ctx.session.index; // Delete session field
delete ctx.session.movies; // Delete session field
await ctx.scene.leave();
});
module.exports = search;
Conclusions
In this post I explained how to deal with the most popular js telegram libraries step by step. As I said, I prefer Telegraf to Node-Telegram-Bot-Api because it is more interesting and readable. If you have any thoughts about it, write at the comments below and correct me if I made some mistakes in this post, cause I don't speak English fluently 😅
In addition
Today in my country, in Ukraine, is a war, which caused by Russia's aggression. I want to have a peace in my country, our army does their best and it would be great if you can support us writing #nowarinukraine in your dev posts. Thanks :)
Oldest comments (15)
Hi bro! I am from Ukraine too and i'll ask you on our native language
Використовую telegraf і хочу відправити повідомлення у чат без команди. Знайшов наче вирішення :
app.telegram.sendMessage(chat.id, 'my text');
Але не знаю звідки дістати chat.id
Був би вдячний за допомогу!
Привіт, взагалі так просто в коді chatId ти зберігати навряд чи зможеш, тому тобі потрібно використовувати якусь базу данних чи сесію, наприклад MongoDB, вище в пості я наглядно показав приклад коду з node-telegram-bot-api, який через forEach проходить усіх юзерів(TelegramUser - модель юзерів, тобто схема, де і потрібно зберігати поле chatId). Зберігати chatId потрібно при реєстрації юзера, тобто його першого повідомлення в чат, трохи вище ніж приклад з mongoDB. chatId є у msg.chat.id. Якщо раніше не працював з mongoDB, то можеш подивитися туторіали на ютуб, бажано англійською, бо там набагато якісніший матеріал) Надіюсь зрозуміло пояснив 😊
Awesome description of all needed features
Thanks! 😊
there is new BOSS in telegram api :) grammyjs
is use coolest conversation api ever 627d0a590e3a8b0b2ba822a1--grammy.n...
thank you
Bro, I didnt understand, if telegraph has more functions (sessions, scenes, etc.), why do you use Node-Telegram-Bot-Api? what makes it more interesting?
I think nothing makes Node-Telegram-Bot-Api more interesting, I haven't wrote about that. I think it is easy to use for small projects, so for telegram bot with small logic, but for big projects where there are many scenes and user interaction with bot - it is easier to use telegraph
Hello, I need a bot that will ask the user for his name and number, how and where I can get this data in order to call or write to the user in the future. Слава Україні!
Where do we hire devs for our complex telegram bot? Is there a place for this here on this site?
Yeah, my freelance account :) contra.com/makar_lutskyi_acx1fag2
Thank you for this post. I was able to get a solid intro to these. It's been 2 years since you wrote this article.
Do you still think that Telegraf is the best option?
nowarinukraine
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more