diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index f8d333e67a..a7aa7a3ab0 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -16,6 +16,7 @@ require('./topics/move')(SocketTopics); require('./topics/tools')(SocketTopics); require('./topics/infinitescroll')(SocketTopics); require('./topics/tags')(SocketTopics); +require('./topics/merge')(SocketTopics); SocketTopics.post = function (socket, data, callback) { if (!data) { diff --git a/src/socket.io/topics/merge.js b/src/socket.io/topics/merge.js new file mode 100644 index 0000000000..54275606e6 --- /dev/null +++ b/src/socket.io/topics/merge.js @@ -0,0 +1,27 @@ +'use strict'; + +var async = require('async'); +var topics = require('../../topics'); +var privileges = require('../../privileges'); + +module.exports = function (SocketTopics) { + SocketTopics.merge = function (socket, tids, callback) { + if (!Array.isArray(tids)) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + async.map(tids, function (tid, next) { + privileges.topics.isAdminOrMod(tid, socket.uid, next); + }, next); + }, + function (allowed, next) { + if (allowed.includes(false)) { + return next(new Error('[[error:no-privileges]]')); + } + topics.merge(tids, next); + }, + ], callback); + }; +}; diff --git a/src/topics.js b/src/topics.js index 5f744316c0..44c263210d 100644 --- a/src/topics.js +++ b/src/topics.js @@ -31,6 +31,7 @@ require('./topics/suggested')(Topics); require('./topics/tools')(Topics); require('./topics/thumb')(Topics); require('./topics/bookmarks')(Topics); +require('./topics/merge')(Topics); Topics.exists = function (tid, callback) { db.isSortedSetMember('topics:tid', tid, callback); @@ -252,7 +253,7 @@ function getMainPostAndReplies(topic, set, uid, start, stop, reverse, callback) return callback(null, []); } - if (topic.mainPid && start === 0) { + if (parseInt(topic.mainPid, 10) && start === 0) { pids.unshift(topic.mainPid); } posts.getPostsByPids(pids, uid, next); diff --git a/src/topics/merge.js b/src/topics/merge.js new file mode 100644 index 0000000000..2c1df0df61 --- /dev/null +++ b/src/topics/merge.js @@ -0,0 +1,33 @@ +'use strict'; + +var async = require('async'); + +module.exports = function (Topics) { + Topics.merge = function (tids, callback) { + var mergeIntoTid = findOldestTopic(tids); + + var otherTids = tids.filter(function (tid) { + return tid && parseInt(tid, 10) !== parseInt(mergeIntoTid, 10); + }); + + async.eachSeries(otherTids, function (tid, next) { + async.waterfall([ + function (next) { + Topics.getPids(tid, next); + }, + function (pids, next) { + async.eachSeries(pids, function (pid, next) { + Topics.movePostToTopic(pid, mergeIntoTid, next); + }, next); + }, + function (next) { + Topics.setTopicField(tid, 'mainPid', 0, next); + }, + ], next); + }, callback); + }; + + function findOldestTopic(tids) { + return Math.min.apply(null, tids); + } +}; diff --git a/src/topics/posts.js b/src/topics/posts.js index ebfbd106c0..9816d8178f 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -326,7 +326,7 @@ module.exports = function (Topics) { }, next); }, function (results, next) { - if (results.mainPid) { + if (parseInt(results.mainPid, 10)) { results.pids = [results.mainPid].concat(results.pids); } next(null, results.pids); diff --git a/test/topics.js b/test/topics.js index e6c341cb9e..15b839f996 100644 --- a/test/topics.js +++ b/test/topics.js @@ -1699,4 +1699,89 @@ describe('Topic\'s', function () { }); }); }); + + describe('topic merge', function (done) { + var uid; + var topic1Data; + var topic2Data; + + before(function (done) { + async.waterfall([ + function (next) { + User.create({ username: 'mergevictim' }, next); + }, + function (_uid, next) { + uid = _uid; + topics.post({ uid: uid, cid: categoryObj.cid, title: 'topic 1', content: 'topic 1 OP' }, next); + }, + function (result, next) { + topic1Data = result.topicData; + topics.post({ uid: uid, cid: categoryObj.cid, title: 'topic 2', content: 'topic 2 OP' }, next); + }, + function (result, next) { + topic2Data = result.topicData; + topics.reply({ uid: uid, content: 'topic 1 reply', tid: topic1Data.tid }, next); + }, + function (postData, next) { + topics.reply({ uid: uid, content: 'topic 2 reply', tid: topic2Data.tid }, next); + }, + ], done); + }); + + it('should error if data is not an array', function (done) { + socketTopics.merge({ uid: 0 }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should error if user does not have privileges', function (done) { + socketTopics.merge({ uid: 0 }, [topic2Data.tid, topic1Data.tid], function (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + done(); + }); + }); + + it('should merge 2 topics', function (done) { + async.waterfall([ + function (next) { + socketTopics.merge({ uid: adminUid }, [topic2Data.tid, topic1Data.tid], next); + }, + function (next) { + async.parallel({ + topic1: function (next) { + async.waterfall([ + function (next) { + topics.getTopicData(topic1Data.tid, next); + }, + function (topicData, next) { + topics.getTopicWithPosts(topicData, 'tid:' + topicData.tid + ':posts', adminUid, 0, 19, false, next); + }, + ], next); + }, + topic2: function (next) { + async.waterfall([ + function (next) { + topics.getTopicData(topic2Data.tid, next); + }, + function (topicData, next) { + topics.getTopicWithPosts(topicData, 'tid:' + topicData.tid + ':posts', adminUid, 0, 19, false, next); + }, + ], next); + }, + }, next); + }, + function (results, next) { + assert.equal(results.topic1.posts.length, 4); + assert.equal(results.topic2.posts.length, 0); + + assert.equal(results.topic1.posts[0].content, 'topic 1 OP'); + assert.equal(results.topic1.posts[1].content, 'topic 2 OP'); + assert.equal(results.topic1.posts[2].content, 'topic 1 reply'); + assert.equal(results.topic1.posts[3].content, 'topic 2 reply'); + done(); + }, + ], done); + }); + }); });