var async = require('async'), gravatar = require('gravatar'), nconf = require('nconf'), validator = require('validator'), S = require('string'), winston = require('winston'), db = require('./database'), posts = require('./posts'), utils = require('./../public/src/utils'), user = require('./user'), categories = require('./categories'), categoryTools = require('./categoryTools'), posts = require('./posts'), threadTools = require('./threadTools'), postTools = require('./postTools'), notifications = require('./notifications'), feed = require('./feed'), favourites = require('./favourites'), meta = require('./meta'); (function(Topics) { Topics.create = function(uid, title, cid, callback) { db.incrObjectField('global', 'nextTid', function(err, tid) { if(err) { return callback(err); } db.setAdd('topics:tid', tid); var slug = tid + '/' + utils.slugify(title), timestamp = Date.now(); db.setObject('topic:' + tid, { 'tid': tid, 'uid': uid, 'cid': cid, 'title': title, 'slug': slug, 'timestamp': timestamp, 'lastposttime': 0, 'postcount': 0, 'viewcount': 0, 'locked': 0, 'deleted': 0, 'pinned': 0 }, function(err) { if(err) { return callback(err); } db.searchIndex('topic', title, tid); user.addTopicIdToUser(uid, tid, timestamp); db.sortedSetAdd('categories:' + cid + ':tid', timestamp, tid); db.incrObjectField('category:' + cid, 'topic_count'); db.incrObjectField('global', 'topicCount'); feed.updateCategory(cid); callback(null, tid); }); }); }; Topics.post = function(uid, title, content, cid, callback) { if (title) { title = title.trim(); } if (!title || title.length < parseInt(meta.config.minimumTitleLength, 10)) { return callback(new Error('title-too-short')); } else if(title.length > parseInt(meta.config.maximumTitleLength, 10)) { return callback(new Error('title-too-long')); } if (content) { content = content.trim(); } if (!content || content.length < meta.config.miminumPostLength) { return callback(new Error('content-too-short')); } if (!cid) { return callback(new Error('invalid-cid')); } async.waterfall([ function(next) { categoryTools.privileges(cid, uid, next); }, function(privileges, next) { if(!privileges.write) { return next(new Error('no-privileges')); } next(); }, function(next) { user.isReadyToPost(uid, next); }, function(next) { Topics.create(uid, title, cid, next); }, function(tid, next) { Topics.reply(tid, uid, content, next); }, function(postData, next) { threadTools.toggleFollow(postData.tid, uid); next(null, postData); }, function(postData, next) { Topics.getTopicForCategoryView(postData.tid, uid, function(err, topicData) { if(err) { return next(err); } topicData.unreplied = 1; next(null, { topicData: topicData, postData: postData }); }); } ], callback); }; Topics.reply = function(tid, uid, content, callback) { var privileges; async.waterfall([ function(next) { threadTools.privileges(tid, uid, next); }, function(privilegesData, next) { privileges = privilegesData; if (!privileges.write) { return next(new Error('no-privileges')); } next(); }, function(next) { user.isReadyToPost(uid, next); }, function(next) { if (content) { content = content.trim(); } if (!content || content.length < meta.config.minimumPostLength) { return next(new Error('content-too-short')); } posts.create(uid, tid, content, next); }, function(postData, next) { db.getObjectField('tid:lastFeedUpdate', tid, function(err, lastFeedUpdate) { var now = Date.now(); if(!lastFeedUpdate || parseInt(lastFeedUpdate, 10) < now - 3600000) { feed.updateTopic(tid); db.setObjectField('tid:lastFeedUpdate', tid, now); } }); feed.updateRecent(); threadTools.notifyFollowers(tid, uid); user.sendPostNotificationToFollowers(uid, tid, postData.pid); Topics.markCategoryUnreadForAll(tid, function(err) { next(err, postData); }); }, function(postData, next) { Topics.markAsUnreadForAll(tid, function(err) { if(err) { return next(err); } Topics.markAsRead(tid, uid, function(err) { Topics.pushUnreadCount(null); next(err, postData); }); }); }, function(postData, next) { posts.addUserInfoToPost(postData, next); }, function(postData, next) { postData.favourited = false; postData.display_moderator_tools = true; postData.display_move_tools = privileges.admin || privileges.moderator; postData.relativeTime = utils.toISOString(postData.timestamp); next(null, postData); } ], callback); } Topics.createTopicFromPosts = function(uid, title, pids, callback) { if(title) { title = title.trim(); } if(!title) { return callback(new Error('invalid-title')); } if(!pids || !pids.length) { return callback(new Error('invalid-pids')); } pids.sort(); var mainPid = pids[0]; async.parallel({ postData: function(callback) { posts.getPostData(mainPid, callback); }, cid: function(callback) { posts.getCidByPid(mainPid, callback); } }, function(err, results) { Topics.create(results.postData.uid, title, results.cid, function(err, tid) { if(err) { return callback(err); } async.eachSeries(pids, move, function(err) { if(err) { return callback(err); } Topics.getTopicData(tid, callback); }); function move(pid, next) { postTools.privileges(pid, uid, function(err, privileges) { if(err) { return next(err); } if(privileges.editable) { Topics.movePostToTopic(pid, tid, next); } else { next(); } }); } }); }); } Topics.movePostToTopic = function(pid, tid, callback) { threadTools.exists(tid, function(exists) { if(!exists) { return callback(new Error('Topic doesn\'t exist')); } posts.getPostFields(pid, ['tid', 'timestamp'], function(err, postData) { if(err) { return callback(err); } if(!postData) { return callback(new Error('Post doesn\'t exist')); } Topics.removePostFromTopic(postData.tid, pid, function(err) { if(err) { return callback(err); } Topics.decreasePostCount(postData.tid); posts.setPostField(pid, 'tid', tid); Topics.increasePostCount(tid); Topics.addPostToTopic(tid, pid, postData.timestamp, callback); }); }); }); } Topics.getTopicData = function(tid, callback) { db.getObject('topic:' + tid, function(err, data) { if(err) { return callback(err, null); } if(data) { data.title = validator.sanitize(data.title).escape(); data.relativeTime = utils.toISOString(data.timestamp); } callback(null, data); }); } Topics.getTopicDataWithUser = function(tid, callback) { Topics.getTopicData(tid, function(err, topic) { if(err) { return callback(err, null); } user.getUserFields(topic.uid, ['username', 'userslug', 'picture'] , function(err, userData) { if(err) { return callback(err, null); } topic.username = userData.username; topic.userslug = userData.userslug topic.picture = userData.picture; callback(null, topic); }); }); } Topics.getTopicPosts = function(tid, start, end, current_user, callback) { posts.getPostsByTid(tid, start, end, function(err, postData) { if(err) { return callback(err); } if (Array.isArray(postData) && !postData.length) { return callback(null, []); } for(var i=0; i 1, 'topic_id': tid, 'expose_tools': privileges.editable ? 1 : 0, 'disableSocialButtons': meta.config.disableSocialButtons !== undefined ? parseInt(meta.config.disableSocialButtons, 10) !== 0 : false, 'posts': topicPosts }); }); }); } Topics.getTopicForCategoryView = function(tid, uid, callback) { function getTopicData(next) { Topics.getTopicDataWithUser(tid, next); } function getReadStatus(next) { if (uid && parseInt(uid, 10) > 0) { Topics.hasReadTopic(tid, uid, function(read) { next(null, read); }); } else { next(null, null); } } function getTeaser(next) { Topics.getTeaser(tid, next); } async.parallel([getTopicData, getReadStatus, getTeaser], function(err, results) { if (err) { return callback(err); } var topicData = results[0], hasRead = results[1], teaser = results[2]; topicData['pin-icon'] = parseInt(topicData.pinned, 10) === 1 ? 'fa-thumb-tack' : 'none'; topicData['lock-icon'] = parseInt(topicData.locked, 10) === 1 ? 'fa-lock' : 'none'; topicData.badgeclass = hasRead ? '' : 'badge-important'; topicData.teaser_username = teaser.username || ''; topicData.teaser_userslug = teaser.userslug || ''; topicData.userslug = teaser.userslug || ''; topicData.teaser_timestamp = utils.toISOString(teaser.timestamp); topicData.teaser_userpicture = teaser.picture; callback(null, topicData); }); } Topics.getAllTopics = function(limit, after, callback) { db.getSetMembers('topics:tid', function(err, tids) { if(err) { return callback(err, null); } var topics = [], numTids, x; // Sort into ascending order tids.sort(function(a, b) { return a - b; }); // Eliminate everything after the "after" tid if (after) { for (x = 0, numTids = tids.length; x < numTids; x++) { if (tids[x] >= after) { tids = tids.slice(0, x); break; } } } if (limit) { if (limit > 0 && limit < tids.length) { tids = tids.slice(tids.length - limit); } } // Sort into descending order tids.sort(function(a, b) { return b - a; }); async.each(tids, function(tid, next) { Topics.getTopicDataWithUser(tid, function(err, topicData) { topics.push(topicData); next(); }); }, function(err) { callback(err, topics); }); }); } Topics.markAllRead = function(uid, callback) { db.getSetMembers('topics:tid', function(err, tids) { if (err) { return callback(err); } if(!tids || !tids.length) { return callback(null); } function markRead(tid, next) { Topics.markAsRead(tid, uid, next); } async.each(tids, markRead, callback); }); } Topics.getTitleByPid = function(pid, callback) { posts.getPostField(pid, 'tid', function(err, tid) { Topics.getTopicField(tid, 'title', function(err, title) { callback(title); }); }); } Topics.markAsUnreadForAll = function(tid, callback) { db.delete('tid:' + tid + ':read_by_uid', callback); } Topics.markAsRead = function(tid, uid, callback) { db.setAdd('tid:' + tid + ':read_by_uid', uid, function(err) { if(callback) { callback(err); } }); Topics.getTopicField(tid, 'cid', function(err, cid) { categories.isTopicsRead(cid, uid, function(read) { if (read) { categories.markAsRead(cid, uid); } }); }); user.notifications.getUnreadByUniqueId(uid, 'topic:' + tid, function(err, nids) { notifications.mark_read_multiple(nids, uid, function() { user.pushNotifCount(uid); }); }); } Topics.markCategoryUnreadForAll = function(tid, callback) { Topics.getTopicField(tid, 'cid', function(err, cid) { if(err) { return callback(err); } categories.markAsUnreadForAll(cid, callback); }); } Topics.hasReadTopics = function(tids, uid, callback) { var sets = []; for (var i = 0, ii = tids.length; i < ii; i++) { sets.push('tid:' + tids[i] + ':read_by_uid'); } db.isMemberOfSets(sets, uid, function(err, hasRead) { callback(hasRead); }); } Topics.hasReadTopic = function(tid, uid, callback) { db.isSetMember('tid:' + tid + ':read_by_uid', uid, function(err, hasRead) { if (err === null) { callback(hasRead); } else { console.log(err); callback(false); } }); } Topics.getTeasers = function(tids, callback) { var teasers = []; if (Array.isArray(tids)) { async.eachSeries(tids, function(tid, next) { Topics.getTeaser(tid, function(err, teaser_info) { if (err) { teaser_info = {}; } teasers.push(teaser_info); next(); }); }, function() { callback(teasers); }); } else callback(teasers); } Topics.getTeaser = function(tid, callback) { threadTools.getLatestUndeletedPid(tid, function(err, pid) { if (err) { return callback(err, null); } posts.getPostFields(pid, ['pid', 'uid', 'timestamp'], function(err, postData) { if (err) { return callback(err, null); } else if(!postData) { return callback(new Error('no-teaser-found')); } user.getUserFields(postData.uid, ['username', 'userslug', 'picture'], function(err, userData) { if (err) { return callback(err, null); } callback(null, { pid: postData.pid, username: userData.username || 'anonymous', userslug: userData.userslug, picture: userData.picture || gravatar.url('', {}, true), timestamp: postData.timestamp }); }); }); }); } Topics.getTopicField = function(tid, field, callback) { db.getObjectField('topic:' + tid, field, callback); } Topics.getTopicFields = function(tid, fields, callback) { db.getObjectFields('topic:' + tid, fields, callback); } Topics.setTopicField = function(tid, field, value, callback) { db.setObjectField('topic:' + tid, field, value, callback); } Topics.increasePostCount = function(tid, callback) { db.incrObjectField('topic:' + tid, 'postcount', callback); } Topics.decreasePostCount = function(tid, callback) { db.decrObjectField('topic:' + tid, 'postcount', callback); } Topics.increaseViewCount = function(tid, callback) { db.incrObjectField('topic:' + tid, 'viewcount', callback); } Topics.isLocked = function(tid, callback) { Topics.getTopicField(tid, 'locked', function(err, locked) { if(err) { return callback(err, null); } callback(null, parseInt(locked, 10) === 1); }); } Topics.updateTimestamp = function(tid, timestamp) { db.sortedSetAdd('topics:recent', timestamp, tid); Topics.setTopicField(tid, 'lastposttime', timestamp); } Topics.onNewPostMade = function(tid, pid, timestamp, callback) { Topics.increasePostCount(tid); Topics.updateTimestamp(tid, timestamp); Topics.addPostToTopic(tid, pid, timestamp, callback); } Topics.addPostToTopic = function(tid, pid, timestamp, callback) { db.sortedSetAdd('tid:' + tid + ':posts', timestamp, pid, callback); } Topics.removePostFromTopic = function(tid, pid, callback) { db.sortedSetRemove('tid:' + tid + ':posts', pid, callback); } Topics.getPids = function(tid, callback) { db.getSortedSetRange('tid:' + tid + ':posts', 0, -1, callback); } Topics.getUids = function(tid, callback) { var uids = {}; Topics.getPids(tid, function(err, pids) { function getUid(pid, next) { posts.getPostField(pid, 'uid', function(err, uid) { if (err) return next(err); uids[uid] = 1; next(null); }); } async.each(pids, getUid, function(err) { if (err) return callback(err, null); callback(null, Object.keys(uids)); }); }); } Topics.delete = function(tid) { Topics.setTopicField(tid, 'deleted', 1); db.sortedSetRemove('topics:recent', tid); Topics.getTopicField(tid, 'cid', function(err, cid) { feed.updateCategory(cid); db.incrObjectFieldBy('category:' + cid, 'topic_count', -1); }); } Topics.restore = function(tid) { Topics.setTopicField(tid, 'deleted', 0); Topics.getTopicField(tid, 'lastposttime', function(err, lastposttime) { db.sortedSetAdd('topics:recent', lastposttime, tid); }); Topics.getTopicField(tid, 'cid', function(err, cid) { feed.updateCategory(cid); db.incrObjectFieldBy('category:' + cid, 'topic_count', 1); }); } Topics.reIndexTopic = function(tid, callback) { Topics.getPids(tid, function(err, pids) { if (err) { return callback(err); } posts.reIndexPids(pids, callback); }); } Topics.reIndexAll = function(callback) { db.getSetMembers('topics:tid', function(err, tids) { if (err) { return callback(err); } async.each(tids, Topics.reIndexTopic, callback); }); } }(exports));