mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-06 22:15:48 +01:00
more work on #5232
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"filter-reset": "Remove Filters",
|
"filter-reset": "Remove Filters",
|
||||||
"filters": "Filter Options",
|
"filters": "Filter Options",
|
||||||
"filter-reporterId": "Reporter UID",
|
"filter-reporterId": "Reporter UID",
|
||||||
|
"filter-targetUid": "Flagged UID",
|
||||||
"filter-type": "Flag Type",
|
"filter-type": "Flag Type",
|
||||||
"filter-type-all": "All Content",
|
"filter-type-all": "All Content",
|
||||||
"filter-type-post": "Post",
|
"filter-type-post": "Post",
|
||||||
@@ -21,6 +22,13 @@
|
|||||||
"filter-quick-mine": "Assigned to me",
|
"filter-quick-mine": "Assigned to me",
|
||||||
"apply-filters": "Apply Filters",
|
"apply-filters": "Apply Filters",
|
||||||
|
|
||||||
|
"quick-links": "Quick Links",
|
||||||
|
"flagged-user": "Flagged User",
|
||||||
|
"reporter": "Reporting User",
|
||||||
|
"view-profile": "View Profile",
|
||||||
|
"start-new-chat": "Start New Chat",
|
||||||
|
"go-to-target": "View Flag Target",
|
||||||
|
|
||||||
"notes": "Flag Notes",
|
"notes": "Flag Notes",
|
||||||
"add-note": "Add Note",
|
"add-note": "Add Note",
|
||||||
"no-notes": "No shared notes.",
|
"no-notes": "No shared notes.",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ define('forum/flags/list', ['components'], function (components) {
|
|||||||
|
|
||||||
Flags.init = function () {
|
Flags.init = function () {
|
||||||
Flags.enableFilterForm();
|
Flags.enableFilterForm();
|
||||||
|
Flags.enableChatButtons();
|
||||||
};
|
};
|
||||||
|
|
||||||
Flags.enableFilterForm = function () {
|
Flags.enableFilterForm = function () {
|
||||||
@@ -29,5 +30,11 @@ define('forum/flags/list', ['components'], function (components) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Flags.enableChatButtons = function () {
|
||||||
|
$('[data-chat]').on('click', function () {
|
||||||
|
app.newChat(this.getAttribute('data-chat'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return Flags;
|
return Flags;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ define('forum/topic/flag', [], function () {
|
|||||||
if (!pid || !reason) {
|
if (!pid || !reason) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
socket.emit('flags.create', {pid: pid, reason: reason}, function (err) {
|
socket.emit('flags.create', {type: 'post', id: pid, reason: reason}, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return app.alertError(err.message);
|
return app.alertError(err.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ modsController.flags.list = function (req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse query string params for filters
|
// Parse query string params for filters
|
||||||
var valid = ['assignee', 'state', 'reporterId', 'type', 'quick'];
|
var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'quick'];
|
||||||
var filters = valid.reduce(function (memo, cur) {
|
var filters = valid.reduce(function (memo, cur) {
|
||||||
if (req.query.hasOwnProperty(cur)) {
|
if (req.query.hasOwnProperty(cur)) {
|
||||||
memo[cur] = req.query[cur];
|
memo[cur] = req.query[cur];
|
||||||
|
|||||||
143
src/flags.js
143
src/flags.js
@@ -1,23 +1,22 @@
|
|||||||
|
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var winston = require('winston');
|
var winston = require('winston');
|
||||||
var db = require('./database');
|
var db = require('./database');
|
||||||
var user = require('./user');
|
var user = require('./user');
|
||||||
|
var groups = require('./groups');
|
||||||
|
var meta = require('./meta');
|
||||||
|
var notifications = require('./notifications');
|
||||||
var analytics = require('./analytics');
|
var analytics = require('./analytics');
|
||||||
var topics = require('./topics');
|
var topics = require('./topics');
|
||||||
var posts = require('./posts');
|
var posts = require('./posts');
|
||||||
|
var privileges = require('./privileges');
|
||||||
|
var plugins = require('./plugins');
|
||||||
var utils = require('../public/src/utils');
|
var utils = require('../public/src/utils');
|
||||||
var _ = require('underscore');
|
var _ = require('underscore');
|
||||||
|
var S = require('string');
|
||||||
|
|
||||||
var Flags = {
|
var Flags = {};
|
||||||
_defaults: {
|
|
||||||
state: 'open',
|
|
||||||
assignee: null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Flags.get = function (flagId, callback) {
|
Flags.get = function (flagId, callback) {
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
@@ -73,6 +72,10 @@ Flags.list = function (filters, uid, callback) {
|
|||||||
sets.push('flags:byAssignee:' + filters[type]);
|
sets.push('flags:byAssignee:' + filters[type]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'targetUid':
|
||||||
|
sets.push('flags:byTargetUid:' + filters[type]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'quick':
|
case 'quick':
|
||||||
switch (filters.quick) {
|
switch (filters.quick) {
|
||||||
case 'mine':
|
case 'mine':
|
||||||
@@ -145,6 +148,43 @@ Flags.list = function (filters, uid, callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Flags.validate = function (payload, callback) {
|
||||||
|
async.parallel({
|
||||||
|
targetExists: async.apply(Flags.targetExists, payload.type, payload.id),
|
||||||
|
target: async.apply(Flags.getTarget, payload.type, payload.id, payload.uid),
|
||||||
|
reporter: async.apply(user.getUserData, payload.uid)
|
||||||
|
}, function (err, data) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.target.deleted) {
|
||||||
|
return callback(new Error('[[error:post-deleted]]'));
|
||||||
|
} else if (data.reporter.banned) {
|
||||||
|
return callback(new Error('[[error:user-banned]]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (payload.type) {
|
||||||
|
case 'post':
|
||||||
|
async.parallel({
|
||||||
|
privileges: async.apply(privileges.posts.get, [payload.id], payload.uid)
|
||||||
|
}, function (err, subdata) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
|
||||||
|
if (!subdata.privileges[0].isAdminOrMod && parseInt(data.reporter.reputation, 10) < minimumReputation) {
|
||||||
|
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
Flags.getTarget = function (type, id, uid, callback) {
|
Flags.getTarget = function (type, id, uid, callback) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'post':
|
case 'post':
|
||||||
@@ -204,17 +244,22 @@ Flags.getNotes = function (flagId, callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Flags.create = function (type, id, uid, reason, callback) {
|
Flags.create = function (type, id, uid, reason, callback) {
|
||||||
|
var targetUid;
|
||||||
|
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
function (next) {
|
function (next) {
|
||||||
// Sanity checks
|
// Sanity checks
|
||||||
async.parallel([
|
async.parallel([
|
||||||
async.apply(Flags.exists, type, id, uid),
|
async.apply(Flags.exists, type, id, uid),
|
||||||
async.apply(Flags.targetExists, type, id)
|
async.apply(Flags.targetExists, type, id),
|
||||||
|
async.apply(Flags.getTargetUid, type, id)
|
||||||
], function (err, checks) {
|
], function (err, checks) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetUid = checks[2] || null;
|
||||||
|
|
||||||
if (checks[0]) {
|
if (checks[0]) {
|
||||||
return next(new Error('[[error:already-flagged]]'));
|
return next(new Error('[[error:already-flagged]]'));
|
||||||
} else if (!checks[1]) {
|
} else if (!checks[1]) {
|
||||||
@@ -226,25 +271,31 @@ Flags.create = function (type, id, uid, reason, callback) {
|
|||||||
},
|
},
|
||||||
async.apply(db.incrObjectField, 'global', 'nextFlagId'),
|
async.apply(db.incrObjectField, 'global', 'nextFlagId'),
|
||||||
function (flagId, next) {
|
function (flagId, next) {
|
||||||
async.parallel([
|
var tasks = [
|
||||||
async.apply(db.setObject.bind(db), 'flag:' + flagId, Object.assign({}, Flags._defaults, {
|
async.apply(db.setObject.bind(db), 'flag:' + flagId, {
|
||||||
flagId: flagId,
|
flagId: flagId,
|
||||||
type: type,
|
type: type,
|
||||||
targetId: id,
|
targetId: id,
|
||||||
description: reason,
|
description: reason,
|
||||||
uid: uid,
|
uid: uid,
|
||||||
datetime: Date.now()
|
datetime: Date.now()
|
||||||
})),
|
}),
|
||||||
async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', Date.now(), flagId), // by time, the default
|
async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', Date.now(), flagId), // by time, the default
|
||||||
async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, Date.now(), flagId), // by reporter
|
async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, Date.now(), flagId), // by reporter
|
||||||
async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, Date.now(), flagId), // by flag type
|
async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, Date.now(), flagId), // by flag type
|
||||||
async.apply(db.setObjectField.bind(db), 'flagHash:flagId', [type, id, uid].join(':'), flagId) // save hash for existence checking
|
async.apply(db.setObjectField.bind(db), 'flagHash:flagId', [type, id, uid].join(':'), flagId) // save hash for existence checking
|
||||||
], function (err, data) {
|
];
|
||||||
|
|
||||||
|
if (targetUid) {
|
||||||
|
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, Date.now(), flagId)); // by target uid
|
||||||
|
}
|
||||||
|
|
||||||
|
async.parallel(tasks, function (err, data) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
Flags.appendHistory(flagId, uid, ['created']);
|
Flags.update(flagId, uid, { "state": "open" });
|
||||||
next(null, flagId);
|
next(null, flagId);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -318,7 +369,7 @@ Flags.exists = function (type, id, uid, callback) {
|
|||||||
|
|
||||||
Flags.targetExists = function (type, id, callback) {
|
Flags.targetExists = function (type, id, callback) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'topic':
|
case 'topic': // just an example...
|
||||||
topics.exists(id, callback);
|
topics.exists(id, callback);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -328,6 +379,14 @@ Flags.targetExists = function (type, id, callback) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Flags.getTargetUid = function (type, id, callback) {
|
||||||
|
switch (type) {
|
||||||
|
case 'post':
|
||||||
|
posts.getPostField(id, 'uid', callback);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Flags.update = function (flagId, uid, changeset, callback) {
|
Flags.update = function (flagId, uid, changeset, callback) {
|
||||||
// Retrieve existing flag data to compare for history-saving purposes
|
// Retrieve existing flag data to compare for history-saving purposes
|
||||||
var fields = ['state', 'assignee'];
|
var fields = ['state', 'assignee'];
|
||||||
@@ -369,7 +428,7 @@ Flags.update = function (flagId, uid, changeset, callback) {
|
|||||||
// Save new object to db (upsert)
|
// Save new object to db (upsert)
|
||||||
tasks.push(async.apply(db.setObject, 'flag:' + flagId, changeset));
|
tasks.push(async.apply(db.setObject, 'flag:' + flagId, changeset));
|
||||||
// Append history
|
// Append history
|
||||||
tasks.push(async.apply(Flags.appendHistory, flagId, uid, history))
|
tasks.push(async.apply(Flags.appendHistory, flagId, uid, history));
|
||||||
|
|
||||||
async.parallel(tasks, function (err, data) {
|
async.parallel(tasks, function (err, data) {
|
||||||
return next(err);
|
return next(err);
|
||||||
@@ -462,4 +521,56 @@ Flags.appendNote = function (flagId, uid, note, callback) {
|
|||||||
], callback);
|
], callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Flags.notify = function (flagObj, uid, callback) {
|
||||||
|
// Notify administrators, mods, and other associated people
|
||||||
|
switch (flagObj.type) {
|
||||||
|
case 'post':
|
||||||
|
async.parallel({
|
||||||
|
post: function (next) {
|
||||||
|
async.waterfall([
|
||||||
|
async.apply(posts.getPostData, flagObj.targetId),
|
||||||
|
async.apply(posts.parsePost)
|
||||||
|
], next);
|
||||||
|
},
|
||||||
|
title: async.apply(topics.getTitleByPid, flagObj.targetId),
|
||||||
|
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
|
||||||
|
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
|
||||||
|
moderators: function (next) {
|
||||||
|
async.waterfall([
|
||||||
|
async.apply(posts.getCidByPid, flagObj.targetId),
|
||||||
|
function (cid, next) {
|
||||||
|
groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, next);
|
||||||
|
}
|
||||||
|
], next);
|
||||||
|
}
|
||||||
|
}, function (err, results) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = S(results.title).decodeHTMLEntities().s;
|
||||||
|
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||||
|
|
||||||
|
notifications.create({
|
||||||
|
bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reporter.username + ', ' + titleEscaped + ']]',
|
||||||
|
bodyLong: results.post.content,
|
||||||
|
pid: flagObj.targetId,
|
||||||
|
path: '/post/' + flagObj.targetId,
|
||||||
|
nid: 'flag:post:' + flagObj.targetId + ':uid:' + uid,
|
||||||
|
from: uid,
|
||||||
|
mergeId: 'notifications:user_flagged_post_in|' + flagObj.targetId,
|
||||||
|
topicTitle: results.title
|
||||||
|
}, function (err, notification) {
|
||||||
|
if (err || !notification) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.fireHook('action:post.flag', {post: results.post, reason: flagObj.description, flaggingUser: flagObj.reporter});
|
||||||
|
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = Flags;
|
module.exports = Flags;
|
||||||
@@ -21,89 +21,22 @@ SocketFlags.create = function (socket, data, callback) {
|
|||||||
return callback(new Error('[[error:not-logged-in]]'));
|
return callback(new Error('[[error:not-logged-in]]'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || !data.pid || !data.reason) {
|
if (!data || !data.type || !data.id || !data.reason) {
|
||||||
return callback(new Error('[[error:invalid-data]]'));
|
return callback(new Error('[[error:invalid-data]]'));
|
||||||
}
|
}
|
||||||
|
|
||||||
var flaggingUser = {};
|
|
||||||
var post;
|
|
||||||
|
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
|
async.apply(flags.validate, {
|
||||||
|
uid: socket.uid,
|
||||||
|
type: data.type,
|
||||||
|
id: data.id
|
||||||
|
}),
|
||||||
function (next) {
|
function (next) {
|
||||||
posts.getPostFields(data.pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next);
|
// If we got here, then no errors occurred
|
||||||
},
|
flags.create(data.type, data.id, socket.uid, data.reason, next);
|
||||||
function (postData, next) {
|
|
||||||
if (parseInt(postData.deleted, 10) === 1) {
|
|
||||||
return next(new Error('[[error:post-deleted]]'));
|
|
||||||
}
|
|
||||||
|
|
||||||
post = postData;
|
|
||||||
topics.getTopicFields(post.tid, ['title', 'cid'], next);
|
|
||||||
},
|
|
||||||
function (topicData, next) {
|
|
||||||
post.topic = topicData;
|
|
||||||
|
|
||||||
async.parallel({
|
|
||||||
isAdminOrMod: function (next) {
|
|
||||||
privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next);
|
|
||||||
},
|
|
||||||
userData: function (next) {
|
|
||||||
user.getUserFields(socket.uid, ['username', 'reputation', 'banned'], next);
|
|
||||||
}
|
|
||||||
}, next);
|
|
||||||
},
|
|
||||||
function (user, next) {
|
|
||||||
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
|
|
||||||
if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < minimumReputation) {
|
|
||||||
return next(new Error('[[error:not-enough-reputation-to-flag]]'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parseInt(user.banned, 10) === 1) {
|
|
||||||
return next(new Error('[[error:user-banned]]'));
|
|
||||||
}
|
|
||||||
|
|
||||||
flaggingUser = user.userData;
|
|
||||||
flaggingUser.uid = socket.uid;
|
|
||||||
|
|
||||||
flags.create('post', post.pid, socket.uid, data.reason, next);
|
|
||||||
},
|
},
|
||||||
function (flagObj, next) {
|
function (flagObj, next) {
|
||||||
async.parallel({
|
flags.notify(flagObj, socket.uid, next);
|
||||||
post: function (next) {
|
|
||||||
posts.parsePost(post, next);
|
|
||||||
},
|
|
||||||
admins: function (next) {
|
|
||||||
groups.getMembers('administrators', 0, -1, next);
|
|
||||||
},
|
|
||||||
globalMods: function (next) {
|
|
||||||
groups.getMembers('Global Moderators', 0, -1, next);
|
|
||||||
},
|
|
||||||
moderators: function (next) {
|
|
||||||
groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next);
|
|
||||||
}
|
|
||||||
}, next);
|
|
||||||
},
|
|
||||||
function (results, next) {
|
|
||||||
var title = S(post.topic.title).decodeHTMLEntities().s;
|
|
||||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
|
||||||
|
|
||||||
notifications.create({
|
|
||||||
bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + titleEscaped + ']]',
|
|
||||||
bodyLong: post.content,
|
|
||||||
pid: data.pid,
|
|
||||||
path: '/post/' + data.pid,
|
|
||||||
nid: 'post_flag:' + data.pid + ':uid:' + socket.uid,
|
|
||||||
from: socket.uid,
|
|
||||||
mergeId: 'notifications:user_flagged_post_in|' + data.pid,
|
|
||||||
topicTitle: post.topic.title
|
|
||||||
}, function (err, notification) {
|
|
||||||
if (err || !notification) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins.fireHook('action:post.flag', {post: post, reason: data.reason, flaggingUser: flaggingUser});
|
|
||||||
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), next);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
], callback);
|
], callback);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user