From fcf3e0770bb1e313ce2e287426e422edb6a1a465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 16 Jul 2019 00:41:42 -0400 Subject: [PATCH] feat: #7743 categories --- src/categories/activeusers.js | 25 +- src/categories/create.js | 398 +++++++++++-------------- src/categories/data.js | 72 ++--- src/categories/delete.js | 149 ++++------ src/categories/index.js | 430 ++++++++++------------------ src/categories/recentreplies.js | 299 +++++++------------ src/categories/topics.js | 235 ++++++--------- src/categories/unread.js | 44 ++- src/categories/update.js | 202 +++++-------- src/categories/watch.js | 76 ++--- src/database/mongo/sorted/remove.js | 4 +- src/database/redis/pubsub.js | 2 +- src/socket.io/admin/categories.js | 2 +- 13 files changed, 711 insertions(+), 1227 deletions(-) diff --git a/src/categories/activeusers.js b/src/categories/activeusers.js index 726f612a6a..5164868529 100644 --- a/src/categories/activeusers.js +++ b/src/categories/activeusers.js @@ -1,28 +1,17 @@ 'use strict'; -var async = require('async'); -var _ = require('lodash'); +const _ = require('lodash'); -var posts = require('../posts'); -var db = require('../database'); +const posts = require('../posts'); +const db = require('../database'); module.exports = function (Categories) { - Categories.getActiveUsers = function (cids, callback) { + Categories.getActiveUsers = async function (cids) { if (!Array.isArray(cids)) { cids = [cids]; } - async.waterfall([ - function (next) { - db.getSortedSetRevRange(cids.map(cid => 'cid:' + cid + ':pids'), 0, 24, next); - }, - function (pids, next) { - posts.getPostsFields(pids, ['uid'], next); - }, - function (posts, next) { - var uids = _.uniq(posts.map(post => post.uid).filter(uid => uid)); - - next(null, uids); - }, - ], callback); + const pids = await db.getSortedSetRevRange(cids.map(cid => 'cid:' + cid + ':pids'), 0, 24); + const postData = await posts.getPostsFields(pids, ['uid']); + return _.uniq(postData.map(post => post.uid).filter(uid => uid)); }; }; diff --git a/src/categories/create.js b/src/categories/create.js index 5a1e318a6f..d461b28f91 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -11,125 +11,102 @@ var utils = require('../utils'); var cache = require('../cache'); module.exports = function (Categories) { - Categories.create = function (data, callback) { - var category; - var parentCid = data.parentCid ? data.parentCid : 0; - - async.waterfall([ - function (next) { - db.incrObjectField('global', 'nextCid', next); - }, - function (cid, next) { - data.name = data.name || 'Category ' + cid; - var slug = cid + '/' + utils.slugify(data.name); - var order = data.order || cid; // If no order provided, place it at the end - var colours = Categories.assignColours(); - - category = { - cid: cid, - name: data.name, - description: data.description ? data.description : '', - descriptionParsed: data.descriptionParsed ? data.descriptionParsed : '', - icon: data.icon ? data.icon : '', - bgColor: data.bgColor || colours[0], - color: data.color || colours[1], - slug: slug, - parentCid: parentCid, - topic_count: 0, - post_count: 0, - disabled: data.disabled ? 1 : 0, - order: order, - link: data.link || '', - numRecentReplies: 1, - class: (data.class ? data.class : 'col-md-3 col-xs-6'), - imageClass: 'cover', - isSection: 0, - }; - - if (data.backgroundImage) { - category.backgroundImage = data.backgroundImage; - } - - plugins.fireHook('filter:category.create', { category: category, data: data }, next); - }, - function (data, next) { - category = data.category; - - var defaultPrivileges = [ - 'find', - 'read', - 'topics:read', - 'topics:create', - 'topics:reply', - 'topics:tag', - 'posts:edit', - 'posts:history', - 'posts:delete', - 'posts:upvote', - 'posts:downvote', - 'topics:delete', - ]; - const modPrivileges = defaultPrivileges.concat([ - 'posts:view_deleted', - 'purge', - ]); - - async.series([ - async.apply(db.setObject, 'category:' + category.cid, category), - function (next) { - if (category.descriptionParsed) { - return next(); - } - Categories.parseDescription(category.cid, category.description, next); - }, - async.apply(db.sortedSetsAdd, ['categories:cid', 'cid:' + parentCid + ':children'], category.order, category.cid), - async.apply(privileges.categories.give, defaultPrivileges, category.cid, 'registered-users'), - async.apply(privileges.categories.give, modPrivileges, category.cid, ['administrators', 'Global Moderators']), - async.apply(privileges.categories.give, ['find', 'read', 'topics:read'], category.cid, ['guests', 'spiders']), - ], next); - }, - function (results, next) { - cache.del(['categories:cid', 'cid:' + parentCid + ':children']); - if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) { - return Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid, next); - } - - next(null, category); - }, - function (_category, next) { - category = _category; - if (data.cloneChildren) { - return duplicateCategoriesChildren(category.cid, data.cloneFromCid, data.uid, next); - } - - next(); - }, - function (next) { - plugins.fireHook('action:category.create', { category: category }); - next(null, category); - }, - ], callback); - }; + Categories.create = async function (data) { + const parentCid = data.parentCid ? data.parentCid : 0; + const cid = await db.incrObjectField('global', 'nextCid'); + + data.name = data.name || 'Category ' + cid; + const slug = cid + '/' + utils.slugify(data.name); + const order = data.order || cid; // If no order provided, place it at the end + const colours = Categories.assignColours(); + + let category = { + cid: cid, + name: data.name, + description: data.description ? data.description : '', + descriptionParsed: data.descriptionParsed ? data.descriptionParsed : '', + icon: data.icon ? data.icon : '', + bgColor: data.bgColor || colours[0], + color: data.color || colours[1], + slug: slug, + parentCid: parentCid, + topic_count: 0, + post_count: 0, + disabled: data.disabled ? 1 : 0, + order: order, + link: data.link || '', + numRecentReplies: 1, + class: (data.class ? data.class : 'col-md-3 col-xs-6'), + imageClass: 'cover', + isSection: 0, + }; + + if (data.backgroundImage) { + category.backgroundImage = data.backgroundImage; + } - function duplicateCategoriesChildren(parentCid, cid, uid, callback) { - Categories.getChildren([cid], uid, function (err, children) { - if (err || !children.length) { - return callback(err); - } + const result = await plugins.fireHook('filter:category.create', { category: category, data: data }); + category = result.category; + + const defaultPrivileges = [ + 'find', + 'read', + 'topics:read', + 'topics:create', + 'topics:reply', + 'topics:tag', + 'posts:edit', + 'posts:history', + 'posts:delete', + 'posts:upvote', + 'posts:downvote', + 'topics:delete', + ]; + const modPrivileges = defaultPrivileges.concat([ + 'posts:view_deleted', + 'purge', + ]); + + await db.setObject('category:' + category.cid, category); + if (!category.descriptionParsed) { + await Categories.parseDescription(category.cid, category.description); + } + await db.sortedSetsAdd(['categories:cid', 'cid:' + parentCid + ':children'], category.order, category.cid); + await privileges.categories.give(defaultPrivileges, category.cid, 'registered-users'); + await privileges.categories.give(modPrivileges, category.cid, ['administrators', 'Global Moderators']); + await privileges.categories.give(['find', 'read', 'topics:read'], category.cid, ['guests', 'spiders']); + + cache.del(['categories:cid', 'cid:' + parentCid + ':children']); + if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) { + category = await Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid); + } - children = children[0]; + if (data.cloneChildren) { + await duplicateCategoriesChildren(category.cid, data.cloneFromCid, data.uid); + } - children.forEach(function (child) { - child.parentCid = parentCid; - child.cloneFromCid = child.cid; - child.cloneChildren = true; - child.name = utils.decodeHTMLEntities(child.name); - child.description = utils.decodeHTMLEntities(child.description); - child.uid = uid; - }); + plugins.fireHook('action:category.create', { category: category }); + return category; + }; - async.each(children, Categories.create, callback); + async function duplicateCategoriesChildren(parentCid, cid, uid) { + let children = await Categories.getChildren([cid], uid); + if (!children.length) { + return; + } + + children = children[0]; + + children.forEach(function (child) { + child.parentCid = parentCid; + child.cloneFromCid = child.cid; + child.cloneChildren = true; + child.name = utils.decodeHTMLEntities(child.name); + child.description = utils.decodeHTMLEntities(child.description); + child.uid = uid; }); + + await async.each(children, Categories.create); } Categories.assignColours = function () { @@ -140,136 +117,89 @@ module.exports = function (Categories) { return [backgrounds[index], text[index]]; }; - Categories.copySettingsFrom = function (fromCid, toCid, copyParent, callback) { - var destination; - async.waterfall([ - function (next) { - async.parallel({ - source: async.apply(db.getObject, 'category:' + fromCid), - destination: async.apply(db.getObject, 'category:' + toCid), - }, next); - }, - function (results, next) { - if (!results.source) { - return next(new Error('[[error:invalid-cid]]')); - } - destination = results.destination; - - var tasks = []; - - const oldParent = parseInt(destination.parentCid, 10) || 0; - const newParent = parseInt(results.source.parentCid, 10) || 0; - if (copyParent && newParent !== parseInt(toCid, 10)) { - tasks.push(async.apply(db.sortedSetRemove, 'cid:' + oldParent + ':children', toCid)); - tasks.push(async.apply(db.sortedSetAdd, 'cid:' + newParent + ':children', results.source.order, toCid)); - tasks.push(function (next) { - cache.del(['cid:' + oldParent + ':children', 'cid:' + newParent + ':children']); - setImmediate(next); - }); - } - - destination.description = results.source.description; - destination.descriptionParsed = results.source.descriptionParsed; - destination.icon = results.source.icon; - destination.bgColor = results.source.bgColor; - destination.color = results.source.color; - destination.link = results.source.link; - destination.numRecentReplies = results.source.numRecentReplies; - destination.class = results.source.class; - destination.image = results.source.image; - destination.imageClass = results.source.imageClass; - - if (copyParent) { - destination.parentCid = results.source.parentCid || 0; - } - - tasks.push(async.apply(db.setObject, 'category:' + toCid, destination)); - - async.series(tasks, next); - }, - function (results, next) { - copyTagWhitelist(fromCid, toCid, next); - }, - function (next) { - Categories.copyPrivilegesFrom(fromCid, toCid, next); - }, - ], function (err) { - callback(err, destination); - }); + Categories.copySettingsFrom = async function (fromCid, toCid, copyParent) { + const [source, destination] = await Promise.all([ + db.getObject('category:' + fromCid), + db.getObject('category:' + toCid), + ]); + if (!source) { + throw new Error('[[error:invalid-cid]]'); + } + + const oldParent = parseInt(destination.parentCid, 10) || 0; + const newParent = parseInt(source.parentCid, 10) || 0; + if (copyParent && newParent !== parseInt(toCid, 10)) { + await db.sortedSetRemove('cid:' + oldParent + ':children', toCid); + await db.sortedSetAdd('cid:' + newParent + ':children', source.order, toCid); + cache.del(['cid:' + oldParent + ':children', 'cid:' + newParent + ':children']); + } + + destination.description = source.description; + destination.descriptionParsed = source.descriptionParsed; + destination.icon = source.icon; + destination.bgColor = source.bgColor; + destination.color = source.color; + destination.link = source.link; + destination.numRecentReplies = source.numRecentReplies; + destination.class = source.class; + destination.image = source.image; + destination.imageClass = source.imageClass; + + if (copyParent) { + destination.parentCid = source.parentCid || 0; + } + + await db.setObject('category:' + toCid, destination); + + await copyTagWhitelist(fromCid, toCid); + + await Categories.copyPrivilegesFrom(fromCid, toCid); + + return destination; }; - function copyTagWhitelist(fromCid, toCid, callback) { - var data; - async.waterfall([ - function (next) { - db.getSortedSetRangeWithScores('cid:' + fromCid + ':tag:whitelist', 0, -1, next); - }, - function (_data, next) { - data = _data; - db.delete('cid:' + toCid + ':tag:whitelist', next); - }, - function (next) { - db.sortedSetAdd('cid:' + toCid + ':tag:whitelist', data.map(item => item.score), data.map(item => item.value), next); - }, - ], callback); + async function copyTagWhitelist(fromCid, toCid) { + const data = await db.getSortedSetRangeWithScores('cid:' + fromCid + ':tag:whitelist', 0, -1); + await db.delete('cid:' + toCid + ':tag:whitelist'); + await db.sortedSetAdd('cid:' + toCid + ':tag:whitelist', data.map(item => item.score), data.map(item => item.value)); + cache.del('cid:' + toCid + ':tag:whitelist'); } - Categories.copyPrivilegesFrom = function (fromCid, toCid, group, callback) { - if (typeof group === 'function') { - callback = group; - group = ''; - } + Categories.copyPrivilegesFrom = async function (fromCid, toCid, group) { + group = group || ''; - async.waterfall([ - function (next) { - plugins.fireHook('filter:categories.copyPrivilegesFrom', { - privileges: group ? privileges.groupPrivilegeList.slice() : privileges.privilegeList.slice(), - fromCid: fromCid, - toCid: toCid, - group: group, - }, next); - }, - function (data, next) { - if (group) { - copyPrivilegesByGroup(data.privileges, data.fromCid, data.toCid, group, next); - } else { - copyPrivileges(data.privileges, data.fromCid, data.toCid, next); - } - }, - ], callback); + const data = await plugins.fireHook('filter:categories.copyPrivilegesFrom', { + privileges: group ? privileges.groupPrivilegeList.slice() : privileges.privilegeList.slice(), + fromCid: fromCid, + toCid: toCid, + group: group, + }); + if (group) { + await copyPrivilegesByGroup(data.privileges, data.fromCid, data.toCid, group); + } else { + await copyPrivileges(data.privileges, data.fromCid, data.toCid); + } }; - function copyPrivileges(privileges, fromCid, toCid, callback) { + async function copyPrivileges(privileges, fromCid, toCid) { const toGroups = privileges.map(privilege => 'group:cid:' + toCid + ':privileges:' + privilege + ':members'); const fromGroups = privileges.map(privilege => 'group:cid:' + fromCid + ':privileges:' + privilege + ':members'); - async.waterfall([ - function (next) { - db.getSortedSetsMembers(toGroups.concat(fromGroups), next); - }, - function (currentMembers, next) { - const copyGroups = _.uniq(_.flatten(currentMembers)); - async.each(copyGroups, function (group, next) { - copyPrivilegesByGroup(privileges, fromCid, toCid, group, next); - }, next); - }, - ], callback); + + const currentMembers = await db.getSortedSetsMembers(toGroups.concat(fromGroups)); + const copyGroups = _.uniq(_.flatten(currentMembers)); + await async.each(copyGroups, async function (group) { + await copyPrivilegesByGroup(privileges, fromCid, toCid, group); + }); } - function copyPrivilegesByGroup(privileges, fromCid, toCid, group, callback) { - async.waterfall([ - function (next) { - const leaveGroups = privileges.map(privilege => 'cid:' + toCid + ':privileges:' + privilege); - groups.leave(leaveGroups, group, next); - }, - function (next) { - const checkGroups = privileges.map(privilege => 'group:cid:' + fromCid + ':privileges:' + privilege + ':members'); - db.isMemberOfSortedSets(checkGroups, group, next); - }, - function (isMembers, next) { - privileges = privileges.filter((priv, index) => isMembers[index]); - const joinGroups = privileges.map(privilege => 'cid:' + toCid + ':privileges:' + privilege); - groups.join(joinGroups, group, next); - }, - ], callback); + async function copyPrivilegesByGroup(privileges, fromCid, toCid, group) { + const leaveGroups = privileges.map(privilege => 'cid:' + toCid + ':privileges:' + privilege); + await groups.leave(leaveGroups, group); + + const checkGroups = privileges.map(privilege => 'group:cid:' + fromCid + ':privileges:' + privilege + ':members'); + const isMembers = await db.isMemberOfSortedSets(checkGroups, group); + privileges = privileges.filter((priv, index) => isMembers[index]); + const joinGroups = privileges.map(privilege => 'cid:' + toCid + ':privileges:' + privilege); + await groups.join(joinGroups, group); } }; diff --git a/src/categories/data.js b/src/categories/data.js index 282b3e2c34..e9d5d8827c 100644 --- a/src/categories/data.js +++ b/src/categories/data.js @@ -1,6 +1,5 @@ 'use strict'; -var async = require('async'); var validator = require('validator'); var db = require('../database'); @@ -11,64 +10,51 @@ const intFields = [ ]; module.exports = function (Categories) { - Categories.getCategoriesFields = function (cids, fields, callback) { + Categories.getCategoriesFields = async function (cids, fields) { if (!Array.isArray(cids) || !cids.length) { - return setImmediate(callback, null, []); + return []; } - - async.waterfall([ - function (next) { - const keys = cids.map(cid => 'category:' + cid); - if (fields.length) { - db.getObjectsFields(keys, fields, next); - } else { - db.getObjects(keys, next); - } - }, - function (categories, next) { - categories.forEach(category => modifyCategory(category, fields)); - next(null, categories); - }, - ], callback); + let categories; + const keys = cids.map(cid => 'category:' + cid); + if (fields.length) { + categories = await db.getObjectsFields(keys, fields); + } else { + categories = await db.getObjects(keys); + } + categories.forEach(category => modifyCategory(category, fields)); + return categories; }; - Categories.getCategoryData = function (cid, callback) { - Categories.getCategoriesFields([cid], [], function (err, categories) { - callback(err, categories && categories.length ? categories[0] : null); - }); + Categories.getCategoryData = async function (cid) { + const categories = await Categories.getCategoriesFields([cid], []); + return categories && categories.length ? categories[0] : null; }; - Categories.getCategoriesData = function (cids, callback) { - Categories.getCategoriesFields(cids, [], callback); + Categories.getCategoriesData = async function (cids) { + return await Categories.getCategoriesFields(cids, []); }; - Categories.getCategoryField = function (cid, field, callback) { - Categories.getCategoryFields(cid, [field], function (err, category) { - callback(err, category ? category[field] : null); - }); + Categories.getCategoryField = async function (cid, field) { + const category = await Categories.getCategoryFields(cid, [field]); + return category ? category[field] : null; }; - Categories.getCategoryFields = function (cid, fields, callback) { - Categories.getCategoriesFields([cid], fields, function (err, categories) { - callback(err, categories ? categories[0] : null); - }); + Categories.getCategoryFields = async function (cid, fields) { + const categories = await Categories.getCategoriesFields([cid], fields); + return categories ? categories[0] : null; }; - Categories.getAllCategoryFields = function (fields, callback) { - async.waterfall([ - async.apply(Categories.getAllCidsFromSet, 'categories:cid'), - function (cids, next) { - Categories.getCategoriesFields(cids, fields, next); - }, - ], callback); + Categories.getAllCategoryFields = async function (fields) { + const cids = await Categories.getAllCidsFromSet('categories:cid'); + return await Categories.getCategoriesFields(cids, fields); }; - Categories.setCategoryField = function (cid, field, value, callback) { - db.setObjectField('category:' + cid, field, value, callback); + Categories.setCategoryField = async function (cid, field, value) { + await db.setObjectField('category:' + cid, field, value); }; - Categories.incrementCategoryFieldBy = function (cid, field, value, callback) { - db.incrObjectFieldBy('category:' + cid, field, value, callback); + Categories.incrementCategoryFieldBy = async function (cid, field, value) { + await db.incrObjectFieldBy('category:' + cid, field, value); }; }; diff --git a/src/categories/delete.js b/src/categories/delete.js index 3024791b2a..033aa3d52d 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -10,107 +10,62 @@ var privileges = require('../privileges'); var cache = require('../cache'); module.exports = function (Categories) { - Categories.purge = function (cid, uid, callback) { - async.waterfall([ - function (next) { - batch.processSortedSet('cid:' + cid + ':tids', function (tids, next) { - async.eachLimit(tids, 10, function (tid, next) { - topics.purgePostsAndTopic(tid, uid, next); - }, next); - }, { alwaysStartAt: 0 }, next); - }, - function (next) { - db.getSortedSetRevRange('cid:' + cid + ':tids:pinned', 0, -1, next); - }, - function (pinnedTids, next) { - async.eachLimit(pinnedTids, 10, function (tid, next) { - topics.purgePostsAndTopic(tid, uid, next); - }, next); - }, - function (next) { - purgeCategory(cid, next); - }, - function (next) { - plugins.fireHook('action:category.delete', { cid: cid, uid: uid }); - next(); - }, - ], callback); - }; + Categories.purge = async function (cid, uid) { + await batch.processSortedSet('cid:' + cid + ':tids', async function (tids) { + await async.eachLimit(tids, 10, async function (tid) { + await topics.purgePostsAndTopic(tid, uid); + }); + }, { alwaysStartAt: 0 }); - function purgeCategory(cid, callback) { - async.series([ - function (next) { - db.sortedSetRemove('categories:cid', cid, next); - }, - function (next) { - removeFromParent(cid, next); - }, - function (next) { - db.deleteAll([ - 'cid:' + cid + ':tids', - 'cid:' + cid + ':tids:pinned', - 'cid:' + cid + ':tids:posts', - 'cid:' + cid + ':pids', - 'cid:' + cid + ':read_by_uid', - 'cid:' + cid + ':uid:watch:state', - 'cid:' + cid + ':children', - 'cid:' + cid + ':tag:whitelist', - 'category:' + cid, - ], next); - }, - function (next) { - groups.destroy(privileges.privilegeList.map(privilege => 'cid:' + cid + ':privileges:' + privilege), next); - }, - ], function (err) { - callback(err); + const pinnedTids = await db.getSortedSetRevRange('cid:' + cid + ':tids:pinned', 0, -1); + await async.eachLimit(pinnedTids, 10, async function (tid) { + await topics.purgePostsAndTopic(tid, uid); }); + await purgeCategory(cid); + plugins.fireHook('action:category.delete', { cid: cid, uid: uid }); + }; + + async function purgeCategory(cid) { + await db.sortedSetRemove('categories:cid', cid); + await removeFromParent(cid); + await db.deleteAll([ + 'cid:' + cid + ':tids', + 'cid:' + cid + ':tids:pinned', + 'cid:' + cid + ':tids:posts', + 'cid:' + cid + ':pids', + 'cid:' + cid + ':read_by_uid', + 'cid:' + cid + ':uid:watch:state', + 'cid:' + cid + ':children', + 'cid:' + cid + ':tag:whitelist', + 'category:' + cid, + ]); + await groups.destroy(privileges.privilegeList.map(privilege => 'cid:' + cid + ':privileges:' + privilege)); } - function removeFromParent(cid, callback) { - async.waterfall([ - function (next) { - async.parallel({ - parentCid: function (next) { - Categories.getCategoryField(cid, 'parentCid', next); - }, - children: function (next) { - db.getSortedSetRange('cid:' + cid + ':children', 0, -1, next); - }, - }, next); - }, - function (results, next) { - async.parallel([ - function (next) { - db.sortedSetRemove('cid:' + results.parentCid + ':children', cid, next); - }, - function (next) { - async.each(results.children, function (cid, next) { - async.parallel([ - function (next) { - db.setObjectField('category:' + cid, 'parentCid', 0, next); - }, - function (next) { - db.sortedSetAdd('cid:0:children', cid, cid, next); - }, - ], next); - }, next); - }, - ], function (err) { - if (err) { - return next(err); - } - cache.del([ - 'categories:cid', - 'cid:0:children', - 'cid:' + results.parentCid + ':children', - 'cid:' + cid + ':children', - 'cid:' + cid + ':tag:whitelist', - ]); - next(); - }); - }, - ], function (err) { - callback(err); + async function removeFromParent(cid) { + const [parentCid, children] = await Promise.all([ + Categories.getCategoryField(cid, 'parentCid'), + db.getSortedSetRange('cid:' + cid + ':children', 0, -1), + ]); + + const bulkAdd = []; + const childrenKeys = children.map(function (cid) { + bulkAdd.push(['cid:0:children', cid, cid]); + return 'category:' + cid; }); + + await Promise.all([ + db.sortedSetRemove('cid:' + parentCid + ':children', cid), + db.setObjectField(childrenKeys, 'parentCid', 0), + db.sortedSetAddBulk(bulkAdd), + ]); + + cache.del([ + 'categories:cid', + 'cid:0:children', + 'cid:' + parentCid + ':children', + 'cid:' + cid + ':children', + 'cid:' + cid + ':tag:whitelist', + ]); } }; diff --git a/src/categories/index.js b/src/categories/index.js index cbc99e3c79..7f2410efb5 100644 --- a/src/categories/index.js +++ b/src/categories/index.js @@ -1,7 +1,6 @@ 'use strict'; -var async = require('async'); var _ = require('lodash'); var db = require('../database'); @@ -23,195 +22,128 @@ require('./recentreplies')(Categories); require('./update')(Categories); require('./watch')(Categories); -Categories.exists = function (cid, callback) { - db.exists('category:' + cid, callback); +Categories.exists = async function (cid) { + return await db.exists('category:' + cid); }; -Categories.getCategoryById = function (data, callback) { - var category; - async.waterfall([ - function (next) { - Categories.getCategories([data.cid], data.uid, next); - }, - function (categories, next) { - if (!categories[0]) { - return callback(null, null); - } - category = categories[0]; - data.category = category; - async.parallel({ - topics: function (next) { - Categories.getCategoryTopics(data, next); - }, - topicCount: function (next) { - Categories.getTopicCount(data, next); - }, - watchState: function (next) { - Categories.getWatchState([data.cid], data.uid, next); - }, - parent: function (next) { - if (category.parentCid) { - Categories.getCategoryData(category.parentCid, next); - } else { - next(); - } - }, - children: function (next) { - getChildrenTree(category, data.uid, next); - }, - }, next); - }, - function (results, next) { - category.topics = results.topics.topics; - category.nextStart = results.topics.nextStart; - category.topic_count = results.topicCount; - category.isWatched = results.watchState[0] === Categories.watchStates.watching; - category.isNotWatched = results.watchState[0] === Categories.watchStates.notwatching; - category.isIgnored = results.watchState[0] === Categories.watchStates.ignoring; - category.parent = results.parent; - - calculateTopicPostCount(category); - plugins.fireHook('filter:category.get', { category: category, uid: data.uid }, next); - }, - function (data, next) { - next(null, data.category); - }, - ], callback); +Categories.getCategoryById = async function (data) { + const categories = await Categories.getCategories([data.cid], data.uid); + if (!categories[0]) { + return null; + } + const category = categories[0]; + data.category = category; + + const promises = [ + Categories.getCategoryTopics(data), + Categories.getTopicCount(data), + Categories.getWatchState([data.cid], data.uid), + getChildrenTree(category, data.uid), + ]; + + if (category.parentCid) { + promises.push(Categories.getCategoryData(category.parentCid)); + } + const [topics, topicCount, watchState, , parent] = await Promise.all(promises); + + category.topics = topics.topics; + category.nextStart = topics.nextStart; + category.topic_count = topicCount; + category.isWatched = watchState[0] === Categories.watchStates.watching; + category.isNotWatched = watchState[0] === Categories.watchStates.notwatching; + category.isIgnored = watchState[0] === Categories.watchStates.ignoring; + category.parent = parent; + + + calculateTopicPostCount(category); + const result = await plugins.fireHook('filter:category.get', { category: category, uid: data.uid }); + return result.category; }; -Categories.getAllCidsFromSet = function (key, callback) { - const cids = cache.get(key); +Categories.getAllCidsFromSet = async function (key) { + let cids = cache.get(key); if (cids) { - return setImmediate(callback, null, cids.slice()); + return cids.slice(); } - db.getSortedSetRange(key, 0, -1, function (err, cids) { - if (err) { - return callback(err); - } - cache.set(key, cids); - callback(null, cids.slice()); - }); + cids = await db.getSortedSetRange(key, 0, -1); + cache.set(key, cids); + return cids.slice(); }; -Categories.getAllCategories = function (uid, callback) { - async.waterfall([ - function (next) { - Categories.getAllCidsFromSet('categories:cid', next); - }, - function (cids, next) { - Categories.getCategories(cids, uid, next); - }, - ], callback); +Categories.getAllCategories = async function (uid) { + const cids = await Categories.getAllCidsFromSet('categories:cid'); + return await Categories.getCategories(cids, uid); }; -Categories.getCidsByPrivilege = function (set, uid, privilege, callback) { - async.waterfall([ - function (next) { - Categories.getAllCidsFromSet(set, next); - }, - function (cids, next) { - privileges.categories.filterCids(privilege, cids, uid, next); - }, - ], callback); +Categories.getCidsByPrivilege = async function (set, uid, privilege) { + const cids = await Categories.getAllCidsFromSet('categories:cid'); + return await privileges.categories.filterCids(privilege, cids, uid); }; -Categories.getCategoriesByPrivilege = function (set, uid, privilege, callback) { - async.waterfall([ - function (next) { - Categories.getCidsByPrivilege(set, uid, privilege, next); - }, - function (cids, next) { - Categories.getCategories(cids, uid, next); - }, - ], callback); +Categories.getCategoriesByPrivilege = async function (set, uid, privilege) { + const cids = await Categories.getCidsByPrivilege(set, uid, privilege); + return await Categories.getCategories(cids, uid); }; -Categories.getModerators = function (cid, callback) { - async.waterfall([ - function (next) { - Categories.getModeratorUids([cid], next); - }, - function (uids, next) { - user.getUsersFields(uids[0], ['uid', 'username', 'userslug', 'picture'], next); - }, - ], callback); +Categories.getModerators = async function (cid) { + const uids = await Categories.getModeratorUids([cid]); + return await user.getUsersFields(uids[0], ['uid', 'username', 'userslug', 'picture']); }; -Categories.getModeratorUids = function (cids, callback) { - var sets; - var uniqGroups; - async.waterfall([ - function (next) { - var groupNames = cids.reduce(function (memo, cid) { - memo.push('cid:' + cid + ':privileges:moderate'); - memo.push('cid:' + cid + ':privileges:groups:moderate'); - return memo; - }, []); - - groups.getMembersOfGroups(groupNames, next); - }, - function (memberSets, next) { - // Every other set is actually a list of user groups, not uids, so convert those to members - sets = memberSets.reduce(function (memo, set, idx) { - if (idx % 2) { - memo.groupNames.push(set); - } else { - memo.uids.push(set); - } +Categories.getModeratorUids = async function (cids) { + const groupNames = cids.reduce(function (memo, cid) { + memo.push('cid:' + cid + ':privileges:moderate'); + memo.push('cid:' + cid + ':privileges:groups:moderate'); + return memo; + }, []); + + const memberSets = await groups.getMembersOfGroups(groupNames); + // Every other set is actually a list of user groups, not uids, so convert those to members + const sets = memberSets.reduce(function (memo, set, idx) { + if (idx % 2) { + memo.groupNames.push(set); + } else { + memo.uids.push(set); + } - return memo; - }, { groupNames: [], uids: [] }); + return memo; + }, { groupNames: [], uids: [] }); - uniqGroups = _.uniq(_.flatten(sets.groupNames)); - groups.getMembersOfGroups(uniqGroups, next); - }, - function (groupUids, next) { - var map = _.zipObject(uniqGroups, groupUids); - const moderatorUids = cids.map(function (cid, index) { - return _.uniq(sets.uids[index].concat(_.flatten(sets.groupNames[index].map(g => map[g])))); - }); - next(null, moderatorUids); - }, - ], callback); + const uniqGroups = _.uniq(_.flatten(sets.groupNames)); + const groupUids = await groups.getMembersOfGroups(uniqGroups); + const map = _.zipObject(uniqGroups, groupUids); + const moderatorUids = cids.map(function (cid, index) { + return _.uniq(sets.uids[index].concat(_.flatten(sets.groupNames[index].map(g => map[g])))); + }); + return moderatorUids; }; -Categories.getCategories = function (cids, uid, callback) { +Categories.getCategories = async function (cids, uid) { if (!Array.isArray(cids)) { - return callback(new Error('[[error:invalid-cid]]')); + throw new Error('[[error:invalid-cid]]'); } if (!cids.length) { - return callback(null, []); + return []; } uid = parseInt(uid, 10); - async.waterfall([ - function (next) { - async.parallel({ - categories: function (next) { - Categories.getCategoriesData(cids, next); - }, - tagWhitelist: function (next) { - Categories.getTagWhitelist(cids, next); - }, - hasRead: function (next) { - Categories.hasReadCategories(cids, uid, next); - }, - }, next); - }, - function (results, next) { - results.categories.forEach(function (category, i) { - if (category) { - category.tagWhitelist = results.tagWhitelist[i]; - category['unread-class'] = (category.topic_count === 0 || (results.hasRead[i] && uid !== 0)) ? '' : 'unread'; - } - }); - next(null, results.categories); - }, - ], callback); + + const [categories, tagWhitelist, hasRead] = await Promise.all([ + Categories.getCategoriesData(cids), + Categories.getTagWhitelist(cids), + Categories.hasReadCategories(cids, uid), + ]); + categories.forEach(function (category, i) { + if (category) { + category.tagWhitelist = tagWhitelist[i]; + category['unread-class'] = (category.topic_count === 0 || (hasRead[i] && uid !== 0)) ? '' : 'unread'; + } + }); + return categories; }; -Categories.getTagWhitelist = function (cids, callback) { +Categories.getTagWhitelist = async function (cids) { const cachedData = {}; const nonCachedCids = cids.filter((cid) => { @@ -224,20 +156,17 @@ Categories.getTagWhitelist = function (cids, callback) { }); if (!nonCachedCids.length) { - return setImmediate(callback, null, _.clone(cids.map(cid => cachedData[cid]))); + return _.clone(cids.map(cid => cachedData[cid])); } const keys = nonCachedCids.map(cid => 'cid:' + cid + ':tag:whitelist'); - db.getSortedSetsMembers(keys, function (err, data) { - if (err) { - return callback(err); - } - nonCachedCids.forEach((cid, index) => { - cachedData[cid] = data[index]; - cache.set('cid:' + cid + ':tag:whitelist', data[index]); - }); - callback(null, _.clone(cids.map(cid => cachedData[cid]))); + const data = await db.getSortedSetsMembers(keys); + + nonCachedCids.forEach((cid, index) => { + cachedData[cid] = data[index]; + cache.set('cid:' + cid + ':tag:whitelist', data[index]); }); + return _.clone(cids.map(cid => cachedData[cid])); }; function calculateTopicPostCount(category) { @@ -263,114 +192,65 @@ function calculateTopicPostCount(category) { category.totalTopicCount = topicCount; } -Categories.getParents = function (cids, callback) { - var categoriesData; - var parentCids; - async.waterfall([ - function (next) { - Categories.getCategoriesFields(cids, ['parentCid'], next); - }, - function (_categoriesData, next) { - categoriesData = _categoriesData; - - parentCids = categoriesData.filter(c => c && c.parentCid).map(c => c.parentCid); - - if (!parentCids.length) { - return callback(null, cids.map(() => null)); - } - - Categories.getCategoriesData(parentCids, next); - }, - function (parentData, next) { - const cidToParent = _.zipObject(parentCids, parentData); - parentData = categoriesData.map(category => cidToParent[category.parentCid]); - next(null, parentData); - }, - ], callback); +Categories.getParents = async function (cids) { + const categoriesData = await Categories.getCategoriesFields(cids, ['parentCid']); + const parentCids = categoriesData.filter(c => c && c.parentCid).map(c => c.parentCid); + if (!parentCids.length) { + return cids.map(() => null); + } + const parentData = await Categories.getCategoriesData(parentCids); + const cidToParent = _.zipObject(parentCids, parentData); + return categoriesData.map(category => cidToParent[category.parentCid]); }; -Categories.getChildren = function (cids, uid, callback) { - var categories; - async.waterfall([ - function (next) { - Categories.getCategoriesFields(cids, ['parentCid'], next); - }, - function (categoryData, next) { - categories = categoryData.map((category, index) => ({ cid: cids[index], parentCid: category.parentCid })); - async.each(categories, function (category, next) { - getChildrenTree(category, uid, next); - }, next); - }, - function (next) { - next(null, categories.map(c => c && c.children)); - }, - ], callback); +Categories.getChildren = async function (cids, uid) { + const categoryData = await Categories.getCategoriesFields(cids, ['parentCid']); + const categories = categoryData.map((category, index) => ({ cid: cids[index], parentCid: category.parentCid })); + await Promise.all(categories.map(c => getChildrenTree(c, uid))); + return categories.map(c => c && c.children); }; -function getChildrenTree(category, uid, callback) { - let children; - async.waterfall([ - function (next) { - Categories.getChildrenCids(category.cid, next); - }, - function (children, next) { - privileges.categories.filterCids('find', children, uid, next); - }, - function (children, next) { - children = children.filter(cid => parseInt(category.cid, 10) !== parseInt(cid, 10)); - if (!children.length) { - category.children = []; - return callback(); - } - Categories.getCategoriesData(children, next); - }, - function (_children, next) { - children = _children.filter(Boolean); - - const cids = children.map(child => child.cid); - Categories.hasReadCategories(cids, uid, next); - }, - function (hasRead, next) { - hasRead.forEach(function (read, i) { - const child = children[i]; - child['unread-class'] = (child.topic_count === 0 || (read && uid !== 0)) ? '' : 'unread'; - }); - Categories.getTree([category].concat(children), category.parentCid); - next(); - }, - ], callback); +async function getChildrenTree(category, uid) { + let childrenCids = await Categories.getChildrenCids(category.cid); + childrenCids = await privileges.categories.filterCids('find', childrenCids, uid); + childrenCids = childrenCids.filter(cid => parseInt(category.cid, 10) !== parseInt(cid, 10)); + if (!childrenCids.length) { + category.children = []; + return; + } + let childrenData = await Categories.getCategoriesData(childrenCids); + childrenData = childrenData.filter(Boolean); + childrenCids = childrenData.map(child => child.cid); + const hasRead = await Categories.hasReadCategories(childrenCids, uid); + childrenData.forEach(function (child, i) { + child['unread-class'] = (child.topic_count === 0 || (hasRead[i] && uid !== 0)) ? '' : 'unread'; + }); + Categories.getTree([category].concat(childrenData), category.parentCid); } -Categories.getChildrenCids = function (rootCid, callback) { +Categories.getChildrenCids = async function (rootCid) { let allCids = []; - function recursive(keys, callback) { - db.getSortedSetRange(keys, 0, -1, function (err, childrenCids) { - if (err) { - return callback(err); - } - childrenCids = childrenCids.filter(cid => !allCids.includes(parseInt(cid, 10))); - if (!childrenCids.length) { - return callback(); - } - const keys = childrenCids.map(cid => 'cid:' + cid + ':children'); - childrenCids.forEach(cid => allCids.push(parseInt(cid, 10))); - recursive(keys, callback); - }); + async function recursive(keys) { + let childrenCids = await db.getSortedSetRange(keys, 0, -1); + + childrenCids = childrenCids.filter(cid => !allCids.includes(parseInt(cid, 10))); + if (!childrenCids.length) { + return; + } + keys = childrenCids.map(cid => 'cid:' + cid + ':children'); + childrenCids.forEach(cid => allCids.push(parseInt(cid, 10))); + recursive(keys); } const key = 'cid:' + rootCid + ':children'; const childrenCids = cache.get(key); if (childrenCids) { - return setImmediate(callback, null, childrenCids.slice()); + return childrenCids.slice(); } - recursive(key, function (err) { - if (err) { - return callback(err); - } - allCids = _.uniq(allCids); - cache.set(key, allCids); - callback(null, allCids.slice()); - }); + await recursive(key); + allCids = _.uniq(allCids); + cache.set(key, allCids); + return allCids.slice(); }; Categories.flattenCategories = function (allCategories, categoryData) { @@ -440,19 +320,13 @@ Categories.getTree = function (categories, parentCid) { return tree; }; -Categories.buildForSelect = function (uid, privilege, callback) { - async.waterfall([ - function (next) { - Categories.getCategoriesByPrivilege('categories:cid', uid, privilege, next); - }, - function (categories, next) { - categories = Categories.getTree(categories); - Categories.buildForSelectCategories(categories, next); - }, - ], callback); +Categories.buildForSelect = async function (uid, privilege) { + let categories = await Categories.getCategoriesByPrivilege('categories:cid', uid, privilege); + categories = Categories.getTree(categories); + return await Categories.buildForSelectCategories(categories); }; -Categories.buildForSelectCategories = function (categories, callback) { +Categories.buildForSelectCategories = async function (categories) { function recursive(category, categoriesData, level, depth) { var bullet = level ? '• ' : ''; category.value = category.cid; @@ -474,7 +348,7 @@ Categories.buildForSelectCategories = function (categories, callback) { categories.forEach(function (category) { recursive(category, categoriesData, '', 0); }); - callback(null, categoriesData); + return categoriesData; }; Categories.async = require('../promisify')(Categories); diff --git a/src/categories/recentreplies.js b/src/categories/recentreplies.js index 02def5dd3b..b624c88aa8 100644 --- a/src/categories/recentreplies.js +++ b/src/categories/recentreplies.js @@ -1,7 +1,6 @@ 'use strict'; -var async = require('async'); var _ = require('lodash'); var db = require('../database'); @@ -11,143 +10,88 @@ var privileges = require('../privileges'); var batch = require('../batch'); module.exports = function (Categories) { - Categories.getRecentReplies = function (cid, uid, count, callback) { + Categories.getRecentReplies = async function (cid, uid, count) { if (!parseInt(count, 10)) { - return callback(null, []); + return []; } - - async.waterfall([ - function (next) { - db.getSortedSetRevRange('cid:' + cid + ':pids', 0, count - 1, next); - }, - function (pids, next) { - privileges.posts.filter('topics:read', pids, uid, next); - }, - function (pids, next) { - posts.getPostSummaryByPids(pids, uid, { stripTags: true }, next); - }, - ], callback); + let pids = await db.getSortedSetRevRange('cid:' + cid + ':pids', 0, count - 1); + pids = await privileges.posts.filter('topics:read', pids, uid); + return await posts.getPostSummaryByPids(pids, uid, { stripTags: true }); }; - Categories.updateRecentTid = function (cid, tid, callback) { - async.waterfall([ - function (next) { - async.parallel({ - count: function (next) { - db.sortedSetCard('cid:' + cid + ':recent_tids', next); - }, - numRecentReplies: function (next) { - db.getObjectField('category:' + cid, 'numRecentReplies', next); - }, - }, next); - }, - function (results, next) { - if (results.count < results.numRecentReplies) { - return db.sortedSetAdd('cid:' + cid + ':recent_tids', Date.now(), tid, callback); - } - db.getSortedSetRangeWithScores('cid:' + cid + ':recent_tids', 0, results.count - results.numRecentReplies, next); - }, - function (data, next) { - if (!data.length) { - return next(); - } - db.sortedSetsRemoveRangeByScore(['cid:' + cid + ':recent_tids'], '-inf', data[data.length - 1].score, next); - }, - function (next) { - db.sortedSetAdd('cid:' + cid + ':recent_tids', Date.now(), tid, next); - }, - ], callback); - }; + Categories.updateRecentTid = async function (cid, tid) { + const [count, numRecentReplies] = await Promise.all([ + db.sortedSetCard('cid:' + cid + ':recent_tids'), + db.getObjectField('category:' + cid, 'numRecentReplies'), + ]); - Categories.updateRecentTidForCid = function (cid, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRevRange('cid:' + cid + ':pids', 0, 0, next); - }, - function (pid, next) { - pid = pid[0]; - posts.getPostField(pid, 'tid', next); - }, - function (tid, next) { - if (!tid) { - return next(); - } + if (count < numRecentReplies) { + return await db.sortedSetAdd('cid:' + cid + ':recent_tids', Date.now(), tid); + } + const data = await db.getSortedSetRangeWithScores('cid:' + cid + ':recent_tids', 0, count - numRecentReplies); + if (data.length) { + await db.sortedSetsRemoveRangeByScore(['cid:' + cid + ':recent_tids'], '-inf', data[data.length - 1].score); + } + await db.sortedSetAdd('cid:' + cid + ':recent_tids', Date.now(), tid); + }; - Categories.updateRecentTid(cid, tid, next); - }, - ], callback); + Categories.updateRecentTidForCid = async function (cid) { + const pids = await db.getSortedSetRevRange('cid:' + cid + ':pids', 0, 0); + if (!pids.length) { + return; + } + const tid = await posts.getPostField(pids[0], 'tid'); + if (!tid) { + return; + } + await Categories.updateRecentTid(cid, tid); }; - Categories.getRecentTopicReplies = function (categoryData, uid, callback) { + Categories.getRecentTopicReplies = async function (categoryData, uid) { if (!Array.isArray(categoryData) || !categoryData.length) { - return callback(); + return; } + const categoriesToLoad = categoryData.filter(category => category && category.numRecentReplies && parseInt(category.numRecentReplies, 10) > 0); + const keys = categoriesToLoad.map(category => 'cid:' + category.cid + ':recent_tids'); + const results = await db.getSortedSetsMembers(keys); + let tids = _.uniq(_.flatten(results).filter(Boolean)); - async.waterfall([ - function (next) { - const categoriesToLoad = categoryData.filter(category => category && category.numRecentReplies && parseInt(category.numRecentReplies, 10) > 0); - const keys = categoriesToLoad.map(category => 'cid:' + category.cid + ':recent_tids'); - db.getSortedSetsMembers(keys, next); - }, - function (results, next) { - var tids = _.uniq(_.flatten(results).filter(Boolean)); + tids = await privileges.topics.filterTids('topics:read', tids, uid); + const topics = await getTopics(tids, uid); + assignTopicsToCategories(categoryData, topics); - privileges.topics.filterTids('topics:read', tids, uid, next); - }, - function (tids, next) { - getTopics(tids, uid, next); - }, - function (topics, next) { - assignTopicsToCategories(categoryData, topics); - - bubbleUpChildrenPosts(categoryData); - - next(); - }, - ], callback); + bubbleUpChildrenPosts(categoryData); }; - function getTopics(tids, uid, callback) { - var topicData; - async.waterfall([ - function (next) { - topics.getTopicsFields(tids, ['tid', 'mainPid', 'slug', 'title', 'teaserPid', 'cid', 'postcount'], next); - }, - function (_topicData, next) { - topicData = _topicData; - topicData.forEach(function (topic) { - if (topic) { - topic.teaserPid = topic.teaserPid || topic.mainPid; - } - }); - var cids = _.uniq(topicData.map(topic => topic && topic.cid).filter(cid => parseInt(cid, 10))); - - async.parallel({ - categoryData: async.apply(Categories.getCategoriesFields, cids, ['cid', 'parentCid']), - teasers: async.apply(topics.getTeasers, _topicData, uid), - }, next); - }, - function (results, next) { - var parentCids = {}; - results.categoryData.forEach(function (category) { - parentCids[category.cid] = category.parentCid; - }); - results.teasers.forEach(function (teaser, index) { - if (teaser) { - teaser.cid = topicData[index].cid; - teaser.parentCid = parseInt(parentCids[teaser.cid], 10) || 0; - teaser.tid = undefined; - teaser.uid = undefined; - teaser.topic = { - slug: topicData[index].slug, - title: topicData[index].title, - }; - } - }); - results.teasers = results.teasers.filter(Boolean); - next(null, results.teasers); - }, - ], callback); + async function getTopics(tids, uid) { + const topicData = await topics.getTopicsFields(tids, ['tid', 'mainPid', 'slug', 'title', 'teaserPid', 'cid', 'postcount']); + topicData.forEach(function (topic) { + if (topic) { + topic.teaserPid = topic.teaserPid || topic.mainPid; + } + }); + var cids = _.uniq(topicData.map(topic => topic && topic.cid).filter(cid => parseInt(cid, 10))); + const [categoryData, teasers] = await Promise.all([ + Categories.getCategoriesFields(cids, ['cid', 'parentCid']), + topics.getTeasers(topicData, uid), + ]); + var parentCids = {}; + categoryData.forEach(function (category) { + parentCids[category.cid] = category.parentCid; + }); + teasers.forEach(function (teaser, index) { + if (teaser) { + teaser.cid = topicData[index].cid; + teaser.parentCid = parseInt(parentCids[teaser.cid], 10) || 0; + teaser.tid = undefined; + teaser.uid = undefined; + teaser.topic = { + slug: topicData[index].slug, + title: topicData[index].title, + }; + } + }); + return teasers.filter(Boolean); } function assignTopicsToCategories(categories, topics) { @@ -188,80 +132,43 @@ module.exports = function (Categories) { getPostsRecursive(child, posts); }); } + // terrible name, should be topics.moveTopicPosts + Categories.moveRecentReplies = async function (tid, oldCid, cid) { + await updatePostCount(tid, oldCid, cid); + const pids = await topics.getPids(tid); + + await batch.processArray(pids, async function (pids) { + const postData = await posts.getPostsFields(pids, ['pid', 'uid', 'timestamp', 'upvotes', 'downvotes']); + const timestamps = postData.map(p => p && p.timestamp); + const bulkRemove = []; + const bulkAdd = []; + postData.forEach((post) => { + bulkRemove.push(['cid:' + oldCid + ':uid:' + post.uid + ':pids', post.pid]); + bulkRemove.push(['cid:' + oldCid + ':uid:' + post.uid + ':pids:votes', post.pid]); + bulkAdd.push(['cid:' + cid + ':uid:' + post.uid + ':pids', post.timestamp, post.pid]); + if (post.votes > 0) { + bulkAdd.push(['cid:' + cid + ':uid:' + post.uid + ':pids:votes', post.votes, post.pid]); + } + }); - Categories.moveRecentReplies = function (tid, oldCid, cid, callback) { - callback = callback || function () {}; - - async.waterfall([ - function (next) { - updatePostCount(tid, oldCid, cid, next); - }, - function (next) { - topics.getPids(tid, next); - }, - function (pids, next) { - batch.processArray(pids, function (pids, next) { - async.waterfall([ - function (next) { - posts.getPostsFields(pids, ['pid', 'uid', 'timestamp', 'upvotes', 'downvotes'], next); - }, - function (postData, next) { - var timestamps = postData.map(p => p && p.timestamp); - - async.parallel([ - function (next) { - db.sortedSetRemove('cid:' + oldCid + ':pids', pids, next); - }, - function (next) { - db.sortedSetAdd('cid:' + cid + ':pids', timestamps, pids, next); - }, - function (next) { - async.each(postData, function (post, next) { - db.sortedSetRemove([ - 'cid:' + oldCid + ':uid:' + post.uid + ':pids', - 'cid:' + oldCid + ':uid:' + post.uid + ':pids:votes', - ], post.pid, next); - }, next); - }, - function (next) { - async.each(postData, function (post, next) { - const keys = ['cid:' + cid + ':uid:' + post.uid + ':pids']; - const scores = [post.timestamp]; - if (post.votes > 0) { - keys.push('cid:' + cid + ':uid:' + post.uid + ':pids:votes'); - scores.push(post.votes); - } - db.sortedSetsAdd(keys, scores, post.pid, next); - }, next); - }, - ], next); - }, - ], next); - }, next); - }, - ], callback); + await Promise.all([ + db.sortedSetRemove('cid:' + oldCid + ':pids', pids), + db.sortedSetAdd('cid:' + cid + ':pids', timestamps, pids), + db.sortedSetRemoveBulk(bulkRemove), + db.sortedSetAddBulk(bulkAdd), + ]); + }, { batch: 500 }); }; - function updatePostCount(tid, oldCid, newCid, callback) { - async.waterfall([ - function (next) { - topics.getTopicField(tid, 'postcount', next); - }, - function (postCount, next) { - if (!postCount) { - return callback(); - } - async.parallel([ - function (next) { - db.incrObjectFieldBy('category:' + oldCid, 'post_count', -postCount, next); - }, - function (next) { - db.incrObjectFieldBy('category:' + newCid, 'post_count', postCount, next); - }, - ], function (err) { - next(err); - }); - }, - ], callback); + async function updatePostCount(tid, oldCid, newCid) { + const postCount = await topics.getTopicField(tid, 'postcount'); + if (!postCount) { + return; + } + + await Promise.all([ + db.incrObjectFieldBy('category:' + oldCid, 'post_count', -postCount), + db.incrObjectFieldBy('category:' + newCid, 'post_count', postCount), + ]); } }; diff --git a/src/categories/topics.js b/src/categories/topics.js index a0d1f330b9..00a646dfb4 100644 --- a/src/categories/topics.js +++ b/src/categories/topics.js @@ -1,6 +1,5 @@ 'use strict'; -var async = require('async'); var _ = require('lodash'); var db = require('../database'); @@ -10,123 +9,89 @@ var meta = require('../meta'); var user = require('../user'); module.exports = function (Categories) { - Categories.getCategoryTopics = function (data, callback) { - async.waterfall([ - function (next) { - plugins.fireHook('filter:category.topics.prepare', data, next); - }, - function (data, next) { - Categories.getTopicIds(data, next); - }, - function (tids, next) { - topics.getTopicsByTids(tids, data.uid, next); - }, - async.apply(user.blocks.filter, data.uid), - function (topicsData, next) { - if (!topicsData.length) { - return next(null, { topics: [], uid: data.uid }); - } - topics.calculateTopicIndices(topicsData, data.start); - - plugins.fireHook('filter:category.topics.get', { cid: data.cid, topics: topicsData, uid: data.uid }, next); - }, - function (results, next) { - next(null, { topics: results.topics, nextStart: data.stop + 1 }); - }, - ], callback); + Categories.getCategoryTopics = async function (data) { + let results = await plugins.fireHook('filter:category.topics.prepare', data); + const tids = await Categories.getTopicIds(results); + let topicsData = await topics.getTopicsByTids(tids, data.uid); + topicsData = await user.blocks.filter(data.uid, topicsData); + + if (!topicsData.length) { + return { topics: [], uid: data.uid }; + } + topics.calculateTopicIndices(topicsData, data.start); + + results = await plugins.fireHook('filter:category.topics.get', { cid: data.cid, topics: topicsData, uid: data.uid }); + return { topics: results.topics, nextStart: data.stop + 1 }; }; - Categories.getTopicIds = function (data, callback) { - var pinnedTids; - - async.waterfall([ - function (next) { - var dataForPinned = _.cloneDeep(data); - dataForPinned.start = 0; - dataForPinned.stop = -1; - - async.parallel({ - pinnedTids: async.apply(Categories.getPinnedTids, dataForPinned), - set: async.apply(Categories.buildTopicsSortedSet, data), - direction: async.apply(Categories.getSortedSetRangeDirection, data.sort), - }, next); - }, - function (results, next) { - var totalPinnedCount = results.pinnedTids.length; - - pinnedTids = results.pinnedTids.slice(data.start, data.stop !== -1 ? data.stop + 1 : undefined); - - var pinnedCount = pinnedTids.length; - - var topicsPerPage = data.stop - data.start + 1; - - var normalTidsToGet = Math.max(0, topicsPerPage - pinnedCount); - - if (!normalTidsToGet && data.stop !== -1) { - return next(null, []); - } - - if (plugins.hasListeners('filter:categories.getTopicIds')) { - return plugins.fireHook('filter:categories.getTopicIds', { - tids: [], - data: data, - pinnedTids: pinnedTids, - allPinnedTids: results.pinnedTids, - totalPinnedCount: totalPinnedCount, - normalTidsToGet: normalTidsToGet, - }, function (err, data) { - callback(err, data && data.tids); - }); - } - - var set = results.set; - var direction = results.direction; - var start = data.start; - if (start > 0 && totalPinnedCount) { - start -= totalPinnedCount - pinnedCount; - } - - var stop = data.stop === -1 ? data.stop : start + normalTidsToGet - 1; - - if (Array.isArray(set)) { - const weights = set.map((s, index) => (index ? 0 : 1)); - db[direction === 'highest-to-lowest' ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({ sets: set, start: start, stop: stop, weights: weights }, next); - } else { - db[direction === 'highest-to-lowest' ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, next); - } - }, - function (normalTids, next) { - normalTids = normalTids.filter(tid => !pinnedTids.includes(tid)); - - next(null, pinnedTids.concat(normalTids)); - }, - ], callback); + Categories.getTopicIds = async function (data) { + const dataForPinned = _.cloneDeep(data); + dataForPinned.start = 0; + dataForPinned.stop = -1; + + const [pinnedTids, set, direction] = await Promise.all([ + Categories.getPinnedTids(dataForPinned), + Categories.buildTopicsSortedSet(data), + Categories.getSortedSetRangeDirection(data.sort), + ]); + + const totalPinnedCount = pinnedTids.length; + const pinnedTidsOnPage = pinnedTids.slice(data.start, data.stop !== -1 ? data.stop + 1 : undefined); + const pinnedCountOnPage = pinnedTidsOnPage.length; + const topicsPerPage = data.stop - data.start + 1; + const normalTidsToGet = Math.max(0, topicsPerPage - pinnedCountOnPage); + + if (!normalTidsToGet && data.stop !== -1) { + return pinnedTidsOnPage; + } + + if (plugins.hasListeners('filter:categories.getTopicIds')) { + const result = await plugins.fireHook('filter:categories.getTopicIds', { + tids: [], + data: data, + pinnedTids: pinnedTidsOnPage, + allPinnedTids: pinnedTids, + totalPinnedCount: totalPinnedCount, + normalTidsToGet: normalTidsToGet, + }); + return result && result.tids; + } + + let start = data.start; + if (start > 0 && totalPinnedCount) { + start -= totalPinnedCount - pinnedCountOnPage; + } + + const stop = data.stop === -1 ? data.stop : start + normalTidsToGet - 1; + let normalTids; + const reverse = direction === 'highest-to-lowest'; + if (Array.isArray(set)) { + const weights = set.map((s, index) => (index ? 0 : 1)); + normalTids = await db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({ sets: set, start: start, stop: stop, weights: weights }); + } else { + normalTids = await db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop); + } + normalTids = normalTids.filter(tid => !pinnedTids.includes(tid)); + + return pinnedTids.concat(normalTids); }; - Categories.getTopicCount = function (data, callback) { + Categories.getTopicCount = async function (data) { if (plugins.hasListeners('filter:categories.getTopicCount')) { - return plugins.fireHook('filter:categories.getTopicCount', { + const result = await plugins.fireHook('filter:categories.getTopicCount', { topicCount: data.category.topic_count, data: data, - }, function (err, data) { - callback(err, data && data.topicCount); }); + return result && result.topicCount; } - async.waterfall([ - function (next) { - Categories.buildTopicsSortedSet(data, next); - }, - function (set, next) { - if (Array.isArray(set)) { - db.sortedSetIntersectCard(set, next); - } else { - next(null, data.category.topic_count); - } - }, - ], callback); + const set = await Categories.buildTopicsSortedSet(data); + if (Array.isArray(set)) { + return await db.sortedSetIntersectCard(set); + } + return data.category.topic_count; }; - Categories.buildTopicsSortedSet = function (data, callback) { + Categories.buildTopicsSortedSet = async function (data) { var cid = data.cid; var set = 'cid:' + cid + ':tids'; var sort = data.sort || (data.settings && data.settings.categoryTopicSort) || meta.config.categoryTopicSort || 'newest_to_oldest'; @@ -148,40 +113,37 @@ module.exports = function (Categories) { set = [set, 'tag:' + data.tag + ':topics']; } } - plugins.fireHook('filter:categories.buildTopicsSortedSet', { + const result = await plugins.fireHook('filter:categories.buildTopicsSortedSet', { set: set, data: data, - }, function (err, data) { - callback(err, data && data.set); }); + return result && result.set; }; - Categories.getSortedSetRangeDirection = function (sort, callback) { + Categories.getSortedSetRangeDirection = async function (sort) { sort = sort || 'newest_to_oldest'; var direction = sort === 'newest_to_oldest' || sort === 'most_posts' || sort === 'most_votes' ? 'highest-to-lowest' : 'lowest-to-highest'; - plugins.fireHook('filter:categories.getSortedSetRangeDirection', { + const result = await plugins.fireHook('filter:categories.getSortedSetRangeDirection', { sort: sort, direction: direction, - }, function (err, data) { - callback(err, data && data.direction); }); + return result && result.direction; }; - Categories.getAllTopicIds = function (cid, start, stop, callback) { - db.getSortedSetRange(['cid:' + cid + ':tids:pinned', 'cid:' + cid + ':tids'], start, stop, callback); + Categories.getAllTopicIds = async function (cid, start, stop) { + return await db.getSortedSetRange(['cid:' + cid + ':tids:pinned', 'cid:' + cid + ':tids'], start, stop); }; - Categories.getPinnedTids = function (data, callback) { + Categories.getPinnedTids = async function (data) { if (plugins.hasListeners('filter:categories.getPinnedTids')) { - return plugins.fireHook('filter:categories.getPinnedTids', { + const result = await plugins.fireHook('filter:categories.getPinnedTids', { pinnedTids: [], data: data, - }, function (err, data) { - callback(err, data && data.pinnedTids); }); + return result && result.pinnedTids; } - db.getSortedSetRevRange('cid:' + data.cid + ':tids:pinned', data.start, data.stop, callback); + return await db.getSortedSetRevRange('cid:' + data.cid + ':tids:pinned', data.start, data.stop); }; Categories.modifyTopicsByPrivilege = function (topics, privileges) { @@ -200,27 +162,18 @@ module.exports = function (Categories) { }); }; - Categories.onNewPostMade = function (cid, pinned, postData, callback) { + Categories.onNewPostMade = async function (cid, pinned, postData) { if (!cid || !postData) { - return setImmediate(callback); + return; } - - async.parallel([ - function (next) { - db.sortedSetAdd('cid:' + cid + ':pids', postData.timestamp, postData.pid, next); - }, - function (next) { - db.incrObjectField('category:' + cid, 'post_count', next); - }, - function (next) { - if (pinned) { - return setImmediate(next); - } - db.sortedSetIncrBy('cid:' + cid + ':tids:posts', 1, postData.tid, err => next(err)); - }, - function (next) { - Categories.updateRecentTid(cid, postData.tid, next); - }, - ], callback); + const promises = [ + db.sortedSetAdd('cid:' + cid + ':pids', postData.timestamp, postData.pid), + db.incrObjectField('category:' + cid, 'post_count'), + Categories.updateRecentTid(cid, postData.tid), + ]; + if (!pinned) { + promises.push(db.sortedSetIncrBy('cid:' + cid + ':tids:posts', 1, postData.tid)); + } + await Promise.all(promises); }; }; diff --git a/src/categories/unread.js b/src/categories/unread.js index c1a6b3d626..4b90efd21b 100644 --- a/src/categories/unread.js +++ b/src/categories/unread.js @@ -1,50 +1,38 @@ 'use strict'; -var async = require('async'); - -var db = require('../database'); +const db = require('../database'); module.exports = function (Categories) { - Categories.markAsRead = function (cids, uid, callback) { - callback = callback || function () {}; + Categories.markAsRead = async function (cids, uid) { if (!Array.isArray(cids) || !cids.length || parseInt(uid, 10) <= 0) { - return setImmediate(callback); + return; } - var keys = cids.map(cid => 'cid:' + cid + ':read_by_uid'); - - async.waterfall([ - function (next) { - db.isMemberOfSets(keys, uid, next); - }, - function (hasRead, next) { - keys = keys.filter((key, index) => !hasRead[index]); - - db.setsAdd(keys, uid, next); - }, - ], callback); + let keys = cids.map(cid => 'cid:' + cid + ':read_by_uid'); + const hasRead = await db.isMemberOfSets(keys, uid); + keys = keys.filter((key, index) => !hasRead[index]); + await db.setsAdd(keys, uid); }; - Categories.markAsUnreadForAll = function (cid, callback) { + Categories.markAsUnreadForAll = async function (cid) { if (!parseInt(cid, 10)) { - return callback(); + return; } - callback = callback || function () {}; - db.delete('cid:' + cid + ':read_by_uid', callback); + await db.delete('cid:' + cid + ':read_by_uid'); }; - Categories.hasReadCategories = function (cids, uid, callback) { + Categories.hasReadCategories = async function (cids, uid) { if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, null, cids.map(() => false)); + return cids.map(() => false); } const sets = cids.map(cid => 'cid:' + cid + ':read_by_uid'); - db.isMemberOfSets(sets, uid, callback); + return await db.isMemberOfSets(sets, uid); }; - Categories.hasReadCategory = function (cid, uid, callback) { + Categories.hasReadCategory = async function (cid, uid) { if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, null, false); + return false; } - db.isSetMember('cid:' + cid + ':read_by_uid', uid, callback); + return await db.isSetMember('cid:' + cid + ':read_by_uid', uid); }; }; diff --git a/src/categories/update.js b/src/categories/update.js index c89590c742..cd9f8cc9b6 100644 --- a/src/categories/update.js +++ b/src/categories/update.js @@ -10,162 +10,88 @@ var plugins = require('../plugins'); var cache = require('../cache'); module.exports = function (Categories) { - Categories.update = function (modified, callback) { + Categories.update = async function (modified) { var cids = Object.keys(modified); - - async.each(cids, function (cid, next) { - updateCategory(cid, modified[cid], next); - }, function (err) { - callback(err, cids); - }); + await Promise.all(cids.map(cid => updateCategory(cid, modified[cid]))); + return cids; }; - function updateCategory(cid, modifiedFields, callback) { - var category; - async.waterfall([ - function (next) { - Categories.exists(cid, next); - }, - function (exists, next) { - if (!exists) { - return callback(); - } + async function updateCategory(cid, modifiedFields) { + const exists = await Categories.exists(cid); + if (!exists) { + return; + } - if (modifiedFields.hasOwnProperty('name')) { - translator.translate(modifiedFields.name, function (translated) { - modifiedFields.slug = cid + '/' + utils.slugify(translated); - next(); - }); - } else { - next(); - } - }, - function (next) { - plugins.fireHook('filter:category.update', { cid: cid, category: modifiedFields }, next); - }, - function (categoryData, next) { - category = categoryData.category; - var fields = Object.keys(category); - // move parent to front, so its updated first - var parentCidIndex = fields.indexOf('parentCid'); - if (parentCidIndex !== -1 && fields.length > 1) { - fields.splice(0, 0, fields.splice(parentCidIndex, 1)[0]); - } + if (modifiedFields.hasOwnProperty('name')) { + const translated = await translator.translate(modifiedFields.name); + modifiedFields.slug = cid + '/' + utils.slugify(translated); + } + const result = await plugins.fireHook('filter:category.update', { cid: cid, category: modifiedFields }); - async.eachSeries(fields, function (key, next) { - updateCategoryField(cid, key, category[key], next); - }, next); - }, - function (next) { - plugins.fireHook('action:category.update', { cid: cid, modified: category }); - next(); - }, - ], callback); + const category = result.category; + var fields = Object.keys(category); + // move parent to front, so its updated first + var parentCidIndex = fields.indexOf('parentCid'); + if (parentCidIndex !== -1 && fields.length > 1) { + fields.splice(0, 0, fields.splice(parentCidIndex, 1)[0]); + } + + await async.eachSeries(fields, async function (key) { + await updateCategoryField(cid, key, category[key]); + }); + plugins.fireHook('action:category.update', { cid: cid, modified: category }); } - function updateCategoryField(cid, key, value, callback) { + async function updateCategoryField(cid, key, value) { if (key === 'parentCid') { - return updateParent(cid, value, callback); + return await updateParent(cid, value); } else if (key === 'tagWhitelist') { - return updateTagWhitelist(cid, value, callback); + return await updateTagWhitelist(cid, value); + } + await db.setObjectField('category:' + cid, key, value); + if (key === 'order') { + await updateOrder(cid, value); + } else if (key === 'description') { + await Categories.parseDescription(cid, value); } - - async.waterfall([ - function (next) { - db.setObjectField('category:' + cid, key, value, next); - }, - function (next) { - if (key === 'order') { - updateOrder(cid, value, next); - } else if (key === 'description') { - Categories.parseDescription(cid, value, next); - } else { - next(); - } - }, - ], callback); } - function updateParent(cid, newParent, callback) { - if (parseInt(cid, 10) === parseInt(newParent, 10)) { - return callback(new Error('[[error:cant-set-self-as-parent]]')); + async function updateParent(cid, newParent) { + newParent = parseInt(newParent, 10) || 0; + if (parseInt(cid, 10) === newParent) { + throw new Error('[[error:cant-set-self-as-parent]]'); } - async.waterfall([ - function (next) { - Categories.getChildrenCids(cid, next); - }, - function (childrenCids, next) { - if (childrenCids.includes(parseInt(newParent, 10))) { - return next(new Error('[[error:cant-set-child-as-parent]]')); - } - Categories.getCategoryField(cid, 'parentCid', next); - }, - function (oldParent, next) { - async.series([ - function (next) { - db.sortedSetRemove('cid:' + oldParent + ':children', cid, next); - }, - function (next) { - newParent = parseInt(newParent, 10) || 0; - db.sortedSetAdd('cid:' + newParent + ':children', cid, cid, next); - }, - function (next) { - db.setObjectField('category:' + cid, 'parentCid', newParent, next); - }, - function (next) { - cache.del(['cid:' + oldParent + ':children', 'cid:' + newParent + ':children']); - next(); - }, - ], next); - }, - ], function (err) { - callback(err); - }); - } + const childrenCids = await Categories.getChildrenCids(cid); + if (childrenCids.includes(newParent)) { + throw new Error('[[error:cant-set-child-as-parent]]'); + } + const oldParent = await Categories.getCategoryField(cid, 'parentCid'); + await Promise.all([ + db.sortedSetRemove('cid:' + oldParent + ':children', cid), + db.sortedSetAdd('cid:' + newParent + ':children', cid, cid), + db.setObjectField('category:' + cid, 'parentCid', newParent), + ]); - function updateTagWhitelist(cid, tags, callback) { - tags = tags.split(','); - tags = tags.map(function (tag) { - return utils.cleanUpTag(tag, meta.config.maximumTagLength); - }).filter(Boolean); + cache.del(['cid:' + oldParent + ':children', 'cid:' + newParent + ':children']); + } - async.waterfall([ - function (next) { - db.delete('cid:' + cid + ':tag:whitelist', next); - }, - function (next) { - var scores = tags.map((tag, index) => index); - db.sortedSetAdd('cid:' + cid + ':tag:whitelist', scores, tags, next); - }, - function (next) { - cache.del('cid:' + cid + ':tag:whitelist'); - next(); - }, - ], callback); + async function updateTagWhitelist(cid, tags) { + tags = tags.split(',').map(tag => utils.cleanUpTag(tag, meta.config.maximumTagLength)) + .filter(Boolean); + await db.delete('cid:' + cid + ':tag:whitelist'); + const scores = tags.map((tag, index) => index); + await db.sortedSetAdd('cid:' + cid + ':tag:whitelist', scores, tags); + cache.del('cid:' + cid + ':tag:whitelist'); } - function updateOrder(cid, order, callback) { - async.waterfall([ - function (next) { - Categories.getCategoryField(cid, 'parentCid', next); - }, - function (parentCid, next) { - db.sortedSetsAdd(['categories:cid', 'cid:' + parentCid + ':children'], order, cid, function (err) { - cache.del(['categories:cid', 'cid:' + parentCid + ':children']); - next(err); - }); - }, - ], err => callback(err)); + async function updateOrder(cid, order) { + const parentCid = await Categories.getCategoryField(cid, 'parentCid'); + await db.sortedSetsAdd(['categories:cid', 'cid:' + parentCid + ':children'], order, cid); + cache.del(['categories:cid', 'cid:' + parentCid + ':children']); } - Categories.parseDescription = function (cid, description, callback) { - async.waterfall([ - function (next) { - plugins.fireHook('filter:parse.raw', description, next); - }, - function (parsedDescription, next) { - Categories.setCategoryField(cid, 'descriptionParsed', parsedDescription, next); - }, - ], callback); + Categories.parseDescription = async function (cid, description) { + const parsedDescription = await plugins.fireHook('filter:parse.raw', description); + await Categories.setCategoryField(cid, 'descriptionParsed', parsedDescription); }; }; diff --git a/src/categories/watch.js b/src/categories/watch.js index 66a9ec6594..76048b942b 100644 --- a/src/categories/watch.js +++ b/src/categories/watch.js @@ -1,7 +1,5 @@ 'use strict'; -const async = require('async'); - const db = require('../database'); const user = require('../user'); @@ -12,69 +10,45 @@ module.exports = function (Categories) { watching: 3, }; - Categories.isIgnored = function (cids, uid, callback) { + Categories.isIgnored = async function (cids, uid) { if (!(parseInt(uid, 10) > 0)) { - return setImmediate(callback, null, cids.map(() => false)); + return cids.map(() => false); } - async.waterfall([ - function (next) { - Categories.getWatchState(cids, uid, next); - }, - function (states, next) { - next(null, states.map(state => state === Categories.watchStates.ignoring)); - }, - ], callback); + const states = await Categories.getWatchState(cids, uid); + return states.map(state => state === Categories.watchStates.ignoring); }; - Categories.getWatchState = function (cids, uid, callback) { + Categories.getWatchState = async function (cids, uid) { if (!(parseInt(uid, 10) > 0)) { - return setImmediate(callback, null, cids.map(() => Categories.watchStates.notwatching)); + return cids.map(() => Categories.watchStates.notwatching); } if (!Array.isArray(cids) || !cids.length) { - return setImmediate(callback, null, []); + return []; } - async.waterfall([ - function (next) { - const keys = cids.map(cid => 'cid:' + cid + ':uid:watch:state'); - async.parallel({ - userSettings: async.apply(user.getSettings, uid), - states: async.apply(db.sortedSetsScore, keys, uid), - }, next); - }, - function (results, next) { - next(null, results.states.map(state => state || Categories.watchStates[results.userSettings.categoryWatchState])); - }, - ], callback); + const keys = cids.map(cid => 'cid:' + cid + ':uid:watch:state'); + const [userSettings, states] = await Promise.all([ + user.getSettings(uid), + db.sortedSetsScore(keys, uid), + ]); + return states.map(state => state || Categories.watchStates[userSettings.categoryWatchState]); }; - Categories.getIgnorers = function (cid, start, stop, callback) { + Categories.getIgnorers = async function (cid, start, stop) { const count = (stop === -1) ? -1 : (stop - start + 1); - db.getSortedSetRevRangeByScore('cid:' + cid + ':uid:watch:state', start, count, Categories.watchStates.ignoring, Categories.watchStates.ignoring, callback); + return await db.getSortedSetRevRangeByScore('cid:' + cid + ':uid:watch:state', start, count, Categories.watchStates.ignoring, Categories.watchStates.ignoring); }; - Categories.filterIgnoringUids = function (cid, uids, callback) { - async.waterfall([ - function (next) { - Categories.getUidsWatchStates(cid, uids, next); - }, - function (states, next) { - const readingUids = uids.filter((uid, index) => uid && states[index] !== Categories.watchStates.ignoring); - next(null, readingUids); - }, - ], callback); + Categories.filterIgnoringUids = async function (cid, uids) { + const states = await Categories.getUidsWatchStates(cid, uids); + const readingUids = uids.filter((uid, index) => uid && states[index] !== Categories.watchStates.ignoring); + return readingUids; }; - Categories.getUidsWatchStates = function (cid, uids, callback) { - async.waterfall([ - function (next) { - async.parallel({ - userSettings: async.apply(user.getMultipleUserSettings, uids), - states: async.apply(db.sortedSetScores, 'cid:' + cid + ':uid:watch:state', uids), - }, next); - }, - function (results, next) { - next(null, results.states.map((state, index) => state || Categories.watchStates[results.userSettings[index].categoryWatchState])); - }, - ], callback); + Categories.getUidsWatchStates = async function (cid, uids) { + const [userSettings, states] = await Promise.all([ + user.getMultipleUserSettings(uids), + db.sortedSetScores('cid:' + cid + ':uid:watch:state', uids), + ]); + return states.map((state, index) => state || Categories.watchStates[userSettings[index].categoryWatchState]); }; }; diff --git a/src/database/mongo/sorted/remove.js b/src/database/mongo/sorted/remove.js index 3fe486d433..33521b349e 100644 --- a/src/database/mongo/sorted/remove.js +++ b/src/database/mongo/sorted/remove.js @@ -38,7 +38,9 @@ module.exports = function (db, module) { return; } var query = { _key: { $in: keys } }; - + if (keys.length === 1) { + query._key = keys[0]; + } if (min !== '-inf') { query.score = { $gte: parseFloat(min) }; } diff --git a/src/database/redis/pubsub.js b/src/database/redis/pubsub.js index cb989207a6..ea967e349d 100644 --- a/src/database/redis/pubsub.js +++ b/src/database/redis/pubsub.js @@ -13,7 +13,7 @@ var PubSub = function () { var subClient = db.connect(); this.pubClient = db.connect(); - channelName = 'db:' + nconf.get('redis:database') + 'pubsub_channel'; + channelName = 'db:' + nconf.get('redis:database') + ':pubsub_channel'; subClient.subscribe(channelName); subClient.on('message', function (channel, message) { diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index 9497abed61..5a2f9236e4 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -123,7 +123,7 @@ function copyPrivilegesToChildrenRecursive(parentCid, category, group, callback) }, function (next) { async.eachSeries(category.children, function (child, next) { - copyPrivilegesToChildrenRecursive(parentCid, child, next); + copyPrivilegesToChildrenRecursive(parentCid, child, group, next); }, next); }, ], callback);