'use strict'; var async = require('async'); var _ = require('lodash'); var validator = require('validator'); var db = require('../database'); var user = require('../user'); var posts = require('../posts'); var meta = require('../meta'); var plugins = require('../plugins'); var utils = require('../../public/src/utils'); module.exports = function (Topics) { Topics.onNewPostMade = function (postData, callback) { async.series([ function (next) { Topics.increasePostCount(postData.tid, next); }, function (next) { Topics.updateTimestamp(postData.tid, postData.timestamp, next); }, function (next) { Topics.addPostToTopic(postData.tid, postData, next); }, ], callback); }; Topics.getTopicPosts = function (tid, set, start, stop, uid, reverse, callback) { async.waterfall([ function (next) { async.parallel({ posts: function (next) { posts.getPostsFromSet(set, start, stop, uid, reverse, next); }, postCount: function (next) { Topics.getTopicField(tid, 'postcount', next); }, }, next); }, function (results, next) { Topics.calculatePostIndices(results.posts, start, stop, results.postCount, reverse); Topics.addPostData(results.posts, uid, next); }, ], callback); }; Topics.addPostData = function (postData, uid, callback) { if (!Array.isArray(postData) || !postData.length) { return callback(null, []); } var pids = postData.map(function (post) { return post && post.pid; }); if (!Array.isArray(pids) || !pids.length) { return callback(null, []); } function getPostUserData(field, method, callback) { var uids = []; postData.forEach(function (postData) { if (postData && parseInt(postData[field], 10) >= 0 && uids.indexOf(postData[field]) === -1) { uids.push(postData[field]); } }); async.waterfall([ function (next) { method(uids, next); }, function (users, next) { var userData = {}; users.forEach(function (user, index) { userData[uids[index]] = user; }); next(null, userData); }, ], callback); } async.waterfall([ function (next) { async.parallel({ bookmarks: function (next) { posts.hasBookmarked(pids, uid, next); }, voteData: function (next) { posts.getVoteStatusByPostIDs(pids, uid, next); }, userData: function (next) { getPostUserData('uid', function (uids, next) { posts.getUserInfoForPosts(uids, uid, next); }, next); }, editors: function (next) { getPostUserData('editor', function (uids, next) { user.getUsersFields(uids, ['uid', 'username', 'userslug'], next); }, next); }, parents: function (next) { Topics.addParentPosts(postData, next); }, replies: function (next) { getPostReplies(pids, uid, next); }, }, next); }, function (results, next) { postData.forEach(function (postObj, i) { if (postObj) { postObj.deleted = parseInt(postObj.deleted, 10) === 1; postObj.user = parseInt(postObj.uid, 10) ? results.userData[postObj.uid] : _.clone(results.userData[postObj.uid]); postObj.editor = postObj.editor ? results.editors[postObj.editor] : null; postObj.bookmarked = results.bookmarks[i]; postObj.upvoted = results.voteData.upvotes[i]; postObj.downvoted = results.voteData.downvotes[i]; postObj.votes = postObj.votes || 0; postObj.replies = results.replies[i]; postObj.selfPost = !!parseInt(uid, 10) && parseInt(uid, 10) === parseInt(postObj.uid, 10); // Username override for guests, if enabled if (parseInt(meta.config.allowGuestHandles, 10) === 1 && parseInt(postObj.uid, 10) === 0 && postObj.handle) { postObj.user.username = validator.escape(String(postObj.handle)); } } }); plugins.fireHook('filter:topics.addPostData', { posts: postData, uid: uid, }, next); }, function (data, next) { next(null, data.posts); }, ], callback); }; Topics.modifyPostsByPrivilege = function (topicData, topicPrivileges) { var loggedIn = !!parseInt(topicPrivileges.uid, 10); topicData.posts.forEach(function (post) { if (post) { post.display_edit_tools = topicPrivileges.isAdminOrMod || (post.selfPost && topicPrivileges['posts:edit']); post.display_delete_tools = topicPrivileges.isAdminOrMod || (post.selfPost && topicPrivileges['posts:delete']); post.display_moderator_tools = post.display_edit_tools || post.display_delete_tools; post.display_move_tools = topicPrivileges.isAdminOrMod && post.index !== 0; post.display_post_menu = topicPrivileges.isAdminOrMod || (post.selfPost && !topicData.locked) || ((loggedIn || topicData.postSharing.length) && !post.deleted); post.ip = topicPrivileges.isAdminOrMod ? post.ip : undefined; posts.modifyPostByPrivilege(post, topicPrivileges); } }); }; Topics.addParentPosts = function (postData, callback) { var parentPids = postData.map(function (postObj) { return postObj && postObj.hasOwnProperty('toPid') ? parseInt(postObj.toPid, 10) : null; }).filter(Boolean); if (!parentPids.length) { return callback(); } var parentPosts; async.waterfall([ async.apply(posts.getPostsFields, parentPids, ['uid']), function (_parentPosts, next) { parentPosts = _parentPosts; var parentUids = _.uniq(parentPosts.map(function (postObj) { return postObj && parseInt(postObj.uid, 10); })); user.getUsersFields(parentUids, ['username'], next); }, function (userData, next) { var usersMap = {}; userData.forEach(function (user) { usersMap[user.uid] = user.username; }); var parents = {}; parentPosts.forEach(function (post, i) { parents[parentPids[i]] = { username: usersMap[post.uid] }; }); postData.forEach(function (post) { post.parent = parents[post.toPid]; }); next(); }, ], callback); }; Topics.calculatePostIndices = function (posts, start, stop, postCount, reverse) { posts.forEach(function (post, index) { if (reverse) { post.index = postCount - (start + index + 1); } else { post.index = start + index + 1; } }); }; Topics.getLatestUndeletedPid = function (tid, callback) { async.waterfall([ function (next) { Topics.getLatestUndeletedReply(tid, next); }, function (pid, next) { if (parseInt(pid, 10)) { return callback(null, pid.toString()); } Topics.getTopicField(tid, 'mainPid', next); }, function (mainPid, next) { posts.getPostFields(mainPid, ['pid', 'deleted'], next); }, function (mainPost, next) { next(null, parseInt(mainPost.pid, 10) && parseInt(mainPost.deleted, 10) !== 1 ? mainPost.pid.toString() : null); }, ], callback); }; Topics.getLatestUndeletedReply = function (tid, callback) { var isDeleted = false; var done = false; var latestPid = null; var index = 0; var pids; async.doWhilst( function (next) { async.waterfall([ function (_next) { db.getSortedSetRevRange('tid:' + tid + ':posts', index, index, _next); }, function (_pids, _next) { pids = _pids; if (!pids.length) { done = true; return next(); } posts.getPostField(pids[0], 'deleted', _next); }, function (deleted, _next) { isDeleted = parseInt(deleted, 10) === 1; if (!isDeleted) { latestPid = pids[0]; } index += 1; _next(); }, ], next); }, function () { return isDeleted && !done; }, function (err) { callback(err, latestPid); } ); }; Topics.addPostToTopic = function (tid, postData, callback) { async.waterfall([ function (next) { Topics.getTopicField(tid, 'mainPid', next); }, function (mainPid, next) { if (!parseInt(mainPid, 10)) { Topics.setTopicField(tid, 'mainPid', postData.pid, next); } else { async.parallel([ function (next) { db.sortedSetAdd('tid:' + tid + ':posts', postData.timestamp, postData.pid, next); }, function (next) { var upvotes = parseInt(postData.upvotes, 10) || 0; var downvotes = parseInt(postData.downvotes, 10) || 0; var votes = upvotes - downvotes; db.sortedSetAdd('tid:' + tid + ':posts:votes', votes, postData.pid, next); }, ], function (err) { next(err); }); } }, function (next) { db.sortedSetIncrBy('tid:' + tid + ':posters', 1, postData.uid, next); }, function (count, next) { Topics.updateTeaser(tid, next); }, ], callback); }; Topics.removePostFromTopic = function (tid, postData, callback) { async.waterfall([ function (next) { db.sortedSetsRemove([ 'tid:' + tid + ':posts', 'tid:' + tid + ':posts:votes', ], postData.pid, next); }, function (next) { db.sortedSetIncrBy('tid:' + tid + ':posters', -1, postData.uid, next); }, function (count, next) { Topics.updateTeaser(tid, next); }, ], callback); }; Topics.getPids = function (tid, callback) { async.waterfall([ function (next) { async.parallel({ mainPid: function (next) { Topics.getTopicField(tid, 'mainPid', next); }, pids: function (next) { db.getSortedSetRange('tid:' + tid + ':posts', 0, -1, next); }, }, next); }, function (results, next) { if (parseInt(results.mainPid, 10)) { results.pids = [results.mainPid].concat(results.pids); } next(null, results.pids); }, ], callback); }; Topics.increasePostCount = function (tid, callback) { incrementFieldAndUpdateSortedSet(tid, 'postcount', 1, 'topics:posts', callback); }; Topics.decreasePostCount = function (tid, callback) { incrementFieldAndUpdateSortedSet(tid, 'postcount', -1, 'topics:posts', callback); }; Topics.increaseViewCount = function (tid, callback) { incrementFieldAndUpdateSortedSet(tid, 'viewcount', 1, 'topics:views', callback); }; function incrementFieldAndUpdateSortedSet(tid, field, by, set, callback) { callback = callback || function () {}; async.waterfall([ function (next) { db.incrObjectFieldBy('topic:' + tid, field, by, next); }, function (value, next) { db.sortedSetAdd(set, value, tid, next); }, ], callback); } Topics.getTitleByPid = function (pid, callback) { Topics.getTopicFieldByPid('title', pid, callback); }; Topics.getTopicFieldByPid = function (field, pid, callback) { async.waterfall([ function (next) { posts.getPostField(pid, 'tid', next); }, function (tid, next) { Topics.getTopicField(tid, field, next); }, ], callback); }; Topics.getTopicDataByPid = function (pid, callback) { async.waterfall([ function (next) { posts.getPostField(pid, 'tid', next); }, function (tid, next) { Topics.getTopicData(tid, next); }, ], callback); }; Topics.getPostCount = function (tid, callback) { db.getObjectField('topic:' + tid, 'postcount', callback); }; function getPostReplies(pids, callerUid, callback) { var arrayOfReplyPids; var replyData; var uniqueUids; var uniquePids; async.waterfall([ function (next) { var keys = pids.map(function (pid) { return 'pid:' + pid + ':replies'; }); db.getSortedSetsMembers(keys, next); }, function (arrayOfPids, next) { arrayOfReplyPids = arrayOfPids; uniquePids = _.uniq(_.flatten(arrayOfPids)); posts.getPostsFields(uniquePids, ['pid', 'uid', 'timestamp'], next); }, function (_replyData, next) { replyData = _replyData; var uids = replyData.map(function (replyData) { return replyData && replyData.uid; }); uniqueUids = _.uniq(uids); user.getUsersWithFields(uniqueUids, ['uid', 'username', 'userslug', 'picture'], callerUid, next); }, function (userData, next) { var uidMap = _.zipObject(uniqueUids, userData); var pidMap = _.zipObject(uniquePids, replyData); var returnData = arrayOfReplyPids.map(function (replyPids) { var uidsUsed = {}; var currentData = { hasMore: false, users: [], text: replyPids.length > 1 ? '[[topic:replies_to_this_post, ' + replyPids.length + ']]' : '[[topic:one_reply_to_this_post]]', count: replyPids.length, timestampISO: replyPids.length ? utils.toISOString(pidMap[replyPids[0]].timestamp) : undefined, }; replyPids.sort(function (a, b) { return parseInt(a, 10) - parseInt(b, 10); }); replyPids.forEach(function (replyPid) { var replyData = pidMap[replyPid]; if (!uidsUsed[replyData.uid] && currentData.users.length < 6) { currentData.users.push(uidMap[replyData.uid]); uidsUsed[replyData.uid] = true; } }); if (currentData.users.length > 5) { currentData.users.pop(); currentData.hasMore = true; } return currentData; }); next(null, returnData); }, ], callback); } };