From 62187caa673eb2b32c8390dd3a9ad6281a9a0790 Mon Sep 17 00:00:00 2001
From: gasoved
Date: Fri, 4 Mar 2022 23:38:16 +0300
Subject: [PATCH] feat: post auto flagging on downvotes #10029 (#10367)
* feat: post auto flagging on downvotes
* fix: just get one admin
---
install/data/defaults.json | 1 +
.../en-GB/admin/settings/reputation.json | 1 +
public/language/en-GB/flags.json | 3 ++-
src/cli/user.js | 2 +-
src/flags.js | 16 +++++++++-------
src/posts/votes.js | 9 +++++++++
src/user/index.js | 4 ++++
src/views/admin/settings/reputation.tpl | 4 ++++
8 files changed, 31 insertions(+), 9 deletions(-)
diff --git a/install/data/defaults.json b/install/data/defaults.json
index 2eb0a15bb1..952702a3fb 100644
--- a/install/data/defaults.json
+++ b/install/data/defaults.json
@@ -87,6 +87,7 @@
"min:rep:aboutme": 0,
"min:rep:signature": 0,
"flags:limitPerTarget": 0,
+ "flags:autoFlagOnDownvoteThreshold": 0,
"notificationType_upvote": "notification",
"notificationType_new-topic": "notification",
"notificationType_new-reply": "notification",
diff --git a/public/language/en-GB/admin/settings/reputation.json b/public/language/en-GB/admin/settings/reputation.json
index 7cfa636521..8ea8cda4c1 100644
--- a/public/language/en-GB/admin/settings/reputation.json
+++ b/public/language/en-GB/admin/settings/reputation.json
@@ -18,5 +18,6 @@
"flags.limit-per-target": "Maximum number of times something can be flagged",
"flags.limit-per-target-placeholder": "Default: 0",
"flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.",
+ "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)",
"flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned"
}
\ No newline at end of file
diff --git a/public/language/en-GB/flags.json b/public/language/en-GB/flags.json
index 5bd46b6b1d..d526bd6e25 100644
--- a/public/language/en-GB/flags.json
+++ b/public/language/en-GB/flags.json
@@ -81,5 +81,6 @@
"bulk-actions": "Bulk Actions",
"bulk-resolve": "Resolve Flag(s)",
"bulk-success": "%1 flags updated",
- "flagged-timeago-readable": "Flagged (%2)"
+ "flagged-timeago-readable": "Flagged (%2)",
+ "auto-flagged": "[Auto Flagged] Received %1 downvotes."
}
\ No newline at end of file
diff --git a/src/cli/user.js b/src/cli/user.js
index 097ff8bd3b..bbd747865f 100644
--- a/src/cli/user.js
+++ b/src/cli/user.js
@@ -101,7 +101,7 @@ async function execute(cmd, args) {
function UserCmdHelpers() {
async function getAdminUidOrFail() {
- const adminUid = (await db.getSortedSetMembers('group:administrators:members')).reverse()[0];
+ const adminUid = await user.getFirstAdminUid();
if (!adminUid) {
const err = new Error('An admin account does not exists to execute the operation.');
err.name = 'UserError';
diff --git a/src/flags.js b/src/flags.js
index 60543ae1ce..990cced428 100644
--- a/src/flags.js
+++ b/src/flags.js
@@ -377,7 +377,7 @@ Flags.deleteNote = async function (flagId, datetime) {
await db.sortedSetRemove(`flag:${flagId}:notes`, note[0]);
};
-Flags.create = async function (type, id, uid, reason, timestamp) {
+Flags.create = async function (type, id, uid, reason, timestamp, forceFlag = false) {
let doHistoryAppend = false;
if (!timestamp) {
timestamp = Date.now();
@@ -387,14 +387,14 @@ Flags.create = async function (type, id, uid, reason, timestamp) {
// Sanity checks
Flags.exists(type, id, uid),
Flags.targetExists(type, id),
- Flags.canFlag(type, id, uid),
+ Flags.canFlag(type, id, uid, forceFlag),
Flags.targetFlagged(type, id),
// Extra data for zset insertion
Flags.getTargetUid(type, id),
Flags.getTargetCid(type, id),
]);
- if (flagExists) {
+ if (!forceFlag && flagExists) {
throw new Error(`[[error:${type}-already-flagged]]`);
} else if (!targetExists) {
throw new Error('[[error:invalid-data]]');
@@ -499,9 +499,9 @@ Flags.exists = async function (type, id, uid) {
return await db.isSortedSetMember('flags:hash', [type, id, uid].join(':'));
};
-Flags.canFlag = async function (type, id, uid) {
+Flags.canFlag = async function (type, id, uid, skipLimitCheck = false) {
const limit = meta.config['flags:limitPerTarget'];
- if (limit > 0) {
+ if (!skipLimitCheck && limit > 0) {
const score = await db.sortedSetScore('flags:byTarget', `${type}:${id}`);
if (score >= limit) {
throw new Error(`[[error:${type}-flagged-too-many-times]]`);
@@ -729,7 +729,7 @@ Flags.appendNote = async function (flagId, uid, note, datetime) {
});
};
-Flags.notify = async function (flagObj, uid) {
+Flags.notify = async function (flagObj, uid, notifySelf = false) {
const [admins, globalMods] = await Promise.all([
groups.getMembers('administrators', 0, -1),
groups.getMembers('Global Moderators', 0, -1),
@@ -780,7 +780,9 @@ Flags.notify = async function (flagObj, uid) {
from: uid,
to: uids,
});
- uids = uids.filter(_uid => parseInt(_uid, 10) !== parseInt(uid, 10));
+ if (!notifySelf) {
+ uids = uids.filter(_uid => parseInt(_uid, 10) !== parseInt(uid, 10));
+ }
await notifications.push(notifObj, uids);
};
diff --git a/src/posts/votes.js b/src/posts/votes.js
index 1a56c4a66d..08466a2fe8 100644
--- a/src/posts/votes.js
+++ b/src/posts/votes.js
@@ -2,10 +2,12 @@
const meta = require('../meta');
const db = require('../database');
+const flags = require('../flags');
const user = require('../user');
const topics = require('../topics');
const plugins = require('../plugins');
const privileges = require('../privileges');
+const translator = require('../translator');
module.exports = function (Posts) {
const votesInProgress = {};
@@ -243,6 +245,13 @@ module.exports = function (Posts) {
if (!postData || !postData.pid || !postData.tid) {
return;
}
+ const threshold = meta.config['flags:autoFlagOnDownvoteThreshold'];
+ if (threshold && postData.votes <= (-threshold)) {
+ const adminUid = await user.getFirstAdminUid();
+ const reportMsg = await translator.translate(`[[flags:auto-flagged, ${-postData.votes}]]`);
+ const flagObj = await flags.create('post', postData.pid, adminUid, reportMsg, null, true);
+ await flags.notify(flagObj, adminUid, true);
+ }
await Promise.all([
updateTopicVoteCount(postData),
db.sortedSetAdd('posts:votes', postData.votes, postData.pid),
diff --git a/src/user/index.js b/src/user/index.js
index 6a52886011..3f409669cf 100644
--- a/src/user/index.js
+++ b/src/user/index.js
@@ -211,6 +211,10 @@ User.getAdminsandGlobalModsandModerators = async function () {
return await User.getUsersData(_.union(...results));
};
+User.getFirstAdminUid = async function () {
+ return (await db.getSortedSetRange('group:administrators:members', 0, 0))[0];
+};
+
User.getModeratorUids = async function () {
const cids = await categories.getAllCidsFromSet('categories:cid');
const uids = await categories.getModeratorUids(cids);
diff --git a/src/views/admin/settings/reputation.tpl b/src/views/admin/settings/reputation.tpl
index c6e5c62387..24f8e632a1 100644
--- a/src/views/admin/settings/reputation.tpl
+++ b/src/views/admin/settings/reputation.tpl
@@ -82,6 +82,10 @@
[[admin/settings/reputation:flags.limit-per-target-help]]
+
+
+
+