diff --git a/src/notifications.js b/src/notifications.js index 731020c0ba..ce2bfe51f3 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -34,18 +34,12 @@ Notifications.privilegedTypes = [ 'notificationType_new-user-flag', ]; -Notifications.getAllNotificationTypes = function (callback) { - async.waterfall([ - function (next) { - plugins.fireHook('filter:user.notificationTypes', { - types: Notifications.baseTypes.slice(), - privilegedTypes: Notifications.privilegedTypes.slice(), - }, next); - }, - function (results, next) { - next(null, results.types.concat(results.privilegedTypes)); - }, - ], callback); +Notifications.getAllNotificationTypes = async function () { + const results = await plugins.fireHook('filter:user.notificationTypes', { + types: Notifications.baseTypes.slice(), + privilegedTypes: Notifications.privilegedTypes.slice(), + }); + return results.types.concat(results.privilegedTypes); }; Notifications.startJobs = function () { @@ -53,133 +47,85 @@ Notifications.startJobs = function () { new cron('*/30 * * * *', Notifications.prune, null, true); }; -Notifications.get = function (nid, callback) { - Notifications.getMultiple([nid], function (err, notifications) { - callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null); - }); +Notifications.get = async function (nid) { + const notifications = await Notifications.getMultiple([nid]); + return Array.isArray(notifications) && notifications.length ? notifications[0] : null; }; -Notifications.getMultiple = function (nids, callback) { +Notifications.getMultiple = async function (nids) { if (!Array.isArray(nids) || !nids.length) { - return setImmediate(callback, null, []); + return []; } - var notifications; - - async.waterfall([ - function (next) { - const keys = nids.map(nid => 'notifications:' + nid); - db.getObjects(keys, next); - }, - function (_notifications, next) { - notifications = _notifications; - - const userKeys = notifications.map(n => n && n.from); - User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], next); - }, - function (usersData, next) { - notifications.forEach(function (notification, index) { - if (notification) { - notification.datetimeISO = utils.toISOString(notification.datetime); - - if (notification.bodyLong) { - notification.bodyLong = utils.escapeHTML(notification.bodyLong); - } - - notification.user = usersData[index]; - if (notification.user) { - notification.image = notification.user.picture || null; - if (notification.user.username === '[[global:guest]]') { - notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2'); - } - } else if (notification.image === 'brand:logo' || !notification.image) { - notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png'; - } + const keys = nids.map(nid => 'notifications:' + nid); + const notifications = await db.getObjects(keys); + + const userKeys = notifications.map(n => n && n.from); + const usersData = await User.getUsersFields(userKeys, ['username', 'userslug', 'picture']); + + notifications.forEach(function (notification, index) { + if (notification) { + notification.datetimeISO = utils.toISOString(notification.datetime); + + if (notification.bodyLong) { + notification.bodyLong = utils.escapeHTML(notification.bodyLong); + } + + notification.user = usersData[index]; + if (notification.user) { + notification.image = notification.user.picture || null; + if (notification.user.username === '[[global:guest]]') { + notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2'); } - }); - next(null, notifications); - }, - ], callback); + } else if (notification.image === 'brand:logo' || !notification.image) { + notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png'; + } + } + }); + return notifications; }; -Notifications.filterExists = function (nids, callback) { - async.waterfall([ - function (next) { - db.isSortedSetMembers('notifications', nids, next); - }, - function (exists, next) { - nids = nids.filter((notifId, idx) => exists[idx]); - next(null, nids); - }, - ], callback); +Notifications.filterExists = async function (nids) { + const exists = await db.isSortedSetMembers('notifications', nids); + return nids.filter((nid, idx) => exists[idx]); }; -Notifications.findRelated = function (mergeIds, set, callback) { +Notifications.findRelated = async function (mergeIds, set) { // A related notification is one in a zset that has the same mergeId - var nids; - - async.waterfall([ - async.apply(db.getSortedSetRevRange, set, 0, -1), - function (_nids, next) { - nids = _nids; - - var keys = nids.map(nid => 'notifications:' + nid); - db.getObjectsFields(keys, ['mergeId'], next); - }, - function (sets, next) { - sets = sets.map(set => String(set.mergeId)); - var mergeSet = new Set(mergeIds.map(id => String(id))); - next(null, nids.filter((nid, idx) => mergeSet.has(sets[idx]))); - }, - ], callback); + const nids = await db.getSortedSetRevRange(set, 0, -1); + + const keys = nids.map(nid => 'notifications:' + nid); + let sets = await db.getObjectsFields(keys, ['mergeId']); + sets = sets.map(set => String(set.mergeId)); + var mergeSet = new Set(mergeIds.map(id => String(id))); + return nids.filter((nid, idx) => mergeSet.has(sets[idx])); }; -Notifications.create = function (data, callback) { +Notifications.create = async function (data) { if (!data.nid) { - return callback(new Error('[[error:no-notification-id]]')); + throw new Error('[[error:no-notification-id]]'); } data.importance = data.importance || 5; - async.waterfall([ - function (next) { - db.getObject('notifications:' + data.nid, next); - }, - function (oldNotification, next) { - if (oldNotification) { - if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) { - return callback(null, null); - } - } - var now = Date.now(); - data.datetime = now; - async.parallel([ - function (next) { - db.sortedSetAdd('notifications', now, data.nid, next); - }, - function (next) { - db.setObject('notifications:' + data.nid, data, next); - }, - ], function (err) { - next(err, data); - }); - }, - ], callback); + const oldNotif = await db.getObject('notifications:' + data.nid); + if (oldNotif && parseInt(oldNotif.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotif.importance, 10) > parseInt(data.importance, 10)) { + return null; + } + const now = Date.now(); + data.datetime = now; + await Promise.all([ + db.sortedSetAdd('notifications', now, data.nid), + db.setObject('notifications:' + data.nid, data), + ]); + return data; }; -Notifications.push = function (notification, uids, callback) { - callback = callback || function () {}; - +Notifications.push = async function (notification, uids) { if (!notification || !notification.nid) { - return callback(); + return; } - - if (!Array.isArray(uids)) { - uids = [uids]; - } - - uids = _.uniq(uids); - + uids = Array.isArray(uids) ? _.uniq(uids) : [uids]; if (!uids.length) { - return callback(); + return; } setTimeout(function () { @@ -191,55 +137,35 @@ Notifications.push = function (notification, uids, callback) { } }); }, 1000); - - callback(); }; -function pushToUids(uids, notification, callback) { - function sendNotification(uids, callback) { +async function pushToUids(uids, notification) { + async function sendNotification(uids) { if (!uids.length) { - return callback(); + return; + } + const oneWeekAgo = Date.now() - 604800000; + const unreadKeys = uids.map(uid => 'uid:' + uid + ':notifications:unread'); + const readKeys = uids.map(uid => 'uid:' + uid + ':notifications:read'); + await db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid); + await db.sortedSetsRemove(readKeys, notification.nid); + await db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo); + await db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo); + const websockets = require('./socket.io'); + if (websockets.server) { + uids.forEach(function (uid) { + websockets.in('uid_' + uid).emit('event:new_notification', notification); + }); } - var oneWeekAgo = Date.now() - 604800000; - var unreadKeys = []; - var readKeys = []; - async.waterfall([ - function (next) { - uids.forEach(function (uid) { - unreadKeys.push('uid:' + uid + ':notifications:unread'); - readKeys.push('uid:' + uid + ':notifications:read'); - }); - - db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next); - }, - function (next) { - db.sortedSetsRemove(readKeys, notification.nid, next); - }, - function (next) { - db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next); - }, - function (next) { - db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next); - }, - function (next) { - var websockets = require('./socket.io'); - if (websockets.server) { - uids.forEach(function (uid) { - websockets.in('uid_' + uid).emit('event:new_notification', notification); - }); - } - next(); - }, - ], callback); } - function sendEmail(uids, callback) { + async function sendEmail(uids) { // Update CTA messaging (as not all notification types need custom text) if (['new-reply', 'new-chat'].includes(notification.type)) { notification['cta-type'] = notification.type; } - async.eachLimit(uids, 3, function (uid, next) { + await async.eachLimit(uids, 3, function (uid, next) { emailer.send('notification', uid, { path: notification.path, subject: utils.stripHTMLTags(notification.subject || '[[notifications:new_notification]]'), @@ -248,237 +174,148 @@ function pushToUids(uids, notification, callback) { notification: notification, showUnsubscribe: true, }, next); - }, callback); + }); } - function getUidsBySettings(uids, callback) { - var uidsToNotify = []; - var uidsToEmail = []; - async.waterfall([ - function (next) { - User.getMultipleUserSettings(uids, next); - }, - function (usersSettings, next) { - usersSettings.forEach(function (userSettings) { - var setting = userSettings['notificationType_' + notification.type] || 'notification'; - - if (setting === 'notification' || setting === 'notificationemail') { - uidsToNotify.push(userSettings.uid); - } - - if (setting === 'email' || setting === 'notificationemail') { - uidsToEmail.push(userSettings.uid); - } - }); - next(null, { uidsToNotify: uidsToNotify, uidsToEmail: uidsToEmail }); - }, - ], callback); - } + async function getUidsBySettings(uids) { + const uidsToNotify = []; + const uidsToEmail = []; + const usersSettings = await User.getMultipleUserSettings(uids); + usersSettings.forEach(function (userSettings) { + const setting = userSettings['notificationType_' + notification.type] || 'notification'; - async.waterfall([ - function (next) { - // Remove uid from recipients list if they have blocked the user triggering the notification - User.blocks.filterUids(notification.from, uids, next); - }, - function (uids, next) { - plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next); - }, - function (data, next) { - if (!data || !data.notification || !data.uids || !data.uids.length) { - return callback(); + if (setting === 'notification' || setting === 'notificationemail') { + uidsToNotify.push(userSettings.uid); } - notification = data.notification; - if (notification.type) { - getUidsBySettings(data.uids, next); - } else { - next(null, { uidsToNotify: data.uids, uidsToEmail: [] }); + + if (setting === 'email' || setting === 'notificationemail') { + uidsToEmail.push(userSettings.uid); } - }, - function (results, next) { - async.parallel([ - function (next) { - sendNotification(results.uidsToNotify, next); - }, - function (next) { - sendEmail(results.uidsToEmail, next); - }, - ], function (err) { - next(err, results); - }); - }, - function (results, next) { - plugins.fireHook('action:notification.pushed', { - notification: notification, - uids: results.uidsToNotify, - uidsNotified: results.uidsToNotify, - uidsEmailed: results.uidsToEmail, - }); - next(); - }, - ], callback); + }); + return { uidsToNotify: uidsToNotify, uidsToEmail: uidsToEmail }; + } + + // Remove uid from recipients list if they have blocked the user triggering the notification + uids = await User.blocks.filterUids(notification.from, uids); + const data = await plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }); + if (!data || !data.notification || !data.uids || !data.uids.length) { + return; + } + + notification = data.notification; + let results = { uidsToNotify: data.uids, uidsToEmail: [] }; + if (notification.type) { + results = await getUidsBySettings(data.uids); + } + await Promise.all([ + sendNotification(results.uidsToNotify), + sendEmail(results.uidsToEmail), + ]); + plugins.fireHook('action:notification.pushed', { + notification: notification, + uids: results.uidsToNotify, + uidsNotified: results.uidsToNotify, + uidsEmailed: results.uidsToEmail, + }); } -Notifications.pushGroup = function (notification, groupName, callback) { - callback = callback || function () {}; +Notifications.pushGroup = async function (notification, groupName) { if (!notification) { - return callback(); + return; } - async.waterfall([ - function (next) { - groups.getMembers(groupName, 0, -1, next); - }, - function (members, next) { - Notifications.push(notification, members, next); - }, - ], callback); + const members = await groups.getMembers(groupName, 0, -1); + await Notifications.push(notification, members); }; -Notifications.pushGroups = function (notification, groupNames, callback) { - callback = callback || function () {}; +Notifications.pushGroups = async function (notification, groupNames) { if (!notification) { - return callback(); + return; } - async.waterfall([ - function (next) { - groups.getMembersOfGroups(groupNames, next); - }, - function (groupMembers, next) { - var members = _.uniq(_.flatten(groupMembers)); - Notifications.push(notification, members, next); - }, - ], callback); + let groupMembers = await groups.getMembersOfGroups(groupNames); + groupMembers = _.uniq(_.flatten(groupMembers)); + await Notifications.push(notification, groupMembers); }; -Notifications.rescind = function (nid, callback) { - callback = callback || function () {}; - - async.parallel([ - async.apply(db.sortedSetRemove, 'notifications', nid), - async.apply(db.delete, 'notifications:' + nid), - ], function (err) { - callback(err); - }); +Notifications.rescind = async function (nid) { + await Promise.all([ + db.sortedSetRemove('notifications', nid), + db.delete('notifications:' + nid), + ]); }; -Notifications.markRead = function (nid, uid, callback) { - callback = callback || function () {}; +Notifications.markRead = async function (nid, uid) { if (parseInt(uid, 10) <= 0 || !nid) { - return setImmediate(callback); + return; } - Notifications.markReadMultiple([nid], uid, callback); + await Notifications.markReadMultiple([nid], uid); }; -Notifications.markUnread = function (nid, uid, callback) { - callback = callback || function () {}; +Notifications.markUnread = async function (nid, uid) { if (parseInt(uid, 10) <= 0 || !nid) { - return setImmediate(callback); + return; } - async.waterfall([ - function (next) { - db.getObject('notifications:' + nid, next); - }, - function (notification, next) { - if (!notification) { - return callback(new Error('[[error:no-notification]]')); - } - notification.datetime = notification.datetime || Date.now(); - - async.parallel([ - async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid), - async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid), - ], next); - }, - ], function (err) { - callback(err); - }); + const notification = await db.getObject('notifications:' + nid); + if (!notification) { + throw new Error('[[error:no-notification]]'); + } + notification.datetime = notification.datetime || Date.now(); + + await Promise.all([ + db.sortedSetRemove('uid:' + uid + ':notifications:read', nid), + db.sortedSetAdd('uid:' + uid + ':notifications:unread', notification.datetime, nid), + ]); }; -Notifications.markReadMultiple = function (nids, uid, callback) { - callback = callback || function () {}; +Notifications.markReadMultiple = async function (nids, uid) { nids = nids.filter(Boolean); if (!Array.isArray(nids) || !nids.length) { - return callback(); + return; } let notificationKeys = nids.map(nid => 'notifications:' + nid); - - async.waterfall([ - async.apply(db.getObjectsFields, notificationKeys, ['mergeId']), - function (mergeIds, next) { - // Isolate mergeIds and find related notifications - mergeIds = _.uniq(mergeIds.map(set => set.mergeId)); - - Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next); - }, - function (relatedNids, next) { - notificationKeys = _.union(nids, relatedNids).map(nid => 'notifications:' + nid); - - db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next); - }, - function (notificationData, next) { - notificationData = notificationData.filter(n => n && n.nid); - - nids = notificationData.map(n => n.nid); - - async.parallel([ - function (next) { - db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next); - }, - function (next) { - const datetimes = notificationData.map(n => (n && n.datetime) || Date.now()); - db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next); - }, - ], next); - }, - ], function (err) { - callback(err); - }); + let mergeIds = await db.getObjectsFields(notificationKeys, ['mergeId']); + // Isolate mergeIds and find related notifications + mergeIds = _.uniq(mergeIds.map(set => set.mergeId)); + + const relatedNids = await Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread'); + notificationKeys = _.union(nids, relatedNids).map(nid => 'notifications:' + nid); + + let notificationData = await db.getObjectsFields(notificationKeys, ['nid', 'datetime']); + notificationData = notificationData.filter(n => n && n.nid); + + nids = notificationData.map(n => n.nid); + const datetimes = notificationData.map(n => (n && n.datetime) || Date.now()); + await Promise.all([ + db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids), + db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids), + ]); }; -Notifications.markAllRead = function (uid, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, next); - }, - function (nids, next) { - Notifications.markReadMultiple(nids, uid, next); - }, - ], callback); +Notifications.markAllRead = async function (uid) { + const nids = await db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99); + await Notifications.markReadMultiple(nids, uid); }; -Notifications.prune = function (callback) { - callback = callback || function () {}; - - async.waterfall([ - function (next) { - const week = 604800000; - const cutoffTime = Date.now() - week; - db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, next); - }, - function (nids, next) { - if (!nids.length) { - return callback(); - } - - async.parallel([ - function (next) { - db.sortedSetRemove('notifications', nids, next); - }, - function (next) { - const keys = nids.map(nid => 'notifications:' + nid); - db.deleteAll(keys, next); - }, - ], next); - }, - ], function (err) { +Notifications.prune = async function () { + const week = 604800000; + const cutoffTime = Date.now() - week; + const nids = await db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime); + if (!nids.length) { + return; + } + try { + await Promise.all([ + db.sortedSetRemove('notifications', nids), + db.deleteAll(nids.map(nid => 'notifications:' + nid)), + ]); + } catch (err) { if (err) { winston.error('Encountered error pruning notifications', err); } - callback(err); - }); + } }; -Notifications.merge = function (notifications, callback) { +Notifications.merge = async function (notifications) { // When passed a set of notification objects, merge any that can be merged var mergeIds = [ 'notifications:upvoted_your_post_in', @@ -575,11 +412,10 @@ Notifications.merge = function (notifications, callback) { return notifications; }, notifications); - plugins.fireHook('filter:notifications.merge', { + const data = await plugins.fireHook('filter:notifications.merge', { notifications: notifications, - }, function (err, data) { - callback(err, data.notifications); }); + return data && data.notifications; }; Notifications.async = require('./promisify')(Notifications);