wingbot 3.27.2 → 3.30.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 +16 -21
- package/plugins/ai.wingbot.goBack/plugin.js +1 -1
- package/plugins/ai.wingbot.goToLastBreadcrumb/plugin.js +1 -1
- package/plugins/ai.wingbot.jumpBack/plugin.js +1 -1
- package/plugins/ai.wingbot.jumpTo/plugin.js +1 -1
- package/plugins/ai.wingbot.stopResponding/plugin.js +3 -1
- package/src/Ai.js +59 -19
- package/src/AiMatching.js +11 -5
- package/src/BotApp.js +20 -10
- package/src/ConversationTester.js +2 -3
- package/src/OrchestratorClient.js +3 -5
- package/src/Processor.js +2 -2
- package/src/Request.js +53 -11
- package/src/Tester.js +22 -4
- package/src/graphApi/schema.gql +1 -0
- package/src/notifications/Notifications.js +31 -3
- package/src/notifications/api/notificationsApiFactory.js +51 -8
- package/src/resolvers/media.js +12 -1
- package/src/resolvers/message.js +127 -17
- package/src/resolvers/utils.js +93 -5
- package/src/tools/MemoryBotConfigStorage.js +1 -1
- package/src/utils/customCondition.js +2 -2
- package/src/utils/getUpdate.js +4 -2
- package/src/utils/quickReplies.js +12 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wingbot",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.30.0",
|
|
4
4
|
"description": "Enterprise Messaging Bot Conversation Engine",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -36,33 +36,28 @@
|
|
|
36
36
|
},
|
|
37
37
|
"homepage": "https://github.com/wingbot.ai/wingbot#readme",
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"cpy-cli": "^
|
|
40
|
-
"eslint": "^
|
|
41
|
-
"eslint-config-airbnb": "^
|
|
42
|
-
"eslint-
|
|
43
|
-
"eslint-plugin-
|
|
44
|
-
"eslint-plugin-
|
|
45
|
-
"eslint-plugin-
|
|
46
|
-
"eslint-plugin-
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"graphql-tools": "^7.0.5",
|
|
51
|
-
"jsdoc-to-markdown": "^6.0.1",
|
|
52
|
-
"mocha": "^8.2.1",
|
|
53
|
-
"move-cli": "^1.2.1",
|
|
39
|
+
"cpy-cli": "^4.1.0",
|
|
40
|
+
"eslint": "^8.11.0",
|
|
41
|
+
"eslint-config-airbnb": "^19.0.4",
|
|
42
|
+
"eslint-plugin-import": "^2.25.4",
|
|
43
|
+
"eslint-plugin-jsdoc": "^38.0.4",
|
|
44
|
+
"eslint-plugin-jsx-a11y": "^6.5.1",
|
|
45
|
+
"eslint-plugin-mocha": "^10.0.3",
|
|
46
|
+
"eslint-plugin-react": "^7.29.4",
|
|
47
|
+
"graphql-markdown": "^6.0.0",
|
|
48
|
+
"jsdoc-to-markdown": "^7.1.1",
|
|
49
|
+
"mocha": "^9.2.2",
|
|
54
50
|
"nyc": "^15.1.0",
|
|
55
51
|
"rimraf": "^3.0.2",
|
|
56
|
-
"sinon": "^
|
|
52
|
+
"sinon": "^13.0.1",
|
|
57
53
|
"tsd-jsdoc": "^2.5.0"
|
|
58
54
|
},
|
|
59
55
|
"dependencies": {
|
|
60
|
-
"graphql": "^15.
|
|
61
|
-
"graphql-mongodb-projection": "^1.1.1",
|
|
56
|
+
"graphql": "^15.8.0",
|
|
62
57
|
"jsonwebtoken": "^8.5.1",
|
|
63
|
-
"node-fetch": "^2.6.
|
|
58
|
+
"node-fetch": "^2.6.7",
|
|
64
59
|
"path-to-regexp": "^6.2.0",
|
|
65
|
-
"uuid": "^8.3.
|
|
60
|
+
"uuid": "^8.3.2",
|
|
66
61
|
"webalize": "^0.1.0"
|
|
67
62
|
},
|
|
68
63
|
"optionalDependencies": {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @param {import('../../src/Responder')} res
|
|
4
4
|
* @param {Function} postback
|
|
5
5
|
*/
|
|
6
|
-
module.exports = (req, res, postback)
|
|
6
|
+
module.exports = function (req, res, postback) {
|
|
7
7
|
if (req.state.beforeLastInteraction
|
|
8
8
|
&& !req.state.beforeLastInteraction.match(/\/\*$/)
|
|
9
9
|
&& req.state.beforeLastInteraction !== res.currentAction()) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @param {import('../../src/Responder')} res
|
|
4
4
|
* @param {Function} postback
|
|
5
5
|
*/
|
|
6
|
-
module.exports = (req, res, postback)
|
|
6
|
+
module.exports = function (req, res, postback) {
|
|
7
7
|
let { breadcrumbs = [] } = req.state;
|
|
8
8
|
let shift = req.state.breadcrumbAction === req.state._lastAction
|
|
9
9
|
? 2
|
package/src/Ai.js
CHANGED
|
@@ -64,6 +64,20 @@ class Ai {
|
|
|
64
64
|
*/
|
|
65
65
|
this.threshold = 0.3;
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Upper limit for NLP resolving of STT alternatives
|
|
69
|
+
*
|
|
70
|
+
* @type {number}
|
|
71
|
+
*/
|
|
72
|
+
this.sttMaxAlternatives = 3;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Minimal score to consider text as recognized well
|
|
76
|
+
*
|
|
77
|
+
* @type {number}
|
|
78
|
+
*/
|
|
79
|
+
this.sttScoreThreshold = 0;
|
|
80
|
+
|
|
67
81
|
/**
|
|
68
82
|
* The logger (console by default)
|
|
69
83
|
*
|
|
@@ -581,35 +595,61 @@ class Ai {
|
|
|
581
595
|
}
|
|
582
596
|
}
|
|
583
597
|
|
|
598
|
+
const texts = req.textAlternatives()
|
|
599
|
+
.filter((alt) => alt.score >= this.sttScoreThreshold)
|
|
600
|
+
.slice(0, this.sttMaxAlternatives);
|
|
601
|
+
|
|
602
|
+
const altMax = Math.max(0, ...texts.map((t) => t.score));
|
|
603
|
+
const altKoef = (1 - this.confidence);
|
|
604
|
+
|
|
584
605
|
const results = await Promise.all(
|
|
585
|
-
|
|
586
|
-
.
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
606
|
+
texts.map(({ text, score }) => model
|
|
607
|
+
.resolve(this.textFilter(text), req)
|
|
608
|
+
.then((res) => ({
|
|
609
|
+
...res,
|
|
610
|
+
intents: res.intents
|
|
611
|
+
.map((i) => ({
|
|
612
|
+
...i,
|
|
613
|
+
score: i.score - (altKoef * (altMax - score))
|
|
614
|
+
}))
|
|
615
|
+
.sort(({ score: a }, { score: z }) => z - a)
|
|
616
|
+
})))
|
|
592
617
|
);
|
|
593
618
|
|
|
594
619
|
results.sort(({
|
|
595
|
-
intents:
|
|
620
|
+
intents: [aIntent = { score: 0 }]
|
|
596
621
|
}, {
|
|
597
|
-
intents:
|
|
598
|
-
}) =>
|
|
599
|
-
const [{ score: aScore = 0 } = {}] = aIntents;
|
|
600
|
-
const [{ score: zScore = 0 } = {}] = zIntents;
|
|
622
|
+
intents: [zIntent = { score: 0 }]
|
|
623
|
+
}) => zIntent.score - aIntent.score);
|
|
601
624
|
|
|
602
|
-
|
|
603
|
-
const z = zAlt * zScore;
|
|
625
|
+
const [winner = { intents: [], entities: [] }, ...others] = results;
|
|
604
626
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
627
|
+
if (others.length === 0) {
|
|
628
|
+
return winner;
|
|
629
|
+
}
|
|
608
630
|
|
|
609
|
-
|
|
631
|
+
const { intents } = winner;
|
|
632
|
+
|
|
633
|
+
const known = new Set(intents.map((i) => i.intent));
|
|
634
|
+
|
|
635
|
+
others.forEach((other) => {
|
|
636
|
+
intents.push(
|
|
637
|
+
...other.intents.filter(({ score, intent }) => {
|
|
638
|
+
if (score >= this.confidence || known.has(intent)) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
known.add(intent);
|
|
642
|
+
return true;
|
|
643
|
+
})
|
|
644
|
+
);
|
|
610
645
|
});
|
|
611
646
|
|
|
612
|
-
|
|
647
|
+
intents.sort(({ score: a }, { score: z }) => z - a);
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
...winner,
|
|
651
|
+
intents
|
|
652
|
+
};
|
|
613
653
|
}
|
|
614
654
|
|
|
615
655
|
/**
|
package/src/AiMatching.js
CHANGED
|
@@ -53,12 +53,16 @@ const COMPARE = {
|
|
|
53
53
|
* @param {Entity[]} [entities]
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {string|number|Function} Comparable
|
|
58
|
+
*/
|
|
59
|
+
|
|
56
60
|
/**
|
|
57
61
|
* @typedef {object} EntityExpression
|
|
58
62
|
* @prop {string} entity - the requested entity
|
|
59
63
|
* @prop {boolean} [optional] - the match is optional
|
|
60
64
|
* @prop {Compare} [op] - comparison operation
|
|
61
|
-
* @prop {
|
|
65
|
+
* @prop {Comparable[]} [compare] - value to compare
|
|
62
66
|
*/
|
|
63
67
|
|
|
64
68
|
/**
|
|
@@ -578,10 +582,12 @@ class AiMatching {
|
|
|
578
582
|
score: 1
|
|
579
583
|
};
|
|
580
584
|
|
|
581
|
-
matching = this
|
|
582
|
-
.
|
|
583
|
-
|
|
584
|
-
|
|
585
|
+
matching = this._entityIsMatching(
|
|
586
|
+
wanted.op,
|
|
587
|
+
wanted.compare,
|
|
588
|
+
requestEntity.value,
|
|
589
|
+
requestState
|
|
590
|
+
);
|
|
585
591
|
}
|
|
586
592
|
} else if (!entityExists) {
|
|
587
593
|
matching = this
|
package/src/BotApp.js
CHANGED
|
@@ -87,14 +87,12 @@ class BotApp {
|
|
|
87
87
|
this._senderLogger = chatLogStorage;
|
|
88
88
|
this._verify = promisify(jwt.verify);
|
|
89
89
|
|
|
90
|
-
this._processor = new Processor(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
);
|
|
90
|
+
this._processor = new Processor(bot, {
|
|
91
|
+
...processorOptions,
|
|
92
|
+
secret,
|
|
93
|
+
fetch: this._fetch,
|
|
94
|
+
apiUrl: gqlApiUrl
|
|
95
|
+
});
|
|
98
96
|
|
|
99
97
|
this._processor.plugin(BotApp.plugin());
|
|
100
98
|
this._preferSynchronousResponse = preferSynchronousResponse;
|
|
@@ -191,7 +189,13 @@ class BotApp {
|
|
|
191
189
|
}
|
|
192
190
|
|
|
193
191
|
async _processIncommingMessage (
|
|
194
|
-
message,
|
|
192
|
+
message,
|
|
193
|
+
senderId,
|
|
194
|
+
pageId,
|
|
195
|
+
appId,
|
|
196
|
+
secret,
|
|
197
|
+
sync = false,
|
|
198
|
+
headers = {}
|
|
195
199
|
) {
|
|
196
200
|
const { mid = null } = message;
|
|
197
201
|
|
|
@@ -386,7 +390,13 @@ class BotApp {
|
|
|
386
390
|
|
|
387
391
|
for (const message of messaging) {
|
|
388
392
|
const res = await this._processIncommingMessage(
|
|
389
|
-
message,
|
|
393
|
+
message,
|
|
394
|
+
senderId,
|
|
395
|
+
pageId,
|
|
396
|
+
appId,
|
|
397
|
+
secret,
|
|
398
|
+
sync,
|
|
399
|
+
headers
|
|
390
400
|
);
|
|
391
401
|
|
|
392
402
|
responses.push(res);
|
|
@@ -305,9 +305,8 @@ class ConversationTester {
|
|
|
305
305
|
return testCases
|
|
306
306
|
// @ts-ignore
|
|
307
307
|
.reduce(
|
|
308
|
-
(map, testCase) => map
|
|
309
|
-
testCase.list, [...(map.get(testCase.list) || []), testCase]
|
|
310
|
-
),
|
|
308
|
+
(map, testCase) => map
|
|
309
|
+
.set(testCase.list, [...(map.get(testCase.list) || []), testCase]),
|
|
311
310
|
new Map()
|
|
312
311
|
);
|
|
313
312
|
}
|
|
@@ -77,11 +77,9 @@ class OrchestratorClient {
|
|
|
77
77
|
*/
|
|
78
78
|
async addConversationTokenToUrl (url, expirationInSeconds, tokenName = 'wbchtoken') {
|
|
79
79
|
const { conversationToken } = await this.getConversationToken(expirationInSeconds);
|
|
80
|
-
return withParams(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
);
|
|
80
|
+
return withParams(url, {
|
|
81
|
+
query: { [tokenName]: conversationToken }
|
|
82
|
+
});
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
/**
|
package/src/Processor.js
CHANGED
|
@@ -100,7 +100,7 @@ const { mergeState } = require('./utils/stateVariables');
|
|
|
100
100
|
* @prop {string|null} [meta.targetAction]
|
|
101
101
|
*/
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
function NAME_FROM_STATE (state) {
|
|
104
104
|
if (state.user && state.user.firstName) {
|
|
105
105
|
return `${state.user.firstName} ${state.user.lastName}`;
|
|
106
106
|
}
|
|
@@ -108,7 +108,7 @@ const NAME_FROM_STATE = (state) => {
|
|
|
108
108
|
return `${state.user.name}`;
|
|
109
109
|
}
|
|
110
110
|
return null;
|
|
111
|
-
}
|
|
111
|
+
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* Messaging event processor
|
package/src/Request.js
CHANGED
|
@@ -742,18 +742,36 @@ class Request {
|
|
|
742
742
|
if (!this.message || typeof this.message.text !== 'string') {
|
|
743
743
|
return [];
|
|
744
744
|
}
|
|
745
|
-
const { text, alternatives = [] } = this.message;
|
|
745
|
+
const { text: messageText, alternatives = [] } = this.message;
|
|
746
|
+
|
|
747
|
+
const unique = new Set();
|
|
748
|
+
let max = 0;
|
|
749
|
+
|
|
750
|
+
const sorted = alternatives
|
|
751
|
+
.slice()
|
|
752
|
+
.map(({ text, score = 1 }) => ({ text, score }))
|
|
753
|
+
.sort(({ score: a }, { score: z }) => z - a)
|
|
754
|
+
.filter((a) => {
|
|
755
|
+
const norm = tokenize(a.text);
|
|
756
|
+
if (unique.has(norm) || !a.text) {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
max = Math.max(max, a.score);
|
|
760
|
+
unique.add(norm);
|
|
761
|
+
return true;
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const normalized = tokenize(messageText);
|
|
746
765
|
|
|
747
|
-
if (
|
|
748
|
-
return
|
|
766
|
+
if (unique.has(normalized)) {
|
|
767
|
+
return sorted;
|
|
749
768
|
}
|
|
750
769
|
|
|
770
|
+
max = max ? Math.min(1, max + 0.1) : 1;
|
|
771
|
+
|
|
751
772
|
return [
|
|
752
|
-
{
|
|
753
|
-
|
|
754
|
-
score: 1
|
|
755
|
-
},
|
|
756
|
-
...alternatives
|
|
773
|
+
{ text: messageText, score: max },
|
|
774
|
+
...sorted
|
|
757
775
|
];
|
|
758
776
|
}
|
|
759
777
|
|
|
@@ -1268,7 +1286,9 @@ class Request {
|
|
|
1268
1286
|
if (this._quickReplyActions === null) {
|
|
1269
1287
|
if (this.state._expectedKeywords) {
|
|
1270
1288
|
this._quickReplyActions = quickReplyAction(
|
|
1271
|
-
this.state._expectedKeywords,
|
|
1289
|
+
this.state._expectedKeywords,
|
|
1290
|
+
this,
|
|
1291
|
+
Ai.ai
|
|
1272
1292
|
);
|
|
1273
1293
|
} else {
|
|
1274
1294
|
this._quickReplyActions = [];
|
|
@@ -1411,7 +1431,8 @@ It looks like the bot isn't connected to class BotApp or the Processor is used w
|
|
|
1411
1431
|
data = {},
|
|
1412
1432
|
refAction = null,
|
|
1413
1433
|
refData = {},
|
|
1414
|
-
timestamp = makeTimestamp()
|
|
1434
|
+
timestamp = makeTimestamp(),
|
|
1435
|
+
features = [FEATURE_TEXT]
|
|
1415
1436
|
) {
|
|
1416
1437
|
const postback = {
|
|
1417
1438
|
payload: {
|
|
@@ -1429,7 +1450,8 @@ It looks like the bot isn't connected to class BotApp or the Processor is used w
|
|
|
1429
1450
|
sender: {
|
|
1430
1451
|
id: senderId
|
|
1431
1452
|
},
|
|
1432
|
-
postback
|
|
1453
|
+
postback,
|
|
1454
|
+
features
|
|
1433
1455
|
};
|
|
1434
1456
|
}
|
|
1435
1457
|
|
|
@@ -1725,4 +1747,24 @@ It looks like the bot isn't connected to class BotApp or the Processor is used w
|
|
|
1725
1747
|
|
|
1726
1748
|
}
|
|
1727
1749
|
|
|
1750
|
+
/**
|
|
1751
|
+
* @constant {string} FEATURE_VOICE channel supports voice messages
|
|
1752
|
+
*/
|
|
1753
|
+
Request.FEATURE_VOICE = FEATURE_VOICE;
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* @constant {string} FEATURE_SSML channel supports SSML voice messages
|
|
1757
|
+
*/
|
|
1758
|
+
Request.FEATURE_SSML = FEATURE_SSML;
|
|
1759
|
+
|
|
1760
|
+
/**
|
|
1761
|
+
* @constant {string} FEATURE_PHRASES channel supports expected phrases messages
|
|
1762
|
+
*/
|
|
1763
|
+
Request.FEATURE_PHRASES = FEATURE_PHRASES;
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* @constant {string} FEATURE_TEXT channel supports text communication
|
|
1767
|
+
*/
|
|
1768
|
+
Request.FEATURE_TEXT = FEATURE_TEXT;
|
|
1769
|
+
|
|
1728
1770
|
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
|
|
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
|
}
|
package/src/graphApi/schema.gql
CHANGED
|
@@ -103,6 +103,28 @@ const DEFAULT_CAMPAIGN_DATA = {
|
|
|
103
103
|
* @prop {number} slideRound
|
|
104
104
|
*/
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* @typedef {object} Subscription
|
|
108
|
+
* @prop {string} senderId
|
|
109
|
+
* @prop {string} pageId
|
|
110
|
+
* @prop {string} [meta]
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @callback PreprocessSubscriptions
|
|
115
|
+
* @param {Subscription[]} subscriptions
|
|
116
|
+
* @param {string} pageId
|
|
117
|
+
* @returns {Promise<Subscription[]>|Subscription[]}
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @callback PreprocessSubscribers
|
|
122
|
+
* @param {string[]} senderIds
|
|
123
|
+
* @param {string} pageId
|
|
124
|
+
* @param {string} tag
|
|
125
|
+
* @returns {Promise<string[]>|string[]} list of senderIds
|
|
126
|
+
*/
|
|
127
|
+
|
|
106
128
|
/**
|
|
107
129
|
* Experimental notifications service
|
|
108
130
|
*
|
|
@@ -121,7 +143,8 @@ class Notifications extends EventEmitter {
|
|
|
121
143
|
* @param {console} [options.log] - logger
|
|
122
144
|
* @param {number} [options.default24Clearance] - use this clearance to ensure delivery in 24h
|
|
123
145
|
* @param {string} [options.allAudienceTag] - tag to mark all users
|
|
124
|
-
* @param {
|
|
146
|
+
* @param {PreprocessSubscribers} [options.preprocessSubscribers] - preprocess senderIds import
|
|
147
|
+
* @param {PreprocessSubscriptions} [options.preprocessSubscriptions]
|
|
125
148
|
*/
|
|
126
149
|
constructor (notificationStorage = new NotificationsStorage(), options = {}) {
|
|
127
150
|
super();
|
|
@@ -138,6 +161,7 @@ class Notifications extends EventEmitter {
|
|
|
138
161
|
this._lts = new Map();
|
|
139
162
|
|
|
140
163
|
this._preprocessSubscribers = options.preprocessSubscribers;
|
|
164
|
+
this._preprocessSubscriptions = options.preprocessSubscriptions;
|
|
141
165
|
}
|
|
142
166
|
|
|
143
167
|
/**
|
|
@@ -150,7 +174,8 @@ class Notifications extends EventEmitter {
|
|
|
150
174
|
*/
|
|
151
175
|
api (acl = null) {
|
|
152
176
|
const options = {
|
|
153
|
-
preprocessSubscribers: this._preprocessSubscribers
|
|
177
|
+
preprocessSubscribers: this._preprocessSubscribers,
|
|
178
|
+
preprocessSubscriptions: this._preprocessSubscriptions
|
|
154
179
|
};
|
|
155
180
|
return api(this._storage, this, acl, options);
|
|
156
181
|
}
|
|
@@ -344,7 +369,10 @@ class Notifications extends EventEmitter {
|
|
|
344
369
|
|
|
345
370
|
await Promise.all(tasks
|
|
346
371
|
.map((task) => this._messageDeliveryByMid(
|
|
347
|
-
task.campaignId,
|
|
372
|
+
task.campaignId,
|
|
373
|
+
eventType,
|
|
374
|
+
senderId,
|
|
375
|
+
pageId
|
|
348
376
|
)));
|
|
349
377
|
|
|
350
378
|
return {
|
|
@@ -3,9 +3,41 @@
|
|
|
3
3
|
*/
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
-
const { infoToProjection } = require('graphql-mongodb-projection');
|
|
7
6
|
const apiAuthorizer = require('../../graphApi/apiAuthorizer');
|
|
8
7
|
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @param {*} info
|
|
11
|
+
* @returns {object}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function getFields (info) {
|
|
15
|
+
if (info.fieldNodes) {
|
|
16
|
+
const fieldNodes = info.fieldNodes.filter((k) => k.kind === 'Field');
|
|
17
|
+
|
|
18
|
+
if (fieldNodes.length !== 1) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return getFields(fieldNodes[0]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const nodes = info.selectionSet ? info.selectionSet.selections : info.fieldNodes;
|
|
26
|
+
|
|
27
|
+
if (!info.selectionSet) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return nodes.reduce((o, fieldInfo) => {
|
|
32
|
+
if (fieldInfo.kind !== 'Field' || !fieldInfo.name || !fieldInfo.name.value) {
|
|
33
|
+
return o;
|
|
34
|
+
}
|
|
35
|
+
return Object.assign(o, {
|
|
36
|
+
[fieldInfo.name.value]: getFields(fieldInfo)
|
|
37
|
+
});
|
|
38
|
+
}, {});
|
|
39
|
+
}
|
|
40
|
+
|
|
9
41
|
function notificationsApiFactory (storage, notifications, acl, options = {}) {
|
|
10
42
|
return {
|
|
11
43
|
async campaigns (args, ctx) {
|
|
@@ -87,9 +119,8 @@ function notificationsApiFactory (storage, notifications, acl, options = {}) {
|
|
|
87
119
|
};
|
|
88
120
|
}
|
|
89
121
|
|
|
90
|
-
const data = await storage
|
|
91
|
-
campaignId, sentWithoutReaction, pageId
|
|
92
|
-
);
|
|
122
|
+
const data = await storage
|
|
123
|
+
.getUnsuccessfulSubscribersByCampaign(campaignId, sentWithoutReaction, pageId);
|
|
93
124
|
|
|
94
125
|
return {
|
|
95
126
|
data,
|
|
@@ -115,7 +146,7 @@ function notificationsApiFactory (storage, notifications, acl, options = {}) {
|
|
|
115
146
|
return null;
|
|
116
147
|
}
|
|
117
148
|
|
|
118
|
-
const fields =
|
|
149
|
+
const fields = getFields(info);
|
|
119
150
|
|
|
120
151
|
const {
|
|
121
152
|
include,
|
|
@@ -132,7 +163,15 @@ function notificationsApiFactory (storage, notifications, acl, options = {}) {
|
|
|
132
163
|
}
|
|
133
164
|
|
|
134
165
|
if (fields.data) {
|
|
135
|
-
const res = storage
|
|
166
|
+
const res = await storage
|
|
167
|
+
.getSubscribtions(include, exclude, limit, pageId, lastKey);
|
|
168
|
+
|
|
169
|
+
if (fields.data.meta && typeof options.preprocessSubscriptions === 'function') {
|
|
170
|
+
res.data = await Promise.resolve(
|
|
171
|
+
options.preprocessSubscriptions(res.data, pageId)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
136
175
|
return Object.assign(res, { count });
|
|
137
176
|
}
|
|
138
177
|
|
|
@@ -159,8 +198,12 @@ function notificationsApiFactory (storage, notifications, acl, options = {}) {
|
|
|
159
198
|
);
|
|
160
199
|
}
|
|
161
200
|
|
|
162
|
-
|
|
163
|
-
|
|
201
|
+
// make it little bit faster
|
|
202
|
+
while (senderIds.length > 0) {
|
|
203
|
+
await Promise.all(
|
|
204
|
+
senderIds.splice(0, 5)
|
|
205
|
+
.map((senderId) => storage.subscribe(`${senderId}`, pageId, tag))
|
|
206
|
+
);
|
|
164
207
|
}
|
|
165
208
|
|
|
166
209
|
return true;
|
package/src/resolvers/media.js
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
const Router = require('../Router');
|
|
7
|
+
const getCondition = require('../utils/getCondition');
|
|
7
8
|
const { stateData, cachedTranslatedCompilator } = require('./utils');
|
|
8
9
|
|
|
9
|
-
function media (
|
|
10
|
+
function media (
|
|
11
|
+
params,
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
{ isLastIndex } = {}
|
|
14
|
+
) {
|
|
10
15
|
const { type, url } = params;
|
|
11
16
|
|
|
12
17
|
const urlString = url || '';
|
|
@@ -19,7 +24,13 @@ function media (params, { isLastIndex }) {
|
|
|
19
24
|
throw new Error(`Unsupported media type: ${type}`);
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
const condition = getCondition(params, 'Media condition');
|
|
28
|
+
|
|
22
29
|
return (req, res) => {
|
|
30
|
+
if (condition && !condition(req, res)) {
|
|
31
|
+
return ret;
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
const data = stateData(req, res);
|
|
24
35
|
const sendUrl = urlTemplate(data);
|
|
25
36
|
|
package/src/resolvers/message.js
CHANGED
|
@@ -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 {
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/resolvers/utils.js
CHANGED
|
@@ -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
|
|
|
@@ -119,18 +202,21 @@ function getText (text, state) {
|
|
|
119
202
|
return renderer(state);
|
|
120
203
|
}
|
|
121
204
|
|
|
205
|
+
// eslint-disable-next-line no-unused-vars
|
|
206
|
+
const DEFAULT_LINK_TRANSLATOR = (senderId, defaultText, urlText, isExtUrl, reqState) => urlText;
|
|
207
|
+
|
|
122
208
|
function processButtons (
|
|
123
209
|
buttons,
|
|
124
210
|
state,
|
|
125
211
|
elem,
|
|
126
212
|
linksMap,
|
|
127
213
|
senderId,
|
|
128
|
-
|
|
129
|
-
linksTranslator = (sndr, defaultText, urlText, isExtUrl, reqState) => urlText,
|
|
214
|
+
linksTranslator,
|
|
130
215
|
allowForbiddenSnippetWords,
|
|
131
216
|
req,
|
|
132
217
|
res
|
|
133
218
|
) {
|
|
219
|
+
const translateLinks = linksTranslator || DEFAULT_LINK_TRANSLATOR;
|
|
134
220
|
|
|
135
221
|
buttons.forEach(({
|
|
136
222
|
title: btnTitle,
|
|
@@ -168,7 +254,7 @@ function processButtons (
|
|
|
168
254
|
case TYPE_URL_WITH_EXT: {
|
|
169
255
|
const hasExtention = type === TYPE_URL_WITH_EXT;
|
|
170
256
|
let urlText = getText(url, state);
|
|
171
|
-
urlText =
|
|
257
|
+
urlText = translateLinks(senderId, defaultText, urlText, isExtUrl, state);
|
|
172
258
|
elem.urlButton(btnTitleText, urlText, hasExtention, webviewHeight);
|
|
173
259
|
break;
|
|
174
260
|
}
|
|
@@ -194,7 +280,9 @@ function processButtons (
|
|
|
194
280
|
|
|
195
281
|
module.exports = {
|
|
196
282
|
getLanguageText,
|
|
283
|
+
getLanguageTextObjects,
|
|
197
284
|
cachedTranslatedCompilator,
|
|
285
|
+
randomizedCompiler,
|
|
198
286
|
getText,
|
|
199
287
|
stateData,
|
|
200
288
|
|
|
@@ -22,7 +22,7 @@ class MemoryBotConfigStorage {
|
|
|
22
22
|
* @param {Function|string[]} [acl] - acl configuration
|
|
23
23
|
* @returns {{updateBot:Function}}
|
|
24
24
|
*/
|
|
25
|
-
api (onUpdate = () => Promise.resolve(), acl) {
|
|
25
|
+
api (onUpdate = () => Promise.resolve(), acl = []) {
|
|
26
26
|
const storage = this;
|
|
27
27
|
return {
|
|
28
28
|
async updateBot (args, ctx) {
|
|
@@ -42,7 +42,7 @@ const isSimpleWord = (string) => {
|
|
|
42
42
|
const isStringNumber = (string) => {
|
|
43
43
|
if (typeof string !== 'string') return false;
|
|
44
44
|
if (string.length === 0) return false;
|
|
45
|
-
return
|
|
45
|
+
return !!/^([0-9]*[, ]{0,1})*[., ]{0,1}[0-9]*$/
|
|
46
46
|
.test((string).replace(/\s+/g, ''));
|
|
47
47
|
};
|
|
48
48
|
|
|
@@ -55,7 +55,7 @@ const stringToNumber = (string) => {
|
|
|
55
55
|
if (typeof string !== 'string') return string;
|
|
56
56
|
if (!isStringNumber(string)) throw new Error('String not a number');
|
|
57
57
|
let trimmed;
|
|
58
|
-
if (
|
|
58
|
+
if (/^[^,.]+,[^,.]+$/.test(string)) {
|
|
59
59
|
trimmed = string.replace(',', '.');
|
|
60
60
|
} else {
|
|
61
61
|
trimmed = string.replace(/(\s|,)+/g, '').replace(',', '.');
|
package/src/utils/getUpdate.js
CHANGED
|
@@ -88,8 +88,10 @@ 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
|
|
92
|
-
|
|
91
|
+
function getSetState (setState, req, res = null, useState = null) {
|
|
92
|
+
if (!setState) {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
93
95
|
const keys = Object.keys(setState)
|
|
94
96
|
.filter((k) => k !== '_');
|
|
95
97
|
|
|
@@ -32,7 +32,12 @@ const { checkSetState } = require('./stateVariables');
|
|
|
32
32
|
* @returns {ExpectedKeyword}
|
|
33
33
|
*/
|
|
34
34
|
function makeExpectedKeyword (
|
|
35
|
-
action,
|
|
35
|
+
action,
|
|
36
|
+
title,
|
|
37
|
+
matcher = null,
|
|
38
|
+
payloadData = {},
|
|
39
|
+
setState = null,
|
|
40
|
+
aiTitle = null
|
|
36
41
|
) {
|
|
37
42
|
let match = null;
|
|
38
43
|
|
|
@@ -226,7 +231,12 @@ function makeQuickReplies (replies, path = '', translate = (w) => w, quickReplyC
|
|
|
226
231
|
const translatedTitle = translate(title);
|
|
227
232
|
const translatedAiTitle = typeof aiTitle === 'string' ? translate(aiTitle) : aiTitle;
|
|
228
233
|
const expect = makeExpectedKeyword(
|
|
229
|
-
absoluteAction,
|
|
234
|
+
absoluteAction,
|
|
235
|
+
translatedTitle,
|
|
236
|
+
match,
|
|
237
|
+
data,
|
|
238
|
+
setState,
|
|
239
|
+
translatedAiTitle
|
|
230
240
|
);
|
|
231
241
|
expectedKeywords.push(expect);
|
|
232
242
|
|