chat-platform 1.2.3

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.
@@ -0,0 +1,1276 @@
1
+ const _ = require('underscore');
2
+ const _s = require('underscore.string');
3
+ const clc = require('cli-color');
4
+ const prettyjson = require('prettyjson');
5
+ const { isEmpty, when } = require('./lib/utils');
6
+ const EventEmitter = require('events').EventEmitter;
7
+ const inherits = require('util').inherits;
8
+ const lcd = require('./helpers/lcd');
9
+ const Table = require('cli-table');
10
+ const request = require('request').defaults({ encoding: null });
11
+
12
+ const identity = function(obj) { return obj; };
13
+ const green = clc.greenBright;
14
+ const white = clc.white;
15
+ const yellow = clc.yellow;
16
+ const red = clc.red;
17
+ const orange = clc.xterm(214);
18
+ const grey = clc.blackBright;
19
+
20
+ if (global['redbot-chat-platform'] == null) {
21
+ global['redbot-chat-platform'] = {
22
+ messageTypes: [],
23
+ events: [],
24
+ platforms: {},
25
+ params: {}
26
+ };
27
+ }
28
+
29
+ let _messageTypes = global['redbot-chat-platform'].messageTypes;
30
+ let _events = global['redbot-chat-platform'].events;
31
+ let _platforms = global['redbot-chat-platform'].platforms;
32
+ let _params = global['redbot-chat-platform'].params;
33
+ let _globalCallbacks = {};
34
+
35
+ const ChatExpress = function(options) {
36
+
37
+ const _this = this;
38
+ this.options = _.extend({
39
+ color: null,
40
+ contextProvider: null,
41
+ connector: null,
42
+ inboundMessage: null,
43
+ transport: null,
44
+ transportDescription: null,
45
+ chatIdKey: null,
46
+ userIdKey: null,
47
+ tsKey: null,
48
+ debug: true,
49
+ onStart: null,
50
+ onStop: null,
51
+ RED: null,
52
+ routes: null,
53
+ routesDescription: null,
54
+ events: null,
55
+ relaxChatId: false,
56
+ bundle: false,
57
+ multiWebHook: true
58
+ }, options);
59
+
60
+ this.ins = [];
61
+ this.outs = [];
62
+ this.uses = [];
63
+
64
+ // init platforms
65
+ _platforms[this.options.transport] = {
66
+ id: this.options.transport,
67
+ name: this.options.transportDescription,
68
+ universal: false,
69
+ color: this.options.color != null ? this.options.color : '#bbbbbb'
70
+ };
71
+
72
+ // configuration warnings
73
+ if (_.isEmpty(this.options.chatIdKey) && !_.isFunction(this.options.chatIdKey)) {
74
+ // eslint-disable-next-line no-console
75
+ console.log(yellow('WARNING: chatIdKey option is empty'));
76
+ }
77
+ if (_.isEmpty(this.options.userIdKey) && !_.isFunction(this.options.userIdKey)) {
78
+ // eslint-disable-next-line no-console
79
+ console.log(yellow('WARNING: userIdKey option is empty'));
80
+ }
81
+ if (_.isEmpty(this.options.transport)) {
82
+ // eslint-disable-next-line no-console
83
+ console.log(yellow('WARNING: transport option is empty'));
84
+ }
85
+
86
+ function evaluateParam(payload, newKey, optionKey, chatServer) {
87
+ var options = _this.options;
88
+ if (options[optionKey] != null) {
89
+ if (_.isString(options[optionKey]) && newKey != options[optionKey]) {
90
+ payload[newKey] = payload[options[optionKey]];
91
+ delete payload[options[optionKey]];
92
+ } else if (_.isFunction(options[optionKey])) {
93
+ payload[newKey] = options[optionKey].call(chatServer, payload);
94
+ }
95
+ }
96
+ }
97
+
98
+ function parseMessage(payload, options, chatServer) {
99
+ var instanceOptions = chatServer.getOptions();
100
+
101
+ payload = _.clone(payload);
102
+ // sets inbound
103
+ payload.inbound = true;
104
+ // sets the transport
105
+ if (!_.isEmpty(_this.options.transport)) {
106
+ payload.transport = _this.options.transport;
107
+ }if (!_.isEmpty(instanceOptions.transport)) {
108
+ // use the new registered transport platform if any
109
+ payload.transport = instanceOptions.transport;
110
+ }
111
+
112
+ evaluateParam(payload, 'chatId', 'chatIdKey', chatServer);
113
+ evaluateParam(payload, 'userId', 'userIdKey', chatServer);
114
+ evaluateParam(payload, 'ts', 'tsKey', chatServer);
115
+ evaluateParam(payload, 'type', 'type', chatServer);
116
+ evaluateParam(payload, 'language', 'language', chatServer);
117
+ // default userId on chatId, never leave it blank, pay attention
118
+ // on removing this constraint (some chat context may be lost)
119
+ payload.userId = !_.isEmpty(payload.userId) ? payload.userId : payload.chatId;
120
+ // evaluate callbacks
121
+ var callbacks = chatServer.getCallbacks();
122
+ _(['chatId', 'userId', 'ts', 'type', 'language', 'messageId']).each(function(callbackName) {
123
+ if (_.isFunction(callbacks[callbackName])) {
124
+ payload[callbackName] = callbacks[callbackName].call(chatServer, payload)
125
+ }
126
+ });
127
+ // at this point should have at least the values chatId and type
128
+ if (payload.chatId == null && !options.relaxChatId) {
129
+ throw 'Error: inbound message key "chatId" for transport ' + _this.options.transport + ' is empty\n\n'
130
+ + 'See here: https://github.com/guidone/node-red-contrib-chatbot/wiki/Universal-Connector-node for an '
131
+ + 'explanation about this error.';
132
+ }
133
+
134
+ return payload;
135
+ }
136
+
137
+ function warningInboundMiddleware(message) {
138
+ // check if message is null, perhaps someone forgot to resolve a promise
139
+ if (message == null) {
140
+ // eslint-disable-next-line no-console
141
+ console.log(yellow('WARNING: a middleware is returning an empty message'));
142
+ }
143
+ if (message.payload == null) {
144
+ // eslint-disable-next-line no-console
145
+ console.log(yellow('WARNING: a middleware is returning an empty payload in message'));
146
+ }
147
+ }
148
+
149
+ function prepareForConsole(payload) {
150
+ var result = _.clone(payload) || {};
151
+ if (result.content instanceof Buffer) {
152
+ result.content = '<Buffer>';
153
+ }
154
+ if (result.ts != null) {
155
+ result.ts = result.ts.toString();
156
+ }
157
+ return result;
158
+ }
159
+
160
+ // eslint-disable-next-line max-params
161
+ async function createMessage(chatId, userId, messageId, inboudMessage, chatServer) {
162
+ const options = chatServer.getOptions();
163
+ const contextProvider = options.contextProvider;
164
+ const onCreateMessage = _.isFunction(options.onCreateMessage) ? options.onCreateMessage : identity;
165
+ inboudMessage = inboudMessage || {};
166
+
167
+ const chatContext = await when(contextProvider.getOrCreate(chatId, userId, {
168
+ chatId: chatId,
169
+ userId: userId,
170
+ transport: options.transport,
171
+ }));
172
+ await chatContext.set({
173
+ authorized: false,
174
+ pending: false,
175
+ language: null
176
+ });
177
+ const message = _.extend({}, inboudMessage, {
178
+ originalMessage: {
179
+ chatId: chatId,
180
+ userId: userId,
181
+ messageId: messageId,
182
+ transport: options.transport,
183
+ language: null
184
+ },
185
+ chat() {
186
+ return contextProvider.get(
187
+ chatId,
188
+ userId,
189
+ {
190
+ userId: this.originalMessage.userId,
191
+ transport: this.originalMessage.transport,
192
+ chatId: this.originalMessage.chatId
193
+ }
194
+ );
195
+ },
196
+ api() {
197
+ return chatServer;
198
+ },
199
+ isTransportAvailable(transport, message) {
200
+ return chatServer.isTransportAvailable(userId, transport, message);
201
+ },
202
+ isTransportPreferred(transport, message) {
203
+ return chatServer.isTransportPreferred(userId, transport, message);
204
+ },
205
+ client() {
206
+ return options.connector;
207
+ },
208
+ get(value) {
209
+ return this.originalMessage[value];
210
+ }
211
+ });
212
+ return onCreateMessage.call(chatServer, message);
213
+ }
214
+
215
+ function inboundMessage(payload, chatServer) {
216
+ var contextProvider = chatServer.getOptions().contextProvider;
217
+ if (chatServer.isDebug()) {
218
+ // eslint-disable-next-line no-console
219
+ console.log(orange('-- INBOUND MESSAGE --'));
220
+ try {
221
+ // eslint-disable-next-line no-console
222
+ console.log(prettyjson.render(payload));
223
+ } catch (e) {
224
+ // eslint-disable-next-line no-console
225
+ console.log('PrettyJSON error');
226
+ }
227
+ // eslint-disable-next-line no-console
228
+ console.log('');
229
+ }
230
+ // parse the message to extract the minimum payload needed for chat-platform to work properly
231
+ // could raise errors, relay to chat server
232
+ try {
233
+ var parsedMessage = parseMessage(payload, _this.options, chatServer);
234
+ } catch(e) {
235
+ chatServer.emit('error', e);
236
+ return;
237
+ }
238
+ // create the node red message structure
239
+ const message = {
240
+ originalMessage: _.extend({}, payload, {
241
+ chatId: parsedMessage.chatId,
242
+ userId: parsedMessage.userId,
243
+ messageId: parsedMessage.messageId,
244
+ transport: parsedMessage.transport,
245
+ language: parsedMessage.language,
246
+ ts: parsedMessage.ts
247
+ }),
248
+ payload: {
249
+ type: parsedMessage.type,
250
+ chatId: parsedMessage.chatId,
251
+ userId: parsedMessage.userId,
252
+ ts: parsedMessage.ts,
253
+ transport: parsedMessage.transport,
254
+ inbound: true
255
+ },
256
+ chat: function() {
257
+ return contextProvider.get(
258
+ parsedMessage.chatId,
259
+ parsedMessage.userId,
260
+ { userId: this.originalMessage.userId, transport: this.originalMessage.transport, chatId: this.originalMessage.chatId }
261
+ );
262
+ },
263
+ api: function() {
264
+ return chatServer;
265
+ },
266
+ client: function() {
267
+ return chatServer.getOptions().connector;
268
+ },
269
+ get(value) {
270
+ return this.originalMessage[value];
271
+ }
272
+ };
273
+ // create empty promise
274
+ let stack = new Promise(function(resolve) {
275
+ resolve(message);
276
+ });
277
+ // if any context provider, then create the context
278
+ if (contextProvider != null) {
279
+ stack = stack
280
+ .then(() => when(contextProvider.getOrCreate(
281
+ parsedMessage.chatId,
282
+ parsedMessage.userId,
283
+ { userId: parsedMessage.userId, transport: parsedMessage.transport, chatId: parsedMessage.chatId }
284
+ )))
285
+ .then(chatContext => when(chatContext.set({ language: parsedMessage.language, authorized: false, pending: false })))
286
+ .then(function() {
287
+ return when(message);
288
+ });
289
+ } else {
290
+ // eslint-disable-next-line no-console
291
+ console.log(yellow('WARNING: context provider was not specified'));
292
+ }
293
+ // run general middleware
294
+ _(_this.uses.concat(chatServer.getUseMiddleWares())).each(function(filter) {
295
+ stack = stack.then(function(message) {
296
+ // encapsulate the promise to catch the error with the source code of the middleware
297
+ return new Promise(function(resolve, reject) {
298
+ warningInboundMiddleware(message);
299
+ when(filter.call(chatServer, message))
300
+ .then(resolve)
301
+ .catch(function (error) {
302
+ error.sourceCode = filter.toString();
303
+ reject(error);
304
+ });
305
+ });
306
+ });
307
+ });
308
+ // run ins middleware without any specific type
309
+ _(_this.ins.concat(chatServer.getInMiddleWares())).each(function(filter) {
310
+ stack = stack.then(function(message) {
311
+ // encapsulate the promise to catch the error with the source code of the middleware
312
+ return new Promise(function(resolve, reject) {
313
+ warningInboundMiddleware(message);
314
+ // if message type is null
315
+ if (filter.type == null) {
316
+ when(filter.method.call(chatServer, message))
317
+ .then(resolve)
318
+ .catch(function(error) {
319
+ error.sourceCode = filter.method.toString();
320
+ reject(error);
321
+ });
322
+ } else {
323
+ resolve(message)
324
+ }
325
+ });
326
+ });
327
+ });
328
+ // run ins middleware without a specific type
329
+ _(_this.ins).each(function(filter) {
330
+ stack = stack.then(function(message) {
331
+ // encapsulate the promise to catch the error with the source code of the middleware
332
+ return new Promise(function (resolve, reject) {
333
+ warningInboundMiddleware(message);
334
+ // if message type is the same
335
+ if (filter.type === message.payload.type || filter.type === '*') {
336
+ when(filter.method.call(chatServer, message))
337
+ .then(resolve)
338
+ .catch(function(error) {
339
+ error.sourceCode = filter.method.toString();
340
+ reject(error);
341
+ });
342
+ } else {
343
+ resolve(message)
344
+ }
345
+ });
346
+ });
347
+ });
348
+ // finally
349
+ stack
350
+ .then(function(message) {
351
+ if (message.payload != null && message.payload.type != null) {
352
+ if (chatServer.isDebug()) {
353
+ // eslint-disable-next-line no-console
354
+ console.log(orange('-- RELAY MESSAGE --'));
355
+ try {
356
+ // eslint-disable-next-line no-console
357
+ console.log(prettyjson.render(prepareForConsole(message.payload)));
358
+ } catch(e) {
359
+ // eslint-disable-next-line no-console
360
+ console.log('Unable to render');
361
+ }
362
+ // eslint-disable-next-line no-console
363
+ console.log('');
364
+ }
365
+ chatServer.emit('message', message);
366
+ } else {
367
+ // do nothing
368
+ if (chatServer.isDebug()) {
369
+ // eslint-disable-next-line no-console
370
+ console.log(orange('-- DISCARDED MESSAGE (not handled by middlewares) --'));
371
+ // eslint-disable-next-line no-console
372
+ console.log('');
373
+ }
374
+ }
375
+
376
+ })
377
+ .catch(function(error) {
378
+ lcd.dump(error, 'Error in chat-platform.js');
379
+ // dump source code if present
380
+ if (error != null && !_.isEmpty(error.sourceCode)) {
381
+ // eslint-disable-next-line no-console
382
+ console.log(lcd.red(error.sourceCode));
383
+ // eslint-disable-next-line no-console
384
+ console.log('');
385
+ }
386
+ if (chatServer != null) {
387
+ chatServer.emit('error', error);
388
+ }
389
+ });
390
+ }
391
+
392
+ function warningOutboundMiddleware(message) {
393
+ if (message == null) {
394
+ // eslint-disable-next-line no-console
395
+ console.log(yellow('WARNING: a middleware is returning an empty value'));
396
+ }
397
+ }
398
+
399
+ function outboundMessage(message, chatServer) {
400
+ // if simulator message, then skip, no matter what is the platform
401
+ if (message.originalMessage != null && message.originalMessage.simulator === true) {
402
+ return;
403
+ }
404
+
405
+ const instanceOptions = chatServer.getOptions();
406
+ // check if the message is from the right platform (in static class or instance)
407
+ /*if (message.originalMessage != null && message.originalMessage.transport !== _this.options.transport &&
408
+ message.originalMessage.transport !== instanceOptions.transport) {
409
+ // exit, it's not from the current platform
410
+ if (chatServer.isDebug()) {
411
+ // eslint-disable-next-line no-console
412
+ console.log(yellow('Skipped incoming message for platform: ' + message.originalMessage.transport));
413
+ }
414
+ return;
415
+ }*/
416
+
417
+ if (chatServer.isDebug()) {
418
+ // eslint-disable-next-line no-console
419
+ console.log(orange('-- OUTBOUND MESSAGE --'));
420
+ // eslint-disable-next-line no-console
421
+ console.log(prettyjson.render(prepareForConsole(message.payload)));
422
+ // eslint-disable-next-line no-console
423
+ console.log('');
424
+ }
425
+
426
+ // create empty promise
427
+ let stack = new Promise(function(resolve) {
428
+ resolve(message);
429
+ });
430
+ // check for chatId, if not present check for userId-chatId translator, otherwise fail
431
+ stack = stack.then(message => {
432
+ const { transport } = instanceOptions;
433
+ if (!isEmpty(message.payload.chatId)) {
434
+ // if there's a chatId, everything ok, skip
435
+ return message;
436
+ } else if (_.isFunction(_globalCallbacks.getChatIdFromUserId) && message.originalMessage.userId != null) {
437
+ // try to call the callback
438
+ try {
439
+ return when(_globalCallbacks.getChatIdFromUserId.call(chatServer, message.originalMessage.userId, transport, message))
440
+ .then(
441
+ chatId => {
442
+ if (isEmpty(chatId)) {
443
+ // eslint-disable-next-line no-console
444
+ console.log(lcd.error(`[onGetChatIdFromUserId] The userId<->chatId resolver was not able to find a valid chatId for user ${message.originalMessage.userId}(${transport})`));
445
+ throw new Error(`The userId<->chatId resolver was not able to find a valid chatId for user ${message.originalMessage.userId}(${transport})`);
446
+ } else {
447
+ if (instanceOptions.debug) {
448
+ // eslint-disable-next-line no-console
449
+ console.log(lcd.green('[onGetChatIdFromUserId]') + lcd.grey(` Resolved ${message.originalMessage.userId}(${transport}) in chatId:${chatId}`));
450
+ }
451
+ message.payload.chatId = chatId;
452
+ return message;
453
+ }
454
+ }
455
+ );
456
+ } catch(e) {
457
+ // eslint-disable-next-line no-console
458
+ console.log(lcd.error(`[onGetChatIdFromUserId] runtime error in chatId<->userId resolver for user ${message.originalMessage.userId}(${transport})`));
459
+ lcd.dump(e);
460
+ throw new Error(`[onGetChatIdFromUserId] runtime error in chatId<->userId resolver for user ${message.originalMessage.userId}(${transport})`);
461
+ }
462
+ } else {
463
+ // provide some feedback for not being able to resolve a chatId
464
+ if (instanceOptions.debug) {
465
+ if (_.isFunction(_globalCallbacks.getChatIdFromUserId) && isEmpty(message.originalMessage.userId)) {
466
+ // eslint-disable-next-line no-console
467
+ console.log(lcd.warn(`[onGetChatIdFromUserId] Callback was provided but incoming message has no userId (${transport}), unable to resolve a valid chatId`));
468
+ } else if (!_.isFunction(_globalCallbacks.getChatIdFromUserId)) {
469
+ // eslint-disable-next-line no-console
470
+ console.log(lcd.warn('[onGetChatIdFromUserId] Callback was NOT provided and chatId is empty'));
471
+ }
472
+ }
473
+ // raise error only if not relaxChatId
474
+ if (!instanceOptions.relaxChatId) {
475
+ throw new Error('Incoming message has empty chatId and no chatId - userId resolved has been provided');
476
+ }
477
+ }
478
+ return message;
479
+ });
480
+ // run general middleware
481
+ _(_this.uses.concat(chatServer.getUseMiddleWares())).each(function(filter) {
482
+ stack = stack.then(function(message) {
483
+ return new Promise(function(resolve, reject) {
484
+ warningOutboundMiddleware(message);
485
+ when(filter.call(chatServer, message))
486
+ .then(resolve)
487
+ .catch(function (error) {
488
+ error.sourceCode = filter.toString();
489
+ reject(error);
490
+ });
491
+ });
492
+ });
493
+ });
494
+ // run outs middleware without a specific typs
495
+ _(_this.outs.concat(chatServer.getOutMiddleWares())).each(function(filter) {
496
+ stack = stack.then(function(message) {
497
+ return new Promise(function(resolve, reject) {
498
+ // check if message is null, perhaps someone forgot to resolve a promise
499
+ warningOutboundMiddleware(message);
500
+ // if message type is the same
501
+ if (filter.type == null) {
502
+ when(filter.method.call(chatServer, message))
503
+ .then(resolve)
504
+ .catch(function (error) {
505
+ error.sourceCode = filter.toString();
506
+ reject(error);
507
+ });
508
+ } else {
509
+ resolve(message);
510
+ }
511
+ });
512
+ });
513
+ });
514
+ // run outs middleware with a specific typs
515
+ _(_this.outs.concat(chatServer.getOutMiddleWares())).each(function(filter) {
516
+ stack = stack.then(function(message) {
517
+ return new Promise(function(resolve, reject) {
518
+ // check if message is null, perhaps someone forgot to resolve a promise
519
+ warningOutboundMiddleware(message);
520
+ // if message type is the same
521
+ if (message.payload != null && filter.type === message.payload.type) {
522
+ when(filter.method.call(chatServer, message))
523
+ .then(resolve)
524
+ .catch(function (error) {
525
+ error.sourceCode = filter.toString();
526
+ reject(error);
527
+ });
528
+ } else {
529
+ resolve(message);
530
+ }
531
+ });
532
+ });
533
+ });
534
+ // finally
535
+ return stack
536
+ .catch(function(error) {
537
+ // eslint-disable-next-line no-console
538
+ console.log(red(error));
539
+ if (chatServer != null) {
540
+ chatServer.emit('error', error);
541
+ }
542
+ // rethrow error so it can be caught by the sender node
543
+ throw error;
544
+ })
545
+ .then(function(message) {
546
+ return message;
547
+ });
548
+ }
549
+
550
+ function unmountEvents(events, chatServer) {
551
+ var options = chatServer.getOptions();
552
+ var connector = options.connector;
553
+ if (connector != null) {
554
+ if (options.inboundMessageEvent != null) {
555
+ connector.off(options.inboundMessageEvent);
556
+ }
557
+ _(events).each(function (callback, eventName) {
558
+ connector.off(eventName);
559
+ });
560
+ }
561
+ }
562
+
563
+ function mountEvents(events, chatServer) {
564
+ var connector = chatServer.getOptions().connector;
565
+ if (connector != null && _.isFunction(connector.on)) {
566
+ _(events).each(function (callback, eventName) {
567
+ connector.on(eventName, callback.bind(chatServer));
568
+ });
569
+ }
570
+ return when(true);
571
+ }
572
+
573
+ function unmountRoutes(RED, routes, chatServer) {
574
+ if (routes != null) {
575
+ const endpoints = _(routes).keys();
576
+ if (!_.isEmpty(endpoints)) {
577
+ let routesCount = RED.httpNode._router.stack.length;
578
+ let idx = 0;
579
+ let stack = RED.httpNode._router.stack;
580
+ for(; idx < stack.length;) {
581
+ const route = stack[idx];
582
+ if (route != null && route.name != null) {
583
+ const routeName = String(route.name).replace('bound ', '');
584
+ if (_.contains(endpoints, routeName)) {
585
+ stack.splice(idx, 1);
586
+ } else {
587
+ idx += 1;
588
+ }
589
+ } else {
590
+ idx += 1;
591
+ }
592
+ }
593
+ if (RED.httpNode._router.stack.length >= routesCount) {
594
+ // eslint-disable-next-line no-console
595
+ chatServer.warning(`Improperly removed some routes from Express. This is normal when multiple bots are running in the same server.`);
596
+ }
597
+ }
598
+ }
599
+ }
600
+
601
+ function generateCallback(route, chatServer) {
602
+ const options = chatServer.getOptions();
603
+ const { webHookScheme, multiWebHook } = options;
604
+ // build specific url if needed
605
+ let specificUrl = '';
606
+ if (multiWebHook && _.isFunction(webHookScheme)) {
607
+ specificUrl = webHookScheme.call(chatServer);
608
+ }
609
+ return `${route}${!_.isEmpty(specificUrl) ? `/${specificUrl}` : ''}`;
610
+ }
611
+
612
+ // eslint-disable-next-line max-params
613
+ function mountRoutes(RED, routes, routesDescription, chatServer) {
614
+ if (routes != null && RED == null) {
615
+ chatServer.warn('"RED" param is empty, impossible to mount the routes');
616
+ }
617
+ if (routes != null && RED != null) {
618
+ const uiPort = RED.settings.get('uiPort');
619
+ const options = chatServer.getOptions();
620
+ // eslint-disable-next-line no-console
621
+ console.log('');
622
+ // eslint-disable-next-line no-console
623
+ console.log(grey('------ WebHooks for ' + options.transport.toUpperCase() + '----------------'));
624
+ _(routes).map((middleware, route) => {
625
+ const host = 'http://localhost' + (uiPort != '80' ? ':' + uiPort : '');
626
+ const callback = generateCallback(route, chatServer);
627
+ // make description
628
+ let description = null;
629
+ if (routesDescription != null && _.isString(routesDescription[route])) {
630
+ description = routesDescription[route];
631
+ } else if (routesDescription != null && _.isFunction(routesDescription[route])) {
632
+ description = routesDescription[route].call(chatServer);
633
+ }
634
+ // eslint-disable-next-line no-console
635
+ console.log(green(host + callback) + (description != null ? grey(' - ') + white(description) : ''));
636
+ // attach to Express instance
637
+ const escaped = String(`^${callback}$`).replace(RegExp('/', 'g'), '\\/');
638
+ RED.httpNode.use(new RegExp(escaped), middleware.bind(chatServer));
639
+ return null;
640
+ });
641
+ // eslint-disable-next-line no-console
642
+ console.log('');
643
+ }
644
+ return when(true);
645
+ }
646
+
647
+ var methods = {
648
+
649
+ 'in': function() {
650
+ var type = null;
651
+ var method = null;
652
+ if (arguments.length === 1) {
653
+ method = arguments[0];
654
+ } else if (arguments.length === 2) {
655
+ type = arguments[0];
656
+ method = arguments[1];
657
+ } else {
658
+ throw '.in() wrong number of parameters';
659
+ }
660
+
661
+ _this.ins.push({
662
+ type: type,
663
+ method: method
664
+ });
665
+ return methods;
666
+ },
667
+
668
+ out: function() {
669
+ var type = null;
670
+ var method = null;
671
+ if (arguments.length === 1) {
672
+ method = arguments[0];
673
+ } else if (arguments.length === 2) {
674
+ type = arguments[0];
675
+ method = arguments[1];
676
+ } else {
677
+ throw '.out() wrong number of parameters';
678
+ }
679
+ _this.outs.push({
680
+ type: type,
681
+ method: method
682
+ });
683
+ return methods;
684
+ },
685
+
686
+ use: function(method) {
687
+ _this.uses.push(method);
688
+ return methods;
689
+ },
690
+
691
+ mixin: function(obj) {
692
+ _this._mixins = _.extend(_this._mixin || {}, obj);
693
+ return methods;
694
+ },
695
+
696
+ registerMessageType: function(type, name, description, validator) {
697
+ if (type == null || typeof type !== 'string') {
698
+ throw 'Missing type in .registerMessageType()';
699
+ }
700
+ name = name != null ? name : _s.capitalize(type);
701
+ let typeDescriptor = _(_messageTypes).findWhere({ type: type });
702
+ if (typeDescriptor == null) {
703
+ typeDescriptor = { type: type };
704
+ _messageTypes.push(typeDescriptor);
705
+ }
706
+ if (name != null) {
707
+ typeDescriptor.name = name;
708
+ }
709
+ if (description != null) {
710
+ typeDescriptor.description = description;
711
+ }
712
+ if (typeDescriptor.platforms == null) {
713
+ typeDescriptor.platforms = {};
714
+ }
715
+ if (typeDescriptor.validators == null) {
716
+ typeDescriptor.validators = {};
717
+ }
718
+ typeDescriptor.platforms[options.transport] = true;
719
+ if (_.isFunction(validator)) {
720
+ typeDescriptor.validators[options.transport] = validator;
721
+ }
722
+ return this;
723
+ },
724
+ registerEvent: function(name, description) {
725
+ if (name == null || typeof name !== 'string') {
726
+ throw 'Missing name in .registerEvent()';
727
+ }
728
+ var eventDescriptor = _(_events).findWhere({ name: name });
729
+ if (eventDescriptor == null) {
730
+ eventDescriptor = { name: name };
731
+ _events.push(eventDescriptor);
732
+ }
733
+ eventDescriptor.name = name;
734
+ if (description != null) {
735
+ eventDescriptor.description = description;
736
+ }
737
+ if (eventDescriptor.platforms == null) {
738
+ eventDescriptor.platforms = {};
739
+ }
740
+ eventDescriptor.platforms[options.transport] = true;
741
+ return this;
742
+ },
743
+ registerParam: function(name, type, config = {}) {
744
+ if (name == null || typeof name !== 'string') {
745
+ throw 'Missing name in .registerParam()';
746
+ }
747
+ if (type == null || typeof type !== 'string') {
748
+ throw 'Missing type in .registerParam()';
749
+ }
750
+ if (_params[options.transport] == null) {
751
+ _params[options.transport] = [];
752
+ }
753
+ _params[options.transport].push({
754
+ name,
755
+ type,
756
+ placeholder: config.placeholder,
757
+ label: !_.isEmpty(config.label) ? config.label : name,
758
+ description: config.description,
759
+ default: config.default,
760
+ options: config.options
761
+ });
762
+ return this;
763
+ },
764
+
765
+ createServer: function(options) {
766
+
767
+ options = _.extend({}, _this.options, options);
768
+ var chatServer = null;
769
+ var _ins = [];
770
+ var _uses = [];
771
+ var _outs = [];
772
+ var _callbacks = {};
773
+
774
+ var ChatServer = function(options) {
775
+ this.options = options;
776
+ this.warn = function(msg) {
777
+ var text = '[' + options.transport.toUpperCase() + '] ' + msg;
778
+ // eslint-disable-next-line no-console
779
+ console.log(yellow(text));
780
+ this.emit('warning', text);
781
+ };
782
+ this.error = function(msg) {
783
+ var text = '[' + options.transport.toUpperCase() + '] ' + msg;
784
+ // eslint-disable-next-line no-console
785
+ console.log(red(text));
786
+ this.emit('error', text);
787
+ };
788
+ this.warning = function(msg) {
789
+ var text = '[' + options.transport.toUpperCase() + '] ' + msg;
790
+ // eslint-disable-next-line no-console
791
+ console.log(red(text));
792
+ this.emit('warning', text);
793
+ };
794
+ this.log = function(obj) {
795
+ // eslint-disable-next-line no-console
796
+ console.log(prettyjson.render(obj));
797
+ };
798
+ this.request = function(options = {}) {
799
+ return new Promise(function(resolve, reject) {
800
+ request(options, function(error, response, body) {
801
+ if (error) {
802
+ reject(`Error calling URL ${options.url}`);
803
+ } else {
804
+ resolve(body);
805
+ }
806
+ });
807
+ });
808
+ };
809
+ this.getOptions = function() {
810
+ return this.options;
811
+ };
812
+ this.isDebug = function() {
813
+ return this.options != null && this.options.debug;
814
+ };
815
+ this.getConnector = function() {
816
+ return this.options.connector;
817
+ };
818
+ this.send = function(message) {
819
+ var _this = this;
820
+ // If more than one message is enqued in the same payload, send one by one through the middlewares if there
821
+ // is not bundle option. Bundle option is used in case of multimodal messages (for example audio and video
822
+ // at the same time) that need to be sent with a unique call
823
+ if (options.bundle || !_.isArray(message.payload)) {
824
+ return outboundMessage(message, this);
825
+ } else {
826
+ var task = when(true);
827
+ _(message.payload).each(function(payload) {
828
+ task = task.then(function() {
829
+ return outboundMessage(_.extend({}, message, { payload: payload }), _this);
830
+ });
831
+ });
832
+ return task;
833
+ }
834
+ };
835
+ this.receive = function(message) {
836
+ inboundMessage(message, this);
837
+ };
838
+ // eslint-disable-next-line max-params
839
+ this.createMessage = function(chatId, userId, messageId, inboudMessage) {
840
+ return createMessage(chatId, userId, messageId, inboudMessage, this);
841
+ };
842
+ this.in = function() {
843
+ var type = null;
844
+ var method = null;
845
+ if (arguments.length === 1) {
846
+ method = arguments[0];
847
+ } else if (arguments.length === 2) {
848
+ type = arguments[0];
849
+ method = arguments[1];
850
+ } else {
851
+ throw '.in() wrong number of parameters';
852
+ }
853
+ _ins.push({
854
+ type: type,
855
+ method: method
856
+ });
857
+ return methods;
858
+ };
859
+ this.getInMiddleWares = function() {
860
+ return _ins;
861
+ };
862
+ this.use = function(method) {
863
+ _uses.push(method);
864
+ return methods;
865
+ };
866
+ this.getUseMiddleWares = function() {
867
+ return _uses;
868
+ };
869
+ this.registerMessageType = function(type, name, description, validator) {
870
+ if (type == null || typeof type !== 'string') {
871
+ throw 'Missing type in .registerMessageType()';
872
+ }
873
+ name = name != null ? name : _s.capitalize(type);
874
+ let typeDescriptor = _(_messageTypes).findWhere({ type: type });
875
+ if (typeDescriptor == null) {
876
+ typeDescriptor = { type: type };
877
+ _messageTypes.push(typeDescriptor);
878
+ }
879
+ if (name != null) {
880
+ typeDescriptor.name = name;
881
+ }
882
+ if (description != null) {
883
+ typeDescriptor.description = description;
884
+ }
885
+ if (typeDescriptor.platforms == null) {
886
+ typeDescriptor.platforms = {};
887
+ }
888
+ if (typeDescriptor.validators == null) {
889
+ typeDescriptor.validators = {};
890
+ }
891
+ typeDescriptor.platforms[options.transport] = true;
892
+ if (_.isFunction(validator)) {
893
+ typeDescriptor.validators[options.transport] = validator;
894
+ }
895
+ return this;
896
+ };
897
+ this.registerEvent = function(name, description) {
898
+ if (name == null || typeof name !== 'string') {
899
+ throw 'Missing name in .registerEvent()';
900
+ }
901
+ var eventDescriptor = _(_events).findWhere({ name: name });
902
+ if (eventDescriptor == null) {
903
+ eventDescriptor = { name: name };
904
+ _events.push(eventDescriptor);
905
+ }
906
+ eventDescriptor.name = name;
907
+ if (description != null) {
908
+ eventDescriptor.description = description;
909
+ }
910
+ if (eventDescriptor.platforms == null) {
911
+ eventDescriptor.platforms = {};
912
+ }
913
+ eventDescriptor.platforms[options.transport] = true;
914
+ return this;
915
+ };
916
+ this.registerParam = function(name, type, config = {}) {
917
+ if (name == null || typeof name !== 'string') {
918
+ throw 'Missing name in .registerParam()';
919
+ }
920
+ if (type == null || typeof type !== 'string') {
921
+ throw 'Missing type in .registerParam()';
922
+ }
923
+ if (_params[options.transport] == null) {
924
+ _params[options.transport] = [];
925
+ }
926
+ _params[options.transport].push({
927
+ name,
928
+ type,
929
+ placeholder: config.placeholder,
930
+ label: !_.isEmpty(config.label) ? config.label : name,
931
+ description: config.description,
932
+ default: config.default,
933
+ options: config.options
934
+ });
935
+ return this;
936
+ };
937
+ this.registerPlatform = function(name, label) {
938
+ _platforms[name] = {
939
+ id: name,
940
+ name: !_.isEmpty(label) ? label : name,
941
+ universal: true
942
+ };
943
+ var options = this.getOptions();
944
+ options.transport = name;
945
+ options.transportDescription = !_.isEmpty(label) ? label : name;
946
+ return this;
947
+ };
948
+ this.out = function() {
949
+ var type = null;
950
+ var method = null;
951
+ if (arguments.length === 1) {
952
+ method = arguments[0];
953
+ } else if (arguments.length === 2) {
954
+ type = arguments[0];
955
+ method = arguments[1];
956
+ // automatically register the type
957
+ this.registerMessageType(type);
958
+ } else {
959
+ throw '.out() wrong number of parameters';
960
+ }
961
+ _outs.push({
962
+ type: type,
963
+ method: method
964
+ });
965
+ return methods;
966
+ };
967
+ this.getOutMiddleWares = function() {
968
+ return _outs;
969
+ };
970
+ this.start = function() {
971
+ var _this = this;
972
+ var stack = when(true);
973
+ var options = this.getOptions();
974
+ if (_.isFunction(options.onStart)) {
975
+ // execute on start callback, ensure it's a properly chained promise
976
+ stack = stack.then(function() {
977
+ return when(options.onStart.call(chatServer));
978
+ });
979
+ }
980
+ return stack
981
+ .then(function() {
982
+ return mountRoutes(options.RED, options.routes, options.routesDescription, _this);
983
+ })
984
+ .then(function() {
985
+ return mountEvents(options.events, _this);
986
+ })
987
+ .then(function() {
988
+ return _.isFunction(options.onStarted) ? when(options.onStarted.call(_this)) : when(true);
989
+ })
990
+ .then(function() {
991
+ if (_this.isDebug()) {
992
+ // eslint-disable-next-line no-console
993
+ console.log(green('Chat server started, transport: ') + white(options.transport));
994
+ }
995
+ // listen to inbound event
996
+ var connector = options.connector;
997
+ if (connector != null && options.inboundMessageEvent != null) {
998
+ connector.on(options.inboundMessageEvent, function (message) {
999
+ inboundMessage(message, chatServer);
1000
+ });
1001
+ }
1002
+ _this.emit('start');
1003
+ },
1004
+ function(error) {
1005
+ // eslint-disable-next-line no-console
1006
+ _this.error(error);
1007
+ });
1008
+ };
1009
+ this.onChatId = function(callback) {
1010
+ _callbacks.chatId = callback;
1011
+ };
1012
+ this.onUserId = function(callback) {
1013
+ _callbacks.userId = callback;
1014
+ };
1015
+ this.onTimestamp = function(callback) {
1016
+ _callbacks.ts = callback;
1017
+ };
1018
+ this.onLanguage = function(callback) {
1019
+ _callbacks.language = callback;
1020
+ };
1021
+ this.onMessageId = function(callback) {
1022
+ _callbacks.messageId = callback;
1023
+ };
1024
+ this.onGetChatIdFromUserId = function(callback) {
1025
+ _globalCallbacks.getChatIdFromUserId = callback;
1026
+ };
1027
+ this.onGetPreferredTransport = function(callback) {
1028
+ _globalCallbacks.onGetPreferredTransport = callback;
1029
+ };
1030
+ this.isTransportAvailable = function(userId, transport, message) {
1031
+ if (!_.isFunction(_globalCallbacks.getChatIdFromUserId)) {
1032
+ throw new Error('Resolver chatId<->userId not defined');
1033
+ }
1034
+ try {
1035
+ return when(_globalCallbacks.getChatIdFromUserId.call(chatServer, userId, transport, message))
1036
+ .then(chatId => {
1037
+ return chatId != null
1038
+ });
1039
+ } catch(e) {
1040
+ // todo better error displaying
1041
+ // eslint-disable-next-line no-console
1042
+ console.log('Error in resolver chatId<->userId', e);
1043
+ throw new Error('Error in resolver chatId<->userId', e);
1044
+ }
1045
+ };
1046
+ this.isTransportPreferred = function(userId, message) {
1047
+ if (!_.isFunction(_globalCallbacks.onGetPreferredTransport)) {
1048
+ throw new Error('Resolver userId<->preferred transport not defined');
1049
+ }
1050
+ try {
1051
+ return when(_globalCallbacks.onGetPreferredTransport.call(chatServer, userId, message))
1052
+ .then(preferredTransport => {
1053
+ return preferredTransport === this.options.transport;
1054
+ });
1055
+ } catch(e) {
1056
+ // todo better error displaying
1057
+ // eslint-disable-next-line no-console
1058
+ console.log('Error in resolver chatId<->preferred transport', e);
1059
+ throw new Error('Error in resolver chatId<->preferred transport', e);
1060
+ }
1061
+ };
1062
+ this.onGetUserIdFromChatId = function(callback) {
1063
+ _callbacks.getUserIdFromChatId = callback;
1064
+ };
1065
+ this.getCallbacks = function() {
1066
+ return _callbacks;
1067
+ };
1068
+ this.stop = function() {
1069
+ this.emit('stop');
1070
+ var options = this.getOptions();
1071
+ unmountRoutes(options.RED, options.routes, this);
1072
+ unmountEvents(options.events, this);
1073
+ var stack = when(true);
1074
+ if (_.isFunction(options.onStop)) {
1075
+ stack = stack.then(function() {
1076
+ return when(options.onStop.call(chatServer));
1077
+ });
1078
+ }
1079
+ return stack;
1080
+ };
1081
+ EventEmitter.call(this);
1082
+ };
1083
+ inherits(ChatServer, EventEmitter);
1084
+ _.extend(ChatServer.prototype, _this._mixins);
1085
+ // create chat instance
1086
+ chatServer = new ChatServer(options);
1087
+ return chatServer;
1088
+ }
1089
+
1090
+ };
1091
+ return methods;
1092
+ };
1093
+
1094
+ /*
1095
+ Static methods for ChatExpress
1096
+ */
1097
+ ChatExpress.getMessageTypes = function() {
1098
+ return _(_messageTypes).map(function(item) {
1099
+ return {
1100
+ value: item.type,
1101
+ label: item.name,
1102
+ platforms: _(item.platforms).keys()
1103
+ };
1104
+ });
1105
+ };
1106
+ ChatExpress.getEvents = function() {
1107
+ return _(_events).map(function(item) {
1108
+ return {
1109
+ value: item.name,
1110
+ label: item.description,
1111
+ platforms: _(item.platforms).keys()
1112
+ };
1113
+ });
1114
+ };
1115
+ ChatExpress.getParams = function() {
1116
+ return _params;
1117
+ };
1118
+
1119
+ function compatibilityTable(items, options) {
1120
+ options = _.extend({ column: 'Column' }, options);
1121
+ // collect platforms except universal
1122
+ var platforms = _(ChatExpress.getPlatforms()).chain()
1123
+ .map(function(platform) {
1124
+ return platform.id;
1125
+ })
1126
+ .reject(function(id) {
1127
+ return id === 'universal';
1128
+ })
1129
+ .sort()
1130
+ .value();
1131
+ // build header
1132
+ var head = [options.column];
1133
+ _(platforms).each(function(name) {
1134
+ head.push(_s.capitalize(name));
1135
+ });
1136
+ var colAligns = ['left'];
1137
+ _.times(platforms.length, function() {
1138
+ colAligns.push('middle');
1139
+ });
1140
+ // create table
1141
+ var table = new Table({
1142
+ head: head,
1143
+ colAligns: colAligns,
1144
+ style: {
1145
+ head: ['green']
1146
+ }
1147
+ });
1148
+ // message types columns
1149
+ _(items).chain()
1150
+ .sortBy(function(type) {
1151
+ return type.name;
1152
+ })
1153
+ .each(function(type) {
1154
+ var row = [type.name];
1155
+ _(platforms).each(function(platform) {
1156
+ if (type.platforms[platform]) {
1157
+ row.push('✔');
1158
+ } else {
1159
+ row.push('');
1160
+ }
1161
+ });
1162
+ table.push(row);
1163
+ });
1164
+ // eslint-disable-next-line no-console
1165
+ console.log(table.toString());
1166
+
1167
+ }
1168
+
1169
+ /**
1170
+ * @method showCompatibilityChart
1171
+ * Print out a compatibility char for message types
1172
+ */
1173
+ ChatExpress.showCompatibilityChart = function() {
1174
+ compatibilityTable(_messageTypes, { column: 'Message type' });
1175
+ compatibilityTable(_events, { column: 'Event name' });
1176
+ };
1177
+
1178
+ /**
1179
+ * @method getPlatforms
1180
+ * Return an array of available platforms
1181
+ * @return {Array}
1182
+ */
1183
+ ChatExpress.getPlatforms = function() {
1184
+ var platforms = _(_platforms).keys();
1185
+
1186
+ return _(platforms).chain()
1187
+ .map(function(platform) {
1188
+ return {
1189
+ id: _platforms[platform].id,
1190
+ name: _platforms[platform].name,
1191
+ universal: _platforms[platform].universal,
1192
+ color: _platforms[platform].color
1193
+ };
1194
+ })
1195
+ .sortBy(function(platform) {
1196
+ return platform.name != null ? platform.name : platform.id;
1197
+ })
1198
+ .value();
1199
+ };
1200
+
1201
+ /**
1202
+ * @method isSupported
1203
+ * Check if a platform is supported (or a message type for a specified platform)
1204
+ * @param {String} platform Platform id (telegram, facebook, ...)
1205
+ * @param {String} type Message type (image, document, ...)
1206
+ * @return {Boolean}
1207
+ */
1208
+ ChatExpress.isSupported = function(platform, type) {
1209
+ if (type == null) {
1210
+ const platforms = ChatExpress.getPlatforms();
1211
+ return _(platforms).findWhere({ id: platform }) != null;
1212
+ } else {
1213
+ const messageType = _(_messageTypes).findWhere({ type: type });
1214
+ return messageType != null && messageType.platforms[platform];
1215
+ }
1216
+ };
1217
+
1218
+ /**
1219
+ * @method isValidFile
1220
+ * Check if a file type is valid (extension, size, etc), if nothing is specified file is assumed to be valid
1221
+ * @param {String} platform Platform id (telegram, facebook, ...)
1222
+ * @param {String} type Message type (image, document, ...)
1223
+ * @param {Object} file The file descriptor
1224
+ * @param {String} file.filename The filename of the file
1225
+ * @param {Buffer} file.buffer The buffer
1226
+ * @param {String} file.extension The extension of the file (with leading dot)
1227
+ * @param {String} file.mimeType Mime type of the file
1228
+ * @return {String} Null if no errors
1229
+ */
1230
+ ChatExpress.isValidFile = function(platform, type, file) {
1231
+ const messageType = _(_messageTypes).findWhere({ type: type });
1232
+ if (messageType != null) {
1233
+ const validator = messageType.validators[platform];
1234
+ if (validator != null) {
1235
+ return validator(file);
1236
+ }
1237
+ }
1238
+ return null;
1239
+ };
1240
+
1241
+ /**
1242
+ * @method registerParam
1243
+ * Register param for all transport/platforms
1244
+ * @param {String} name
1245
+ * @param {String} type Type of param
1246
+ * @param {Object} config Params of the param, pun intended
1247
+ * @chainable
1248
+ */
1249
+ ChatExpress.registerParam = function(name, type, config = {}) {
1250
+ if (name == null || typeof name !== 'string') {
1251
+ throw 'Missing name in .registerParam()';
1252
+ }
1253
+ if (type == null || typeof type !== 'string') {
1254
+ throw 'Missing type in .registerParam()';
1255
+ }
1256
+ if (_params.all == null) {
1257
+ _params.all = [];
1258
+ }
1259
+ _params.all.push({
1260
+ name,
1261
+ type,
1262
+ placeholder: config.placeholder,
1263
+ label: !_.isEmpty(config.label) ? config.label : name,
1264
+ description: config.description,
1265
+ default: config.default,
1266
+ options: config.options
1267
+ });
1268
+ return this;
1269
+ };
1270
+
1271
+ ChatExpress.reset = function() {
1272
+ // reset global callbacks, will be re-registered with deploy
1273
+ _globalCallbacks = {};
1274
+ };
1275
+
1276
+ module.exports = ChatExpress;