UNPKG

shiro

Version:

Online quiz game engine, inspired by russian tv show 'What? Where? When?' (Million Dollar Mind Game).

556 lines (459 loc) 13.9 kB
var _ = require('lodash') , level = require('level') , async = require('async') // token , token = { meta : 'meta' , user : 'user' , message: 'message' } // usernames blacklist , blockedNames = /(^_|^[Mm][Ee]$|\b[Gg][Aa][Mm][Ee]\b|^[Tt]eam([A-Z\W]|$)|\b[Tt][Ee][Aa][Mm]\b|\b[Aa][Dd][Mm][Ii][Nn]\b)/ // globals , sockets = {} // back reference ; module.exports = Chat; function Chat(options) { // environment this._env = options.env || function(){}; // db file this._storage = options.storage; // db instance this._db = undefined; // meta data this.meta = {}; // list of users this.user = {}; // messages this.message = {}; } Chat.prototype.init = function Chat_init(callback) { var _chat = this , now ; // connect to db and get stuff level(this._storage, {keyEncoding: 'utf8', valueEncoding: 'json'}, function(err, db) { if (err) return callback(err); _chat._db = db; async.series( { meta : _chat._fetchSlice.bind(_chat, token.meta) , user : _chat._fetchSlice.bind(_chat, token.user) , message: _chat._fetchSlice.bind(_chat, token.message) }, function(err, res) { if (err) return callback(err); _chat.meta = res.meta; _chat.user = res.user; _chat.message = res.message; // {{{ create chat instance id // to prevent user name collisions // after db reset if (!_chat.meta.instance) { now = process.hrtime(); _chat.save('meta', 'instance', Date.now().toString(36) + now[1].toString(36) + now[0].toString(36), callback); } else { callback(null); } }); }); // set event listeners this._initEventListeners(); } // saves data to db Chat.prototype.save = function Chat_save(channel, key, value, callback) { var _chat = this; this._db.put(token[channel]+':'+key, value, function(err) { if (err) return callback(err); // update local _chat[channel][key] = value; return callback(null); }); } // loads data from db Chat.prototype.load = function Chat_load(channel, key, callback) { this._db.get(token[channel]+':'+key, function(err, value) { if (err) { // reset local if (key in _chat[channel]) { delete _chat[channel][key]; } if (err.notFound) { return callback(null, undefined); } return callback(err); } // update local _chat[channel][key] = value; return callback(null, value); }); } // deletes data from db Chat.prototype.delete = function Chat_delete(channel, key, callback) { var _chat = this; this._db.del(token[channel]+':'+key, function(err) { if (err) return callback(err); // update local delete _chat[channel][key]; return callback(null); }); } // logins with existing user Chat.prototype.login = function Chat_login(user, callback) { var _chat = this , userData ; if (!user.nickname || !user.password) { return callback({code: 400, message: 'Missing data.'}); } if (!this.user[user.nickname] || this.user[user.nickname].password != user.password) { return callback({code: 401, message: 'Wrong user/password combination.'}); } // create user object userData = {socketid: user.socketid, nickname: user.nickname, password: user.password}; // save this.save('user', userData.nickname, userData, function(err) { if (err) return callback({code: 500, message: err}); sockets[userData.socketid] = userData; return callback(null, userData); }); } // creates new user Chat.prototype.join = function Chat_join(user, callback) { var _chat = this , userData , password ; if (!user.nickname) { return callback({code: 400, message: 'Missing data.'}); } if (this.user[user.nickname] && !user['force']) { return callback({code: 400, message: 'User '+user.nickname+' already exists.'}); } if (user.nickname.match(blockedNames) && !user['force']) { return callback({code: 400, message: 'Name '+user.nickname+' is not allowed.'}); } // create password password = user.password || this._generatePassword(); // create user object userData = {socketid: user.socketid, nickname: user.nickname, password: password}; // clean up (to be) dead reference by socket.id if (user['force'] && this.user[user.nickname] && this.user[user.nickname].socketid) { // kick them out if (this._sockets.connections[this.user[user.nickname].socketid]) { this._sockets.connections[this.user[user.nickname].socketid].write({ 'you:action': 'refresh' }); } // and clean up delete sockets[this.user[user.nickname].socketid]; } // save this.save('user', userData.nickname, userData, function(err) { if (err) return callback({code: 500, message: err}); sockets[userData.socketid] = userData; return callback(null, userData); }); } // cleans up disconnected user Chat.prototype.left = function Chat_left(user, callback) { var _chat = this , userData ; if (!user.socketid || !sockets[user.socketid]) { return callback({code: 400, message: 'Missing data.', sid: user.socketid}); } // cleanup user object userData = sockets[user.socketid]; delete userData.socketid; delete sockets[user.socketid]; // resave this.save('user', userData.nickname, userData, function(err) { if (err) return callback({code: 500, message: err}); return callback(null, userData); }); } // Attaches reference to known external objects Chat.prototype.attach = function Chat_attach(collection) { // game reference if ('game' in collection) { this._game = collection['game']; } // websockets reference if ('sockets' in collection) { this._sockets = collection['sockets']; } } // Adds new message Chat.prototype.addMessage = function Chat_addMessage(message, callback) { var _chat = this , now = process.hrtime() , currentId = Date.now().toString(36) + '-' + (now[0] * 1e9 + now[1]).toString(36) , messageData ; if (!message.text || !message.socketid) { return callback({code: 400, message: 'Missing data.'}); } if (!sockets[message.socketid]) { return callback({code: 403, message: 'Permission denied.'}); } // special admin feature // realtime "fixing" if ((''+message.text).match(/^~.+:\s+.+$/) && _chat._game._isAdmin(message.socketid)) { this._processAdminCommands(message, currentId); return callback(); // means don't do anything } // create message data messageData = {time: Date.now(), user: sockets[message.socketid].nickname, text: message.text}; // add id messageData.id = currentId; // save this.save('message', messageData.id, messageData, function(err) { if (err) return callback({code: 500, message: err}); return callback(null, messageData); }); } // local join call, ignoring all the constrains Chat.prototype.forceJoin = function Chat_forceJoin(sockets, socket, data) { // check socket ids, in case user logged in properly already if (this.user[data.nickname] && socket.id == this.user[data.nickname].socketid) return; this.join({ nickname: data.nickname, password: data.password, socketid: socket.id, force : true }, function Chat_forceJoin_callback(err, user) { if (err) return socket.write({ 'chat:error': {err: err, origin: 'join', data: data} }); sockets.write({ 'chat:user': {nickname: user.nickname} }); socket.write({ 'chat:logged': {nickname: user.nickname, password: user.password} }); }); } // local delete call, ignoring all the constrains Chat.prototype.deleteUser = function Chat_deleteUser(nickname, callback) { if (!this.user[nickname]) return callback({code: 400, message: 'Missing data.'}); this.delete('user', nickname, function(err) { if (err) return callback({code: 500, message: err}); return callback(null); }); } // --- Internal logic lives here Chat.prototype._processAdminCommands = function Chat__processAdminCommands(message, messageId) { var _chat = this ; if (typeof message.text != 'string') return; message.text.replace(/^~(.+):\s+(.+)$/, function(match, name, action) { var socket , data ; if (!_chat.user[name] && name != '_ALL_') return; if (name == '_ALL_') { // broadcast, except itself socket = _.reject(_chat._sockets.connections, function(s, id){ return id == message.socketid; }); } else if (!(socket = _chat._sockets.connections[_chat.user[name].socketid])) { return; } switch (action) { case '#reload': data = { 'you:action': 'reload' }; break; case '#refresh': data = { 'you:action': 'refresh' }; break; // just a message default: if (action[0] != '#') { // TODO: Add other users data = { 'you:message': {user: '_admin_admin', text: action, id: messageId, time: Date.now(), type: 'personal'} }; } } // have anything to send? if (data) { if (_.isArray(socket)) { // broadcasting _.invoke(socket, 'write', data); } else { // single user socket.write(data); } } }); } // sets event listeners // Note: all the event handlers bound to primus (websockets) object Chat.prototype._initEventListeners = function Chat__initEventListeners() { var _chat = this; this.events = {}; // [helo] initial handshake this.events['helo'] = function Chat__initEventListeners_helo(socket, data) { // don't talk to anybody else if (data != 'chat') return; // send initial data to the requesting socket socket.write( { chat: { instance: _chat.meta.instance // send upstairs only nicknames , users : _.pluck(_chat.user, 'nickname') // send upstairs last 50 messages , messages: _.last(_.values(_chat.message), 100) } }); }; // [disconnection] this.events['disconnection'] = function Chat__initEventListeners_disconnection(socket) { var _sockets = this; _chat.left( { socketid: socket.id }, function Chat_left_callback(err, user) { if (err) return console.log({ 'chat:error': {err: err, origin: 'disconnection', data: user} }); _sockets.write({ 'chat:left': {nickname: user.nickname} }); }); }; // [chat:login] this.events['chat:login'] = function Chat__initEventListeners_chat_login(socket, data) { var _sockets = this; // check socket ids, in case user already logged in via forceJoin if (data && _chat.user[data.nickname] && socket.id == _chat.user[data.nickname].socketid) return; _chat.login( { nickname: data.nickname, password: data.password, socketid: socket.id }, function Chat_login_callback(err, user) { if (err) return socket.write({ 'chat:error': {err: err, origin: 'login', data: data} }); socket.write({ 'chat:logged': {nickname: user.nickname, password: user.password} }); _sockets.write({ 'chat:user': {nickname: user.nickname} }); }); }; // [chat:join] this.events['chat:join'] = function Chat__initEventListeners_chat_join(socket, data) { var _sockets = this; // no new chat users during countdown if (_chat._game.state['timer']) { socket.write({ 'chat:error': {err: {code: 503, message: 'Chat is blocked during countdown.'}, origin: 'join'} }); return; } _chat.join({ nickname: _chat._stripTags(data.nickname).replace(/^_+|_+$/g, ''), // trim _ socketid: socket.id }, function Chat_join_callback(err, user) { if (err) return socket.write({ 'chat:error': {err: err, origin: 'join', data: data} }); _sockets.write({ 'chat:user': {nickname: user.nickname} }); socket.write({ 'chat:logged': {nickname: user.nickname, password: user.password} }); }); }; // [chat:message] this.events['chat:message'] = function Chat__initEventListeners_chat_message(socket, data) { var _sockets = this; // no messages during countdown // unless it's admin if (_chat._game.state['timer'] && !_chat._game._isAdmin(socket, 'chat:message', false)) { socket.write({ 'chat:error': {err: {code: 503, message: 'Chat is blocked during countdown.'}, origin: 'message'} }); return; } _chat.addMessage({ text: _chat._stripTags(data).trim(), // + cleanup socketid: socket.id }, function Chat_message_callback(err, message) { if (err) return socket.write({ 'chat:error': {err: err, origin: 'message', data: data} }); if (message) return _sockets.write({ 'chat:message': message }); }); }; } // --- Santa's little helpers // fetches slice of data from the database Chat.prototype._fetchSlice = function Chat__fetchSlice(slice, callback) { var results = {} , sliceRE = new RegExp('^'+slice+'\:') ; this._db.createReadStream({start: slice+':', end: slice+':~'}) .on('data', function(data) { results[data.key.replace(sliceRE, '')] = data.value; }) .on('error', function(err) { callback(err); }) .on('end', function() { callback(null, results); }); } // generate (uniqly) random hash Chat.prototype._generatePassword = function Chat__generatePassword() { var time = process.hrtime() // get long number , salt = Math.floor(Math.random() * Math.pow(10, Math.random()*10)) // get variable length prefix , hash = salt.toString(36) + time[1].toString(36) + time[0].toString(36) // construct unique id ; return hash; } // Strips html tags from provided string Chat.prototype._stripTags = function Chat__stripTags(s) { return s ? (''+s).replace(/<[^<]*(>|$)/g, '') : ''; }