From 0ced71be391ab77089d9e29170116f4a7213754a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 26 Sep 2019 22:51:11 -0400 Subject: [PATCH] refactor: async/await flags --- src/flags.js | 415 ++++++++++++++++--------------------------- src/topics/sorted.js | 2 +- 2 files changed, 156 insertions(+), 261 deletions(-) diff --git a/src/flags.js b/src/flags.js index c8b79e47b1..ec01d6d0d4 100644 --- a/src/flags.js +++ b/src/flags.js @@ -105,14 +105,11 @@ Flags.get = async function (flagId) { return data.flag; }; -Flags.list = function (filters, uid, callback) { - if (typeof filters === 'function' && !uid && !callback) { - callback = filters; - filters = {}; - } +Flags.list = async function (filters, uid) { + filters = filters || {}; - var sets = []; - var orSets = []; + let sets = []; + const orSets = []; // Default filter filters.page = filters.hasOwnProperty('page') ? Math.abs(parseInt(filters.page, 10) || 1) : 1; @@ -129,279 +126,177 @@ Flags.list = function (filters, uid, callback) { } sets = (sets.length || orSets.length) ? sets : ['flags:datetime']; // No filter default - async.waterfall([ - function (next) { - if (sets.length === 1) { - db.getSortedSetRevRange(sets[0], 0, -1, next); - } else if (sets.length > 1) { - db.getSortedSetRevIntersect({ sets: sets, start: 0, stop: -1, aggregate: 'MAX' }, next); - } else { - next(null, []); - } - }, - function (flagIds, next) { - // Find flags according to "or" rules, if any - if (orSets.length) { - db.getSortedSetRevUnion({ sets: orSets, start: 0, stop: -1, aggregate: 'MAX' }, function (err, _flagIds) { - if (err) { - return next(err); - } - - if (sets.length) { - // If flag ids are already present, return a subset of flags that are in both sets - next(null, _.intersection(flagIds, _flagIds)); - } else { - // Otherwise, return all flags returned via orSets - next(null, _.union(flagIds, _flagIds)); - } - }); - } else { - setImmediate(next, null, flagIds); - } - }, - function (flagIds, next) { - // Create subset for parsing based on page number (n=20) - const flagsPerPage = Math.abs(parseInt(filters.perPage, 10) || 1); - const pageCount = Math.ceil(flagIds.length / flagsPerPage); - flagIds = flagIds.slice((filters.page - 1) * flagsPerPage, filters.page * flagsPerPage); - - async.map(flagIds, function (flagId, next) { - async.waterfall([ - async.apply(db.getObject, 'flag:' + flagId), - function (flagObj, next) { - user.getUserFields(flagObj.uid, ['username', 'picture'], function (err, userObj) { - next(err, { state: 'open', - ...flagObj, - reporter: { - username: userObj.username, - picture: userObj.picture, - 'icon:bgColor': userObj['icon:bgColor'], - 'icon:text': userObj['icon:text'], - } }); - }); - }, - ], function (err, flagObj) { - if (err) { - return next(err); - } - - switch (flagObj.state) { - case 'open': - flagObj.labelClass = 'info'; - break; - case 'wip': - flagObj.labelClass = 'warning'; - break; - case 'resolved': - flagObj.labelClass = 'success'; - break; - case 'rejected': - flagObj.labelClass = 'danger'; - break; - } - - next(null, Object.assign(flagObj, { - description: validator.escape(String(flagObj.description)), - target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId, - datetimeISO: utils.toISOString(flagObj.datetime), - })); - }); - }, function (err, flags) { - next(err, flags, pageCount); - }); - }, - function (flags, pageCount, next) { - plugins.fireHook('filter:flags.list', { - flags: flags, - page: filters.page, - }, function (err, data) { - next(err, { - flags: data.flags, - page: data.page, - pageCount: pageCount, - }); - }); - }, - ], callback); -}; - -Flags.validate = function (payload, callback) { - async.parallel({ - 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); - } + let flagIds = []; + if (sets.length === 1) { + flagIds = await db.getSortedSetRevRange(sets[0], 0, -1); + } else if (sets.length > 1) { + flagIds = await db.getSortedSetRevIntersect({ sets: sets, start: 0, stop: -1, aggregate: 'MAX' }); + } - if (!data.target) { - return callback(new Error('[[error:invalid-data]]')); - } else if (data.target.deleted) { - return callback(new Error('[[error:post-deleted]]')); - } else if (!data.reporter || !data.reporter.userslug) { - return callback(new Error('[[error:no-user]]')); - } else if (data.reporter.banned) { - return callback(new Error('[[error:user-banned]]')); + if (orSets.length) { + const _flagIds = await db.getSortedSetRevUnion({ sets: orSets, start: 0, stop: -1, aggregate: 'MAX' }); + if (sets.length) { + // If flag ids are already present, return a subset of flags that are in both sets + flagIds = _.intersection(flagIds, _flagIds); + } else { + // Otherwise, return all flags returned via orSets + flagIds = _.union(flagIds, _flagIds); } + } - switch (payload.type) { - case 'post': - privileges.posts.canEdit(payload.id, payload.uid, function (err, editable) { - if (err) { - return callback(err); - } - - // Check if reporter meets rep threshold (or can edit the target post, in which case threshold does not apply) - if (!editable.flag && !meta.config['reputation:disabled'] && data.reporter.reputation < meta.config['min:rep:flag']) { - return callback(new Error('[[error:not-enough-reputation-to-flag]]')); - } + // Create subset for parsing based on page number (n=20) + const flagsPerPage = Math.abs(parseInt(filters.perPage, 10) || 1); + const pageCount = Math.ceil(flagIds.length / flagsPerPage); + flagIds = flagIds.slice((filters.page - 1) * flagsPerPage, filters.page * flagsPerPage); + + const flags = await Promise.all(flagIds.map(async (flagId) => { + let flagObj = await db.getObject('flag:' + flagId); + const userObj = await user.getUserFields(flagObj.uid, ['username', 'picture']); + flagObj = { + state: 'open', + ...flagObj, + reporter: { + username: userObj.username, + picture: userObj.picture, + 'icon:bgColor': userObj['icon:bgColor'], + 'icon:text': userObj['icon:text'], + }, + }; + const stateToLabel = { + open: 'info', + wip: 'warning', + resolved: 'success', + rejected: 'danger', + }; + flagObj.labelClass = stateToLabel[flagObj.state]; + + return Object.assign(flagObj, { + description: validator.escape(String(flagObj.description)), + target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId, + datetimeISO: utils.toISOString(flagObj.datetime), + }); + })); - callback(); - }); - break; + const data = await plugins.fireHook('filter:flags.list', { + flags: flags, + page: filters.page, + }); - case 'user': - privileges.users.canEdit(payload.uid, payload.id, function (err, editable) { - if (err) { - return callback(err); - } + return { + flags: data.flags, + page: data.page, + pageCount: pageCount, + }; +}; - // Check if reporter meets rep threshold (or can edit the target user, in which case threshold does not apply) - if (!editable && !meta.config['reputation:disabled'] && data.reporter.reputation < meta.config['min:rep:flag']) { - return callback(new Error('[[error:not-enough-reputation-to-flag]]')); - } +Flags.validate = async function (payload) { + const [target, reporter] = await Promise.all([ + Flags.getTarget(payload.type, payload.id, payload.uid), + user.getUserData(payload.uid), + ]); - callback(); - }); - break; + if (!target) { + throw new Error('[[error:invalid-data]]'); + } else if (target.deleted) { + throw new Error('[[error:post-deleted]]'); + } else if (!reporter || !reporter.userslug) { + throw new Error('[[error:no-user]]'); + } else if (reporter.banned) { + throw new Error('[[error:user-banned]]'); + } - default: - callback(new Error('[[error:invalid-data]]')); - break; + if (payload.type === 'post') { + const editable = await privileges.posts.canEdit(payload.id, payload.uid); + if (!editable.flag && !meta.config['reputation:disabled'] && reporter.reputation < meta.config['min:rep:flag']) { + throw new Error('[[error:not-enough-reputation-to-flag]]'); } - }); + } else if (payload.type === 'user') { + const editable = await privileges.users.canEdit(payload.uid, payload.id); + if (!editable && !meta.config['reputation:disabled'] && reporter.reputation < meta.config['min:rep:flag']) { + throw new Error('[[error:not-enough-reputation-to-flag]]'); + } + } else { + throw new Error('[[error:invalid-data]]'); + } }; -Flags.getNotes = function (flagId, callback) { - async.waterfall([ - async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':notes', 0, -1), - function (notes, next) { - var uids = []; - var noteObj; - notes = notes.map(function (note) { - try { - noteObj = JSON.parse(note.value); - uids.push(noteObj[0]); - return { - uid: noteObj[0], - content: noteObj[1], - datetime: note.score, - datetimeISO: utils.toISOString(note.score), - }; - } catch (e) { - return next(e); - } - }); - next(null, notes, uids); - }, - function (notes, uids, next) { - user.getUsersFields(uids, ['username', 'userslug', 'picture'], function (err, users) { - if (err) { - return next(err); - } - - next(null, notes.map(function (note, idx) { - note.user = users[idx]; - note.content = validator.escape(note.content); - return note; - })); - }); - }, - ], callback); +Flags.getNotes = async function (flagId) { + let notes = await db.getSortedSetRevRangeWithScores('flag:' + flagId + ':notes', 0, -1); + const uids = []; + notes = notes.map(function (note) { + const noteObj = JSON.parse(note.value); + uids.push(noteObj[0]); + return { + uid: noteObj[0], + content: noteObj[1], + datetime: note.score, + datetimeISO: utils.toISOString(note.score), + }; + }); + const userData = await user.getUsersFields(uids, ['username', 'userslug', 'picture']); + return notes.map(function (note, idx) { + note.user = userData[idx]; + note.content = validator.escape(note.content); + return note; + }); }; -Flags.create = function (type, id, uid, reason, timestamp, callback) { - var targetUid; - var targetCid; - var doHistoryAppend = false; - - // timestamp is optional - if (typeof timestamp === 'function' && !callback) { - callback = timestamp; +Flags.create = async function (type, id, uid, reason, timestamp) { + let doHistoryAppend = false; + if (!timestamp) { timestamp = Date.now(); doHistoryAppend = true; } + const [exists, targetExists, targetUid, targetCid] = await Promise.all([ + // Sanity checks + Flags.exists(type, id, uid), + Flags.targetExists(type, id), + // Extra data for zset insertion + Flags.getTargetUid(type, id), + Flags.getTargetCid(type, id), + ]); + if (exists) { + throw new Error('[[error:already-flagged]]'); + } else if (!targetExists) { + throw new Error('[[error:invalid-data]]'); + } + const flagId = await db.incrObjectField('global', 'nextFlagId'); + + await db.setObject('flag:' + flagId, { + flagId: flagId, + type: type, + targetId: id, + description: reason, + uid: uid, + datetime: timestamp, + }); + await db.sortedSetAdd('flags:datetime', timestamp, flagId); // by time, the default + 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 analytics.increment('flags'); // some fancy analytics + + if (targetUid) { + await db.sortedSetAdd('flags:byTargetUid:' + targetUid, timestamp, flagId); // by target uid + } - async.waterfall([ - function (next) { - async.parallel([ - // Sanity checks - async.apply(Flags.exists, type, id, uid), - async.apply(Flags.targetExists, type, id), - - // Extra data for zset insertion - async.apply(Flags.getTargetUid, type, id), - async.apply(Flags.getTargetCid, type, id), - ], function (err, checks) { - if (err) { - return next(err); - } - - targetUid = checks[2] || null; - targetCid = checks[3] || null; - - if (checks[0]) { - return next(new Error('[[error:already-flagged]]')); - } else if (!checks[1]) { - return next(new Error('[[error:invalid-data]]')); - } - next(); - }); - }, - async.apply(db.incrObjectField, 'global', 'nextFlagId'), - function (flagId, next) { - var tasks = [ - async.apply(db.setObject.bind(db), 'flag:' + flagId, { - flagId: flagId, - type: type, - targetId: id, - description: reason, - uid: uid, - datetime: timestamp, - }), - async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', timestamp, flagId), // by time, the default - async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, timestamp, flagId), // by reporter - async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, timestamp, flagId), // by flag type - async.apply(db.sortedSetAdd.bind(db), 'flags:hash', flagId, [type, id, uid].join(':')), // save zset for duplicate checking - async.apply(analytics.increment, 'flags'), // some fancy analytics - ]; - - if (targetUid) { - tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, timestamp, flagId)); // by target uid - } - - if (targetCid) { - tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byCid:' + targetCid, timestamp, flagId)); // by target cid - } + if (targetCid) { + await db.sortedSetAdd('flags:byCid:' + targetCid, timestamp, flagId); // by target cid + } - if (type === 'post') { - tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byPid:' + id, timestamp, flagId)); // by target pid - if (targetUid) { - tasks.push(async.apply(db.sortedSetIncrBy.bind(db), 'users:flags', 1, targetUid)); - tasks.push(async.apply(user.incrementUserFieldBy, targetUid, 'flags', 1)); - } - } + if (type === 'post') { + await db.sortedSetAdd('flags:byPid:' + id, timestamp, flagId); // by target pid + if (targetUid) { + await db.sortedSetIncrBy('users:flags', 1, targetUid); + await user.incrementUserFieldBy(targetUid, 'flags', 1); + } + } - if (doHistoryAppend) { - tasks.push(async.apply(Flags.update, flagId, uid, { state: 'open' })); - } + if (doHistoryAppend) { + await Flags.update(flagId, uid, { state: 'open' }); + } - async.series(tasks, function (err) { - next(err, flagId); - }); - }, - async.apply(Flags.get), - ], callback); + return await Flags.get(flagId); }; Flags.exists = async function (type, id, uid) { diff --git a/src/topics/sorted.js b/src/topics/sorted.js index 4048187e8b..7c6f7933d3 100644 --- a/src/topics/sorted.js +++ b/src/topics/sorted.js @@ -12,7 +12,7 @@ const plugins = require('../plugins'); module.exports = function (Topics) { Topics.getSortedTopics = async function (params) { - var data = { + const data = { nextStart: 0, topicCount: 0, topics: [],