wingbot 3.32.0 → 3.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.32.0",
3
+ "version": "3.33.1",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/Ai.js CHANGED
@@ -229,7 +229,7 @@ class Ai {
229
229
  *
230
230
  * @param {string} path
231
231
  * @param {IntentRule|IntentRule[]} intents
232
- * @param {string} [title] - disambiguation title
232
+ * @param {string|Function} [title] - disambiguation title
233
233
  * @param {object} [meta] - metadata for multibot environments
234
234
  * @param {object} [meta.targetAppId] - target application id
235
235
  * @param {object} [meta.targetAction] - target action
@@ -276,7 +276,7 @@ class Ai {
276
276
  *
277
277
  * @param {string} path
278
278
  * @param {IntentRule|IntentRule[]} intents
279
- * @param {string} [title] - disambiguation title
279
+ * @param {string|Function} [title] - disambiguation title
280
280
  * @returns {object} - the middleware
281
281
  * @memberOf Ai
282
282
  * @example
@@ -17,6 +17,67 @@ const { shouldExecuteResolver } = require('./resolvers/resolverTags');
17
17
 
18
18
  const MESSAGE_RESOLVER_NAME = 'botbuild.message';
19
19
 
20
+ /** @typedef {import('./Router').Middleware} Middleware */
21
+
22
+ /**
23
+ * @typedef {object} Resolver
24
+ * @prop {string} type
25
+ * @prop {object} params
26
+ * @prop {string} [params.staticBlockId]
27
+ * @prop {string} [tag]
28
+ */
29
+
30
+ /** @typedef {import('./resolvers/bounce').BounceAllow} BounceAllow */
31
+ /** @typedef {import('./resolvers/bounce').BounceReturn} BounceReturn */
32
+
33
+ /**
34
+ * @typedef {object} Route
35
+ * @prop {number} id
36
+ * @prop {string|null} path
37
+ * @prop {Resolver[]} resolvers
38
+ * @prop {boolean} [isFallback]
39
+ * @prop {string[]} [aiTags]
40
+ * @prop {boolean} [isResponder]
41
+ * @prop {number} [respondsToRouteId]
42
+ * @prop {string|any[]} [aiTitle]
43
+ * @prop {boolean} [aiGlobal]
44
+ * @prop {BounceAllow} [bounceAllowedTo]
45
+ * @prop {BounceReturn} [bounceReturn]
46
+ * @prop {boolean} [isEntryPoint]
47
+ */
48
+
49
+ /**
50
+ * @typedef {object} RouteTransformation
51
+ * @prop {string} [expectedPath]
52
+ *
53
+ * @typedef {RouteTransformation & Route} TransformedRoute
54
+ */
55
+
56
+ /**
57
+ * @typedef {Map<string|number,string>} LinksMap
58
+ */
59
+
60
+ /** @type {TransformedRoute} */
61
+ const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] };
62
+
63
+ /**
64
+ * @typedef {object} Block
65
+ * @prop {string} staticBlockId
66
+ * @prop {Route[]} routes
67
+ * @prop {boolean} [isRoot]
68
+ * @prop {boolean} [disabled]
69
+ * @prop {string} [blockName]
70
+ * @prop {string} [blockType]
71
+ */
72
+
73
+ /**
74
+ * @typedef {object} BotConfig
75
+ * @prop {string} botId - the ID of bot
76
+ * @prop {string} snapshot - snapshot stage of bot
77
+ * @prop {string|Promise<string>} token - authorization token for bot
78
+ * @prop {string} [url] - specify alternative configuration resource
79
+ */
80
+
20
81
  /**
21
82
  * @typedef {object} ConfigStorage
22
83
  * @prop {{():Promise}} invalidateConfig
@@ -25,6 +86,61 @@ const MESSAGE_RESOLVER_NAME = 'botbuild.message';
25
86
  * @prop {{():Promise<Object>}} getConfig
26
87
  */
27
88
 
89
+ /**
90
+ * @typedef {object} RouteConfig
91
+ * @prop {string} path
92
+ * @prop {boolean} enabled
93
+ * @prop {object} configuration
94
+ */
95
+
96
+ /**
97
+ * @callback LinkTranslator
98
+ * @param {string} senderId
99
+ * @param {string} textLabel
100
+ * @param {string} urlText
101
+ * @param {boolean} [isExtUrl]
102
+ * @param {object} [state]
103
+ * @returns {string}
104
+ */
105
+
106
+ /**
107
+ * @typedef {object} BotContext
108
+ * @prop {LinkTranslator} [linksTranslator] - function, that translates links globally
109
+ * @prop {ConfigStorage} [configStorage] - function, that translates links globally
110
+ * @prop {boolean} [allowForbiddenSnippetWords] - disable security rule
111
+ * @prop {RouteConfig[]} [routeConfigs] - list of disabled routes
112
+ * @prop {object} [configuration] - context data
113
+ * @prop {LinksMap} [linksMap]
114
+ * @prop {boolean} [isLastIndex]
115
+ * @prop {boolean} [isLastMessage]
116
+ * @prop {BuildRouter} [router]
117
+ * @prop {string} [path]
118
+ * @prop {boolean} [isFallback]
119
+ * @prop {string} [expectedPath]
120
+ * @prop {boolean} [isResponder]
121
+ * @prop {string|number} [routeId]
122
+ * @prop {string} [blockName]
123
+ * @prop {string} [blockType]
124
+ * @prop {boolean} [isRoot]
125
+ * @prop {string} [staticBlockId]
126
+ * @prop {Block[]} [blocks]
127
+ * @prop {object} [BuildRouter]
128
+ */
129
+
130
+ function deepFreeze (object) {
131
+ const propNames = Object.getOwnPropertyNames(object);
132
+
133
+ for (const name of propNames) {
134
+ const value = object[name];
135
+
136
+ if (value && typeof value === 'object') {
137
+ deepFreeze(value);
138
+ }
139
+ }
140
+
141
+ return Object.freeze(object);
142
+ }
143
+
28
144
  /**
29
145
  * Build bot from Wingbot configuration file or snapshot url
30
146
  *
@@ -36,16 +152,14 @@ class BuildRouter extends Router {
36
152
  * Create new router from configuration
37
153
  *
38
154
  * @constructor
39
- * @param {object} block
40
- * @param {string} [block.botId] - the ID of bot
41
- * @param {string} [block.snapshot] - snapshot stage of bot
42
- * @param {string|Promise<string>} [block.token] - authorization token for bot
43
- * @param {string} [block.url] - specify alternative configuration resource
155
+ * @param {BotConfig|Block} block
44
156
  * @param {Plugins} plugins - custom code blocks resource
45
157
  * @param {object} context - the building context
46
158
  * @param {object} [context.linksTranslator] - function, that translates links globally
47
159
  * @param {ConfigStorage} [context.configStorage] - function, that translates links globally
48
160
  * @param {boolean} [context.allowForbiddenSnippetWords] - disable security rule
161
+ * @param {RouteConfig[]} [context.routeConfigs] - list of disabled routes
162
+ * @param {object} [context.configuration]
49
163
  * @param {fetch} [fetchFn] - override a request function
50
164
  * @example
51
165
  *
@@ -76,8 +190,10 @@ class BuildRouter extends Router {
76
190
 
77
191
  this._plugins = plugins;
78
192
 
193
+ /** @type {BotContext} */
79
194
  this._context = context;
80
195
 
196
+ /** @type {LinksMap} */
81
197
  this._linksMap = new Map();
82
198
 
83
199
  this._loadBotUrl = null;
@@ -94,7 +210,7 @@ class BuildRouter extends Router {
94
210
 
95
211
  this.resources = defaultResourceMap();
96
212
 
97
- this._loadBotAuthorization = block.token || null;
213
+ this._loadBotAuthorization = 'token' in block ? block.token : null;
98
214
 
99
215
  this._configStorage = context.configStorage;
100
216
 
@@ -112,7 +228,7 @@ class BuildRouter extends Router {
112
228
  this._snapshot = null;
113
229
  this._botId = null;
114
230
 
115
- if (typeof block.routes === 'object') {
231
+ if ('routes' in block) {
116
232
  this._buildBot(block);
117
233
  } else if (typeof block.url === 'string') {
118
234
  this._loadBotUrl = block.url;
@@ -315,6 +431,12 @@ class BuildRouter extends Router {
315
431
  }
316
432
  }
317
433
 
434
+ /**
435
+ *
436
+ * @param {Block} block
437
+ * @param {number} setConfigTimestamp
438
+ * @param {string} lastmod
439
+ */
318
440
  _buildBot (block, setConfigTimestamp = Number.MAX_SAFE_INTEGER, lastmod = '-') {
319
441
  try {
320
442
  if (setConfigTimestamp !== Number.MAX_SAFE_INTEGER) {
@@ -390,7 +512,13 @@ class BuildRouter extends Router {
390
512
  });
391
513
  }
392
514
 
515
+ /**
516
+ *
517
+ * @param {Block} block
518
+ * @returns {LinksMap}
519
+ */
393
520
  _createLinksMap (block) {
521
+ /** @type {LinksMap} */
394
522
  const linksMap = new Map();
395
523
 
396
524
  block.routes
@@ -402,35 +530,140 @@ class BuildRouter extends Router {
402
530
  if (prevLinksMap) {
403
531
  for (const [from, to] of prevLinksMap.entries()) {
404
532
  if (!linksMap.has(from)) {
405
-
406
- (path.posix || path).join('..', to);
407
-
408
- linksMap.set(from, (path.posix || path).join('..', to));
533
+ linksMap.set(from, this._joinPaths('..', to));
409
534
  }
410
535
  }
411
536
  }
412
537
 
413
538
  block.routes.forEach((route) => {
414
- let resolver;
415
- for (resolver of route.resolvers) {
416
- if (resolver.type !== 'botbuild.include') {
417
- continue;
418
- }
419
- this._findEntryPointsInResolver(linksMap, resolver, route, this._context);
539
+ const enabledNestedBlock = this._getBlockById(this._getIncludedBlockId(route));
540
+ if (!enabledNestedBlock) {
541
+ return;
542
+ }
543
+ const routeConfig = this._getRouteConfig(route);
544
+ if (this._enabledByRouteConfig(routeConfig)) {
545
+ this._findEntryPointsInResolver(linksMap, enabledNestedBlock, route);
420
546
  }
421
547
  });
422
548
 
423
549
  return linksMap;
424
550
  }
425
551
 
426
- _findEntryPointsInResolver (linksMap, resolver, route, context) {
427
- const includedBlock = (context.blocks || [])
428
- .find((b) => b.staticBlockId === resolver.params.staticBlockId);
552
+ /**
553
+ *
554
+ * @param {RouteConfig} routeConfig
555
+ */
556
+ _enabledByRouteConfig (routeConfig) {
557
+ return !this._context.routeConfigs || (routeConfig && routeConfig.enabled);
558
+ }
429
559
 
430
- if (!includedBlock) {
431
- return;
560
+ _joinPaths (...args) {
561
+ return (path.posix || path).join(...args);
562
+ }
563
+
564
+ _normalizeConfigPath (routePath, ctxPath = null) {
565
+ const joined = ctxPath
566
+ ? this._joinPaths(ctxPath, routePath)
567
+ : routePath;
568
+
569
+ return joined
570
+ .replace(/^\/?|(_responder)?\/?$/g, '/');
571
+ }
572
+
573
+ /**
574
+ *
575
+ * @param {Route} route
576
+ * @returns {string|null}
577
+ */
578
+ _getIncludedBlockId (route) {
579
+ const includeResolver = route.resolvers.find((r) => r.type === 'botbuild.include');
580
+
581
+ return includeResolver
582
+ ? includeResolver.params.staticBlockId
583
+ : null;
584
+ }
585
+
586
+ /**
587
+ *
588
+ * @param {string} staticBlockId
589
+ * @returns {Block|null}
590
+ */
591
+ _getBlockById (staticBlockId) {
592
+ if (!staticBlockId) {
593
+ return null;
594
+ }
595
+ const nestedBlock = (this._context.blocks || [])
596
+ .find((b) => b.staticBlockId === staticBlockId);
597
+
598
+ if (!nestedBlock || nestedBlock.disabled) {
599
+ return null;
600
+ }
601
+ return nestedBlock;
602
+ }
603
+
604
+ /**
605
+ *
606
+ * @param {TransformedRoute} route
607
+ * @returns {RouteConfig}
608
+ */
609
+ _getRouteConfig (route) {
610
+ const { path: ctxPath, routeConfigs } = this._context;
611
+ if (!routeConfigs || !route.path || route.isFallback) {
612
+ return null;
613
+ }
614
+
615
+ let rPath;
616
+ if (!route.isResponder) {
617
+ rPath = route.path;
618
+ } else if (route.expectedPath) {
619
+ rPath = route.expectedPath;
620
+ } else if (this._linksMap.has(route.respondsToRouteId)) {
621
+ rPath = this._linksMap.get(route.respondsToRouteId);
622
+ } else {
623
+ throw new Error('Illegal state');
432
624
  }
433
625
 
626
+ const routePath = this._normalizeConfigPath(rPath, ctxPath);
627
+ return routeConfigs.find((config) => {
628
+ const configPath = this._normalizeConfigPath(config.path);
629
+
630
+ return configPath === routePath;
631
+ });
632
+ }
633
+
634
+ /**
635
+ *
636
+ * @param {string} routePath
637
+ * @returns {object}
638
+ */
639
+ getConfiguration (routePath) {
640
+ const { path: ctxPath, routeConfigs, configuration = {} } = this._context;
641
+ if (!routeConfigs || !routePath) {
642
+ return deepFreeze(configuration);
643
+ }
644
+ const normalized = this._normalizeConfigPath(routePath, ctxPath);
645
+
646
+ const configs = routeConfigs
647
+ .filter((config) => {
648
+ const configPath = this._normalizeConfigPath(config.path);
649
+ return normalized.startsWith(configPath);
650
+ })
651
+ .sort((a, z) => a.path.length - z.path.length);
652
+
653
+ return deepFreeze(configs.reduce((cfg, config) => ({
654
+ ...cfg,
655
+ ...config.configuration
656
+ }), configuration));
657
+ }
658
+
659
+ /**
660
+ *
661
+ * @param {LinksMap} linksMap
662
+ * @param {Block} includedBlock
663
+ * @param {TransformedRoute} route
664
+ * @returns {void}
665
+ */
666
+ _findEntryPointsInResolver (linksMap, includedBlock, route) {
434
667
  let basePath = `${route.path}/`;
435
668
 
436
669
  if (route.isFallback) {
@@ -438,14 +671,20 @@ class BuildRouter extends Router {
438
671
  }
439
672
 
440
673
  includedBlock.routes.forEach((blockRoute) => {
441
- if (!blockRoute.isEntryPoint || blockRoute.isRoot) {
674
+ if (!blockRoute.isEntryPoint) {
442
675
  return;
443
676
  }
444
677
 
445
- linksMap.set(`${blockRoute.id}`, `${basePath}${blockRoute.path}`);
678
+ linksMap.set(blockRoute.id, `${basePath}${blockRoute.path}`);
446
679
  });
447
680
  }
448
681
 
682
+ /**
683
+ *
684
+ * @param {TransformedRoute} route
685
+ * @param {boolean} nextRouteIsSameResponder
686
+ * @returns {Middleware[]}
687
+ */
449
688
  _buildRouteHead (route, nextRouteIsSameResponder) {
450
689
  const resolvers = [];
451
690
 
@@ -453,10 +692,10 @@ class BuildRouter extends Router {
453
692
  let aiResolver = null;
454
693
 
455
694
  if (route.aiTags && route.aiTags.length) {
456
- let { aiTitle = null } = route;
695
+ let aiTitle = null;
457
696
 
458
- if (aiTitle) {
459
- aiTitle = cachedTranslatedCompilator(aiTitle);
697
+ if (route.aiTitle) {
698
+ aiTitle = cachedTranslatedCompilator(route.aiTitle);
460
699
  }
461
700
 
462
701
  if (route.aiGlobal) {
@@ -488,8 +727,25 @@ class BuildRouter extends Router {
488
727
  return resolvers;
489
728
  }
490
729
 
730
+ /**
731
+ *
732
+ * @param {TransformedRoute[]} routes
733
+ */
491
734
  _buildRoutes (routes) {
492
735
  routes.forEach((route, i) => {
736
+ const routeConfig = this._getRouteConfig(route);
737
+
738
+ if (routeConfig && !routeConfig.enabled) {
739
+ return;
740
+ }
741
+
742
+ const includedBlockId = this._getIncludedBlockId(route);
743
+ const nestedBlock = this._getBlockById(includedBlockId);
744
+
745
+ if (includedBlockId && (!nestedBlock || !this._enabledByRouteConfig(routeConfig))) {
746
+ return;
747
+ }
748
+
493
749
  const nextRoute = routes.length > (i + 1)
494
750
  ? routes[i + 1]
495
751
  : null;
@@ -523,6 +779,11 @@ class BuildRouter extends Router {
523
779
  });
524
780
  }
525
781
 
782
+ /**
783
+ *
784
+ * @param {Resolver[]} resolvers
785
+ * @returns {number}
786
+ */
526
787
  _lastMessageIndex (resolvers) {
527
788
  for (let i = resolvers.length - 1; i >= 0; i--) {
528
789
  if (resolvers[i].type === MESSAGE_RESOLVER_NAME) {
@@ -532,15 +793,30 @@ class BuildRouter extends Router {
532
793
  return -1;
533
794
  }
534
795
 
535
- buildResolvers (resolvers, route = {}, buildInfo = {}) {
796
+ /**
797
+ *
798
+ * @param {Resolver[]} resolvers
799
+ * @param {TransformedRoute} route
800
+ * @param {*} buildInfo
801
+ * @returns {Middleware[]}
802
+ */
803
+ buildResolvers (resolvers, route = DUMMY_ROUTE, buildInfo = {}) {
536
804
  const {
537
805
  path: ctxPath, isFallback, isResponder, expectedPath, id
538
806
  } = route;
539
807
 
808
+ const routeConfig = this._getRouteConfig(route);
809
+ const routeConfigData = routeConfig && routeConfig.configuration;
810
+
540
811
  const lastMessageIndex = this._lastMessageIndex(resolvers);
541
812
  const lastIndex = resolvers.length - 1;
542
813
 
543
814
  return resolvers.map((resolver, i) => {
815
+ const configuration = deepFreeze({
816
+ ...this._context.configuration,
817
+ ...routeConfigData
818
+ });
819
+
544
820
  const context = {
545
821
  ...this._context,
546
822
  isLastIndex: lastIndex === i && !buildInfo.expectedToAddResolver,
@@ -551,13 +827,25 @@ class BuildRouter extends Router {
551
827
  isFallback,
552
828
  isResponder,
553
829
  expectedPath,
554
- routeId: id
830
+ routeId: id,
831
+ configuration
555
832
  };
556
833
 
557
- return this._resolverFactory(resolver, context, buildInfo);
834
+ const resFn = this._resolverFactory(resolver, context, buildInfo);
835
+
836
+ // @ts-ignore
837
+ resFn.configuration = configuration;
838
+ return resFn;
558
839
  });
559
840
  }
560
841
 
842
+ /**
843
+ *
844
+ * @param {Resolver} resolver
845
+ * @param {BotContext} context
846
+ * @param {*} buildInfo
847
+ * @returns {Middleware}
848
+ */
561
849
  _resolverFactory (resolver, context, buildInfo) {
562
850
  const { type } = resolver;
563
851
 
@@ -618,9 +906,9 @@ class BuildRouter extends Router {
618
906
  }
619
907
 
620
908
  /**
621
- * @param {object[]} blocks - blocks list
909
+ * @param {Block[]} blocks - blocks list
622
910
  * @param {Plugins} [plugins]
623
- * @param {object} [context]
911
+ * @param {BotContext} [context]
624
912
  */
625
913
  BuildRouter.fromData = function fromData (blocks, plugins = new Plugins(), context = {}) {
626
914
 
@@ -603,19 +603,13 @@ class ConversationTester {
603
603
  } else {
604
604
  const quickReplyRequired = action.match(/^>/);
605
605
  const cleanAction = action.replace(/^>/, '');
606
- let found;
607
606
 
608
607
  // action in quick reply
609
608
  if (action.match(/^>\//)) {
610
609
  await t.quickReply(cleanAction);
611
- found = true;
610
+ } else if (quickReplyRequired) {
611
+ await t.quickReplyText(cleanAction);
612
612
  } else {
613
- found = await t.quickReplyText(cleanAction);
614
- }
615
-
616
- if (!found && quickReplyRequired) {
617
- throw new Error(`Quick reply "${action.replace(/^>/, '')}" was required, but has not been found`);
618
- } else if (!found) {
619
613
  await t.text(action);
620
614
  }
621
615
  }
package/src/Processor.js CHANGED
@@ -14,6 +14,7 @@ const { mergeState } = require('./utils/stateVariables');
14
14
  /** @typedef {import('./wingbot/CustomEntityDetectionModel').Intent} Intent */
15
15
  /** @typedef {import('./ReducerWrapper')} ReducerWrapper */
16
16
  /** @typedef {import('./Router')} Router */
17
+ /** @typedef {import('./BuildRouter')} BuildRouter */
17
18
 
18
19
  /**
19
20
  * @typedef {object} AutoTypingConfig
@@ -121,7 +122,7 @@ class Processor extends EventEmitter {
121
122
  /**
122
123
  * Creates an instance of Processor
123
124
  *
124
- * @param {ReducerWrapper|Router} reducer
125
+ * @param {ReducerWrapper|Router|BuildRouter} reducer
125
126
  * @param {ProcessorOptions} [options] - processor options
126
127
  *
127
128
  * @memberOf Processor
@@ -548,6 +549,11 @@ class Processor extends EventEmitter {
548
549
  res.setBookmark(aByAi);
549
550
  }
550
551
 
552
+ if ('getConfiguration' in this.reducer) {
553
+ const configuration = this.reducer.getConfiguration(state._lastAction);
554
+ req.configuration = Object.freeze(configuration);
555
+ }
556
+
551
557
  // process setState
552
558
  const setState = req.getSetState(req.AI_SETSTATE.EXCLUDE_WITH_SET_ENTITIES);
553
559
  await Ai.ai.processSetStateEntities(req, setState);
package/src/Request.js CHANGED
@@ -264,6 +264,9 @@ class Request {
264
264
  * @constant {string} FEATURE_TRACKING channel supports tracking protocol
265
265
  */
266
266
  this.FEATURE_TRACKING = FEATURE_TRACKING;
267
+
268
+ /** @type {object} */
269
+ this.configuration = Object.freeze({});
267
270
  }
268
271
 
269
272
  get data () {
package/src/Router.js CHANGED
@@ -140,7 +140,12 @@ class Router extends ReducerWrapper {
140
140
 
141
141
  const reducersArray = reducer.map((re) => {
142
142
  const {
143
- resolverPath, reduce, isReducer, globalIntents: gis, globalIntentsMeta = {}
143
+ resolverPath,
144
+ reduce,
145
+ isReducer,
146
+ globalIntents: gis,
147
+ globalIntentsMeta,
148
+ configuration
144
149
  } = this._createReducer(
145
150
  re,
146
151
  pathContext.path
@@ -149,16 +154,19 @@ class Router extends ReducerWrapper {
149
154
  Object.assign(pathContext, { path: resolverPath });
150
155
  Object.assign(pathContext.globalIntentsMeta, globalIntentsMeta);
151
156
  isAnyReducer = isAnyReducer || isReducer;
152
- return { reduce, isReducer };
157
+ return { reduce, isReducer, configuration };
153
158
  });
154
159
 
155
160
  return {
156
- reducers: reducersArray, isReducer: isAnyReducer, isOr: true, globalIntents
161
+ reducers: reducersArray,
162
+ isReducer: isAnyReducer,
163
+ isOr: true,
164
+ globalIntents
157
165
  };
158
166
  }
159
167
 
160
168
  const {
161
- resolverPath, reduce, isReducer, globalIntents, globalIntentsMeta = {}
169
+ resolverPath, reduce, isReducer, globalIntents, globalIntentsMeta, configuration
162
170
  } = this._createReducer(
163
171
  reducer,
164
172
  pathContext.path
@@ -167,7 +175,9 @@ class Router extends ReducerWrapper {
167
175
  Object.assign(pathContext, { path: resolverPath });
168
176
  Object.assign(pathContext.globalIntentsMeta, globalIntentsMeta);
169
177
 
170
- return { reduce, isReducer, globalIntents };
178
+ return {
179
+ reduce, isReducer, globalIntents, configuration
180
+ };
171
181
  });
172
182
  }
173
183
 
@@ -178,7 +188,8 @@ class Router extends ReducerWrapper {
178
188
  const {
179
189
  globalIntents = new Map(),
180
190
  path,
181
- globalIntentsMeta = {}
191
+ globalIntentsMeta = {},
192
+ configuration = null
182
193
  } = reducer;
183
194
 
184
195
  if (typeof reducer === 'string' || path) {
@@ -219,7 +230,7 @@ class Router extends ReducerWrapper {
219
230
  }
220
231
 
221
232
  return {
222
- resolverPath, isReducer, reduce, globalIntents, globalIntentsMeta
233
+ resolverPath, isReducer, reduce, globalIntents, globalIntentsMeta, configuration
223
234
  };
224
235
  }
225
236
 
@@ -291,6 +302,9 @@ class Router extends ReducerWrapper {
291
302
 
292
303
  let pathContext = `${path === '/' ? '' : path}${route.path.replace(/\/\*/, '')}`;
293
304
  res.setPath(path, route.path);
305
+ if (reducer.configuration) {
306
+ req.configuration = reducer.configuration;
307
+ }
294
308
 
295
309
  let result;
296
310
 
package/src/Tester.js CHANGED
@@ -452,7 +452,7 @@ class Tester {
452
452
  }
453
453
 
454
454
  /**
455
- * Send quick reply if text exactly matches, otherwise returns false
455
+ * Send quick reply if text exactly matches, otherwise throws exception
456
456
  *
457
457
  * @param {string} text
458
458
  * @returns {Promise<boolean>}
@@ -460,22 +460,37 @@ class Tester {
460
460
  * @memberOf Tester
461
461
  */
462
462
  async quickReplyText (text) {
463
+ let found = '';
464
+ let but = 'has not been found';
463
465
 
464
466
  if (this.responses.length !== 0) {
465
- const search = tokenize(text);
467
+ const normalize = (t) => `${t}`.toLocaleLowerCase().replace(/\s+/g, ' ').trim();
468
+ const normalizedText = normalize(text);
469
+ const search = tokenize(normalizedText);
466
470
  const last = this.responses[this.responses.length - 1];
467
471
  const quickReplys = asserts.getQuickReplies(last);
468
- const res = quickReplys
472
+ let res = quickReplys
469
473
  .filter(({ title = '', payload }) => title && payload && tokenize(title) === search);
470
474
 
471
- if (res[0]) {
475
+ if (res.length > 1) {
476
+ res = res
477
+ .filter(({ title = '' }) => normalize(title) === normalizedText);
478
+ }
479
+
480
+ if (res.length === 1) {
472
481
  const { title, payload } = res[0];
473
482
  await this.processMessage(Request.quickReplyText(this.senderId, title, payload));
474
483
  return true;
475
484
  }
485
+
486
+ if (res.length > 1) {
487
+ but = 'found multiple occurences';
488
+ }
489
+
490
+ found = ` (found: ${quickReplys.map((q) => q.title).filter((q) => !!q).join(', ')})`;
476
491
  }
477
492
 
478
- return false;
493
+ throw new Error(`Quick reply "${text}" was required, but ${but}.${found}`);
479
494
  }
480
495
 
481
496
  /**
@@ -545,7 +545,7 @@ class Notifications extends EventEmitter {
545
545
  if (!campaign.hasEditableCondition) {
546
546
  fn = customFn(campaign.condition, `Campaign "${campaign.name}" condition`);
547
547
  } else {
548
- fn = customCondition(campaign.editableCondition, `Campaign "${campaign.name}" condition`);
548
+ fn = customCondition(campaign.editableCondition, req.configuration, `Campaign "${campaign.name}" condition`);
549
549
  }
550
550
 
551
551
  const fnRes = fn(req, res);
@@ -22,12 +22,15 @@ const BOUNCE_RETURN = {
22
22
  IF_POSSIBLE: 'ifpos'
23
23
  };
24
24
 
25
+ /** @typedef {import('../BuildRouter').Route} Route */
26
+
27
+ /** @typedef {BOUNCE_RETURN} BounceReturn */
28
+ /** @typedef {BOUNCE_ALLOW} BounceAllow */
29
+
25
30
  /**
26
31
  *
27
32
  *
28
- * @param {object} route
29
- * @param {BOUNCE_ALLOW} [route.bounceAllowedTo]
30
- * @param {BOUNCE_RETURN} [route.bounceReturn]
33
+ * @param {Route} route
31
34
  * @param {boolean} nextRouteIsSameResponder
32
35
  * @param {string} [referredRoutePath]
33
36
  * @returns {Function|null}
@@ -11,19 +11,26 @@ const {
11
11
  processButtons
12
12
  } = require('./utils');
13
13
 
14
- function button (params, {
15
- isLastIndex,
16
- linksMap,
17
- linksTranslator,
18
- allowForbiddenSnippetWords
19
- }) {
14
+ /** @typedef {import('../BuildRouter').BotContext} BotContext */
15
+ /** @typedef {import('../Router').Resolver} Resolver */
16
+
17
+ /**
18
+ *
19
+ * @param {object} params
20
+ * @param {BotContext} context
21
+ * @returns {Resolver}
22
+ */
23
+ function button (params, context) {
24
+ const {
25
+ isLastIndex
26
+ } = context;
20
27
  const {
21
28
  buttons = [],
22
29
  text = null
23
30
  } = params;
24
31
  const compiledText = cachedTranslatedCompilator(text);
25
32
 
26
- const condition = getCondition(params, '', allowForbiddenSnippetWords);
33
+ const condition = getCondition(params, context, 'button');
27
34
 
28
35
  const ret = isLastIndex ? Router.END : Router.CONTINUE;
29
36
 
@@ -37,17 +44,15 @@ function button (params, {
37
44
  }
38
45
  }
39
46
 
40
- const state = stateData(req, res);
47
+ const state = stateData(req, res, context.configuration);
41
48
  const tpl = res.button(compiledText(state));
42
49
 
43
50
  processButtons(
44
51
  buttons,
45
52
  state,
46
53
  tpl,
47
- linksMap,
48
54
  req.senderId,
49
- linksTranslator,
50
- allowForbiddenSnippetWords,
55
+ context,
51
56
  req,
52
57
  res
53
58
  );
@@ -17,12 +17,12 @@ const {
17
17
  TYPE_URL_WITH_EXT
18
18
  } = require('./utils');
19
19
 
20
- function carousel (params, {
21
- isLastIndex,
22
- linksMap,
23
- linksTranslator = (a, b, c) => c,
24
- allowForbiddenSnippetWords
25
- }) {
20
+ function carousel (params, context) {
21
+ const {
22
+ isLastIndex,
23
+ linksMap,
24
+ linksTranslator = (a, b, c) => c
25
+ } = context;
26
26
  const {
27
27
  items = [],
28
28
  shareable = false,
@@ -36,7 +36,7 @@ function carousel (params, {
36
36
  return ret;
37
37
  }
38
38
 
39
- const state = stateData(req, res);
39
+ const state = stateData(req, res, context.configuration);
40
40
  const isSquare = imageAspect === ASPECT_SQUARE;
41
41
  const tpl = res.genericTemplate(shareable, isSquare);
42
42
 
@@ -99,10 +99,8 @@ function carousel (params, {
99
99
  buttons,
100
100
  state,
101
101
  elem,
102
- linksMap,
103
102
  senderId,
104
- linksTranslator,
105
- allowForbiddenSnippetWords,
103
+ context,
106
104
  req,
107
105
  res
108
106
  );
@@ -7,11 +7,17 @@ const Router = require('../Router');
7
7
  const getCondition = require('../utils/getCondition');
8
8
  const { stateData, cachedTranslatedCompilator } = require('./utils');
9
9
 
10
- function media (
11
- params,
12
- // @ts-ignore
13
- { isLastIndex } = {}
14
- ) {
10
+ /** @typedef {import('../BuildRouter').BotContext} BotContext */
11
+ /** @typedef {import('../Router').Resolver} Resolver */
12
+
13
+ /**
14
+ *
15
+ * @param {object} params
16
+ * @param {BotContext} context
17
+ * @returns {Resolver}
18
+ */
19
+ function media (params, context = {}) {
20
+ const { isLastIndex } = context;
15
21
  const { type, url } = params;
16
22
 
17
23
  const urlString = url || '';
@@ -24,14 +30,14 @@ function media (
24
30
  throw new Error(`Unsupported media type: ${type}`);
25
31
  }
26
32
 
27
- const condition = getCondition(params, 'Media condition');
33
+ const condition = getCondition(params, context, 'Media condition');
28
34
 
29
35
  return (req, res) => {
30
36
  if (condition && !condition(req, res)) {
31
37
  return ret;
32
38
  }
33
39
 
34
- const data = stateData(req, res);
40
+ const data = stateData(req, res, context.configuration);
35
41
  const sendUrl = urlTemplate(data);
36
42
 
37
43
  res[type](sendUrl, true);
@@ -44,10 +44,10 @@ function getVoiceControlFromParams (params, lang = null) {
44
44
  return Object.keys(voiceControl).length > 0 ? voiceControl : null;
45
45
  }
46
46
 
47
- function parseReplies (replies, linksMap, allowForbiddenSnippetWords) {
47
+ function parseReplies (replies, linksMap, context) {
48
48
  return replies.map((reply) => {
49
49
 
50
- const condition = getCondition(reply, 'Quick reply condition', allowForbiddenSnippetWords);
50
+ const condition = getCondition(reply, context, 'Quick reply condition');
51
51
 
52
52
  if (reply.isLocation) {
53
53
  return {
@@ -78,6 +78,10 @@ function parseReplies (replies, linksMap, allowForbiddenSnippetWords) {
78
78
  }
79
79
  }
80
80
 
81
+ if (!action) {
82
+ return null;
83
+ }
84
+
81
85
  const ret = {
82
86
  action,
83
87
  condition,
@@ -104,7 +108,8 @@ function parseReplies (replies, linksMap, allowForbiddenSnippetWords) {
104
108
  }
105
109
 
106
110
  return ret;
107
- });
111
+ })
112
+ .filter((r) => r !== null);
108
113
  }
109
114
 
110
115
  /**
@@ -154,11 +159,20 @@ function findSupportedMessages (text, features, lang = null) {
154
159
  };
155
160
  }
156
161
 
157
- function message (params, {
158
- // @ts-ignore
159
- isLastIndex, isLastMessage, linksMap, allowForbiddenSnippetWords
160
- } = {}) {
162
+ /** @typedef {import('../BuildRouter').BotContext} BotContext */
163
+ /** @typedef {import('../Router').Resolver} Resolver */
161
164
 
165
+ /**
166
+ *
167
+ * @param {object} params
168
+ * @param {BotContext} context
169
+ * @returns {Resolver}
170
+ */
171
+ function message (params, context = {}) {
172
+ const {
173
+ // @ts-ignore
174
+ isLastIndex, isLastMessage, linksMap, configuration
175
+ } = context;
162
176
  if (typeof params.text !== 'string' && !Array.isArray(params.text)) {
163
177
  throw new Error('Message should be a text!');
164
178
  }
@@ -168,11 +182,11 @@ function message (params, {
168
182
  if (params.replies && !Array.isArray(params.replies)) {
169
183
  throw new Error('Replies should be an array');
170
184
  } else if (params.replies && params.replies.length > 0) {
171
- quickReplies = parseReplies(params.replies, linksMap, allowForbiddenSnippetWords);
185
+ quickReplies = parseReplies(params.replies, linksMap, context);
172
186
  }
173
187
 
174
188
  // compile condition
175
- const condition = getCondition(params, 'Message condition', allowForbiddenSnippetWords);
189
+ const condition = getCondition(params, context, 'Message condition');
176
190
 
177
191
  const ret = isLastIndex ? Router.END : Router.CONTINUE;
178
192
 
@@ -184,7 +198,7 @@ function message (params, {
184
198
  if (condition && !condition(req, res)) {
185
199
  return ret;
186
200
  }
187
- const data = stateData(req, res);
201
+ const data = stateData(req, res, configuration);
188
202
 
189
203
  // filter supported messages
190
204
  const supportedText = findSupportedMessages(
@@ -7,7 +7,17 @@ const Router = require('../Router');
7
7
  const getCondition = require('../utils/getCondition');
8
8
  const { shouldExecuteResolver } = require('./resolverTags');
9
9
 
10
- function postback (params, { linksMap, isLastIndex, allowForbiddenSnippetWords }) {
10
+ /** @typedef {import('../BuildRouter').BotContext} BotContext */
11
+ /** @typedef {import('../Router').Resolver} Resolver */
12
+
13
+ /**
14
+ *
15
+ * @param {object} params
16
+ * @param {BotContext} context
17
+ * @returns {Resolver}
18
+ */
19
+ function postback (params, context) {
20
+ const { linksMap, isLastIndex } = context;
11
21
  const {
12
22
  routeId,
13
23
  postBack: staticAction
@@ -22,12 +32,12 @@ function postback (params, { linksMap, isLastIndex, allowForbiddenSnippetWords }
22
32
  }
23
33
  }
24
34
 
25
- const condition = getCondition(params, '', allowForbiddenSnippetWords);
35
+ const condition = getCondition(params, context, 'postback');
26
36
 
27
37
  const ret = isLastIndex ? Router.END : Router.CONTINUE;
28
38
 
29
39
  return (req, res, postBack) => {
30
- if (!shouldExecuteResolver(req, params)) {
40
+ if (!action || !shouldExecuteResolver(req, params)) {
31
41
  return ret;
32
42
  }
33
43
 
@@ -8,11 +8,20 @@ const Ai = require('../Ai');
8
8
  const { getSetState } = require('../utils/getUpdate');
9
9
  const getCondition = require('../utils/getCondition');
10
10
 
11
- function setState (params, { isLastIndex, allowForbiddenSnippetWords }) {
11
+ /** @typedef {import('../BuildRouter').BotContext} BotContext */
12
+ /** @typedef {import('../Router').Resolver} Resolver */
13
+
14
+ /**
15
+ *
16
+ * @param {object} params
17
+ * @param {BotContext} context
18
+ * @returns {Resolver}
19
+ */
20
+ function setState (params, context) {
12
21
 
13
- const condition = getCondition(params, '', allowForbiddenSnippetWords);
22
+ const condition = getCondition(params, context, 'setState');
14
23
 
15
- const ret = isLastIndex ? Router.END : Router.CONTINUE;
24
+ const ret = context.isLastIndex ? Router.END : Router.CONTINUE;
16
25
 
17
26
  return async (req, res) => {
18
27
  if (condition !== null) {
@@ -6,15 +6,24 @@
6
6
  const Router = require('../Router');
7
7
  const getCondition = require('../utils/getCondition');
8
8
 
9
- function subscribtions (params, { isLastIndex, allowForbiddenSnippetWords }) {
9
+ /** @typedef {import('../BuildRouter').BotContext} BotContext */
10
+ /** @typedef {import('../Router').Resolver} Resolver */
11
+
12
+ /**
13
+ *
14
+ * @param {object} params
15
+ * @param {BotContext} context
16
+ * @returns {Resolver}
17
+ */
18
+ function subscribtions (params, context) {
10
19
  const {
11
20
  tags = [],
12
21
  unsetTag = false
13
22
  } = params;
14
23
 
15
- const condition = getCondition(params, '', allowForbiddenSnippetWords);
24
+ const condition = getCondition(params, context, 'subscribtions');
16
25
 
17
- const ret = isLastIndex ? Router.END : Router.CONTINUE;
26
+ const ret = context.isLastIndex ? Router.END : Router.CONTINUE;
18
27
  const method = unsetTag ? 'unsubscribe' : 'subscribe';
19
28
 
20
29
  return async (req, res) => {
@@ -35,6 +44,7 @@ function subscribtions (params, { isLastIndex, allowForbiddenSnippetWords }) {
35
44
  }
36
45
 
37
46
  if (tags.length === 0 && unsetTag) {
47
+ // @ts-ignore
38
48
  res.unsubscribe();
39
49
  }
40
50
 
@@ -205,18 +205,18 @@ function getText (text, state) {
205
205
  // eslint-disable-next-line no-unused-vars
206
206
  const DEFAULT_LINK_TRANSLATOR = (senderId, defaultText, urlText, isExtUrl, reqState) => urlText;
207
207
 
208
+ /** @typedef {import('../BuildRouter').BotContext} BotContext */
209
+
208
210
  function processButtons (
209
211
  buttons,
210
212
  state,
211
213
  elem,
212
- linksMap,
213
214
  senderId,
214
- linksTranslator,
215
- allowForbiddenSnippetWords,
215
+ context,
216
216
  req,
217
217
  res
218
218
  ) {
219
- const translateLinks = linksTranslator || DEFAULT_LINK_TRANSLATOR;
219
+ const translateLinks = context.linksTranslator || DEFAULT_LINK_TRANSLATOR;
220
220
 
221
221
  buttons.forEach(({
222
222
  title: btnTitle,
@@ -230,7 +230,7 @@ function processButtons (
230
230
  if (hasCondition) {
231
231
  const condition = getCondition({
232
232
  hasCondition, conditionFn, hasEditableCondition, editableCondition
233
- }, 'Quick reply condition', allowForbiddenSnippetWords);
233
+ }, context, 'Quick reply condition');
234
234
 
235
235
  if (!condition(req, res)) {
236
236
  return;
@@ -259,7 +259,7 @@ function processButtons (
259
259
  break;
260
260
  }
261
261
  case TYPE_POSTBACK: {
262
- let postbackAction = linksMap.get(targetRouteId) || action;
262
+ let postbackAction = context.linksMap.get(targetRouteId) || action;
263
263
 
264
264
  if (postbackAction === '/') {
265
265
  postbackAction = './';
@@ -148,15 +148,16 @@ const compare = (variable, operator, value = undefined) => {
148
148
  /**
149
149
  *
150
150
  * @param {{value:string, operator:string, variable:string}[][]} condition
151
+ * @param {object} configuration
151
152
  * @param {string} description
152
153
  */
153
- function customCondition (condition, description = '') {
154
+ function customCondition (condition, configuration, description = '') {
154
155
  if (typeof condition !== 'object' || !Array.isArray(condition)) {
155
156
  throw new Error(`Invalid condition (${description}) type`);
156
157
  }
157
158
 
158
159
  const resolver = (req, res) => condition.some((condList) => condList.every((cond) => {
159
- const data = stateData(req, res);
160
+ const data = stateData(req, res, configuration);
160
161
  const variableValue = getValue(cond.variable, data);
161
162
  const isRegExp = [
162
163
  ConditionOperators['matches regexp'],
@@ -6,7 +6,20 @@
6
6
  const customCondition = require('./customCondition');
7
7
  const customFn = require('./customFn');
8
8
 
9
- module.exports = (params, description = '', allowForbiddenSnippetWords = false) => {
9
+ /** @typedef {import('../BuildRouter').BotContext} BotContext */
10
+
11
+ /**
12
+ *
13
+ * @param {object} params
14
+ * @param {BotContext} context
15
+ * @param {string} description
16
+ * @returns {Function}
17
+ */
18
+ module.exports = function getCondition (params, context, description = '') {
19
+ const {
20
+ allowForbiddenSnippetWords = false,
21
+ configuration
22
+ } = context;
10
23
  const {
11
24
  hasCondition = false,
12
25
  conditionFn = '() => true',
@@ -18,7 +31,7 @@ module.exports = (params, description = '', allowForbiddenSnippetWords = false)
18
31
 
19
32
  if (hasCondition) {
20
33
  if (hasEditableCondition) {
21
- condition = customCondition(editableCondition);
34
+ condition = customCondition(editableCondition, configuration, description);
22
35
  } else {
23
36
  condition = customFn(conditionFn, description, allowForbiddenSnippetWords);
24
37
  }
@@ -20,7 +20,7 @@ const UNSUBSCRIBE = '_$unsubscribe';
20
20
 
21
21
  function getUpdate (attr, value, currentState = {}) {
22
22
  let param;
23
- let rest = attr;
23
+ let rest = attr && attr.replace(/\u2219/g, '.');
24
24
  let state = currentState;
25
25
  const ret = {};
26
26
  let up = ret;
@@ -53,7 +53,7 @@ function getUpdate (attr, value, currentState = {}) {
53
53
 
54
54
  function getValue (attr, currentState = {}) {
55
55
  let param;
56
- let rest = attr;
56
+ let rest = attr && attr.replace(/\u2219/g, '.');
57
57
  let state = currentState;
58
58
 
59
59
  do {
@@ -88,7 +88,7 @@ function toArray (previousValue) {
88
88
  const ENTITY_HBS_REGEXP = /^\s*\{\{\[?@([^@[\]{}\s]+)(\])?\}\}\s*$/;
89
89
  const VARIABLE_HBS_REGEXP = /^\s*\{\{\[?([^@[\]{}\s]+)\]?\}\}\s*$/;
90
90
 
91
- function getSetState (setState, req, res = null, useState = null) {
91
+ function getSetState (setState, req, res = null, useState = null, configuration = null) {
92
92
  if (!setState) {
93
93
  return {};
94
94
  }
@@ -179,7 +179,7 @@ function getSetState (setState, req, res = null, useState = null) {
179
179
  values = [];
180
180
  } else {
181
181
  const useValue = typeof value === 'string'
182
- ? handlebars.compile(value)(stateData(req, res))
182
+ ? handlebars.compile(value)(stateData(req, res, configuration))
183
183
  .split(/(?<!\\),/g)
184
184
  .map((v) => v.replace(/\\,/g, ',').trim())
185
185
  : value;
@@ -205,7 +205,7 @@ function getSetState (setState, req, res = null, useState = null) {
205
205
  set = val;
206
206
  }
207
207
  } else if (typeof val === 'string') {
208
- set = handlebars.compile(val)(stateData(req, res));
208
+ set = handlebars.compile(val)(stateData(req, res, configuration));
209
209
  } else if (val === null
210
210
  || SCALAR_TYPES.includes(typeof val)) {
211
211
  set = val;
@@ -3,11 +3,14 @@
3
3
  */
4
4
  'use strict';
5
5
 
6
- module.exports = function stateData (req, res = null) {
6
+ module.exports = function stateData (req, res = null, configuration = null) {
7
+ const c = configuration || req.configuration;
7
8
  return {
8
9
  ...req.state,
9
10
  ...(res ? res.newState : {}),
10
11
  ...req.actionData(),
11
- ...(res ? res.data : {})
12
+ ...(res ? res.data : {}),
13
+ c,
14
+ configuration: c
12
15
  };
13
16
  };