wingbot 3.30.0 → 3.31.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.30.0",
3
+ "version": "3.31.0",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -53,6 +53,7 @@
53
53
  "tsd-jsdoc": "^2.5.0"
54
54
  },
55
55
  "dependencies": {
56
+ "form-data": "^4.0.0",
56
57
  "graphql": "^15.8.0",
57
58
  "jsonwebtoken": "^8.5.1",
58
59
  "node-fetch": "^2.6.7",
@@ -5,6 +5,7 @@
5
5
 
6
6
  const jwt = require('jsonwebtoken');
7
7
  const { default: fetch, Headers } = require('node-fetch');
8
+ const FormData = require('form-data');
8
9
  const crypto = require('crypto');
9
10
  const { Agent } = require('https');
10
11
  const { promisify } = require('util');
@@ -12,6 +13,7 @@ const ReturnSender = require('./ReturnSender');
12
13
 
13
14
  /** @typedef {import('./ReturnSender').ReturnSenderOptions} ReturnSenderOptions */
14
15
  /** @typedef {import('./ReturnSender').ChatLogStorage} ChatLogStorage */
16
+ /** @typedef {import('./ReturnSender').UploadResult} UploadResult */
15
17
 
16
18
  /**
17
19
  * @typedef {object} TlsOptions
@@ -91,6 +93,38 @@ class BotAppSender extends ReturnSender {
91
93
  return this._agent;
92
94
  }
93
95
 
96
+ /**
97
+ *
98
+ * @param {Buffer} data
99
+ * @param {string} contentType
100
+ * @param {string} fileName
101
+ * @returns {Promise<UploadResult>}
102
+ */
103
+ async upload (data, contentType, fileName) {
104
+ const formData = new FormData();
105
+
106
+ const nonce = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36).padEnd(11, '0');
107
+
108
+ formData.append('nonce', nonce);
109
+ formData.append('f0', data, { filename: fileName, contentType });
110
+
111
+ const [token, agent] = await Promise.all([
112
+ BotAppSender.signBody(nonce, this._secret, this._appId),
113
+ this._getAgent()
114
+ ]);
115
+
116
+ const headers = new Headers();
117
+
118
+ headers.set('Authorization', token);
119
+
120
+ const response = await this._fetch(this._apiUrl, {
121
+ headers, body: formData, agent, method: 'POST'
122
+ })
123
+ .then((r) => r.json());
124
+
125
+ return response;
126
+ }
127
+
94
128
  async _send (payload) {
95
129
  // attach sender
96
130
  if (typeof payload.sender === 'undefined') {
package/src/Responder.js CHANGED
@@ -60,6 +60,8 @@ Object.freeze(ExpectedInput);
60
60
  * @prop {string} [voice]
61
61
  */
62
62
 
63
+ /** @typedef {import('./ReturnSender').UploadResult} UploadResult */
64
+
63
65
  /**
64
66
  * Instance of responder is passed as second parameter of handler (res)
65
67
  *
@@ -537,6 +539,7 @@ class Responder {
537
539
  });
538
540
  } else {
539
541
  this._quickReplyCollector.push({
542
+ // @ts-ignore
540
543
  action: this.toAbsoluteAction(action),
541
544
  title,
542
545
  data,
@@ -749,6 +752,30 @@ class Responder {
749
752
  return ret;
750
753
  }
751
754
 
755
+ /**
756
+ *
757
+ * @param {Buffer} data
758
+ * @param {string} contentType
759
+ * @param {string} fileName
760
+ * @returns {Promise<UploadResult>}
761
+ */
762
+ async upload (data, contentType, fileName) {
763
+ const result = await this._messageSender.upload(data, contentType, fileName);
764
+
765
+ if (!result.url) {
766
+ throw new Error(`Got no url on file upload ${fileName}. Probably a compatibility issue.`);
767
+ }
768
+
769
+ let [type] = contentType.split('/');
770
+
771
+ if (!['image', 'video', 'audio'].includes(type)) {
772
+ type = 'file';
773
+ }
774
+
775
+ this._attachment(result.url, type, true);
776
+ return result;
777
+ }
778
+
752
779
  /**
753
780
  * Sends image as response. Requires appUrl option to send images from server
754
781
  *
@@ -23,6 +23,12 @@ const { FLAG_DO_NOT_LOG } = require('./flags');
23
23
  * @prop {textFilter} [confidentInputFilter] - filter for confident input (@CONFIDENT)
24
24
  */
25
25
 
26
+ /**
27
+ * @typedef {object} UploadResult
28
+ * @prop {string} [url]
29
+ * @prop {string|number} [attachmentId]
30
+ */
31
+
26
32
  /**
27
33
  * Text filter function
28
34
  *
@@ -319,6 +325,17 @@ class ReturnSender {
319
325
  this._visitedInteractions.push(action);
320
326
  }
321
327
 
328
+ /**
329
+ *
330
+ * @param {Buffer} data
331
+ * @param {string} contentType
332
+ * @param {string} fileName
333
+ * @returns {Promise<UploadResult>}
334
+ */
335
+ async upload (data, contentType, fileName) { // eslint-disable-line no-unused-vars
336
+ throw new Error('#upload() not supported by this channel');
337
+ }
338
+
322
339
  send (payload) {
323
340
  if (this._finished) {
324
341
  throw new Error('Cannot send message after sender is finished');
@@ -452,7 +452,7 @@ class NotificationsStorage {
452
452
  * @param {string} tag
453
453
  * @returns {Promise}
454
454
  */
455
- subscribe (senderId, pageId, tag) {
455
+ _subscribe (senderId, pageId, tag) {
456
456
  const key = `${senderId}|${pageId}`;
457
457
  let subscribtion = this._subscribtions.get(key);
458
458
  if (!subscribtion) {
@@ -467,6 +467,18 @@ class NotificationsStorage {
467
467
  }
468
468
 
469
469
  this._subscribtions.set(key, subscribtion);
470
+ }
471
+
472
+ /**
473
+ *
474
+ * @param {string|string[]} senderId
475
+ * @param {string} pageId
476
+ * @param {string} tag
477
+ * @returns {Promise}
478
+ */
479
+ subscribe (senderId, pageId, tag) {
480
+ const insert = Array.isArray(senderId) ? senderId : [senderId];
481
+ insert.forEach((sender) => this._subscribe(sender, pageId, tag));
470
482
  return Promise.resolve();
471
483
  }
472
484
 
@@ -198,12 +198,16 @@ function notificationsApiFactory (storage, notifications, acl, options = {}) {
198
198
  );
199
199
  }
200
200
 
201
+ senderIds = senderIds.slice();
202
+
201
203
  // make it little bit faster
202
204
  while (senderIds.length > 0) {
203
- await Promise.all(
204
- senderIds.splice(0, 5)
205
- .map((senderId) => storage.subscribe(`${senderId}`, pageId, tag))
206
- );
205
+ if (storage.subscribe.length > 3) {
206
+ const insertSenders = senderIds.splice(0, 500);
207
+ await storage.subscribe(insertSenders, pageId, tag, true);
208
+ } else {
209
+ await storage.subscribe(senderIds.pop(), pageId, tag);
210
+ }
207
211
  }
208
212
 
209
213
  return true;
@@ -9,20 +9,27 @@ const Request = require('../Request');
9
9
  const Router = require('../Router');
10
10
  const getCondition = require('../utils/getCondition');
11
11
  const {
12
- stateData, getLanguageText, getLanguageTextObjects, randomizedCompiler
12
+ stateData,
13
+ getLanguageText,
14
+ getLanguageTextObjects,
15
+ randomizedCompiler,
16
+ cachedTranslatedCompilator
13
17
  } = require('./utils');
14
18
  const {
15
19
  FEATURE_SSML, FEATURE_TEXT, FEATURE_VOICE
16
20
  } = require('../features');
17
21
 
22
+ /** @typedef {import('../Responder').VoiceControl} VoiceControl */
23
+ /** @typedef {import('./utils').Translations} Translations */
24
+ /** @typedef {import('./utils').TextObject} TextObject */
18
25
  /**
19
26
  * Returns voice control props from params
20
27
  *
21
28
  * @param {any} params
22
29
  * @param {string} lang
23
- * @returns {null | import('../Responder').VoiceControl}
30
+ * @returns {null | VoiceControl}
24
31
  */
25
- function getVoiceControl (params, lang = null) {
32
+ function getVoiceControlFromParams (params, lang = null) {
26
33
  const voiceControl = {};
27
34
 
28
35
  const voiceControlProps = ['speed', 'pitch', 'volume', 'voice', 'style', 'language'];
@@ -75,7 +82,7 @@ function parseReplies (replies, linksMap, allowForbiddenSnippetWords) {
75
82
  action,
76
83
  condition,
77
84
  title: reply.title
78
- ? randomizedCompiler(reply.title)
85
+ ? cachedTranslatedCompilator(reply.title)
79
86
  : null,
80
87
  data: {}
81
88
  };
@@ -101,10 +108,10 @@ function parseReplies (replies, linksMap, allowForbiddenSnippetWords) {
101
108
  }
102
109
 
103
110
  /**
104
- * @param {import('./utils').Translations} text
111
+ * @param {Translations} text
105
112
  * @param {string[]} features
106
113
  * @param {string} lang
107
- * @returns {{translations: import('./utils').TextObject[],ssmlAlternatives:string[] | null}}
114
+ * @returns {{translations:TextObject[],ssmlAlternatives:string[] | null}}
108
115
  */
109
116
  function findSupportedMessages (text, features, lang = null) {
110
117
  let translations = getLanguageTextObjects(text, lang);
@@ -180,7 +187,6 @@ function message (params, {
180
187
  const data = stateData(req, res);
181
188
 
182
189
  // filter supported messages
183
- /** @type {{translations:import('./utils').TextObject[],ssmlAlternatives:string[]}} */
184
190
  const supportedText = findSupportedMessages(
185
191
  params.text,
186
192
  req.features,
@@ -231,7 +237,7 @@ function message (params, {
231
237
  }
232
238
 
233
239
  // generate voice control
234
- let voiceControl = getVoiceControl(params, data.lang);
240
+ let voiceControl = getVoiceControlFromParams(params, data.lang);
235
241
  if (supportedText.ssmlAlternatives) {
236
242
  // find SSML alternative
237
243
  const ssmlAlternativeTemplate = randomizedCompiler([{