From c513b88dff57d76dfd0dd551ec6a801d66195a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 9 Jul 2020 12:51:05 -0400 Subject: [PATCH] feat: #8427, daily downvote limits --- install/data/defaults.json | 2 + .../en-GB/admin/settings/reputation.json | 2 + public/language/en-GB/error.json | 2 + src/posts/votes.js | 34 +++++++++++++++-- src/views/admin/settings/reputation.tpl | 5 ++- test/posts.js | 38 ++++++++++++++++++- 6 files changed, 77 insertions(+), 6 deletions(-) diff --git a/install/data/defaults.json b/install/data/defaults.json index 8d0112ba74..b9f5f039f9 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -70,6 +70,8 @@ "reputation:disabled": 0, "downvote:disabled": 0, "disableSignatures": 0, + "downvotesPerDay": 10, + "downvotesPerUserPerDay": 3, "min:rep:downvote": 0, "min:rep:flag": 0, "min:rep:profile-picture": 0, diff --git a/public/language/en-GB/admin/settings/reputation.json b/public/language/en-GB/admin/settings/reputation.json index 910909ff65..77fe61ac6a 100644 --- a/public/language/en-GB/admin/settings/reputation.json +++ b/public/language/en-GB/admin/settings/reputation.json @@ -5,6 +5,8 @@ "votes-are-public": "All Votes Are Public", "thresholds": "Activity Thresholds", "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", "min-rep-flag": "Minimum reputation to flag posts", "min-rep-website": "Minimum reputation to add \"Website\" to user profile", "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 521ea9a7e7..1158b27847 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -165,6 +165,8 @@ "not-enough-reputation-min-rep-cover-picture": "You do not have enough reputation to add a cover picture", "already-flagged": "You have already flagged this post", "self-vote": "You cannot vote on your own post", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading.", diff --git a/src/posts/votes.js b/src/posts/votes.js index 92ed268d4c..fd224695d4 100644 --- a/src/posts/votes.js +++ b/src/posts/votes.js @@ -119,18 +119,17 @@ module.exports = function (Posts) { } async function unvote(pid, uid, command) { - const [owner, voteStatus, reputation] = await Promise.all([ + const [owner, voteStatus] = await Promise.all([ Posts.getPostField(pid, 'uid'), Posts.hasVoted(pid, uid), - user.getUserField(uid, 'reputation'), ]); if (parseInt(uid, 10) === parseInt(owner, 10)) { throw new Error('[[error:self-vote]]'); } - if (command === 'downvote' && reputation < meta.config['min:rep:downvote']) { - throw new Error('[[error:not-enough-reputation-to-downvote]]'); + if (command === 'downvote') { + await checkDownvoteLimitation(pid, uid); } let hook; @@ -159,6 +158,33 @@ module.exports = function (Posts) { return await vote(voteStatus.upvoted ? 'downvote' : 'upvote', true, pid, uid); } + async function checkDownvoteLimitation(pid, uid) { + const oneDay = 86400000; + const [reputation, targetUid, downvotedPids] = await Promise.all([ + user.getUserField(uid, 'reputation'), + Posts.getPostField(pid, 'uid'), + db.getSortedSetRevRangeByScore( + 'uid:' + uid + ':downvote', 0, -1, '+inf', Date.now() - oneDay + ), + ]); + + if (reputation < meta.config['min:rep:downvote']) { + throw new Error('[[error:not-enough-reputation-to-downvote]]'); + } + + if (meta.config.downvotesPerDay && downvotedPids.length >= meta.config.downvotesPerDay) { + throw new Error('[[error:too-many-downvotes-today, ' + meta.config.downvotesPerDay + ']]'); + } + + if (meta.config.downvotesPerUserPerDay) { + const postData = await Posts.getPostsFields(downvotedPids, ['uid']); + const targetDownvotes = postData.filter(p => p.uid === targetUid).length; + if (targetDownvotes >= meta.config.downvotesPerUserPerDay) { + throw new Error('[[error:too-many-downvotes-today-user, ' + meta.config.downvotesPerUserPerDay + ']]'); + } + } + } + async function vote(type, unvote, pid, uid) { uid = parseInt(uid, 10); if (uid <= 0) { diff --git a/src/views/admin/settings/reputation.tpl b/src/views/admin/settings/reputation.tpl index b05e7e8238..4d802383b9 100644 --- a/src/views/admin/settings/reputation.tpl +++ b/src/views/admin/settings/reputation.tpl @@ -32,7 +32,10 @@
[[admin/settings/reputation:thresholds]]
- [[admin/settings/reputation:min-rep-downvote]]

+ [[admin/settings/reputation:min-rep-downvote]]

+ [[admin/settings/reputation:downvotes-per-day]]

+ [[admin/settings/reputation:downvotes-per-user-per-day]]

[[admin/settings/reputation:min-rep-flag]]

[[admin/settings/reputation:min-rep-website]]

[[admin/settings/reputation:min-rep-aboutme]]

diff --git a/test/posts.js b/test/posts.js index 9bc9fa09ef..df8b113f89 100644 --- a/test/posts.js +++ b/test/posts.js @@ -242,6 +242,42 @@ describe('Post\'s', function () { }); }); }); + + it('should prevent downvoting more than total daily limit', async () => { + const oldValue = meta.config.downvotesPerDay; + meta.config.downvotesPerDay = 1; + let err; + const p1 = await topics.reply({ + uid: voteeUid, + tid: topicData.tid, + content: 'raw content', + }); + try { + await socketPosts.downvote({ uid: voterUid }, { pid: p1.pid, room_id: 'topic_1' }); + } catch (_err) { + err = _err; + } + assert.equal(err.message, '[[error:too-many-downvotes-today, 1]]'); + meta.config.downvotesPerDay = oldValue; + }); + + it('should prevent downvoting target user more than total daily limit', async () => { + const oldValue = meta.config.downvotesPerUserPerDay; + meta.config.downvotesPerUserPerDay = 1; + let err; + const p1 = await topics.reply({ + uid: voteeUid, + tid: topicData.tid, + content: 'raw content', + }); + try { + await socketPosts.downvote({ uid: voterUid }, { pid: p1.pid, room_id: 'topic_1' }); + } catch (_err) { + err = _err; + } + assert.equal(err.message, '[[error:too-many-downvotes-today-user, 1]]'); + meta.config.downvotesPerUserPerDay = oldValue; + }); }); describe('bookmarking', function () { @@ -910,7 +946,7 @@ describe('Post\'s', function () { it('should get pid index', function (done) { socketPosts.getPidIndex({ uid: voterUid }, { pid: pid, tid: topicData.tid, topicPostSort: 'oldest_to_newest' }, function (err, index) { assert.ifError(err); - assert.equal(index, 2); + assert.equal(index, 4); done(); }); });