Мой опыт создания телеграм-бота на NodeJS/grammY

от автора

Арест Павла Дурова стал настолько ярким событием, что мне пришлось повнимательнее присмотреться к этому мессенджеру — чем же таким он значимо отличается от остальных социальных сетей. Так в поле моего зрения попали боты. Так-то я больше по веб-приложениям — ну, тех, что в браузере. Но боты тоже оказались ничего так.

Так как я предпочитаю использовать JavaScript и на фронте, и на бэке, то среда существования для бота была определена сразу же — nodejs. Осталось определиться с библиотекой — Telegraf или grammY? Так как у второй в примере использовался кошерный import, а у первой — старомодный require, я выбрал grammY.

Под катом — пример телеграм-бота в виде nodejs-приложения с использованием библиотеки grammY, который запускается как в режиме long polling, так и в режиме webhook, созданный с применением моей любимой технологии — внедрения зависимостей через конструктор (TL;DR).

Общая схема взаимодействия

Чуть покопавшись в описаниях того, что такое боты и с чем их едят, пришёл в восторг. Телеграм уверенно идёт по пути создания супер-аппа (по образу китайского WeChat). Сформировав первоначально базу пользователей, Телеграм теперь даёт возможность всем подряд добавить недостающую им функциональность посредством ботов (и мини-аппов).

С точки зрения веб-разработчика можно представить, что телеграм-клиент — это своего рода браузер с усечёнными возможностями по отображению информации. Взаимодействие пользователя с этим «браузером» строится по принципу чата — отправил какую-то информацию в чат, получил какую-то информацию из чата. Вместо широкого набора возможностей Web API обычного браузера Телеграм предлагает свой вариант — Telegram API. При этом у всех «браузеров» (телеграм-клиентов) есть один общий шлюз (телеграм-сервер), через который они могут общаться с внешним миром (ботами), а внешний мир (боты) — с «браузерами» (телеграм-клиентами).

Скрытый текст

Если проводить аналогию с реальными браузерами, то сразу же вспоминается Web Push API. Пользователь разрешает в браузере получение push-уведомлений, после чего браузер регистрирует разрешение и связывается со своим push-сервером, регистрируя endpoint для доставки сообщений. Этот endpoint пользователь отправляет на бэк, где он и сохраняется (сплошная линия на диаграмме внизу). Чтобы бэк мог отправить сообщение пользователю в браузер, бэк должен для начала отправить сообщение на push-сервер, пользуясь сохранённым endpoint’ом. В endpoint’е различным браузерам соответствует различный push-сервер:

  • Chrome: fcm.googleapis.com

  • Safari: web.push.apple.com

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

Бэкенд стороннего сервиса отправляет сообщение на push-сервер браузера (пунктирная линия), и этот сервер уже перенаправляет уведомление в соответствующий браузер на основании зарегистрированного endpoint (если браузер запущен, разумеется).

Web Push API

Web Push API

По сути, в Телеграме улучшили Web Push API, значительно усложнив формат передаваемых сообщений и дав возможность пользователю «браузера» (телеграм-клиента) не только получать сообщения через шлюз (телеграм-сервер), но и отправлять их. Внешние сервисы, с которыми пользователь может взаимодействовать через шлюз посредством своей клиентской программы в мобильном устройстве (или в компьютере) и которые со своей стороны могут взаимодействовать с пользователем, называются ботами.

Схема подключения бота

Схема подключения бота

Телеграм-бот может быть подключен к шлюзу (телеграм-серверу) в одном из двух режимов:

  • long polling: бот работает на любом компьютере (десктоп, ноутбук, сервер) и сам опрашивает шлюз на предмет новых сообщений от пользователей.

  • webhook: бот работает на веб-сервере и способен принимать сообщения от шлюза по HTTPS-соединению.

Библиотека grammY поддерживает оба режима. Long polling удобен для разработки и для проектов с низкой загрузкой, webhook — для высоконагруженных проектов.

Регистрация бота

Про регистрацию написано много (раз, два, три). Всё сводится к тому, что нужно через бот @BotFather получить токен для подключения к API (шлюзу). Что-то типа такого:

2338700115:AAGKevlLXYhEnaYLВSyTjqcRkVQeUl8kiRo

Если токен действующий, то при его подстановке в этот адрес вместо {TOKEN}:

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

Телеграм возвращает информацию о боте:

{   "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   } }

Токен используется при создании бота в grammY:

import {Bot} from 'grammy';  const bot = new Bot(token, opts);

Я не буду в этом посте описывать, как создавать nodejs-приложение, подключать npm-пакеты и т.п. — остановлюсь на принципиальных моментах, связанных с ботами.

Добавление команд

Командой для бота в Телеграме считается строка, начинающаяся с /. Есть три команды, наличие которых ожидается для каждого бота:

  • /start: начало взаимодействия пользователя с ботом.

  • /help: запрос пользователя на получение справки о работе с ботом.

  • /settings: (если применимо) настройка бота пользователем.

Команды можно интерактивно добавлять на телеграм-клиенте через @BotFather , но более правильным, на мой взгляд, является добавление команд через бота при его запуске:

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

В этом случае можно варьировать описание команд в зависимости от предпочтений пользователя (например, выбранного пользователем языка общения).

Добавление обработчиков

После создания бота и добавления к нему списка команд к боту добавляются обработчики, реагирующие на эти самые команды, и обработчики, реагирующие на другие события, не являющися командами (новое сообщение, реакция на предыдущее сообщение, редактирование сообщения, отправка файла и т.п.):

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) => {});

Список команд мы определяем сами, а вот список «других событий» («фильтров» в терминологии grammY) формируется более извилистым путём. Тем не менее, суть обработчиков («middleware» в терминах grammY) в обоих случаях примерно одинакова — получить на вход контекст запроса (ctx), отреагировать на поступившую информацию, сформировать ответ и отправить его пользователю:

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

Этой информации уже достаточно для того, чтобы сделать простого бота, реагирующего на текстовые команды, отправляемые через телеграм-клиента.

Запуск бота в режиме long polling

Тут всё просто:

const bot = new Bot(token, opts1); await bot.api.setMyCommands(commands, opts2); bot.command('...', (ctx) => {}); bot.on('...', (ctx) => {});  // start the bot in the long polling mode bot.start(opts3).catch((e) => {});

Всё, бот работает прямо с вашего ноутбука/десктопа/сервера, опрашивает телеграм-шлюз на предмет сообщений, поступивших для бота, обрабатывает их и отправляет обратно. Можно в таком же виде запустить бот на каком-нибудь VPS или выкатить на какую другую площадку.

Коротко о режиме webhook

Режим «webhook» — это запуск бота «по-взрослому«. В этом режиме бот при старте связывается с телеграм-шлюзом и сообщает ему свой адрес, на который шлюз будет присылать боту сообщения от пользователей по мере их появления. Что-то типа:

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

Сообщения присылаются в виде HTTP POST запросов:

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

Сразу понятно, что бот в этом режиме должен представлять из себя HTTPS-сервер.

Сама библиотека grammY не является таким сервером, но предоставляет адаптеры для подключения бота к популярным веб-серверам в nodejs. Вот пример подключения бота к express:

import {Bot, webhookCallback} from 'grammy';  const app = express(); const bot = new Bot(token);  app.use(webhookCallback(bot, 'express'));

В webhook-режиме запускается непосредственно веб-сервер, который перенаправляет webhook’у HTTP-запросы, приходящие от телеграм-шлюза. Webhook извлекает входные данные из запроса при помощи адаптера, передаёт их боту на обработку, принимает результат от бота и возвращает результат обработки обратно в телеграм-шлюз.

Архитектура приложения

Библиотека grammY создана для адаптации телеграм-шлюза к nodejs-приложениям в очень широком спектре применения, но её функционал всё равно нуждается в дополнительной доработке согласно конкретным бизнес-требованиям. Вот, что мне нужно в общем случае (для любого бота):

  • возможность запуска/останова бота как в режиме long polling, так и в режиме webhook, локально или на виртуальном сервере;

  • считывание конфигурации бота из внешнего источника (файл или переменные окружения);

  • добавление списка доступных команд и обработчиков для них при запуске бота;

  • регистрация адреса бота на телеграм-шлюзе при работе в режиме webhook;

  • запуск бота в режиме отдельного веб-сервера (с поддержкой HTTPS) или в режиме сервера приложений, спрятанного за прокси-сервером (nginx/apache).

Компоненты разработки

Компоненты разработки

Я сторонник архитектуры «модульный монолит«, соответственно, весь типовой код, который отвечает за общение приложения с телеграм-шлюзом, и его зависимости логично вынести в отдельный модуль (npm-пакет, типа @flancer32/teq-telegram-bot), а в бот-приложениях просто подключать этот модуль (вместе со всеми зависимостями) и имплементировать уже только бизнес-логику работы бота (обработку команд).

В бот-приложении, в файле package.json это описывается так:

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

Этот пакет, в свою очередь, должен тянуть все остальные зависимости, обеспечивающие работу бота, включая grammY.

npm-пакеты

В своих приложениях для связывания программных модулей я использую инверсию управления (IoC), а конкретно — внедрение зависимостей в конструкторе объектов. Реализация этого подхода — в моём собственном пакете @teqfw/di.

Для запуска nodejs-приложения из командной строки я использую пакет commander, который, в свою очередь, обёрнут в пакет @teqfw/core. В core-пакет таже ещё реализована настройка правил разрешения зависимостей в коде и загрузка конфигурации node-приложения из JSON-файла.

Я предпочитаю в своих приложениях использовать по максимуму node-модули, поэтому для всех трёх имплементаций веб-сервера в Node (HTTP, HTTP/2, HTTPS) сделал свою обёртку @teqfw/web, вместо того, чтобы использовать сторонние обёртки (express, fastify, koa, …)

Таким образом дерево зависимостей npm-пакетов в моём типовом бот-приложении (bot-app) можно отобразить так:

Дерево npm-пакетов

Дерево npm-пакетов
  • зелёное: grammy и его зависимости;

  • синее: веб-сервер, IoC и CLI;

  • жёлтое: npm-пакет, содержащий общую логику работы чат-бота (настройка бота и запуск бота в обоих режимах);

  • красное: бот-приложение, содержащее собственно сам бизнес-функционал бота.

Можно диаграмму дерева зависимостей представить в таком виде, скрыв все зависимости общего пакета:

Усечённое дерево зависимостей

Усечённое дерево зависимостей

Таким образом, достаточно прописать в зависимостях бот-приложения общий пакет, а всё остальное подтянется автоматом.

Общий npm-пакет

Пакет @flancer32/teq-telegram-bot реализует функционал, общий для всех ботов:

  • Загрузка конфигурации бота (токен) из внешних источников (JSON-файл).

  • Запуск node-приложения

    • в виде бота в режиме long polling.

    • в режиме webhook в виде веб-сервера (http & http/2 — как application-сервер за прокси сервером, https — как самостоятельный сервер).

  • Общие для всех ботов действия (инициализация списка команд при старте бота, регистрация webhook’а и т.п.).

  • Определяет точки расширения, в которых приложения могут добавлять собственную логику.

Варианты использования бот-библиотеки

Варианты использования бот-библиотеки

Консольные команды

В общем пакете реализованы и подключены две консольные команды, которые обрабатываются commander‘ом:

Запуск и останов бота в режиме webhook осуществляется средствами пакета @teqfw/web:

  • ./bin/tequila.mjs web-server-start

  • ./bin/tequila.mjs web-server-stop

Веб-запросы

В общем npm-пакете осуществляет только подключение обработчика веб-запросов (Telegram_Bot_Back_Web_Handler_Bot) для всех путей, начинающихся на https://.../telegram-bot.

Именно на этот адрес будет отправлять все запросы телеграм-шлюз и этот адрес регистрируется общей библиотекой на шлюзе при старте бот-приложения в webhook-режиме.

Конфигурация

Каждый плагин (npm-пакет) в моём модульном монолите может иметь свои собственные настройки (конфигурацию). Для своих приложений я сохраняю настройки в JSON-формате в файле ./etc/local.json. Шаблон настроек я обычно держу под контролем версий в файле ./etc/init.json.

В нашей общей библиотеке пока что есть только один конфигурационный параметр — токен для подключения к телеграм-шлюзу:

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

Для отражения структуры конфигурационных параметров в коде используется объект Telegram_Bot_Back_Plugin_Dto_Config_Local.

Общие действия

На данный момент, помимо старта/останова, следующие действия являются общими для всех ботов:

  • инициализация библиотеки grammY токеном, считанным из конфигурации приложения.

  • инициализация списка команд бота через телеграм-шлюз при старте приложения.

  • добавление обработчиков на события (команды бота и другие события).

  • создание webhook-адаптера для интеграции с плагином @teqfw/web.

  • регистрация в телеграм-шлюзе endpoint’а бота при его старте в webhook-режиме.

Общие действия выполняются в объекте Telegram_Bot_Back_Mod_Bot.

API

Я уже описывал ранее, каким образом можно использовать интерфейсы в чистом JavaScript. В общем npm-пакете определяется интерфейс объекта, который должен быть имплементирован в бот-приложении — Telegram_Bot_Back_Api_Setup:

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

Общий пакет не знает, какие конкретно команды будут в бот-приложении и какие обработчики событий, но он ожидает от контейнера объектов такую зависимость, которая даст возможность модели Telegram_Bot_Back_Mod_Bot проинициализировать при старте приложения и список команд, и обработчики событий.

Внедрение имплементации вместо интерфейса

Внедрение имплементации вместо интерфейса

Бот-приложение

Так как базовый функционал для работы с телеграм-шлюзом у нас расположен во внешних библиотеках (grammY, @teqfw/di, @tefw/core, @tefw/web), то в коде бот-приложения нам остаётся лишь добавить собственно бизнес-логику самого бота и связующий код, который позволит контейнеру объектов корректно создать и внедрить нужные зависимости.

Для этого в минимуме нужно 5 файлов:

  • ./package.json: дескритор npm-пакета.

  • ./teqfw.json: дескриптор teq-приложения.

  • имплементация интерфейса Telegram_Bot_Back_Api_Setup: основной файл бот-приложения в котором к боту привязывается кастомная бизнес-логика.

  • ./cfg/local.json: локальная конфигурация бот-приложения (содержит токен).

  • ./bin/tequila.mjs: стартер приложения.

package.json

Архитектура «модульный монолит» подразумевает, что приложение, хоть и модульное, но собирается воедино. Для nodejs/npm приложений главным файлом является package.json. Для нашего приложения интересным является конфигурация выполняемых npm-команд и зависимости:

// ./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

Файл ./teqfw.json позволяет нашему npm-пакету, соответствующему бот-приложению, использовать возможности Контейнера Объектов @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"       }     }   } }

Инструкции предписывают Контейнеру искать модули с префиксом Demo в каталоге ./src/, а для внедрения объекта с интерфейсом Telegram_Bot_Back_Api_Setup использовать es6-модуль Demo_Back_Di_Replace_Telegram_Bot_Back_Api_Setup.

Такое длинное для имплементации имя обусловлено моими субъективными предпочтениями в органзиации структуры каталогов в своих приложениях. Вполне можно было бы обойтись и таким именем: Demo_Bot_Setup.

Имплементация интерфейса

В моём примере я вынес обработчики событий в отдельные es6-модули и оставил в имплементации только добавление команд и обработчиков к боту:

/**  * @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;         };     } } 

Обработчики событий

Все обработчики команд у меня находятся в пространстве Demo_Back_Bot_Cmd, а обработчики прочих событий (фильтры) — в пространстве Demo_Back_Bot_Filter. Код типового обработчика:

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);         };     } }

Смысл обработки сводится к получению из контекста запроса необходимой информации, формированию на её основе управляющего воздействия и к созданию ответа с результатом.

Как правило, обработчик содержит гораздо больше кода, чем приведено в примере, поэтому рационально выносить его код в отдельный файл (или даже в группу файлов).

Головной файл

В npm-пакете бот-приложения также должен находиться и файл, представляющий из себя nodejs-приложение для запуска бота. Вот код этого файла:

#!/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));

Код одинаков для всех приложений и, полагаю, может быть внесён в @teqfw/core, но пока что лично я размещаю его в файле ./bin/tequila.mjs.

Локальная конфигурация приложения

Приложение на базе Tequila Framework ищет локальную конфигурацию в файле ./cfg/local.json. В нашем случае в этом файле должны лежать настройки подключения к телеграм-шлюзу и настройки работы веб-сервера:

{   "@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"   } }

В принципе, можно считывать конфигурацию и из переменных окружения, но мне удобнее вот так.

Запуск бота с самоподписным сертификатом

Про запуск бота в режиме webhook есть замечательный материал на английском языке — Marvin’s Marvellous Guide. В этом пункте я просто опишу команды, которые позволяют запустить на виртуальном сервере бот-приложение в режиме веб-сервера (webhook).

Создание сертификата

Подробное описание процесса создания — здесь.

$ 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"

В результате будет создано два файла в каталоге ./etc/:

  • ./etc/bot.key

  • ./etc/bot.pem

Конфигурация веб-сервера

В локальной конфигурации приложения (файл ./cfg/local.json) нужно прописать пути к ключу и сертификату, а также доменное имя для бота и порт, который слушает бот:

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

Запуск бота в режиме webhook

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

Посмотреть состояние бота в этом режиме:

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"   } }

Пример работы бота

Подключиться к боту в телеграм-клиенте можно здесь — flancer64_demo_grammy_bot.

Работа бота на ru-локали

Работа бота на ru-локали

Заключение

Спасибо всем, кто промотал статью до этого места — мне самому бывает влом читать длинные портянки текста и просто интересно, чем это всё закончится. Тем же, кто дочитал до заключения, пусть и вполглаза — мой искренний респект!

После ознакомления с основами ботостроения в Телеграм я пришёл к выводу, что Web 3.0 вполне себе можно построить не на браузерах, а на вот таких вот клиентах с упрощённым интерфейсом взаимодействия с пользователем (текстовые сообщения, возможно, с голосовым набором, плюс пересылка файлов) и широкой сетью ботов, взаимодействующих друг с другом.

P.S.

КДПВ создана Dall-E через браузерное приложение (исходники).


ссылка на оригинал статьи https://habr.com/ru/articles/837610/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *