From d6e36c3166731f6001f24b6456185dc8e4dbb6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 14 Jul 2019 00:22:17 -0400 Subject: [PATCH] feat: #7743, user/create, user/data, user/delete --- src/user/create.js | 318 ++++++++++++++++----------------------- src/user/data.js | 178 +++++++++------------- src/user/delete.js | 368 ++++++++++++++++----------------------------- 3 files changed, 331 insertions(+), 533 deletions(-) diff --git a/src/user/create.js b/src/user/create.js index 00fbddaff1..658d909f4e 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -1,6 +1,5 @@ 'use strict'; -var async = require('async'); var zxcvbn = require('zxcvbn'); var db = require('../database'); var utils = require('../utils'); @@ -10,215 +9,162 @@ var meta = require('../meta'); module.exports = function (User) { - User.create = function (data, callback) { + User.create = async function (data) { data.username = data.username.trim(); data.userslug = utils.slugify(data.username); if (data.email !== undefined) { data.email = String(data.email).trim(); } - var timestamp = data.timestamp || Date.now(); - var userData; - var userNameChanged = false; - - async.waterfall([ - function (next) { - User.isDataValid(data, next); - }, - function (next) { - userData = { - username: data.username, - userslug: data.userslug, - email: data.email || '', - joindate: timestamp, - lastonline: timestamp, - picture: data.picture || '', - fullname: data.fullname || '', - location: data.location || '', - birthday: data.birthday || '', - website: '', - signature: '', - uploadedpicture: '', - profileviews: 0, - reputation: 0, - postcount: 0, - topiccount: 0, - lastposttime: 0, - banned: 0, - status: 'online', - gdpr_consent: data.gdpr_consent === true ? 1 : 0, - acceptTos: data.acceptTos === true ? 1 : 0, - }; - - User.uniqueUsername(userData, next); - }, - function (renamedUsername, next) { - userNameChanged = !!renamedUsername; - - if (userNameChanged) { - userData.username = renamedUsername; - userData.userslug = utils.slugify(renamedUsername); - } - plugins.fireHook('filter:user.create', { user: userData, data: data }, next); - }, - function (results, next) { - userData = results.user; - db.incrObjectField('global', 'nextUid', next); - }, - function (uid, next) { - userData.uid = uid; - db.setObject('user:' + uid, userData, next); - }, - function (next) { - async.parallel([ - function (next) { - db.incrObjectField('global', 'userCount', next); - }, - function (next) { - const bulk = [ - ['username:uid', userData.uid, userData.username], - ['user:' + userData.uid + ':usernames', timestamp, userData.username], - ['username:sorted', 0, userData.username.toLowerCase() + ':' + userData.uid], - ['userslug:uid', userData.uid, userData.userslug], - ['users:joindate', timestamp, userData.uid], - ['users:online', timestamp, userData.uid], - ['users:postcount', 0, userData.uid], - ['users:reputation', 0, userData.uid], - ]; - - if (parseInt(userData.uid, 10) !== 1) { - bulk.push(['users:notvalidated', timestamp, userData.uid]); - } - if (userData.email) { - bulk.push(['email:uid', userData.uid, userData.email.toLowerCase()]); - bulk.push(['email:sorted', 0, userData.email.toLowerCase() + ':' + userData.uid]); - bulk.push(['user:' + userData.uid + ':emails', timestamp, userData.email]); - } - db.sortedSetAddBulk(bulk, next); - }, - function (next) { - groups.join('registered-users', userData.uid, next); - }, - function (next) { - User.notifications.sendWelcomeNotification(userData.uid, next); - }, - function (next) { - if (userData.email && userData.uid > 1 && meta.config.requireEmailConfirmation) { - User.email.sendValidationEmail(userData.uid, { - email: userData.email, - }); - } - - if (!data.password) { - return next(); - } - - User.hashPassword(data.password, function (err, hash) { - if (err) { - return next(err); - } - - async.parallel([ - async.apply(User.setUserField, userData.uid, 'password', hash), - async.apply(User.reset.updateExpiry, userData.uid), - ], next); - }); - }, - function (next) { - User.updateDigestSetting(userData.uid, meta.config.dailyDigestFreq, next); - }, - ], next); - }, - function (results, next) { - if (userNameChanged) { - User.notifications.sendNameChangeNotification(userData.uid, userData.username); - } - plugins.fireHook('action:user.create', { user: userData }); - next(null, userData.uid); - }, - ], callback); - }; + const timestamp = data.timestamp || Date.now(); + + await User.isDataValid(data); + + let userData = { + username: data.username, + userslug: data.userslug, + email: data.email || '', + joindate: timestamp, + lastonline: timestamp, + picture: data.picture || '', + fullname: data.fullname || '', + location: data.location || '', + birthday: data.birthday || '', + website: '', + signature: '', + uploadedpicture: '', + profileviews: 0, + reputation: 0, + postcount: 0, + topiccount: 0, + lastposttime: 0, + banned: 0, + status: 'online', + gdpr_consent: data.gdpr_consent === true ? 1 : 0, + acceptTos: data.acceptTos === true ? 1 : 0, + }; + const renamedUsername = await User.uniqueUsername(userData); + const userNameChanged = !!renamedUsername; + if (userNameChanged) { + userData.username = renamedUsername; + userData.userslug = utils.slugify(renamedUsername); + } + + const results = await plugins.fireHook('filter:user.create', { user: userData, data: data }); + userData = results.user; + + const uid = await db.incrObjectField('global', 'nextUid'); + userData.uid = uid; + + await db.setObject('user:' + uid, userData); + + const bulkAdd = [ + ['username:uid', userData.uid, userData.username], + ['user:' + userData.uid + ':usernames', timestamp, userData.username], + ['username:sorted', 0, userData.username.toLowerCase() + ':' + userData.uid], + ['userslug:uid', userData.uid, userData.userslug], + ['users:joindate', timestamp, userData.uid], + ['users:online', timestamp, userData.uid], + ['users:postcount', 0, userData.uid], + ['users:reputation', 0, userData.uid], + ]; - User.isDataValid = function (userData, callback) { - async.parallel({ - emailValid: function (next) { - if (userData.email) { - next(!utils.isEmailValid(userData.email) ? new Error('[[error:invalid-email]]') : null); - } else { - next(); - } - }, - userNameValid: function (next) { - next((!utils.isUserNameValid(userData.username) || !userData.userslug) ? new Error('[[error:invalid-username, ' + userData.username + ']]') : null); - }, - passwordValid: function (next) { - if (userData.password) { - User.isPasswordValid(userData.password, next); - } else { - next(); - } - }, - emailAvailable: function (next) { - if (userData.email) { - User.email.available(userData.email, function (err, available) { - if (err) { - return next(err); - } - next(!available ? new Error('[[error:email-taken]]') : null); - }); - } else { - next(); - } - }, - }, function (err) { - callback(err); - }); + if (parseInt(userData.uid, 10) !== 1) { + bulkAdd.push(['users:notvalidated', timestamp, userData.uid]); + } + if (userData.email) { + bulkAdd.push(['email:uid', userData.uid, userData.email.toLowerCase()]); + bulkAdd.push(['email:sorted', 0, userData.email.toLowerCase() + ':' + userData.uid]); + bulkAdd.push(['user:' + userData.uid + ':emails', timestamp, userData.email]); + } + + await Promise.all([ + db.incrObjectField('global', 'userCount'), + db.sortedSetAddBulk(bulkAdd), + groups.join('registered-users', userData.uid), + User.notifications.sendWelcomeNotification(userData.uid), + storePassword(userData.uid, data.password), + User.updateDigestSetting(userData.uid, meta.config.dailyDigestFreq), + ]); + + if (userData.email && userData.uid > 1 && meta.config.requireEmailConfirmation) { + User.email.sendValidationEmail(userData.uid, { + email: userData.email, + }); + } + if (userNameChanged) { + await User.notifications.sendNameChangeNotification(userData.uid, userData.username); + } + plugins.fireHook('action:user.create', { user: userData, data: data }); + return userData.uid; }; - User.isPasswordValid = function (password, minStrength, callback) { - if (typeof minStrength === 'function' && !callback) { - callback = minStrength; - minStrength = meta.config.minimumPasswordStrength; + async function storePassword(uid, password) { + if (!password) { + return; + } + const hash = await User.hashPassword(password); + await Promise.all([ + User.setUserField(uid, 'password', hash), + User.reset.updateExpiry(uid), + ]); + } + + User.isDataValid = async function (userData) { + if (userData.email && !utils.isEmailValid(userData.email)) { + throw new Error('[[error:invalid-email]]'); + } + + if (!utils.isUserNameValid(userData.username) || !userData.userslug) { + throw new Error('[[error:invalid-username, ' + userData.username + ']]'); } + if (userData.password) { + await User.isPasswordValid(userData.password); + } + + if (userData.email) { + const available = await User.email.available(userData.email); + if (!available) { + throw new Error('[[error:email-taken]]'); + } + } + }; + + // this function doesnt need to be async, but there is exising code that uses it + // with a callback so it is marked async otherwise it breaks the callback code + User.isPasswordValid = async function (password, minStrength) { + minStrength = minStrength || meta.config.minimumPasswordStrength; + // Sanity checks: Checks if defined and is string if (!password || !utils.isPasswordValid(password)) { - return callback(new Error('[[error:invalid-password]]')); + throw new Error('[[error:invalid-password]]'); } if (password.length < meta.config.minimumPasswordLength) { - return callback(new Error('[[reset_password:password_too_short]]')); + throw new Error('[[reset_password:password_too_short]]'); } if (password.length > 512) { - return callback(new Error('[[error:password-too-long]]')); + throw new Error('[[error:password-too-long]]'); } var strength = zxcvbn(password); if (strength.score < minStrength) { - return callback(new Error('[[user:weak_password]]')); + throw new Error('[[user:weak_password]]'); } - - callback(); }; - User.uniqueUsername = function (userData, callback) { - var numTries = 0; - function go(username) { - async.waterfall([ - function (next) { - meta.userOrGroupExists(username, next); - }, - function (exists) { - if (!exists) { - return callback(null, numTries ? username : null); - } - username = userData.username + ' ' + numTries.toString(32); - numTries += 1; - go(username); - }, - ], callback); - } - - go(userData.userslug); + User.uniqueUsername = async function (userData) { + let numTries = 0; + let username = userData.username; + while (true) { + /* eslint-disable no-await-in-loop */ + const exists = await meta.userOrGroupExists(username); + if (!exists) { + return numTries ? username : null; + } + username = userData.username + ' ' + numTries.toString(32); + numTries += 1; + } }; }; diff --git a/src/user/data.js b/src/user/data.js index 571482be0b..e242685ce7 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -46,14 +46,33 @@ module.exports = function (User) { 'email:confirmed': 0, }; - User.getUsersFields = function (uids, fields, callback) { + User.getUsersFields = async function (uids, fields) { if (!Array.isArray(uids) || !uids.length) { - return setImmediate(callback, null, []); + return []; } uids = uids.map(uid => (isNaN(uid) ? 0 : parseInt(uid, 10))); - var fieldsToRemove = []; + const fieldsToRemove = []; + ensureRequiredFields(fields, fieldsToRemove); + + const uniqueUids = _.uniq(uids).filter(uid => uid > 0); + + const results = await plugins.fireHook('filter:user.whitelistFields', { uids: uids, whitelist: fieldWhitelist.slice() }); + if (!fields.length) { + fields = results.whitelist; + } else { + // Never allow password retrieval via this method + fields = fields.filter(value => value !== 'password'); + } + + let users = await db.getObjectsFields(uniqueUids.map(uid => 'user:' + uid), fields); + users = uidsToUsers(uids, uniqueUids, users); + + return await modifyUserData(users, fields, fieldsToRemove); + }; + + function ensureRequiredFields(fields, fieldsToRemove) { function addField(field) { if (!fields.includes(field)) { fields.push(field); @@ -76,60 +95,11 @@ module.exports = function (User) { if (fields.includes('banned') && !fields.includes('banned:expire')) { addField('banned:expire'); } - - var uniqueUids = _.uniq(uids).filter(uid => uid > 0); - - async.waterfall([ - function (next) { - plugins.fireHook('filter:user.whitelistFields', { uids: uids, whitelist: fieldWhitelist.slice() }, next); - }, - function (results, next) { - if (!fields.length) { - fields = results.whitelist; - } else { - // Never allow password retrieval via this method - fields = fields.filter(value => value !== 'password'); - } - - db.getObjectsFields(uidsToUserKeys(uniqueUids), fields, next); - }, - function (users, next) { - users = uidsToUsers(uids, uniqueUids, users); - - modifyUserData(users, fields, fieldsToRemove, next); - }, - ], callback); - }; - - - User.getUserField = function (uid, field, callback) { - User.getUserFields(uid, [field], function (err, user) { - callback(err, user ? user[field] : null); - }); - }; - - User.getUserFields = function (uid, fields, callback) { - User.getUsersFields([uid], fields, function (err, users) { - callback(err, users ? users[0] : null); - }); - }; - - User.getUserData = function (uid, callback) { - User.getUsersData([uid], function (err, users) { - callback(err, users ? users[0] : null); - }); - }; - - User.getUsersData = function (uids, callback) { - User.getUsersFields(uids, [], callback); - }; + } function uidsToUsers(uids, uniqueUids, usersData) { - var uidToUser = uniqueUids.reduce(function (memo, uid, idx) { - memo[uid] = usersData[idx]; - return memo; - }, {}); - var users = uids.map(function (uid) { + const uidToUser = _.zipObject(uniqueUids, usersData); + const users = uids.map(function (uid) { const returnPayload = uidToUser[uid] || _.clone(User.guestData); if (uid > 0 && !returnPayload.uid) { returnPayload.oldUid = parseInt(uid, 10); @@ -140,14 +110,29 @@ module.exports = function (User) { return users; } - function uidsToUserKeys(uids) { - return uids.map(uid => 'user:' + uid); - } + User.getUserField = async function (uid, field) { + const user = await User.getUserFields(uid, [field]); + return user ? user[field] : null; + }; + + User.getUserFields = async function (uid, fields) { + const users = await User.getUsersFields([uid], fields); + return users ? users[0] : null; + }; + + User.getUserData = async function (uid) { + const users = await User.getUsersData([uid]); + return users ? users[0] : null; + }; + + User.getUsersData = async function (uids) { + return await User.getUsersFields(uids, []); + }; - function modifyUserData(users, requestedFields, fieldsToRemove, callback) { - async.map(users, function (user, next) { + async function modifyUserData(users, requestedFields, fieldsToRemove) { + users = await async.map(users, async function (user) { if (!user) { - return next(null, user); + return user; } db.parseIntFields(user, intFields, requestedFields); @@ -209,27 +194,18 @@ module.exports = function (User) { } if (user.hasOwnProperty('banned') || user.hasOwnProperty('banned:expire')) { - var result = User.bans.calcExpiredFromUserData(user); - var unban = result.banned && result.banExpired; + const result = User.bans.calcExpiredFromUserData(user); + const unban = result.banned && result.banExpired; user.banned_until = unban ? 0 : user['banned:expire']; user.banned_until_readable = user.banned_until && !unban ? utils.toISOString(user.banned_until) : 'Not Banned'; if (unban) { - return User.bans.unban(user.uid, function (err) { - if (err) { - return next(err); - } - user.banned = false; - next(null, user); - }); + await User.bans.unban(user.uid); + user.banned = false; } } - next(null, user); - }, function (err, users) { - if (err) { - return callback(err); - } - plugins.fireHook('filter:users.get', users, callback); + return user; }); + return await plugins.fireHook('filter:users.get', users); } function parseGroupTitle(user) { @@ -261,46 +237,30 @@ module.exports = function (User) { return meta.config.defaultAvatar.startsWith('http') ? meta.config.defaultAvatar : nconf.get('relative_path') + meta.config.defaultAvatar; }; - User.setUserField = function (uid, field, value, callback) { - User.setUserFields(uid, { [field]: value }, callback); + User.setUserField = async function (uid, field, value) { + await User.setUserFields(uid, { [field]: value }); }; - User.setUserFields = function (uid, data, callback) { - callback = callback || function () {}; - async.waterfall([ - function (next) { - db.setObject('user:' + uid, data, next); - }, - function (next) { - for (var field in data) { - if (data.hasOwnProperty(field)) { - plugins.fireHook('action:user.set', { uid: uid, field: field, value: data[field], type: 'set' }); - } - } - next(); - }, - ], callback); + User.setUserFields = async function (uid, data) { + await db.setObject('user:' + uid, data); + for (var field in data) { + if (data.hasOwnProperty(field)) { + plugins.fireHook('action:user.set', { uid: uid, field: field, value: data[field], type: 'set' }); + } + } }; - User.incrementUserFieldBy = function (uid, field, value, callback) { - incrDecrUserFieldBy(uid, field, value, 'increment', callback); + User.incrementUserFieldBy = async function (uid, field, value) { + return await incrDecrUserFieldBy(uid, field, value, 'increment'); }; - User.decrementUserFieldBy = function (uid, field, value, callback) { - incrDecrUserFieldBy(uid, field, -value, 'decrement', callback); + User.decrementUserFieldBy = async function (uid, field, value) { + return await incrDecrUserFieldBy(uid, field, -value, 'decrement'); }; - function incrDecrUserFieldBy(uid, field, value, type, callback) { - callback = callback || function () {}; - async.waterfall([ - function (next) { - db.incrObjectFieldBy('user:' + uid, field, value, next); - }, - function (value, next) { - plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: type }); - - next(null, value); - }, - ], callback); + async function incrDecrUserFieldBy(uid, field, value, type) { + const newValue = await db.incrObjectFieldBy('user:' + uid, field, value); + plugins.fireHook('action:user.set', { uid: uid, field: field, value: newValue, type: type }); + return newValue; } }; diff --git a/src/user/delete.js b/src/user/delete.js index 27ae2b8bb4..586e170bf3 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -17,66 +17,49 @@ var file = require('../file'); module.exports = function (User) { var deletesInProgress = {}; - User.delete = function (callerUid, uid, callback) { + User.delete = async function (callerUid, uid) { if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, new Error('[[error:invalid-uid]]')); + throw new Error('[[error:invalid-uid]]'); } if (deletesInProgress[uid]) { - return setImmediate(callback, new Error('[[error:already-deleting]]')); + throw new Error('[[error:already-deleting]]'); } deletesInProgress[uid] = 'user.delete'; - async.waterfall([ - function (next) { - removeFromSortedSets(uid, next); - }, - function (next) { - deletePosts(callerUid, uid, next); - }, - function (next) { - deleteTopics(callerUid, uid, next); - }, - function (next) { - deleteUploads(uid, next); - }, - function (next) { - User.deleteAccount(uid, next); - }, - ], callback); + await removeFromSortedSets(uid); + await deletePosts(callerUid, uid); + await deleteTopics(callerUid, uid); + await deleteUploads(uid); + const userData = await User.deleteAccount(uid); + return userData; }; - function deletePosts(callerUid, uid, callback) { - batch.processSortedSet('uid:' + uid + ':posts', function (ids, next) { - async.eachSeries(ids, function (pid, next) { - posts.purge(pid, callerUid, next); - }, next); - }, { alwaysStartAt: 0 }, callback); + async function deletePosts(callerUid, uid) { + await batch.processSortedSet('uid:' + uid + ':posts', async function (ids) { + await async.eachSeries(ids, async function (pid) { + await posts.purge(pid, callerUid); + }); + }, { alwaysStartAt: 0 }); } - function deleteTopics(callerUid, uid, callback) { - batch.processSortedSet('uid:' + uid + ':topics', function (ids, next) { - async.eachSeries(ids, function (tid, next) { - topics.purge(tid, callerUid, next); - }, next); - }, { alwaysStartAt: 0 }, callback); + async function deleteTopics(callerUid, uid) { + await batch.processSortedSet('uid:' + uid + ':topics', async function (ids) { + await async.eachSeries(ids, async function (tid) { + await topics.purge(tid, callerUid); + }); + }, { alwaysStartAt: 0 }); } - function deleteUploads(uid, callback) { - batch.processSortedSet('uid:' + uid + ':uploads', function (uploadNames, next) { - async.waterfall([ - function (next) { - async.each(uploadNames, function (uploadName, next) { - file.delete(path.join(nconf.get('upload_path'), uploadName), next); - }, next); - }, - function (next) { - db.sortedSetRemove('uid:' + uid + ':uploads', uploadNames, next); - }, - ], next); - }, { alwaysStartAt: 0 }, callback); + async function deleteUploads(uid) { + await batch.processSortedSet('uid:' + uid + ':uploads', async function (uploadNames) { + await async.each(uploadNames, async function (uploadName) { + await file.delete(path.join(nconf.get('upload_path'), uploadName)); + }); + await db.sortedSetRemove('uid:' + uid + ':uploads', uploadNames); + }, { alwaysStartAt: 0 }); } - function removeFromSortedSets(uid, callback) { - db.sortedSetsRemove([ + async function removeFromSortedSets(uid) { + await db.sortedSetsRemove([ 'users:joindate', 'users:postcount', 'users:reputation', @@ -88,217 +71,126 @@ module.exports = function (User) { 'digest:day:uids', 'digest:week:uids', 'digest:month:uids', - ], uid, callback); + ], uid); } - User.deleteAccount = function (uid, callback) { + User.deleteAccount = async function (uid) { if (deletesInProgress[uid] === 'user.deleteAccount') { - return setImmediate(callback, new Error('[[error:already-deleting]]')); + throw new Error('[[error:already-deleting]]'); } deletesInProgress[uid] = 'user.deleteAccount'; - var userData; - async.waterfall([ - function (next) { - removeFromSortedSets(uid, next); - }, - function (next) { - db.getObject('user:' + uid, next); - }, - function (_userData, next) { - if (!_userData || !_userData.username) { - delete deletesInProgress[uid]; - return callback(new Error('[[error:no-user]]')); - } - userData = _userData; - plugins.fireHook('static:user.delete', { uid: uid }, next); - }, - function (next) { - deleteVotes(uid, next); - }, - function (next) { - deleteChats(uid, next); - }, - function (next) { - User.auth.revokeAllSessions(uid, next); - }, - function (next) { - async.parallel([ - function (next) { - db.sortedSetRemove('username:uid', userData.username, next); - }, - function (next) { - db.sortedSetRemove('username:sorted', userData.username.toLowerCase() + ':' + uid, next); - }, - function (next) { - db.sortedSetRemove('userslug:uid', userData.userslug, next); - }, - function (next) { - db.sortedSetRemove('fullname:uid', userData.fullname, next); - }, - function (next) { - if (userData.email) { - async.parallel([ - async.apply(db.sortedSetRemove, 'email:uid', userData.email.toLowerCase()), - async.apply(db.sortedSetRemove, 'email:sorted', userData.email.toLowerCase() + ':' + uid), - ], next); - } else { - next(); - } - }, - function (next) { - db.decrObjectField('global', 'userCount', next); - }, - function (next) { - var keys = [ - 'uid:' + uid + ':notifications:read', - 'uid:' + uid + ':notifications:unread', - 'uid:' + uid + ':bookmarks', - 'uid:' + uid + ':followed_tids', - 'uid:' + uid + ':ignored_tids', - 'user:' + uid + ':settings', - 'user:' + uid + ':usernames', - 'user:' + uid + ':emails', - 'uid:' + uid + ':topics', 'uid:' + uid + ':posts', - 'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread', - 'uid:' + uid + ':chat:rooms', 'uid:' + uid + ':chat:rooms:unread', - 'uid:' + uid + ':upvote', 'uid:' + uid + ':downvote', - 'uid:' + uid + ':flag:pids', - 'uid:' + uid + ':sessions', 'uid:' + uid + ':sessionUUID:sessionId', - 'invitation:uid:' + uid, - ]; - db.deleteAll(keys, next); - }, - function (next) { - db.setRemove('invitation:uids', uid, next); - }, - function (next) { - deleteUserIps(uid, next); - }, - function (next) { - deleteBans(uid, next); - }, - function (next) { - deleteUserFromFollowers(uid, next); - }, - function (next) { - groups.leaveAllGroups(uid, next); - }, - ], next); - }, - function (results, next) { - db.deleteAll(['followers:' + uid, 'following:' + uid, 'user:' + uid], next); - }, - ], function (err) { + + await removeFromSortedSets(uid); + const userData = await db.getObject('user:' + uid); + + if (!userData || !userData.username) { delete deletesInProgress[uid]; - callback(err, userData); - }); - }; + throw new Error('[[error:no-user]]'); + } - function deleteVotes(uid, callback) { - async.waterfall([ - function (next) { - async.parallel({ - upvotedPids: async.apply(db.getSortedSetRange, 'uid:' + uid + ':upvote', 0, -1), - downvotedPids: async.apply(db.getSortedSetRange, 'uid:' + uid + ':downvote', 0, -1), - }, next); - }, - function (pids, next) { - pids = _.uniq(pids.upvotedPids.concat(pids.downvotedPids).filter(Boolean)); + await plugins.fireHook('static:user.delete', { uid: uid }); + await deleteVotes(uid); + await deleteChats(uid); + await User.auth.revokeAllSessions(uid); + + const keys = [ + 'uid:' + uid + ':notifications:read', + 'uid:' + uid + ':notifications:unread', + 'uid:' + uid + ':bookmarks', + 'uid:' + uid + ':followed_tids', + 'uid:' + uid + ':ignored_tids', + 'user:' + uid + ':settings', + 'user:' + uid + ':usernames', + 'user:' + uid + ':emails', + 'uid:' + uid + ':topics', 'uid:' + uid + ':posts', + 'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread', + 'uid:' + uid + ':chat:rooms', 'uid:' + uid + ':chat:rooms:unread', + 'uid:' + uid + ':upvote', 'uid:' + uid + ':downvote', + 'uid:' + uid + ':flag:pids', + 'uid:' + uid + ':sessions', 'uid:' + uid + ':sessionUUID:sessionId', + 'invitation:uid:' + uid, + ]; + + const bulkRemove = [ + ['username:uid', userData.username], + ['username:sorted', userData.username.toLowerCase() + ':' + uid], + ['userslug:uid', userData.userslug], + ['fullname:uid', userData.fullname], + ]; + if (userData.email) { + bulkRemove.push(['email:uid', userData.email.toLowerCase()]); + bulkRemove.push(['email:sorted', userData.email.toLowerCase() + ':' + uid]); + } - async.eachSeries(pids, function (pid, next) { - posts.unvote(pid, uid, next); - }, next); - }, - ], function (err) { - callback(err); + await Promise.all([ + db.sortedSetRemoveBulk(bulkRemove), + db.decrObjectField('global', 'userCount'), + db.deleteAll(keys), + db.setRemove('invitation:uids', uid), + deleteUserIps(uid), + deleteBans(uid), + deleteUserFromFollowers(uid), + groups.leaveAllGroups(uid), + ]); + await db.deleteAll(['followers:' + uid, 'following:' + uid, 'user:' + uid]); + delete deletesInProgress[uid]; + return userData; + }; + + async function deleteVotes(uid) { + const [upvotedPids, downvotedPids] = await Promise.all([ + db.getSortedSetRange('uid:' + uid + ':upvote', 0, -1), + db.getSortedSetRange('uid:' + uid + ':downvote', 0, -1), + ]); + const pids = _.uniq(upvotedPids.concat(downvotedPids).filter(Boolean)); + await async.eachSeries(pids, async function (pid) { + await posts.unvote(pid, uid); }); } - function deleteChats(uid, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRange('uid:' + uid + ':chat:rooms', 0, -1, next); - }, - function (roomIds, next) { - var userKeys = roomIds.map(function (roomId) { - return 'uid:' + uid + ':chat:room:' + roomId + ':mids'; - }); + async function deleteChats(uid) { + const roomIds = await db.getSortedSetRange('uid:' + uid + ':chat:rooms', 0, -1); + const userKeys = roomIds.map(roomId => 'uid:' + uid + ':chat:room:' + roomId + ':mids'); - async.parallel([ - async.apply(messaging.leaveRooms, uid, roomIds), - async.apply(db.deleteAll, userKeys), - ], next); - }, - ], function (err) { - callback(err); - }); + await Promise.all([ + messaging.leaveRooms(uid, roomIds), + db.deleteAll(userKeys), + ]); } - function deleteUserIps(uid, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRange('uid:' + uid + ':ip', 0, -1, next); - }, - function (ips, next) { - var keys = ips.map(function (ip) { - return 'ip:' + ip + ':uid'; - }); - db.sortedSetsRemove(keys, uid, next); - }, - function (next) { - db.delete('uid:' + uid + ':ip', next); - }, - ], callback); + async function deleteUserIps(uid) { + const ips = await db.getSortedSetRange('uid:' + uid + ':ip', 0, -1); + await db.sortedSetsRemove(ips.map(ip => 'ip:' + ip + ':uid'), uid); + await db.delete('uid:' + uid + ':ip'); } - function deleteBans(uid, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRange('uid:' + uid + ':bans:timestamp', 0, -1, next); - }, - function (bans, next) { - db.deleteAll(bans, next); - }, - function (next) { - db.delete('uid:' + uid + ':bans:timestamp', next); - }, - ], callback); + async function deleteBans(uid) { + const bans = await db.getSortedSetRange('uid:' + uid + ':bans:timestamp', 0, -1); + await db.deleteAll(bans); + await db.delete('uid:' + uid + ':bans:timestamp'); } - function deleteUserFromFollowers(uid, callback) { - async.parallel({ - followers: async.apply(db.getSortedSetRange, 'followers:' + uid, 0, -1), - following: async.apply(db.getSortedSetRange, 'following:' + uid, 0, -1), - }, function (err, results) { - function updateCount(uids, name, fieldName, next) { - async.each(uids, function (uid, next) { - db.sortedSetCard(name + uid, function (err, count) { - if (err) { - return next(err); - } - count = parseInt(count, 10) || 0; - db.setObjectField('user:' + uid, fieldName, count, next); - }); - }, next); - } - - if (err) { - return callback(err); - } - - var followingSets = results.followers.map(function (uid) { - return 'following:' + uid; + async function deleteUserFromFollowers(uid) { + const [followers, following] = await Promise.all([ + db.getSortedSetRange('followers:' + uid, 0, -1), + db.getSortedSetRange('following:' + uid, 0, -1), + ]); + + async function updateCount(uids, name, fieldName) { + await async.each(uids, async function (uid) { + let count = await db.sortedSetCard(name + uid); + count = parseInt(count, 10) || 0; + await db.setObjectField('user:' + uid, fieldName, count); }); + } - var followerSets = results.following.map(function (uid) { - return 'followers:' + uid; - }); + const followingSets = followers.map(uid => 'following:' + uid); + const followerSets = following.map(uid => 'followers:' + uid); - async.parallel([ - async.apply(db.sortedSetsRemove, followerSets.concat(followingSets), uid), - async.apply(updateCount, results.following, 'followers:', 'followerCount'), - async.apply(updateCount, results.followers, 'following:', 'followingCount'), - ], callback); - }); + await Promise.all([ + db.sortedSetsRemove(followerSets.concat(followingSets), uid), + updateCount(following, 'followers:', 'followerCount'), + updateCount(followers, 'following:', 'followingCount'), + ]); } };