Skip to content

DementevVV/maxbot-ts

Repository files navigation

@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

About

Typed TypeScript SDK for MAX Bot API - middleware pipeline, long polling & webhook, inline keyboards, Composer routers, Filter DSL, Histogram metrics.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors