UNPKG

node-chat-server

Version:
289 lines (253 loc) 9.98 kB
var http = require('http'); var WS = require('ws'); var WebSocketServer = WS.Server; function copy(a, b) { if(!b) b = {}; for(var m in a){ b[m] = a[m]; } return b; } function ChatServer(options){ // if(!options) return console.log(`node-chat-server: you must provide an options object.`) // if(!options.create) return console.log(`node-chat-server: you must provide a 'create' method in options for creating a new chat message.`) // if(!options.authorize) return console.log(`node-chat-server: you must provide an 'authorize' method in options for authorizing connecting sockets.`) // if(!options.getMessages) return console.log(`node-chat-server: you must provide a 'getMessages' method in options for retrieving a set of chat messages.`) var chatServer = this; var context = options.context || chatServer; var port = options.port || 8080; var httpServer = http.createServer(); var server = new WebSocketServer({ server: httpServer }); chatServer.sockets = []; chatServer.httpServer = httpServer; chatServer.wsServer = server; chatServer.copy = copy; chatServer.options = options; this.events = {}; function sendMessage(message, userId, done) { userId = userId.toString(); chatServer.sockets.map(function (socket) { if(socket.user && ((socket.user.id || socket.user._id).toString() === userId)){ socket.action('chatMessage', message); } }); } function socketClosingError(socket){ var user = socket.user; var id; if(user){ id = user.id || user._id || user.name || 'Unknown'; } else{ id = socket.id || socket._id || socket.name || 'Unknown'; } chatServer.emit('error', `socket '${id}' did not close correctly.`); } var actions = { authorize(socket, data, done){ if(!options.authorize) return done('options.authorize is missing'); options.authorize.call(context, data, function (err, user) { if(err){ console.log(`node-chat-server: authorization error ${ err.message || err.toString() }`); return done(err); } if(!user){ return done('authorizationFailed'); } socket.user = user; chatServer.sockets.push(socket); chatServer.emit('socketAuthorized', socket, chatServer); if(options.log){ console.log(`node-chat-server: authorizing ${user.name || user.id || user._id}. ${chatServer.sockets.length} connected sockets.`) } done(null, user); }); }, create(socket, data, done){ if(!options.create) return done('options.create is missing'); var message = copy(data); if(!message.to) { return done(`node-chat-server: a chat message must contain a 'to' property which should be a valid id of a user or a group.`); } var user = socket.user; message.createdAt = new Date().toISOString(); message.from = (user.id || user._id).toString(); if(options.log){ console.log(`node-chat-server: creating chat message for ${user.name || user.id || user._id}`) } options.create.call(context, message, function (err, msg) { if(err){ return done(err); } if(msg.isGroup){ options.getGroupUserIds.call(context, { groupId: msg.to }, function (err, userIds) { if(err){ return done(err); } userIds.map(function (userId) { sendMessage(msg, userId); }); done(null, msg); }); } else{ sendMessage(msg, msg.to); sendMessage(msg, msg.from); done(null, msg); } }); }, getMessages(socket, data, done){ if(!options.getMessages) return done('options.getMessages is missing'); var query = copy(data); query.ids = [(socket.user.id || socket.user._id).toString(), query.with]; if(!query.skip){ query.skip = 0; } if(!query.limit){ query.limit = 10; } options.getMessages.call(context, query, done); }, read(socket, data, done){ if(!options.read) return done('options.read is missing'); options.read.call(context, data.id, done); } }; server.on('connection', function (socket) { // fired for every incoming socket connection. if(options.log){ console.log(`node-chat-server: connecting socket`) } socket.action = function(type, data){ // run an action on the client. if (socket.readyState === WS.OPEN) { try{ socket.send(JSON.stringify({type: type, data: data})); } catch(err){ socketClosingError(socket); } } else{ socketClosingError(socket); } }; socket.on('message', function(msg){ try { var json = JSON.parse(msg); if(!json.type){ return console.error(`json does not have a 'type'`); } // is this socket is not authorized and is not trying to authorize.. if(options.authorize && (json.type !== 'authorize') && (!socket.user || (!socket.user.id && !socket.user._id))){ // if not authorized, it can use only the open actions. if(options.openActions && options.openActions[json.type]){ return options.openActions[json.type].call(context, socket, json.data, function(err, res){ if (socket.readyState === WS.OPEN) { try{ socket.send(JSON.stringify({id: json.id, error: err, data: res})); } catch(err){ socketClosingError(socket); } } else{ socketClosingError(socket); } }, socket); } // if unauthorized sockets try to do anything other then // requesting authorization or using the open actions - disconnect them immediately. else{ if(options.log){ console.log(`node-chat-server: disconnecting unauthorized socket - `, json.type) } return socket.close(); } } if(options.log){ console.log('node-chat-server: got - ', json); } var action = actions[json.type] || options.actions[json.type]; if(action){ // perform a chat server action. action.call(context, socket, json.data, function(err, res){ if(socket.isClosed) return; if (socket.readyState === WS.OPEN) { socket.send(JSON.stringify({id: json.id, error: err, data: res})); } }, socket); } else{ return console.error(`cannot find action ${json.type}`); } } catch (e) { return console.error(e); } }); socket.on('close', function () { socket.isClosed = true; var index = chatServer.sockets.indexOf(socket); if(options.log){ console.log(`node-chat-server: socket closing`) } if(index > -1){ chatServer.sockets.splice(index, 1); chatServer.emit('socketClosed', socket, chatServer); if(options.log){ console.log(`node-chat-server: splicing authorized socket. ${chatServer.sockets.length} connected sockets`) } } }); chatServer.emit('socketConnected', socket, chatServer); }); httpServer.listen(port, function(){ if(options.log){ console.log(`node-chat-server: listening at port ${port}`) } if(options.onOpen){ options.onOpen.call(context); } }); } ChatServer.prototype = { on(eventName, listener){ // add a listener to 'eventName', return false in listener to stop the event. var event = this.events[eventName]; if (!event) { event = this.events[eventName] = {listeners: []}; } event.listeners.push(listener); return this; }, off(eventName, listener){ // remove a listener. if (!eventName){ // calling off() with no arguments removes all listeners for all events. this.events = {}; } else if (!listener){ // calling off('eventName') with no listener removes all event listeners for 'eventName'. delete this.events[eventName]; } else{ // calling off('eventName', listener) will only remove listener. var event = this.events[eventName]; if (event) { event.listeners = event.listeners.filter((l)=>{ return (l === listener); }); if (!event.listeners.length) delete this.events[eventName]; } } return this; }, emit(eventName, args){ // emit a named event var args = [].slice.call(arguments, 1); if(this.options.log){ console.log('node-chat-server - emitting ' + eventName); } var cont, event = this.events[eventName]; if (!event) return; for (var i = 0; i < event.listeners.length; i++) { cont = event.listeners[i].apply(null, args); if (cont === false) break; // if a listener returned false, stop here. } return this; }, send(type, data, userIds) { if(typeof userIds === 'string'){ userIds = [userIds]; } this.sockets.map(function (socket) { if(socket.user && ((userIds.indexOf(socket.user.id) > -1) || (userIds.indexOf(socket.user._id) > -1))){ socket.action(type, data); } }); }, broadcast(type, data){ this.sockets.map(s => s.action(type, data)); } } module.exports = ChatServer;