mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46: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:website": 0,
|
||||||
"min:rep:aboutme": 0,
|
"min:rep:aboutme": 0,
|
||||||
"min:rep:signature": 0,
|
"min:rep:signature": 0,
|
||||||
|
"flags:limitPerTarget": 0,
|
||||||
"notificationType_upvote": "notification",
|
"notificationType_upvote": "notification",
|
||||||
"notificationType_new-topic": "notification",
|
"notificationType_new-topic": "notification",
|
||||||
"notificationType_new-reply": "notification",
|
"notificationType_new-reply": "notification",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"settings/general": "General",
|
"settings/general": "General",
|
||||||
"settings/homepage": "Home Page",
|
"settings/homepage": "Home Page",
|
||||||
"settings/navigation": "Navigation",
|
"settings/navigation": "Navigation",
|
||||||
"settings/reputation": "Reputation",
|
"settings/reputation": "Reputation & Flags",
|
||||||
"settings/email": "Email",
|
"settings/email": "Email",
|
||||||
"settings/user": "Users",
|
"settings/user": "Users",
|
||||||
"settings/group": "Groups",
|
"settings/group": "Groups",
|
||||||
|
|||||||
@@ -12,5 +12,9 @@
|
|||||||
"min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile",
|
"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-signature": "Minimum reputation to add \"Signature\" to user profile",
|
||||||
"min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" 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-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-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",
|
"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",
|
"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": "You can only downvote %1 times a day",
|
||||||
"too-many-downvotes-today-user": "You can only downvote a user %1 times a day",
|
"too-many-downvotes-today-user": "You can only downvote a user %1 times a day",
|
||||||
|
|||||||
31
src/flags.js
31
src/flags.js
@@ -277,7 +277,7 @@ Flags.create = async function (type, id, uid, reason, timestamp) {
|
|||||||
timestamp = Date.now();
|
timestamp = Date.now();
|
||||||
doHistoryAppend = true;
|
doHistoryAppend = true;
|
||||||
}
|
}
|
||||||
const [flagExists, targetExists, canFlag, targetUid, targetCid] = await Promise.all([
|
const [flagExists, targetExists,, targetUid, targetCid] = await Promise.all([
|
||||||
// Sanity checks
|
// Sanity checks
|
||||||
Flags.exists(type, id, uid),
|
Flags.exists(type, id, uid),
|
||||||
Flags.targetExists(type, id),
|
Flags.targetExists(type, id),
|
||||||
@@ -287,12 +287,11 @@ Flags.create = async function (type, id, uid, reason, timestamp) {
|
|||||||
Flags.getTargetCid(type, id),
|
Flags.getTargetCid(type, id),
|
||||||
]);
|
]);
|
||||||
if (flagExists) {
|
if (flagExists) {
|
||||||
throw new Error('[[error:already-flagged]]');
|
throw new Error(`[[error:${type}-already-flagged]]`);
|
||||||
} else if (!targetExists) {
|
} else if (!targetExists) {
|
||||||
throw new Error('[[error:invalid-data]]');
|
throw new Error('[[error:invalid-data]]');
|
||||||
} else if (!canFlag) {
|
|
||||||
throw new Error('[[error:no-privileges]]');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const flagId = await db.incrObjectField('global', 'nextFlagId');
|
const flagId = await db.incrObjectField('global', 'nextFlagId');
|
||||||
|
|
||||||
await db.setObject('flag:' + flagId, {
|
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:byReporter:' + uid, timestamp, flagId); // by reporter
|
||||||
await db.sortedSetAdd('flags:byType:' + type, timestamp, flagId); // by flag type
|
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.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
|
await analytics.increment('flags'); // some fancy analytics
|
||||||
|
|
||||||
if (targetUid) {
|
if (targetUid) {
|
||||||
@@ -336,13 +336,28 @@ Flags.exists = async function (type, id, uid) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Flags.canFlag = async function (type, id, uid) {
|
Flags.canFlag = async function (type, id, uid) {
|
||||||
if (type === 'user') {
|
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]]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canRead = await privileges.posts.can('topics:read', id, uid);
|
||||||
|
switch (type) {
|
||||||
|
case 'user':
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case 'post':
|
||||||
|
if (!canRead) {
|
||||||
|
throw new Error('[[error:no-privileges]]');
|
||||||
}
|
}
|
||||||
if (type === 'post') {
|
break;
|
||||||
return await privileges.posts.can('topics:read', id, uid);
|
|
||||||
}
|
default:
|
||||||
throw new Error('[[error:invalid-data]]');
|
throw new Error('[[error:invalid-data]]');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Flags.getTarget = async function (type, id, uid) {
|
Flags.getTarget = async function (type, id, uid) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const db = require('../../database');
|
|||||||
const batch = require('../../batch');
|
const batch = require('../../batch');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'Re add deleted topics to topics:recent',
|
name: 'Re-add deleted topics to topics:recent',
|
||||||
timestamp: Date.UTC(2018, 9, 11),
|
timestamp: Date.UTC(2018, 9, 11),
|
||||||
method: async function () {
|
method: async function () {
|
||||||
const progress = this.progress;
|
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';
|
'use strict';
|
||||||
|
|
||||||
const db = require('../../database');
|
const db = require('../../database');
|
||||||
const winston = require('winston');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// you should use spaces
|
// you should use spaces
|
||||||
|
|||||||
@@ -27,21 +27,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<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"
|
<div class="form-group">
|
||||||
data-field="min:rep:downvote"><br />
|
<label for="min:rep:downvote">[[admin/settings/reputation:min-rep-downvote]]</label>
|
||||||
<strong>[[admin/settings/reputation:downvotes-per-day]]</strong><br /> <input type="text" class="form-control" placeholder="10" data-field="downvotesPerDay"><br />
|
<input type="text" class="form-control" placeholder="0" data-field="min:rep:downvote" id="min:rep:downvote">
|
||||||
<strong>[[admin/settings/reputation:downvotes-per-user-per-day]]</strong><br /> <input type="text" class="form-control" placeholder="3" data-field="downvotesPerUserPerDay"><br />
|
</div>
|
||||||
<strong>[[admin/settings/reputation:min-rep-flag]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:flag"><br />
|
<div class="form-group">
|
||||||
<strong>[[admin/settings/reputation:min-rep-website]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:website"><br />
|
<label for="downvotesPerDay">[[admin/settings/reputation:downvotes-per-day]]</label>
|
||||||
<strong>[[admin/settings/reputation:min-rep-aboutme]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:aboutme"><br />
|
<input type="text" class="form-control" placeholder="10" data-field="downvotesPerDay" id="downvotesPerDay">
|
||||||
<strong>[[admin/settings/reputation:min-rep-signature]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:signature"><br />
|
</div>
|
||||||
<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 />
|
<div class="form-group">
|
||||||
<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 />
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user