mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-20 15:30:39 +01:00
flag updating and note appending, #5232
This commit is contained in:
@@ -14,5 +14,7 @@
|
||||
"state-wip": "Work in Progress",
|
||||
"state-resolved": "Resolved",
|
||||
"state-rejected": "Rejected",
|
||||
"no-assignee": "Not Assigned"
|
||||
"no-assignee": "Not Assigned",
|
||||
"updated": "Flag Details Updated",
|
||||
"note-added": "Note Added"
|
||||
}
|
||||
60
public/src/client/flags/detail.js
Normal file
60
public/src/client/flags/detail.js
Normal file
@@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
|
||||
/* globals define */
|
||||
|
||||
define('forum/flags/detail', ['components'], function (components) {
|
||||
var Flags = {};
|
||||
|
||||
Flags.init = function () {
|
||||
// Update attributes
|
||||
$('#state').val(ajaxify.data.state).removeAttr('disabled');
|
||||
$('#assignee').val(ajaxify.data.assignee).removeAttr('disabled');
|
||||
|
||||
$('[data-action]').on('click', function () {
|
||||
var action = this.getAttribute('data-action');
|
||||
|
||||
switch (action) {
|
||||
case 'update':
|
||||
socket.emit('flags.update', {
|
||||
flagId: ajaxify.data.flagId,
|
||||
data: $('#attributes').serializeArray()
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
} else {
|
||||
app.alertSuccess('[[flags:updated]]');
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'appendNote':
|
||||
socket.emit('flags.appendNote', {
|
||||
flagId: ajaxify.data.flagId,
|
||||
note: document.getElementById('note').value
|
||||
}, function (err, notes) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
} else {
|
||||
app.alertSuccess('[[flags:note-added]]');
|
||||
Flags.reloadNotes(notes);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Flags.reloadNotes = function (notes) {
|
||||
templates.parse('flags/detail', 'notes', {
|
||||
notes: notes
|
||||
}, function (html) {
|
||||
var wrapperEl = components.get('flag/notes');
|
||||
wrapperEl.empty();
|
||||
wrapperEl.html(html);
|
||||
wrapperEl.find('span.timeago').timeago();
|
||||
document.getElementById('note').value = '';
|
||||
});
|
||||
};
|
||||
|
||||
return Flags;
|
||||
});
|
||||
@@ -48,7 +48,7 @@ define('forum/topic/flag', [], function () {
|
||||
if (!pid || !reason) {
|
||||
return;
|
||||
}
|
||||
socket.emit('posts.flag', {pid: pid, reason: reason}, function (err) {
|
||||
socket.emit('flags.create', {pid: pid, reason: reason}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
146
src/flags.js
146
src/flags.js
@@ -373,138 +373,52 @@ Flags.dismissByUid = function (uid, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
// New method signature (type, id, flagObj, callback) and name (.update())
|
||||
// uid used in history string, which should be rewritten too.
|
||||
Flags.update = function (uid, pid, flagObj, callback) {
|
||||
Flags.update = function (flagId, uid, changeset, callback) {
|
||||
// Retrieve existing flag data to compare for history-saving purposes
|
||||
var changes = [];
|
||||
var changeset = {};
|
||||
var prop;
|
||||
var fields = ['state', 'assignee'];
|
||||
|
||||
posts.getPostData(pid, function (err, postData) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
async.waterfall([
|
||||
async.apply(db.getObjectFields.bind(db), 'flag:' + flagId, fields),
|
||||
function (current, next) {
|
||||
for(var prop in changeset) {
|
||||
if (changeset.hasOwnProperty(prop)) {
|
||||
if (current[prop] === changeset[prop]) {
|
||||
delete changeset[prop];
|
||||
}
|
||||
|
||||
// Track new additions
|
||||
for(prop in flagObj) {
|
||||
if (flagObj.hasOwnProperty(prop) && !postData.hasOwnProperty('flag:' + prop) && flagObj[prop].length) {
|
||||
changes.push(prop);
|
||||
}
|
||||
}
|
||||
|
||||
// Track changed items
|
||||
for(prop in postData) {
|
||||
if (
|
||||
postData.hasOwnProperty(prop) && prop.startsWith('flag:') &&
|
||||
flagObj.hasOwnProperty(prop.slice(5)) &&
|
||||
postData[prop] !== flagObj[prop.slice(5)]
|
||||
) {
|
||||
changes.push(prop.slice(5));
|
||||
}
|
||||
if (!Object.keys(changeset).length) {
|
||||
// No changes
|
||||
return next();
|
||||
}
|
||||
|
||||
changeset = changes.reduce(function (memo, prop) {
|
||||
memo['flag:' + prop] = flagObj[prop];
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
// Append changes to history string
|
||||
if (changes.length) {
|
||||
try {
|
||||
var history = JSON.parse(postData['flag:history'] || '[]');
|
||||
|
||||
changes.forEach(function (property) {
|
||||
switch(property) {
|
||||
case 'assignee': // intentional fall-through
|
||||
case 'state':
|
||||
history.unshift({
|
||||
uid: uid,
|
||||
type: property,
|
||||
value: flagObj[property],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
break;
|
||||
|
||||
case 'notes':
|
||||
history.unshift({
|
||||
uid: uid,
|
||||
type: property,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
async.parallel([
|
||||
// Save new object to db (upsert)
|
||||
async.apply(db.setObject, 'flag:' + flagId, changeset),
|
||||
// Append history
|
||||
async.apply(Flags.appendHistory, flagId, uid, Object.keys(changeset))
|
||||
], next);
|
||||
}
|
||||
});
|
||||
|
||||
changeset['flag:history'] = JSON.stringify(history);
|
||||
} catch (e) {
|
||||
winston.warn('[flags/update] Unable to deserialise post flag history, likely malformed data');
|
||||
}
|
||||
}
|
||||
|
||||
// Save flag data into post hash
|
||||
if (changes.length) {
|
||||
posts.setPostFields(pid, changeset, callback);
|
||||
} else {
|
||||
setImmediate(callback);
|
||||
}
|
||||
});
|
||||
], callback);
|
||||
};
|
||||
|
||||
// To be rewritten and deprecated
|
||||
Flags.expandFlagHistory = function (posts, callback) {
|
||||
// Expand flag history
|
||||
async.map(posts, function (post, next) {
|
||||
var history;
|
||||
Flags.appendHistory = function (flagId, uid, changeset, callback) {
|
||||
return callback();
|
||||
};
|
||||
|
||||
Flags.appendNote = function (flagId, uid, note, callback) {
|
||||
var payload;
|
||||
try {
|
||||
history = JSON.parse(post['flag:history'] || '[]');
|
||||
payload = JSON.stringify([uid, note]);
|
||||
} catch (e) {
|
||||
winston.warn('[flags/get] Unable to deserialise post flag history, likely malformed data');
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
async.map(history, function (event, next) {
|
||||
event.timestampISO = new Date(event.timestamp).toISOString();
|
||||
|
||||
async.parallel([
|
||||
function (next) {
|
||||
user.getUserFields(event.uid, ['username', 'picture'], function (err, userData) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
event.user = userData;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
if (event.type === 'assignee') {
|
||||
user.getUserField(parseInt(event.value, 10), 'username', function (err, username) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
event.label = username || 'Unknown user';
|
||||
next(null);
|
||||
});
|
||||
} else if (event.type === 'state') {
|
||||
event.label = '[[topic:flag_manage_state_' + event.value + ']]';
|
||||
setImmediate(next);
|
||||
} else {
|
||||
setImmediate(next);
|
||||
}
|
||||
}
|
||||
], function (err) {
|
||||
next(err, event);
|
||||
});
|
||||
}, function (err, history) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
post['flag:history'] = history;
|
||||
next(null, post);
|
||||
});
|
||||
}, callback);
|
||||
async.waterfall([
|
||||
async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', Date.now(), payload),
|
||||
async.apply(Flags.getNotes, flagId)
|
||||
], callback);
|
||||
};
|
||||
|
||||
module.exports = Flags;
|
||||
167
src/socket.io/flags.js
Normal file
167
src/socket.io/flags.js
Normal file
@@ -0,0 +1,167 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var S = require('string');
|
||||
|
||||
var user = require('../user');
|
||||
var groups = require('../groups');
|
||||
var posts = require('../posts');
|
||||
var topics = require('../topics');
|
||||
var privileges = require('../privileges');
|
||||
var notifications = require('../notifications');
|
||||
var plugins = require('../plugins');
|
||||
var meta = require('../meta');
|
||||
var utils = require('../../public/src/utils');
|
||||
var flags = require('../flags');
|
||||
|
||||
var SocketFlags = {};
|
||||
|
||||
SocketFlags.create = function (socket, data, callback) {
|
||||
if (!socket.uid) {
|
||||
return callback(new Error('[[error:not-logged-in]]'));
|
||||
}
|
||||
|
||||
if (!data || !data.pid || !data.reason) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
var flaggingUser = {};
|
||||
var post;
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
posts.getPostFields(data.pid, ['pid', 'tid', 'uid', 'content', 'deleted'], 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 (next) {
|
||||
async.parallel({
|
||||
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);
|
||||
};
|
||||
|
||||
SocketFlags.update = function (socket, data, callback) {
|
||||
if (!data || !(data.flagId && data.data)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
var payload = {};
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
async.apply(user.isAdminOrGlobalMod, socket.uid),
|
||||
async.apply(user.isModeratorOfAnyCategory, socket.uid)
|
||||
], function (err, results) {
|
||||
next(err, results[0] || results[1]);
|
||||
});
|
||||
},
|
||||
function (allowed, next) {
|
||||
if (!allowed) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
|
||||
// Translate form data into object
|
||||
payload = data.data.reduce(function (memo, cur) {
|
||||
memo[cur.name] = cur.value;
|
||||
return memo;
|
||||
}, payload);
|
||||
|
||||
flags.update(data.flagId, socket.uid, payload, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketFlags.appendNote = function (socket, data, callback) {
|
||||
if (!data || !(data.flagId && data.note)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
async.apply(user.isAdminOrGlobalMod, socket.uid),
|
||||
async.apply(user.isModeratorOfAnyCategory, socket.uid)
|
||||
], function (err, results) {
|
||||
next(err, results[0] || results[1]);
|
||||
});
|
||||
},
|
||||
function (allowed, next) {
|
||||
if (!allowed) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
|
||||
flags.appendNote(data.flagId, socket.uid, data.note, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
module.exports = SocketFlags;
|
||||
@@ -123,8 +123,10 @@ var ratelimit = require('../middleware/ratelimit');
|
||||
}
|
||||
|
||||
function requireModules() {
|
||||
var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
|
||||
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist'
|
||||
var modules = [
|
||||
'admin', 'categories', 'groups', 'meta', 'modules',
|
||||
'notifications', 'plugins', 'posts', 'topics', 'user',
|
||||
'blacklist', 'flags'
|
||||
];
|
||||
|
||||
modules.forEach(function (module) {
|
||||
|
||||
@@ -20,7 +20,6 @@ require('./posts/move')(SocketPosts);
|
||||
require('./posts/votes')(SocketPosts);
|
||||
require('./posts/bookmarks')(SocketPosts);
|
||||
require('./posts/tools')(SocketPosts);
|
||||
require('./posts/flag')(SocketPosts);
|
||||
|
||||
SocketPosts.reply = function (socket, data, callback) {
|
||||
if (!data || !data.tid || !data.content) {
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var S = require('string');
|
||||
|
||||
var user = require('../../user');
|
||||
var groups = require('../../groups');
|
||||
var posts = require('../../posts');
|
||||
var topics = require('../../topics');
|
||||
var privileges = require('../../privileges');
|
||||
var notifications = require('../../notifications');
|
||||
var plugins = require('../../plugins');
|
||||
var meta = require('../../meta');
|
||||
var utils = require('../../../public/src/utils');
|
||||
var flags = require('../../flags');
|
||||
|
||||
module.exports = function (SocketPosts) {
|
||||
|
||||
SocketPosts.flag = function (socket, data, callback) {
|
||||
if (!socket.uid) {
|
||||
return callback(new Error('[[error:not-logged-in]]'));
|
||||
}
|
||||
|
||||
if (!data || !data.pid || !data.reason) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
var flaggingUser = {};
|
||||
var post;
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
posts.getPostFields(data.pid, ['pid', 'tid', 'uid', 'content', 'deleted'], 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 (next) {
|
||||
async.parallel({
|
||||
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);
|
||||
};
|
||||
|
||||
SocketPosts.dismissFlag = function (socket, pid, callback) {
|
||||
if (!pid || !socket.uid) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.isAdminOrGlobalMod(socket.uid, next);
|
||||
},
|
||||
function (isAdminOrGlobalModerator, next) {
|
||||
if (!isAdminOrGlobalModerator) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
flags.dismiss(pid, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketPosts.dismissAllFlags = function (socket, data, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.isAdminOrGlobalMod(socket.uid, next);
|
||||
},
|
||||
function (isAdminOrGlobalModerator, next) {
|
||||
if (!isAdminOrGlobalModerator) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
flags.dismissAll(next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketPosts.updateFlag = function (socket, data, callback) {
|
||||
if (!data || !(data.pid && data.data)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
var payload = {};
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
async.apply(user.isAdminOrGlobalMod, socket.uid),
|
||||
async.apply(user.isModeratorOfAnyCategory, socket.uid)
|
||||
], function (err, results) {
|
||||
next(err, results[0] || results[1]);
|
||||
});
|
||||
},
|
||||
function (allowed, next) {
|
||||
if (!allowed) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
|
||||
// Translate form data into object
|
||||
payload = data.data.reduce(function (memo, cur) {
|
||||
memo[cur.name] = cur.value;
|
||||
return memo;
|
||||
}, payload);
|
||||
|
||||
flags.update(socket.uid, data.pid, payload, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user