mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 08:36:12 +01:00
refactor: flag sanity checks, +feat: flag limits
- Added new config flag:limitPerTarget, to disallow flags after an item has already been flagged x times (default 0, or infinite) - New zset flags:byTarget, score is the number of times a flag has been made against that item - "already-flagged" translation key removed, now "post-already-flagged" or "user-already-flagged" -- this fixed bug where flagging a user you've already flagged would tell you you've already flagged this post already. - Refactored Flags.canFlag to throw errors only, instead of returning boolean - Updated ACP form inputs for reputation settings page to be more bootstrappy - +1 upgrade script
This commit is contained in:
@@ -79,6 +79,7 @@
|
||||
"min:rep:website": 0,
|
||||
"min:rep:aboutme": 0,
|
||||
"min:rep:signature": 0,
|
||||
"flags:limitPerTarget": 0,
|
||||
"notificationType_upvote": "notification",
|
||||
"notificationType_new-topic": "notification",
|
||||
"notificationType_new-reply": "notification",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"settings/general": "General",
|
||||
"settings/homepage": "Home Page",
|
||||
"settings/navigation": "Navigation",
|
||||
"settings/reputation": "Reputation",
|
||||
"settings/reputation": "Reputation & Flags",
|
||||
"settings/email": "Email",
|
||||
"settings/user": "Users",
|
||||
"settings/group": "Groups",
|
||||
|
||||
@@ -12,5 +12,9 @@
|
||||
"min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile",
|
||||
"min-rep-signature": "Minimum reputation to add \"Signature\" to user profile",
|
||||
"min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile",
|
||||
"min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile"
|
||||
"min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile",
|
||||
|
||||
"flags": "Flag Settings",
|
||||
"flags.limit-per-target": "Maximum number of times something can be flagged",
|
||||
"flags.limit-per-target-placeholder": "Default: 0"
|
||||
}
|
||||
@@ -163,7 +163,10 @@
|
||||
"not-enough-reputation-min-rep-signature": "You do not have enough reputation to add a signature",
|
||||
"not-enough-reputation-min-rep-profile-picture": "You do not have enough reputation to add a profile 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",
|
||||
"post-already-flagged": "You have already flagged this post",
|
||||
"user-already-flagged": "You have already flagged this user",
|
||||
"post-flagged-too-many-times": "This post has been flagged by others already",
|
||||
"user-flagged-too-many-times": "This user has been flagged by others already",
|
||||
"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",
|
||||
|
||||
33
src/flags.js
33
src/flags.js
@@ -277,7 +277,7 @@ Flags.create = async function (type, id, uid, reason, timestamp) {
|
||||
timestamp = Date.now();
|
||||
doHistoryAppend = true;
|
||||
}
|
||||
const [flagExists, targetExists, canFlag, targetUid, targetCid] = await Promise.all([
|
||||
const [flagExists, targetExists,, targetUid, targetCid] = await Promise.all([
|
||||
// Sanity checks
|
||||
Flags.exists(type, id, uid),
|
||||
Flags.targetExists(type, id),
|
||||
@@ -287,12 +287,11 @@ Flags.create = async function (type, id, uid, reason, timestamp) {
|
||||
Flags.getTargetCid(type, id),
|
||||
]);
|
||||
if (flagExists) {
|
||||
throw new Error('[[error:already-flagged]]');
|
||||
throw new Error(`[[error:${type}-already-flagged]]`);
|
||||
} else if (!targetExists) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
} else if (!canFlag) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
const flagId = await db.incrObjectField('global', 'nextFlagId');
|
||||
|
||||
await db.setObject('flag:' + flagId, {
|
||||
@@ -307,6 +306,7 @@ Flags.create = async function (type, id, uid, reason, timestamp) {
|
||||
await db.sortedSetAdd('flags:byReporter:' + uid, timestamp, flagId); // by reporter
|
||||
await db.sortedSetAdd('flags:byType:' + type, timestamp, flagId); // by flag type
|
||||
await db.sortedSetAdd('flags:hash', flagId, [type, id, uid].join(':')); // save zset for duplicate checking
|
||||
await db.sortedSetIncrBy('flags:byTarget', 1, [type, id].join(':')); // by flag target (score is count)
|
||||
await analytics.increment('flags'); // some fancy analytics
|
||||
|
||||
if (targetUid) {
|
||||
@@ -336,13 +336,28 @@ Flags.exists = async function (type, id, uid) {
|
||||
};
|
||||
|
||||
Flags.canFlag = async function (type, id, uid) {
|
||||
if (type === 'user') {
|
||||
return true;
|
||||
const limit = meta.config['flags:limitPerTarget'];
|
||||
if (limit > 0) {
|
||||
const score = await db.sortedSetScore('flags:byTarget', `${type}:${id}`);
|
||||
if (score >= limit) {
|
||||
throw new Error(`[[error:${type}-flagged-too-many-times]]`);
|
||||
}
|
||||
}
|
||||
if (type === 'post') {
|
||||
return await privileges.posts.can('topics:read', id, uid);
|
||||
|
||||
const canRead = await privileges.posts.can('topics:read', id, uid);
|
||||
switch (type) {
|
||||
case 'user':
|
||||
return true;
|
||||
|
||||
case 'post':
|
||||
if (!canRead) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
};
|
||||
|
||||
Flags.getTarget = async function (type, id, uid) {
|
||||
|
||||
@@ -5,7 +5,7 @@ const db = require('../../database');
|
||||
const batch = require('../../batch');
|
||||
|
||||
module.exports = {
|
||||
name: 'Re add deleted topics to topics:recent',
|
||||
name: 'Re-add deleted topics to topics:recent',
|
||||
timestamp: Date.UTC(2018, 9, 11),
|
||||
method: async function () {
|
||||
const progress = this.progress;
|
||||
|
||||
15
src/upgrades/1.14.3/track_flags_by_target.js
Normal file
15
src/upgrades/1.14.3/track_flags_by_target.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const db = require('../../database');
|
||||
|
||||
module.exports = {
|
||||
name: 'New sorted set for tracking flags by target',
|
||||
timestamp: Date.UTC(2020, 6, 15),
|
||||
method: async () => {
|
||||
const flags = await db.getSortedSetRange('flags:hash', 0, -1);
|
||||
await Promise.all(flags.map(async (flag) => {
|
||||
flag = flag.split(':').slice(0, 2);
|
||||
await db.sortedSetIncrBy('flags:byTarget', 1, flag.join(':'));
|
||||
}));
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const db = require('../../database');
|
||||
const winston = require('winston');
|
||||
|
||||
module.exports = {
|
||||
// you should use spaces
|
||||
|
||||
@@ -27,21 +27,58 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/reputation:thresholds]]</div>
|
||||
<div class="col-sm-10 col-xs-12">
|
||||
<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: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-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-signature]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:signature"><br />
|
||||
<strong>[[admin/settings/reputation:min-rep-profile-picture]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:profile-picture"><br />
|
||||
<strong>[[admin/settings/reputation:min-rep-cover-picture]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:cover-picture"><br />
|
||||
<div class="form-group">
|
||||
<label for="min:rep:downvote">[[admin/settings/reputation:min-rep-downvote]]</label>
|
||||
<input type="text" class="form-control" placeholder="0" data-field="min:rep:downvote" id="min:rep:downvote">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="downvotesPerDay">[[admin/settings/reputation:downvotes-per-day]]</label>
|
||||
<input type="text" class="form-control" placeholder="10" data-field="downvotesPerDay" id="downvotesPerDay">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="downvotesPerUserPerDay">[[admin/settings/reputation:downvotes-per-user-per-day]]</label>
|
||||
<input type="text" class="form-control" placeholder="3" data-field="downvotesPerUserPerDay" id="downvotesPerUserPerDay">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="min:rep:flag">[[admin/settings/reputation:min-rep-flag]]</label>
|
||||
<input type="text" class="form-control" placeholder="0" data-field="min:rep:flag" id="min:rep:flag">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="min:rep:website">[[admin/settings/reputation:min-rep-website]]</label>
|
||||
<input type="text" class="form-control" placeholder="0" data-field="min:rep:website" id="min:rep:website">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="min:rep:aboutme">[[admin/settings/reputation:min-rep-aboutme]]</label>
|
||||
<input type="text" class="form-control" placeholder="0" data-field="min:rep:aboutme" id="min:rep:aboutme">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="min:rep:signature">[[admin/settings/reputation:min-rep-signature]]</label>
|
||||
<input type="text" class="form-control" placeholder="0" data-field="min:rep:signature" id="min:rep:signature">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="min:rep:profile-picture">[[admin/settings/reputation:min-rep-profile-picture]]</label>
|
||||
<input type="text" class="form-control" placeholder="0" data-field="min:rep:profile-picture" id="min:rep:profile-picture">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="min:rep:cover-picture">[[admin/settings/reputation:min-rep-cover-picture]]</label>
|
||||
<input type="text" class="form-control" placeholder="0" data-field="min:rep:cover-picture" id="min:rep:cover-picture">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/reputation:flags]]</div>
|
||||
<div class="col-sm-10 col-xs-12">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="flags:limitPerTarget">[[admin/settings/reputation:flags.limit-per-target]]</label>
|
||||
<input type="text" class="form-control" placeholder="[[admin/settings/reputation:flags.limit-per-target-placeholder]]" data-field="flags:limitPerTarget" id="flags:limitPerTarget">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user