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

@dementevdev/maxbot-ts

v1.0.0

Published

TypeScript SDK для MAX Bot API

Readme

@dementevdev/maxbot-ts

TypeScript SDK для MAX Bot API.

Содержание

Начало работы

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

Возможности

Маршрутизация

Продвинутое использование

Справочник


Установка

npm i @dementevdev/maxbot-ts

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

import { Bot } from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});

bot.onMessage(async (ctx) => {
  await ctx.reply("Привет!");
});

// Graceful shutdown: дожидаемся завершения текущих обработчиков
process.once("SIGINT", async () => {
  await bot.stop();
  process.exit(0);
});

await bot.start();

Webhook: замените transport: "polling" на transport: "webhook" и добавьте секцию webhook: { port, url } — см. раздел Webhook ниже.

Стабильность API

Пакет следует SemVer. Список стабильных экспортов с v1.0.0:

| Категория | Символы | | ---------------- | --------------------------------------------------------------------------------------------------------- | | Бот | Bot, BotConfig, Context, EventHandler | | Контексты | MessageContext, CallbackContext, ChatContext, BotStartedContext | | Маршрутизация | bot.command(), bot.hears(), bot.action(), bot.on(), bot.use(), bot.filter() | | Триггеры | HearsTrigger, CommandTrigger, matchCommand | | Жизненный цикл | bot.start(), bot.stop(), bot.getMe(), bot.sendMessage(), bot.sendPrivateMessage() | | Утилиты | InlineKeyboard, md, html, Histogram, DEFAULT_BUCKETS | | Ошибки | MaxError, RateLimitError, AuthError, NetworkError, ApiError, ValidationError, NotFoundError | | Composer | Composer, Filter | | Фильтры | hasText, isPrivateChat, commandIs, hasCallbackPayload, payloadIs | | Sub-entry points | @dementevdev/maxbot-ts/middleware, @dementevdev/maxbot-ts/filters |

Experimental (сигнатуры стабильны, могут расширяться до v2.0.0): BotMetrics, onMetrics, HistogramSnapshot.

Полная semver-политика → раздел Versioning в конце документа.


Ограничение параллелизма обновлений

По умолчанию бот обрабатывает не более 10 обновлений одновременно (concurrency: 10). Это защищает от burst-нагрузки: новые обновления ждут в очереди, а не запускают неограниченное число параллельных промисов.

import { Bot } from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
  concurrency: 20, // максимум 20 одновременно обрабатываемых update
});
  • Ограничение применяется на уровне одного update.
  • Все хендлеры внутри одного update выполняются параллельно.
  • Infinity отключает ограничение и возвращает прежнее поведение.

Webhook

import { Bot } from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "webhook",
  webhook: {
    port: 3000,
    url: "https://mybot.example.com/webhook",
    autoRegister: true,
    secretToken: process.env.WEBHOOK_SECRET,
  },
});

bot.onMessage(async (ctx) => {
  await ctx.reply("Привет!");
});

bot.start();

allowedUpdates — фильтрация типов обновлений

По умолчанию бот получает все типы обновлений. Опция allowedUpdates в polling позволяет указать только нужные — сервер не будет отправлять остальные, снижая трафик.

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
  polling: {
    allowedUpdates: ["message_created", "message_callback"],
  },
});

Доступные типы (UpdateType):

| Тип | Когда приходит | | -------------------- | -------------------------------------- | | message_created | Новое сообщение в чате или диалоге | | message_edited | Редактирование сообщения | | message_removed | Удаление сообщения | | message_callback | Нажатие inline-кнопки | | bot_started | Нажатие Start / переход по deep link | | bot_added | Бот добавлен в чат | | bot_removed | Бот удалён из чата | | user_added | Пользователь добавлен в чат через бота | | user_removed | Пользователь удалён из чата через бота | | chat_title_changed | Изменение названия чата |

Если не указать allowedUpdates — приходят все типы (поведение по умолчанию).

Inline‑клавиатура

import { Bot, InlineKeyboard, md } from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});

bot.onMessage(async (ctx) => {
  const keyboard = new InlineKeyboard()
    .button("Да", "yes")
    .row()
    .button("Нет", "no")
    .build();

  await ctx.reply(md.bold("Выберите вариант:"), {
    format: "markdown",
    attachments: [keyboard],
  });
});

bot.onCallback(async (ctx) => {
  await ctx.answer({ notification: `Вы нажали: ${ctx.data}` });
});

bot.start();

Загрузка файлов

Ограничения multipart upload:

  • Максимальный размер файла: 4 ГБ
  • Можно загружать только один файл за раз
import { Bot } from "@dementevdev/maxbot-ts";
import { readFile } from "node:fs/promises";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});

async function uploadImage() {
  const { url } = await bot.api.uploads.getUploadUrl("image");
  const buffer = await readFile("./image.png");
  const blob = new Blob([buffer]);

  const payload = await bot.api.uploads.uploadFile(url, blob, "image.png");
  return payload;
}

Работа с ошибками

Все ошибки SDK наследуют MaxError. Иерархия:

| Класс | HTTP | Поля | Когда возникает | | ----------------- | ------- | ------------------------- | ---------------------------------- | | RateLimitError | 429 | retryAfter: number (мс) | Превышен лимит запросов к API | | AuthError | 401 | — | Невалидный или отсутствующий токен | | ApiError | 4xx/5xx | statusCode, apiCode? | Ошибка на стороне сервера MAX | | NotFoundError | 404 | — | Чат / сообщение / ресурс не найден | | ValidationError | — | — | Некорректные параметры вызова | | NetworkError | — | cause?: Error | Таймаут, обрыв сети | | MaxError | — | message | Базовый класс всех ошибок SDK |

import {
  Bot,
  MaxError,
  RateLimitError,
  AuthError,
  ApiError,
  NetworkError,
} from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});

bot.onError((error) => {
  if (error instanceof RateLimitError) {
    // retryAfter — мс до следующего окна.
    // Polling-транспорт сам делает паузу; здесь можно залогировать.
    console.warn(`Rate limit, retry через ${error.retryAfter}мс`);
    return;
  }

  if (error instanceof AuthError) {
    console.error("Неверный токен — бот не может работать");
    process.exit(1);
  }

  if (error instanceof NetworkError) {
    // Временная ошибка сети — транспорт восстановится сам
    console.warn("Сеть недоступна:", error.message);
    return;
  }

  if (error instanceof MaxError) {
    console.error("Ошибка SDK:", error.message);
    return;
  }

  console.error("Неизвестная ошибка:", error);
});

Retry-поведение: polling-транспорт при RateLimitError автоматически выдерживает паузу retryAfter мс перед следующим запросом. При NetworkError — экспоненциальный backoff. Приложению не нужно реализовывать retry вручную.

Метрики и наблюдаемость

onMetrics — per-update метрики

import { Bot } from "@dementevdev/maxbot-ts";
import type { BotMetrics } from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
  onMetrics: (m: BotMetrics) => {
    // m.totalMs        — общее время обработки update (middleware + handlers)
    // m.middlewareTimes — массив ms по каждому middleware в порядке регистрации
    // m.queueSizeAtStart — размер очереди ДО захвата слота (backpressure индикатор)
    // m.handlerErrors  — число ошибок в handlers для этого update
    // m.updateType     — тип update ('message_created', 'message_callback', ...)

    if (m.totalMs > 500) {
      console.warn(`Slow update: ${m.updateType} — ${m.totalMs}ms`);
    }
    if (m.queueSizeAtStart > 5) {
      console.warn(`Queue pressure: ${m.queueSizeAtStart} waiting`);
    }
    if (m.handlerErrors > 0) {
      console.error(`Handler errors: ${m.handlerErrors} in ${m.updateType}`);
    }
  },
});

onMetrics не влияет на производительность если не задан — замер включается только при его наличии.

onSlowHandler — порог по времени

// Срабатывает только если total time >= 200ms
bot.onSlowHandler(200, (ms, updateType) => {
  console.warn(`Slow: ${updateType} — ${ms}ms`);
});

Разница с onMetrics: onSlowHandler — простой threshold-алерт без детализации по middleware.

Histogram — гистограмма задержек

Histogram накапливает измерения totalMs по корзинам (buckets) в формате, совместимом с Prometheus. Удобен для экспорта p50/p95/p99 в любую систему мониторинга.

import { Bot, Histogram } from "@dementevdev/maxbot-ts";

const histogram = new Histogram(); // дефолтные корзины: 5, 10, 25, 50, 100, 250, 500, 1000 мс

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
  onMetrics: (m) => histogram.observe(m.totalMs),
});

// Публикуем снимок раз в минуту
setInterval(() => {
  const snap = histogram.snapshot();
  // snap.buckets: { "5": 12, "10": 45, "25": 78, ..., "+Inf": 100 } — кумулятивно
  // snap.count: 100   — общее число обновлений
  // snap.sum: 3 200   — суммарное время в мс (для расчёта среднего: sum / count)
  console.log(snap);
  histogram.reset(); // сброс для следующего окна
}, 60_000);

Кастомные корзины — передайте массив границ в мс:

const h = new Histogram([10, 50, 200, 1000, 5000]);

Корзины автоматически сортируются и дедуплицируются.

Структура снимка HistogramSnapshot:

| Поле | Тип | Описание | | --------- | ------------------------ | ------------------------------------------------------------------------- | | buckets | Record<string, number> | Кумулятивные счётчики: "50" = число наблюдений ≤ 50ms, "+Inf" = всего | | sum | number | Сумма всех значений в мс | | count | number | Число наблюдений (= buckets["+Inf"]) |

Интеграция с Prometheus через prom-client:

import { Registry, Histogram as PromHistogram } from "prom-client";

const registry = new Registry();
const promHist = new PromHistogram({
  name: "bot_update_duration_ms",
  help: "Update processing duration",
  buckets: [5, 10, 25, 50, 100, 250, 500, 1000],
  registers: [registry],
});

bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
  onMetrics: (m) => promHist.observe(m.totalMs),
});

Рекомендации по параметру concurrency

| Профиль бота | Рекомендуемое значение | Обоснование | | ------------------------------------ | ---------------------- | ---------------------------------- | | Приватные диалоги, быстрые ответы | 10 (по умолчанию) | Баланс параллелизма и защиты | | Бот в большой группе / публичный | 20–50 | Больше одновременных пользователей | | Webhook-бот с тяжёлой бизнес-логикой | 5–10 | Снизить нагрузку на downstream | | Бот с медленными API-вызовами (> 1s) | 20–100 | Большинство времени — I/O ожидание | | Минимальный сервис / отладка | 1 | Строгая очерёдность | | Без ограничений (legacy-режим) | Infinity | Не рекомендуется в проде |

Как определить правильное значение:

  1. Запустите с onMetrics и наблюдайте queueSizeAtStart
  2. Если очередь стабильно > 0 — увеличьте concurrency
  3. Если растут ошибки downstream API — уменьшите
  4. Ориентир: concurrency ≈ (среднее время handler) / (желаемая latency) × throughput
// Диагностика: подбор concurrency по метрикам
const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
  concurrency: 10, // начните с дефолта
  onMetrics: (m) => {
    // Если queueSizeAtStart регулярно > 3 — увеличьте concurrency
    // Если totalMs растёт — возможно, downstream перегружен
    console.log(
      JSON.stringify({
        type: m.updateType,
        ms: m.totalMs,
        queue: m.queueSizeAtStart,
        errors: m.handlerErrors,
      }),
    );
  },
});

SDK предоставляет полноценный middleware-пайплайн аналогично Koa/Telegraf.

bot.use() — глобальные middleware

import { Bot } from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});

// Логирование каждого update
bot.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  console.log(`${ctx.raw.update_type} — ${Date.now() - start}ms`);
});

// Ранний выход без вызова next() останавливает пайплайн
bot.use(async (ctx, next) => {
  if (ctx.raw.update_type === "message_created") {
    const chatType = ctx.raw.message?.recipient?.chat_type;
    if (chatType !== "dialog") return; // игнорируем групповые сообщения
  }
  await next();
});

bot.onMessage(async (ctx) => {
  await ctx.reply("Привет из private chat!");
});

bot.start();

Middleware выполняется внутри одного слота семафора — все ограничения concurrency сохраняются даже при длинных middleware-цепочках.

bot.command() / bot.hears() / bot.action()

// Команды (срабатывает на /start и /start <args>)
bot.command("start", async (ctx) => {
  await ctx.reply("Добро пожаловать!");
});

// Произвольный текст: строка, RegExp или массив паттернов (HearsTrigger)
bot.hears(/^ping$/i, async (ctx) => {
  await ctx.reply("pong 🏓");
});

// Массив — срабатывает если ANY из паттернов совпал (первый побеждает)
bot.hears(["привет", "hello", /^hi$/i], async (ctx) => {
  await ctx.reply("Привет!");
});

// Нажатие inline-кнопки по payload (строка, RegExp или массив)
bot.action("confirm", async (ctx) => {
  await ctx.answer({ notification: "Подтверждено ✓" });
});

// Несколько строковых payload — одним обработчиком
bot.action(["yes", "confirm", "ok"], async (ctx) => {
  await ctx.answer({ notification: "Принято!" });
});

// Несколько RegExp-паттернов
bot.action([/^buy:(\d+)$/, /^sell:(\d+)$/], async (ctx) => {
  if (isCallbackContext(ctx) && ctx.match) {
    const id = ctx.match[1];
    await ctx.answer({ notification: `Позиция #${id}` });
  }
});

// Все методы возвращают this — можно чейнить
bot
  .command("help", async (ctx) => ctx.reply("/start — начать"))
  .hears("привет", async (ctx) => ctx.reply("Привет!"));

HearsTrigger — тип паттерна для hears() и action(): string | RegExp | ReadonlyArray<string | RegExp>. При массиве срабатывает первый совпавший паттерн; ctx.match устанавливается только если совпал RegExp.

Variadic middlewares

command(), hears(), action(), on(), onMessage(), onCallback(), onStart() принимают произвольное число middleware перед финальным обработчиком. Промежуточные должны вызвать next(), иначе цепочка прерывается.

// Guard: блокирует неавторизованных
const authMw: Middleware<Context> = async (ctx, next) => {
  if (!isAuthorized(ctx)) return; // next() не вызван → handler не выполнится
  await next();
};

// Логирование
const logMw: Middleware<Context> = async (ctx, next) => {
  console.log("before");
  await next();
  console.log("after");
};

bot.command("pay", authMw, logMw, async (ctx) => {
  /* ... */
});
bot.hears(/^\d+/, authMw, async (ctx) => {
  /* ... */
});
bot.action(/^buy:/, authMw, logMw, async (ctx) => {
  /* ... */
});
bot.on("message_created", logMw, async (ctx) => {
  /* ... */
});

ctx.match — capture-группы RegExp

При использовании RegExp-паттерна в hears() и action() результат exec() сохраняется в ctx.match. Для строкового паттерна ctx.match равен null.

import { isMessageContext, isCallbackContext } from "@dementevdev/maxbot-ts";

// hears: извлекаем числа из текста
bot.hears(/(\d+)\+(\d+)/, async (ctx) => {
  if (isMessageContext(ctx) && ctx.match) {
    const a = Number(ctx.match[1]);
    const b = Number(ctx.match[2]);
    await ctx.reply(`${a} + ${b} = ${a + b}`);
  }
});

// action: извлекаем id из payload кнопки вида "buy:42"
bot.action(/^buy:(\d+)$/, async (ctx) => {
  if (isCallbackContext(ctx) && ctx.match) {
    const productId = ctx.match[1]; // "42"
    await ctx.answer({ notification: `Заказ #${productId} оформлен` });
  }
});

| Свойство | Тип | Доступно в | Значение при строковом паттерне | | ----------- | ------------------------- | ----------------------------------- | ------------------------------- | | ctx.match | RegExpExecArray \| null | MessageContext, CallbackContext | null |

bot.onStart() — deep link и старт бота

Событие bot_started возникает когда пользователь нажимает кнопку Start в диалоге или переходит по ссылке вида https://max.ru/...?start=<payload>.

ctx.startPayload содержит значение параметра start из ссылки — используйте для реферальных программ, onboarding-флоу и контекстного запуска.

import { Bot, isBotStartedContext } from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});

// Convenience-метод — алиас для bot.on('bot_started', ...)
bot.onStart(async (ctx) => {
  if (isBotStartedContext(ctx)) {
    if (ctx.startPayload) {
      // Пользователь перешёл по deep link: https://max.ru/...?start=ref_123
      await ctx.reply(`Вы пришли по реферальной ссылке: ${ctx.startPayload}`);
    } else {
      // Обычный запуск без параметра
      await ctx.reply("Добро пожаловать! Введите /help для справки.");
    }
  }
});

bot.start();

| Свойство / метод | Тип | Описание | | --------------------- | ----------------------------- | --------------------------------------- | | ctx.startPayload | string \| null \| undefined | Payload из deep link, null если нет | | ctx.chatId | number | ID диалога | | ctx.userId | number | ID пользователя, нажавшего Start | | ctx.user | User | Объект пользователя | | isBotStartedContext | type guard | Сужает Context до BotStartedContext |

ctx.editMessage / ctx.deleteMessage / ctx.sendAction

Методы доступны в MessageContext и CallbackContext. sendAction также доступен в BotStartedContext.

// ── MessageContext ──────────────────────────────────────────────────────────
bot.onMessage(async (ctx) => {
  if (!isMessageContext(ctx)) return;

  // Редактировать входящее сообщение (требует rights, обычно своё)
  await ctx.editMessage("обновлённый текст");

  // Удалить сообщение
  await ctx.deleteMessage();

  // Произвольное действие: 'typing_on', 'sending_photo', 'mark_seen', …
  await ctx.sendAction("sending_file");

  // sendTyping() — удобный псевдоним для sendAction('typing_on')
  await ctx.sendTyping();
});

// ── CallbackContext ─────────────────────────────────────────────────────────
bot.onCallback(async (ctx) => {
  if (!isCallbackContext(ctx)) return;

  // Редактировать сообщение с кнопкой
  await ctx.editMessage("результат обработан", { format: "markdown" });

  // Удалить сообщение с кнопкой
  await ctx.deleteMessage();

  // Показать "печатает..." пока обрабатывается запрос
  await ctx.sendAction("typing_on");
  const result = await processRequest();
  await ctx.reply(result);
});

// ── BotStartedContext ───────────────────────────────────────────────────────
bot.onStart(async (ctx) => {
  if (!isBotStartedContext(ctx)) return;
  await ctx.sendAction("typing_on");
  await ctx.reply("Добро пожаловать!");
});

| Метод | Доступен в | Бросает если | | ------------------------------ | -------------------------------------------------------- | --------------- | | ctx.editMessage(text, opts?) | MessageContext, CallbackContext | нет messageId | | ctx.deleteMessage() | MessageContext, CallbackContext | нет messageId | | ctx.sendAction(action) | MessageContext, CallbackContext, BotStartedContext | нет chatId | | ctx.sendTyping() | MessageContext | нет chatId |

onSlowHandler — см. раздел Метрики и наблюдаемость выше.


Filter DSL

Набор предикатов для type-safe фильтрации контекста в middleware:

import {
  Bot,
  hasText,
  isPrivateChat,
  commandIs,
  hasCallbackPayload,
  payloadIs,
} from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});

const isStart = commandIs("start");
const isDeleteAction = payloadIs(/^delete:/);

bot.use(async (ctx, next) => {
  if (isStart(ctx)) {
    await ctx.reply("Привет! /help — список команд");
    return;
  }

  if (isDeleteAction(ctx)) {
    const id = ctx.data.split(":")[1]; // ctx: CallbackContext, ctx.data: string
    await ctx.answer({ notification: `Удаляю ${id}` });
    return;
  }

  if (hasText(ctx)) {
    // ctx: MessageContext, ctx.text: string — без undefined
    await ctx.reply(`Эхо: ${ctx.text}`);
    return;
  }

  await next();
});

| Предикат | Сужает тип | Описание | | ------------------------- | ------------------------------------ | ------------------------------------------------- | | hasText(ctx) | MessageContext & { text: string } | Сообщение с непустым текстом | | isPrivateChat(ctx) | MessageContext | Приватный чат (dialog) | | commandIs(name) | MessageContext & { text: string } | Фабрика — совпадение по команде | | hasCallbackPayload(ctx) | CallbackContext & { data: string } | Callback с непустым payload | | payloadIs(pattern) | CallbackContext & { data: string } | Фабрика — совпадение payload по строке или RegExp |


Composer — суб-роутер

Composer — независимый роутер, который собирается отдельно от бота и подключается через bot.use(composer.middleware()). Поддерживает те же методы маршрутизации что и Bot: command, hears, action, on, use, filter.

Базовое использование

import { Bot, Composer } from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});

const router = new Composer();
router.command("start", async (ctx) => ctx.reply("Привет!"));
router.hears(/(\d+)\+(\d+)/, async (ctx) => {
  const [, a, b] = ctx.match!;
  await ctx.reply(`${a} + ${b} = ${Number(a) + Number(b)}`);
});

bot.use(router.middleware());
await bot.start();

Изоляция модулей

Разбивайте большой бот на независимые файлы — каждый экспортирует один Composer:

// shop.ts
import { Composer } from "@dementevdev/maxbot-ts";
export const shopRouter = new Composer();

shopRouter.command("catalog", catalogHandler);
shopRouter.command("cart", cartHandler);
shopRouter.action(/^add:(\d+)/, addToCartHandler);

// admin.ts
import { Composer } from "@dementevdev/maxbot-ts";
import { isAdmin } from "./guards.js";
export const adminRouter = new Composer();

adminRouter.use(isAdmin); // middleware только для этого роутера
adminRouter.command("stats", statsHandler);
adminRouter.command("ban", banHandler);

// bot.ts
import { Bot } from "@dementevdev/maxbot-ts";
import { shopRouter } from "./shop.js";
import { adminRouter } from "./admin.js";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});
bot.use(shopRouter.middleware());
bot.use(adminRouter.middleware());
await bot.start();

Вложенные Composer (under-router)

const outer = new Composer();
const inner = new Composer();

inner.command("nested", handler);
outer.use(inner.middleware());

bot.use(outer.middleware());

API

| Метод | Описание | | --------------------------- | ----------------------------------------------------------- | | use(middleware) | Добавить middleware в цепочку | | on(event, ...fns) | Подписаться на UpdateType | | command(name, ...fns) | Обработать команду /name | | hears(pattern, ...fns) | Обработать текст (строка или RegExp, устанавливает match) | | action(pattern, ...fns) | Обработать callback payload | | filter(predicate, ...fns) | Запустить fns только если predicate вернул true | | middleware() | Вернуть Middleware для bot.use() |


bot.filter() / ctx.has()

bot.filter()

Подключить middleware только для контекстов, удовлетворяющих предикату. Несовпадающие обновления прозрачно перетекают к следующим обработчикам.

import { Bot, hasText, isPrivateChat } from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});

// Только приватные чаты с текстом
bot.filter(isPrivateChat, async (ctx, next) => {
  console.log("приватный чат");
  await next();
});

// Только сообщения с текстом — ctx.text: string (не undefined)
bot.filter(hasText, async (ctx) => {
  await ctx.reply(`Эхо: ${ctx.text}`);
});

// Комбинация: Composer подключается только для приватных чатов
const privateRouter = new Composer();
privateRouter.command("start", startHandler);

bot.filter(isPrivateChat, privateRouter.middleware());

ctx.has()

Проверить тип контекста прямо внутри обработчика с автоматическим сужением типа:

bot.use(async (ctx, next) => {
  if (ctx.has(hasText)) {
    // ctx: MessageContext & { text: string }
    console.log(ctx.text.toUpperCase()); // ctx.text — string, не undefined
    await ctx.sendTyping();
  }

  if (ctx.has(isCallbackContext)) {
    // ctx: CallbackContext
    await ctx.answer();
  }

  await next();
});

Доступен на всех контекстах: MessageContext, CallbackContext, ChatContext, BotStartedContext.

bot.onStart(async (ctx) => {
  if (ctx.has(isPrivateChat)) {
    // обрабатываем только приватный старт
  }
});

Расширение контекста (Custom Context)

import { Bot } from "@dementevdev/maxbot-ts";
import type { Context } from "@dementevdev/maxbot-ts";

// Тип расширенного контекста — intersection с базовым
type SessionCtx = Context & { session: { userId: number; count: number } };

const sessions = new Map<number, { userId: number; count: number }>();

const bot = new Bot<SessionCtx>({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
  contextFactory: (base): SessionCtx => {
    const userId =
      base.raw.update_type === "message_created"
        ? base.raw.message.sender.user_id
        : 0;
    const session = sessions.get(userId) ?? { userId, count: 0 };
    sessions.set(userId, session);
    return Object.assign(base as SessionCtx, { session });
  },
});

// В middleware ctx.session типизирован без кастов
bot.use(async (ctx, next) => {
  ctx.session.count += 1;
  await next();
});

bot.command("stats", async (ctx) => {
  await ctx.reply(`Сообщений: ${ctx.session.count}`);
});

Паттерны (Patterns)

Паттерн 1: Слоистая middleware-архитектура

// 1. Глобальная инфраструктура (логи, трейсинг)
bot.use(loggingMiddleware);
bot.use(tracingMiddleware);

// 2. Безопасность (auth, rate-limit)
bot.use(authMiddleware);
bot.use(rateLimitMiddleware);

// 3. Бизнес-роутинг
bot.command("start", startHandler);
bot.action(/^order:/, orderHandler);
bot.onMessage(fallbackHandler);

Паттерн 2: Переиспользуемые предикаты

// predicates.ts
export const isAdminMessage =
  (adminIds: number[]) =>
  (ctx: Context): ctx is MessageContext =>
    ctx instanceof MessageContext &&
    adminIds.includes(ctx.message.sender.user_id);

// bot.ts
const isAdmin = isAdminMessage([123456789]);

bot.use(async (ctx, next) => {
  if (!isAdmin(ctx)) {
    await ctx.reply("Нет доступа");
    return;
  }
  await next();
});

Паттерн 3: Корректный graceful shutdown

import { Bot } from "@dementevdev/maxbot-ts";

const bot = new Bot({
  token: process.env.MAX_BOT_TOKEN!,
  transport: "polling",
});

bot.onMessage(async (ctx) => {
  await ctx.reply("OK");
});

async function main() {
  await bot.start();
  console.log("Бот запущен");

  // Graceful shutdown: ждём завершения текущих обработчиков
  process.once("SIGINT", async () => {
    console.log("Останавливаем бота...");
    await bot.stop();
    process.exit(0);
  });
}

main().catch(console.error);

Migration Guide: от handlers к middleware

Было (handlers-only)

const bot = new Bot({ token, transport: "polling" });

bot.onMessage(async (ctx) => {
  if (!ctx.text) return;
  if (ctx.text === "/start") {
    await ctx.reply("Привет!");
    return;
  }
  await ctx.reply(`Эхо: ${ctx.text}`);
});

bot.onCallback(async (ctx) => {
  if (ctx.data === "yes") await ctx.answer({ notification: "Да!" });
});

Стало (middleware/composer — те же гарантии, лучший DX)

const bot = new Bot({ token, transport: "polling" });

// Опциональный глобальный middleware добавляется через use()
// Старые onMessage/onCallback продолжают работать без изменений

bot.command("start", async (ctx) => {
  await ctx.reply("Привет!");
});

bot.onMessage(async (ctx) => {
  if (!ctx.text || ctx.text.startsWith("/")) return;
  await ctx.reply(`Эхо: ${ctx.text}`);
});

bot.action("yes", async (ctx) => {
  await ctx.answer({ notification: "Да!" });
});

Что изменилось:

  • onMessage/onCallback — полная обратная совместимость, работают как раньше
  • bot.use() добавляет middleware до всех существующих handlers
  • bot.command(), bot.hears(), bot.action() — sugar поверх bot.on()
  • Все middleware и handlers выполняются внутри одного semaphore-slot → concurrency по-прежнему работает

Локальные примеры

В папке examples/ лежат готовые сценарии:

| Файл | Описание | | -------------------------- | --------------------------------------------------------- | | echo-bot.js | Минимальный эхо-бот | | keyboard-bot.js | Inline-клавиатура и callback | | command-bot.js | Команды, hears, action | | middleware-bot.js | Middleware-пайплайн, логирование, rate-limit, variadic мw | | custom-context-bot.js | Расширение контекста через contextFactory | | filters-bot.js | Filter DSL (hasText, payloadIs, commandIs) | | webhook-bot.js | Webhook-режим | | send-media-bot.js | Загрузка и отправка медиа | | all-buttons-bot.js | Все типы кнопок: callback/link/contact/geo/openApp | | graceful-shutdown-bot.js | onMetrics, onSlowHandler, onUnknownUpdate, SIGTERM | | broadcast-bot.js | Рассылка подписчикам, sendPrivateMessage, getMe | | wizard-bot.js | Многошаговый диалог (FSM по шагам) | | group-bot.js | Группы: bot_added, user_added, chat_title_changed | | typing-bot.js | sendTyping() + replyWithQuote() + /typingtest | | match-bot.js | ctx.match: capture-группы в hears() и action() | | start-payload-bot.js | bot.onStart(), ctx.startPayload, deep link |

Эта папка не публикуется в npm-пакет (ограничение через поле files в package.json).

Для запуска примеров локально:

npm run build
node examples/echo-bot.js

API

Доступ к низкоуровневым методам через bot.api:

  • bot.api.bots.getMe()
  • bot.api.chats.*
  • bot.api.messages.*
  • bot.api.subscriptions.*
  • bot.api.uploads.*

Полные типы экспортируются из пакета:

import type { Message, Update, Chat, BotInfo } from "@dementevdev/maxbot-ts";

Формат contact‑вложения (request_contact)

При нажатии кнопки request_contact бот получает сообщение с вложением типа contact. Полезные поля payload:

  • vcf_info: vCard строка
  • vcf_phone: телефон
  • max_info: объект пользователя (User)

Формат location‑вложения (request_geo_location)

При нажатии кнопки request_geo_location бот получает сообщение с вложением типа location:

  • latitude: число
  • longitude: число

Sub-entry points (tree-shaking)

Модули middleware и filters доступны как отдельные точки входа — они не попадают в бандл тех, кто их не импортирует:

// Только core SDK — middleware и filters не включаются
import { Bot } from "@dementevdev/maxbot-ts";

// Только middleware-типы — без остального SDK
import type { Middleware, NextFn } from "@dementevdev/maxbot-ts/middleware";

// Только фильтры
import { hasText, payloadIs } from "@dementevdev/maxbot-ts/filters";

Декларации типов (*.d.ts) включены в каждый sub-entry point.


Versioning (Semver policy)

| Тип изменения | Версия | Примеры | | --------------------------------------- | --------- | ------------------------------------------------- | | Новые публичные методы / опции конфига | minor | bot.use(), BotConfig.onMetrics, commandIs() | | Изменение поведения без смены сигнатуры | minor | новый параметр с дефолтом | | Breaking change типов публичного API | major | изменение Context, Middleware<Ctx> | | Breaking change рантайм-поведения | major | изменение семантики next(), concurrency | | Bugfixes, внутренние рефакторинги | patch | — |

Стабильный API (c v1.0.0):

  • Bot, BotConfig, Context, EventHandler
  • MessageContext, CallbackContext, ChatContext, BotStartedContext
  • ctx.match — результат RegExp capture-групп из hears()/action()
  • ctx.startPayload — payload из deep link в BotStartedContext
  • ctx.editMessage(), ctx.deleteMessage(), ctx.sendAction() — редактирование, удаление, действие
  • bot.onMessage(), bot.onCallback(), bot.onStart(), bot.on(), bot.start(), bot.stop()
  • bot.command(), bot.hears(), bot.action() — variadic middlewares: bot.command('x', mw1, mw2, handler)
  • HearsTrigger — массив паттернов: bot.hears([/^\d+$/, 'помощь'], handler) срабатывает на любой из паттернов
  • polling.allowedUpdates — фильтр типов обновлений
  • bot.api.*

Experimental (могут поменяться до v2.0.0 minor-версией):

  • BotMetrics, onMetrics — если понадобится расширить структуру метрик
  • Filter predicates — могут добавляться новые, сигнатуры существующих стабильны

Лицензия

MIT