umbot поддерживает middleware в стиле telegraf и vk-io — функции, которые вызываются до запуска бизнес-логики (BotController.action).
import { T_ALISA } from 'umbot/plugins';
// Глобальный middleware (для всех платформ)
bot.use(async (ctx, next) => {
console.log('Запрос:', ctx.appType);
await next(); // обязательно вызвать next() для продолжения
});
// middleware для конкретной платформы
bot.use(T_ALISA, async (ctx, next) => {
// requestObject — сырой payload платформы (unknown), нужен каст
const request = ctx.requestObject as Record<string, unknown> | null;
const session = request?.session as Record<string, unknown> | undefined;
if (!session?.user_id) {
ctx.text = 'Некорректный запрос';
ctx.isEnd = true;
// next() не вызывается → action() не запустится
return;
}
await next();
});
action(), команды, шаги): это не «луковица» в стиле Koa.
Порядок: сначала глобальная цепочка целиком — вместе с кодом после await next(), затем платформенная цепочка,
и только потом обработчик. Пример с глобальным mw1 (лог до/после next()) и платформенным mw2:
порядок 1, 4, 2, 3, action() — код после next() у mw1 выполняется до платформенной цепочки и до
обработчика. Поэтому после await next() ответ (ctx.text, кнопки) ещё не сформирован — не читайте его там.
Ответ целиком доступен в responseCb у bot.start() / bot.webhookHandle() (пример ниже).Каркас middleware-фабрики с настройками и тестом создаёт npx umbot add middleware <name> — файл
src/middleware/<name>.ts (подробнее — в описании CLI).
bot.use(async (ctx, next) => {
console.log(`[${ctx.appType}] Запрос от ${ctx.userId}: ${ctx.userCommand}`);
await next();
});
Ответ в middleware ещё не сформирован (обработчик выполняется после всех middleware). Чтобы залогировать ответ,
используйте responseCb встроенного сервера (у webhookHandle тот же третий аргумент):
bot.start('0.0.0.0', 3000, (_req, res, state) => {
console.log(`Ответ ${state.statusCode}:`, state.body);
state.defaultSend(res, state);
});
bot.use(async (ctx, next) => {
// Пропускаем приветствие и помощь
if (ctx.messageId === 0 || ctx.userCommand === 'помощь') {
await next();
return;
}
// Проверяем, авторизован ли пользователь
if (!ctx.userData?.isAuthorized) {
ctx.text = 'Для использования бота необходимо авторизоваться.';
ctx.isEnd = true;
return; // next() не вызываем — action() не запустится
}
await next();
});
import { T_TELEGRAM } from 'umbot/plugins';
// Только для Telegram
bot.use(T_TELEGRAM, async (ctx, next) => {
// ⚠️ payload может быть строкой или объектом — всегда сначала сужайте тип
const payload = ctx.payload as { command?: string } | null | undefined;
if (payload?.command === 'cancel') {
ctx.text = 'Действие отменено.';
ctx.isEnd = true;
return;
}
await next();
});
Фреймворк поставляется с встроенной middleware для ограничения частоты входящих запросов (rateLimiter). Лимит берётся из свойства limit адаптера платформы: у Telegram, VK, Viber и MAX он равен 30 req/sec. Исходящие сообщения MAX отдельно ограничиваются очередью API-клиента до 2 сообщений в секунду на диалог. Если свойство limit не задано или равно 0/null, rateLimiter пропускает запросы без ограничений.
import { rateLimiter } from 'umbot/middleware';
bot.use(rateLimiter()); // дефолт: queue=100, idle=60s
// или с кастомными параметрами
bot.use(rateLimiter(200, 120_000)); // queue=200, idle 2 мин
// общий лимит на платформу вместо лимита на пользователя
bot.use(rateLimiter(100, 60_000, (ctx) => ctx.appType ?? ''));
Ключ счётчика (getKey, третий параметр). По умолчанию лимит считается на пару {platform, userId}. Если у
платформы нет подписи вебхука или секрет не задан, userId задаёт отправитель запроса, и, меняя его, можно обойти
лимит на пользователя. getKey задаёт свой ключ (к нему добавляется платформа): ctx => ctx.appType ?? '' ограничивает
суммарную нагрузку на платформу. Помните, что лимит берётся из limit адаптера: у Алисы, SmartApp и Маруси он не задан,
и middleware их не ограничивает.
Что делает:
appContext.platforms[platform].limit (TG/VK/Viber/MAX = 30 по умолчанию; 0/null — без ограничений).{platform, userId}: счётчик запросов сбрасывается каждую секунду.maxQueueSize).ctx.platformOptions.rateLimitOverflow = true и
бросает RateLimitQueueOverflowError (экспорт из umbot/middleware). Ядро пишет ошибку в лог и не запускает
обработку команд, платформа получает 200 — повторной доставки не будет. Флаг и класс ошибки позволяют отличить
отказ по перегрузке от ошибки в бизнес-логике (например, в responseCb у bot.start()).destroyRateLimiter() (тоже из umbot/middleware) останавливает таймеры очистки и отклоняет ожидающие в очереди
запросы всех созданных лимитеров — для hot-reload и тестов; при обычном завершении процесса вызывать не нужно,
таймеры не держат процесс.inactivityTimeout) — O(1) на запись: поток запросов с
новыми userId (на Алисе и Марусе его задаёт отправитель) не превращает каждый запрос в обход всех ключей..unref() — не блокируют выход процесса.Важно: rateLimiter ограничивает входящие запросы (от платформы к вам), а не исходящие API-вызовы (от вас к API платформы).
Проверяет, имеет ли пользователь право продолжить диалог.
import { authGuard } from 'umbot/middleware';
// Белый список ID пользователей
const ADMIN_IDS = ['12345', '67890'];
bot.use(
authGuard((ctx) => ADMIN_IDS.includes(String(ctx.userId)), {
deniedText: 'Команда доступна только администраторам',
}),
);
// Асинхронная проверка — например, через БД
bot.use(
authGuard(async (ctx) => {
const user = await db.users.findOne({ id: ctx.userId });
return !!user?.isActive;
}),
);
Поведение:
check вернул true — вызывается next().false или check выбросил исключение — пользователю отправляется deniedText, next() НЕ вызывается.check логируются через appContext.logError, но не ломают pipeline.Сигнатура: authGuard(check, options?) где check: (ctx) => boolean | Promise<boolean>.
Присваивает каждому входящему запросу уникальный requestId — полезно для сквозного трейсинга логов.
import { requestId } from 'umbot/middleware';
bot.use(requestId());
// В других middleware или командах:
// ⚠️ addCommand требует непустые слоты (кроме welcome/help — для них есть дефолтные).
// Команда с пустым массивом слотов просто не зарегистрируется.
bot.addCommand('debug', ['debug'], (_, ctx) => {
console.log('request id:', ctx.platformOptions.requestId);
});
Что делает:
ctx.platformOptions.requestId = crypto.randomUUID() (или fallback на timestamp+random для старых рантаймов).Возвращает "сервис на техобслуживании", пока check() возвращает true.
import { maintenance } from 'umbot/middleware';
let isDown = false;
// В админ-команде можно менять isDown
bot.addCommand('admin_maintenance', ['включить обслуживание'], (_, ctx) => {
isDown = true;
ctx.text = 'Maintenance mode ON';
});
bot.use(
maintenance(() => isDown, {
message: 'Бот обновляется. Попробуйте через 5 минут.',
}),
);
Поведение:
check() возвращает false — запрос идёт дальше нормально.true — пользователю отправляется message, next() не вызывается.check() выбрасывает исключение — запрос пропускается (защита от аварийного отключения сервиса).check.Фильтрует входящие запросы по IP клиента.
import { ipFilter } from 'umbot/middleware';
// Только доверенные сети (например, диапазоны IP платформы)
bot.use(
ipFilter({
whitelist: ['203.0.113.0/24', '198.51.100.0/24'],
deniedText: 'Forbidden',
rejectWithoutIp: true, // запрос с неизвестным IP клиента тоже отклоняется
}),
);
// Или blacklist (запретить спам-диапазоны)
bot.use(
ipFilter({
blacklist: ['203.0.113.42', '198.51.100.0/24'],
}),
);
Поведение:
'192.168.1.10'), так и CIDR ('10.0.0.0/8').::ffff:127.0.0.1 → 127.0.0.1).ctx.platformOptions.clientIp. В bot.start()/webhookHandle() фреймворк заполняет его из
req.socket.remoteAddress; в serverless его нужно передать третьим аргументом:
bot.webhookEvent(body, headers, clientIp). Обработчик Yandex Cloud Functions, сгенерированный CLI
(create from-flow --usecloud), передаёт event.requestContext.identity.sourceIp.bot.run()/BotTest, webhookEvent() без clientIp): по умолчанию запрос пропускается без
фильтрации (одно предупреждение в лог), чтобы бот не ломался в dev/test окружении. Опция rejectWithoutIp: true
отклоняет такие запросы — включайте её в продакшене вместе с whitelist, иначе забытый clientIp молча отключает
фильтр. У long polling (bot.startPolling()) IP клиента нет вовсе: с rejectWithoutIp: true отклоняются все
обновления, а сам фильтр для polling не нужен.X-Forwarded-For не используется, потому что клиент может его
подделать. За прокси ограничивайте доступ на уровне самого прокси.⚠️ Важно: ipFilter НЕ заменяет реальную защиту через reverse proxy / фаервол. Это дополнительный уровень.
import { MiddlewareNext, BotController } from 'umbot';
export function myMiddleware(options: { skip?: boolean } = {}) {
return async (ctx: BotController, next: MiddlewareNext): Promise<void> => {
try {
// ваша логика ДО обработки запроса
} catch (e) {
ctx.appContext.logError('myMiddleware error', { error: e });
// Решите: скрыть ошибку (continue) или блокировать (return без next)
}
await next(); // обязательно, чтобы дальше шла обработка командами/шагами
// ваша логика ПОСЛЕ обработки (например, замеры времени, логирование ответа)
};
}
Правила:
next() или осознанно завершайте ответ через ctx.text = ... (без next()).bot.use(mw1); bot.use(mw2); → сначала mw1).bot.use(T_TELEGRAM, mw).Полный справочник — API v-3.1 · все версии.