From 44e78e4775204ae00b0176932eb9bd239b276f34 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Mar 2026 12:13:50 -0400 Subject: [PATCH] fix: sync follow counts on local and remote follows, #14105 --- src/activitypub/helpers.js | 2 +- src/activitypub/inbox.js | 19 +++++++++-------- src/controllers/activitypub/index.js | 12 ++++------- src/user/follow.js | 31 +++++++++++++++++++++------- 4 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index a6dae62caf..3b6e7549aa 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -473,7 +473,7 @@ Helpers.generateCollection = async ({ set, method, count, page, perPage, url }) } else if (set) { method = method.bind(null, set); } - count = count || await db.sortedSetCard(set); + count = count ?? await db.sortedSetCard(set); const pageCount = Math.max(1, Math.ceil(count / perPage)); let items = []; let paginate = true; diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index d0ee3976a4..cd14579362 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -517,11 +517,12 @@ inbox.follow = async (req) => { } const now = Date.now(); - await db.sortedSetAdd(`followersRemote:${id}`, now, actor); - await db.sortedSetAdd(`followingRemote:${actor}`, now, id); // for following backreference (actor pruning) - - const followerRemoteCount = await db.sortedSetCard(`followersRemote:${id}`); - await user.setUserField(id, 'followerRemoteCount', followerRemoteCount); + await Promise.all([ + db.sortedSetAdd(`followersRemote:${id}`, now, actor), + db.sortedSetAdd(`followingRemote:${actor}`, now, id), // for following backreference (actor pruning) + user.syncFollowCounts(id, false, true), + user.syncFollowCounts(actor, true, false), + ]); activitypub.actors._followerCache.del(id); await user.onFollow(actor, id); @@ -600,8 +601,8 @@ inbox.accept = async (req) => { db.sortedSetAdd(`followingRemote:${id}`, timestamp, actor), db.sortedSetAdd(`followersRemote:${actor}`, timestamp, id), // for followers backreference and notes assertion checking ]); - const followingRemoteCount = await db.sortedSetCard(`followingRemote:${id}`); - await user.setUserField(id, 'followingRemoteCount', followingRemoteCount); + await user.syncFollowCounts(id, true, false); + await user.syncFollowCounts(actor, false, true); } else if (localType === 'category') { if (!await db.isSortedSetMember(`followRequests:cid.${id}`, actor)) { if (await db.isSortedSetMember(`cid:${id}:following`, actor)) return; // already following @@ -649,9 +650,9 @@ inbox.undo = async (req) => { await Promise.all([ db.sortedSetRemove(`followersRemote:${id}`, actor), db.sortedSetRemove(`followingRemote:${actor}`, id), + user.syncFollowCounts(id, false, true), + user.syncFollowCounts(actor, true, false), ]); - const followerRemoteCount = await db.sortedSetCard(`followerRemote:${id}`); - await user.setUserField(id, 'followerRemoteCount', followerRemoteCount); notifications.rescind(`follow:${id}:uid:${actor}`); activitypub.actors._followerCache.del(id); break; diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 086e90933b..b7eaa334c8 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -66,10 +66,8 @@ Controller.fetch = async (req, res, next) => { }; Controller.getFollowing = async (req, res) => { - const { followingCount, followingRemoteCount } = await user.getUserFields(req.params.uid, ['followingCount', 'followingRemoteCount']); - const totalItems = parseInt(followingCount || 0, 10) + parseInt(followingRemoteCount || 0, 10); - - const count = totalItems; + const followingCount = await user.getUserField(req.params.uid, 'followingCount'); + const count = parseInt(followingCount, 10); const collection = await activitypub.helpers.generateCollection({ method: user.getFollowing.bind(null, req.params.uid), count, @@ -92,10 +90,8 @@ Controller.getFollowing = async (req, res) => { }; Controller.getFollowers = async (req, res) => { - const { followerCount, followerRemoteCount } = await user.getUserFields(req.params.uid, ['followerCount', 'followerRemoteCount']); - const totalItems = parseInt(followerCount || 0, 10) + parseInt(followerRemoteCount || 0, 10); - - const count = totalItems; + const followerCount = await user.getUserField(req.params.uid, 'followerCount'); + const count = parseInt(followerCount, 10); const collection = await activitypub.helpers.generateCollection({ method: user.getFollowers.bind(null, req.params.uid), count, diff --git a/src/user/follow.js b/src/user/follow.js index 7e561d07e5..d14251d38c 100644 --- a/src/user/follow.js +++ b/src/user/follow.js @@ -58,15 +58,32 @@ module.exports = function (User) { ]); } - const [followingCount, followingRemoteCount, followerCount, followerRemoteCount] = await db.sortedSetsCard([ - `following:${uid}`, `followingRemote:${uid}`, `followers:${theiruid}`, `followersRemote:${theiruid}`, - ]); - await Promise.all([ - User.setUserField(uid, 'followingCount', followingCount + followingRemoteCount), - User.setUserField(theiruid, 'followerCount', followerCount + followerRemoteCount), - ]); + User.syncFollowCounts(uid, true, false); + User.syncFollowCounts(theiruid, false, true); } + User.syncFollowCounts = async function (uid, following, followers) { + const sets = []; + const property = []; + if (following) { + property.push('followingCount'); + sets.push(`following:${uid}`, `followingRemote:${uid}`); + } + if (followers) { + property.push('followerCount'); + sets.push(`followers:${uid}`, `followersRemote:${uid}`); + }; + + const values = await db.sortedSetsCard(sets); + const payload = property.reduce((payload, cur, idx) => { + const sum = values[idx * 2] + values[(idx * 2) + 1]; + payload[cur] = sum; + return payload; + }, {}); + + await User.setUserFields(uid, payload); + }; + User.getFollowing = async function (uid, start, stop) { return await getFollow(uid, 'following', start, stop); };