TypeScript SDK для MAX Bot API.
- Базовый URL: https://platform-api.max.ru
- Node.js >= 18 (встроенный fetch)
Начало работы
Конфигурация
Возможности
- Inline‑клавиатура
- Загрузка файлов
- Работа с ошибками
- Метрики и наблюдаемость
- Рекомендации по concurrency
Маршрутизация
Продвинутое использование
Справочник
npm i @dementevdev/maxbot-tsimport { 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 ниже.
Пакет следует 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отключает ограничение и возвращает прежнее поведение.
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 в 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— приходят все типы (поведение по умолчанию).
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 вручную.
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 не влияет на производительность если не задан — замер включается только при его наличии.
// Срабатывает только если total time >= 200ms
bot.onSlowHandler(200, (ms, updateType) => {
console.warn(`Slow: ${updateType} — ${ms}ms`);
});Разница с onMetrics: onSlowHandler — простой threshold-алерт без детализации по middleware.
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),
});| Профиль бота | Рекомендуемое значение | Обоснование |
|---|---|---|
| Приватные диалоги, быстрые ответы | 10 (по умолчанию) |
Баланс параллелизма и защиты |
| Бот в большой группе / публичный | 20–50 |
Больше одновременных пользователей |
| Webhook-бот с тяжёлой бизнес-логикой | 5–10 |
Снизить нагрузку на downstream |
| Бот с медленными API-вызовами (> 1s) | 20–100 |
Большинство времени — I/O ожидание |
| Минимальный сервис / отладка | 1 |
Строгая очерёдность |
| Без ограничений (legacy-режим) | Infinity |
Не рекомендуется в проде |
Как определить правильное значение:
- Запустите с
onMetricsи наблюдайтеqueueSizeAtStart - Если очередь стабильно > 0 — увеличьте
concurrency - Если растут ошибки downstream API — уменьшите
- Ориентир:
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.
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-цепочках.
// Команды (срабатывает на /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.
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) => {
/* ... */
});При использовании 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_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 |
Методы доступны в 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— см. раздел Метрики и наблюдаемость выше.
Набор предикатов для 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 — независимый роутер, который собирается отдельно от бота и подключается через 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();const outer = new Composer();
const inner = new Composer();
inner.command("nested", handler);
outer.use(inner.middleware());
bot.use(outer.middleware());| Метод | Описание |
|---|---|
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() |
Подключить 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());Проверить тип контекста прямо внутри обработчика с автоматическим сужением типа:
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)) {
// обрабатываем только приватный старт
}
});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}`);
});// 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);// 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();
});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);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: "Да!" });
});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 до всех существующих handlersbot.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Доступ к низкоуровневым методам через 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";При нажатии кнопки request_contact бот получает сообщение с вложением типа contact. Полезные поля payload:
- vcf_info: vCard строка
- vcf_phone: телефон
- max_info: объект пользователя (User)
При нажатии кнопки request_geo_location бот получает сообщение с вложением типа location:
- latitude: число
- longitude: число
Модули 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.
| Тип изменения | Версия | Примеры |
|---|---|---|
| Новые публичные методы / опции конфига | 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,EventHandlerMessageContext,CallbackContext,ChatContext,BotStartedContextctx.match— результат RegExp capture-групп изhears()/action()ctx.startPayload— payload из deep link вBotStartedContextctx.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 — могут добавляться новые, сигнатуры существующих стабильны