npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

messenger-bot-adapters

v1.0.4

Published

Platform-agnostic bot adapters for MAX, Telegram and VK

Readme

messenger-bot-adapters

Единый интерфейс для ботов в Telegram, MAX и VK. Пишете логику один раз — адаптер прозрачно транслирует вызовы в API нужного мессенджера.

Установка

npm install messenger-bot-adapters

SDK платформы загружается лениво — только при создании нужного адаптера. Установите SDK только той платформы, которую используете:

| Платформа | Доп. зависимость | |---|---| | Telegram | не нужна — работает через REST API | | MAX | npm install @maxhub/max-bot-api (>= 0.2.2) | | VK | npm install vk-io (>= 4.10.1) |

Быстрый старт

import { TelegramBotAdapter } from 'messenger-bot-adapters';

const adapter = new TelegramBotAdapter(process.env.BOT_TOKEN!);

adapter.onMessage((msg) => {
  adapter.sendMessage(msg.chatId, `Привет! Вы написали: ${msg.text}`);
});

await adapter.startPolling();

Создание адаптера

import {
  TelegramBotAdapter,
  MaxBotAdapter,
  VkBotAdapter,
} from 'messenger-bot-adapters';
import type { IBotAdapter } from 'messenger-bot-adapters';

function createAdapter(): IBotAdapter {
  const token = process.env.BOT_TOKEN!;
  const webhookSecret = process.env.BOT_WEBHOOK_SECRET;

  switch (process.env.MESSENGER_PLATFORM) {
    case 'telegram':
      return new TelegramBotAdapter(token, { webhookSecret });
    case 'vk':
      return new VkBotAdapter(token, { webhookSecret });
    default:
      return new MaxBotAdapter(token, { webhookSecret });
  }
}

Параметры конструкторов

Все три адаптера принимают одинаковый набор параметров:

| Параметр | Тип | Обязательный | Описание | |---|---|---|---| | token | string | да | Токен бота | | options.webhookSecret | string | нет | Секрет для проверки подписи входящих вебхуков |

TelegramBotAdapter принимает дополнительный параметр:

| Параметр | Тип | Обязательный | Описание | |---|---|---|---| | options.getMessages | (chatId: number, opts?: GetMessagesOptions) => ServiceMessage[] | нет | Кастомный источник истории сообщений для getMessages(). Если не передан, метод возвращает []. |


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

onMessage

Вызывается при каждом входящем сообщении от пользователя или бота.

adapter.onMessage((msg) => {
  if (msg.isBot) return; // игнорируем сообщения от самого бота

  console.log(msg.chatId, msg.text);

  if (msg.attachments?.length) {
    for (const att of msg.attachments) {
      console.log(att.type, att.filename, att.size);
    }
  }

  if (msg.contact) {
    console.log(msg.contact.name, msg.contact.phone);
  }
});

ServiceMessage

| Поле | Тип | Описание | |---|---|---| | messageId | string | Идентификатор сообщения | | chatId | number | Идентификатор чата | | text | string \| undefined | Текст сообщения | | contact | { name: string; phone: string } \| undefined | Контакт, если пользователь поделился им. Заполняется в Telegram и MAX; в VK всегда undefined. | | attachments | ServiceAttachment[] \| undefined | Вложения (фото, видео, документы, аудио) | | timestamp | number | Время сообщения (unix ms) | | senderName | string \| undefined | Имя отправителя. В VK не заполняется. | | isBot | boolean \| undefined | true, если сообщение отправлено самим ботом | | agentId | string \| undefined | Идентификатор агента поддержки, приславшего сообщение |

ServiceAttachment

| Поле | Тип | Описание | |---|---|---| | type | 'image' \| 'video' \| 'audio' \| 'file' | Тип вложения | | payload | Record<string, unknown> | Платформенный объект вложения в исходном виде | | filename | string \| undefined | Имя файла | | size | number \| undefined | Размер в байтах |


onStart

Вызывается при старте бота пользователем (команда /start или переход по deeplink).

Deeplink-ссылки с токеном:

| Платформа | Формат ссылки | |---|---| | Telegram | https://t.me/mybot?start=TOKEN | | MAX | https://max.ru/join/mybot?payload=token%3DTOKEN | | VK | https://vk.com/mygroup?ref=TOKEN |

adapter.onStart(async (event, token) => {
  if (token) {
    // Пользователь перешёл по deeplink — token содержит переданное значение
    const result = await verifyToken(token, event.userId, event.chatId);
    if (result.ok) {
      await adapter.sendMessage(event.chatId, 'Вы успешно подписались на уведомления.');
    } else {
      await adapter.sendMessage(event.chatId, 'Ошибка при подписке.');
    }
  } else {
    // Обычный /start без токена
    await adapter.sendMessage(event.chatId, 'Добро пожаловать!');
  }
});

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

| Параметр | Тип | Описание | |---|---|---| | event.chatId | number | Идентификатор чата | | event.userId | number | Идентификатор пользователя | | token | string \| null | Значение из deeplink-параметра; null при обычном /start |


onCallback

Вызывается при нажатии inline-кнопки (созданной через options.buttons в sendMessage).

adapter.onCallback(async (event) => {
  if (event.data === 'yes') {
    await adapter.sendMessage(event.chatId, 'Вы выбрали «Да»');
  }
});

CallbackEvent

| Поле | Тип | Описание | |---|---|---| | chatId | number | Идентификатор чата | | userId | number | Идентификатор пользователя, нажавшего кнопку | | data | string | Значение поля data кнопки |


Методы

sendMessage

sendMessage(chatId: number, text: string, options?: SendOptions): Promise<ServiceMessage>

| Параметр | Тип | Обязательный | Описание | |---|---|---|---| | chatId | number | да | Идентификатор чата | | text | string | да | Текст сообщения | | options | SendOptions | нет | Дополнительные параметры |

SendOptions

| Поле | Тип | Описание | |---|---|---| | format | 'html' | HTML-разметка текста. Поддерживается в Telegram и MAX; в VK игнорируется. | | buttons | Array<Array<{ text: string; data: string }>> | Inline-кнопки. Внешний массив — строки кнопок, внутренний — кнопки в строке. Нажатие попадает в onCallback. | | replyKeyboard | Array<Array<string \| { label: string; requestContact: true }>> | Клавиатура быстрых ответов под полем ввода. Строка — текстовая кнопка, объект с requestContact: true — кнопка запроса контакта (в VK работает как текстовая). | | removeReplyKeyboard | boolean | Убрать ранее показанную клавиатуру быстрых ответов. В MAX не поддерживается. | | links | Array<Array<{ text: string; url: string }>> | Ссылки под сообщением. Та же структура строк, что и у buttons. | | attachments | ServiceAttachment[] | Вложения для отправки. Получите объект через uploadFile, затем передайте сюда. |

Примеры

// Простой текст
await adapter.sendMessage(chatId, 'Привет!');

// HTML
await adapter.sendMessage(chatId, '<b>Жирный</b> и <i>курсив</i>', { format: 'html' });

// Inline-кнопки
await adapter.sendMessage(chatId, 'Продолжить поиск?', {
  buttons: [
    [{ text: 'Да, ищем дальше', data: 'search_again' }],
    [{ text: 'Нет, выйти', data: 'exit' }],
  ],
});

// Несколько кнопок в одной строке
await adapter.sendMessage(chatId, 'Выберите предмет:', {
  buttons: [[
    { text: 'Математика', data: 'subject:math' },
    { text: 'Физика', data: 'subject:physics' },
  ]],
});

// Клавиатура быстрых ответов
await adapter.sendMessage(chatId, 'Главное меню:', {
  replyKeyboard: [
    ['Найти репетитора'],
    ['Настройки', 'Помощь'],
  ],
});

// Запрос контакта
await adapter.sendMessage(chatId, 'Поделитесь контактом:', {
  replyKeyboard: [[{ label: 'Поделиться контактом', requestContact: true }]],
});

// Убрать клавиатуру
await adapter.sendMessage(chatId, 'Готово.', { removeReplyKeyboard: true });

// Ссылка под сообщением
await adapter.sendMessage(chatId, 'Заявка создана:', {
  links: [[{ text: 'Личный кабинет', url: 'https://example.com/client/home' }]],
});

// Отправка файла
const attachment = await adapter.uploadFile(buffer, 'application/pdf', 'document.pdf', chatId);
await adapter.sendMessage(chatId, 'Держите документ:', { attachments: [attachment] });

uploadFile

Загружает файл на серверы платформы и возвращает объект вложения для использования в sendMessage.

uploadFile(file: Buffer, mimetype: string, filename: string, chatId?: number): Promise<ServiceAttachment>

| Параметр | Тип | Обязательный | Описание | |---|---|---|---| | file | Buffer | да | Содержимое файла | | mimetype | string | да | MIME-тип файла, например image/jpeg, application/pdf | | filename | string | да | Имя файла с расширением | | chatId | number | VK: да, остальные: нет | Идентификатор чата, к которому привязывается загрузка. В VK обязателен — без него загрузка завершится ошибкой. |


getMessages

Возвращает историю сообщений чата.

getMessages(chatId: number, options?: GetMessagesOptions): Promise<ServiceMessage[]>

| Параметр | Тип | Обязательный | Описание | |---|---|---|---| | chatId | number | да | Идентификатор чата | | options | GetMessagesOptions | нет | Параметры выборки |

GetMessagesOptions

| Поле | Тип | Описание | Поддержка | |---|---|---|---| | from | number | Начало диапазона (unix ms) | MAX | | to | number | Конец диапазона (unix ms) | MAX | | count | number | Максимальное количество сообщений | MAX, VK (по умолчанию 200) |

Для Telegram getMessages делегируется в options.getMessages, переданный в конструктор. Если параметр не передан, метод всегда возвращает [].

// Последние 50 сообщений
const messages = await adapter.getMessages(chatId, { count: 50 });

// Сообщения в диапазоне времени
const messages = await adapter.getMessages(chatId, { from: startTs, to: endTs });

registerCommands

Регистрирует команды бота в меню платформы.

registerCommands(commands: Array<{ name: string; description: string }>): Promise<void>

| Параметр | Тип | Описание | |---|---|---| | commands[].name | string | Имя команды без / | | commands[].description | string | Описание команды, видимое пользователю |

В Telegram и MAX команда start добавляется автоматически. В VK метод является no-op — команды не регистрируются.

await adapter.registerCommands([
  { name: 'help', description: 'Справка' },
  { name: 'settings', description: 'Настройки' },
]);

setWebhook

Регистрирует URL вебхука в платформе.

setWebhook(url: string, secret?: string): Promise<void>

| Параметр | Тип | Обязательный | Описание | |---|---|---|---| | url | string | да | Публичный HTTPS-URL, на который платформа будет отправлять обновления | | secret | string | нет | Секрет для подписи запросов. Должен совпадать с webhookSecret, переданным в конструктор. |


deleteWebhook

Удаляет зарегистрированный вебхук. Вызывайте перед startPolling, чтобы сбросить ранее установленный вебхук.

deleteWebhook(): Promise<void>

startPolling

Запускает получение обновлений через long-polling. Используйте в разработке; в продакшне предпочтителен вебхук.

startPolling(): Promise<void>

processWebhook

Обрабатывает входящий POST-запрос от платформы: проверяет подпись, запускает обработчики событий и возвращает строку-ответ.

processWebhook(body: unknown, headers: Record<string, string | string[] | undefined>): string | false

| Параметр | Тип | Описание | |---|---|---| | body | unknown | Разобранное тело запроса | | headers | Record<string, string \| string[] \| undefined> | Заголовки запроса |

Возвращаемое значение

| Значение | Когда | Что делать | |---|---|---| | 'ok' | Telegram, MAX — любое валидное обновление | Вернуть 200 OK с телом 'ok' | | 'ok' | VK — любое событие кроме confirmation | Вернуть 200 OK с телом 'ok' | | confirmationCode | VK — событие confirmation при регистрации вебхука | Вернуть 200 OK с этой строкой в теле; VK убедится, что URL принадлежит вам | | false | Подпись не прошла проверку (любая платформа) | Вернуть 401 Unauthorized |

Код подтверждения VK получается автоматически при вызове setWebhook и кешируется внутри адаптера — вручную его передавать не нужно.

app.post('/webhook', (req, res) => {
  const result = adapter.processWebhook(req.body, req.headers);

  if (result === false) {
    res.status(401).json({ ok: false, error: 'Unauthorized' });
    return;
  }

  res.status(200).send(result);
});

Webhook и Polling

Polling (разработка)

await adapter.registerCommands([{ name: 'help', description: 'Справка' }]);

try {
  await adapter.deleteWebhook();
} catch (err) {
  console.warn('deleteWebhook failed:', err);
}

await adapter.startPolling();

Webhook (продакшн)

import express from 'express';

await adapter.registerCommands([{ name: 'help', description: 'Справка' }]);
await adapter.setWebhook('https://my-service.example.com/webhook', process.env.BOT_WEBHOOK_SECRET);

const app = express();
app.use(express.json());

app.post('/webhook', (req, res) => {
  const result = adapter.processWebhook(req.body, req.headers);
  if (result === false) {
    res.status(401).json({ ok: false, error: 'Unauthorized' });
    return;
  }
  res.status(200).send(result);
});

app.listen(3000);

Обработка ошибок

Все методы могут бросить BotApiError при ошибках платформенного API.

Поля BotApiError

| Поле | Тип | Описание | |---|---|---| | code | BotApiErrorCode | Нормализованный код ошибки | | platformCode | string \| undefined | Оригинальный код ошибки платформы: HTTP-статус у Telegram, код из тела ответа у MAX, числовой код ошибки VK API у VK | | message | string | Текст ошибки |

BotApiErrorCode

| Код | Описание | |---|---| | bot_blocked | Пользователь остановил или заблокировал бота | | chat_not_found | Чат не найден | | unsupported_feature | Платформа не поддерживает запрошенную функцию | | unknown_error | Неожиданная ошибка платформы |

import { BotApiError } from 'messenger-bot-adapters';

try {
  await adapter.sendMessage(chatId, 'Уведомление');
} catch (err) {
  if (err instanceof BotApiError) {
    switch (err.code) {
      case 'bot_blocked':
        await markChatAsInactive(chatId);
        break;
      case 'chat_not_found':
        await removeChat(chatId);
        break;
      case 'unknown_error':
        console.error('Bot API error:', err.message, 'platformCode:', err.platformCode);
        break;
    }
  } else {
    throw err;
  }
}

Своя реализация адаптера

Чтобы добавить поддержку нового мессенджера, унаследуйтесь от BaseAdapter:

import { BaseAdapter } from 'messenger-bot-adapters';
import type {
  ServiceMessage, ServiceAttachment, SendOptions,
  GetMessagesOptions, BotStartedEvent, CallbackEvent,
} from 'messenger-bot-adapters';

export class MyAdapter extends BaseAdapter {
  onMessage(handler: (msg: ServiceMessage) => void): void { /* ... */ }
  onStart(handler: (event: BotStartedEvent, token: string | null) => void): void { /* ... */ }
  onCallback(handler: (event: CallbackEvent) => void): void { /* ... */ }

  async sendMessage(chatId: number, text: string, options?: SendOptions): Promise<ServiceMessage> { /* ... */ }
  async getMessages(chatId: number, options?: GetMessagesOptions): Promise<ServiceMessage[]> { /* ... */ }
  async uploadFile(file: Buffer, mimetype: string, filename: string, chatId?: number): Promise<ServiceAttachment> { /* ... */ }
  async registerCommands(commands: Array<{ name: string; description: string }>): Promise<void> { /* ... */ }
  async setWebhook(url: string, secret?: string): Promise<void> { /* ... */ }
  async deleteWebhook(): Promise<void> { /* ... */ }
  async startPolling(): Promise<void> { /* ... */ }

  protected verifyWebhookSignature(
    headers: Record<string, string | string[] | undefined>,
    body?: unknown,
  ): boolean { /* ... */ }

  protected handleWebhookUpdate(body: unknown): void { /* ... */ }
}

BaseAdapter предоставляет готовую реализацию processWebhook и утилиту extractToken(payload) для разбора deeplink-токенов.


Различия платформ

| | Telegram | MAX | VK | |---|---|---|---| | msg.senderName | first_name + last_name | имя из профиля | не заполняется | | msg.contact | при шаринге контакта | при шаринге контакта (из VCF) | никогда не заполняется | | replyKeyboard | нативная | эмулируется inline callback-кнопками | нативная | | requestContact | нативный шаринг | нативный шаринг | текстовая кнопка | | removeReplyKeyboard | ✅ | — | ✅ | | format: 'html' | ✅ | ✅ | игнорируется | | registerCommands | регистрирует start + переданные | регистрирует start + переданные | no-op | | getMessages | через options.getMessages; [] если не передан | платформенный API (from, to, count) | платформенный API (только count, по умолчанию 200) | | uploadFilechatId | необязателен | необязателен | обязателен | | Проверка подписи webhook | заголовок x-telegram-bot-api-secret-token | заголовок x-max-bot-api-secret | поле body.secret | | deeplink-токен в onStart | параметр /start TOKEN | поле payload события bot_started | referralValue (ref в ссылке) |


Требования

  • Node.js >= 20
  • TypeScript >= 5 (опционально, но рекомендуется)

Лицензия

MIT