UNPKG

umbot

Version:

Мультиплатформенный фреймворк для создания голосовых навыков и чат-ботов с единой бизнес-логикой. Встроенная поддержка ВКонтакте, Telegram, Viber, MAX, Яндекс Алисы, Маруси и Сбера SmartApp. Архитектура на адаптерах позволяет подключать любые другие платф

244 lines (243 loc) 14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BasePlatform = exports.EMPTY_CONTEXT_ERROR = exports.EMPTY_QUERY_ERROR = void 0; const Base_1 = require("../../Base"); const index_1 = require("../../../index"); const crypto_1 = require("crypto"); exports.EMPTY_QUERY_ERROR = 'Получено пустое тело запроса от платформы, дальнейшая корректная работа невозможна. Скорее всего запрос пришел не от платформы.'; exports.EMPTY_CONTEXT_ERROR = 'Не указан контекст приложения, дальнейшая работа приложения невозможна. Проверьте корректность настройки приложения.'; /** * Базовый адаптер для создания голосовых навыков и чат-ботов для собственной платформы (WeChat, WhatsApp, Slack и др.). * * Чтобы подключить другую платформу, которая не поставляется из коробки, унаследуйтесь от класса и реализуйте * все абстрактные методы. Адаптер автоматически зарегистрируется в системе * при подключении через `bot.use(new MyPlatformAdapter())`. * * === Обязательные методы === * - {@link isPlatformOnQuery} — определяет, относится ли запрос к платформе или нет * - {@link setQueryData} — обрабатывает запрос и заполняет `controller` данными * - {@link getContent} — формирует ответ в формате платформы * * === Опциональные === * - {@link getQueryExample} — генерирует пример запроса необходимого для тестов * - {@link isLocalStorage}, {@link getLocalStorage}, {@link setLocalStorage} — если платформа поддерживает сохранение локального состояния * - {@link soundProcessing} — кастомная обработка TTS/звуков (для голосовых платформ, или отправка аудиофайла в боте). * * @see Bot * @see BotController * @see BasePlatform */ class BasePlatform extends Base_1.BasePlugin { /** * Время ответа навыка в миллисекундах при превышении этого времени, будет отправлено предупреждение */ WARMING_TIME_REQUEST = 2000; /** * Максимальное время ответа навыка в миллисекундах при превышении этого времени, будет отправлена ошибка */ MAX_TIME_REQUEST = 2900; _token; _platformOptions; /** * Имя поля в заголовке запроса, по которому можно проверить корректность полученного запроса от платформы. */ signatureName; /** * Определяет лимит платформы. * В значение указывается количество запросов, которое можно отправить платформе за 1 секунду. * В случае если у платформы нет ограничений, можно указать 0 или null. * По умолчанию null */ limit = null; /** * Контекст приложения */ appContext; /** * Имя платформы */ platformName = 'unknown'; /** * Определяет тип платформы(голосовая или чат-бот) */ isVoice = true; constructor(platformToken, additionalPlatformOptions) { super(); this._token = platformToken; this._platformOptions = additionalPlatformOptions; } /** * Инициализация адаптера. * Определять не обязательно. Стоит указывать в случаях, когда нужно выполнить доп логику, например указать токены или писать какую-то статистику по использованию. * @param appContext */ init(appContext) { appContext.platforms[this.platformName] = this; this.appContext = appContext; appContext.appConfig.tokens[this.platformName] ??= {}; } /** * Генерирует пример входящего запроса для локального тестирования вашего приложения. * Позволяет эмулировать запрос от платформы с заданным текстом, ID пользователя, номером сообщения и состоянием. * Необходимо указывать для того, чтобы можно было корректно проверить работоспособность приложения. * * Обязательно определите метод, если планируется тестирования приложения через инструменты предоставляемые платформой. Это существенно упростит процесс разработки приложения. * @param query Запрос пользователя * @param userId Идентификатор пользователя * @param count Порядковый номер запроса * @param state Данные из локального хранилища */ getQueryExample(query, userId, count, state) { // Формат запроса от вашей платформы return { text: query, userId, messageId: count, state, }; } /** * Проверяет полученный запрос от платформы на корректность. * Из коробки проверка идет по sha256. Если по какой-то причине поведение по умолчанию не подходит, то просто переопределите метод. * @param query * @param headers */ isCorrectQuery(query, headers) { if (this.appContext?.appConfig.tokens[this.platformName]?.token && this.signatureName) { if (!headers?.[this.signatureName]) { return false; } const token = this.appContext?.appConfig.tokens[this.platformName].token; const payload = typeof query === 'string' ? query : JSON.stringify(query); const expected = (0, crypto_1.createHmac)('sha256', token).update(payload).digest('hex'); try { return (0, crypto_1.timingSafeEqual)(Buffer.from(headers[this.signatureName]), Buffer.from(expected)); } catch { return false; } } return true; } /** * Формирует ответ с оценкой * @param controller - контроллер приложения */ getRatingContext(controller) { return this.getContent(controller); } /** * Отправка текста пользователю * Этот метод используется для активных рассылок — когда голосовой навык или чат-бот инициирует диалог первым (например, уведомление). * В методе реализована механика преобразования текстового значения `controllerOrText` в контроллер, а также базовый механизм для отправки ответа. * * Переопределять данный метод не рекомендуется. Переопределить стоит только в том случае, если по каким-то технических условиям текущая реализация метода вам не подходит. * * Если платформа не поддерживает возможность начать диалог самостоятельно, то можно оставить метод пустым, либо вывести любую заглушку. * @param userId Ид пользователя, которому нужно отправить сообщение * @param controllerOrText Контроллер приложения или текст. Если необходимо отправить просто текст, можно передать строку, в случае, если необходимо передать картинку звук и тд, то необходимо корректно заполнить контроллер. */ send(userId, controllerOrText) { let controller; if (typeof controllerOrText === 'string') { controller = new index_1.BaseBotController(this.appContext); controller.text = controllerOrText; } else { controller = controllerOrText; } if (!controller.userId) { controller.userId = userId; } return this.getContent(controller); } /** * Устанавливает время начала обработки запроса. * Используется для измерения времени обработки запроса */ #initProcessingTime(controller) { controller.platformOptions.timeStart = Date.now(); } /** * Устанавливает время начала обработки запроса. * Используется для измерения времени выполнения */ updateTimeStart(controller) { this.#initProcessingTime(controller); } /** * Получает время выполнения запроса в миллисекундах * @returns {number} Время выполнения запроса */ getProcessingTime(controller) { return Date.now() - controller.platformOptions.timeStart; } /** * При превышении установленного времени исполнения, пишет информацию в лог * После вызова getContent() автоматически проверяется время выполнения. * Если оно превышает 2000 мс — пишется warning, если >2900 мс — ошибка. */ _timeLimitLog(controller) { const timeEnd = this.getProcessingTime(controller); if (timeEnd >= this.MAX_TIME_REQUEST) { controller.platformOptions.error = `${this.constructor.name}:getContent(): Превышено ограничение на отправку ответа. Время ответа составило: ${timeEnd / 1000} сек.`; } else if (timeEnd >= this.WARMING_TIME_REQUEST) { this.appContext?.logWarn(`${this.constructor.name}:getContent(): Время ответа составило: ${timeEnd / 1000} сек, рекомендуется проверить нагрузку на сервер, и корректность работы самого приложения.`); } } /** * Дополнительная обработка для звуков. * В данном методе стоит реализовать логику, с помощью которой будут наложены дополнительные эффекты для озвучивания текста пользователю * @param _controller */ soundProcessing(_controller) { // custom logic } /** * Инициализирует TTS (Text-to-Speech) в контроллере. * Обрабатывает звуки и стандартные звуковые эффекты * @param controller Тип приложения * @protected */ _initTTS(controller) { if (controller.sound.sounds.length || controller.sound.isUsedStandardSound) { controller.tts ??= controller.text; return this.soundProcessing(controller); } } /** * Указывает, поддерживает ли платформа локальное хранилище. * * @param _controller - контроллер приложения * @returns `true`, если локальное хранилище доступно */ isLocalStorage(_controller) { return false; } /** * Получает данные из локального хранилища платформы. * * @param _controller - контроллер приложения * @returns данные, сохранённые ранее */ getLocalStorage(_controller) { return null; } /** * Сохраняет данные в локальное хранилище платформы. * * @param _data - данные для сохранения * @param _controller - контроллер приложения */ setLocalStorage(_data, _controller) { // TODO document why this method 'setLocalStorage' is empty } /** * Флаг, указывающий, что платформа голосовая (например, Алиса, Маруся). */ static isVoice() { return true; } } exports.BasePlatform = BasePlatform;