diff --git a/src/user/digest.js b/src/user/digest.js index 8b6d465c4f..c792ad8f83 100644 --- a/src/user/digest.js +++ b/src/user/digest.js @@ -14,182 +14,122 @@ var utils = require('../utils'); var Digest = module.exports; -Digest.execute = function (payload, callback) { - callback = callback || function () {}; - - var digestsDisabled = meta.config.disableEmailSubscriptions === 1; +Digest.execute = async function (payload) { + const digestsDisabled = meta.config.disableEmailSubscriptions === 1; if (digestsDisabled) { winston.info('[user/jobs] Did not send digests (' + payload.interval + ') because subscription system is disabled.'); - return callback(); + return; + } + let subscribers = payload.subscribers; + if (!subscribers) { + subscribers = await Digest.getSubscribers(payload.interval); + } + if (!subscribers.length) { + return; + } + try { + const count = await Digest.send({ + interval: payload.interval, + subscribers: subscribers, + }); + winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. ' + count + ' email(s) sent.'); + } catch (err) { + winston.error('[user/jobs] Could not send digests (' + payload.interval + ')', err); + throw err; } - - async.waterfall([ - function (next) { - if (payload.subscribers) { - setImmediate(next, undefined, payload.subscribers); - } else { - Digest.getSubscribers(payload.interval, next); - } - }, - function (subscribers, next) { - if (!subscribers.length) { - return callback(); - } - - var data = { - interval: payload.interval, - subscribers: subscribers, - }; - - Digest.send(data, next); - }, - ], function (err, count) { - if (err) { - winston.error('[user/jobs] Could not send digests (' + payload.interval + ')', err); - } else { - winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. ' + count + ' email(s) sent.'); - } - - callback(err); - }); }; -Digest.getSubscribers = function (interval, callback) { - async.waterfall([ - function (next) { - var subs = []; +Digest.getSubscribers = async function (interval) { + var subscribers = []; - batch.processSortedSet('users:joindate', function (uids, next) { - async.waterfall([ - function (next) { - user.getMultipleUserSettings(uids, next); - }, - function (settings, next) { - const subUids = []; - settings.forEach(function (hash) { - if (hash.dailyDigestFreq === interval) { - subUids.push(hash.uid); - } - }); - user.bans.filterBanned(subUids, next); - }, - function (uids, next) { - subs = subs.concat(uids); - next(); - }, - ], next); - }, { interval: 1000 }, function (err) { - next(err, subs); - }); - }, - function (subscribers, next) { - plugins.fireHook('filter:digest.subscribers', { - interval: interval, - subscribers: subscribers, - }, next); - }, - function (results, next) { - next(null, results.subscribers); - }, - ], callback); + await batch.processSortedSet('users:joindate', async function (uids) { + const settings = await user.getMultipleUserSettings(uids); + let subUids = []; + settings.forEach(function (hash) { + if (hash.dailyDigestFreq === interval) { + subUids.push(hash.uid); + } + }); + subUids = await user.bans.filterBanned(subUids); + subscribers = subscribers.concat(subUids); + }, { interval: 1000 }); + + const results = await plugins.fireHook('filter:digest.subscribers', { + interval: interval, + subscribers: subscribers, + }); + return results.subscribers; }; -Digest.send = function (data, callback) { +Digest.send = async function (data) { var emailsSent = 0; if (!data || !data.subscribers || !data.subscribers.length) { - return callback(null, emailsSent); + return emailsSent; } - var now = new Date(); - - async.waterfall([ - function (next) { - user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline'], next); - }, - function (users, next) { - async.eachLimit(users, 100, function (userObj, next) { - async.waterfall([ - function (next) { - async.parallel({ - notifications: async.apply(user.notifications.getDailyUnread, userObj.uid), - topics: async.apply(getTermTopics, data.interval, userObj.uid, 0, 9), - }, next); - }, - function (data, next) { - var notifications = data.notifications.filter(Boolean); - - // If there are no notifications and no new topics, don't bother sending a digest - if (!notifications.length && !data.topics.length) { - return next(); - } - - notifications.forEach(function (notification) { - if (notification.image && !notification.image.startsWith('http')) { - notification.image = nconf.get('url') + notification.image; - } - }); + const now = new Date(); + + const users = await user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline']); + + async.eachLimit(users, 100, async function (userObj) { + let [notifications, topics] = await Promise.all([ + user.notifications.getDailyUnread(userObj.uid), + getTermTopics(data.interval, userObj.uid, 0, 9), + ]); + notifications = notifications.filter(Boolean); + // If there are no notifications and no new topics, don't bother sending a digest + if (!notifications.length && !topics.length) { + return; + } - // Fix relative paths in topic data - data.topics = data.topics.map(function (topicObj) { - var user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user; - if (user && user.picture && utils.isRelativeUrl(user.picture)) { - user.picture = nconf.get('base_url') + user.picture; - } + notifications.forEach(function (notification) { + if (notification.image && !notification.image.startsWith('http')) { + notification.image = nconf.get('url') + notification.image; + } + }); - return topicObj; - }); - emailsSent += 1; - emailer.send('digest', userObj.uid, { - subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]', - username: userObj.username, - userslug: userObj.userslug, - notifications: notifications, - recent: data.topics, - interval: data.interval, - showUnsubscribe: true, - }, function (err) { - if (err) { - winston.error('[user/jobs] Could not send digest email', err); - } - }); - next(); - }, - ], next); - }, next); - }, - ], function (err) { - callback(err, emailsSent); + // Fix relative paths in topic data + topics = topics.map(function (topicObj) { + const user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user; + if (user && user.picture && utils.isRelativeUrl(user.picture)) { + user.picture = nconf.get('base_url') + user.picture; + } + return topicObj; + }); + emailsSent += 1; + emailer.send('digest', userObj.uid, { + subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]', + username: userObj.username, + userslug: userObj.userslug, + notifications: notifications, + recent: data.topics, + interval: data.interval, + showUnsubscribe: true, + }, function (err) { + if (err) { + winston.error('[user/jobs] Could not send digest email', err); + } + }); }); + return emailsSent; +}; - function getTermTopics(term, uid, start, stop, callback) { - const options = { - uid: uid, - start: start, - stop: stop, - term: term, - sort: 'posts', - teaserPost: 'last-post', - }; - - async.waterfall([ - function (next) { - topics.getSortedTopics(options, next); - }, - function (data, next) { - if (!data.topics.length) { - topics.getLatestTopics(options, next); - } else { - next(null, data); - } - }, - (data, next) => { - data.topics.forEach(function (topicObj) { - if (topicObj && topicObj.teaser && topicObj.teaser.content && topicObj.teaser.content.length > 255) { - topicObj.teaser.content = topicObj.teaser.content.slice(0, 255) + '...'; - } - }); - - next(null, data.topics); - }, - ], callback); +async function getTermTopics(term, uid, start, stop) { + const options = { + uid: uid, + start: start, + stop: stop, + term: term, + sort: 'posts', + teaserPost: 'last-post', + }; + let data = await topics.getSortedTopics(options); + if (!data.topics.length) { + data = await topics.getLatestTopics(options); } -}; + data.topics.forEach(function (topicObj) { + if (topicObj && topicObj.teaser && topicObj.teaser.content && topicObj.teaser.content.length > 255) { + topicObj.teaser.content = topicObj.teaser.content.slice(0, 255) + '...'; + } + }); + return data.topics; +} diff --git a/src/user/email.js b/src/user/email.js index 99c267583b..1a76974c59 100644 --- a/src/user/email.js +++ b/src/user/email.js @@ -1,7 +1,6 @@ 'use strict'; -var async = require('async'); var nconf = require('nconf'); var user = require('../user'); @@ -13,30 +12,24 @@ var emailer = require('../emailer'); var UserEmail = module.exports; -UserEmail.exists = function (email, callback) { - user.getUidByEmail(email.toLowerCase(), function (err, exists) { - callback(err, !!exists); - }); +UserEmail.exists = async function (email) { + const uid = await user.getUidByEmail(email.toLowerCase()); + return !!uid; }; -UserEmail.available = function (email, callback) { - db.isSortedSetMember('email:uid', email.toLowerCase(), function (err, exists) { - callback(err, !exists); - }); +UserEmail.available = async function (email) { + const exists = await db.isSortedSetMember('email:uid', email.toLowerCase()); + return !exists; }; -UserEmail.sendValidationEmail = function (uid, options, callback) { +UserEmail.sendValidationEmail = async function (uid, options) { /* * Options: * - email, overrides email retrieval * - force, sends email even if it is too soon to send another */ - // Handling for 2 arguments - if (arguments.length === 2 && typeof options === 'function') { - callback = options; - options = {}; - } + options = options || {}; // Fallback behaviour (email passed in as second argument) if (typeof options === 'string') { @@ -45,117 +38,66 @@ UserEmail.sendValidationEmail = function (uid, options, callback) { }; } - callback = callback || function () {}; - var confirm_code = utils.generateUUID(); - var confirm_link = nconf.get('url') + '/confirm/' + confirm_code; - - var emailInterval = meta.config.emailConfirmInterval; - - async.waterfall([ - function (next) { - // If no email passed in (default), retrieve email from uid - if (options.email && options.email.length) { - return setImmediate(next, null, options.email); - } - - user.getUserField(uid, 'email', next); - }, - function (email, next) { - options.email = email; - if (!options.email) { - return callback(); - } - - if (options.force) { - return setImmediate(next, null, false); - } - - db.get('uid:' + uid + ':confirm:email:sent', next); - }, - function (sent, next) { - if (sent) { - return next(new Error('[[error:confirm-email-already-sent, ' + emailInterval + ']]')); - } - db.set('uid:' + uid + ':confirm:email:sent', 1, next); - }, - function (next) { - db.pexpireAt('uid:' + uid + ':confirm:email:sent', Date.now() + (emailInterval * 60 * 1000), next); - }, - function (next) { - plugins.fireHook('filter:user.verify.code', confirm_code, next); - }, - function (_confirm_code, next) { - confirm_code = _confirm_code; - db.setObject('confirm:' + confirm_code, { - email: options.email.toLowerCase(), - uid: uid, - }, next); - }, - function (next) { - db.expireAt('confirm:' + confirm_code, Math.floor((Date.now() / 1000) + (60 * 60 * 24)), next); - }, - function (next) { - user.getUserField(uid, 'username', next); - }, - function (username, next) { - var title = meta.config.title || meta.config.browserTitle || 'NodeBB'; - var template = options.template || 'welcome'; - - var data = { - username: username, - confirm_link: confirm_link, - confirm_code: confirm_code, - - subject: options.subject || '[[email:welcome-to, ' + title + ']]', - template: template, - uid: uid, - }; - - if (plugins.hasListeners('action:user.verify')) { - plugins.fireHook('action:user.verify', { uid: uid, data: data }); - next(); - } else { - emailer.send(template, uid, data, next); - } - }, - function (next) { - next(null, confirm_code); - }, - ], callback); -}; + let confirm_code = utils.generateUUID(); + const confirm_link = nconf.get('url') + '/confirm/' + confirm_code; -UserEmail.confirm = function (code, callback) { - var confirmObj; - async.waterfall([ - function (next) { - db.getObject('confirm:' + code, next); - }, - function (_confirmObj, next) { - confirmObj = _confirmObj; - if (!confirmObj || !confirmObj.uid || !confirmObj.email) { - return next(new Error('[[error:invalid-data]]')); - } - - user.getUserField(confirmObj.uid, 'email', next); - }, - function (currentEmail, next) { - if (!currentEmail || currentEmail.toLowerCase() !== confirmObj.email) { - return next(new Error('[[error:invalid-email]]')); - } - - async.series([ - async.apply(user.setUserField, confirmObj.uid, 'email:confirmed', 1), - async.apply(db.delete, 'confirm:' + code), - async.apply(db.delete, 'uid:' + confirmObj.uid + ':confirm:email:sent'), - function (next) { - db.sortedSetRemove('users:notvalidated', confirmObj.uid, next); - }, - function (next) { - plugins.fireHook('action:user.email.confirmed', { uid: confirmObj.uid, email: confirmObj.email }, next); - }, - ], next); - }, - ], function (err) { - callback(err); + const emailInterval = meta.config.emailConfirmInterval; + + // If no email passed in (default), retrieve email from uid + if (!options.email || !options.email.length) { + options.email = await user.getUserField(uid, 'email'); + } + if (!options.email) { + return; + } + let sent = false; + if (!options.force) { + sent = await db.get('uid:' + uid + ':confirm:email:sent'); + } + if (sent) { + throw new Error('[[error:confirm-email-already-sent, ' + emailInterval + ']]'); + } + await db.set('uid:' + uid + ':confirm:email:sent', 1); + await db.pexpireAt('uid:' + uid + ':confirm:email:sent', Date.now() + (emailInterval * 60 * 1000)); + confirm_code = await plugins.fireHook('filter:user.verify.code', confirm_code); + + await db.setObject('confirm:' + confirm_code, { + email: options.email.toLowerCase(), + uid: uid, }); + await db.expireAt('confirm:' + confirm_code, Math.floor((Date.now() / 1000) + (60 * 60 * 24))); + const username = await user.getUserField(uid, 'username'); + + const data = { + username: username, + confirm_link: confirm_link, + confirm_code: confirm_code, + + subject: options.subject || '[[email:welcome-to, ' + (meta.config.title || meta.config.browserTitle || 'NodeBB') + ']]', + template: options.template || 'welcome', + uid: uid, + }; + + if (plugins.hasListeners('action:user.verify')) { + plugins.fireHook('action:user.verify', { uid: uid, data: data }); + } else { + await emailer.send(data.template, uid, data); + } + return confirm_code; +}; + +UserEmail.confirm = async function (code) { + const confirmObj = await db.getObject('confirm:' + code); + if (!confirmObj || !confirmObj.uid || !confirmObj.email) { + throw new Error('[[error:invalid-data]]'); + } + const currentEmail = await user.getUserField(confirmObj.uid, 'email'); + if (!currentEmail || currentEmail.toLowerCase() !== confirmObj.email) { + throw new Error('[[error:invalid-email]]'); + } + await user.setUserField(confirmObj.uid, 'email:confirmed', 1); + await db.delete('confirm:' + code); + await db.delete('uid:' + confirmObj.uid + ':confirm:email:sent'); + await db.sortedSetRemove('users:notvalidated', confirmObj.uid); + await plugins.fireHook('action:user.email.confirmed', { uid: confirmObj.uid, email: confirmObj.email }); }; diff --git a/src/user/follow.js b/src/user/follow.js index 50a011f111..9e37a62a8d 100644 --- a/src/user/follow.js +++ b/src/user/follow.js @@ -1,103 +1,85 @@ 'use strict'; -var async = require('async'); var plugins = require('../plugins'); var db = require('../database'); module.exports = function (User) { - User.follow = function (uid, followuid, callback) { - toggleFollow('follow', uid, followuid, callback); + User.follow = async function (uid, followuid) { + await toggleFollow('follow', uid, followuid); }; - User.unfollow = function (uid, unfollowuid, callback) { - toggleFollow('unfollow', uid, unfollowuid, callback); + User.unfollow = async function (uid, unfollowuid) { + await toggleFollow('unfollow', uid, unfollowuid); }; - function toggleFollow(type, uid, theiruid, callback) { + async function toggleFollow(type, uid, theiruid) { if (parseInt(uid, 10) <= 0 || parseInt(theiruid, 10) <= 0) { - return callback(new Error('[[error:invalid-uid]]')); + throw new Error('[[error:invalid-uid]]'); } if (parseInt(uid, 10) === parseInt(theiruid, 10)) { - return callback(new Error('[[error:you-cant-follow-yourself]]')); + throw new Error('[[error:you-cant-follow-yourself]]'); + } + const exists = await User.exists(theiruid); + if (!exists) { + throw new Error('[[error:no-user]]'); + } + const isFollowing = await User.isFollowing(uid, theiruid); + if (type === 'follow') { + if (isFollowing) { + throw new Error('[[error:already-following]]'); + } + const now = Date.now(); + await Promise.all([ + db.sortedSetAddBulk([ + ['following:' + uid, now, theiruid], + ['followers:' + theiruid, now, uid], + ]), + User.incrementUserFieldBy(uid, 'followingCount', 1), + User.incrementUserFieldBy(theiruid, 'followerCount', 1), + ]); + } else { + if (!isFollowing) { + throw new Error('[[error:not-following]]'); + } + await Promise.all([ + db.sortedSetRemoveBulk([ + ['following:' + uid, theiruid], + ['followers:' + theiruid, uid], + ]), + User.decrementUserFieldBy(uid, 'followingCount', 1), + User.decrementUserFieldBy(theiruid, 'followerCount', 1), + ]); } - - async.waterfall([ - function (next) { - User.exists(theiruid, next); - }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:no-user]]')); - } - User.isFollowing(uid, theiruid, next); - }, - function (isFollowing, next) { - if (type === 'follow') { - if (isFollowing) { - return next(new Error('[[error:already-following]]')); - } - var now = Date.now(); - async.parallel([ - async.apply(db.sortedSetAddBulk, [ - ['following:' + uid, now, theiruid], - ['followers:' + theiruid, now, uid], - ]), - async.apply(User.incrementUserFieldBy, uid, 'followingCount', 1), - async.apply(User.incrementUserFieldBy, theiruid, 'followerCount', 1), - ], next); - } else { - if (!isFollowing) { - return next(new Error('[[error:not-following]]')); - } - async.parallel([ - async.apply(db.sortedSetRemove, 'following:' + uid, theiruid), - async.apply(db.sortedSetRemove, 'followers:' + theiruid, uid), - async.apply(User.decrementUserFieldBy, uid, 'followingCount', 1), - async.apply(User.decrementUserFieldBy, theiruid, 'followerCount', 1), - ], next); - } - }, - ], function (err) { - callback(err); - }); } - User.getFollowing = function (uid, start, stop, callback) { - getFollow(uid, 'following', start, stop, callback); + User.getFollowing = async function (uid, start, stop) { + return await getFollow(uid, 'following', start, stop); }; - User.getFollowers = function (uid, start, stop, callback) { - getFollow(uid, 'followers', start, stop, callback); + User.getFollowers = async function (uid, start, stop) { + return await getFollow(uid, 'followers', start, stop); }; - function getFollow(uid, type, start, stop, callback) { + async function getFollow(uid, type, start, stop) { if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, null, []); + return []; } - async.waterfall([ - function (next) { - db.getSortedSetRevRange(type + ':' + uid, start, stop, next); - }, - function (uids, next) { - plugins.fireHook('filter:user.' + type, { - uids: uids, - uid: uid, - start: start, - stop: stop, - }, next); - }, - function (data, next) { - User.getUsers(data.uids, uid, next); - }, - ], callback); + const uids = await db.getSortedSetRevRange(type + ':' + uid, start, stop); + const data = await plugins.fireHook('filter:user.' + type, { + uids: uids, + uid: uid, + start: start, + stop: stop, + }); + return await User.getUsers(data.uids, uid); } - User.isFollowing = function (uid, theirid, callback) { + User.isFollowing = async function (uid, theirid) { if (parseInt(uid, 10) <= 0 || parseInt(theirid, 10) <= 0) { - return setImmediate(callback, null, false); + return false; } - db.isSortedSetMember('following:' + uid, theirid, callback); + return await db.isSortedSetMember('following:' + uid, theirid); }; };