summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/migrations/20160703062241-support-authorship.js28
-rw-r--r--lib/models/author.js43
-rw-r--r--lib/models/note.js8
-rw-r--r--lib/models/revision.js3
-rwxr-xr-xlib/ot/editor-socketio-server.js5
-rw-r--r--lib/realtime.js156
6 files changed, 240 insertions, 3 deletions
diff --git a/lib/migrations/20160703062241-support-authorship.js b/lib/migrations/20160703062241-support-authorship.js
new file mode 100644
index 00000000..239327ec
--- /dev/null
+++ b/lib/migrations/20160703062241-support-authorship.js
@@ -0,0 +1,28 @@
+'use strict';
+
+module.exports = {
+ up: function (queryInterface, Sequelize) {
+ queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT);
+ queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT);
+ queryInterface.createTable('Authors', {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ color: Sequelize.STRING,
+ noteId: Sequelize.UUID,
+ userId: Sequelize.UUID,
+ createdAt: Sequelize.DATE,
+ updatedAt: Sequelize.DATE
+ });
+ return;
+ },
+
+ down: function (queryInterface, Sequelize) {
+ queryInterface.dropTable('Authors');
+ queryInterface.removeColumn('Revisions', 'authorship');
+ queryInterface.removeColumn('Notes', 'authorship');
+ return;
+ }
+};
diff --git a/lib/models/author.js b/lib/models/author.js
new file mode 100644
index 00000000..0b0f149d
--- /dev/null
+++ b/lib/models/author.js
@@ -0,0 +1,43 @@
+"use strict";
+
+// external modules
+var Sequelize = require("sequelize");
+
+// core
+var logger = require("../logger.js");
+
+module.exports = function (sequelize, DataTypes) {
+ var Author = sequelize.define("Author", {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true
+ },
+ color: {
+ type: DataTypes.STRING
+ }
+ }, {
+ indexes: [
+ {
+ unique: true,
+ fields: ['noteId', 'userId']
+ }
+ ],
+ classMethods: {
+ associate: function (models) {
+ Author.belongsTo(models.Note, {
+ foreignKey: "noteId",
+ as: "note",
+ constraints: false
+ });
+ Author.belongsTo(models.User, {
+ foreignKey: "userId",
+ as: "user",
+ constraints: false
+ });
+ }
+ }
+ });
+
+ return Author;
+}; \ No newline at end of file
diff --git a/lib/models/note.js b/lib/models/note.js
index f9a8ec61..5ee5c7de 100644
--- a/lib/models/note.js
+++ b/lib/models/note.js
@@ -51,6 +51,9 @@ module.exports = function (sequelize, DataTypes) {
content: {
type: DataTypes.TEXT
},
+ authorship: {
+ type: DataTypes.TEXT
+ },
lastchangeAt: {
type: DataTypes.DATE
},
@@ -74,6 +77,11 @@ module.exports = function (sequelize, DataTypes) {
foreignKey: "noteId",
constraints: false
});
+ Note.hasMany(models.Author, {
+ foreignKey: "noteId",
+ as: "authors",
+ constraints: false
+ });
},
checkFileExist: function (filePath) {
try {
diff --git a/lib/models/revision.js b/lib/models/revision.js
index bb89782f..f525ea55 100644
--- a/lib/models/revision.js
+++ b/lib/models/revision.js
@@ -30,6 +30,9 @@ module.exports = function (sequelize, DataTypes) {
},
length: {
type: DataTypes.INTEGER
+ },
+ authorship: {
+ type: DataTypes.TEXT
}
}, {
classMethods: {
diff --git a/lib/ot/editor-socketio-server.js b/lib/ot/editor-socketio-server.js
index 9e4ddf96..45ed5036 100755
--- a/lib/ot/editor-socketio-server.js
+++ b/lib/ot/editor-socketio-server.js
@@ -10,7 +10,7 @@ var util = require('util');
var LZString = require('lz-string');
var logger = require('../logger');
-function EditorSocketIOServer(document, operations, docId, mayWrite) {
+function EditorSocketIOServer(document, operations, docId, mayWrite, operationCallback) {
EventEmitter.call(this);
Server.call(this, document, operations);
this.users = {};
@@ -18,6 +18,7 @@ function EditorSocketIOServer(document, operations, docId, mayWrite) {
this.mayWrite = mayWrite || function (_, cb) {
cb(true);
};
+ this.operationCallback = operationCallback;
}
util.inherits(EditorSocketIOServer, Server);
@@ -51,6 +52,8 @@ EditorSocketIOServer.prototype.addClient = function (socket) {
}
try {
self.onOperation(socket, revision, operation, selection);
+ if (typeof self.operationCallback === 'function')
+ self.operationCallback(socket, operation);
} catch (err) {
socket.disconnect(true);
}
diff --git a/lib/realtime.js b/lib/realtime.js
index 0e9af740..68089570 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -151,6 +151,7 @@ function finishUpdateNote(note, _note, callback) {
var values = {
title: title,
content: body,
+ authorship: LZString.compressToBase64(JSON.stringify(note.authorship)),
lastchangeuserId: note.lastchangeuser,
lastchangeAt: Date.now()
};
@@ -404,6 +405,13 @@ function startConnection(socket) {
}, {
model: models.User,
as: "lastchangeuser"
+ }, {
+ model: models.Author,
+ as: "authors",
+ include: [{
+ model: models.User,
+ as: "user"
+ }]
}];
models.Note.findOne({
@@ -424,7 +432,19 @@ function startConnection(socket) {
var body = LZString.decompressFromBase64(note.content);
var createtime = note.createdAt;
var updatetime = note.lastchangeAt;
- var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit);
+ var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback);
+
+ var authors = {};
+ for (var i = 0; i < note.authors.length; i++) {
+ var author = note.authors[i];
+ var profile = models.User.parseProfile(author.user.profile);
+ authors[author.userId] = {
+ userid: author.userId,
+ color: author.color,
+ photo: profile.photo,
+ name: profile.name
+ };
+ }
notes[noteId] = {
id: noteId,
@@ -437,7 +457,9 @@ function startConnection(socket) {
users: {},
createtime: moment(createtime).valueOf(),
updatetime: moment(updatetime).valueOf(),
- server: server
+ server: server,
+ authors: authors,
+ authorship: note.authorship ? JSON.parse(LZString.decompressFromBase64(note.authorship)) : []
};
return finishConnection(socket, notes[noteId], users[socket.id]);
@@ -581,6 +603,136 @@ function ifMayEdit(socket, callback) {
return callback(mayEdit);
}
+function operationCallback(socket, operation) {
+ var noteId = socket.noteId;
+ if (!noteId || !notes[noteId]) return;
+ var note = notes[noteId];
+ var userId = null;
+ // save authors
+ if (socket.request.user && socket.request.user.logged_in) {
+ var socketId = socket.id;
+ var user = users[socketId];
+ userId = socket.request.user.id;
+ if (!note.authors[userId]) {
+ models.Author.create({
+ noteId: noteId,
+ userId: userId,
+ color: users[socketId].color
+ }).then(function (author) {
+ note.authors[author.userId] = {
+ userid: author.userId,
+ color: author.color,
+ photo: user.photo,
+ name: user.name
+ };
+ }).catch(function (err) {
+ return logger.error('operation callback failed: ' + err);
+ });
+ }
+ }
+ // save authorship
+ var index = 0;
+ var authorships = note.authorship;
+ var timestamp = Date.now();
+ for (var i = 0; i < operation.length; i++) {
+ var op = operation[i];
+ if (ot.TextOperation.isRetain(op)) {
+ index += op;
+ } else if (ot.TextOperation.isInsert(op)) {
+ var opStart = index;
+ var opEnd = index + op.length;
+ var inserted = false;
+ // authorship format: [userId, startPos, endPos, createdAt, updatedAt]
+ if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]);
+ else {
+ for (var j = 0; j < authorships.length; j++) {
+ var authorship = authorships[j];
+ if (!inserted) {
+ var nextAuthorship = authorships[j + 1] || -1;
+ if (nextAuthorship != -1 && nextAuthorship[1] >= opEnd || j >= authorships.length - 1) {
+ if (authorship[1] < opStart && authorship[2] > opStart) {
+ // divide
+ var postLength = authorship[2] - opStart;
+ authorship[2] = opStart;
+ authorship[4] = timestamp;
+ authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
+ authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]);
+ j += 2;
+ inserted = true;
+ } else if (authorship[1] >= opStart) {
+ authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]);
+ j += 1;
+ inserted = true;
+ } else if (authorship[2] <= opStart) {
+ authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
+ j += 1;
+ inserted = true;
+ }
+ }
+ }
+ if (authorship[1] >= opStart) {
+ authorship[1] += op.length;
+ authorship[2] += op.length;
+ }
+ }
+ }
+ index += op.length;
+ } else if (ot.TextOperation.isDelete(op)) {
+ var opStart = index;
+ var opEnd = index - op;
+ if (operation.length == 1) {
+ authorships = [];
+ } else if (authorships.length > 0) {
+ for (var j = 0; j < authorships.length; j++) {
+ var authorship = authorships[j];
+ if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
+ authorships.splice(j, 1);
+ j -= 1;
+ } else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) {
+ authorship[2] += op;
+ authorship[4] = timestamp;
+ } else if (authorship[2] >= opStart && authorship[2] <= opEnd) {
+ authorship[2] = opStart;
+ authorship[4] = timestamp;
+ } else if (authorship[1] >= opStart && authorship[1] <= opEnd) {
+ authorship[1] = opEnd;
+ authorship[4] = timestamp;
+ }
+ if (authorship[1] >= opEnd) {
+ authorship[1] += op;
+ authorship[2] += op;
+ }
+ }
+ }
+ index += op;
+ }
+ }
+ // merge
+ for (var j = 0; j < authorships.length; j++) {
+ var authorship = authorships[j];
+ for (var k = j + 1; k < authorships.length; k++) {
+ var nextAuthorship = authorships[k];
+ if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) {
+ var minTimestamp = Math.min(authorship[3], nextAuthorship[3]);
+ var maxTimestamp = Math.max(authorship[3], nextAuthorship[3]);
+ authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]);
+ authorships.splice(k, 1);
+ j -= 1;
+ break;
+ }
+ }
+ }
+ // clear
+ for (var j = 0; j < authorships.length; j++) {
+ var authorship = authorships[j];
+ if (!authorship[0]) {
+ authorships.splice(j, 1);
+ j -= 1;
+ }
+ }
+ note.authorship = authorships;
+}
+
function connection(socket) {
if (config.maintenance) return;
parseNoteIdFromSocket(socket, function (err, noteId) {