umbot
Version:
Мультиплатформенный фреймворк для создания голосовых навыков и чат-ботов с единой бизнес-логикой. Встроенная поддержка ВКонтакте, Telegram, Viber, MAX, Яндекс Алисы, Маруси и Сбера SmartApp. Архитектура на адаптерах позволяет подключать любые другие платф
244 lines (243 loc) • 14 kB
JavaScript
;
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;