vvlad1973-telegram-framework
Version:
Current version: *7.9.5*
440 lines (380 loc) • 13.7 kB
JavaScript
;
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;
}