mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 11:05:54 +01:00 
			
		
		
		
	feat: #8427, daily downvote limits
This commit is contained in:
		| @@ -70,6 +70,8 @@ | |||||||
|     "reputation:disabled": 0, |     "reputation:disabled": 0, | ||||||
|     "downvote:disabled": 0, |     "downvote:disabled": 0, | ||||||
|     "disableSignatures": 0, |     "disableSignatures": 0, | ||||||
|  |     "downvotesPerDay": 10, | ||||||
|  |     "downvotesPerUserPerDay": 3, | ||||||
|     "min:rep:downvote": 0, |     "min:rep:downvote": 0, | ||||||
|     "min:rep:flag": 0, |     "min:rep:flag": 0, | ||||||
|     "min:rep:profile-picture": 0, |     "min:rep:profile-picture": 0, | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ | |||||||
| 	"votes-are-public": "All Votes Are Public", | 	"votes-are-public": "All Votes Are Public", | ||||||
| 	"thresholds": "Activity Thresholds", | 	"thresholds": "Activity Thresholds", | ||||||
| 	"min-rep-downvote": "Minimum reputation to downvote posts", | 	"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-flag": "Minimum reputation to flag posts", | ||||||
| 	"min-rep-website": "Minimum reputation to add \"Website\" to user profile", | 	"min-rep-website": "Minimum reputation to add \"Website\" to user profile", | ||||||
| 	"min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", | 	"min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", | ||||||
|   | |||||||
| @@ -165,6 +165,8 @@ | |||||||
| 	"not-enough-reputation-min-rep-cover-picture": "You do not have enough reputation to add a cover picture", | 	"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", | 	"already-flagged": "You have already flagged this post", | ||||||
| 	"self-vote": "You cannot vote on your own 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.", | 	"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.", | ||||||
|  |  | ||||||
|   | |||||||
| @@ -119,18 +119,17 @@ module.exports = function (Posts) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	async function unvote(pid, uid, command) { | 	async function unvote(pid, uid, command) { | ||||||
| 		const [owner, voteStatus, reputation] = await Promise.all([ | 		const [owner, voteStatus] = await Promise.all([ | ||||||
| 			Posts.getPostField(pid, 'uid'), | 			Posts.getPostField(pid, 'uid'), | ||||||
| 			Posts.hasVoted(pid, uid), | 			Posts.hasVoted(pid, uid), | ||||||
| 			user.getUserField(uid, 'reputation'), |  | ||||||
| 		]); | 		]); | ||||||
|  |  | ||||||
| 		if (parseInt(uid, 10) === parseInt(owner, 10)) { | 		if (parseInt(uid, 10) === parseInt(owner, 10)) { | ||||||
| 			throw new Error('[[error:self-vote]]'); | 			throw new Error('[[error:self-vote]]'); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if (command === 'downvote' && reputation < meta.config['min:rep:downvote']) { | 		if (command === 'downvote') { | ||||||
| 			throw new Error('[[error:not-enough-reputation-to-downvote]]'); | 			await checkDownvoteLimitation(pid, uid); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		let hook; | 		let hook; | ||||||
| @@ -159,6 +158,33 @@ module.exports = function (Posts) { | |||||||
| 		return await vote(voteStatus.upvoted ? 'downvote' : 'upvote', true, pid, uid); | 		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) { | 	async function vote(type, unvote, pid, uid) { | ||||||
| 		uid = parseInt(uid, 10); | 		uid = parseInt(uid, 10); | ||||||
| 		if (uid <= 0) { | 		if (uid <= 0) { | ||||||
|   | |||||||
| @@ -32,7 +32,10 @@ | |||||||
| 	<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/reputation:thresholds]]</div> | 	<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/reputation:thresholds]]</div> | ||||||
| 	<div class="col-sm-10 col-xs-12"> | 	<div class="col-sm-10 col-xs-12"> | ||||||
| 		<form> | 		<form> | ||||||
| 			<strong>[[admin/settings/reputation:min-rep-downvote]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:downvote"><br /> | 			<strong>[[admin/settings/reputation:min-rep-downvote]]</strong><br /> <input type="text" class="form-control" placeholder="0" | ||||||
|  | 			data-field="min:rep:downvote"><br /> | ||||||
|  | 			<strong>[[admin/settings/reputation:downvotes-per-day]]</strong><br /> <input type="text" class="form-control" placeholder="10" data-field="downvotesPerDay"><br /> | ||||||
|  | 			<strong>[[admin/settings/reputation:downvotes-per-user-per-day]]</strong><br /> <input type="text" class="form-control" placeholder="3" data-field="downvotesPerUserPerDay"><br /> | ||||||
| 			<strong>[[admin/settings/reputation:min-rep-flag]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:flag"><br /> | 			<strong>[[admin/settings/reputation:min-rep-flag]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:flag"><br /> | ||||||
| 			<strong>[[admin/settings/reputation:min-rep-website]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:website"><br /> | 			<strong>[[admin/settings/reputation:min-rep-website]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:website"><br /> | ||||||
| 			<strong>[[admin/settings/reputation:min-rep-aboutme]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:aboutme"><br /> | 			<strong>[[admin/settings/reputation:min-rep-aboutme]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:aboutme"><br /> | ||||||
|   | |||||||
| @@ -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 () { | 	describe('bookmarking', function () { | ||||||
| @@ -910,7 +946,7 @@ describe('Post\'s', function () { | |||||||
| 		it('should get pid index', function (done) { | 		it('should get pid index', function (done) { | ||||||
| 			socketPosts.getPidIndex({ uid: voterUid }, { pid: pid, tid: topicData.tid, topicPostSort: 'oldest_to_newest' }, function (err, index) { | 			socketPosts.getPidIndex({ uid: voterUid }, { pid: pid, tid: topicData.tid, topicPostSort: 'oldest_to_newest' }, function (err, index) { | ||||||
| 				assert.ifError(err); | 				assert.ifError(err); | ||||||
| 				assert.equal(index, 2); | 				assert.equal(index, 4); | ||||||
| 				done(); | 				done(); | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user