My experience creating a telegram bot on NodeJS

The arrest of Pavel Durov was such a striking event that I had to take a closer look at this messenger – how is it significantly different from other social networks. That's how bots came into my field of vision. Actually, I'm more into web applications – well, those in the browser. But the bots turned out to be pretty good too.

Since I prefer to use JavaScript both on the front and on the back, the environment for the bot was immediately determined – nodejs. It remains to decide on the library – Telegraph or grammY? Since the second one in the example used kosher importand the first one is old-fashioned requireI chose grammY.

Below is an example of a telegram bot in the form of a nodejs application using the library grammYwhich is launched in both mode long poolingand in the mode webhookcreated using my favorite technique – constructor-based dependency injection (TL;DR).

General scheme of interaction

Having dug a little into the descriptions of what bots are and what they are eaten with, I was delighted. Telegram is confidently moving along the path of creating a super-app (in the image of the Chinese WeChat). Having initially formed a user base, Telegram now gives everyone the opportunity to add the functionality they lack through bots (and mini-apps).

From a web developer's point of view, you can imagine that the Telegram client is a kind of browser with limited capabilities for displaying information. The user's interaction with this “browser” is built on the principle of a chat – sent some information to the chat, received some information from the chat. Instead of a wide range of possibilities Web API Telegram offers its own version of a regular browser – Telegram API. At the same time, everyone “browsers” (Telegram clients) have one common gateway (Telegram server), through which they can communicate with the outside world (bots), and the outside world (bots) – with “browsers” (Telegram clients).

Hidden text

If we draw an analogy with real browsers, then we immediately remember Web Push API. The user allows the browser to receive push notifications, after which the browser registers the permission and contacts its push server, registering an endpoint for message delivery. The user sends this endpoint to the backend, where it is saved (solid line in the diagram below). In order for the backend to send a message to the user in the browser, the backend must first send a message to the push server using the saved endpoint. In the endpoint, different browsers correspond to different push servers:

  • Chrome: fcm.googleapis.com

  • Safari: web.push.apple.com

  • Firefox: updates.push.services.mozilla.com

The third-party service backend sends a message to the browser push server (dotted line), and this server then forwards the notification to the appropriate browser based on the registered endpoint (if the browser is running, of course).

Web Push API

Web Push API

Essentially, Telegram has improved the Web Push API, significantly complicating the format of transmitted messages and giving the user the ability to “browser” (Telegram client) not only receive messages through the gateway (Telegram server), but also send them. External services with which the user can interact through the gateway via his client program on a mobile device (or computer) and which, for their part, can interact with the user, are called bots.

Bot connection diagram

Bot connection diagram

The Telegram bot can be connected to a gateway (Telegram server) in one of two modes:

  • long pooling: the bot works on any computer (desktop, laptop, server) and polls the gateway for new messages from users.

  • webhook: the bot runs on a web server and is able to receive messages from the gateway via an HTTPS connection.

Library grammY supports both modes. Long pooling convenient for development and for projects with low load, webhook – for high-load projects.

Bot registration

Much has been written about registration (oncetwo, three). It all comes down to the fact that you need to use a bot @BotFather get a token to connect to the API (gateway). Something like this:

2338700115:AAGKevlLXYhEnaYLВSyTjqcRkVQeUl8kiRo

If the token is valid, then when it is substituted into this address instead of {TOKEN}:

https://api.telegram.org/bot{TOKEN}/getMe

Telegram returns information about the bot:

{
  "ok": true,
  "result": {
    "id": 2338700115,
    "is_bot": true,
    "first_name": "...",
    "username": "..._bot",
    "can_join_groups": true,
    "can_read_all_group_messages": false,
    "supports_inline_queries": false,
    "can_connect_to_business": false,
    "has_main_web_app": false
  }
}

The token is used when creating a bot in grammY:

import {Bot} from 'grammy';

const bot = new Bot(token, opts);

I will not describe in this post how to create a nodejs application, connect npm packages, etc. – I will focus on the fundamental points related to bots.

Adding commands

A command for a bot in Telegram is considered to be a line starting with /There are three commands, the presence of which expected for each bot:

  • /start: the beginning of the user's interaction with the bot.

  • /help: user request for help with working with the bot.

  • /settings: (if applicable) bot customization by the user.

Commands can be added interactively on the Telegram client via @BotFather but more correct, in my opinion, is to add commands through the bot when it is launched:

const cmdRu = [
    {command: '...', description: '...'},
];
await bot.api.setMyCommands(cmdRu, {language_code: 'ru'});

In this case, you can vary the description of commands depending on the user's preferences (for example, the user's chosen language of communication).

Adding handlers

After creating a bot and adding a list of commands to it, handlers are added to the bot that respond to these same commands, and handlers that respond to other events that are not commands (new message, reaction to the previous message, editing a message, sending a file, etc.):

const bot = new Bot(token, opts1);
const commands = [
    {command: '...', description: '...'},
];
await bot.api.setMyCommands(commands, opts2);
// add the command handlers
bot.command('help', (ctx) => {});
// add the event handlers
bot.on('message:file', (ctx) => {});

We define the list of commands ourselves, but here is the list of “other events” (“filters” in terminology grammY) is formed more by a winding path. However, the essence of handlers (“middleware” in terms of grammY) in both cases is approximately the same – get the request context as input (ctx), respond to the received information, generate a response and send it to the user:

const middleware = function (ctx) {
    const msgIn = ctx.message;
    // ...
    const msgOut="...";
    ctx.reply(msgOut, opts).catch((e) => {});
};

This information is already enough to make a simple bot that responds to text commands sent through a telegram client.

Launching the bot in long pooling mode

Everything is simple here:

const bot = new Bot(token, opts1);
await bot.api.setMyCommands(commands, opts2);
bot.command('...', (ctx) => {});
bot.on('...', (ctx) => {});

// start the bot in the long pooling mode
bot.start(opts3).catch((e) => {});

That's it, the bot works directly from your laptop/desktop/server, queries the telegram gateway for messages received for the bot, processes them and sends them back. You can run the bot in the same form on some VPS or roll it out to some another site.

Briefly about webhook mode

Mode “webhook” – this is the launch of the bot “like an adult“. In this mode, the bot, when starting, contacts the telegram gateway and tells it its address, to which the gateway will send messages from users to the bot as they appear. Something like:

https://grammy.demo.tg.wiredgeese.com/bot/

Messages are sent as HTTP POST requests:

POST /bot/ HTTP/1.1
Host: grammy.demo.tg.wiredgeese.com
Content-Type: application/json
...

It is immediately clear what the bot in this mode should look like HTTPS server.

The library itself grammY is not such a server, but provides adapters to connect the bot to popular web servers in nodejs. Here is an example of connecting the bot to express:

import {Bot, webhookCallback} from 'grammy';

const app = express();
const bot = new Bot(token);

app.use(webhookCallback(bot, 'express'));

In webhook mode, the web server is launched directly, which redirects HTTP requests coming from the telegram gateway to the webhook. The webhook extracts input data from the request using an adapter, passes it to the bot for processing, accepts the result from the bot and returns the processing result back to the telegram gateway.

Application architecture

Library grammY created to adapt the telegram gateway to nodejs applications in a very wide range of applications, but its functionality still needs additional refinement according to specific business requirements. Here's what I need in general (for any bot):

  • the ability to start/stop the bot as in the mode long poolingand in the mode webhooklocally or on a virtual server;

  • reading bot configuration from an external source (file or environment variables);

  • adding a list of available commands and handlers for them when starting the bot;

  • registering the bot address on the telegram gateway when working in the mode webhook;

  • running the bot in standalone web server mode (with HTTPS support) or in application server mode hidden behind a proxy server (nginx/apache).

Development Components

Development Components

I am a supporter of architecture”modular monolith“, accordingly, all the standard code that is responsible for the application's communication with the telegram gateway and its dependencies should logically be moved to a separate module (npm package, like @flancer32/teq-telegram-bot), and in bot applications, simply connect this module (along with all dependencies) and implement only the business logic of the bot (command processing).

In the bot application, in the file package.json it is described like this:

{
  "dependencies": {
    "@flancer32/teq-telegram-bot": "github:flancer32/teq-telegram-bot",
    ...
  }
}

This package, in turn, must pull all other dependencies that ensure the bot works, including grammY.

npm packages

In my applications, I use inversion of control (IoC), and specifically – dependency injection in the object constructor. The implementation of this approach is in my own package @teqfw/di.

To run nodejs application from command line I use package commanderwhich, in turn, is wrapped in a bag @teqfw/coreThe core package also implements the configuration of dependency resolution rules in the code and loading the node application configuration from a JSON file.

I prefer to use node modules as much as possible in my applications, so for all three implementations of the web server in Node (HTTP, HTTP/2, HTTPS) made my own wrapper @teqfw/webinstead of using third-party wrappers (express, fastify, koa…)

So the tree of npm package dependencies in my typical bot application (bot-app) can be displayed like this:

npm package tree

npm package tree

  • green: grammy and its dependencies;

  • blue: web server, IoC and CLI;

  • yellow: npm package containing the general logic of the chatbot (configuring the bot and launching the bot in both modes);

  • red: a bot application that contains the actual business functionality of the bot.

You can represent the dependency tree diagram like this, hiding all dependencies of the common package:

Truncated dependency tree

Truncated dependency tree

Thus, it is enough to register a common package in the bot application dependencies, and everything else will be pulled in automatically.

Common npm package

Plastic bag @flancer32/teq-telegram-bot implements functionality common to all bots:

  • Loading bot configuration (token) from external sources (JSON file).

  • Launching a node application

    • as a bot in mode long pooling.

    • in mode webhook as a web server (http & http/2 – as an application server behind a proxy server, https – as a standalone server).

  • Actions common to all bots (initialization of the command list when the bot starts, registration of a webhook, etc.).

  • Defines extension points where applications can add their own logic.

Bot library use cases

Bot library use cases

Console commands

The overall package also includes: connected two console commands that are processed commander'om:

Starting and stopping the bot in mode webhook is carried out by means of the package @teqfw/web:

Web requests

In the general npm package it only implements connection web request handler (Telegram_Bot_Back_Web_Handler_Bot) for all paths starting with https://.../telegram-bot.

It is to this address that the telegram gateway will send all requests, and this address is registered by the shared library on the gateway when the bot application starts. webhook-mode.

Configuration

Each plugin (npm package) in my modular monolith can have its own settings (configuration). For my applications, I store the settings in JSON format in a file ./etc/local.json. I usually keep the settings template under version control in a file ./etc/init.json.

In our common library there is only one configuration parameter so far – a token for connecting to the telegram gateway:

{
  "@flancer32/teq-telegram-bot": {
    "apiKeyTelegram": "..."
  }
}

To reflect the structure of configuration parameters in the code, an object is used Telegram_Bot_Back_Plugin_Dto_Config_Local.

General actions

At the moment, besides start/stop, the following actions are common to all bots:

  • library initialization grammY token read from the application configuration.

  • initialization of the bot command list via the telegram gateway when the application starts.

  • adding handlers to events (bot commands and other events).

  • creating a webhook adapter for integration with the plugin @teqfw/web.

  • registration in the telegram gateway of the bot's endpoint when it starts in webhook-mode.

Common actions are performed in the object Telegram_Bot_Back_Mod_Bot.

API

I have already described how interfaces can be used in pure JavaScript. In the general npm package, an object interface is defined that should be implemented in the bot application – Telegram_Bot_Back_Api_Setup:

/**
 * @interface
 */
class Telegram_Bot_Back_Api_Setup {
  
  async commands(bot) {}
  
  handlers(bot) {}

}

The general package does not know what specific commands will be in the bot application and what event handlers, but it expects such a dependency from the object container that will enable the model Telegram_Bot_Back_Mod_Bot Initialize both the command list and event handlers when the application starts.

Implementation instead of interface

Implementation instead of interface

Bot application

Since the basic functionality for working with the telegram gateway is located in external libraries (grammY, @teqfw/di, @tefw/core, @tefw/web), then in the bot application code we only need to add the actual business logic of the bot itself and the connecting code that will allow the object container to correctly create and implement the necessary dependencies.

For this you need at least 5 files:

  • ./package.json: npm package handle.

  • ./teqfw.json: teq application handle.

  • interface implementation Telegram_Bot_Back_Api_Setup: the main file of the bot application in which custom business logic is attached to the bot.

  • ./cfg/local.json: local configuration of the bot application (contains the token).

  • ./bin/tequila.mjs: application starter.

package.json

Architecture “modular monolith” implies that the application, although modular, is assembled together. For nodejs/npm applications, the main file is package.json. For our application, the interesting thing is the configuration of the executed npm commands and dependencies:

// ./package.json
{
  "scripts": {
    "help": "node ./bin/tequila.mjs -h",
    "start": "node ./bin/tequila.mjs tg-bot-start",
    "stop": "node ./bin/tequila.mjs tg-bot-stop",
    "web-start": "node ./bin/tequila.mjs web-server-start",
    "web-stop": "node ./bin/tequila.mjs web-server-stop"
  },
  "dependencies": {
    "@flancer32/teq-telegram-bot": "github:flancer32/teq-telegram-bot"
  },
}

teqfw.json

File ./teqfw.json allows our npm package corresponding to the bot application to use the capabilities of the Object Container @teqfw/di:

{
  "@teqfw/di": {
    "autoload": {
      "ns": "Demo",
      "path": "./src"
    },
    "replaces": {
      "Telegram_Bot_Back_Api_Setup": {
        "back": "Demo_Back_Di_Replace_Telegram_Bot_Back_Api_Setup"
      }
    }
  }
}

The instructions tell the Container to look for modules with the prefix Demo in the catalog ./src/and to implement an object with an interface Telegram_Bot_Back_Api_Setup use es6 module Demo_Back_Di_Replace_Telegram_Bot_Back_Api_Setup.

The name, which is so long to implement, is due to my subjective preferences in organizing the directory structure in my applications. It would be quite possible to get by with a name like this: Demo_Bot_Setup.

Interface implementation

In my example, I moved the event handlers into separate es6 modules and left in implementation only adding commands and handlers to the bot:

/**
 * @implements {Telegram_Bot_Back_Api_Setup}
 */
export default class Demo_Back_Di_Replace_Telegram_Bot_Back_Api_Setup {
    constructor(
        {
            Demo_Back_Defaults$: DEF,
            TeqFw_Core_Shared_Api_Logger$$: logger,
            Demo_Back_Bot_Cmd_Help$: cmdHelp,
            Demo_Back_Bot_Cmd_Settings$: cmdSettings,
            Demo_Back_Bot_Cmd_Start$: cmdStart,
            Demo_Back_Bot_Filter_Message$: filterMessage,
        }
    ) {
        // VARS
        const CMD = DEF.CMD;

        // INSTANCE METHODS
        this.commands = async function (bot) {
            // добавляет команды и их описание на русском и английском языках
        };

        this.handlers = function (bot) {
            bot.command(CMD.HELP, cmdHelp);
            bot.command(CMD.SETTINGS, cmdSettings);
            bot.command(CMD.START, cmdStart);
            bot.on('message', filterMessage);
            return bot;
        };
    }
}

Event handlers

All my command handlers are in space Demo_Back_Bot_Cmdand handlers of other events (filters) – in space Demo_Back_Bot_Filter. Typical handler code:

export default class Demo_Back_Bot_Cmd_Start {
    constructor() {
        return async (ctx) => {
            const from = ctx.message.from;
            const msgDef="Start";
            const msgRu = 'Начало';
            const msg = (from.language_code === 'ru') ? msgRu : msgDef;
            // https://core.telegram.org/bots/api#sendmessage
            await ctx.reply(msg);
        };
    }
}

The meaning of processing comes down to obtaining the necessary information from the context of the request, forming a control action on its basis, and creating a response with the result.

As a rule, the handler contains much more code than is given in the example, so it is rational to put its code in a separate file (or even in group of files).

Head file

The npm package of the bot application should also contain a file representing a nodejs application for running the bot. Here is the code for this file:

#!/usr/bin/env node
'use strict';
import {dirname, join} from 'node:path';
import {fileURLToPath} from 'node:url';
import teq from '@teqfw/core';

const url = new URL(import.meta.url);
const script = fileURLToPath(url);
const bin = dirname(script);
const path = join(bin, '..');

teq({path}).catch((e) => console.error(e));

The code is the same for all applications and I believe can be included in @teqfw/corebut for now I personally place it in a file ./bin/tequila.mjs.

Local application configuration

Tequila Framework based application looks for local configuration in file ./cfg/local.json. In our case, this file should contain the settings for connecting to the telegram gateway and the settings for the web server:

{
  "@flancer32/teq-telegram-bot": {
    "apiKeyTelegram": "..."
  },
  "@teqfw/web": {
    "server": {
      "secure": {
        "cert": "path/to/the/cert",
        "key": "path/to/the/key"
      },
      "port": 8483
    },
    "urlBase": "virtual.server.com"
  }
}

In principle, you can read the configuration from environment variables, but I find it more convenient this way.

Launching a bot with a self-signed certificate

About launching the bot in mode webhook There is some great material in English – Marvin's Marvellous Guide. In this section I will simply describe the commands that allow you to run a bot application in web server mode on a virtual server (webhook).

Creating a certificate

Detailed description of the creation process – Here.

$ mkdir etc
$ cd ./etc
$ openssl req -newkey rsa:2048 -sha256 -nodes -keyout bot.key \
    -x509 -days 3650 -out bot.pem \
    -subj "/C=LV/ST=Riga/L=Bolderay/O=Test Bot/CN=grammy.demo.tg.teqfw.com"

This will create two files in the ./etc/ directory:

  • ./etc/bot.key

  • ./etc/bot.pem

Web server configuration

In the local configuration of the application (file ./cfg/local.json), you need to specify the paths to the key and certificate, as well as the domain name for the bot and the port that the bot listens to:

  "@teqfw/web": {
    "server": {
      "secure": {
        "cert": "etc/bot.pem",
        "key": "etc/bot.key"
      },
      "port": 8443
    },
    "urlBase": "grammy.demo.tg.teqfw.com:8443"
  }

Launching the bot in webhook mode

$ npm run web-start
...
...: Web server is started on port 8443 in HTTPS mode (without web sockets).
...
$ npm run web-stop

View the bot's status in this mode:

https://api.telegram.org/bot{TOKEN}/getWebhookInfo
{
  "ok": true,
  "result": {
    "url": "https://grammy.demo.tg.teqfw.com:8443/telegram-bot",
    "has_custom_certificate": true,
    "pending_update_count": 0,
    "last_error_date": 1725019662,
    "last_error_message": "Connection refused",
    "max_connections": 40,
    "ip_address": "167.86.94.59"
  }
}

Example of the bot working

You can connect to the bot in the telegram client here – flancer64_demo_grammy_bot.

Bot work in ru-locale

Bot work in ru-locale

Conclusion

Thanks to everyone who has scrolled this far – I myself sometimes find it too lazy to read long pages of text and am simply curious about how it will all end. My sincere respect to those who have read to the conclusion, even if only half-heartedly!

After getting acquainted with the basics of bot building in Telegram, I came to the conclusion that Web 3.0 It is entirely possible to build not on browsers, but on clients like these with a simplified user interface (text messages, possibly with voice dialing, plus file transfer) and a wide network of bots interacting with each other.

P.S.

KDPV was created by Dall-E through browser application (sources).

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *