/** * The MessageHandler handles all Messages that comes from Socket.IO and controls the sessions */ /* * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka * * 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. */ var async = require("async"); var padManager = require("./PadManager"); var Changeset = require("./Changeset"); var AttributePoolFactory = require("./AttributePoolFactory"); var authorManager = require("./AuthorManager"); /** * Saves the Socket class we need to send and recieve data from the client */ var socketio; /** * This Method is called by server.js to tell the message handler on which socket it should send * @param socket_io The Socket */ exports.setSocketIO = function(socket_io) { socketio=socket_io; } /** * Handles the connection of a new user * @param client the new client */ exports.handleConnect = function(client) { } /** * Handles the disconnection of a user * @param client the client that leaves */ exports.handleDisconnect = function(client) { } /** * Handles a message from a user * @param client the client that send this message * @param message the message from the client */ exports.handleMessage = function(client, message) { //Check what type of message we get and delegate to the other methodes if(message.type == "CLIENT_READY") { handleClientReady(client, message); } else if(message.type == "CHANGESET_REQ") { handleChangesetRequest(client, message); } //if the message type is unkown, throw an exception else { throw "unkown Message Type: '" + message.type + "'"; } } function handleClientReady(client, message) { //send the timeslider client the clientVars, with this values its able to start createTimesliderClientVars (message.padId, function(err, clientVars) { if(err) throw err; client.json.send({type: "CLIENT_VARS", data: clientVars}); }) } /** * Handles a request for a rough changeset, the timeslider client needs it */ function handleChangesetRequest(client, message) { var granularity = message.data.granularity; var start = message.data.start; var end = start + (100 * granularity); var padId = message.padId; //build the requested rough changesets and send them back getChangesetInfo(padId, start, end, granularity, function(err, changesetInfo) { if(err) throw err; var data = changesetInfo; data.requestID = message.data.requestID; client.json.send({type: "CHANGESET_REQ", data: data}); }); } function createTimesliderClientVars (padId, callback) { var clientVars = { viewId: padId, colorPalette: ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd"], sliderEnabled : true, supportsSlider: true, savedRevisions: [], padIdForUrl: padId, fullWidth: false, disableRightBar: false, initialChangesets: [], hooks: [], initialStyledContents: {} }; var pad; var initialChangesets = []; async.series([ //get the pad from the database function(callback) { padManager.getPad(padId, function(err, _pad) { pad = _pad; callback(err); }); }, //get all authors and add them to function(callback) { var historicalAuthorData = {}; //get all authors out of the attribut pool var authors = pad.getAllAuthors(); //get all author data out of the database async.forEach(authors, function(authorId, callback) { authorManager.getAuthor(authorId, function(err, author) { historicalAuthorData[authorId] = author; callback(err); }); }, function(err) { //add historicalAuthorData to the clientVars and continue clientVars.historicalAuthorData = historicalAuthorData; clientVars.initialStyledContents.historicalAuthorData = historicalAuthorData; callback(err); }); }, //get the timestamp of the last revision function(callback) { pad.getRevisionDate(pad.getHeadRevisionNumber(), function(err, date) { clientVars.currentTime = date; callback(err); }); }, function(callback) { //get the head revision Number var lastRev = pad.getHeadRevisionNumber(); //add the revNum to the client Vars clientVars.revNum = lastRev; clientVars.totalRevs = lastRev; var atext = Changeset.cloneAText(pad.atext); var attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); var apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; clientVars.initialStyledContents.apool = apool; clientVars.initialStyledContents.atext = atext; var granularities = [100, 10, 1]; //get the latest rough changesets async.forEach(granularities, function(granularity, callback) { var topGranularity = granularity*10; getChangesetInfo(padId, Math.floor(lastRev / topGranularity)*topGranularity, Math.floor(lastRev / topGranularity)*topGranularity+topGranularity, granularity, function(err, changeset) { clientVars.initialChangesets.push(changeset); callback(err); }); }, callback); } ], function(err) { callback(err, clientVars); }); } /** * Tries to rebuild the getChangestInfo function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L144 */ function getChangesetInfo(padId, startNum, endNum, granularity, callback) { var forwardsChangesets = []; var backwardsChangesets = []; var timeDeltas = []; var apool = AttributePoolFactory.createAttributePool(); var pad; var composedChangesets = {}; var revisionDate = []; var lines; async.series([ //get the pad from the database function(callback) { padManager.getPad(padId, function(err, _pad) { pad = _pad; callback(err); }); }, function(callback) { //calculate the last full endnum var lastRev = pad.getHeadRevisionNumber(); if (endNum > lastRev+1) { endNum = lastRev+1; } endNum = Math.floor(endNum / granularity)*granularity; var compositesChangesetNeeded = []; var revTimesNeeded = []; //figure out which composite Changeset and revTimes we need, to load them in bulk var compositeStart = startNum; while (compositeStart < endNum) { var compositeEnd = compositeStart + granularity; //add the composite Changeset we needed compositesChangesetNeeded.push({start: compositeStart, end: compositeEnd}); //add the t1 time we need revTimesNeeded.push(compositeStart == 0 ? 0 : compositeStart - 1); //add the t2 time we need revTimesNeeded.push(compositeEnd - 1); compositeStart += granularity; } //get all needed db values parallel async.parallel([ function(callback) { //get all needed composite Changesets async.forEach(compositesChangesetNeeded, function(item, callback) { composePadChangesets(padId, item.start, item.end, function(err, changeset) { composedChangesets[item.start + "/" + item.end] = changeset; callback(err); }); }, callback); }, function(callback) { //get all needed revision Dates async.forEach(revTimesNeeded, function(revNum, callback) { pad.getRevisionDate(revNum, function(err, revDate) { revisionDate[revNum] = Math.floor(revDate/1000); callback(err); }); }, callback); }, //get the lines function(callback) { getPadLines(padId, startNum-1, function(err, _lines) { lines = _lines; callback(err); }); } ], callback); }, //doesn't know what happens here excatly :/ function(callback) { var compositeStart = startNum; while (compositeStart < endNum) { if (compositeStart + granularity > endNum) { break; } var compositeEnd = compositeStart + granularity; var forwards = composedChangesets[compositeStart + "/" + compositeEnd]; var backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); Changeset.mutateTextLines(forwards, lines.textlines); var forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); var backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); var t1, t2; if (compositeStart == 0) { t1 = revisionDate[0]; } else { t1 = revisionDate[compositeStart - 1]; } t2 = revisionDate[compositeEnd - 1]; timeDeltas.push(t2 - t1); forwardsChangesets.push(forwards2); backwardsChangesets.push(backwards2); compositeStart += granularity; } callback(); } ], function(err) { if(err) { callback(err); } else { callback(null, {forwardsChangesets: forwardsChangesets, backwardsChangesets: backwardsChangesets, apool: apool.toJsonable(), actualEndNum: endNum, timeDeltas: timeDeltas, start: startNum, granularity: granularity }); } }); } /** * Tries to rebuild the getPadLines function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L263 */ function getPadLines(padId, revNum, callback) { var atext; var result = {}; var pad; async.series([ //get the pad from the database function(callback) { padManager.getPad(padId, function(err, _pad) { pad = _pad; callback(err); }); }, //get the atext function(callback) { if(revNum >= 0) { pad.getInternalRevisionAText(revNum, function(err, _atext) { atext = _atext; callback(err); }); } else { atext = Changeset.makeAText("\n"); callback(null); } }, function(callback) { result.textlines = Changeset.splitTextLines(atext.text); result.alines = Changeset.splitAttributionLines(atext.attribs, atext.text); callback(null); } ], function(err) { callback(err, result); }); } /** * Tries to rebuild the composePadChangeset function of the original Etherpad * https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241 */ function composePadChangesets(padId, startNum, endNum, callback) { var pad; var changesets = []; var changeset; async.series([ //get the pad from the database function(callback) { padManager.getPad(padId, function(err, _pad) { pad = _pad; callback(err); }); }, //fetch all changesets we need function(callback) { var changesetsNeeded=[]; //create a array for all changesets, we will //replace the values with the changeset later for(var r=startNum;r