|
|
|
@ -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,68 +71,29 @@ 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) {
|
|
|
|
|
|
|
|
|
|
await removeFromSortedSets(uid);
|
|
|
|
|
const userData = await db.getObject('user:' + uid);
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
throw new Error('[[error:no-user]]');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
function (next) {
|
|
|
|
|
db.decrObjectField('global', 'userCount', next);
|
|
|
|
|
},
|
|
|
|
|
function (next) {
|
|
|
|
|
var keys = [
|
|
|
|
|
|
|
|
|
|
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',
|
|
|
|
@ -166,139 +110,87 @@ module.exports = function (User) {
|
|
|
|
|
'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) {
|
|
|
|
|
|
|
|
|
|
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]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
callback(err, userData);
|
|
|
|
|
});
|
|
|
|
|
return userData;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
async.eachSeries(pids, function (pid, next) {
|
|
|
|
|
posts.unvote(pid, uid, next);
|
|
|
|
|
}, next);
|
|
|
|
|
},
|
|
|
|
|
], function (err) {
|
|
|
|
|
callback(err);
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
db.setObjectField('user:' + uid, fieldName, count, next);
|
|
|
|
|
await db.setObjectField('user:' + uid, fieldName, count);
|
|
|
|
|
});
|
|
|
|
|
}, next);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (err) {
|
|
|
|
|
return callback(err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var followingSets = results.followers.map(function (uid) {
|
|
|
|
|
return 'following:' + uid;
|
|
|
|
|
});
|
|
|
|
|
const followingSets = followers.map(uid => 'following:' + uid);
|
|
|
|
|
const followerSets = following.map(uid => 'followers:' + uid);
|
|
|
|
|
|
|
|
|
|
var followerSets = results.following.map(function (uid) {
|
|
|
|
|
return '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'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|