chat-platform 3.0.0 → 3.1.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/chat-platform.js +157 -1
- package/package.json +3 -2
package/chat-platform.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
+
const ws = require('ws');
|
|
2
3
|
const clc = require('cli-color');
|
|
3
4
|
const prettyjson = require('prettyjson');
|
|
4
5
|
const { when } = require('./lib/utils');
|
|
@@ -23,11 +24,20 @@ if (global['redbot-chat-platform'] == null) {
|
|
|
23
24
|
params: {}
|
|
24
25
|
};
|
|
25
26
|
}
|
|
27
|
+
// WebSocket endpoints are mounted on the raw http server, which is shared by every chatbot of the
|
|
28
|
+
// instance: the registry has to live in the global space like the others, and it has to survive
|
|
29
|
+
// ChatExpress.reset() since the "upgrade" listener attached to the server is never removed
|
|
30
|
+
if (global['redbot-chat-platform'].wsRoutes == null) {
|
|
31
|
+
global['redbot-chat-platform'].wsRoutes = {};
|
|
32
|
+
global['redbot-chat-platform'].wsServers = new WeakSet();
|
|
33
|
+
}
|
|
26
34
|
|
|
27
35
|
let _messageTypes = global['redbot-chat-platform'].messageTypes;
|
|
28
36
|
let _events = global['redbot-chat-platform'].events;
|
|
29
37
|
let _platforms = global['redbot-chat-platform'].platforms;
|
|
30
38
|
let _params = global['redbot-chat-platform'].params;
|
|
39
|
+
let _wsRoutes = global['redbot-chat-platform'].wsRoutes;
|
|
40
|
+
let _wsServers = global['redbot-chat-platform'].wsServers;
|
|
31
41
|
let _globalCallbacks = {};
|
|
32
42
|
|
|
33
43
|
const ChatExpress = function(options) {
|
|
@@ -50,6 +60,9 @@ const ChatExpress = function(options) {
|
|
|
50
60
|
RED: null,
|
|
51
61
|
routes: null,
|
|
52
62
|
routesDescription: null,
|
|
63
|
+
wsRoutes: null,
|
|
64
|
+
wsRoutesDescription: null,
|
|
65
|
+
wsVerifyClient: null,
|
|
53
66
|
events: null,
|
|
54
67
|
relaxChatId: false,
|
|
55
68
|
bundle: false,
|
|
@@ -634,6 +647,145 @@ const ChatExpress = function(options) {
|
|
|
634
647
|
return when(true);
|
|
635
648
|
}
|
|
636
649
|
|
|
650
|
+
/**
|
|
651
|
+
* @method wsPathname
|
|
652
|
+
* The pathname an upgrade request was sent to, without the query string
|
|
653
|
+
* @param {string} url
|
|
654
|
+
* @return {string}
|
|
655
|
+
*/
|
|
656
|
+
function wsPathname(url) {
|
|
657
|
+
const pathname = String(url != null ? url : '').split('?')[0];
|
|
658
|
+
// a trailing slash is the same endpoint: "/redbot/my-bot/ws/" === "/redbot/my-bot/ws"
|
|
659
|
+
return pathname.length > 1 && pathname.slice(-1) === '/' ? pathname.slice(0, -1) : pathname;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* @method wsFullPath
|
|
664
|
+
* Absolute path of a WebSocket endpoint. Like the Express ones, the paths declared by a platform are
|
|
665
|
+
* relative to `httpNodeRoot`, but an upgrade request never enters the Express router so the prefix
|
|
666
|
+
* has to be applied by hand
|
|
667
|
+
* @param {object} RED
|
|
668
|
+
* @param {string} route
|
|
669
|
+
* @return {string}
|
|
670
|
+
*/
|
|
671
|
+
function wsFullPath(RED, route) {
|
|
672
|
+
const root = RED.settings != null && !_.isEmpty(RED.settings.httpNodeRoot) ?
|
|
673
|
+
RED.settings.httpNodeRoot : '/';
|
|
674
|
+
const prefix = root.slice(-1) === '/' ? root.slice(0, -1) : root;
|
|
675
|
+
return `${prefix}${route.charAt(0) === '/' ? route : `/${route}`}`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* @method dispatchUpgrade
|
|
680
|
+
* The one and only "upgrade" listener of the process: hand the request over to the WebSocket endpoint
|
|
681
|
+
* registered on that path, if any.
|
|
682
|
+
* Node-RED attaches its own listeners to the same server (the editor "comms" channel and the core
|
|
683
|
+
* WebSocket nodes) and, exactly like them, a request that matches nothing here is left alone instead
|
|
684
|
+
* of being destroyed, so that the other listeners can still handle it
|
|
685
|
+
*/
|
|
686
|
+
function dispatchUpgrade(request, socket, head) {
|
|
687
|
+
const entry = _wsRoutes[wsPathname(request.url)];
|
|
688
|
+
if (entry == null) {
|
|
689
|
+
// not one of ours: don't destroy the socket, another listener may want to handle it
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
entry.wss.handleUpgrade(request, socket, head, function done(connection) {
|
|
693
|
+
entry.wss.emit('connection', connection, request);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* @method mountUpgradeListener
|
|
699
|
+
* Attach the dispatcher to the http server, once per server: a Node-RED deploy re-creates every
|
|
700
|
+
* configuration node, so a listener per chatbot would pile up at every deploy
|
|
701
|
+
* @param {object} server
|
|
702
|
+
*/
|
|
703
|
+
function mountUpgradeListener(server) {
|
|
704
|
+
if (_wsServers.has(server)) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
server.on('upgrade', dispatchUpgrade);
|
|
708
|
+
_wsServers.add(server);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* @method mountWsRoutes
|
|
713
|
+
* Mount the WebSocket endpoints of a chat server. Unlike the Express routes these are matched exactly,
|
|
714
|
+
* so `wsRoutes` can also be a function (bound to the chat server) returning the routes, for a platform
|
|
715
|
+
* that needs to build the path out of its own configuration (i.e. a bot id in the middle of the path)
|
|
716
|
+
* @param {object} RED
|
|
717
|
+
* @param {object|function} wsRoutes
|
|
718
|
+
* @param {object} wsRoutesDescription
|
|
719
|
+
* @param {object} chatServer
|
|
720
|
+
*/
|
|
721
|
+
// eslint-disable-next-line max-params
|
|
722
|
+
function mountWsRoutes(RED, wsRoutes, wsRoutesDescription, chatServer) {
|
|
723
|
+
const routes = _.isFunction(wsRoutes) ? wsRoutes.call(chatServer) : wsRoutes;
|
|
724
|
+
if (routes == null || _.isEmpty(routes)) {
|
|
725
|
+
return when(true);
|
|
726
|
+
}
|
|
727
|
+
// the descriptions are keyed by route: when the routes are generated, so are their descriptions
|
|
728
|
+
const descriptions = _.isFunction(wsRoutesDescription) ?
|
|
729
|
+
wsRoutesDescription.call(chatServer) : wsRoutesDescription;
|
|
730
|
+
if (RED == null || RED.server == null) {
|
|
731
|
+
chatServer.warn('Impossible to mount the WebSocket endpoints: ' +
|
|
732
|
+
(RED == null ? '"RED" param is empty' : '"RED.server" is not available'));
|
|
733
|
+
return when(true);
|
|
734
|
+
}
|
|
735
|
+
const options = chatServer.getOptions();
|
|
736
|
+
const uiPort = RED.settings.get('uiPort');
|
|
737
|
+
mountUpgradeListener(RED.server);
|
|
738
|
+
// eslint-disable-next-line no-console
|
|
739
|
+
console.log(lcd.timestamp() + '');
|
|
740
|
+
// eslint-disable-next-line no-console
|
|
741
|
+
console.log(lcd.timestamp() + grey('------ WebSockets for ' + options.transport.toUpperCase() + '--------------'));
|
|
742
|
+
_.each(routes, (handler, route) => {
|
|
743
|
+
const host = 'ws://localhost' + (uiPort != '80' ? ':' + uiPort : '');
|
|
744
|
+
const path = wsFullPath(RED, generateCallback(route, chatServer));
|
|
745
|
+
if (_wsRoutes[path] != null) {
|
|
746
|
+
chatServer.error(`The WebSocket endpoint ${path} is already mounted by another chatbot, skipped`);
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
let description = null;
|
|
750
|
+
if (descriptions != null && _.isString(descriptions[route])) {
|
|
751
|
+
description = descriptions[route];
|
|
752
|
+
} else if (descriptions != null && _.isFunction(descriptions[route])) {
|
|
753
|
+
description = descriptions[route].call(chatServer);
|
|
754
|
+
}
|
|
755
|
+
// eslint-disable-next-line no-console
|
|
756
|
+
console.log(lcd.timestamp() + green(host + path) + (description != null ? grey(' - ') + white(description) : ''));
|
|
757
|
+
const wss = new ws.Server(_.isFunction(options.wsVerifyClient) ?
|
|
758
|
+
{ noServer: true, verifyClient: options.wsVerifyClient.bind(chatServer) } : { noServer: true });
|
|
759
|
+
wss.setMaxListeners(0);
|
|
760
|
+
wss.on('connection', handler.bind(chatServer));
|
|
761
|
+
wss.on('error', error => chatServer.error(`WebSocket endpoint ${path}: ${error.message}`));
|
|
762
|
+
_wsRoutes[path] = { wss: wss, chatServer: chatServer };
|
|
763
|
+
return null;
|
|
764
|
+
});
|
|
765
|
+
// eslint-disable-next-line no-console
|
|
766
|
+
console.log(lcd.timestamp() + '');
|
|
767
|
+
return when(true);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* @method unmountWsRoutes
|
|
772
|
+
* Remove every WebSocket endpoint of a chat server. The dispatcher attached to the http server stays
|
|
773
|
+
* there (it's shared by the whole process and it does nothing without a matching endpoint)
|
|
774
|
+
* @param {object} chatServer
|
|
775
|
+
*/
|
|
776
|
+
function unmountWsRoutes(chatServer) {
|
|
777
|
+
_.keys(_wsRoutes).forEach(path => {
|
|
778
|
+
const entry = _wsRoutes[path];
|
|
779
|
+
if (entry == null || entry.chatServer !== chatServer) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
delete _wsRoutes[path];
|
|
783
|
+
// closing the server terminates the connected clients: the widgets will reconnect once the
|
|
784
|
+
// chatbot is up again, a Node-RED deploy re-creates the configuration nodes
|
|
785
|
+
entry.wss.close();
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
637
789
|
var methods = {
|
|
638
790
|
|
|
639
791
|
'in': function() {
|
|
@@ -1005,6 +1157,9 @@ const ChatExpress = function(options) {
|
|
|
1005
1157
|
.then(function() {
|
|
1006
1158
|
return mountRoutes(options.RED, options.routes, options.routesDescription, _this);
|
|
1007
1159
|
})
|
|
1160
|
+
.then(function() {
|
|
1161
|
+
return mountWsRoutes(options.RED, options.wsRoutes, options.wsRoutesDescription, _this);
|
|
1162
|
+
})
|
|
1008
1163
|
.then(function() {
|
|
1009
1164
|
return mountEvents(options.events, _this);
|
|
1010
1165
|
})
|
|
@@ -1018,7 +1173,7 @@ const ChatExpress = function(options) {
|
|
|
1018
1173
|
}
|
|
1019
1174
|
// listen to inbound event
|
|
1020
1175
|
var connector = options.connector;
|
|
1021
|
-
if (connector != null && options.inboundMessageEvent != null) {
|
|
1176
|
+
if (connector != null && options.inboundMessageEvent != null && _.isFunction(connector.on)) {
|
|
1022
1177
|
connector.on(options.inboundMessageEvent, function (message) {
|
|
1023
1178
|
inboundMessage(message, chatServer);
|
|
1024
1179
|
});
|
|
@@ -1093,6 +1248,7 @@ const ChatExpress = function(options) {
|
|
|
1093
1248
|
this.emit('stop');
|
|
1094
1249
|
var options = this.getOptions();
|
|
1095
1250
|
unmountRoutes(options.RED, options.routes, this);
|
|
1251
|
+
unmountWsRoutes(this);
|
|
1096
1252
|
unmountEvents(options.events, this);
|
|
1097
1253
|
var stack = when(true);
|
|
1098
1254
|
if (_.isFunction(options.onStop)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chat-platform",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Universal Chat Platform",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"lodash": "^4.18.1",
|
|
20
20
|
"prettyjson": "^1.2.1",
|
|
21
21
|
"sequelize": "^6.37.8",
|
|
22
|
-
"sqlite3": "^5.1.7"
|
|
22
|
+
"sqlite3": "^5.1.7",
|
|
23
|
+
"ws": "^7.5.7"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"chai": "^4.1.1",
|