//****************************************************************************//
//* *//
//* License *//
//* *//
//* Copyright 2017 Item Consulting AS *//
//* *//
//* Licensed under the Apache License, Version 2.0 (the "License"); *//
//* you may not use this file except in compliance with the License. *//
//* You may obtain a copy of the License at *//
//* *//
//* http://www.apache.org/licenses/LICENSE-2.0 *//
//* *//
//* Unless required by applicable law or agreed to in writing, software *//
//* distributed under the License is distributed on an "AS IS" BASIS, *//
//* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *//
//* See the License for the specific language governing permissions and *//
//* limitations under the License. *//
//* *//
//* *//
//****************************************************************************//
//*************************************************//
//* variable declarations *//
//*************************************************//
var websocket = require('/lib/xp/websocket');
var ioLib = require('/lib/xp/io');
var portal = require('/lib/xp/portal');
var clientExpansions = {};
var responseObject = {
webSocket: {
data: {
user: "test"
},
subProtocols: ["text"]
}
};
var groups = {};
function defaultEventHandler(event) {
log.info(JSON.stringify(event));
}
var additionalEventHandlers = {
error: [],
message: [],
open: [],
close: []
};
var eventHandlers = {
error: defaultEventHandler,
message: defaultEventHandler,
open: defaultEventHandler,
close: defaultEventHandler
};
//*************************************************//
//* Bindings *//
//*************************************************//
/**
* @name wsUtil
* @class wsUtil
* @classdesc Server side websocket utility extension library for Enonic XP
* @requires /lib/xp/websocket
* @requires /lib/xp/io
* @requires /lib/xp/portal
* @author Per Arne Drevland
* @version 1.1.0
* @example
* var ws = require('/lib/wsUtil');
* @hideconstructor
*/
exports.addHandlers = addHandlers;
exports.getGroupUsers = getGroupUsers;
exports.addUserToGroup = addUserToGroup;
exports.createGroup = createGroup;
exports.send = send;
exports.openWebsockets = openWebsockets;
exports.sendSocketResponse = sendSocketResponse;
exports.setEventHandler = setEventHandler;
exports.setEventHandlers = setEventHandlers;
exports.setSocketRequestResponse = setSocketRequestResponse;
exports.SocketEmitter = SocketEmitter;
exports.removeUserFromGroup = removeUserFromGroup;
exports.sendToGroup = sendToGroup;
exports.returnScript = returnScript;
exports.extend = extend;
exports.expandClient = expandClient;
exports.getWsEvents = getWsEvents;
//*************************************************//
//* Function declarations *//
//*************************************************//
/**
* @memberOf wsUtil
* @description extends the sendToGroup function from EnonicXP to stringify objects
* @param {string} name Name of the group
* @param {object|string} message Message to send to the group
* @since 0.0.1
* @example
* // send to space 'global'
* ws.sendToGroup('global', { hello: 'everyone in global' });
*/
function sendToGroup(name, message) {
if (typeof message === 'object') message = JSON.stringify(message);
websocket.sendToGroup(name, message);
}
/**
* @class wsUtil.SocketEmitter
* @memberOf wsUtil
* @classdesc Create a new SocketEmitter instance to handle individual socket connections and emit events and listen
* to events created by the client.
* @returns {SocketEmitterInterface} Interface for handling incoming sockets
* @since 0.0.1
* @example
* // Simple chat example
*
* var socketEmitter = new ws.SocketEmitter(); // create instance
* var motd = 'Welcome to our chat';
* var users = {};
* socketEmitter.connect(connectionCallback);
* function connectionCallback(socket) {
*
* socket.emit('motd', motd); // Send message of the day
*
* socket.on('register-username', function(username) { // Register username
* if (!users[username]) { // if not taken
* users[username] = socket.id; // Register the new user
* socket.emit('username', 'ok'); // Tell the client that username is ok
* socket.broadcast('user-enter', username); // Tell all clients that a new user has entered the chat
* }
* else { // if taken
* socket.emit('username', 'taken'); // Tell the client to chose again
* }
* });
*
* socket.on('public-message', function(message) {
* socketEmitter.broadcast('public-message', message); // Broadcast public messages
* });
*
* socket.on('private-message', function(message) {
* socket.sendTo(users[message.username], message.content); // Send private message with a simple user lookup
* });
*
* socket.on('disconnect', function() { // Clean up stuff when user leaves
* for (var username in users) { // Find the correct user id
* if (users.hasOwnProperty(username) && users[username] === socket.id) {
* delete users[username]; // remove the user
* socketEmitter.broadcast('user-leave', username); // Broadcast that a user has left the chat
* }
* }
* });
* }
*/
function SocketEmitter() {
/**
* @memberOf wsUtil.SocketEmitter
* @property {EmitterUsers} _users Users that has a socket connection
* @private
* @inner
*/
var _users = {};
/**
* @memberOf wsUtil.SocketEmitter
* @property {EmitterHandlers} _handlers Handlers for all users and events
* @private
* @inner
*/
var _handlers = {};
/**
* @memberOf wsUtil.SocketEmitter
* @property {ConnectionCallback} _cb The callback function to call when a user connects
* @private
* @inner
*/
var _cb;
//
// When a new user connects, do the following
// Create a new user object and instantiate new on and emit functionalities.
// Bind the new user object to the users object
// Call the connect function
//
addHandlers("open", function (event) {
var user = { id: event.session.id, on: new On(event.session.id), emit: new Emit(event.session.id), sendTo: sendTo};
_users[event.session.id] = user;
_cb(user);
});
//
// When a user close the connection, do the following
// If handlers have disconnect event, fire the disconnect handler
// Delete the user object from the users object
// Remove the handlers for the user
//
addHandlers("close", function (event) {
if (_handlers[event.session.id].hasOwnProperty("disconnect")) {
_handlers[event.session.id].disconnect()
}
delete _users[event.session.id];
delete _handlers[event.session.id];
});
//
// When a new message event arrives, do the following
// Try to parse the message, if the client side don't send the message as an object, it is probably an error
// Log error as info
// Try to call the users handler function for the event. If it fails it could be that the event handler doesn't exist
// Log error as info
//
addHandlers("message", function(event) {
try {
var msg = JSON.parse(event.message);
if (_handlers.hasOwnProperty(event.session.id) && _handlers[event.session.id].hasOwnProperty(msg.event)) {
try {
_handlers[event.session.id][msg.event](msg.object);
} catch (e) {
log.info(e);
}
}
else {
log.info("SOCKET-LIB: Unhandled event: " + msg.event);
log.info(JSON.stringify(event));
}
} catch (e) {
log.info("SOCKET-LIB: Wrong JSON format for client emit object");
log.info(JSON.stringify(event));
}
});
/**
* @memberOf wsUtil.SocketEmitter.EmitterUsers
* @description Let the connected socket object send message to other socket objects
* @param {string} id The id of the socket recipient
* @param {string} event The name of the event to emit
* @param {object|string} message The content of the event
* @since 0.0.1
* @example
* // Send message to a connected user
* socketEmitter.sendTo('someId', 'someEvent', someMessage);
*/
function sendTo(id, event, message) {
var object = { event: event, object: message};
send(id, object);
}
/**
* @class
* @description Emit events with content to specific socket connection
* @param {string} id The id of the socket connection
* @returns {EmitInterface} Takes an event and a message object and sends to the socket connection
* @since 0.0.1
* @inner
* @memberOf wsUtil.SocketEmitter
* @private
*/
function Emit(id) {
/**
* @interface EmitInterface
*/
return function (event, object) {
var obj = { event: event, object: object};
send(id, obj);
}
}
/**
* @class
* @memberOf wsUtil.SocketEmitter
* @description The emit event handlers acts on emit events from the clients
* @param {string} id The id of the connection
* @returns {OnInterface} Binds a handler to an emit event
* @since 0.0.1
* @inner
* @private
* */
function On(id) {
if (!_handlers.hasOwnProperty(id)) {
_handlers[id] = {};
}
/**
* @interface OnInterface
*/
return function(event, handler) {
_handlers[id][event] = handler;
}
}
/**
* @description This is the event that is being called when a new user connects
* @param {ConnectionCallback} callback The callback to call when users connects
* @memberOf wsUtil.SocketEmitter
* */
function connect(callback) {
_cb = callback;
}
/**
* @description Emit an event to every connected users
* @param {string} event The name of the event
* @param {object|string} object The message of the emit event
* @memberOf wsUtil.SocketEmitter
* @since 0.0.1
* */
function broadcast(event, object) {
var msg = {event: event, object: object};
for (var id in _users) {
if (_users.hasOwnProperty(id)) {
send(id, msg);
}
}
}
/**
* @interface SocketEmitterInterface
*/
return {
connect: connect,
broadcast: broadcast
}
}
/**
* @memberOf wsUtil
* @description Extends the library functionalities to create reusable extensions
* @param exportObject {object} The object that extends the library
* @see [Creating extensions]{@link https://itemconsulting.github.io/lib-xp-wsutil/extensions.html}
* @example
* // lib/extension.js
* var ws = require('/lib/wsUtil');
*
* var extensionObject = { extension: extension };
* ws.extend(extensionObject);
*
* function extension() {
* ws.addHandler('open', function(event) { log.info('Extension says hi!'); });
* }
* exports.extension = ws;
*
*
* // service/extensionService/extensionService.js
*
* var extensionLib = require('extension').extension
*
* extensionLib.openWebsockets(exports);
* extensionLib.extension(); // Activate extension
*/
function extend(exportObject) {
for (var name in exportObject) {
if (exportObject.hasOwnProperty(name)) exports[name] = exportObject[name]
}
return exports;
}
/**
* @description Expand the client library with new functionalities. NOTE: The expansion object have direct access to the client library's inner variables and functions
* @memberOf wsUtil
* @param name {string | object} Name of the new client function or an object with key=name value=function
* @param func {function} The function for the new client interface.
* @example
* // service/websocket/websocket.js
* var ws = require('path/to/wsUtil');
*
* ws.expandClient('hello', function() { send('Hello'); // use the inner send function });
*
* ws.openWebsockets(exports);
*
* // socket.js
*
* var cws = new ExpWs();
*
* cws.hello();
*/
function expandClient(name, func) {
if (typeof name === 'string') {
clientExpansions[name] = func;
}
else if (typeof name === 'object' && !func) {
for (var n in name) {
if (name.hasOwnProperty(n)) {
clientExpansions[n] = name[n];
}
}
}
}
/**
* @memberOf wsUtil
* @description Add additional socket event handlers. This is the secondary handlers for the event and they are being pushed to the additional event handlers array
* @param {string} event The name of the socket event
* @param {function} handler The handler to bind to the event
* @since 0.0.1
* */
function addHandlers(event, handler) {
if (additionalEventHandlers[event]) {
additionalEventHandlers[event].push(handler);
}
else {
log.error("Event missing, must be one of 'open', 'close', 'error' or 'message'")
}
}
/**
* @memberOf wsUtil
* @description Get all connected users from a group
* @param {string} group Name of the group
* @returns {?string[]} The array of users or undefined if group doesn't exist
* @since 0.0.1
*/
function getGroupUsers(group) {
if (groups[group]) {
return groups[group].users;
}
else return undefined;
}
/**
* @memberOf wsUtil
* @description Adds a user to a group if it exists. Create the group if it doesn't exist and the autoCreate flag is set
* or log an error if not
* @param {string} id The id of the user
* @param {string} group The name of the group
* @param {boolean} [autoCreate] The flag to create the group if it doesn't exist
* @since 0.0.1
*
*/
function addUserToGroup(id, group, autoCreate) {
if (groups[group]) {
groups[group].users.push(id);
websocket.addToGroup(group, id);
}
else if (autoCreate) {
createGroup(group, true);
addUserToGroup(id, group, autoCreate);
}
else {
log.error('No such group, try setting the autoCreate flag to true');
}
}
/**
* @memberOf wsUtil
* @description Removes a user from a group of given name, if the group is empty, it removes the group
* @param {string} name Name of the group
* @param {string} id The id of the user
* @since 0.0.1
*/
function removeUserFromGroup(name, id) {
groups[name].users.splice(groups[name].users.indexOf(id),1);
if (groups[name].users.length === 0) {
delete groups[name];
}
websocket.removeFromGroup(name, id);
}
/**
* @memberOf wsUtil
* @description Creates a group with given name. If the autoRemove flag is set, if removes a user from the group when user closes connection
* @param {string} name The name of the group
* @param {boolean} [autoRemove] Removes the user from the group on close connection
* @since 0.0.1
*/
function createGroup(name, autoRemove) {
if (!groups.hasOwnProperty(name)) {
groups[name] = {users: []};
websocket.addToGroup(name);
if (autoRemove) {
var found = false;
additionalEventHandlers.close.forEach(function (t) {
if (t.name === 'autoRemove') {
found = true;
}
});
if (!found) {
additionalEventHandlers.close.push(function autoRemove(event) {
websocket.removeFromGroup(name, event.session.id);
groups[name].users.splice(groups[name].users.indexOf(event.session.id), 1);
if (groups[name].users.length === 0) {
delete groups[name];
}
})
}
}
}
}
/**
* @memberOf wsUtil
* @description Sends a message to given client, if the message is of type object it will be stringified
* @param {string} id The id of the user
* @param {string|object} message The message to send
* @since 0.0.1
*/
function send(id, message) {
if (typeof message === 'object') {
websocket.send(id, JSON.stringify(message));
}
else {
websocket.send(id, message);
}
}
/**
* @memberOf wsUtil
* @desc The function that binds to the exports webSocketEvent method. This function checks the ws event object for type
* and calls the appropriate handler. Then it loops over the additional handlers and calls each one of them.
* @param {object} event The event sent from the client
* @since 1.1.0
*/
function getWsEvents(event) {
if (eventHandlers[event.type]) {
if (event.type === 'message') {
try {
eventHandlers.message(JSON.parse(event.message));
} catch(e) {
eventHandlers.message(event.message);
}
}
else {
eventHandlers[event.type](event);
}
}
additionalEventHandlers[event.type].forEach(function(handler) {
handler(event);
})
}
/**
* @memberOf wsUtil
* @description Opens the websocket connection and delegate events to handlers
* @param {object} exp The exports object from the Enonic module that is assigned to handle websocket events
* @param {string} [host=portal.serviceUrl({ service: 'websocket'})] The host for the service that serves the web sockets
* @since 0.0.1
*/
function openWebsockets(exp, host) {
exp.get = function(req) {
return sendSocketResponse(req, host);
};
exp.webSocketEvent = wsEvents;
}
function returnScript(host) {
host = host || portal.serviceUrl({ service: 'websocket'});
var file = ioLib.readText(ioLib.getResource(resolve('../assets/clientws.js')).getStream());
file = file.replace('&HOST&', host).replace('&CLIENTEXPANSIONS&', JSON.stringify(clientExpansions, function(key, val) {
return (typeof val === 'function') ? '' + val : val;
}).replace(/"/g, "").replace(/\\n/g, "").replace(/\\/g, '"'));
return {
body: file,
contentType: 'application/javascript'
}
}
/**
* @memberOf wsUtil
* @description Send the socket response for a socket request
* @example
* // someSocketService.js
* ws = require('/lib/wsUtil');
* exports.get = ws.sendSocketResponse;
* // or
* exports.get = function(req) {
* return ws.sendSocketResponse(req);
* }
* @param {object} req The request object passed to the server
* @param {string} [host=portal.serviceUrl({ service: 'websocket'})] - The url to the service that serves the web sockets
* @returns {SocketResponse} The response object
* @since 0.0.1
*/
function sendSocketResponse(req, host) {
if (!req.webSocket) {
return returnScript(host);
}
else return responseObject;
}
/**
* @memberOf wsUtil
* @description Sets a handler for given socket event
* @param {'open'|'close'|'message'|'error'} event The name of the event
* @param {function} handler The event handler
* @since 0.0.1
*/
function setEventHandler(event, handler) {
if (eventHandlers.hasOwnProperty(event)) {
eventHandlers[event] = handler;
}
else {
log.error("Event missing, must be one of 'open', 'close', 'error' or 'message'")
}
}
/**
* @memberOf wsUtil
* @description Set all socket event handlers
* @param {object} handlers
* @since 0.0.1
*/
function setEventHandlers(handlers) {
eventHandlers = handlers;
}
/**
* @memberOf wsUtil
* @description set custom socket response object. Default object is taken from {@link http://docs.enonic.com/en/stable/developer/ssjs/websockets.html Enonic doc}
* @param {SocketResponse} response The socket response object
* @since 0.0.1
*/
function setSocketRequestResponse(response) {
responseObject = response;
}
//*************************************************//
//* Type definitions *//
//*************************************************//
/**
* @typedef {object} EmitterUsers
* @memberOf wsUtil.SocketEmitter
* @alias EmitterUsers
* @property {string} id The session id of the user
* @property on {wsUtil.SocketEmitter~On} The client emitted event handler interface
* @property {wsUtil.SocketEmitter~Emit} emit The server side emit message functionality,
* @property {function} sendTo The server side send message to specific user functionality
*/
/**
* @typedef {object} EmitterHandlers
* @alias EmitterHandlers
* @memberOf wsUtil.SocketEmitter
* @property {object} id Id of the user
* @property {object} id.event name of the event
* @property {function} id.event.function the actual handler
*/
/**
* @typedef {function} ConnectionCallback
* @alias ConnectionCallback
* @memberOf wsUtil.SocketEmitter
* @description Callback function for connected users, this is being called with the connected user interface
* @param {EmitterUsers} socket The connected user interface
* @example
* socketEmitter.connect(connectionCallback);
*
* function connectionCallback(socket) {
* // do something with connected socket
* }
*/
/**
* @typedef {object} SocketEmitterInterface
* @alias SocketEmitterInterface
* @memberOf wsUtil.SocketEmitter
* @description Call the SocketEmitter constructor to receive a SocketEmitter interface
* Use the interface to act on connection events from clients and broadcast events from the server
*
* @example
* // Create the SocketEmitter
* var socketEmitter = new ws.SocketEmitter();
* // Get connection events
* socketEmitter.connect(function(socket) {
* // Use the client socket connection to emit messages
* socket.emit('new-connection', { greetings: 'Hello from server' });
* // Use the client socket connection to listen to emitted events from user
* socket.on('some-event', function(message) {
* log.info('some-event message ' + JSON.stringify(message));
* });
* });
* // Broadcast messages to all users
* socketEmitter.broadcast('hello-everybody', { message: 'Hello from server' });
*
* @property {ConnectionCallback} connect A event that fires every time a user connects to the web socket
* @property {function} broadcast Emit an event to all connected users
*/
/**
* @typedef {function} EmitInterface
* @alias EmitFunction
* @memberOf wsUtil.SocketEmitter~Emit
* @description Returned function from the Emit class created by a user that has connected. This function takes an event and a message object/string and sends it to the
* user that created the instance
* @param {string} event The name of the emitted event
* @param {string|object} message The message being sent
* @example
* // emitting any events
* socketEmitter.connect(function(socket) {
* socket.emit('Hello is there anyone in there?', { just: 'nod if you can hear me' });
* });
*
*/
/**
* @typedef {function} OnInterface
* @alias OnFunction
* @memberOf wsUtil.SocketEmitter~On
* @description Returned function from the On class instance. This function takes an event and a handler. The handler is being called when a client emits the event
* with or without parameters
* @param {string} event The name of the emitted event from the client
* @param {function} handler The emitted event handler
* @example
*
* socketEmitter.connect(function(socket) {
* // example of handler without parameters
* socket.on('What is the meaning of life, the universe and everything?', function() {
* socket.emit('answer', calculateTheMeaningOfLifeTheUniverseAndEverything());
* });
* // example of handler with parameters
* socket.on('isPrime?', function(number) {
* function isPrime(num, iter) {
* if (!iter) iter = Math.floor(Math.sqrt(num));
* if (iter > 1) return isPrime(num, iter-1) ? num % iter !== 0 : false;
* return true
* }
* socket.emit('isPrime!', isPrime(number));
* });
* })
*/
/**
* @typedef {object} SocketResponse
* @memberOf wsUtil
* @description The response object sent from the server
* @property {object} webSocket Main response object
* @property {object} webSocket.data Additional data object
* @property {string[]} webSocket.subProtocols Array of sub protocols to support
*/