Create a telegram bot using Node.js

These days chatbots are used extensively by various companies for customer acquisition and retention, marketing etc. In this article, we'll see how to build a telegram bot using Node.js (GitHub).

The following packages will be used to build the telegram bot:

  1. Telegraf.js

  2. Express.js

  3. Dotenv


Installation

Install the required packages using the following command:

npm i telegraf express dotenv

Setup

  1. Create a telegram bot using BotFather

    Search for BotFather in telegram and enter /newbot. Give your bot a name and copy the bot token.

  2. Create a config.env file

    Your config.env file should look like this:

BOT_TOKEN = "<YOUR BOT TOKEN>"
PORT = 8000
  1. Create a server.js file

    Here's an outline for the server.js file, with the packages imported and the bot-related code added.

const app = require("express")(); // For creating the server
const {
  Telegraf,
  session,
  Scenes: { Stage, WizardScene },
} = require("telegraf");
const dotenv = require("dotenv"); // For reading the .env file
dotenv.config({ path: "./config.env" }); 
const bot = new Telegraf(process.env.BOT_TOKEN);

session({
  property: "chatSession",
  getSessionKey: (ctx) => ctx.chat && ctx.chat.id,
});
bot.use(session());
bot.use(stage.middleware());

bot.command("/addPreference", async (ctx) => {
  ctx.scene.enter("choose course");
});

bot.launch();
app.listen(process.env.PORT, () => {
  console.log("listening at", process.env.PORT);
});

The bot will start chatting with the user once the user types and sends the command /addPreference.


Code

This bot will ask the user to input their preferred domain and reply with the user's choice of technologies.

Using WizardScene from Telegraf.js, a 'conversation-like-scene' can be created.

First, the bot will ask for the user's preferred domain:

const course = new WizardScene(
  "choose course",
  async (ctx) => {
    ctx.reply(`Hey There! Choose your preferred domain`, {
      reply_markup: {
        inline_keyboard: [
          [
            {
              text: "Frontend",
              callback_data: "Frontend",
            },
            {
              text: "Backend",
              callback_data: "Backend",
            },
          ],
        ],
      },
    });
    ctx.wizard.cursor = 0;
    return ctx.wizard.next();
  },

Looks confusing, right?

Let's understand a few terms first:

ctx - It is short for Context. It is an object created per request and contains the following props:

PropertyDescription
ctx.telegramTelegram client instance
ctx.updateTypeUpdate type (message, inline_query, etc.)
[ctx.updateSubTypes]Update subtypes (text, sticker, audio, etc.)
[ctx.message]Received message
[ctx.editedMessage]Edited message
[ctx.inlineQuery]Received inline query
[ctx.chosenInlineResult]Received inline query result
[ctx.callbackQuery]Received callback query
[ctx.shippingQuery]Shipping query
[ctx.preCheckoutQuery]Precheckout query
[ctx.channelPost]New incoming channel post of any kind — text, photo, sticker, etc.
[ctx.editedChannelPost]New version of a channel post that is known to the bot and was edited
[ctx.poll]New version of an anonymous poll that is known to the bot and was changed
[ctx.pollAnswer]This object represents an answer of a user in a non-anonymous poll.
[ctx.chat]Current chat info
[ctx.from]Sender info
[ctx.match]Regex match (available only for hears, command, action, inlineQuery handlers)
ctx.webhookReplyShortcut to ctx.telegram.webhookReply
  • return ctx.wizard.next - It moves the cursor to the next step of the conversation

  • ctx.wizard.cursor=0 sets the position of the cursor to 0.

    It is important to know the position of the cursor as it helps in identifying which step of the conversation the bot is currently at.

  • WizardScene - It is used to collect information like a form.

  • Stage - A stage holds every scene. In Telegraf, the scene must be added to a stage before entering into it.

  • Session - It helps in keeping the data persisted between commands.

Next, the bot will ask for the user's preference of technology based on the selected domain.

async (ctx) => {
    if (ctx.callbackQuery == undefined) {
      ctx.reply("Incorrect input. Bot has left the chat");
      ctx.scene.leave();
    } else if (ctx.callbackQuery.data == "Backend") {
      ctx.answerCbQuery();
      ctx.wizard.state.domain = ctx.callbackQuery.data;
      ctx.reply("What technologies do you prefer to work with?", {
        reply_markup: {
          inline_keyboard: [
            [
              {
                text: "Node.js",
                callback_data: "Node.js",
              },
              {
                text: "Django",
                callback_data: "Django",
              },
            ],
          ],
        },
      });
      ctx.wizard.cursor = 1;

      return ctx.wizard.next();
    } else {
      ctx.answerCbQuery();

      ctx.wizard.state.domain = ctx.callbackQuery.data;

      ctx.reply("What technologies do you prefer to work with?", {
        reply_markup: {
          inline_keyboard: [
            [
              {
                text: "Basic HTML/CSS/JS",
                callback_data: "HTML/CSS/JS",
              },
              {
                text: "React",
                callback_data: "React",
              },
            ],
          ],
        },
      });
      ctx.wizard.cursor = 1;

      return ctx.wizard.next();
    }
  },
  • ctx.callbackQuery will return undefined if the user doesn't click on the inline buttons and does something else, like typing a reply to the bot's questions.

  • ctx.answerCbQuery() is used to answer the callback query. It is important as sometimes the inline buttons may continue loading after clicking, which results in the crashing of the bot.

  • ctx.wizard.state.var_name - It is used to store data across the WizardScene.

💡
If the user types something as a reply to the question asked by the bot instead of clicking on the buttons, the bot leaves the chat saying "Incorrect input. Bot has left the chat".

Lastly, the bot will confirm the user's input and reply with a thank you message.

async (ctx) => {
    if (ctx.callbackQuery == undefined) {
      ctx.reply("Incorrect input. Bot has left the chat");
      ctx.scene.leave();
    } else {
      ctx.answerCbQuery();

      ctx.wizard.state.tech = ctx.callbackQuery.data;
      ctx.reply(
        `You have entered:
Preferred Domain: ${ctx.wizard.state.domain}
Preferred technologies: ${ctx.wizard.state.tech}

Do you want to change it?`,
        {
          reply_markup: {
            inline_keyboard: [
              [
                {
                  text: "Yes",
                  callback_data: "Yes",
                },
                {
                  text: "No",
                  callback_data: "No",
                },
              ],
            ],
          },
        }
      );
      ctx.wizard.cursor = 2;

      return ctx.wizard.next();
    }
  },
  async (ctx) => {
    if (ctx.callbackQuery == undefined) {
      ctx.reply("Incorrect input. Bot has left the chat");
      ctx.scene.leave();
    } else if (ctx.callbackQuery.data == "No") {
      ctx.answerCbQuery();

      ctx.reply("Thank you for your response");
      ctx.scene.leave();
    } else {
      ctx.answerCbQuery();

      return ctx.wizard.steps[0](ctx);
    }
  }
);
  • return ctx.wizard.steps[<index>](ctx) - Helps in returning to a particular step in the conversation.

server.js

const app = require("express")(); // For creating the server
const {
  Telegraf,
  session,
  Scenes: { Stage, WizardScene },
} = require("telegraf");
const dotenv = require("dotenv"); // For reading the .env file
dotenv.config({ path: "./config.env" }); // For reading the .env file
const bot = new Telegraf(process.env.BOT_TOKEN);

const course = new WizardScene(
  "choose course",
  async (ctx) => {
    ctx.reply(`Hey There! Choose your preferred domain`, {
      reply_markup: {
        inline_keyboard: [
          [
            {
              text: "Frontend",
              callback_data: "Frontend",
            },
            {
              text: "Backend",
              callback_data: "Backend",
            },
          ],
        ],
      },
    });
    ctx.wizard.cursor = 0;
    return ctx.wizard.next();
  },
  async (ctx) => {
    if (ctx.callbackQuery == undefined) {
      ctx.reply("Incorrect input. Bot has left the chat");
      ctx.scene.leave();
    } else if (ctx.callbackQuery.data == "Backend") {
      ctx.answerCbQuery();
      ctx.wizard.state.domain = ctx.callbackQuery.data;
      ctx.reply("What technologies do you prefer to work with?", {
        reply_markup: {
          inline_keyboard: [
            [
              {
                text: "Node.js",
                callback_data: "Node.js",
              },
              {
                text: "Django",
                callback_data: "Django",
              },
            ],
          ],
        },
      });
      ctx.wizard.cursor = 1;

      return ctx.wizard.next();
    } else {
      ctx.answerCbQuery();

      ctx.wizard.state.domain = ctx.callbackQuery.data;

      ctx.reply("What technologies do you prefer to work with?", {
        reply_markup: {
          inline_keyboard: [
            [
              {
                text: "Basic HTML/CSS/JS",
                callback_data: "HTML/CSS/JS",
              },
              {
                text: "React",
                callback_data: "React",
              },
            ],
          ],
        },
      });
      ctx.wizard.cursor = 1;

      return ctx.wizard.next();
    }
  },
  async (ctx) => {
    if (ctx.callbackQuery == undefined) {
      ctx.reply("Incorrect input. Bot has left the chat");
      ctx.scene.leave();
    } else {
      ctx.answerCbQuery();

      ctx.wizard.state.tech = ctx.callbackQuery.data;
      ctx.reply(
        `You have entered:
Preferred Domain: ${ctx.wizard.state.domain}
Preferred technologies: ${ctx.wizard.state.tech}

Do you want to change it?`,
        {
          reply_markup: {
            inline_keyboard: [
              [
                {
                  text: "Yes",
                  callback_data: "Yes",
                },
                {
                  text: "No",
                  callback_data: "No",
                },
              ],
            ],
          },
        }
      );
      ctx.wizard.cursor = 2;

      return ctx.wizard.next();
    }
  },
  async (ctx) => {
    if (ctx.callbackQuery == undefined) {
      ctx.reply("Incorrect input. Bot has left the chat");
      ctx.scene.leave();
    } else if (ctx.callbackQuery.data == "No") {
      ctx.answerCbQuery();

      ctx.reply("Thank you for your response");
      ctx.scene.leave();
    } else {
      ctx.answerCbQuery();

      return ctx.wizard.steps[0](ctx);
    }
  }
);

session({
  property: "chatSession",
  getSessionKey: (ctx) => ctx.chat && ctx.chat.id,
});
bot.use(session());

const stage = new Stage([course], { sessionName: "chatSession" });
bot.use(stage.middleware());
stage.register(course);

bot.command("/addPreference", async (ctx) => {
  ctx.scene.enter("choose course");
});

bot.launch();
app.listen(process.env.PORT, () => {
  console.log("listening at", process.env.PORT);
});

Don't forget to create a new stage using:

const stage = new Stage([<name_of_WizardScene>], { sessionName: "chatSession" });

Register the WizardScene with the stage:

stage.register(<name_of_WizardScene>);


Conclusion

Telegraf.js makes it very easy for developers to work with telegram bots using Node.js.

If you want to dive deeper into this topic, you can refer Telegraf.js documentation and Youtube.

Â