Арест Павла Дурова стал настолько ярким событием, что мне пришлось повнимательнее присмотреться к этому мессенджеру — чем же таким он значимо отличается от остальных социальных сетей. Так в поле моего зрения попали боты. Так-то я больше по веб-приложениям — ну, тех, что в браузере. Но боты тоже оказались ничего так.
Так как я предпочитаю использовать JavaScript и на фронте, и на бэке, то среда существования для бота была определена сразу же — nodejs. Осталось определиться с библиотекой — Telegraf или grammY? Так как у второй в примере использовался кошерный import
, а у первой — старомодный require
, я выбрал grammY
.
Под катом — пример телеграм-бота в виде nodejs-приложения с использованием библиотеки grammY
, который запускается как в режиме long polling
, так и в режиме webhook
, созданный с применением моей любимой технологии — внедрения зависимостей через конструктор (TL;DR).
-
демо-бот: flancer64_demo_grammy_bot
-
исходный код бот-приложения: @flancer64/tg-bot-habr-demo-grammy
-
исходный код общей библиотеки: @flancer32/teq-telegram-bot
Общая схема взаимодействия
Чуть покопавшись в описаниях того, что такое боты и с чем их едят, пришёл в восторг. Телеграм уверенно идёт по пути создания супер-аппа (по образу китайского 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, значительно усложнив формат передаваемых сообщений и дав возможность пользователю «браузера» (телеграм-клиента) не только получать сообщения через шлюз (телеграм-сервер), но и отправлять их. Внешние сервисы, с которыми пользователь может взаимодействовать через шлюз посредством своей клиентской программы в мобильном устройстве (или в компьютере) и которые со своей стороны могут взаимодействовать с пользователем, называются ботами.
Телеграм-бот может быть подключен к шлюзу (телеграм-серверу) в одном из двух режимов:
-
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
) можно отобразить так:
-
зелёное:
grammy
и его зависимости; -
синее: веб-сервер, IoC и CLI;
-
жёлтое: npm-пакет, содержащий общую логику работы чат-бота (настройка бота и запуск бота в обоих режимах);
-
красное: бот-приложение, содержащее собственно сам бизнес-функционал бота.
Можно диаграмму дерева зависимостей представить в таком виде, скрыв все зависимости общего пакета:
Таким образом, достаточно прописать в зависимостях бот-приложения общий пакет, а всё остальное подтянется автоматом.
Общий npm-пакет
Пакет @flancer32/teq-telegram-bot
реализует функционал, общий для всех ботов:
-
Загрузка конфигурации бота (токен) из внешних источников (JSON-файл).
-
Запуск node-приложения
-
в виде бота в режиме
long polling
. -
в режиме
webhook
в виде веб-сервера (http & http/2 — как application-сервер за прокси сервером, https — как самостоятельный сервер).
-
-
Общие для всех ботов действия (инициализация списка команд при старте бота, регистрация webhook’а и т.п.).
-
Определяет точки расширения, в которых приложения могут добавлять собственную логику.
Консольные команды
В общем пакете реализованы и подключены две консольные команды, которые обрабатываются commander
‘ом:
-
./bin/tequila.mjs tg-bot-start
(Telegram_Bot_Back_Cli_Start): запуск бота в режимеlong polling
-
./bin/tequila.mjs tg-bot-stop
(Telegram_Bot_Back_Cli_Stop): останов бота, запущенного в режимеlong polling
Запуск и останов бота в режиме 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.
Заключение
Спасибо всем, кто промотал статью до этого места — мне самому бывает влом читать длинные портянки текста и просто интересно, чем это всё закончится. Тем же, кто дочитал до заключения, пусть и вполглаза — мой искренний респект!
После ознакомления с основами ботостроения в Телеграм я пришёл к выводу, что Web 3.0 вполне себе можно построить не на браузерах, а на вот таких вот клиентах с упрощённым интерфейсом взаимодействия с пользователем (текстовые сообщения, возможно, с голосовым набором, плюс пересылка файлов) и широкой сетью ботов, взаимодействующих друг с другом.
P.S.
КДПВ создана Dall-E через браузерное приложение (исходники).
ссылка на оригинал статьи https://habr.com/ru/articles/837610/
Добавить комментарий