Skip to content

Commit

Permalink
refactor: flag sanity checks, +feat: flag limits
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
julianlam committed Jul 15, 2020
1 parent cd94c24 commit e3e55f2
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 25 deletions.
1 change: 1 addition & 0 deletions install/data/defaults.json
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion public/language/en-GB/admin/menu.json
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion public/language/en-GB/admin/settings/reputation.json
Expand Up @@ -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"
}
5 changes: 4 additions & 1 deletion public/language/en-GB/error.json
Expand Up @@ -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",
Expand Down
33 changes: 24 additions & 9 deletions src/flags.js
Expand Up @@ -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),
Expand All @@ -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, {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/upgrades/1.14.1/readd_deleted_recent_topics.js
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions src/upgrades/1.14.3/track_flags_by_target.js
@@ -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 change: 0 additions & 1 deletion src/upgrades/TEMPLATE
@@ -1,7 +1,6 @@
'use strict';

const db = require('../../database');
const winston = require('winston');

module.exports = {
// you should use spaces
Expand Down
59 changes: 48 additions & 11 deletions src/views/admin/settings/reputation.tpl
Expand Up @@ -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>
Expand Down

0 comments on commit e3e55f2

Please sign in to comment.