diff --git a/install/package.json b/install/package.json index 99411a5c3d..2dffc94228 100644 --- a/install/package.json +++ b/install/package.json @@ -39,6 +39,7 @@ "cropperjs": "^1.2.2", "csurf": "^1.9.0", "daemon": "^1.1.0", + "diff": "^3.4.0", "express": "^4.16.2", "express-session": "^1.15.6", "express-useragent": "1.0.8", diff --git a/src/posts.js b/src/posts.js index cf30bd4c64..6061017b89 100644 --- a/src/posts.js +++ b/src/posts.js @@ -25,6 +25,7 @@ require('./posts/tools')(Posts); require('./posts/votes')(Posts); require('./posts/bookmarks')(Posts); require('./posts/queue')(Posts); +require('./posts/diffs')(Posts); Posts.exists = function (pid, callback) { db.isSortedSetMember('posts:pid', pid, callback); diff --git a/src/posts/diffs.js b/src/posts/diffs.js new file mode 100644 index 0000000000..60e9510f92 --- /dev/null +++ b/src/posts/diffs.js @@ -0,0 +1,51 @@ +'use strict'; + +var async = require('async'); + +var db = require('../database'); +var diff = require('diff'); + +module.exports = function (Posts) { + Posts.diffs = {}; + + Posts.diffs.list = function (pid, callback) { + db.getSortedSetRangeWithScores('post:' + pid + ':diffs', 0, -1, function (err, diffs) { + callback(err, diffs ? diffs.map(function (diffObj) { + return diffObj.score; + }) : null); + }); + }; + + Posts.diffs.save = function (pid, oldContent, newContent, callback) { + db.sortedSetAdd('post:' + pid + ':diffs', Date.now(), diff.createPatch('', newContent, oldContent), callback); + }; + + Posts.diffs.load = function (pid, since, callback) { + // Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since` + since = parseInt(since, 10); + + if (isNaN(since) || since > Date.now()) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.parallel({ + post: async.apply(Posts.getPostData, pid), + diffs: async.apply(db.getSortedSetRangeByScore.bind(db), 'post:' + pid + ':diffs', 0, -1, since, Date.now()), + }, function (err, data) { + if (err) { + return callback(err); + } + + // Replace content with re-constructed content from that point in time + data.post.content = data.diffs.reverse().reduce(function (content, diffString) { + return diff.applyPatch(content, diffString); + }, data.post.content); + + // Clear editor data (as it is outdated for this content) + delete data.post.edited; + data.post.editor = null; + + return callback(null, data.post); + }); + }; +}; diff --git a/src/posts/edit.js b/src/posts/edit.js index 8780fa6016..2ca8e3b534 100644 --- a/src/posts/edit.js +++ b/src/posts/edit.js @@ -20,6 +20,7 @@ module.exports = function (Posts) { }); Posts.edit = function (data, callback) { + var oldContent; // for diffing purposes var postData; var results; @@ -39,6 +40,7 @@ module.exports = function (Posts) { } postData = _postData; + oldContent = postData.content; postData.content = data.content; postData.edited = Date.now(); postData.editor = data.uid; @@ -63,6 +65,9 @@ module.exports = function (Posts) { results = _results; Posts.setPostFields(data.pid, postData, next); }, + function (next) { + Posts.diffs.save(data.pid, oldContent, data.content, next); + }, function (next) { postData.cid = results.topic.cid; postData.topic = results.topic; diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index 39dd1fe64e..e0b7884ece 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -20,6 +20,7 @@ require('./posts/move')(SocketPosts); require('./posts/votes')(SocketPosts); require('./posts/bookmarks')(SocketPosts); require('./posts/tools')(SocketPosts); +require('./posts/diffs')(SocketPosts); SocketPosts.reply = function (socket, data, callback) { if (!data || !data.tid || (parseInt(meta.config.minimumPostLength, 10) !== 0 && !data.content)) { diff --git a/src/socket.io/posts/diffs.js b/src/socket.io/posts/diffs.js new file mode 100644 index 0000000000..3ce7542399 --- /dev/null +++ b/src/socket.io/posts/diffs.js @@ -0,0 +1,13 @@ +'use strict'; + +var posts = require('../../posts'); + +module.exports = function (SocketPosts) { + SocketPosts.getDiffs = function (socket, data, callback) { + posts.diffs.list(data.pid, callback); + }; + + SocketPosts.showPostAt = function (socket, data, callback) { + posts.diffs.load(data.pid, data.since, callback); + }; +};