diff --git a/src/posts/user.js b/src/posts/user.js index 49706ec59b..25e5183996 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -1,16 +1,16 @@ 'use strict'; -var async = require('async'); -var validator = require('validator'); -var _ = require('lodash'); +const async = require('async'); +const validator = require('validator'); +const _ = require('lodash'); const db = require('../database'); -var user = require('../user'); +const user = require('../user'); const topics = require('../topics'); -var groups = require('../groups'); -var meta = require('../meta'); -var plugins = require('../plugins'); -var privileges = require('../privileges'); +const groups = require('../groups'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const privileges = require('../privileges'); module.exports = function (Posts) { Posts.getUserInfoForPosts = async function (uids, uid) { @@ -158,7 +158,7 @@ module.exports = function (Posts) { db.sortedSetRemoveBulk(bulkRemove), db.sortedSetAddBulk(bulkAdd), user.incrementUserPostCountBy(toUid, pids.length), - updateReputation(toUid, repChange), + user.incrementUserReputationBy(toUid, repChange), handleMainPidOwnerChange(postData, toUid), reduceCounters(postsByUser), updateTopicPosters(postData, toUid), @@ -171,7 +171,7 @@ module.exports = function (Posts) { const repChange = posts.reduce((acc, val) => acc + val.votes, 0); await Promise.all([ user.incrementUserPostCountBy(uid, -posts.length), - updateReputation(uid, -repChange), + user.incrementUserReputationBy(uid, -repChange), ]); }); } @@ -187,14 +187,6 @@ module.exports = function (Posts) { }); } - async function updateReputation(uid, change) { - if (!change) { - return; - } - const newReputation = await user.incrementUserFieldBy(uid, 'reputation', change); - await db.sortedSetAdd('users:reputation', newReputation, uid); - } - async function handleMainPidOwnerChange(postData, toUid) { const tids = _.uniq(postData.map(p => p.tid)); const topicData = await topics.getTopicsFields(tids, ['mainPid', 'timestamp']); @@ -230,7 +222,10 @@ module.exports = function (Posts) { async function reduceTopicCounts(postsByUser) { await async.eachSeries(Object.keys(postsByUser), async function (uid) { const posts = postsByUser[uid]; - await user.incrementUserFieldBy(uid, 'topiccount', -posts.length); + const exists = await user.exists(uid); + if (exists) { + await user.incrementUserFieldBy(uid, 'topiccount', -posts.length); + } }); } }; diff --git a/src/posts/votes.js b/src/posts/votes.js index d19814d0d6..38f2346163 100644 --- a/src/posts/votes.js +++ b/src/posts/votes.js @@ -179,10 +179,7 @@ module.exports = function (Posts) { } const postData = await Posts.getPostFields(pid, ['pid', 'uid', 'tid']); - const newReputation = await user[type === 'upvote' ? 'incrementUserFieldBy' : 'decrementUserFieldBy'](postData.uid, 'reputation', 1); - if (parseInt(postData.uid, 10)) { - await db.sortedSetAdd('users:reputation', newReputation, postData.uid); - } + const newReputation = user.incrementUserReputationBy(postData.uid, type === 'upvote' ? 1 : -1); await adjustPostVotes(postData, uid, type, unvote); diff --git a/src/upgrades/1.13.3/fix_users_sorted_sets.js b/src/upgrades/1.13.3/fix_users_sorted_sets.js new file mode 100644 index 0000000000..8e4c992905 --- /dev/null +++ b/src/upgrades/1.13.3/fix_users_sorted_sets.js @@ -0,0 +1,58 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Fix user sorted sets', + timestamp: Date.UTC(2020, 4, 2), + method: async function (callback) { + const progress = this.progress; + const nextUid = await db.getObjectField('global', 'nextUid'); + const allUids = []; + for (let i = 1; i <= nextUid; i++) { + allUids.push(i); + } + + progress.total = nextUid; + let totalUserCount = 0; + + await db.delete('user:null'); + await db.sortedSetsRemove([ + 'users:joindate', + 'users:reputation', + 'users:postcount', + ], 'null'); + + await batch.processArray(allUids, async function (uids) { + progress.incr(uids.length); + const userData = await db.getObjects(uids.map(id => 'user:' + id)); + + await Promise.all(userData.map(async function (userData, index) { + if (!userData || !userData.uid) { + await db.sortedSetsRemove([ + 'users:joindate', + 'users:reputation', + 'users:postcount', + ], uids[index]); + if (userData && !userData.uid) { + await db.delete('user:' + uids[index]); + } + return; + } + totalUserCount += 1; + await db.sortedSetAddBulk([ + ['users:joindate', userData.joindate, uids[index]], + ['users:reputation', userData.reputation, uids[index]], + ['users:postcount', userData.postcount, uids[index]], + ]); + })); + }, { + progress: progress, + batch: 500, + }); + + await db.setObjectField('global', 'userCount', totalUserCount); + callback(); + }, +}; diff --git a/src/user/index.js b/src/user/index.js index 44dd7bc19a..8afc88bf55 100644 --- a/src/user/index.js +++ b/src/user/index.js @@ -41,7 +41,7 @@ require('./blocks')(User); require('./uploads')(User); User.exists = async function (uid) { - return await db.exists('user:' + uid); + return await db.isSortedSetMember('users:joindate', uid); }; User.existsBySlug = async function (userslug) { diff --git a/src/user/posts.js b/src/user/posts.js index b444c8a41d..95c96e51d4 100644 --- a/src/user/posts.js +++ b/src/user/posts.js @@ -67,12 +67,26 @@ module.exports = function (User) { }; User.incrementUserPostCountBy = async function (uid, value) { - const newpostcount = await User.incrementUserFieldBy(uid, 'postcount', value); - if (parseInt(uid, 10) <= 0) { + return await incrementUserFieldAndSetBy(uid, 'postcount', 'users:postcount', value); + }; + + User.incrementUserReputationBy = async function (uid, value) { + return await incrementUserFieldAndSetBy(uid, 'reputation', 'users:reputation', value); + }; + + async function incrementUserFieldAndSetBy(uid, field, set, value) { + value = parseInt(value, 10); + if (!value || !field || !(parseInt(uid, 10) > 0)) { return; } - await db.sortedSetAdd('users:postcount', newpostcount, uid); - }; + const exists = await User.exists(uid); + if (!exists) { + return; + } + const newValue = await User.incrementUserFieldBy(uid, field, value); + await db.sortedSetAdd(set, newValue, uid); + return newValue; + } User.getPostIds = async function (uid, start, stop) { const pids = await db.getSortedSetRevRange('uid:' + uid + ':posts', start, stop); diff --git a/test/user.js b/test/user.js index 046c6cd7ac..ca4ce837a2 100644 --- a/test/user.js +++ b/test/user.js @@ -11,6 +11,7 @@ var db = require('./mocks/databasemock'); var User = require('../src/user'); var Topics = require('../src/topics'); var Categories = require('../src/categories'); +var Posts = require('../src/posts'); var Password = require('../src/password'); var groups = require('../src/groups'); var helpers = require('./helpers'); @@ -412,6 +413,40 @@ describe('User', function () { }); }); }); + + it('should not re-add user to users:postcount if post is deleted after user deletion', async function () { + const uid = await User.create({ username: 'olduserwithposts' }); + assert(await db.isSortedSetMember('users:postcount', uid)); + + const result = await Topics.post({ + uid: uid, + title: 'old user topic', + content: 'old user topic post content', + cid: testCid, + }); + assert.equal(await db.sortedSetScore('users:postcount', uid), 1); + await User.deleteAccount(uid); + assert(!await db.isSortedSetMember('users:postcount', uid)); + await Posts.purge(result.postData.pid, 1); + assert(!await db.isSortedSetMember('users:postcount', uid)); + }); + + it('should not re-add user to users:reputation if post is upvoted after user deletion', async function () { + const uid = await User.create({ username: 'olduserwithpostsupvote' }); + assert(await db.isSortedSetMember('users:reputation', uid)); + + const result = await Topics.post({ + uid: uid, + title: 'old user topic', + content: 'old user topic post content', + cid: testCid, + }); + assert.equal(await db.sortedSetScore('users:reputation', uid), 0); + await User.deleteAccount(uid); + assert(!await db.isSortedSetMember('users:reputation', uid)); + await Posts.upvote(result.postData.pid, 1); + assert(!await db.isSortedSetMember('users:reputation', uid)); + }); }); describe('passwordReset', function () {