var async = require('async'), gravatar = require('gravatar'), nconf = require('nconf'), validator = require('validator'), 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') websockets = require('./websockets'); (function(Topics) { Topics.post = function(uid, title, content, cid, callback) { categoryTools.privileges(cid, uid, function(err, privileges) { if(err) { return callback(err); } else if(!privileges.write) { return callback(new Error('no-privileges')); } else if (!cid) { return callback(new Error('invalid-cid')); } else if (!title || title.length < meta.config.minimumTitleLength) { return callback(new Error('title-too-short'), null); } else if (!content || content.length < meta.config.miminumPostLength) { return callback(new Error('content-too-short'), null); } if (content) { content = content.trim(); } if (title) { title = title.trim(); } user.getUserField(uid, 'lastposttime', function(err, lastposttime) { if (err) { return callback(err); } if(!lastposttime) { lastposttime = 0; } if (Date.now() - lastposttime < meta.config.postDelay * 1000) { return callback(new Error('too-many-posts'), null); } db.incrObjectField('global', 'nextTid', function(err, tid) { if(err) { return callback(err); } db.setAdd('topics:tid', tid); var slug = tid + '/' + utils.slugify(title); var 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 }); db.searchIndex('topic', title, tid); user.addTopicIdToUser(uid, tid); // let everyone know that there is an unread topic in this category db.delete('cid:' + cid + ':read_by_uid', function(err, data) { Topics.markAsRead(tid, uid); }); // in future it may be possible to add topics to several categories, so leaving the door open here. db.sortedSetAdd('categories:' + cid + ':tid', timestamp, tid); db.incrObjectField('category:' + cid, 'topic_count'); db.incrObjectField('global', 'topicCount'); feed.updateCategory(cid); posts.create(uid, tid, content, function(err, postData) { if(err) { return callback(err, null); } else if(!postData) { return callback(new Error('invalid-post'), null); } // Auto-subscribe the post creator to the newly created topic threadTools.toggleFollow(tid, uid); Topics.pushUnreadCount(); Topics.getTopicForCategoryView(tid, uid, function(topicData) { topicData.unreplied = 1; callback(null, { topicData: topicData, postData: postData }); }); }); }); }); }); }; 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(); if(data.timestamp) { data.relativeTime = new Date(parseInt(data.timestamp, 10)).toISOString(); } } 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(postData) { if (Array.isArray(postData) && !postData.length) { return callback([]); } for(var i=0; i 1, 'topic_id': tid, 'expose_tools': privileges.editable ? 1 : 0, '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, function(err, teaser) { if (err) teaser = {}; next(null, teaser); }); } async.parallel([getTopicData, getReadStatus, getTeaser], function(err, results) { if (err) { throw new Error(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_text = teaser.text || ''; topicData.teaser_username = teaser.username || ''; topicData.teaser_userslug = teaser.userslug || ''; topicData.userslug = teaser.userslug || ''; topicData.teaser_timestamp = teaser.timestamp ? (new Date(parseInt(teaser.timestamp,10)).toISOString()) : ''; topicData.teaser_userpicture = teaser.picture; callback(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, null); } if (tids && tids.length) { for (var i = 0; i < tids.length; ++i) { Topics.markAsRead(tids[i], uid); } } callback(null, true); }); } 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) { db.setAdd('tid:' + tid + ':read_by_uid', uid); 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() { }); }); } 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', 'content', '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); } var stripped = postData.content, timestamp = postData.timestamp, returnObj = { "pid": postData.pid, "username": userData.username || 'anonymous', "userslug": userData.userslug, "picture": userData.picture || gravatar.url('', {}, https = nconf.get('https')), "timestamp": timestamp }; if (postData.content) { stripped = postData.content.replace(/>.+\n\n/, ''); postTools.parse(stripped, function(err, stripped) { returnObj.text = utils.strip_tags(stripped); callback(null, returnObj); }); } else { returnObj.text = ''; callback(null, returnObj); } }); }); }); } Topics.emitTitleTooShortAlert = function(socket) { socket.emit('event:alert', { type: 'danger', timeout: 2000, title: 'Title too short', message: "Please enter a longer title. At least " + meta.config.minimumTitleLength + " characters.", alert_id: 'post_error' }); } 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.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.addPostToTopic = function(tid, pid) { db.listAppend('tid:' + tid + ':posts', pid); } Topics.getPids = function(tid, callback) { db.getListRange('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) { callback(err); } else { posts.reIndexPids(pids, function(err) { if (err) { callback(err); } else { callback(null); } }); } }); } Topics.reIndexAll = function(callback) { db.getSetMembers('topics:tid', function(err, tids) { if (err) { callback(err, null); } else { async.each(tids, Topics.reIndexTopic, function(err) { if (err) { callback(err, null); } else { callback(null, 'All topics reindexed.'); } }); } }); } }(exports));