UNPKG

wingbot

Version:

Enterprise Messaging Bot Conversation Engine

1,057 lines (883 loc) 32.7 kB
/* * @author David Menger */ 'use strict'; const { decompress } = require('compress-json'); const { brotliCompress, brotliDecompress } = require('zlib'); const { promisify } = require('util'); const { default: fetch } = require('node-fetch'); const assert = require('assert'); const path = require('path'); const Router = require('./Router'); const Plugins = require('./Plugins'); const Ai = require('./Ai'); const expected = require('./resolvers/expected'); const bounce = require('./resolvers/bounce'); const { cachedTranslatedCompilator } = require('./resolvers/utils'); const defaultResourceMap = require('./defaultResourceMap'); const { shouldExecuteResolver } = require('./resolvers/resolverTags'); const MESSAGE_RESOLVER_NAME = 'botbuild.message'; const PLUGIN_RESOLVER_NAME = 'botbuild.customCode'; /** @typedef {import('./Router').BaseConfiguration} BaseConfiguration */ /** * @typedef {object} BuildInfo * @prop {boolean} [expectedToAddResolver] * @prop {boolean} [attachedRouter] * @prop {boolean} [notLastMessage] */ /** * * @template {object} [S=object] * @template {BaseConfiguration} [C=object] * @typedef {import('./Router').Middleware<S,C>} Middleware */ /** * @typedef {object} Resolver * @prop {string|number} [id] - only for text messages with random characters * @prop {string} type * @prop {object} params * @prop {string} [params.staticBlockId] * @prop {object} [params.items] * @prop {string} [tag] */ /** @typedef {import('./resolvers/bounce').BounceAllow} BounceAllow */ /** @typedef {import('./resolvers/bounce').BounceReturn} BounceReturn */ /** * @typedef {object} Route * @prop {number} id * @prop {string|null} path * @prop {string|null} [skill] * @prop {Resolver[]} resolvers * @prop {boolean} [isFallback] * @prop {string[]} [aiTags] * @prop {boolean} [isResponder] * @prop {number} [respondsToRouteId] * @prop {string|any[]} [aiTitle] * @prop {boolean} [aiGlobal] * @prop {BounceAllow} [bounceAllowedTo] * @prop {BounceReturn} [bounceReturn] * @prop {boolean} [isEntryPoint] */ /** * @typedef {object} RouteTransformation * @prop {string} [expectedPath] * * @typedef {RouteTransformation & Route} TransformedRoute */ /** * @typedef {Map<string|number,string>} LinksMap */ /** * @typedef {Map<string|number, Block>} BlockMap */ /** @type {TransformedRoute} */ const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] }; /** * @typedef {object} Block * @prop {string} staticBlockId * @prop {Route[]} routes * @prop {boolean} [isRoot] * @prop {boolean} [disabled] * @prop {string} [blockName] * @prop {string} [blockType] */ /** * @typedef {object} BotConfig * @prop {string} botId - the ID of bot * @prop {string} snapshot - snapshot stage of bot * @prop {string|Promise<string>} token - authorization token for bot * @prop {string} [url] - specify alternative configuration resource */ /** * @typedef {object} ConfigStorage * @prop {{():Promise}} invalidateConfig * @prop {{():Promise<number>}} getConfigTimestamp * @prop {{(config:Object):Promise<Object>}} updateConfig * @prop {{():Promise<Object>}} getConfig */ /** * @typedef {object} RouteConfig * @prop {string} path * @prop {boolean} [enabled] */ /** * @callback LinkTranslator * @param {string} senderId * @param {string} textLabel * @param {string} urlText * @param {boolean} isExtUrl * @param {object} state * @param {string} pageId * @returns {string} */ /** * @typedef {object} ILogger * @prop {Function} log * @prop {Function} warn * @prop {Function} error */ /** * @template {BaseConfiguration} [C=object] * @typedef {object} BuildRouterContext * @prop {LinkTranslator} [linksTranslator] - function, that translates links globally * @prop {ConfigStorage} [configStorage] - function, that translates links globally * @prop {boolean} [allowForbiddenSnippetWords] - disable security rule * @prop {Middleware} [defaultPlugin] - to be able to test configurations without plugins * @prop {RouteConfig[]} [routeConfigs] - list of disabled routes * @prop {boolean|keyof ILogger} [canaryLogs] * @prop {ILogger} [log] * @prop {C} [configuration] - context data */ /** * @callback NestedLinksMapFactory * @returns {LinksMap} */ /** * @typedef {object} BotContextExtention * @prop {BlockMap} [nestedBlocksByStaticId] * @prop {LinksMap} [linksMap] * @prop {boolean} [isLastIndex] * @prop {boolean} [isLastMessage] * @prop {BuildRouter} [router] * @prop {string} [path] * @prop {boolean} [isFallback] * @prop {string} [expectedPath] * @prop {boolean} [isResponder] * @prop {string|number} [routeId] * @prop {string} [blockName] * @prop {string} [blockType] * @prop {boolean} [isRoot] * @prop {string} [staticBlockId] * @prop {Block[]} [blocks] * @prop {NestedLinksMapFactory} [nestedLinksMapFactory] * @prop {object} [BuildRouter] * @prop {Ai} [ai] * @prop {string|number} [resolverId] - only for text messages with random characters */ /** * @template {BaseConfiguration} [C=object] * @typedef {BotContextExtention & BuildRouterContext<C>} BotContext */ /** * Build bot from Wingbot configuration file or snapshot url * * @template {object} [S=object] * @template {BaseConfiguration} [C=object] * @class BuildRouter * @extends {Router<S,C>} */ class BuildRouter extends Router { /** * Create new router from configuration * * @constructor * @param {BotConfig|Block} block * @param {Plugins} plugins - custom code blocks resource * @param {BuildRouterContext<C>|Promise<BuildRouterContext<C>>} context - the building context * @param {fetch} [fetchFn] - override a request function * @example * * // usage of plugins * * const { BuildRouter, Plugins } = require('wingbot'); * const dynamoDb = require('./lib/dynamodb'); * const config = require('./config'); * * const plugins = new Plugins(); * * plugins.register('exampleBlock', async (req, res, postBack) => { * await res.run('responseBlockName'); * }); * * const bot = new BuildRouter({ * botId: 'b7a71c27-c295-4ab0-b64e-6835b50a0db0', * snapshot: 'master', * token: 'adjsadlkadjj92n9u9' * }, plugins); * * module.exports = bot; */ constructor (block, plugins = new Plugins(), context = {}, fetchFn = fetch) { super( context instanceof Promise ? context.then((c) => c.configuration) : context.configuration ); this._validateBlock(block); this._plugins = plugins; /** @type {BotContext<C>|Promise<BuildRouterContext<C>>} */ this._context = context; /** @type {BotContext<C>} */ this._resolvedContext = context instanceof Promise ? null : context; /** @type {LinksMap} */ this._linksMap = new Map(); /** @type {LinksMap} */ this._nestedLinksMap = null; this._loadBotUrl = null; this._loadBotAuthorization = null; this._botLoaded = null; this._fetch = fetchFn; this._prebuiltRoutesCount = null; this._prebuiltGlobalIntents = null; this._brotliCompress = promisify(brotliCompress); this._brotliDecompress = promisify(brotliDecompress); this.resources = defaultResourceMap(); this._loadBotAuthorization = 'token' in block ? block.token : null; /** @type {ConfigStorage|Promise<ConfigStorage>} */ this._configStorage = context instanceof Promise ? context.then((c) => c.configStorage) : context.configStorage; this._runningReqs = []; this._configTs = 0; /** * Timeout, when the router is not checking for new configuration * * @type {number} */ this.keepConfigFor = 60000; this._snapshot = null; this._botId = null; if ('routes' in block) { this._buildBot(block); } else if (typeof block.url === 'string') { this._loadBotUrl = block.url; } else if (typeof block.botId === 'string') { this._snapshot = block.snapshot || 'production'; this._botId = block.botId; this._loadBotUrl = `https://api.wingbot.ai/bots/${this._botId}/snapshots/${this._snapshot}/blocks`; } } get snapshot () { return this._snapshot; } get botId () { return this._botId; } _validateBlock (block, action = 'build') { // @ts-ignore assert.ok(block && typeof block === 'object', `Bot ${action} failed: expected the block to be an object, ${typeof block} given`); // @ts-ignore assert.ok( block.url || block.botId || block.routes, `Bot ${action} failed: "url", "botId" or "routes" in block, none given` ); // @ts-ignore assert.ok( block.url || block.botId || block.routes, `Bot ${action} failed: "url", "botId" or "routes" in block, none given` ); if (block.routes) { // @ts-ignore assert.ok(Array.isArray(block.routes), `Bot ${action} failed: "routes" in a block should be an array`); } } async preload () { if (this._botLoaded === null) { this._botLoaded = this._checkForBotUpdate() .then(() => { this._botLoaded = null; }) .catch((e) => { // be able to try in again later this._botLoaded = null; throw e; }); } return this._botLoaded; } async reduce (...args) { await this.preload(); let runningRequest; try { const reducePromise = super.reduce(...args); runningRequest = reducePromise .catch(() => { /* mute fails */ }); this._runningReqs.push(runningRequest); return await reducePromise; } finally { if (runningRequest) { this._runningReqs = this._runningReqs .filter((rr) => rr !== runningRequest); } } } async _checkForBotUpdate () { if (this._configTs > Date.now() - this.keepConfigFor) { // do not update recently updated of fixed configurations return; } /** @type {ConfigStorage} */ let configStorage; let snapshot; /** @type {BotContext<C>} */ let context; if (this._context instanceof Promise || this._configStorage instanceof Promise) { [ configStorage, snapshot, context ] = await Promise.all([ Promise.resolve(this._configStorage), this.loadBot(), this._context ]); this._context = context; this._resolvedContext = context; this._configStorage = configStorage; } else { configStorage = this._configStorage; context = this._context; } if (!configStorage) { // not need to wait for existing requests, there are no existing ones try { if (!snapshot) { snapshot = await this.loadBot(); } this.buildWithSnapshot(snapshot.blocks); } catch (e) { if (this._configTs > 0 && !snapshot) { // mute // eslint-disable-next-line no-console console.info('Loading new state failed - recovering', e); } else { throw e; } } return; } try { // check for current TS const ts = await configStorage.getConfigTimestamp(); if (ts <= this._configTs && this._configTs !== 0 && ts !== 0) { // do not update, when there is no better configuration return; } if (snapshot) { snapshot = await this._updateConfig(configStorage, snapshot); } if (ts !== 0 && !snapshot) { // probably someone has updated the configuration snapshot = await configStorage.getConfig(); snapshot = await this._decompressIfCompressed(snapshot); } if (!snapshot || typeof snapshot !== 'object' || !Array.isArray(snapshot.blocks)) { // there is no configuration, load it from server snapshot = await this.loadBot(); snapshot = await this._updateConfig(configStorage, snapshot); } // wait for running request await Promise.all(this._runningReqs); this.buildWithSnapshot(snapshot.blocks, snapshot.timestamp, snapshot.lastmod); } catch (e) { await configStorage.invalidateConfig(); throw e; } } async _updateConfig (configStorage, snapshot) { const buf = await this._brotliCompress(Buffer.from(JSON.stringify(snapshot))); const compressed = { compression: 'brotli', base64url: buf.toString('base64url'), timestamp: snapshot.timestamp }; const updated = await configStorage.updateConfig(compressed); const decoded = await this._decompressIfCompressed(updated); return decoded; } async _decompressIfCompressed (snapshot) { if (!snapshot || snapshot.compression !== 'brotli' || !snapshot.base64url) { return snapshot; } const buf = Buffer.from(snapshot.base64url, 'base64url'); const data = await this._brotliDecompress(buf); const parsed = JSON.parse(data.toString('utf8')); return { ...parsed, timestamp: snapshot.timestamp }; } /** * Loads conversation configuration * * @returns {Promise<object>} */ async loadBot () { const options = {}; let auth; if (this._loadBotAuthorization) { auth = await Promise.resolve(this._loadBotAuthorization); Object.assign(options, { headers: { Authorization: auth, 'X-Compressed-Blocks': '1' } }); } const response = await this._fetch(this._loadBotUrl, options); if (response.status === 404) { throw new Error(this._botId ? `Bot load failed: Snapshot '${this._snapshot}' does not exist or not deployed on botId '${this._botId}'` : `Bot load failed: Url ${this._loadBotUrl} does not exist (returned status 404)`); } if (response.status === 401 && !auth) { throw new Error('Bot load failed: 401 - missing authorization token'); } if (response.status === 403 || response.status === 401) { const reason = this._botId ? `The token probably does not match snapshot '${this._snapshot}' and botId '${this._botId}'` : `The call to '${this._loadBotUrl}' was unauthorized`; throw new Error(`Bot load failed: ${response.status} - ${reason}`); } if (response.status >= 400) { throw new Error(`Bot load failed: ${response.status} - ${response.statusText} (snapshot '${this._snapshot}', botId '${this._botId})`); } const snapshot = await response.json(); if (snapshot && snapshot.compressedBlocks) { snapshot.blocks = decompress(snapshot.blocks); } this._validateBlocks(snapshot && snapshot.blocks, 'load'); return snapshot; } _validateBlocks (blocks, action = 'build') { if (!Array.isArray(blocks)) { throw new Error(`Bot ${action} failed: expected array of "blocks" in the body`); } blocks.forEach((b) => this._validateBlock(b, action)); } buildWithSnapshot (blocks, setConfigTimestamp = Number.MAX_SAFE_INTEGER, lastmod = '-') { this._validateBlocks(blocks); Object.assign(this._resolvedContext, { blocks, nestedBlocksByStaticId: null }); const rootBlock = blocks.find((block) => block.isRoot); if (!rootBlock) { throw new Error('Root block (block.isRoot = true) not found - probably invalid bot snapshot used'); } this._buildBot(rootBlock, setConfigTimestamp, lastmod); } resetRouter () { if (this._prebuiltRoutesCount !== null) { this._routes = this._routes.slice(0, this._prebuiltRoutesCount - 1); this._configTs = 0; } if (this._prebuiltGlobalIntents !== null) { this.globalIntents.clear(); this._prebuiltGlobalIntents.forEach(([k, v]) => this.globalIntents.set(k, v)); } } /** * * @param {Block} block * @param {number} setConfigTimestamp * @param {string} lastmod */ _buildBot (block, setConfigTimestamp = Number.MAX_SAFE_INTEGER, lastmod = '-') { try { if (setConfigTimestamp !== Number.MAX_SAFE_INTEGER) { // eslint-disable-next-line no-console console.log(`[wingbot.ai BuildRouter] reloaded snapshot from ${new Date(setConfigTimestamp).toUTCString()} (${lastmod})`); } } catch (e) { // noop } if (this._prebuiltGlobalIntents === null) { this._prebuiltGlobalIntents = Array.from(this.globalIntents.entries()); } else { this.globalIntents.clear(); this._prebuiltGlobalIntents.forEach(([k, v]) => this.globalIntents.set(k, v)); } if (this._prebuiltRoutesCount === null) { this._prebuiltRoutesCount = this._routes.length; } else { this._routes = this._routes.slice(0, this._prebuiltRoutesCount); } const { blockName, blockType, isRoot, staticBlockId } = block; this._resolvedContext = { ...this._resolvedContext, blockName, blockType, isRoot, staticBlockId, BuildRouter }; const [linksMap, nestedBlocksByStaticId] = this._createLinksMap(block); // @ts-ignore this._linksMap = linksMap; this._nestedLinksMap = null; // @ts-ignore this._buildRoutes(block.routes, nestedBlocksByStaticId); this._configTs = setConfigTimestamp; // this event should not be propagated to the parent this.emit('rebuild'); } _getNestedLinksMap () { if (!this._nestedLinksMap) { /** @type {LinksMap} */ const linksMap = new Map(); for (const [from, to] of this._linksMap.entries()) { linksMap.set(from, `../${to}`); // this._joinPaths('..', to) } this._nestedLinksMap = linksMap; } return this._nestedLinksMap; } /** * * returns {[LinksMap, BlockMap]} * * @param {Block} block */ _createLinksMap (block) { const { linksMap: prevLinksMap, blocks = [], nestedLinksMapFactory } = this._resolvedContext; /** @type {LinksMap} */ const linksMap = nestedLinksMapFactory ? new Map(nestedLinksMapFactory()) : new Map(); if (prevLinksMap && !nestedLinksMapFactory) { for (const [from, to] of prevLinksMap.entries()) { linksMap.set(from, `../${to}`); // this._joinPaths('..', to) } } const expectedFromResponders = new Set(); const blocksById = new Map(); block.routes .forEach((route) => { if (!route.isResponder) { linksMap.set(route.id, route.path); } blocksById.set(route.id, route); }); let { nestedBlocksByStaticId } = this._resolvedContext; if (!nestedBlocksByStaticId) { nestedBlocksByStaticId = new Map(); blocks.forEach((b) => { if (b.staticBlockId && !b.disabled) { nestedBlocksByStaticId.set(b.staticBlockId, b); } }); Object.assign(this._resolvedContext, { nestedBlocksByStaticId }); } block.routes.forEach((route) => { const enabledNestedBlock = nestedBlocksByStaticId.get(this._getIncludedBlockId(route)); if (enabledNestedBlock) { const routeConfig = this._getRouteConfig(route); if (this._enabledByRouteConfig(routeConfig)) { this._findEntryPointsInResolver(linksMap, enabledNestedBlock, route); } } if (route.isResponder) { // create the pseudopath ant set to set to corresponding route const referredRoutePath = linksMap.get(route.respondsToRouteId); if (referredRoutePath) { const expectedPath = `${referredRoutePath}_responder` .replace(/^\//, ''); Object.assign(route, { path: expectedPath }); if (!expectedFromResponders.has(route.respondsToRouteId)) { expectedFromResponders.add(route.respondsToRouteId); const referredRoute = blocksById.get(route.respondsToRouteId); Object.assign(referredRoute, { expectedPath }); } } } }); return [linksMap, nestedBlocksByStaticId]; } /** * * @param {RouteConfig} routeConfig */ _enabledByRouteConfig (routeConfig) { return !routeConfig || routeConfig.enabled !== false; } _joinPaths (...args) { return (path.posix || path).join(...args); } _normalizeConfigPath (routePath, ctxPath = null) { const joined = ctxPath ? this._joinPaths(ctxPath, routePath) : routePath; return joined .replace(/^\/?|(_responder)?\/?$/g, '/') .replace(/\/\/+$/, '/'); } /** * * @param {Route} route * @returns {string|null} */ _getIncludedBlockId (route) { const includeResolver = route.resolvers.find((r) => r.type === 'botbuild.include'); return includeResolver ? includeResolver.params.staticBlockId : null; } /** * * @param {TransformedRoute} route * @returns {RouteConfig} */ _getRouteConfig (route) { const { path: ctxPath, routeConfigs } = this._resolvedContext; if (!routeConfigs || !route.path || route.isFallback) { return null; } let rPath; if (!route.isResponder) { rPath = route.path; } else if (route.expectedPath) { rPath = route.expectedPath; } else if (this._linksMap.has(route.respondsToRouteId)) { rPath = this._linksMap.get(route.respondsToRouteId); } else { throw new Error('Illegal state'); } const routePath = this._normalizeConfigPath(rPath, ctxPath); return routeConfigs.find((config) => { const configPath = this._normalizeConfigPath(config.path); return configPath === routePath; }); } /** * * @param {LinksMap} linksMap * @param {Block} includedBlock * @param {TransformedRoute} route * @returns {void} */ _findEntryPointsInResolver (linksMap, includedBlock, route) { let basePath = `${route.path}/`; if (route.isFallback) { basePath = ''; } includedBlock.routes.forEach((blockRoute) => { if (!blockRoute.isEntryPoint) { return; } linksMap.set(blockRoute.id, `${basePath}${blockRoute.path}`); }); } /** * * @param {TransformedRoute} route * @param {boolean} nextRouteIsSameResponder * @param {string} includedBlockId * @returns {Middleware<S,C>[]} */ _buildRouteHead (route, nextRouteIsSameResponder, includedBlockId) { const resolvers = []; if (!route.isFallback) { let aiResolver = null; if (route.aiTags && route.aiTags.length) { let aiTitle = null; if (route.aiTitle) { aiTitle = cachedTranslatedCompilator(route.aiTitle); } if (route.aiGlobal) { aiResolver = Ai.ai.global(route.path, route.aiTags, aiTitle); } else if (route.isResponder) { aiResolver = Ai.ai.match(route.aiTags); } else { aiResolver = Ai.ai.local(route.path, route.aiTags, aiTitle); } } if (aiResolver && route.isResponder) { resolvers.push(route.path, aiResolver); } else if (aiResolver) { resolvers.push(aiResolver); } else if (route.path) { resolvers.push(route.path); } if (route.isResponder) { const referredRoutePath = this._linksMap.get(route.respondsToRouteId); const bounceResolver = bounce(route, nextRouteIsSameResponder, referredRoutePath); if (bounceResolver) { resolvers.push(bounceResolver); } } else if (!includedBlockId && route.skill) { resolvers.push((req, res) => { res.trackAsSkill(route.skill); return Router.CONTINUE; }); } } return resolvers; } /** * * @param {TransformedRoute[]} routes * @param {BlockMap} nestedBlocksByStaticId */ _buildRoutes (routes, nestedBlocksByStaticId) { routes.forEach((route, i) => { const routeConfig = this._getRouteConfig(route); if (routeConfig && !routeConfig.enabled) { return; } const includedBlockId = this._getIncludedBlockId(route); const nestedBlock = nestedBlocksByStaticId.get(includedBlockId); if (includedBlockId && (!nestedBlock || !this._enabledByRouteConfig(routeConfig))) { return; } const nextRoute = routes.length > (i + 1) ? routes[i + 1] : null; let nextRouteIsSameResponder = false; if (nextRoute && route.isResponder && nextRoute.isResponder) { nextRouteIsSameResponder = nextRoute.respondsToRouteId === route.respondsToRouteId; } const buildInfo = { expectedToAddResolver: !!route.expectedPath, attachedRouter: false }; const resolvers = [ ...this._buildRouteHead(route, nextRouteIsSameResponder, includedBlockId), ...this.buildResolvers(route.resolvers, route, buildInfo) ]; if (route.expectedPath) { // attach expected before last message, if there is resolvers.push(expected({ path: route.expectedPath, attachedRouter: buildInfo.attachedRouter }, { isLastIndex: true })); } this.use(...resolvers); }); } /** * * @param {Resolver[]} resolvers * @returns {number} */ _lastMessageIndex (resolvers) { for (let i = resolvers.length - 1; i >= 0; i--) { const { type, params = {} } = resolvers[i]; if (MESSAGE_RESOLVER_NAME === type || (type === PLUGIN_RESOLVER_NAME && params && params.items && Object.keys(params.items).length)) { return i; } } return -1; } /** * * @param {Resolver[]} resolvers * @param {TransformedRoute} [route] * @param {BuildInfo} [buildInfo] * @param {BlockMap} [nestedBlocksByStaticId=null] * @returns {Middleware<S,C>[]} */ buildResolvers (resolvers, route = DUMMY_ROUTE, buildInfo = {}, nestedBlocksByStaticId = null) { const { path: ctxPath, isFallback, isResponder, expectedPath, id } = route; const lastMessageIndex = this._lastMessageIndex(resolvers); const lastIndex = resolvers.length - 1; /** @type {C} */ const configuration = this._configuration instanceof Promise ? null : this._configuration; return resolvers.map((resolver, i) => { const context = { ai: Ai.ai, ...this._resolvedContext, isLastIndex: lastIndex === i && !buildInfo.expectedToAddResolver, isLastMessage: lastMessageIndex === i && !buildInfo.notLastMessage, router: this, linksMap: this._linksMap, nestedLinksMapFactory: this._getNestedLinksMap.bind(this), path: ctxPath, isFallback, isResponder, expectedPath, routeId: id, configuration, resolverId: resolver.id, nestedBlocksByStaticId }; const resFn = this._resolverFactory(resolver, context, buildInfo); // @ts-ignore if (typeof resFn.configuration === 'undefined') { // @ts-ignore resFn.configuration = configuration; } return resFn; }); } /** * * @param {Resolver} resolver * @param {BotContext<C>} context * @param {BuildInfo} buildInfo * @returns {Middleware<S,C>} */ _resolverFactory (resolver, context, buildInfo) { const { type } = resolver; if (!this.resources.has(type)) { throw new Error(`Unknown Resolver: ${type} Ensure its registration.`); } const factoryFn = this.resources.get(type); const fn = factoryFn(resolver.params, context, this._plugins); if (fn.reduce) { Object.assign(buildInfo, { attachedRouter: true }); } if ([ 'botbuild.include', 'botbuild.path', 'botbuild.customCode', 'botbuild.inlineCode', 'botbuild.plugin', 'botbuild.postback' ].includes(type)) { return fn; } const retFn = (req, ...rest) => { if (!shouldExecuteResolver(req, resolver.tag, context.isFallback)) { return context.isLastIndex ? Router.END : Router.CONTINUE; } return typeof fn === 'function' ? fn(req, ...rest) : fn.reduce(req, ...rest); }; if (typeof fn.globalIntentsMeta === 'object') { retFn.globalIntentsMeta = fn.globalIntentsMeta; } if (fn.globalIntents) { retFn.globalIntents = fn.globalIntents; } if (fn.path) { retFn.path = fn.path; } if (resolver.tag) { if (!retFn.globalIntentsMeta) { retFn.globalIntentsMeta = {}; } Object.assign(retFn.globalIntentsMeta, { resolverTag: resolver.tag }); } return retFn; } } /** * @template {BaseConfiguration} [C=object] * @param {Block[]} blocks - blocks list * @param {Plugins} [plugins] * @param {BotContext<C>} [context] */ BuildRouter.fromData = function fromData (blocks, plugins = new Plugins(), context = {}) { // @ts-ignore assert.ok(Array.isArray(blocks), 'Bot build failed: "blocks" should be an array'); // @ts-ignore assert.ok(blocks.every((b) => b && typeof b === 'object'), 'Bot build failed: "blocks" should be an array of objects'); const rootBlock = blocks.find((block) => block.isRoot); // @ts-ignore assert.ok(rootBlock, 'Bot build failed: there is no block with "block.isRoot=true" property'); return new BuildRouter(rootBlock, plugins, ({ blocks, ...context })); }; module.exports = BuildRouter;