mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-27 00:56:13 +01:00
feat: consolidation of flags to reduce flagspam, #8510
Squashed commit of the following: commitc6d0939620Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 24 13:41:32 2020 -0400 fix: more tests commit32f9af2a87Merge:e509075354eae927d1Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 24 10:53:04 2020 -0400 Merge remote-tracking branch 'origin/master' into singleton-flags commite509075351Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 24 10:52:46 2020 -0400 fix: controllers-admin test commitfd5af99e30Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 17 17:26:55 2020 -0400 fix(tests): dummy commit to trigger travisCI commitc452a6ffcfAuthor: Julian Lam <julian@nodebb.org> Date: Fri Jul 17 17:05:09 2020 -0400 fix(openapi): openapi spec changes commit8089a74e89Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 17 15:48:00 2020 -0400 fix: reversing the order of reports for display purposes commita099892b37Author: Julian Lam <julian@nodebb.org> Date: Fri Jul 17 15:45:44 2020 -0400 refactor: run all flag creation calls in a single batch commitb24999682fAuthor: Julian Lam <julian@nodebb.org> Date: Fri Jul 17 15:08:23 2020 -0400 feat: handling multiple reporters per flag, #8510 commit08c75c0200Author: Julian Lam <julian@nodebb.org> Date: Thu Jul 16 20:53:18 2020 -0400 feat: upgrade script for #8510
This commit is contained in:
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"state": "State",
|
"state": "State",
|
||||||
"reporter": "Reporter",
|
"reports": "Reports",
|
||||||
"reported-at": "Reported At",
|
"first-reported": "First Reported",
|
||||||
"description": "Description",
|
|
||||||
"no-flags": "Hooray! No flags found.",
|
"no-flags": "Hooray! No flags found.",
|
||||||
"assignee": "Assignee",
|
"assignee": "Assignee",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
|
|||||||
@@ -3089,6 +3089,9 @@ paths:
|
|||||||
type: boolean
|
type: boolean
|
||||||
downvoted:
|
downvoted:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
flagId:
|
||||||
|
type: number
|
||||||
|
description: The flag identifier, if this particular post has been flagged before
|
||||||
"/api/topic/tid/{id}":
|
"/api/topic/tid/{id}":
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@@ -3735,6 +3738,9 @@ paths:
|
|||||||
type: boolean
|
type: boolean
|
||||||
display_post_menu:
|
display_post_menu:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
flagId:
|
||||||
|
type: number
|
||||||
|
description: The flag identifier, if this particular post has been flagged before
|
||||||
category:
|
category:
|
||||||
$ref: components/schemas/CategoryObject.yaml#/CategoryObject
|
$ref: components/schemas/CategoryObject.yaml#/CategoryObject
|
||||||
tagWhitelist:
|
tagWhitelist:
|
||||||
@@ -4788,6 +4794,9 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
state:
|
state:
|
||||||
type: string
|
type: string
|
||||||
|
heat:
|
||||||
|
type: number
|
||||||
|
description: The number of reports that make up this flag
|
||||||
flagId:
|
flagId:
|
||||||
type: number
|
type: number
|
||||||
type:
|
type:
|
||||||
@@ -4796,34 +4805,8 @@ paths:
|
|||||||
oneOf:
|
oneOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: number
|
- type: number
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
uid:
|
|
||||||
type: number
|
|
||||||
description: A user identifier
|
|
||||||
datetime:
|
datetime:
|
||||||
type: number
|
type: number
|
||||||
reporter:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: A friendly name for a given user account
|
|
||||||
picture:
|
|
||||||
nullable: true
|
|
||||||
type: string
|
|
||||||
icon:bgColor:
|
|
||||||
type: string
|
|
||||||
description: A six-character hexadecimal colour code assigned to the user. This
|
|
||||||
value is used in conjunction with
|
|
||||||
`icon:text` for the user's auto-generated
|
|
||||||
icon
|
|
||||||
example: "#f44336"
|
|
||||||
icon:text:
|
|
||||||
type: string
|
|
||||||
description: A single-letter representation of a username. This is used in the
|
|
||||||
auto-generated icon given to users without
|
|
||||||
an avatar
|
|
||||||
labelClass:
|
labelClass:
|
||||||
type: string
|
type: string
|
||||||
target_readable:
|
target_readable:
|
||||||
@@ -4886,11 +4869,6 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
targetId:
|
targetId:
|
||||||
type: number
|
type: number
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
uid:
|
|
||||||
type: number
|
|
||||||
description: A user identifier
|
|
||||||
datetime:
|
datetime:
|
||||||
type: number
|
type: number
|
||||||
datetimeISO:
|
datetimeISO:
|
||||||
@@ -5006,6 +4984,17 @@ paths:
|
|||||||
`icon:text` for the user's auto-generated
|
`icon:text` for the user's auto-generated
|
||||||
icon
|
icon
|
||||||
example: "#f44336"
|
example: "#f44336"
|
||||||
|
reports:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
timestamp:
|
||||||
|
type: number
|
||||||
|
timestampISO:
|
||||||
|
type: string
|
||||||
reporter:
|
reporter:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ define('forum/flags/list', ['components', 'Chart'], function (components, Chart)
|
|||||||
Flags.init = function () {
|
Flags.init = function () {
|
||||||
Flags.enableFilterForm();
|
Flags.enableFilterForm();
|
||||||
|
|
||||||
|
components.get('flags/list').on('click', '[data-flag-id]', function () {
|
||||||
|
var flagId = this.getAttribute('data-flag-id');
|
||||||
|
ajaxify.go('flags/' + flagId);
|
||||||
|
});
|
||||||
|
|
||||||
var graphWrapper = $('#flags-daily-wrapper');
|
var graphWrapper = $('#flags-daily-wrapper');
|
||||||
var graphFooter = graphWrapper.siblings('.panel-footer');
|
var graphFooter = graphWrapper.siblings('.panel-footer');
|
||||||
$('#flags-daily-wrapper').one('shown.bs.collapse', function () {
|
$('#flags-daily-wrapper').one('shown.bs.collapse', function () {
|
||||||
|
|||||||
141
src/flags.js
141
src/flags.js
@@ -84,32 +84,28 @@ Flags.init = async function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Flags.get = async function (flagId) {
|
Flags.get = async function (flagId) {
|
||||||
const [base, history, notes] = await Promise.all([
|
const [base, history, notes, reports] = await Promise.all([
|
||||||
db.getObject('flag:' + flagId),
|
db.getObject('flag:' + flagId),
|
||||||
Flags.getHistory(flagId),
|
Flags.getHistory(flagId),
|
||||||
Flags.getNotes(flagId),
|
Flags.getNotes(flagId),
|
||||||
|
Flags.getReports(flagId),
|
||||||
]);
|
]);
|
||||||
if (!base) {
|
if (!base) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [userObj, targetObj] = await Promise.all([
|
|
||||||
user.getUserFields(base.uid, ['username', 'userslug', 'picture', 'reputation']),
|
|
||||||
Flags.getTarget(base.type, base.targetId, 0),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const flagObj = {
|
const flagObj = {
|
||||||
state: 'open',
|
state: 'open',
|
||||||
assignee: null,
|
assignee: null,
|
||||||
...base,
|
...base,
|
||||||
description: validator.escape(base.description),
|
|
||||||
datetimeISO: utils.toISOString(base.datetime),
|
datetimeISO: utils.toISOString(base.datetime),
|
||||||
target_readable: base.type.charAt(0).toUpperCase() + base.type.slice(1) + ' ' + base.targetId,
|
target_readable: base.type.charAt(0).toUpperCase() + base.type.slice(1) + ' ' + base.targetId,
|
||||||
target: targetObj,
|
target: await Flags.getTarget(base.type, base.targetId, 0),
|
||||||
history: history,
|
history: history,
|
||||||
notes: notes,
|
notes: notes,
|
||||||
reporter: userObj,
|
reports: reports,
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = await plugins.fireHook('filter:flags.get', {
|
const data = await plugins.fireHook('filter:flags.get', {
|
||||||
flag: flagObj,
|
flag: flagObj,
|
||||||
});
|
});
|
||||||
@@ -160,24 +156,19 @@ Flags.list = async function (filters, uid) {
|
|||||||
const pageCount = Math.ceil(flagIds.length / flagsPerPage);
|
const pageCount = Math.ceil(flagIds.length / flagsPerPage);
|
||||||
flagIds = flagIds.slice((filters.page - 1) * flagsPerPage, filters.page * flagsPerPage);
|
flagIds = flagIds.slice((filters.page - 1) * flagsPerPage, filters.page * flagsPerPage);
|
||||||
|
|
||||||
const flags = await Promise.all(flagIds.map(async (flagId) => {
|
const reportCounts = await db.sortedSetsCard(flagIds.map(flagId => `flag:${flagId}:reports`));
|
||||||
|
|
||||||
|
const flags = await Promise.all(flagIds.map(async (flagId, idx) => {
|
||||||
let flagObj = await db.getObject('flag:' + flagId);
|
let flagObj = await db.getObject('flag:' + flagId);
|
||||||
const userObj = await user.getUserFields(flagObj.uid, ['username', 'picture']);
|
|
||||||
flagObj = {
|
flagObj = {
|
||||||
state: 'open',
|
state: 'open',
|
||||||
assignee: null,
|
assignee: null,
|
||||||
|
heat: reportCounts[idx],
|
||||||
...flagObj,
|
...flagObj,
|
||||||
reporter: {
|
|
||||||
username: userObj.username,
|
|
||||||
picture: userObj.picture,
|
|
||||||
'icon:bgColor': userObj['icon:bgColor'],
|
|
||||||
'icon:text': userObj['icon:text'],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
flagObj.labelClass = Flags._constants.state_class[flagObj.state];
|
flagObj.labelClass = Flags._constants.state_class[flagObj.state];
|
||||||
|
|
||||||
return Object.assign(flagObj, {
|
return Object.assign(flagObj, {
|
||||||
description: validator.escape(String(flagObj.description)),
|
|
||||||
target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId,
|
target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId,
|
||||||
datetimeISO: utils.toISOString(flagObj.datetime),
|
datetimeISO: utils.toISOString(flagObj.datetime),
|
||||||
});
|
});
|
||||||
@@ -242,6 +233,24 @@ Flags.getNote = async function (flagId, datetime) {
|
|||||||
return notes[0];
|
return notes[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Flags.getFlagIdByTarget = async function (type, id) {
|
||||||
|
let method;
|
||||||
|
switch (type) {
|
||||||
|
case 'post':
|
||||||
|
method = posts.getPostField;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'user':
|
||||||
|
method = user.getUserField;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error('[[error:invalid-data]]');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await method(id, 'flagId');
|
||||||
|
};
|
||||||
|
|
||||||
async function modifyNotes(notes) {
|
async function modifyNotes(notes) {
|
||||||
const uids = [];
|
const uids = [];
|
||||||
notes = notes.map(function (note) {
|
notes = notes.map(function (note) {
|
||||||
@@ -277,11 +286,13 @@ Flags.create = async function (type, id, uid, reason, timestamp) {
|
|||||||
timestamp = Date.now();
|
timestamp = Date.now();
|
||||||
doHistoryAppend = true;
|
doHistoryAppend = true;
|
||||||
}
|
}
|
||||||
const [flagExists, targetExists,, targetUid, targetCid] = await Promise.all([
|
const [flagExists, targetExists,, targetFlagged, 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),
|
||||||
Flags.canFlag(type, id, uid),
|
Flags.canFlag(type, id, uid),
|
||||||
|
Flags.targetFlagged(type, id),
|
||||||
|
|
||||||
// Extra data for zset insertion
|
// Extra data for zset insertion
|
||||||
Flags.getTargetUid(type, id),
|
Flags.getTargetUid(type, id),
|
||||||
Flags.getTargetCid(type, id),
|
Flags.getTargetCid(type, id),
|
||||||
@@ -292,45 +303,91 @@ Flags.create = async function (type, id, uid, reason, timestamp) {
|
|||||||
throw new Error('[[error:invalid-data]]');
|
throw new Error('[[error:invalid-data]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
const flagId = await db.incrObjectField('global', 'nextFlagId');
|
// If the flag already exists, just add the report
|
||||||
|
if (targetFlagged) {
|
||||||
|
const flagId = await Flags.getFlagIdByTarget(type, id);
|
||||||
|
await Promise.all([
|
||||||
|
Flags.addReport(flagId, uid, reason, timestamp),
|
||||||
|
Flags.update(flagId, uid, { state: 'open' }),
|
||||||
|
]);
|
||||||
|
|
||||||
await db.setObject('flag:' + flagId, {
|
return await Flags.get(flagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const flagId = await db.incrObjectField('global', 'nextFlagId');
|
||||||
|
const batched = [];
|
||||||
|
|
||||||
|
batched.push(
|
||||||
|
db.setObject.bind(db, 'flag:' + flagId, {
|
||||||
flagId: flagId,
|
flagId: flagId,
|
||||||
type: type,
|
type: type,
|
||||||
targetId: id,
|
targetId: id,
|
||||||
description: reason,
|
|
||||||
uid: uid,
|
|
||||||
datetime: timestamp,
|
datetime: timestamp,
|
||||||
});
|
}),
|
||||||
await db.sortedSetAdd('flags:datetime', timestamp, flagId); // by time, the default
|
Flags.addReport.bind(Flags, flagId, uid, reason, timestamp),
|
||||||
await db.sortedSetAdd('flags:byReporter:' + uid, timestamp, flagId); // by reporter
|
db.sortedSetAdd.bind(db, 'flags:datetime', timestamp, flagId), // by time, the default
|
||||||
await db.sortedSetAdd('flags:byType:' + type, timestamp, flagId); // by flag type
|
db.sortedSetAdd.bind(db, 'flags:byType:' + type, timestamp, flagId), // by flag type
|
||||||
await db.sortedSetAdd('flags:hash', flagId, [type, id, uid].join(':')); // save zset for duplicate checking
|
db.sortedSetAdd.bind(db, '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)
|
db.sortedSetIncrBy.bind(db, 'flags:byTarget', 1, [type, id].join(':')), // by flag target (score is count)
|
||||||
await analytics.increment('flags'); // some fancy analytics
|
analytics.increment.bind(analytics, 'flags') // some fancy analytics
|
||||||
|
);
|
||||||
|
|
||||||
if (targetUid) {
|
if (targetUid) {
|
||||||
await db.sortedSetAdd('flags:byTargetUid:' + targetUid, timestamp, flagId); // by target uid
|
batched.push(db.sortedSetAdd.bind(db, 'flags:byTargetUid:' + targetUid, timestamp, flagId)); // by target uid
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetCid) {
|
if (targetCid) {
|
||||||
await db.sortedSetAdd('flags:byCid:' + targetCid, timestamp, flagId); // by target cid
|
batched.push(db.sortedSetAdd.bind(db, 'flags:byCid:' + targetCid, timestamp, flagId)); // by target cid
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'post') {
|
if (type === 'post') {
|
||||||
await db.sortedSetAdd('flags:byPid:' + id, timestamp, flagId); // by target pid
|
batched.push(
|
||||||
|
db.sortedSetAdd.bind(db, 'flags:byPid:' + id, timestamp, flagId), // by target pid
|
||||||
|
posts.setPostField.bind(posts, id, 'flagId', flagId)
|
||||||
|
);
|
||||||
|
|
||||||
if (targetUid) {
|
if (targetUid) {
|
||||||
await user.incrementUserFlagsBy(targetUid, 1);
|
batched.push(user.incrementUserFlagsBy.bind(user, targetUid, 1));
|
||||||
}
|
}
|
||||||
|
} else if (type === 'user') {
|
||||||
|
batched.push(user.setUserField.bind(user, id, 'flagId', flagId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run all the database calls in one single batched call...
|
||||||
|
await Promise.all(batched.map(async method => await method()));
|
||||||
|
|
||||||
if (doHistoryAppend) {
|
if (doHistoryAppend) {
|
||||||
await Flags.update(flagId, uid, { state: 'open' });
|
Flags.update(flagId, uid, { state: 'open' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Flags.get(flagId);
|
return await Flags.get(flagId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Flags.getReports = async function (flagId) {
|
||||||
|
const [reports, reporterUids] = await Promise.all([
|
||||||
|
db.getSortedSetRevRangeWithScores(`flag:${flagId}:reports`, 0, -1),
|
||||||
|
db.getSortedSetRevRange(`flag:${flagId}:reporters`, 0, -1),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all(reports.map(async (report, idx) => {
|
||||||
|
report.timestamp = report.score;
|
||||||
|
report.timestampISO = new Date(report.score).toISOString();
|
||||||
|
delete report.score;
|
||||||
|
report.reporter = await user.getUserFields(reporterUids[idx], ['username', 'userslug', 'picture', 'reputation']);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return reports;
|
||||||
|
};
|
||||||
|
|
||||||
|
Flags.addReport = async function (flagId, uid, reason, timestamp) {
|
||||||
|
// adds to reporters/report zsets
|
||||||
|
await Promise.all([
|
||||||
|
db.sortedSetAdd(`flags:byReporter:${uid}`, timestamp, flagId),
|
||||||
|
db.sortedSetAdd(`flag:${flagId}:reports`, timestamp, reason),
|
||||||
|
db.sortedSetAdd(`flag:${flagId}:reporters`, timestamp, uid),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
Flags.exists = async function (type, id, uid) {
|
Flags.exists = async function (type, id, uid) {
|
||||||
return await db.isSortedSetMember('flags:hash', [type, id, uid].join(':'));
|
return await db.isSortedSetMember('flags:hash', [type, id, uid].join(':'));
|
||||||
};
|
};
|
||||||
@@ -386,6 +443,10 @@ Flags.targetExists = async function (type, id) {
|
|||||||
throw new Error('[[error:invalid-data]]');
|
throw new Error('[[error:invalid-data]]');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Flags.targetFlagged = async function (type, id) {
|
||||||
|
return await db.sortedSetScore('flags:byTarget', [type, id].join(':')) >= 1;
|
||||||
|
};
|
||||||
|
|
||||||
Flags.getTargetUid = async function (type, id) {
|
Flags.getTargetUid = async function (type, id) {
|
||||||
if (type === 'post') {
|
if (type === 'post') {
|
||||||
return await posts.getPostField(id, 'uid');
|
return await posts.getPostField(id, 'uid');
|
||||||
@@ -443,7 +504,7 @@ Flags.update = async function (flagId, uid, changeset) {
|
|||||||
tasks.push(db.sortedSetAdd('flags:byState:' + changeset[prop], now, flagId));
|
tasks.push(db.sortedSetAdd('flags:byState:' + changeset[prop], now, flagId));
|
||||||
tasks.push(db.sortedSetRemove('flags:byState:' + current[prop], flagId));
|
tasks.push(db.sortedSetRemove('flags:byState:' + current[prop], flagId));
|
||||||
if (changeset[prop] === 'resolved' || changeset[prop] === 'rejected') {
|
if (changeset[prop] === 'resolved' || changeset[prop] === 'rejected') {
|
||||||
tasks.push(notifications.rescind('flag:' + current.type + ':' + current.targetId + ':uid:' + current.uid));
|
tasks.push(notifications.rescind('flag:' + current.type + ':' + current.targetId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (prop === 'assignee') {
|
} else if (prop === 'assignee') {
|
||||||
@@ -545,11 +606,11 @@ Flags.notify = async function (flagObj, uid) {
|
|||||||
|
|
||||||
notifObj = await notifications.create({
|
notifObj = await notifications.create({
|
||||||
type: 'new-post-flag',
|
type: 'new-post-flag',
|
||||||
bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reporter.username + ', ' + titleEscaped + ']]',
|
bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reports[flagObj.reports.length - 1].reporter.username + ', ' + titleEscaped + ']]',
|
||||||
bodyLong: flagObj.description,
|
bodyLong: flagObj.description,
|
||||||
pid: flagObj.targetId,
|
pid: flagObj.targetId,
|
||||||
path: '/flags/' + flagObj.flagId,
|
path: '/flags/' + flagObj.flagId,
|
||||||
nid: 'flag:post:' + flagObj.targetId + ':uid:' + uid,
|
nid: 'flag:post:' + flagObj.targetId,
|
||||||
from: uid,
|
from: uid,
|
||||||
mergeId: 'notifications:user_flagged_post_in|' + flagObj.targetId,
|
mergeId: 'notifications:user_flagged_post_in|' + flagObj.targetId,
|
||||||
topicTitle: title,
|
topicTitle: title,
|
||||||
@@ -558,10 +619,10 @@ Flags.notify = async function (flagObj, uid) {
|
|||||||
} else if (flagObj.type === 'user') {
|
} else if (flagObj.type === 'user') {
|
||||||
notifObj = await notifications.create({
|
notifObj = await notifications.create({
|
||||||
type: 'new-user-flag',
|
type: 'new-user-flag',
|
||||||
bodyShort: '[[notifications:user_flagged_user, ' + flagObj.reporter.username + ', ' + flagObj.target.username + ']]',
|
bodyShort: '[[notifications:user_flagged_user, ' + flagObj.reports[flagObj.reports.length - 1].reporter.username + ', ' + flagObj.target.username + ']]',
|
||||||
bodyLong: flagObj.description,
|
bodyLong: flagObj.description,
|
||||||
path: '/flags/' + flagObj.flagId,
|
path: '/flags/' + flagObj.flagId,
|
||||||
nid: 'flag:user:' + flagObj.targetId + ':uid:' + uid,
|
nid: 'flag:user:' + flagObj.targetId,
|
||||||
from: uid,
|
from: uid,
|
||||||
mergeId: 'notifications:user_flagged_user|' + flagObj.targetId,
|
mergeId: 'notifications:user_flagged_user|' + flagObj.targetId,
|
||||||
});
|
});
|
||||||
|
|||||||
46
src/upgrades/1.14.3/consolidate_flags.js
Normal file
46
src/upgrades/1.14.3/consolidate_flags.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const db = require('../../database');
|
||||||
|
const batch = require('../../batch');
|
||||||
|
const posts = require('../../posts');
|
||||||
|
const user = require('../../user');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'Consolidate multiple flags reports, going forward',
|
||||||
|
timestamp: Date.UTC(2020, 6, 16),
|
||||||
|
method: async function () {
|
||||||
|
const progress = this.progress;
|
||||||
|
|
||||||
|
let flags = await db.getSortedSetRange('flags:datetime', 0, -1);
|
||||||
|
flags = flags.map(flagId => `flag:${flagId}`);
|
||||||
|
flags = await db.getObjectsFields(flags, ['flagId', 'type', 'targetId', 'uid', 'description', 'datetime']);
|
||||||
|
progress.total = flags.length;
|
||||||
|
|
||||||
|
await batch.processArray(flags, async function (subset) {
|
||||||
|
progress.incr(subset.length);
|
||||||
|
|
||||||
|
await Promise.all(subset.map(async (flagObj) => {
|
||||||
|
const methods = [];
|
||||||
|
switch (flagObj.type) {
|
||||||
|
case 'post':
|
||||||
|
methods.push(posts.setPostField.bind(posts, flagObj.targetId, 'flagId', flagObj.flagId));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'user':
|
||||||
|
methods.push(user.setUserField.bind(user, flagObj.targetId, 'flagId', flagObj.flagId));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
methods.push(
|
||||||
|
db.sortedSetAdd.bind(db, `flag:${flagObj.flagId}:reports`, flagObj.datetime, flagObj.description),
|
||||||
|
db.sortedSetAdd.bind(db, `flag:${flagObj.flagId}:reporters`, flagObj.datetime, flagObj.uid)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(methods.map(async method => method()));
|
||||||
|
}));
|
||||||
|
}, {
|
||||||
|
progress: progress,
|
||||||
|
batch: 500,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -712,7 +712,9 @@ describe('Admin Controllers', function () {
|
|||||||
request(nconf.get('url') + '/api/flags/' + flagId, { jar: moderatorJar, json: true }, function (err, res, body) {
|
request(nconf.get('url') + '/api/flags/' + flagId, { jar: moderatorJar, json: true }, function (err, res, body) {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
assert(body);
|
assert(body);
|
||||||
assert.equal(body.reporter.username, 'regular');
|
assert(body.reports);
|
||||||
|
assert(Array.isArray(body.reports));
|
||||||
|
assert.equal(body.reports[0].reporter.username, 'regular');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ describe('Flags', function () {
|
|||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
var compare = {
|
var compare = {
|
||||||
flagId: 1,
|
flagId: 1,
|
||||||
uid: 1,
|
|
||||||
targetId: 1,
|
targetId: 1,
|
||||||
type: 'post',
|
type: 'post',
|
||||||
description: 'Test flag',
|
state: 'open',
|
||||||
|
target_readable: 'Post 1',
|
||||||
};
|
};
|
||||||
assert(flagData);
|
assert(flagData);
|
||||||
for (var key in compare) {
|
for (var key in compare) {
|
||||||
@@ -124,11 +124,10 @@ describe('Flags', function () {
|
|||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
var compare = {
|
var compare = {
|
||||||
flagId: 1,
|
flagId: 1,
|
||||||
uid: 1,
|
|
||||||
targetId: 1,
|
targetId: 1,
|
||||||
type: 'post',
|
type: 'post',
|
||||||
description: 'Test flag',
|
|
||||||
state: 'open',
|
state: 'open',
|
||||||
|
target_readable: 'Post 1',
|
||||||
};
|
};
|
||||||
assert(flagData);
|
assert(flagData);
|
||||||
for (var key in compare) {
|
for (var key in compare) {
|
||||||
@@ -378,14 +377,14 @@ describe('Flags', function () {
|
|||||||
await sleep(2000);
|
await sleep(2000);
|
||||||
|
|
||||||
let userNotifs = await User.notifications.getAll(adminUid);
|
let userNotifs = await User.notifications.getAll(adminUid);
|
||||||
assert(userNotifs.includes('flag:post:' + result.postData.pid + ':uid:' + uid1));
|
assert(userNotifs.includes('flag:post:' + result.postData.pid));
|
||||||
|
|
||||||
await Flags.update(flagId, adminUid, {
|
await Flags.update(flagId, adminUid, {
|
||||||
state: 'resolved',
|
state: 'resolved',
|
||||||
});
|
});
|
||||||
|
|
||||||
userNotifs = await User.notifications.getAll(adminUid);
|
userNotifs = await User.notifications.getAll(adminUid);
|
||||||
assert(!userNotifs.includes('flag:post:' + result.postData.pid + ':uid:' + uid1));
|
assert(!userNotifs.includes('flag:post:' + result.postData.pid));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user