"use strict"; var async = require('async'), winston = require('winston'), db = require('./database'), posts = require('./posts'), user = require('./user'), plugins = require('./plugins'), meta = require('./meta'); (function (Favourites) { var votesInProgress = {}; function vote(type, unvote, pid, uid, callback) { uid = parseInt(uid, 10); if (uid === 0) { return callback(new Error('[[error:not-logged-in]]')); } posts.getPostFields(pid, ['pid', 'uid'], function (err, postData) { if (err) { return callback(err); } var now = Date.now(); if(type === 'upvote' && !unvote) { db.sortedSetAdd('uid:' + uid + ':upvote', now, pid); } else { db.sortedSetRemove('uid:' + uid + ':upvote', pid); } if(type === 'upvote' || unvote) { db.sortedSetRemove('uid:' + uid + ':downvote', pid); } else { db.sortedSetAdd('uid:' + uid + ':downvote', now, pid); } user[type === 'upvote' ? 'incrementUserFieldBy' : 'decrementUserFieldBy'](postData.uid, 'reputation', 1, function (err, newreputation) { if (err) { return callback(err); } if (parseInt(postData.uid, 10)) { db.sortedSetAdd('users:reputation', newreputation, postData.uid); } adjustPostVotes(pid, uid, type, unvote, function(err, votes) { postData.votes = votes; callback(err, { user: { reputation: newreputation }, post: postData, upvote: type === 'upvote' && !unvote, downvote: type === 'downvote' && !unvote }); }); }); }); } function adjustPostVotes(pid, uid, type, unvote, callback) { var notType = (type === 'upvote' ? 'downvote' : 'upvote'); async.series([ function(next) { if (unvote) { db.setRemove('pid:' + pid + ':' + type, uid, next); } else { db.setAdd('pid:' + pid + ':' + type, uid, next); } }, function(next) { db.setRemove('pid:' + pid + ':' + notType, uid, next); } ], function(err) { if (err) { return callback(err); } async.parallel({ upvotes: function(next) { db.setCount('pid:' + pid + ':upvote', next); }, downvotes: function(next) { db.setCount('pid:' + pid + ':downvote', next); } }, function(err, results) { if (err) { return callback(err); } var voteCount = parseInt(results.upvotes, 10) - parseInt(results.downvotes, 10); posts.updatePostVoteCount(pid, voteCount, function(err) { callback(err, voteCount); }); }); }); } Favourites.upvote = function(pid, uid, callback) { if (parseInt(meta.config['reputation:disabled'], 10) === 1) { return callback(new Error('[[error:reputation-system-disabled]]')); } toggleVote('upvote', pid, uid, callback); }; Favourites.downvote = function(pid, uid, callback) { if (parseInt(meta.config['reputation:disabled'], 10) === 1) { return callback(new Error('[[error:reputation-system-disabled]]')); } if (parseInt(meta.config['downvote:disabled'], 10) === 1) { return callback(new Error('[[error:downvoting-disabled]]')); } user.getUserField(uid, 'reputation', function(err, reputation) { if (err) { return callback(err); } if (reputation < parseInt(meta.config['privileges:downvote'], 10)) { return callback(new Error('[[error:not-enough-reputation-to-downvote]]')); } toggleVote('downvote', pid, uid, callback); }); }; function voteInProgress(pid, uid) { return Array.isArray(votesInProgress[uid]) && votesInProgress[uid].indexOf(parseInt(pid, 10)) !== -1; } function putVoteInProgress(pid, uid) { votesInProgress[uid] = votesInProgress[uid] || []; votesInProgress[uid].push(parseInt(pid, 10)); } function clearVoteProgress(pid, uid) { if (Array.isArray(votesInProgress[uid])) { var index = votesInProgress[uid].indexOf(parseInt(pid, 10)); if (index !== -1) { votesInProgress[uid].splice(index, 1); } } } function toggleVote(type, pid, uid, callback) { function done(err, data) { clearVoteProgress(pid, uid); callback(err, data); } if (voteInProgress(pid, uid)) { return callback(new Error('[[error:already-voting-for-this-post]]')); } putVoteInProgress(pid, uid); unvote(pid, uid, type, function(err) { if (err) { return done(err); } vote(type, false, pid, uid, done); }); } Favourites.unvote = function(pid, uid, callback) { unvote(pid, uid, 'unvote', callback); }; function unvote(pid, uid, command, callback) { async.parallel({ owner: function(next) { posts.getPostField(pid, 'uid', next); }, voteStatus: function(next) { Favourites.hasVoted(pid, uid, next); } }, function(err, results) { if (err) { return callback(err); } if (parseInt(uid, 10) === parseInt(results.owner, 10)) { return callback(new Error('[[error:cant-vote-self-post]]')); } var voteStatus = results.voteStatus, hook, current = voteStatus.upvoted ? 'upvote' : 'downvote'; if (voteStatus.upvoted && command === 'downvote' || voteStatus.downvoted && command === 'upvote') { // e.g. User *has* upvoted, and clicks downvote hook = command; } else if (voteStatus.upvoted || voteStatus.downvoted) { // e.g. User *has* upvotes, clicks upvote (so we "unvote") hook = 'unvote'; } else { // e.g. User *has not* voted, clicks upvote hook = command; current = 'unvote'; } plugins.fireHook('action:post.' + hook, { pid: pid, uid: uid, owner: results.owner, current: current }); if (!voteStatus || (!voteStatus.upvoted && !voteStatus.downvoted)) { return callback(); } vote(voteStatus.upvoted ? 'downvote' : 'upvote', true, pid, uid, callback); }); } Favourites.hasVoted = function(pid, uid, callback) { if (!parseInt(uid, 10)) { return callback(null, {upvoted: false, downvoted: false}); } db.isMemberOfSets(['pid:' + pid + ':upvote', 'pid:' + pid + ':downvote'], uid, function(err, hasVoted) { if (err) { return callback(err); } callback (null, {upvoted: hasVoted[0], downvoted: hasVoted[1]}); }); }; Favourites.getVoteStatusByPostIDs = function(pids, uid, callback) { if (!parseInt(uid, 10)) { var data = pids.map(function() {return false;}); return callback(null, {upvotes: data, downvotes: data}); } var upvoteSets = [], downvoteSets = []; for (var i=0; i