wingbot 3.27.0-alpha.5 → 3.28.0

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.27.0-alpha.5",
3
+ "version": "3.28.0",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/BotApp.js CHANGED
@@ -262,7 +262,8 @@ class BotApp {
262
262
  pageId,
263
263
  appId,
264
264
  secret,
265
- fetch: this._fetch
265
+ fetch: this._fetch,
266
+ tls: this._tls
266
267
  };
267
268
 
268
269
  const sender = new BotAppSender(options, senderId, message, this._senderLogger);
package/src/Request.js CHANGED
@@ -1411,7 +1411,8 @@ It looks like the bot isn't connected to class BotApp or the Processor is used w
1411
1411
  data = {},
1412
1412
  refAction = null,
1413
1413
  refData = {},
1414
- timestamp = makeTimestamp()
1414
+ timestamp = makeTimestamp(),
1415
+ features = [FEATURE_TEXT]
1415
1416
  ) {
1416
1417
  const postback = {
1417
1418
  payload: {
@@ -1429,7 +1430,8 @@ It looks like the bot isn't connected to class BotApp or the Processor is used w
1429
1430
  sender: {
1430
1431
  id: senderId
1431
1432
  },
1432
- postback
1433
+ postback,
1434
+ features
1433
1435
  };
1434
1436
  }
1435
1437
 
@@ -1725,4 +1727,24 @@ It looks like the bot isn't connected to class BotApp or the Processor is used w
1725
1727
 
1726
1728
  }
1727
1729
 
1730
+ /**
1731
+ * @constant {string} FEATURE_VOICE channel supports voice messages
1732
+ */
1733
+ Request.FEATURE_VOICE = FEATURE_VOICE;
1734
+
1735
+ /**
1736
+ * @constant {string} FEATURE_SSML channel supports SSML voice messages
1737
+ */
1738
+ Request.FEATURE_SSML = FEATURE_SSML;
1739
+
1740
+ /**
1741
+ * @constant {string} FEATURE_PHRASES channel supports expected phrases messages
1742
+ */
1743
+ Request.FEATURE_PHRASES = FEATURE_PHRASES;
1744
+
1745
+ /**
1746
+ * @constant {string} FEATURE_TEXT channel supports text communication
1747
+ */
1748
+ Request.FEATURE_TEXT = FEATURE_TEXT;
1749
+
1728
1750
  module.exports = Request;
package/src/Tester.js CHANGED
@@ -14,6 +14,8 @@ const AnyResponseAssert = require('./testTools/AnyResponseAssert');
14
14
  const ResponseAssert = require('./testTools/ResponseAssert');
15
15
 
16
16
  const Router = require('./Router'); // eslint-disable-line no-unused-vars
17
+ const ReducerWrapper = require('./ReducerWrapper'); // eslint-disable-line no-unused-vars
18
+ const { FEATURE_TEXT } = require('./features');
17
19
 
18
20
  /**
19
21
  * Utility for testing requests
@@ -118,6 +120,11 @@ class Tester {
118
120
  * @prop {console} use own loggger
119
121
  */
120
122
  this.senderLogger = undefined;
123
+
124
+ /**
125
+ * @prop {string[]}
126
+ */
127
+ this.features = null;
121
128
  }
122
129
 
123
130
  dealloc () {
@@ -148,6 +155,15 @@ class Tester {
148
155
  this._responsesMock = [];
149
156
  }
150
157
 
158
+ /**
159
+ * Set features for all messages
160
+ *
161
+ * @param {string[]} [features]
162
+ */
163
+ setFeatures (features = [FEATURE_TEXT]) {
164
+ this.features = features;
165
+ }
166
+
151
167
  /**
152
168
  * Use tester as a connector :)
153
169
  *
@@ -164,6 +180,10 @@ class Tester {
164
180
  });
165
181
  }
166
182
 
183
+ if (this.features) {
184
+ Object.assign(message, { features: this.features });
185
+ }
186
+
167
187
  const messageSender = new ReturnSender({}, senderId, message, this.senderLogger);
168
188
  messageSender.simulatesOptIn = true;
169
189
 
@@ -259,7 +279,7 @@ class Tester {
259
279
  }
260
280
 
261
281
  /**
262
- * Checks, that a plugin used a block as a responde
282
+ * Checks, that a plugin used a block as a response
263
283
  *
264
284
  * @param {string} blockName
265
285
  * @returns {this}
@@ -308,7 +328,6 @@ class Tester {
308
328
  *
309
329
  * @param {string} text
310
330
  * @returns {Promise}
311
- *
312
331
  * @memberOf Tester
313
332
  */
314
333
  text (text) {
@@ -430,12 +449,11 @@ class Tester {
430
449
  * @param {string} [refAction=null] - referred action
431
450
  * @param {object} [refData={}] - referred action data
432
451
  * @returns {Promise}
433
- *
434
452
  * @memberOf Tester
435
453
  */
436
454
  postBack (action, data = {}, refAction = null, refData = {}) {
437
455
  return this.processMessage(Request
438
- .postBack(this.senderId, action, data, refAction, refData));
456
+ .postBack(this.senderId, action, data, refAction, refData, null));
439
457
  }
440
458
 
441
459
  }
@@ -2,10 +2,40 @@
2
2
  * @author David Menger
3
3
  */
4
4
  'use strict';
5
-
5
+ // eslint-disable-next-line no-unused-vars
6
+ const Responder = require('../Responder');
7
+ // eslint-disable-next-line no-unused-vars
8
+ const Request = require('../Request');
6
9
  const Router = require('../Router');
7
10
  const getCondition = require('../utils/getCondition');
8
- const { cachedTranslatedCompilator, stateData } = require('./utils');
11
+ const {
12
+ stateData, getLanguageText, getLanguageTextObjects, randomizedCompiler
13
+ } = require('./utils');
14
+ const {
15
+ FEATURE_SSML, FEATURE_TEXT, FEATURE_VOICE
16
+ } = require('../features');
17
+
18
+ /**
19
+ * Returns voice control props from params
20
+ *
21
+ * @param {any} params
22
+ * @param {string} lang
23
+ * @returns {null | import('../Responder').VoiceControl}
24
+ */
25
+ function getVoiceControl (params, lang = null) {
26
+ const voiceControl = {};
27
+
28
+ const voiceControlProps = ['speed', 'pitch', 'volume', 'voice', 'style', 'language'];
29
+
30
+ voiceControlProps.forEach((prop) => {
31
+ if (params[prop]) {
32
+ voiceControl[prop] = getLanguageText(params[prop], lang);
33
+ }
34
+ });
35
+
36
+ // if voiceControl is empty, return null
37
+ return Object.keys(voiceControl).length > 0 ? voiceControl : null;
38
+ }
9
39
 
10
40
  function parseReplies (replies, linksMap, allowForbiddenSnippetWords) {
11
41
  return replies.map((reply) => {
@@ -45,7 +75,7 @@ function parseReplies (replies, linksMap, allowForbiddenSnippetWords) {
45
75
  action,
46
76
  condition,
47
77
  title: reply.title
48
- ? cachedTranslatedCompilator(reply.title)
78
+ ? randomizedCompiler(reply.title)
49
79
  : null,
50
80
  data: {}
51
81
  };
@@ -70,43 +100,108 @@ function parseReplies (replies, linksMap, allowForbiddenSnippetWords) {
70
100
  });
71
101
  }
72
102
 
103
+ /**
104
+ * @param {import('./utils').Translations} text
105
+ * @param {string[]} features
106
+ * @param {string} lang
107
+ * @returns {{translations: import('./utils').TextObject[],ssmlAlternatives:string[] | null}}
108
+ */
109
+ function findSupportedMessages (text, features, lang = null) {
110
+ let translations = getLanguageTextObjects(text, lang);
111
+
112
+ const useSSML = features.includes(FEATURE_SSML);
113
+ const useText = features.includes(FEATURE_TEXT);
114
+ const useVoice = features.includes(FEATURE_VOICE);
115
+
116
+ // filter out SSML alternatives - they will be used in voice control
117
+ let ssmlAlternatives = translations.filter((t) => t.p === 's').map((t) => t.t);
118
+ if (ssmlAlternatives.length === 0 || !useSSML) {
119
+ ssmlAlternatives = null;
120
+ }
121
+
122
+ translations = translations.filter((t) => t.p !== 's');
123
+
124
+ // find supported text alternatives
125
+ translations = translations.filter((translation) => {
126
+ // always use text+voice
127
+ if (!translation.p) {
128
+ return true;
129
+ }
130
+
131
+ // text only
132
+ if (useText && translation.p === 't') {
133
+ return true;
134
+ }
135
+
136
+ // voice (& SSML) only
137
+ if (useVoice && translation.p === 'v') {
138
+ return true;
139
+ }
140
+
141
+ return false;
142
+ });
143
+
144
+ return {
145
+ translations,
146
+ ssmlAlternatives
147
+ };
148
+ }
149
+
73
150
  function message (params, {
151
+ // @ts-ignore
74
152
  isLastIndex, isLastMessage, linksMap, allowForbiddenSnippetWords
75
- }) {
153
+ } = {}) {
154
+
76
155
  if (typeof params.text !== 'string' && !Array.isArray(params.text)) {
77
156
  throw new Error('Message should be a text!');
78
157
  }
79
158
 
80
- const textTemplate = cachedTranslatedCompilator(params.text);
81
-
159
+ // parse quick replies
82
160
  let quickReplies = null;
83
-
84
161
  if (params.replies && !Array.isArray(params.replies)) {
85
162
  throw new Error('Replies should be an array');
86
163
  } else if (params.replies && params.replies.length > 0) {
87
164
  quickReplies = parseReplies(params.replies, linksMap, allowForbiddenSnippetWords);
88
165
  }
89
166
 
167
+ // compile condition
90
168
  const condition = getCondition(params, 'Message condition', allowForbiddenSnippetWords);
91
169
 
92
170
  const ret = isLastIndex ? Router.END : Router.CONTINUE;
93
171
 
172
+ /**
173
+ * @param {Request} req
174
+ * @param {Responder} res
175
+ */
94
176
  return (req, res) => {
95
- if (condition !== null) {
96
- if (!condition(req, res)) {
97
- return ret;
98
- }
177
+ if (condition && !condition(req, res)) {
178
+ return ret;
99
179
  }
100
-
101
180
  const data = stateData(req, res);
181
+
182
+ // filter supported messages
183
+ /** @type {{translations:import('./utils').TextObject[],ssmlAlternatives:string[]}} */
184
+ const supportedText = findSupportedMessages(
185
+ params.text,
186
+ req.features,
187
+ data.lang
188
+ );
189
+
190
+ // find random alternative
191
+ const textTemplate = randomizedCompiler([{
192
+ l: data.lang,
193
+ t: supportedText.translations.map((x) => x.t)
194
+ }]);
195
+
102
196
  const text = textTemplate(data)
103
197
  .trim();
104
198
 
199
+ let sendReplies;
105
200
  if (quickReplies) {
106
201
  const okQuickReplies = quickReplies
107
202
  .filter((reply) => reply.condition(req, res));
108
203
 
109
- const sendReplies = okQuickReplies
204
+ sendReplies = okQuickReplies
110
205
  .filter((reply) => reply.title
111
206
  || reply.isLocation
112
207
  || reply.isEmail
@@ -123,8 +218,6 @@ function message (params, {
123
218
  return rep;
124
219
  });
125
220
 
126
- res.text(text, sendReplies);
127
-
128
221
  okQuickReplies
129
222
  .filter((reply) => !reply.title && reply.match)
130
223
  .forEach(({
@@ -134,10 +227,27 @@ function message (params, {
134
227
  });
135
228
  } else {
136
229
  // replies on last index will be present, so the addQuickReply will be working
137
- const sendReplies = isLastMessage ? [] : undefined;
138
- res.text(text, sendReplies);
230
+ sendReplies = isLastMessage ? [] : undefined;
139
231
  }
140
232
 
233
+ // generate voice control
234
+ let voiceControl = getVoiceControl(params, data.lang);
235
+ if (supportedText.ssmlAlternatives) {
236
+ // find SSML alternative
237
+ const ssmlAlternativeTemplate = randomizedCompiler([{
238
+ l: data.lang,
239
+ t: supportedText.ssmlAlternatives
240
+ }]);
241
+
242
+ voiceControl = {
243
+ ...voiceControl,
244
+ ssml: ssmlAlternativeTemplate(data)
245
+ .trim()
246
+ };
247
+ }
248
+
249
+ res.text(text, sendReplies, voiceControl);
250
+
141
251
  return ret;
142
252
  };
143
253
  }
@@ -19,6 +19,28 @@ const WEBVIEW_FULL = 'full';
19
19
  const WEBVIEW_TALL = 'tall';
20
20
  const WEBVIEW_COMPACT = 'compact';
21
21
 
22
+ /**
23
+ * @typedef {Translation[] | Translation | string[] | string} Translations
24
+ */
25
+
26
+ /**
27
+ * @typedef {object} TextObject
28
+ * @prop {string} t - text
29
+ * @prop {string} l - lang
30
+ * @prop {string|null} p - purpose
31
+ */
32
+
33
+ /**
34
+ * @typedef Translation
35
+ * @property {string|string[]} t - text alternatives
36
+ * @property {string | null} l - language
37
+ * @property {string|string[]} [p] - purposes
38
+ * null = default + voice
39
+ * t = text
40
+ * v = voice
41
+ * s = ssml
42
+ */
43
+
22
44
  function isArrayOfObjects (translations) {
23
45
  return Array.isArray(translations)
24
46
  && typeof translations[0] === 'object'
@@ -36,18 +58,20 @@ function isTextObjectEmpty (text) {
36
58
  }
37
59
 
38
60
  /**
39
- *
40
- * @param {{t:string,l:string}[]|string} translations
61
+ * @param {Translations} translations
41
62
  * @param {string} [lang]
42
63
  * @returns {null|string}
43
64
  */
44
65
  function getLanguageText (translations, lang = null) {
45
66
  let foundText;
67
+
46
68
  if (isArrayOfObjects(translations)) {
47
69
  if (lang) {
70
+ // @ts-ignore
48
71
  foundText = translations.find((t) => t.l === lang);
49
72
  }
50
73
  if (isTextObjectEmpty(foundText)) {
74
+ // @ts-ignore
51
75
  foundText = translations.find((t) => !isTextObjectEmpty(t));
52
76
  }
53
77
  foundText = foundText ? foundText.t : null;
@@ -63,6 +87,58 @@ function getLanguageText (translations, lang = null) {
63
87
  }
64
88
  return foundText || '';
65
89
  }
90
+ /**
91
+ *
92
+ * @param {Translations} translations
93
+ * @param {string} [lang]
94
+ * @returns {TextObject[]}
95
+ */
96
+ function getLanguageTextObjects (translations, lang = null) {
97
+ /** @type {{t:string|string[],l?:string,p?:string|string[]}[]} */
98
+ let foundTexts;
99
+
100
+ if (!Array.isArray(translations) || typeof translations[0] === 'string') {
101
+ // @ts-ignore
102
+ foundTexts = [{ t: translations, l: lang }];
103
+ } else { // is array of objects
104
+ foundTexts = translations
105
+ // @ts-ignore
106
+ .filter(({ l = null, ...to }) => l === lang && !isTextObjectEmpty(to));
107
+
108
+ if (foundTexts.length === 0) {
109
+ const { l: firstNonEmptyLang = null } = translations
110
+ // @ts-ignore
111
+ .find((to) => !isTextObjectEmpty(to)) || {};
112
+
113
+ foundTexts = translations
114
+ // @ts-ignore
115
+ .filter(({ l = null, ...to }) => l === firstNonEmptyLang && !isTextObjectEmpty(to));
116
+ }
117
+ }
118
+
119
+ return foundTexts
120
+ .reduce((a, to) => {
121
+ if (Array.isArray(to.t)) {
122
+ const purposes = Array.isArray(to.p)
123
+ ? to.p
124
+ : [to.p];
125
+ a.push(
126
+ ...to.t.map((t, i) => ({
127
+ ...to, t, p: purposes[i] || null
128
+ }))
129
+ );
130
+ } else {
131
+ a.push(to);
132
+ }
133
+ return a;
134
+ }, [])
135
+ .filter((to) => !!to.t)
136
+ .map(({
137
+ t, l = null, p = null, ...rest
138
+ }) => ({
139
+ t, l, p, ...rest
140
+ }));
141
+ }
66
142
 
67
143
  function randomizedCompiler (text, lang) {
68
144
  const texts = getLanguageText(text, lang);
@@ -100,6 +176,13 @@ function randomizedCompiler (text, lang) {
100
176
  };
101
177
  }
102
178
 
179
+ /**
180
+ * l - language
181
+ * t - alternatives
182
+ *
183
+ * @param {{l:string,t:string[]}[] | string} text
184
+ * @returns {(state:any)=>string}
185
+ */
103
186
  function cachedTranslatedCompilator (text) {
104
187
  const cache = new Map();
105
188
 
@@ -194,7 +277,9 @@ function processButtons (
194
277
 
195
278
  module.exports = {
196
279
  getLanguageText,
280
+ getLanguageTextObjects,
197
281
  cachedTranslatedCompilator,
282
+ randomizedCompiler,
198
283
  getText,
199
284
  stateData,
200
285
 
@@ -58,6 +58,14 @@ const { replaceDiacritics } = require('../utils');
58
58
 
59
59
  /** @typedef {import('../Request')} Request */
60
60
 
61
+ function optionalWrap (l, r, content) {
62
+ const consistent = !l === !r;
63
+ if (consistent) {
64
+ return `(${content})`;
65
+ }
66
+ return `${l || ''}(${content})${r || ''}`;
67
+ }
68
+
61
69
  class CustomEntityDetectionModel {
62
70
 
63
71
  /**
@@ -508,16 +516,16 @@ class CustomEntityDetectionModel {
508
516
  return null;
509
517
  }
510
518
 
511
- let replaced = source.replace(/@[A-Z0-9-]+/g, (value) => {
519
+ let replaced = source.replace(/(\()?@([A-Z0-9-]+)(\))?/g, (value, l, ent, r) => {
512
520
  const matchingEntities = entities
513
- .filter((e) => `@${e.entity.toUpperCase()}` === value)
521
+ .filter((e) => e.entity.toUpperCase() === ent)
514
522
  .map((e) => this._escapeRegex(e.text, options.replaceDiacritics));
515
523
 
516
524
  if (matchingEntities.length === 0) {
517
- return value;
525
+ return optionalWrap(l, r, `@${ent}`);
518
526
  }
519
527
  matchingEntities.sort((a, z) => z.length - a.length);
520
- return `(${matchingEntities.join('|')})`;
528
+ return optionalWrap(l, r, matchingEntities.join('|'));
521
529
  });
522
530
 
523
531
  if (options.matchWholeWords && !searchWithinWords) {