Skip to content

Commit 2f117a2

Browse files
committed
Add support of saving authors and authorship
1 parent 44fd0a6 commit 2f117a2

6 files changed

Lines changed: 240 additions & 3 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
module.exports = {
4+
up: function (queryInterface, Sequelize) {
5+
queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT);
6+
queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT);
7+
queryInterface.createTable('Authors', {
8+
id: {
9+
type: Sequelize.INTEGER,
10+
primaryKey: true,
11+
autoIncrement: true
12+
},
13+
color: Sequelize.STRING,
14+
noteId: Sequelize.UUID,
15+
userId: Sequelize.UUID,
16+
createdAt: Sequelize.DATE,
17+
updatedAt: Sequelize.DATE
18+
});
19+
return;
20+
},
21+
22+
down: function (queryInterface, Sequelize) {
23+
queryInterface.dropTable('Authors');
24+
queryInterface.removeColumn('Revisions', 'authorship');
25+
queryInterface.removeColumn('Notes', 'authorship');
26+
return;
27+
}
28+
};

lib/models/author.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use strict";
2+
3+
// external modules
4+
var Sequelize = require("sequelize");
5+
6+
// core
7+
var logger = require("../logger.js");
8+
9+
module.exports = function (sequelize, DataTypes) {
10+
var Author = sequelize.define("Author", {
11+
id: {
12+
type: Sequelize.INTEGER,
13+
primaryKey: true,
14+
autoIncrement: true
15+
},
16+
color: {
17+
type: DataTypes.STRING
18+
}
19+
}, {
20+
indexes: [
21+
{
22+
unique: true,
23+
fields: ['noteId', 'userId']
24+
}
25+
],
26+
classMethods: {
27+
associate: function (models) {
28+
Author.belongsTo(models.Note, {
29+
foreignKey: "noteId",
30+
as: "note",
31+
constraints: false
32+
});
33+
Author.belongsTo(models.User, {
34+
foreignKey: "userId",
35+
as: "user",
36+
constraints: false
37+
});
38+
}
39+
}
40+
});
41+
42+
return Author;
43+
};

lib/models/note.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ module.exports = function (sequelize, DataTypes) {
5151
content: {
5252
type: DataTypes.TEXT
5353
},
54+
authorship: {
55+
type: DataTypes.TEXT
56+
},
5457
lastchangeAt: {
5558
type: DataTypes.DATE
5659
},
@@ -74,6 +77,11 @@ module.exports = function (sequelize, DataTypes) {
7477
foreignKey: "noteId",
7578
constraints: false
7679
});
80+
Note.hasMany(models.Author, {
81+
foreignKey: "noteId",
82+
as: "authors",
83+
constraints: false
84+
});
7785
},
7886
checkFileExist: function (filePath) {
7987
try {

lib/models/revision.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ module.exports = function (sequelize, DataTypes) {
3030
},
3131
length: {
3232
type: DataTypes.INTEGER
33+
},
34+
authorship: {
35+
type: DataTypes.TEXT
3336
}
3437
}, {
3538
classMethods: {

lib/ot/editor-socketio-server.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ var util = require('util');
1010
var LZString = require('lz-string');
1111
var logger = require('../logger');
1212

13-
function EditorSocketIOServer(document, operations, docId, mayWrite) {
13+
function EditorSocketIOServer(document, operations, docId, mayWrite, operationCallback) {
1414
EventEmitter.call(this);
1515
Server.call(this, document, operations);
1616
this.users = {};
1717
this.docId = docId;
1818
this.mayWrite = mayWrite || function (_, cb) {
1919
cb(true);
2020
};
21+
this.operationCallback = operationCallback;
2122
}
2223

2324
util.inherits(EditorSocketIOServer, Server);
@@ -51,6 +52,8 @@ EditorSocketIOServer.prototype.addClient = function (socket) {
5152
}
5253
try {
5354
self.onOperation(socket, revision, operation, selection);
55+
if (typeof self.operationCallback === 'function')
56+
self.operationCallback(socket, operation);
5457
} catch (err) {
5558
socket.disconnect(true);
5659
}

lib/realtime.js

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ function finishUpdateNote(note, _note, callback) {
151151
var values = {
152152
title: title,
153153
content: body,
154+
authorship: LZString.compressToBase64(JSON.stringify(note.authorship)),
154155
lastchangeuserId: note.lastchangeuser,
155156
lastchangeAt: Date.now()
156157
};
@@ -404,6 +405,13 @@ function startConnection(socket) {
404405
}, {
405406
model: models.User,
406407
as: "lastchangeuser"
408+
}, {
409+
model: models.Author,
410+
as: "authors",
411+
include: [{
412+
model: models.User,
413+
as: "user"
414+
}]
407415
}];
408416

409417
models.Note.findOne({
@@ -424,7 +432,19 @@ function startConnection(socket) {
424432
var body = LZString.decompressFromBase64(note.content);
425433
var createtime = note.createdAt;
426434
var updatetime = note.lastchangeAt;
427-
var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit);
435+
var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback);
436+
437+
var authors = {};
438+
for (var i = 0; i < note.authors.length; i++) {
439+
var author = note.authors[i];
440+
var profile = models.User.parseProfile(author.user.profile);
441+
authors[author.userId] = {
442+
userid: author.userId,
443+
color: author.color,
444+
photo: profile.photo,
445+
name: profile.name
446+
};
447+
}
428448

429449
notes[noteId] = {
430450
id: noteId,
@@ -437,7 +457,9 @@ function startConnection(socket) {
437457
users: {},
438458
createtime: moment(createtime).valueOf(),
439459
updatetime: moment(updatetime).valueOf(),
440-
server: server
460+
server: server,
461+
authors: authors,
462+
authorship: note.authorship ? JSON.parse(LZString.decompressFromBase64(note.authorship)) : []
441463
};
442464

443465
return finishConnection(socket, notes[noteId], users[socket.id]);
@@ -581,6 +603,136 @@ function ifMayEdit(socket, callback) {
581603
return callback(mayEdit);
582604
}
583605

606+
function operationCallback(socket, operation) {
607+
var noteId = socket.noteId;
608+
if (!noteId || !notes[noteId]) return;
609+
var note = notes[noteId];
610+
var userId = null;
611+
// save authors
612+
if (socket.request.user && socket.request.user.logged_in) {
613+
var socketId = socket.id;
614+
var user = users[socketId];
615+
userId = socket.request.user.id;
616+
if (!note.authors[userId]) {
617+
models.Author.create({
618+
noteId: noteId,
619+
userId: userId,
620+
color: users[socketId].color
621+
}).then(function (author) {
622+
note.authors[author.userId] = {
623+
userid: author.userId,
624+
color: author.color,
625+
photo: user.photo,
626+
name: user.name
627+
};
628+
}).catch(function (err) {
629+
return logger.error('operation callback failed: ' + err);
630+
});
631+
}
632+
}
633+
// save authorship
634+
var index = 0;
635+
var authorships = note.authorship;
636+
var timestamp = Date.now();
637+
for (var i = 0; i < operation.length; i++) {
638+
var op = operation[i];
639+
if (ot.TextOperation.isRetain(op)) {
640+
index += op;
641+
} else if (ot.TextOperation.isInsert(op)) {
642+
var opStart = index;
643+
var opEnd = index + op.length;
644+
var inserted = false;
645+
// authorship format: [userId, startPos, endPos, createdAt, updatedAt]
646+
if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]);
647+
else {
648+
for (var j = 0; j < authorships.length; j++) {
649+
var authorship = authorships[j];
650+
if (!inserted) {
651+
var nextAuthorship = authorships[j + 1] || -1;
652+
if (nextAuthorship != -1 && nextAuthorship[1] >= opEnd || j >= authorships.length - 1) {
653+
if (authorship[1] < opStart && authorship[2] > opStart) {
654+
// divide
655+
var postLength = authorship[2] - opStart;
656+
authorship[2] = opStart;
657+
authorship[4] = timestamp;
658+
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
659+
authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]);
660+
j += 2;
661+
inserted = true;
662+
} else if (authorship[1] >= opStart) {
663+
authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]);
664+
j += 1;
665+
inserted = true;
666+
} else if (authorship[2] <= opStart) {
667+
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
668+
j += 1;
669+
inserted = true;
670+
}
671+
}
672+
}
673+
if (authorship[1] >= opStart) {
674+
authorship[1] += op.length;
675+
authorship[2] += op.length;
676+
}
677+
}
678+
}
679+
index += op.length;
680+
} else if (ot.TextOperation.isDelete(op)) {
681+
var opStart = index;
682+
var opEnd = index - op;
683+
if (operation.length == 1) {
684+
authorships = [];
685+
} else if (authorships.length > 0) {
686+
for (var j = 0; j < authorships.length; j++) {
687+
var authorship = authorships[j];
688+
if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
689+
authorships.splice(j, 1);
690+
j -= 1;
691+
} else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) {
692+
authorship[2] += op;
693+
authorship[4] = timestamp;
694+
} else if (authorship[2] >= opStart && authorship[2] <= opEnd) {
695+
authorship[2] = opStart;
696+
authorship[4] = timestamp;
697+
} else if (authorship[1] >= opStart && authorship[1] <= opEnd) {
698+
authorship[1] = opEnd;
699+
authorship[4] = timestamp;
700+
}
701+
if (authorship[1] >= opEnd) {
702+
authorship[1] += op;
703+
authorship[2] += op;
704+
}
705+
}
706+
}
707+
index += op;
708+
}
709+
}
710+
// merge
711+
for (var j = 0; j < authorships.length; j++) {
712+
var authorship = authorships[j];
713+
for (var k = j + 1; k < authorships.length; k++) {
714+
var nextAuthorship = authorships[k];
715+
if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) {
716+
var minTimestamp = Math.min(authorship[3], nextAuthorship[3]);
717+
var maxTimestamp = Math.max(authorship[3], nextAuthorship[3]);
718+
authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]);
719+
authorships.splice(k, 1);
720+
j -= 1;
721+
break;
722+
}
723+
}
724+
}
725+
// clear
726+
for (var j = 0; j < authorships.length; j++) {
727+
var authorship = authorships[j];
728+
if (!authorship[0]) {
729+
authorships.splice(j, 1);
730+
j -= 1;
731+
}
732+
}
733+
note.authorship = authorships;
734+
}
735+
584736
function connection(socket) {
585737
if (config.maintenance) return;
586738
parseNoteIdFromSocket(socket, function (err, noteId) {

0 commit comments

Comments
 (0)