Telegram bots with Node.js
Telegram bots, besides regular chatbots, can also be used as "dashboards". Authentication is already there, as well as the UI to send commands to receive specific data. One additional option is to implement a cronjob that executes some commands and sends a response to the user. Also setting up the bot is much faster compared to other platforms.
Prerequisites
- Telegram app installed
Telegram setup
Find BotFather
account and send /newbot
command. Choose name and username, after that you'll get an access token for a newly created bot. To add default commands for it, send /setcommands
command and send commands as it's stated in the instructions.
Setup for development environment
Bootstrapping bot
Bottender is a great framework for developing bots, it supports multiple platforms. Start with the following command and choose platform and session store. Update .env
file with the access token that BotFather
sent.
npx create-bottender-app <bot-name>cd <bot-name>npm i express body-parser cron ngrok shelljs pinonpm i nodemon -D
Publicly exposed URL
Telegram bots require publicly available URLs for the webhooks. Ngrok creates a secure public URL pointing to the local server while Bottender enables webhook integrations. To automate this process, a custom server has to be implemented.
Server setup
Bellow implementation starts the cronjob and custom server with an automated connection to the tunnel and webhook URL setup.
// server.jsconst { bottender } = require('bottender');const ngrok = require('ngrok');const shell = require('shelljs');const { setupCustomServer } = require('./src/custom-server');const { logger } = require('./src/logger');const { setupScheduler } = require('./src/scheduler');const app = bottender({dev: process.env.NODE_ENV !== 'production',});const setWebhookUrl = (url) =>shell.exec(`npm run telegram-webhook:set ${url}/webhooks/telegram`);const connectToTunnel = async (port) => {const url = await ngrok.connect({addr: port,onStatusChange: (status) => {switch (status) {case 'connected': {logger.info('Connected to tunnel...');break;}case 'closed': {logger.warn('Connection to tunnel is closed...');logger.info('Reconnecting...');return connectToTunnel(port);}}},});setWebhookUrl(url);};(async () => {try {await app.prepare();const port = Number(process.env.PORT) || 5000;setupCustomServer(app, port);if (process.env.NODE_ENV !== 'production') {await connectToTunnel(port);}setupScheduler();} catch (error) {logger.error(error, 'Setting up failed...');}})();
Custom server implementation for the webhook
// src/custom-server.jsconst bodyParser = require('body-parser');const express = require('express');const { logger } = require('./logger');const setupCustomServer = (app, port) => {// the request handler of the bottender appconst handle = app.getRequestHandler();const server = express();const verify = (req, _, buf) => {req.rawBody = buf.toString();};server.use(bodyParser.json({ verify }));server.use(bodyParser.urlencoded({ extended: false, verify }));// route for webhook requestserver.all('*', (req, res) => {return handle(req, res);});server.listen(port, (err) => {if (err) throw err;logger.info(`Ready on http://localhost:${port}`);});};module.exports = {setupCustomServer,};
Custom scheduler
// src/scheduler.jsconst { getClient } = require('bottender');const { CronJob } = require('cron');const { CHAT_ID, CRONJOB_INTERVAL, replyMarkup, TIMEZONE } = require('./constants');const { executeCustomCommand } = require('./services');const client = getClient('telegram');const setupScheduler = () =>new CronJob(CRONJOB_INTERVAL,async function () {const response = await executeCustomCommand();await client.sendMessage(CHAT_ID, response, {parseMode: 'HTML',replyMarkup,});},null,true,TIMEZONE,);module.exports = {setupScheduler,};
Npm scripts
// package.json{// ..."scripts": {"dev": "nodemon server.js","lint": "eslint . ","lint:fix": "npm run lint -- --fix","start": "node server.js","telegram-webhook:set": "echo 'Y' | bottender telegram webhook set -w $1","test": "jest"}// ...}
Linter
Update ecmaVersion
field in eslint config to 2021
.
Logger
const logger = require('pino')();module.exports = {logger,};
Bot development
Below is the bot entry point, multiple handlers can be specified.
// src/index.jsconst { router, telegram } = require('bottender/router');const { HandleMessage } = require('./handlers/message-handler');module.exports = async function App() {return router([telegram.message(HandleMessage)]);};
Bellow is basic message handler implementation. Restricting the access to the bot can be done by chat id. For a message with HTML content, parseMode
parameter should be set to HTML
. Quick replies can be added in replyMarkup
field. Received bot commands have a type bot_command
.
// src/handlers/message-handler.jsconst { ADMIN_CHAT_ID } = require('../constants');const { handleCustomLogic } = require('../services');async function HandleMessage(context) {const chatId = context.event._rawEvent.message?.chat?.id;if (chatId !== ADMIN_CHAT_ID) {await context.sendMessage('Access denied!');return;}const isBotCommand = !!context.event._rawEvent.message?.entities?.find((entity) => entity.type === 'bot_command');const message = isBotCommand? context.event.text.replace('/', ''): context.event.text;const response = await handleCustomLogic(message);await context.sendMessage(response, {parseMode: 'HTML',replyMarkup: {keyboard: [[{text: '/command',},],],},});}module.exports = {HandleMessage,};
Error handling
Define the custom error handler in the _error.js
file.
const { logger } = require('./src/logger');module.exports = async (context, props) => {logger.error(props.error);await context.sendMessage('There are some unexpected errors that happened. Please try again later. Sorry for the inconvenience.');};
Deployment
One of the options to deploy a Telegram bot is fly.io running the following commands for the setup and deployment.
curl -L https://fly.io/install.sh | shfly auth signupfly launchfly deploy --no-cachefly secrets set TELEGRAM_ACCESS_TOKEN=<ACCESS_TOKEN>npx bottender telegram webhook set -w https://<PROJECT_NAME>.fly.dev/webhooks/telegram
More details about the deployment are covered in Deploying Node.js apps to Fly.io post.
Boilerplate
Here is the link to the boilerplate I use for the development.