var async = require('async'), gravatar = require('gravatar'), path = require('path'), nconf = require('nconf'), validator = require('validator'), S = require('string'), winston = require('winston'), db = require('./database'), posts = require('./posts'), utils = require('./../public/src/utils'), plugins = require('./plugins'), user = require('./user'), categories = require('./categories'), categoryTools = require('./categoryTools'), posts = require('./posts'), threadTools = require('./threadTools'), postTools = require('./postTools'), notifications = require('./notifications'), favourites = require('./favourites'), meta = require('./meta'), Plugins = require('./plugins'); (function(Topics) { Topics.create = function(data, callback) { var uid = data.uid, title = data.title, cid = data.cid, thumb = data.thumb; db.incrObjectField('global', 'nextTid', function(err, tid) { if(err) { return callback(err); } var slug = tid + '/' + utils.slugify(title), timestamp = Date.now(); var topicData = { 'tid': tid, 'uid': uid, 'cid': cid, 'title': title, 'slug': slug, 'timestamp': timestamp, 'lastposttime': 0, 'postcount': 0, 'viewcount': 0, 'locked': 0, 'deleted': 0, 'pinned': 0 }; if(thumb) { topicData['thumb'] = thumb; } db.setObject('topic:' + tid, topicData, function(err) { if(err) { return callback(err); } db.sortedSetAdd('topics:tid', timestamp, tid); Plugins.fireHook('action:topic.save', tid); user.addTopicIdToUser(uid, tid, timestamp); db.sortedSetAdd('categories:' + cid + ':tid', timestamp, tid); db.incrObjectField('category:' + cid, 'topic_count'); db.incrObjectField('global', 'topicCount'); callback(null, tid); }); }); }; Topics.post = function(data, callback) { var uid = data.uid, title = data.title, content = data.content, cid = data.cid, thumb = data.thumb; 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.exists(cid, next); }, function(categoryExists, next) { if(!categoryExists) { return next(new Error('category doesn\'t exist')) } 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: uid, title: title, cid: cid, thumb: thumb}, next); }, function(tid, next) { Topics.reply({uid:uid, tid:tid, content:content}, next); }, function(postData, next) { threadTools.toggleFollow(postData.tid, uid); next(null, postData); }, function(postData, next) { Topics.getTopicsByTids([postData.tid], data.cid, uid, function(err, topicData) { if(err) { return next(err); } if(!topicData || !topicData.length) { return next(new Error('no-topic')); } topicData = topicData[0]; topicData.unreplied = 1; next(null, { topicData: topicData, postData: postData }); }); } ], callback); }; Topics.reply = function(data, callback) { var tid = data.tid, uid = data.uid, toPid = data.toPid, content = data.content, privileges, postData; async.waterfall([ function(next) { threadTools.exists(tid, next); }, function(topicExists, next) { if (!topicExists) { return next(new Error('topic doesn\'t exist')); } 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:uid, tid:tid, content:content, toPid:toPid}, next); }, function(data, next) { postData = data; threadTools.notifyFollowers(tid, postData.pid, uid); user.sendPostNotificationToFollowers(uid, tid, postData.pid); next(); }, function(next) { Topics.markAsUnreadForAll(tid, next); }, function(next) { Topics.markAsRead(tid, uid, next); }, function(next) { Topics.pushUnreadCount(); posts.addUserInfoToPost(postData, next); }, function(postData,next) { posts.getPidIndex(postData.pid, next); }, function(index, next) { postData.index = index; postData.favourited = false; postData.votes = 0; 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({uid: results.postData.uid, title: title, cid: 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(err, exists) { if(err || !exists) { return callback(err || new Error('Topic doesn\'t exist')); } posts.getPostFields(pid, ['deleted', '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); } if(!parseInt(postData.deleted, 10)) { Topics.decreasePostCount(postData.tid); Topics.increasePostCount(tid); } posts.setPostField(pid, 'tid', 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.escape(data.title); data.relativeTime = utils.toISOString(data.timestamp); } callback(null, data); }); }; Topics.getTopicDataWithUser = function(tid, callback) { Topics.getTopicData(tid, function(err, topic) { if (err || !topic) { return callback(err || new Error('topic doesn\'t exist')); } user.getUserFields(topic.uid, ['username', 'userslug', 'picture'] , function(err, userData) { if (err) { return callback(err); } if (!userData) { userData = {}; } topic.username = userData.username || 'Anonymous'; topic.userslug = userData.userslug || ''; topic.picture = userData.picture || gravatar.url('', {}, true); callback(null, topic); }); }); }; Topics.getTopicPosts = function(tid, start, end, uid, reverse, callback) { posts.getPostsByTid(tid, start, end, reverse, function(err, postData) { if(err) { return callback(err); } if (Array.isArray(postData) && !postData.length) { return callback(null, []); } for(var i=0; i