Overview Link to heading

Here is a guide for creating a Telegram chatbot using NodeJS, DigitalOcean Functions, and the OpenAI API. It will be divided into several parts.

Why NodeJS? Link to heading

I consider NodeJS to be a promising modern backend technology. I really enjoy coding with it and want to gain more experience in it.

Why DigitalOcean? Link to heading

I chose DigitalOcean because it has a low learning curve and the prices are reasonable. Additionally, DigitalOcean Functions are completely free at low request rates. In the future, I plan to try AWS as well.

Step 1. Create bot Link to heading

What do we need to create our own AI chatbot? First and foremost, we need to create an empty bot to get started. Here are the official docs for that. As a result, we will get a token; let’s name it TELEGRAM_BOT_TOKEN.

create_bot_pic

Now, you can find your bot in the Telegram app through the “search” feature. However, the bot is currently silent and will not respond yet. We need to write some code to teach the bot to respond. There are two approaches to interacting with the bot: polling and webhook. We will discuss the pros and cons of each later.

For now, let’s try to run the bot locally using polling.

Step 2. Choose JS library Link to heading

We do not need to implement all of Telegram’s Bot APIs using HTTP from scratch. There are many client libraries available for various programming languages. After browsing GitHub, I found a very popular JS library: node-telegram-bot-api. It supports almost all Telegram Bot API features. Another promising alternative is grammY, a TypeScript library that I plan to test soon.

Step 3. Launch locally Link to heading

Let’s try to launch a simple ping-pong chatbot using this library and the example from the README.

example.js:

const TelegramBot = require('node-telegram-bot-api');

// replace the value below with the Telegram token you receive from @BotFather
const token = "TELEGRAM_BOT_TOKEN";

// Create a bot that uses 'polling' to fetch new updates
const bot = new TelegramBot(token, {polling: true});

// Listen for any kind of message. There are different kinds of
// messages.
bot.on("text", (msg) => {
  const chatId = msg.chat.id;

  // send a message to the chat acknowledging receipt of their message
  bot.sendMessage(chatId, 'Received your message');
});

Run this code on your machine using node:

$ node example.js

… and try messaging your bot through the Telegram app.

Okay, it works.

Step 4. Polling vs Webhook Link to heading

We cannot keep your bot online all the time on a local machine for safety and availability reasons. Therefore, we need to deploy the bot on a remote machine. This requires hosting, and we need to pay for this hosting. Even if the bot does not interact with users, we will still incur costs for the VM resources, as the bot is always polling for new updates.

As I mentioned earlier, there is a second approach - webhooks. We need to set up an HTTP endpoint, and Telegram will push each chat message to this endpoint. If there are no messages for the bot in a chat, there will be no requests to our endpoint. This allows us to use serverless functions. Each call to our endpoint triggers a serverless function. When there is no traffic, we do not consume any resources. Great!

DigitalOcean has a serverless product called DigitalOcean Functions. Let’s use it. DigitalOcean Functions are free at low request rates.

Step 5. Set up DigitalOcean Functions Link to heading

Firstly, we need DigitalOcean account. Sign up and sign in.

Now, install the doctl tool. It is a console tool used to interact with the DigitalOcean API. Unfortunately, this tool is mandatory to deploy serverless functions because adding functions through the UI works only for very simple and straightforward functions. Here are the docs. Make sure to complete all the steps, including the installation of serverless support. After that, we create a Functions namespace:

$ doctl serverless namespaces create --label "example-namespace" --region "fra1"

It is also possible to do this through the UI.

Now we need to write the code in a folder with a strict structure. You can read the detailed docs here.

$ doctl serverless init --language js example-functions-project

We get a folder named example-functions-project with a configuration file and a JS file.

Deploy it:

$ doctl serverless deploy example-functions-project --remote-build

We get our function deployed. Now we can invoke our function using doctl or an HTTP request. For a Telegram webhook, we need an HTTP endpoint, so let’s see how to get the invoke URL for the endpoint:

$ doctl serverless functions get sample/hello --url
https://faas-fra1-cfec6ae7.doserverless.co/api/v1/web/fn-7ca17e91-d162-4f4a-9a5f-e28e32fa0bd3/sample/hello

Test this URL with curl. It works.

Step 6. Turn on webhook Link to heading

Set up the URL from the previous section for our Telegram bot. Use your TELEGRAM_BOT_TOKEN and curl for this.

$ curl -H 'Content-Type: application/json' "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" -d '{"url": "https://faas-fra1-cfec6ae7.doserverless.co/api/v1/web/fn-7ca17e91-d162-4f4a-9a5f-e28e32fa0bd3/sample/hello"}'
{"ok":true,"result":true,"description":"Webhook was set”}

See the docs

Step 7. Code Link to heading

Let’s fill the JS file with code to accept messages from Telegram and respond.

Info
I prepared a ready-to-deploy template for creating a Telegram bot on DigitalOcean. See it on GitHub - https://github.com/artshmelev/telegram-bot-digitalocean-quickstart

hello.js:

const TelegramBot = require("node-telegram-bot-api");

async function main(event) {
  try {
    console.log(event);
    const token = process.env.TELEGRAM_BOT_TOKEN;
    const bot = new TelegramBot(token);
    let globalResolve = () => {};

    bot.on("text", async (msg) => {
      await bot.sendMessage(msg.chat.id, `Received: ${msg.text}`);
      globalResolve("ok");
    });

    await new Promise((resolve) => {
      globalResolve = resolve;
      // processUpdate does not return Promise
      // thats why we need this workaround
      bot.processUpdate(event);
      setTimeout(() => {
        resolve("timeout");
      }, 29000);
    });
  } catch (err) {
    console.log(`Error: ${err}`);
    return {
      body: err,
      statusCode: 200,
      headers: {
        "Content-Type": "text/plain",
      },
    };
  }

  console.log("Finish");
  return {
    body: {},
    statusCode: 200,
    headers: {
      "Content-Type": "application/json",
    },
  };
}

exports.main = main;

The code is fairly straightforward, but there are a few details to mention:

  • We retrieve the token from an environment variable for safety reasons.
  • bot.on("text", F) is the handler for incoming text messages.
  • The client library node-telegram-bot-api does not provide an asynchronous function to process only one message (see this issue). To work around this, we need to create a timer inside a promise and await the promise.

Now, look at the project file
project.yml:

parameters: {}
environment: {}
packages:
    - name: sample
      shared: false
      environment: {}
      parameters: {}
      annotations: {}
      functions:
        - name: hello
          binary: false
          main: ""
          runtime: nodejs:18
          web: true
          webSecure: false
          parameters: {}
          environment:
            TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN}"
          annotations: {}
          limits: {}

The only changes are:

  1. Set the runtime version of NodeJS.
  2. Set the environment variable for the token.

You can place your TELEGRAM_BOT_TOKEN inside an .env file in the project folder:
.env:

TELEGRAM_BOT_TOKEN=<your_token_here>

Now you can deploy this function and use your chatbot through the Telegram app.

run_bot

Info

Useful Insights Link to heading

  • DigitalOcean Functions has limited support. It does not provide logs by default, making it really hard to debug and test code. However, I will show you next time how we can deal with this.
  • The node-telegram-bot-api library does not have a suitable async function to process only one update from a Telegram webhook. A workaround is required to handle this.

Summary Link to heading

We have now completed the entire process, from creating the bot to launching it on DigitalOcean Functions. We have a working bot and a working template for deploying our bot in production.

In the next post, we will make our bot “smart” using the OpenAI API. Additionally, we will address the lack of default logging on DigitalOcean Functions. Stay tuned!