umbot
Version:
Мультиплатформенный фреймворк для создания голосовых навыков и чат-ботов с единой бизнес-логикой. Встроенная поддержка ВКонтакте, Telegram, Viber, MAX, Яндекс Алисы, Маруси и Сбера SmartApp. Архитектура на адаптерах позволяет подключать любые другие платф
944 lines (943 loc) • 52.3 kB
TypeScript
import { IAppConfig, IAppParam, TAppType, TAppMode } from './interfaces/IAppContext';
import { TBotContent, TBotResponseCb, TCommandGroupMode, TPlugin } from './interfaces/IBot';
import { ICommandParam, TSlots, TCommandResolver, IStepParam } from './utils/CommandReg';
import { IncomingMessage, ServerResponse, Server } from 'node:http';
import { BotController, IUserData } from '../controller';
import { AppContext } from './AppContext';
import { ILogger } from './interfaces/ILogger';
/**
* Тип для класса контроллера приложения
*/
export type TBotControllerClass<T extends IUserData = IUserData> = new (appContext: AppContext) => BotController<T>;
/**
* Результат выполнения запроса от платформы - ответ, который будет отправлен пользователю
* Может быть ответом для Алисы, Маруси или текстовым сообщением
*
* @example
* ```ts
* // Ответ для Алисы
* const alisaResponse: TRunResult = {
* response: {
* text: 'Привет!',
* end_session: false
* },
* version: '1.0'
* };
*
* // Ответ для Маруси
* const marusiaResponse: TRunResult = {
* response: {
* text: 'Привет!',
* end_session: false
* },
* version: '1.0'
* };
*
* // Простой текстовый ответ
* const textResponse: TRunResult = 'Привет!';
* ```
*/
export type TRunResult = object | string;
export * from './interfaces/IBot';
/**
* Функция для обработки следующего шага в цепочке промежуточных функций
*/
export type MiddlewareNext = () => Promise<void>;
/**
* Функция промежуточной обработки
*/
export type MiddlewareFn = (ctx: BotController, next: MiddlewareNext) => void | Promise<void>;
/**
* Тип для кастомного обработчика определения платформы.
*
* Позволяет разработчику взять на себя определение типа платформы для входящего запроса.
* Обработчик выполняется **до** встроенного автоопределения через адаптеры.
*
* @param query - Тело запроса (обычно объект, может быть строкой, если запрос не распарсен).
* @param headers - HTTP-заголовки запроса (если доступны).
* @param detect - Функция, вызывающая стандартный механизм определения платформы
* (перебор зарегистрированных адаптеров). Принимает те же параметры,
* что и резолвер, и возвращает имя платформы или `null`.
* @returns Имя платформы (строка) или `null`, если платформа не определена.
*
* @example
* ```ts
* bot.setPlatformResolver((query, headers, detect) => {
* // Сначала пробуем стандартное определение
* const platform = detect(query, headers);
* if (platform === 'telegram' && headers?.['x-force-vk']) {
* return 'vk';
* }
* return platform;
* });
* ```
*/
export type TPlatformResolver = (query: unknown, headers?: Record<string, unknown>, detect?: (uBody: unknown, headers?: Record<string, unknown>) => TAppType | null) => TAppType | null;
/**
* Мультиплатформенный фреймворк для разработки голосовых навыков и чат-ботов. Он даёт единую бизнес-логику для все платформ — но одинаково эффективен, даже если вы работаете только с одной.
*
* **`Bot` — главный класс**, управляющий всем жизненным циклом приложения:
* - регистрацией платформ (Алиса, Telegram, VK, Маруся, Max и др.);
* - обработкой входящих запросов;
* - маршрутизацией команд;
* - middleware;
* - работой с базой данных;
* - логированием
* - метриками.
*
* Фреймворк построен на **адаптерах** — каждый адаптер отвечает за преобразование
* специфичного для платформы запроса в унифицированный `BotController`, который
* содержит всю информацию о пользователе, текст команды, NLU, состояние и методы ответа
* (кнопки, карточки, TTS). Вы пишете **один код**, а фреймворк доставляет его
* на все поддерживаемые платформы.
*
* ## 📖 КРАТКОЕ РУКОВОДСТВО
*
* 1. **Создайте приложение:** `const bot = new Bot();`
* 2. **Настройте токены:** `bot.setAppConfig({ tokens: { telegram: {token: '...'}} });`
* 3. **Добавьте логику при необходимости:** `bot.initBotController(MyController);`
* 4. **Добавьте команды:** `bot.addCommand('start', ['старт'], handler);`
* 5. **Запустите:** `bot.start();`
*
* ## 🎯 Ключевые возможности
* * - ✅ **Поддержка множества платформ** через подключаемые адаптеры (Алиса, Telegram, VK, Маруся и др.)
* * - ✅ **Единая логика** для голосовых навыков и ботов
* * - ✅ **Мощная система команд и интентов** с поддержкой регулярных выражений
* * - ✅ **Управление состоянием диалога** (шаги) и пользовательскими данными
* * - ✅ **Встроенная работа с БД** (MongoDB через плагины)
* * - ✅ **Middleware и плагины** для расширения функциональности
* * - ✅ **Гибкая настройка** (режимы разработки/продакшена, защита от ReDoS, кастомные резолверы команд)
* * - ✅ **Логирование и метрики** для отладки и мониторинга
*
* ## 🚀 БЫСТРЫЙ СТАРТ
* Создание простого Telegram бота
* ```ts
* import { Bot } from 'umbot';
* import { botPlatforms } from 'umbot/plugins';
*
* // 1. Создаем бота для Telegram
* const bot = new Bot();
*
* // 2. Настраиваем токен (рекомендуется через .env файл)
* bot.setAppConfig({
* env: 'local'
* });
*
* // 3. Говорим приложению, что нужно поддерживать только платформы для чат-ботов
* bot.use(botPlatforms);
*
* // 4. Добавляем команды
* bot.addCommand('help', ['помощь', 'справка'], (cmd, controller) => {
* controller.text = 'Я могу:\n• Приветствовать\n• Помогать\n• И многое другое!';
* });
*
* // 5. Запускаем сервер
* bot.start('localhost', 3000);
*
* // 6. Настройте webhook в Telegram: https://api.telegram.org/bot{YOUR_TOKEN}/setWebhook?url=https://ваш-домен/webhook
* ```
*
* Создание простого приложения со своим контроллером:
* ```ts
* const bot = new Bot();
* bot.setPlatformParams({
* intents: [{
* name: 'greeting',
* slots: ['привет', 'здравствуй']
* }]
* });
*
* class MyController extends BotController {
* public action(intentName: string | null): void {
* if (intentName === 'greeting') {
* this.text = 'Привет! Я ваш помощник 🤖';
* this.buttons
* .addBtn('Помощь')
* .addBtn('Настройки');
* }
* }
* }
*
* bot.initBotController(MyController);
* ```
* Использование с базой данных:
* ```ts
* import { Bot } from 'umbot'
* import { MongoAdapter } from 'umbot/plugins'
* const bot = new Bot();
* bot.use(new MongoAdapter({
* host: 'localhost',
* database: 'bot_db',
* user: 'user',
* pass: 'password'
* }));
* ```
*
* @template TUserData Тип пользовательских данных, по умолчанию {@link IUserData}
* @see BotController
*/
export declare class Bot<TUserData extends IUserData = IUserData> {
#private;
/**
* Полученный запрос от пользователя.
* Может быть JSON-строкой, текстом или null
*/
protected _content: TBotContent;
/**
* Создает новый экземпляр приложения
*
* @param {TAppType} [type] - Тип платформы (по умолчанию автоопределение)
* @param {TBotControllerClass} [botController] - Контроллер с логикой
*
* @throws {Error} Если не удалось инициализировать тип платформы
*
* @example
* ```ts
* // Создание навыка для Алисы
* const bot = new Bot(T_ALISA, MyController);
*
* // Создание бота для Telegram
* const bot = new Bot(T_TELEGRAM, MyController);
*
* // Создание бота для VK
* const bot = new Bot(T_VK, MyController);
*
* // Создание приложения по умолчанию
* const bot = new Bot();
* ```
*/
constructor(type?: TAppType, botController?: TBotControllerClass<TUserData>);
/**
* Явно устанавливает тип платформы для всего приложения. Стоит использовать в крайнем случае
* @param appType
*/
set appType(appType: TAppType | 'auto');
/**
* Возвращает установленный тип приложения.
*/
get appType(): string;
/**
* Задает режим работы с регулярным выражениями.
* При значении auto, регулярные выражения будут группироваться в группу, благодаря чему уменьшается время обработки. Логика начинает отрабатывать после того, как добавили более 300 команд.
* При значении no-group, группировка регулярных выражений производиться не будет, из-за чего каждое регулярное выражение будет обрабатываться отдельно. Указывать данное значение стоит в том случае, если вы получаете сильную деградацию при обработке групп.
* При значении group, все регулярные выражения будут добавляться в группу. Перед использованием данного значения, перепроверьте производительность, так как при группировке определенных регулярных выражений, производительность может быть ниже.
* @param mode - Определяет режим работы с регулярными выражениями.
*/
setCommandGroupMode(mode: TCommandGroupMode): this;
/**
* Устанавливает кастомный обработчик для определения типа платформы.
*
* По умолчанию фреймворк определяет платформу автоматически, перебирая зарегистрированные адаптеры
* и вызывая их метод `isPlatformOnQuery`. Этот метод позволяет переопределить логику определения,
* что полезно в следующих случаях:
* - Нужно добавить поддержку кастомной платформы, не создавая адаптер.
* - Требуется изменить стандартное поведение для конкретного набора запросов
* (например, по заголовкам или по содержимому запроса).
* - Необходимо выполнить какую-то логику до того, как запрос попадёт в адаптер.
*
* **Важно:** резолвер выполняется **до** вызова `isPlatformOnQuery` любого адаптера.
* Если резолвер возвращает строку (имя платформы), фреймворк использует это значение
* и не выполняет стандартное автоопределение. Если возвращает `null`, то запускается
* стандартный перебор адаптеров.
*
* В резолвер передаётся опциональная функция `detect`, которая вызывает встроенное
* автоопределение. Это позволяет, например, получить результат стандартного определения
* и затем скорректировать его.
*
* @param resolver - Функция, принимающая запрос, заголовки и опционально `detect`.
* Должна вернуть имя платформы (строка) или `null`.
* @returns Тот же экземпляр приложения для цепочечных вызовов.
*
* @example
* // Простейший резолвер, который для всех запросов использует платформу 'alisa'
* bot.setPlatformResolver(() => 'alisa');
*
* @example
* // Резолвер, который вызывает стандартное определение и при необходимости
* // заменяет результат для конкретного заголовка.
* bot.setPlatformResolver((query, headers, detect) => {
* const platform = detect?.(query, headers);
* if (platform === 'telegram' && headers?.['x-force-vk']) {
* return 'vk';
* }
* return platform;
* });
*
* @example
* // Полное переопределение: обработка запросов от собственного сервиса.
* bot.setPlatformResolver((query) => {
* if (typeof query === 'object' && query?.source === 'my_service') {
* return 'my_platform';
* }
* return null;
* });
*
* @remarks
* - Резолвер может быть асинхронным, если это необходимо. Для этого просто объявите
* функцию как `async` и возвращайте `Promise<string | null>`. Фреймворк дождётся
* результата перед продолжением.
* - Внутри резолвера можно модифицировать `query` или `headers`, если нужно повлиять
* на дальнейшую обработку (например, добавить недостающие поля).
* - Если резолвер возвращает имя платформы, для которой нет зарегистрированного
* адаптера, фреймворк выбросит ошибку. Убедитесь, что нужный адаптер подключен.
*/
setPlatformResolver(resolver: TPlatformResolver): this;
/**
* Позволяет установить свою реализацию для логирования
* @param logger
*/
setLogger(logger: ILogger | null): this;
/**
* Регистрирует команду — обработчик, срабатывающий при совпадении входящего текста с одним из шаблонов.
*
* Поиск команд оптимизирован:
* 1. Сначала проверяется точное совпадение
* 2. Если точного совпадения нет — выполняется последовательный перебор в порядке регистрации
*
* Первая совпавшая команда выполняется.
*
* @param {string} commandName - Уникальное имя команды (например, `'greeting'`). Используется для логирования и отладки.
* @param {TSlots} slots - Массив шаблонов для сопоставления:
* - Если элемент — строка → ищется как подстрока (`text.includes(...)`).
* - Если элемент — RegExp → проверяется как регулярное выражение (`.test(text)`).
* - Параметр `isPattern` учитывается **только если в `slots` нет RegExp**.
* - При наличии хотя бы одного `RegExp`, `isPattern = false` игнорируется, и каждый элемент
* обрабатывается согласно своему типу.
* @param {ICommandParam['cb']} cb - Обработчик команды. Принимает:
* - `text` — исходный текст от пользователя;
* - `controller` — экземпляр `BotController` для формирования ответа (кнопки, текст, шаги, данные и т.д.);
*
* Поддерживает `async`.
* @param {boolean} isPattern - Если `true` и в `slots` **нет RegExp**, все строки преобразуются в регулярные выражения.
* ⚠️ Используйте с осторожностью: возможен ReDoS. Все RegExp проверяются на уязвимости.
*
* @example
* Простая текстовая команда:
* ```ts
* bot.addCommand(
* 'greeting',
* ['привет', 'здравствуй'],
* (cmd, ctrl) => {
* ctrl.text = 'Здравствуйте!';
* }
* );
* ```
*
* @example
* Команда с регулярными выражениями:
* ```ts
* // Обработка чисел от 0 до 999
* bot.addCommand(
* 'number',
* ['\\b(\\d{0,3})\\b'],
* (cmd, ctrl) => {
* ctrl.text = `Вы ввели число: ${cmd}`;
* },
* true // включаем поддержку регулярных выражений
* );
* ```
*
* @example
* Команда с доступом к состоянию:
* ```ts
* bot.addCommand(
* 'stats',
* ['статистика'],
* async (cmd, ctrl) => {
* if (ctrl) {
* // Доступ к пользовательским данным
* const visits = ctrl.userData?.visits || 0;
* ctrl.text = `Вы использовали приложение ${visits} раз`;
*
* // Доступ к кнопкам и другим UI элементам
* ctrl.buttons
* .addBtn('Сбросить статистику')
* .addBtn('Закрыть');
* }
* }
* );
* ```
*
* @example
* // Асинхронная команда (работа с API):
* ```ts
* bot.addCommand('weather', ['погода'], async (text, controller) => {
* const weather = await fetch('Какой-то сервис для получения погоды');
* controller.text = `Погода: ${await weather.text()}`;
* });
* ```
*
* @example
* // Fallback: срабатывает, если ни одна команда не подошла:
* ```ts
* bot.addCommand('*', [], (text, controller) => {
* controller.text = `Извините, я не понял "${text}". Скажите "помощь" для списка команд.`;
* });
* ```
*
* @remarks
* Поиск команд оптимизирован:
* 1. Сначала проверяется точное совпадение
* 2. Если точного совпадения нет — выполняется последовательный перебор
*
* При регистрации более 300 команд с регулярными выражениями
* фреймворк автоматически объединяет их в группы для повышения производительности.
*
* При isPattern=true используются регулярные выражения JavaScript
* В callback доступен весь функционал BotController
* Можно использовать async функции в callback
*/
addCommand<TBotController extends BotController = BotController>(commandName: string, slots: TSlots, cb: ICommandParam<TBotController>['cb'], isPattern?: boolean): this;
/**
* Удаляет зарегистрированную команду по имени
* @param commandName - Имя команды
*/
removeCommand(commandName: string): this;
/**
* Удаляет **все** зарегистрированные команды
*
* > ⚠️ Это **глобальная операция**: все сценарии станут недоступны.
* > Используйте с осторожностью (например, при перезагрузке логики приложения).
*/
clearCommands(): this;
/**
* Регистрирует обработчик для именованного шага диалога.
*
* Шаг — это часть **многошагового сценария** (например: "регистрация", "оформление заказа").
* После вызова `ctx.thisIntentName = 'myStep'` в команде или другом шаге,
* следующее сообщение пользователя будет обработано этим обработчиком.
*
* > 💡 Обработчик получает полный `BotController`, как и в командах:
* > доступны `this.text`, `this.userData`, `this.buttons` и т.д.
*
* @param stepName — Уникальное имя шага (например, `'enter_email'`).
* @param handler — Функция, вызываемая при получении сообщения в этом шаге.
* @returns Текущий экземпляр `Bot` (для цепочки вызовов).
*
* @example
* ```ts
* bot.addCommand('start', ['начать'], (_, ctx) => {
* ctx.text = 'Готовы начать приключение?';
* ctx.buttons.addBtn('Да').addBtn('Нет');
* ctx.thisIntentName = 'confirm';
* });
* bot.addStep('confirm', (ctx) => {
* if (ctx.userCommand === 'да') {
* ctx.text = 'Отлично! Добро пожаловать.';
* } else {
* ctx.text = 'Извините, вход запрещён.';
* ctx.thisIntentName = 'goodbye'; // переходим к другому шагу
* }
* });
* ```
* @example
* ```ts
* // Пример многошаговой формы
* bot.addCommand('order', ['заказать'], (_, ctx) => {
* ctx.text = 'Введите ваше имя:';
* ctx.thisIntentName = 'step_name';
* });
*
* bot.addStep('step_name', (ctx) => {
* ctx.userData.name = ctx.userCommand;
* ctx.text = `Приятно познакомиться, ${ctx.userData.name}! Теперь введите email:`;
* ctx.thisIntentName = 'step_email';
* });
*
* bot.addStep('step_email', (ctx) => {
* ctx.userData.email = ctx.userCommand;
* ctx.text = `Заказ оформлен! Имя: ${ctx.userData.name}, Email: ${ctx.userData.email}`;
* ctx.thisIntentName = null; // сбрасываем шаг
* });
* ```
*/
addStep<TBotController extends BotController = BotController>(stepName: string, handler: IStepParam<TBotController>['cb']): this;
/**
* Удаляет зарегистрированный шаг по имени.
*
* После удаления шаг больше не будет обрабатываться, даже если активен у пользователя.
* (Рекомендуется завершать активные сценарии через `ctx.clearStep()` перед удалением.)
*
* @param stepName — Имя шага для удаления.
* @returns Текущий экземпляр `Bot`.
*/
removeStep(stepName: string): this;
/**
* Удаляет **все** зарегистрированные шаги.
*
* > ⚠️ Это **глобальная операция**: все сценарии станут недоступны.
* > Используйте с осторожностью (например, при перезагрузке логики приложения).
*
* @returns Текущий экземпляр `Bot`.
*/
clearSteps(): this;
/**
* Удаляет **все** зарегистрированные платформы, плагины и middleware службы.
*
* > ⚠️ Это **глобальная операция**: все сценарии станут недоступны.
* > Используйте с осторожностью (например, при перезагрузке логики приложения).
*
* @returns Текущий экземпляр `Bot`.
*/
clearUse(): this;
/**
* Устанавливает режим работы приложения
*
* @param {'dev' | 'prod' | 'strict_prod'} appMode - Режим работы:
* - 'dev': подробные логи, отладочная информация, отключена проверка ReDoS
* - 'prod': минимальные логи, производительность, НО небезопасные RegExp всё равно регистрируются
* - 'strict_prod': строгая проверка безопасности — любая RegExp с потенциальным ReDoS отклоняется с ошибкой
*
* ⚠️ ВАЖНО: В продакшене всегда используйте 'strict_prod' для защиты от атак через регулярные выражения.
* Режим 'prod' оставлен для обратной совместимости, но небезопасен.
*
* @example
* // Для продакшена (обязательно!)
* bot.setAppMode('strict_prod');
*
* // Для разработки
* bot.setAppMode('dev');
* @param appMode
*/
setAppMode(appMode: TAppMode): this;
/**
* Позволяет заменить встроенный механизм сопоставления команд на **кастомный алгоритм поиска**.
*
* По умолчанию `umbot` использует **оптимизированный поиск**:
* 1. Сначала проверяется точное совпадение
* 2. Если точного совпадения нет — выполняется последовательный перебор с поддержкой:
* - подстрок (`includes`),
* - простых регулярных выражений.
*
* Это обеспечивает **предсказуемость**, **простоту отладки** и **соответствие поведению большинства платформ**:
* первая совпавшая команда (в порядке регистрации) — выигрывает.
*
* Однако при:
* - количестве команд >1000,
* - высокой нагрузке (>1000 RPS),
* - необходимости в fuzzy-поиске или сложной маршрутизации,
* вы можете подключить оптимизированный resolver.
*
* @param resolver - Функция вида `(userText: string, commands: Map<string, ICommand>) => string | null`.
* Должна вернуть имя команды или `null`, если совпадений нет.
*
* @example
* ```ts
* // Пример: кэширование частых запросов
* const cache = new Map<string, string | null>();
* bot.setCustomCommandResolver((text, commands) => {
* if (cache.has(text)) return cache.get(text)!;
*
* for (const [name, cmd] of commands) {
* if (cmd.slots && cmd.slots.some(slot => typeof slot === 'string' && text.includes(slot))) {
* cache.set(text, name);
* return name;
* }
* }
* cache.set(text, null);
* return null;
* });
* ```
*
* @remarks
* **Рекомендации при реализации resolver'а:**
* - Сохраняйте **порядок регистрации команд**, если логика зависит от приоритета.
* - Используйте **кэширование** для часто встречающихся фраз (но учитывайте потребление памяти).
* - Для fuzzy-поиска — рассмотрите `fuse.js`, `natural` или trie-структуры.
* - При работе с регулярными выражениями **обязательно проверяйте их на ReDoS**.
* - Избегайте тяжёлых синхронных операций — они блокируют event loop.
*/
setCustomCommandResolver(resolver: TCommandResolver): this;
/**
* Задаёт **инфраструктурную конфигурацию** приложения: подключение к БД, загрузку `.env`, и другие
* настройки, связанные с окружением выполнения (а не с бизнес-логикой приложения).
*
* > 🔒 **Безопасность**: никогда не храните секреты (пароли, токены, API-ключи) прямо в коде.
* > Всегда используйте `.env`-файлы или переменные окружения.
*
* @param {IAppConfig} config - Конфигурация приложения
*
* @example
* ```ts
* // Конфигурация с базой данных
* bot.setAppConfig({
* db: {
* host: 'localhost',
* database: 'bot_db',
* user: 'user',
* pass: 'password'
* }
* });
*
*
* @remarks
* Важно! Чувствительные данные рекомендуется сохранять в .env файл, передав путь к нему:
* ```ts
* bot.setAppConfig({
* env: './.env', // путь до файла
* });
* ```
*/
setAppConfig(config: Partial<IAppConfig>): this;
/**
* Возвращает контекст приложения — центральный объект для расширенной настройки фреймворка.
*
* **Когда использовать:**
* - Замена HTTP-клиента: `bot.getAppContext().httpClient = customFetch`
* - Доступ к зарегистрированным адаптерам: `bot.getAppContext().platforms`
* - Настройка метрик и логирования
*
* **Пример:**
* ```ts
* // Добавление таймаутов к запросам
* const customFetch: THttpClient = async (url, init) => {
* const controller = new AbortController();
* const timeout = setTimeout(() => controller.abort(), 5000);
* return fetch(url, { ...init, signal: controller.signal });
* };
*
* bot.getAppContext().httpClient = customFetch;
*
* ⚠️ Важно: Не модифицируйте внутренние поля контекста напрямую (например, commands, steps). Используйте публичные методы addCommand(), addStep().
*/
getAppContext(): AppContext;
/**
* Задаёт параметры, управляющие **логикой приложения** на всех платформах.
*
* Сюда входят:
* - список интентов по умолчанию (`help`, `welcome` и др.),
* - тексты ответов по умолчанию (`welcome_text`, `help_text`, `empty_text`),
* - другие настройки, не зависящие от конкретной платформы
*
* @param {IAppParam} params - Параметры платформы
*
* @example
* ```ts
* // Базовая настройка
* bot.setPlatformParams({
* intents: [{
* name: 'help',
* slots: ['помощь', 'справка']
* }],
* welcome_text: 'Привет! Я ваш помощник.',
* help_text: 'Скажите "помощь", чтобы увидеть команды.',
* empty_text: 'Извините, я не понял.'
* });
* ```
*/
setPlatformParams(params: IAppParam): this;
/**
* Устанавливает контроллер, метод `action()` которого вызывается **после обработки каждого запроса** —
* независимо от того, была ли распознана команда, активен ли шаг сценария, или обработан базовый интент.
*
* Метод `action()` получает флаги `isCommand` и `isStep`, чтобы вы могли:
* - добавить общую логику (аналитика, логирование, трассировка);
* - модифицировать ответ глобально (например, добавить рекламу, кнопку "Оценить");
* - применить кросс-функциональные правила (rate limiting, мутации текста и т.д.);
*
* > 💡 **Рекомендация**: основную бизнес-логику размещайте в командах (`addCommand`) и шагах (`addStep`),
* > а в `action()` — только **сквозную** логику, которую не хочется дублировать.
*
* Контроллер по умолчанию уже обрабатывает интенты `welcome`, `help` и `fallback`,
* но вы можете заменить его, если нужно кастомное поведение.
*
* @param {BotController<TUserData>} fn - Экземпляр контроллера, наследующий `BotController<TUserData>`
*
* @example
* ```ts
* class MyController extends BotController {
* public action(intentName: string): void {
* switch (intentName) {
* case 'greeting':
* this.text = 'Привет!';
* break;
* case 'help':
* this.text = 'Чем могу помочь?';
* break;
* }
* }
* }
*
* bot.initBotController(MyController);
* ```
*
* @example
* ```ts
* // Добавление рекламы ко всем ответам
* class MyController extends BotController {
* public action(
* intentName: string | null,
* isCommand: boolean = false,
* isStep: boolean = false
* ): void {
* // Добавим кнопку "Подписаться" везде, кроме шагов
* if (!isStep) {
* this.buttons.addBtn('Подписаться на рассылку', 'http://localhost');
* }
*
* // Логирование
* console.log(`Обработано: ${isCommand ? 'команда' : isStep ? 'шаг' : 'интент'} – ${intentName}`);
* }
* }
*
* bot.initBotController(MyController);
* ```
*/
initBotController(fn: TBotControllerClass<TUserData>): this;
/**
* Устанавливает контент запроса.
* Используется для передачи данных от пользователя в платформу.
* Не рекомендуется использовать напрямую, использовать только в крайнем случае, либо для тестов
*
* @param {TBotContent} content - Контент запроса
*
* @example
* ```ts
* // Установка текстового сообщения
* bot.setContent('Привет!');
*
* // Установка JSON-данных
* bot.setContent({
* request: {
* command: 'привет',
* original_utterance: 'Привет, мир!'
* }
* });
* ```
*/
setContent(content: TBotContent): void;
/**
* Очищает состояние пользователя
*/
protected _clearState(botController: BotController): void;
/**
* Регистрирует middleware, вызываемый **до** выполнения `BotController.action()`.
*
* Middleware получает доступ к полному `BotController` (включая `text`, `isEnd`, `userData`, `buttons` и т.д.)
* и может:
* - Модифицировать контекст
* - Прервать выполнение (если не вызвать `next()`)
* - Выполнить логирование, tracing, rate limiting и др.
*
* @example
* // Глобальный middleware (для всех платформ)
* bot.use(async (ctx, next) => {
* console.log('Запрос от:', ctx.appType);
* await next();
* });
*
* @param fn - Middleware-функция
* @returns Текущий экземпляр `Bot` для цепочки вызовов
*/
use(fn: MiddlewareFn): this;
/**
* Регистрирует middleware, вызываемый только для указанной платформы.
*
* @example
* // Только для Алисы
* bot.use(T_ALISA, async (ctx, next) => {
* if (!ctx.appContext.requestObject?.session?.user_id) {
* ctx.text = 'Некорректный запрос';
* ctx.isEnd = true;
* // next() не вызывается → action() не запускается
* return;
* }
* await next();
* });
*
* @param platform - Идентификатор платформы (`alisa`, `telegram`, `vk`, и т.д.)
* @param fn - Middleware-функция
* @returns Текущий экземпляр `Bot`
*/
use(platform: TAppType, fn: MiddlewareFn): this;
/**
* Регистрирует плагин — объект, расширяющий функциональность приложения.
*
* Плагин может быть как функцией, так и классом.
* Если он реализован как метод, то должен быть указан флаг isPlugin.
* В случае когда используется класс, то должен быть реализован метод `init(appContext: AppContext)`
*
* @param plugin — Объект плагина, совместимый с `TPlugin`.
* @returns Текущий экземпляр `Bot`.
*
* @example
* import {AlisaAdapter} from 'umbot/plugins'
* bot.use(new AlisaAdapter());
*/
use(plugin: TPlugin): this;
/**
* Установка контроллера. Используется только для тестирования
* @param botController
* @protected
*/
protected _setBotController(botController: BotController<TUserData>): void;
/**
* Получение контроллера приложения
* Не использовать!
* @private
*/
getBotController(): BotController<TUserData> | null;
/**
* Выполняет непосредственную обработку входящего запроса от платформы.
* Этот метод **не запускает HTTP-сервер** и **не обрабатывает HTTP-запросы напрямую** —
* он принимает уже распарсенные данные и возвращает результат обработки.
*
* Обычно вызывается **внутри {@link webhookHandle}**, но может использоваться напрямую,
* если вы реализуете собственный обработчик запросов, тестируете логику приложения
* или запускаете его вне HTTP-контекста (например, из консоли или очереди сообщений).
*
* @param {TAppType | null} [appType] - Тип приложения. Если не указан, будет определен автоматически в зависимости от запроса.
* @param {string | object} [content] - Входные данные для обработки (например, текст сообщения или объект запроса).
* @returns {Promise<TRunResult>} Результат обработки запроса
* @throws {Error} Если не удаётся определить платформу или отсутствуют данные для обработки.
*
* @example
* ```ts
* // Обработка запроса
* const result = await bot.run();
* console.log(result);
* ```
*/
run(appType?: TAppType | null, content?: string | object | null): Promise<TRunResult>;
/**
* Обрабатывает входящий webhook-запрос от поддерживаемой платформы (Telegram, VK, Алиса и др.).
* Метод автоматически распознаёт платформу по заголовкам или телу запроса и делегирует обработку
* соответствующему адаптеру. Ответ отправляется автоматически через переданный объект `res`.
*
* @param req - Объект входящего запроса (IncomingMessage или совместимый)
* @param res - Объект ответа (ServerResponse или совместимый)
* @param responseCb - Callback, для пользовательской обработки ответа пользователю. Стоит использовать в том случае, если есть необходимость переопределить стандартный ответ фреймворка.
* Если передан, ВЫ ДОЛЖНЫ вызвать res.end() самостоятельно.
* Без колбэка фреймворк автоматически завершит ответ через res.end().
*
* @example
* ```ts
* // Использование с Express
* import express from 'express';
* const app = express();
* app.use(express.json());
*
* const bot = new Bot();
*
* app.post('/webhook', (req, res) => bot.webhookHandle(req, res));
* ```
* @example
* // Использование с встроенным HTTP-сервером Node.js
* import { createServer } from 'http';
*
* const bot = new Bot();
* const server = createServer((req, res) => {
* if (req.method === 'POST' && req.url === '/webhook') {
* bot.webhookHandle(req, res);
* } else {
* res.statusCode = 404;
* res.end();
* }
* });
* server.listen(3000);
* @example
* ```ts
* // Пример с переопределением ответа
* import { createServer } from 'http';
*
* const bot = new Bot();
* const server = createServer((req, res) => {
* if (req.method === 'POST' && req.url === '/webhook') {
* // В случае если вернулся статус отличный от 200, вернет содержимое какой-то страницы.
* bot.webhookHandle(req, res, (_reg: IncomingMessage, _res: ServerResponse, state: IBotResponseState) => {
* if (state.statusCode === 200) {
* return state.defaultSend(_res, state);
* }
* res.statusCode = 200;
* res.end(...);// Какое-то содержимое страницы
* });
* } else {
* res.statusCode = 404;
* res.end();
* }
* });
* server.listen(3000);
*
* ```
*/
webhookHandle(req: IncomingMessage, res: ServerResponse, responseCb?: TBotResponseCb): Promise<void>;
/**
* Запускает встроенный HTTP-сервер на указанном хосте и порту для приёма webhook-запросов
* от поддерживаемых платформ. Сервер использует нативный `http.createServer`.
*
* Метод возвращает экземпляр `http.Server`, что позволяет, например, корректно
* остановить сервер или добавить обработчики событий (`'listening'`, `'error'` и т.д.).
*
* Если требуется интеграция с фреймворком (например, Express, Fastify и др.),
* рекомендуется использовать {@link webhookHandle} как middleware-обработчик.
*
* @see webhookHandle
*
* @param {string} hostname - Имя хоста
* @param {number} port - Порт
* @param responseCb - Callback, для пользовательской обработки ответа пользователю. Стоит использовать в том случае, если есть необходимость переопределить стандартный ответ фреймворка.
*
* @example
* ```ts
* // Запуск встроенного сервера
* const bot = new Bot();
* bot.start('0.0.0.0', 8080);
*
* // Интеграция с Express (вместо встроенного сервера)
* import express from 'express';
* import { Bot } from 'umbot';
*
* const bot = new Bot();
* const app = express();
* app.use(express.json());
* app.post('/webhook', (req, res) => bot.webhookHandle(req, res));
*
* app.listen(3000, () => {
* console.log('Bot listening on port 3000');
* });
* ```
* ```ts
* // Пример с переопределением ответа
* const bot = new Bot();
* // В случае если вернулся статус отличный от 200, вернет содержимое какой-то страницы.
* bot.start('0.0.0.0', 8080, (reg: IncomingMessage, _res: ServerResponse, state: IBotResponseState) => {
* if (state.statusCode === 200) {
* return state.defaultSend(_res, state);
* }
* res.statusCode = 200;
* res.end(...);// Какое-то содержимое страницы
* });
* ```
*/
start(hostname?: string, port?: number, responseCb?: TBotResponseCb): Server;
/**
* Корректно завершает работу встроенного HTTP-сервера (если он был запущен через {@link start}).
* Ожидает завершения всех текущих запросов, освобождает сетевые ресурсы и отменяет
* все активные асинхронные операции, связанные с жизненным циклом приложения.
*
* Метод безопасен для повторного вызова.
*
* @returns {Promise<void>} Завершается, когда сервер остановлен и все ресурсы освобождены.
*
* @example
* ```ts
* const bot = new Bot();
* bot.start('localhost', 3000);
*
* // ... позже, при завершении приложения
* await bot.close();
* ```
*/
close(): Promise<void>;
/**
* Отправка текста пользователю
* Этот метод используется для активных рассылок — когда голосовой навык или чат-бот инициирует диалог первым (например, уведомление).
* В методе реализована механика преобразования текстового значения `controllerOrText` в контроллер, а также базовый механизм для отправки ответа.
*
* Если платформа не поддерживает возможность начать диалог самостоятельно, то вернется false
* @param userId Ид пользователя, которому нужно отправить сообщение
* @param controllerOrText Контроллер приложения или текст. Если необходимо отправить просто текст, можно передать строку, в случае, если необходимо передать картинку звук и тд, то необходимо корректно заполнить контроллер.
* @param platform Платформа, на которую необходимо отправить запрос
*/
send(userId: string | number, controllerOrText: BotController | string, platform: TAppType): Promise<unknown | boolean>;
}