Add support of saving authors and authorship
This commit is contained in:
parent
44fd0a617b
commit
2f117a22cd
6 changed files with 240 additions and 3 deletions
28
lib/migrations/20160703062241-support-authorship.js
Normal file
28
lib/migrations/20160703062241-support-authorship.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
43
lib/models/author.js
Normal file
43
lib/models/author.js
Normal file
|
@ -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;
|
||||||
|
};
|
|
@ -51,6 +51,9 @@ module.exports = function (sequelize, DataTypes) {
|
||||||
content: {
|
content: {
|
||||||
type: DataTypes.TEXT
|
type: DataTypes.TEXT
|
||||||
},
|
},
|
||||||
|
authorship: {
|
||||||
|
type: DataTypes.TEXT
|
||||||
|
},
|
||||||
lastchangeAt: {
|
lastchangeAt: {
|
||||||
type: DataTypes.DATE
|
type: DataTypes.DATE
|
||||||
},
|
},
|
||||||
|
@ -74,6 +77,11 @@ module.exports = function (sequelize, DataTypes) {
|
||||||
foreignKey: "noteId",
|
foreignKey: "noteId",
|
||||||
constraints: false
|
constraints: false
|
||||||
});
|
});
|
||||||
|
Note.hasMany(models.Author, {
|
||||||
|
foreignKey: "noteId",
|
||||||
|
as: "authors",
|
||||||
|
constraints: false
|
||||||
|
});
|
||||||
},
|
},
|
||||||
checkFileExist: function (filePath) {
|
checkFileExist: function (filePath) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -30,6 +30,9 @@ module.exports = function (sequelize, DataTypes) {
|
||||||
},
|
},
|
||||||
length: {
|
length: {
|
||||||
type: DataTypes.INTEGER
|
type: DataTypes.INTEGER
|
||||||
|
},
|
||||||
|
authorship: {
|
||||||
|
type: DataTypes.TEXT
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
classMethods: {
|
classMethods: {
|
||||||
|
|
|
@ -10,7 +10,7 @@ var util = require('util');
|
||||||
var LZString = require('lz-string');
|
var LZString = require('lz-string');
|
||||||
var logger = require('../logger');
|
var logger = require('../logger');
|
||||||
|
|
||||||
function EditorSocketIOServer(document, operations, docId, mayWrite) {
|
function EditorSocketIOServer(document, operations, docId, mayWrite, operationCallback) {
|
||||||
EventEmitter.call(this);
|
EventEmitter.call(this);
|
||||||
Server.call(this, document, operations);
|
Server.call(this, document, operations);
|
||||||
this.users = {};
|
this.users = {};
|
||||||
|
@ -18,6 +18,7 @@ function EditorSocketIOServer(document, operations, docId, mayWrite) {
|
||||||
this.mayWrite = mayWrite || function (_, cb) {
|
this.mayWrite = mayWrite || function (_, cb) {
|
||||||
cb(true);
|
cb(true);
|
||||||
};
|
};
|
||||||
|
this.operationCallback = operationCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
util.inherits(EditorSocketIOServer, Server);
|
util.inherits(EditorSocketIOServer, Server);
|
||||||
|
@ -51,6 +52,8 @@ EditorSocketIOServer.prototype.addClient = function (socket) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
self.onOperation(socket, revision, operation, selection);
|
self.onOperation(socket, revision, operation, selection);
|
||||||
|
if (typeof self.operationCallback === 'function')
|
||||||
|
self.operationCallback(socket, operation);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
socket.disconnect(true);
|
socket.disconnect(true);
|
||||||
}
|
}
|
||||||
|
|
156
lib/realtime.js
156
lib/realtime.js
|
@ -151,6 +151,7 @@ function finishUpdateNote(note, _note, callback) {
|
||||||
var values = {
|
var values = {
|
||||||
title: title,
|
title: title,
|
||||||
content: body,
|
content: body,
|
||||||
|
authorship: LZString.compressToBase64(JSON.stringify(note.authorship)),
|
||||||
lastchangeuserId: note.lastchangeuser,
|
lastchangeuserId: note.lastchangeuser,
|
||||||
lastchangeAt: Date.now()
|
lastchangeAt: Date.now()
|
||||||
};
|
};
|
||||||
|
@ -404,6 +405,13 @@ function startConnection(socket) {
|
||||||
}, {
|
}, {
|
||||||
model: models.User,
|
model: models.User,
|
||||||
as: "lastchangeuser"
|
as: "lastchangeuser"
|
||||||
|
}, {
|
||||||
|
model: models.Author,
|
||||||
|
as: "authors",
|
||||||
|
include: [{
|
||||||
|
model: models.User,
|
||||||
|
as: "user"
|
||||||
|
}]
|
||||||
}];
|
}];
|
||||||
|
|
||||||
models.Note.findOne({
|
models.Note.findOne({
|
||||||
|
@ -424,7 +432,19 @@ function startConnection(socket) {
|
||||||
var body = LZString.decompressFromBase64(note.content);
|
var body = LZString.decompressFromBase64(note.content);
|
||||||
var createtime = note.createdAt;
|
var createtime = note.createdAt;
|
||||||
var updatetime = note.lastchangeAt;
|
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] = {
|
notes[noteId] = {
|
||||||
id: noteId,
|
id: noteId,
|
||||||
|
@ -437,7 +457,9 @@ function startConnection(socket) {
|
||||||
users: {},
|
users: {},
|
||||||
createtime: moment(createtime).valueOf(),
|
createtime: moment(createtime).valueOf(),
|
||||||
updatetime: moment(updatetime).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]);
|
return finishConnection(socket, notes[noteId], users[socket.id]);
|
||||||
|
@ -581,6 +603,136 @@ function ifMayEdit(socket, callback) {
|
||||||
return callback(mayEdit);
|
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) {
|
function connection(socket) {
|
||||||
if (config.maintenance) return;
|
if (config.maintenance) return;
|
||||||
parseNoteIdFromSocket(socket, function (err, noteId) {
|
parseNoteIdFromSocket(socket, function (err, noteId) {
|
||||||
|
|
Loading…
Reference in a new issue