refactor: store local follow backreferences for remote users (both followers and following), update actor pruning to take local follow counts into consideration, fixes #12701

This commit is contained in:
Julian Lam
2024-07-19 14:37:32 -04:00
parent a884681abf
commit 39f92eac16
5 changed files with 67 additions and 14 deletions

View File

@@ -207,12 +207,19 @@ Actors.getLocalFollowers = async (id) => {
return response;
};
Actors.getLocalFollowersCount = async (id) => {
if (!activitypub.helpers.isUri(id)) {
return false;
Actors.getLocalFollowCounts = async (actor) => {
let followers = 0; // x local followers
let following = 0; // following x local users
if (!activitypub.helpers.isUri(actor)) {
return { followers, following };
}
return await db.sortedSetCard(`followersRemote:${id}`);
[followers, following] = await Promise.all([
db.sortedSetCard(`followersRemote:${actor}`),
db.sortedSetCard(`followingRemote:${actor}`),
]);
return { followers, following };
};
Actors.remove = async (id) => {
@@ -270,7 +277,7 @@ Actors.prune = async () => {
await batch.processArray(uids, async (uids) => {
const exists = await db.exists(uids.map(uid => `userRemote:${uid}`));
const counts = await db.sortedSetsCard(uids.map(uid => `uid:${uid}:posts`));
const postCounts = await db.sortedSetsCard(uids.map(uid => `uid:${uid}:posts`));
await Promise.all(uids.map(async (uid, idx) => {
if (!exists[idx]) {
// id in zset but not asserted, handle and return early
@@ -278,8 +285,9 @@ Actors.prune = async () => {
return;
}
const count = counts[idx];
if (count < 1) {
const { followers, following } = await Actors.getLocalFollowCounts(uid);
const postCount = postCounts[idx];
if ([postCount, followers, following].every(metric => metric < 1)) {
try {
await user.deleteAccount(uid);
deletionCount += 1;

View File

@@ -235,8 +235,8 @@ inbox.announce = async (req) => {
} else { // Remote object
// Follower check
if (!cid) {
const numFollowers = await activitypub.actors.getLocalFollowersCount(actor);
if (!numFollowers) {
const { followers } = await activitypub.actors.getLocalFollowCounts(actor);
if (!followers) {
winston.verbose(`[activitypub/inbox.announce] Rejecting ${object.id} via ${actor} due to no followers`);
reject('Announce', object, actor);
return;
@@ -300,6 +300,7 @@ 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);
@@ -422,7 +423,10 @@ inbox.undo = async (req) => {
throw new Error('[[error:invalid-uid]]');
}
await db.sortedSetRemove(`followersRemote:${id}`, actor);
await Promise.all([
db.sortedSetRemove(`followersRemote:${id}`, actor),
db.sortedSetRemove(`followingRemote:${actor}`, id),
]);
const followerRemoteCount = await db.sortedSetCard(`followerRemote:${id}`);
await user.setUserField(id, 'followerRemoteCount', followerRemoteCount);
notifications.rescind(`follow:${id}:uid:${actor}`);

View File

@@ -40,10 +40,10 @@ Mocks.profile = async (actors) => {
let hostname;
let {
url, preferredUsername, published, icon, image,
name, summary, followers, followerCount, followingCount,
inbox, endpoints,
name, summary, followers, inbox, endpoints,
} = actor;
preferredUsername = preferredUsername || slugify(name);
const { followers: followerCount, following: followingCount } = await activitypub.actors.getLocalFollowCounts(uid);
try {
({ hostname } = new URL(actor.id));

View File

@@ -181,7 +181,7 @@ async function assertRelation(post) {
*/
// Is followed by at least one local user
const numFollowers = await activitypub.actors.getLocalFollowersCount(post.uid);
const { followers } = await activitypub.actors.getLocalFollowCounts(post.uid);
// Local user is mentioned
const { tag } = post._activitypub;
@@ -201,7 +201,7 @@ async function assertRelation(post) {
uids = uids.filter(Boolean);
}
return numFollowers > 0 || uids.length;
return followers > 0 || uids.length;
}
Notes.updateLocalRecipients = async (id, { to, cc }) => {

View File

@@ -0,0 +1,41 @@
'use strict';
const db = require('../../database');
const batch = require('../../batch');
const activitypub = require('../../activitypub');
module.exports = {
name: 'Establish follow backreference sorted sets for remote users',
timestamp: Date.UTC(2024, 4, 1),
method: async function () {
const { progress } = this;
const bulkOp = [];
const now = Date.now();
const reassert = [];
await batch.processSortedSet('users:joindate', async (uids) => {
const [_followers, _following] = await Promise.all([
db.getSortedSetsMembers(uids.map(uid => `followersRemote:${uid}`)),
db.getSortedSetsMembers(uids.map(uid => `followingRemote:${uid}`)),
]);
const toCheck = Array.from(new Set(_following.flat()));
const asserted = await db.isSortedSetMembers('usersRemote:lastCrawled', toCheck);
reassert.push(...toCheck.filter((actor, idx) => !asserted[idx]));
uids.forEach((uid, idx) => {
const followers = _followers[idx];
if (followers.length) {
bulkOp.push(...followers.map(actor => [`followingRemote:${actor}`, now, uid]))
}
});
progress.incr(uids.length);
}, { progress });
await Promise.all([
db.sortedSetAddBulk(bulkOp),
activitypub.actors.assert(Array.from(new Set(reassert))),
]);
},
};