UNPKG

vvlad1973-telegram-framework

Version:
440 lines (380 loc) 13.7 kB
'use strict'; import { safe as jsonc } from 'jsonc'; import { getCallerName, isTextMatch, clone } from 'vvlad1973-utils'; import { ValidationError } from 'vvlad1973-error-definitions'; import SimpleLogger from 'vvlad1973-simple-logger'; export class Scenario { actions = {}; context = {}; script = []; sender; filler; logger; stackCounter = 0; constructor(logger) { this.actions['setDelay'] = this.setDelay; this.actions['isMatch'] = this.isMatch; this.actions['doSwitch'] = this.doSwitch; this.actions['doDialog'] = this.doDialog; this.logger = logger ?? new SimpleLogger(); } #logEntry(functionName) { functionName = functionName ?? getCallerName(); this.logger.trace(`Function ${functionName}() execution...`); } #logExit(functionName) { functionName = functionName ?? getCallerName(); this.logger.trace(`Function ${functionName}() complete`); } loadDialogs(dialogs) { return new Promise((resolve, reject) => { let self = this, scenario = [], keys = []; function loadItem(item) { let fileName; if (typeof item === 'string') fileName = item; else if (typeof item === 'object') if (item.file) fileName = item.file; else if (item.collection) collection = item.collection; if (fileName) { try { let [error, bulk] = jsonc.readSync(fileName, { stripComments: true, }); if (error) throw new ValidationError( `Corrupt dialog file ${fileName}: ${error.message}` ); // bulk = require(path.resolve(process.cwd(), fileName)); if (Array.isArray(bulk)) { for (let [index, dialog] of bulk.entries()) { if (dialog._id) { if (keys.includes(dialog._id)) self.logger.warn( `Dialog ID ${dialog._id} is duplicated and was skipped` ); else { self.logger.trace(`Dialog ID ${dialog._id} was loaded`); keys.push(dialog._id); scenario.push(dialog); } } else throw new ValidationError( `Unknown dialog ID for item ${index}` ); } } else throw new ValidationError('Wrong format of dialogs file'); } catch (error) { self.logger.error(error.message); } } } if (Array.isArray(dialogs)) { for (let item of dialogs) loadItem(item); } else loadItem(dialogs); self.script = scenario; return resolve(); }); } doAction(data, request, user, context, self) { self = self ?? this; self.stackCounter++; self.#logEntry(`Scenario.doAction[${self.stackCounter}]`); let actionsData = clone(data); return new Promise(async (resolve, reject) => { if (typeof actionsData === 'object') { if (Array.isArray(actionsData)) { for (const item of actionsData) { try { await self.doAction(item, request, user, context, self); } catch (error) { // self.logger.error(error); return reject(error); } } } else if (typeof self.actions[actionsData.action] === 'function') { self.logger.info( `[userId=${user?.id}] Action ${actionsData.action} execution...` ); self.logger.debug( actionsData.options, `[userId=${user?.id}] Action options:` ); try { let result; try { result = await self.actions[actionsData.action]( actionsData, request, user, context, self ); } catch (error) { if (actionsData.result?.false) { result = false; self.logger.error(error); } else throw error; } self.logger.info( `[userId=${user?.id}] Action ${actionsData.action} complete` ); self.logger.debug(result, `[userId=${user?.id}] Action result:`); if (actionsData.result?.true && result) { self.logger.info( `[userId=${user?.id}] Result True action is defined. Action will execute...` ); await self.doAction( actionsData.result.true, request, user, context, self ); } else if ( actionsData.result?.false && typeof result !== 'undefined' && !result ) { self.logger.info( `[userId=${user?.id}] Result False action is defined. Action will execute...` ); await self.doAction( actionsData.result.false, request, user, context, self ); } } catch (error) { return reject(error); } } else if (actionsData.action) self.logger.warn( `[userId=${user.id}] Action ${actionsData.action} not defined` ); else self.logger.error( actionsData, `[userId=${user.id}] Unknown format of data for doAction()` ); self.#logExit(`Scenario.doAction[${self.stackCounter}]`); self.stackCounter--; return resolve(); } else self.logger.error( actionsData, `[userId=${user.id}] Unknown format of data for doAction()` ); }); } async setDelay(data, request, user, context, self) { let defaultDelay = context?.defaults?.delay, delay = data?.options?.delay, chatAction = data?.options?.chat_action, intervalId; if ( chatAction && user?.id && typeof context.telegramBot.sendChatAction === 'function' ) { await context.telegramBot.sendChatAction(user.id, chatAction); intervalId = setInterval( async () => await context.telegramBot.sendChatAction(user.id, chatAction), 3000 ); } delay = 1000 * (delay ?? (defaultDelay instanceof Promise ? await defaultDelay : defaultDelay) ?? 3); await new Promise((resolve, reject) => { setTimeout(async () => { if (intervalId) clearInterval(intervalId); return resolve(); }, delay); }); } async isMatch(data, request = {}, user = {}, context = {}, self = {}) { let { type, contents, params, chat, object } = request, options = data?.options, value = options?.value ?? contents, result = isTextMatch( value, options?.pattern, options?.is_regexp ?? options?.isRegExp, options?.flags ); return result; } async doSwitch(data = {}, request = {}, user = {}, context = {}, self = {}) { let { type, contents, params, chat, object } = request, options = data?.options, cases = data?.cases ?? options?.cases, defaultCase = data?.default ?? options?.default, isCaseFound = false, value = options?.value ?? contents; self.logger.info(`[userId=${user?.id}] Searching case...`); if (Array.isArray(cases)) for (const item of cases) { if ( isTextMatch( value, item.case, options?.is_regexp ?? options?.isRegExp, options?.flags ) ) { isCaseFound = true; self.logger.debug(`[userId=${user?.id}] Case found:\n${item.case}`); await self.doAction(item.actions, request, user, context, self); } } if (!isCaseFound) { if (defaultCase) { self.logger.debug( `[userId=${user?.id}] Case not found. Default actions will execute...` ); await self.doAction(defaultCase, request, user, context, self); } else self.logger.debug( `[userId=${user?.id}] Neither case nor default actions not found` ); } } async doDialog(data = {}, request = {}, user = {}, context = {}, self = {}) { let isDialogFound = false, dialogId = data?.options.dialog; if (dialogId) { self.logger.info( `[userId=${user?.id}] Searching dialog id=[${dialogId}]...` ); for (const item of self.script) if (item._id === dialogId) { isDialogFound = true; self.logger.info( `[userId=${user?.id}] Dialog id=[${dialogId}] was found` ); await self.doAction(item.actions, request, user, context, self); break; } if (!isDialogFound) self.logger.warn( `[userId=${user?.id}] Dialog id=[${dialogId}] not found` ); } } async process(request, user, context) { this.#logEntry(); // Если user — строка, преобразуем в объект с id if (typeof user === 'string') user = { id: user }; const resolvedUser = { id: user.id, channel: await resolveValue(user.channel), role: await resolveValue(user.role), state: await resolveValue(user.state), substate: await resolveValue(user.substate), }; const { chat, type, contents, params, chat_type, object, route, command } = request; let isDialogFound = false; // Логируем начало поиска this.logger.info(`[userId=%s] Searching dialog...`, resolvedUser?.id); this.logger.debug( { route, role: resolvedUser.role, state: resolvedUser.state, substate: resolvedUser.substate, command, type, contents, params, chat, object, chat_type, }, `[userId=${resolvedUser.id}] Searching dialog criterias:` ); // Поиск подходящего диалога for (const item of this.script) { if ( (item.is_active || ( typeof item.is_active === 'undefined' && ( item.received || item.user || item.chat || item.chat_type || item.state || item.substate || item.role || item.route || item.channel ) )) && (await checkDialog(request, resolvedUser, item)) ) { isDialogFound = true; // Логируем, если диалог найден this.logger.info( `[userId=${resolvedUser.id}] Found dialog _id=[${item._id}]. Executing...` ); this.logger.debug( item.actions, `[userId=${resolvedUser.id}] Dialog _id=[${item._id}] contents:` ); try { await this.doAction(item.actions, request, user, context, this); } catch (error) { this.logger.error(error); } this.logger.info( `[userId=${resolvedUser.id}] Dialog _id=[${item._id}] finished` ); if (!item.passThrough) { break; } } } // Логируем, если диалог не найден if (!isDialogFound) this.logger.info(`[userId=${resolvedUser.id}] Dialog not found. Done`); this.#logExit(); } } function matchCondition(value, pattern) { if (typeof pattern !== 'string') return true; // Если pattern не задан, любое значение подходит const strValue = value == null ? '' : String(value); return Boolean(strValue.match(new RegExp(pattern, 'i'))); } async function checkDialog(request, user = {}, dialogItem) { if (!dialogItem) return false; // Если dialogItem отсутствует, условия не выполняются const role = user.role instanceof Promise ? await user.role : user.role; const state = user.state instanceof Promise ? await user.state : user.state; const substate = user.substate instanceof Promise ? await user.substate : user.substate; const { chat, type, contents, params, chat_type, route, channel } = request; const item = { ...dialogItem }; if(typeof item.received?.command === 'string') { item.received.text = item.received.command; item.received.type = 'command'; } return ( matchCondition(state, item.state) && matchCondition(substate, item.substate) && matchCondition(role, item.role) && matchCondition(type, item.received?.type) && matchCondition(contents, item.received?.text) && matchCondition(params, item.received?.params) && matchCondition(chat, item.chat) && matchCondition(route, item.route) && matchCondition(channel, item.channel) && matchCondition(chat_type, item.chat_type) && matchCondition(user?.id ?? '', item.user) // Гарантируем, что user.id - строка ); } async function resolveValue(value) { return value instanceof Promise ? await value : value; }