feat: #7743, user/digest, user/email, user/follow

v1.18.x
Barış Soner Uşaklı 6 years ago
parent 13aaf07bf5
commit c610eb1430

@ -14,182 +14,122 @@ var utils = require('../utils');
var Digest = module.exports; var Digest = module.exports;
Digest.execute = function (payload, callback) { Digest.execute = async function (payload) {
callback = callback || function () {}; const digestsDisabled = meta.config.disableEmailSubscriptions === 1;
var digestsDisabled = meta.config.disableEmailSubscriptions === 1;
if (digestsDisabled) { if (digestsDisabled) {
winston.info('[user/jobs] Did not send digests (' + payload.interval + ') because subscription system is disabled.'); 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) { Digest.getSubscribers = async function (interval) {
async.waterfall([ var subscribers = [];
function (next) {
var subs = [];
batch.processSortedSet('users:joindate', function (uids, next) { await batch.processSortedSet('users:joindate', async function (uids) {
async.waterfall([ const settings = await user.getMultipleUserSettings(uids);
function (next) { let subUids = [];
user.getMultipleUserSettings(uids, next); settings.forEach(function (hash) {
}, if (hash.dailyDigestFreq === interval) {
function (settings, next) { subUids.push(hash.uid);
const subUids = []; }
settings.forEach(function (hash) { });
if (hash.dailyDigestFreq === interval) { subUids = await user.bans.filterBanned(subUids);
subUids.push(hash.uid); subscribers = subscribers.concat(subUids);
} }, { interval: 1000 });
});
user.bans.filterBanned(subUids, next); const results = await plugins.fireHook('filter:digest.subscribers', {
}, interval: interval,
function (uids, next) { subscribers: subscribers,
subs = subs.concat(uids); });
next(); return results.subscribers;
},
], 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);
}; };
Digest.send = function (data, callback) { Digest.send = async function (data) {
var emailsSent = 0; var emailsSent = 0;
if (!data || !data.subscribers || !data.subscribers.length) { if (!data || !data.subscribers || !data.subscribers.length) {
return callback(null, emailsSent); return emailsSent;
} }
var now = new Date(); const now = new Date();
async.waterfall([ const users = await user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline']);
function (next) {
user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline'], next); async.eachLimit(users, 100, async function (userObj) {
}, let [notifications, topics] = await Promise.all([
function (users, next) { user.notifications.getDailyUnread(userObj.uid),
async.eachLimit(users, 100, function (userObj, next) { getTermTopics(data.interval, userObj.uid, 0, 9),
async.waterfall([ ]);
function (next) { notifications = notifications.filter(Boolean);
async.parallel({ // If there are no notifications and no new topics, don't bother sending a digest
notifications: async.apply(user.notifications.getDailyUnread, userObj.uid), if (!notifications.length && !topics.length) {
topics: async.apply(getTermTopics, data.interval, userObj.uid, 0, 9), return;
}, 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;
}
});
// Fix relative paths in topic data notifications.forEach(function (notification) {
data.topics = data.topics.map(function (topicObj) { if (notification.image && !notification.image.startsWith('http')) {
var user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user; notification.image = nconf.get('url') + notification.image;
if (user && user.picture && utils.isRelativeUrl(user.picture)) { }
user.picture = nconf.get('base_url') + user.picture; });
}
return topicObj; // Fix relative paths in topic data
}); topics = topics.map(function (topicObj) {
emailsSent += 1; const user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user;
emailer.send('digest', userObj.uid, { if (user && user.picture && utils.isRelativeUrl(user.picture)) {
subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]', user.picture = nconf.get('base_url') + user.picture;
username: userObj.username, }
userslug: userObj.userslug, return topicObj;
notifications: notifications, });
recent: data.topics, emailsSent += 1;
interval: data.interval, emailer.send('digest', userObj.uid, {
showUnsubscribe: true, subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]',
}, function (err) { username: userObj.username,
if (err) { userslug: userObj.userslug,
winston.error('[user/jobs] Could not send digest email', err); notifications: notifications,
} recent: data.topics,
}); interval: data.interval,
next(); showUnsubscribe: true,
}, }, function (err) {
], next); if (err) {
}, next); winston.error('[user/jobs] Could not send digest email', err);
}, }
], function (err) { });
callback(err, emailsSent);
}); });
return emailsSent;
};
function getTermTopics(term, uid, start, stop, callback) { async function getTermTopics(term, uid, start, stop) {
const options = { const options = {
uid: uid, uid: uid,
start: start, start: start,
stop: stop, stop: stop,
term: term, term: term,
sort: 'posts', sort: 'posts',
teaserPost: 'last-post', teaserPost: 'last-post',
}; };
let data = await topics.getSortedTopics(options);
async.waterfall([ if (!data.topics.length) {
function (next) { data = await topics.getLatestTopics(options);
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);
} }
}; 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;
}

@ -1,7 +1,6 @@
'use strict'; 'use strict';
var async = require('async');
var nconf = require('nconf'); var nconf = require('nconf');
var user = require('../user'); var user = require('../user');
@ -13,30 +12,24 @@ var emailer = require('../emailer');
var UserEmail = module.exports; var UserEmail = module.exports;
UserEmail.exists = function (email, callback) { UserEmail.exists = async function (email) {
user.getUidByEmail(email.toLowerCase(), function (err, exists) { const uid = await user.getUidByEmail(email.toLowerCase());
callback(err, !!exists); return !!uid;
});
}; };
UserEmail.available = function (email, callback) { UserEmail.available = async function (email) {
db.isSortedSetMember('email:uid', email.toLowerCase(), function (err, exists) { const exists = await db.isSortedSetMember('email:uid', email.toLowerCase());
callback(err, !exists); return !exists;
});
}; };
UserEmail.sendValidationEmail = function (uid, options, callback) { UserEmail.sendValidationEmail = async function (uid, options) {
/* /*
* Options: * Options:
* - email, overrides email retrieval * - email, overrides email retrieval
* - force, sends email even if it is too soon to send another * - force, sends email even if it is too soon to send another
*/ */
// Handling for 2 arguments options = options || {};
if (arguments.length === 2 && typeof options === 'function') {
callback = options;
options = {};
}
// Fallback behaviour (email passed in as second argument) // Fallback behaviour (email passed in as second argument)
if (typeof options === 'string') { if (typeof options === 'string') {
@ -45,117 +38,66 @@ UserEmail.sendValidationEmail = function (uid, options, callback) {
}; };
} }
callback = callback || function () {}; let confirm_code = utils.generateUUID();
var confirm_code = utils.generateUUID(); const confirm_link = nconf.get('url') + '/confirm/' + confirm_code;
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);
};
UserEmail.confirm = function (code, callback) { const emailInterval = meta.config.emailConfirmInterval;
var confirmObj;
async.waterfall([ // If no email passed in (default), retrieve email from uid
function (next) { if (!options.email || !options.email.length) {
db.getObject('confirm:' + code, next); options.email = await user.getUserField(uid, 'email');
}, }
function (_confirmObj, next) { if (!options.email) {
confirmObj = _confirmObj; return;
if (!confirmObj || !confirmObj.uid || !confirmObj.email) { }
return next(new Error('[[error:invalid-data]]')); let sent = false;
} if (!options.force) {
sent = await db.get('uid:' + uid + ':confirm:email:sent');
user.getUserField(confirmObj.uid, 'email', next); }
}, if (sent) {
function (currentEmail, next) { throw new Error('[[error:confirm-email-already-sent, ' + emailInterval + ']]');
if (!currentEmail || currentEmail.toLowerCase() !== confirmObj.email) { }
return next(new Error('[[error:invalid-email]]')); 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);
async.series([
async.apply(user.setUserField, confirmObj.uid, 'email:confirmed', 1), await db.setObject('confirm:' + confirm_code, {
async.apply(db.delete, 'confirm:' + code), email: options.email.toLowerCase(),
async.apply(db.delete, 'uid:' + confirmObj.uid + ':confirm:email:sent'), uid: uid,
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);
}); });
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 });
}; };

@ -1,103 +1,85 @@
'use strict'; 'use strict';
var async = require('async');
var plugins = require('../plugins'); var plugins = require('../plugins');
var db = require('../database'); var db = require('../database');
module.exports = function (User) { module.exports = function (User) {
User.follow = function (uid, followuid, callback) { User.follow = async function (uid, followuid) {
toggleFollow('follow', uid, followuid, callback); await toggleFollow('follow', uid, followuid);
}; };
User.unfollow = function (uid, unfollowuid, callback) { User.unfollow = async function (uid, unfollowuid) {
toggleFollow('unfollow', uid, unfollowuid, callback); 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) { 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)) { 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) { User.getFollowing = async function (uid, start, stop) {
getFollow(uid, 'following', start, stop, callback); return await getFollow(uid, 'following', start, stop);
}; };
User.getFollowers = function (uid, start, stop, callback) { User.getFollowers = async function (uid, start, stop) {
getFollow(uid, 'followers', start, stop, callback); 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) { if (parseInt(uid, 10) <= 0) {
return setImmediate(callback, null, []); return [];
} }
async.waterfall([ const uids = await db.getSortedSetRevRange(type + ':' + uid, start, stop);
function (next) { const data = await plugins.fireHook('filter:user.' + type, {
db.getSortedSetRevRange(type + ':' + uid, start, stop, next); uids: uids,
}, uid: uid,
function (uids, next) { start: start,
plugins.fireHook('filter:user.' + type, { stop: stop,
uids: uids, });
uid: uid, return await User.getUsers(data.uids, uid);
start: start,
stop: stop,
}, next);
},
function (data, next) {
User.getUsers(data.uids, uid, next);
},
], callback);
} }
User.isFollowing = function (uid, theirid, callback) { User.isFollowing = async function (uid, theirid) {
if (parseInt(uid, 10) <= 0 || parseInt(theirid, 10) <= 0) { 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);
}; };
}; };

Loading…
Cancel
Save