From ac7b7f81b3d267db677760d47ae635ccd6bb87be Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 24 Mar 2025 15:15:48 -0400 Subject: [PATCH] feat: remote user to category migration should also migrate local user follows into category watches --- src/activitypub/actors.js | 7 +++ test/activitypub/actors.js | 89 +++++++++++++++++++++++++------------- 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/activitypub/actors.js b/src/activitypub/actors.js index 4dc4576979..d2b08befcd 100644 --- a/src/activitypub/actors.js +++ b/src/activitypub/actors.js @@ -418,6 +418,13 @@ async function _migratePersonToGroup(categoryObjs) { }); } })); + + const followers = await db.getSortedSetMembersWithScores(`followersRemote:${id}`); + await db.sortedSetAdd( + `cid:${id}:uid:watch:state`, + followers.map(() => categories.watchStates.tracking), + followers.map(({ value }) => value), + ); await user.deleteAccount(id); })); await categories.onTopicsMoved(ids); diff --git a/test/activitypub/actors.js b/test/activitypub/actors.js index 2be6a5c2f1..90a246bd9f 100644 --- a/test/activitypub/actors.js +++ b/test/activitypub/actors.js @@ -76,46 +76,73 @@ describe('Actor asserton', () => { assert.strictEqual(assertion[0].cid, actor.id); }); - it('should not migrate a user to a category if .assert is called', async () => { - // ... because the user isn't due for an update and so is filtered out during qualification - const { id } = helpers.mocks.person(); - await activitypub.actors.assert([id]); + describe('remote user to remote category migration', () => { + it('should not migrate a user to a category if .assert is called', async () => { + // ... because the user isn't due for an update and so is filtered out during qualification + const { id } = helpers.mocks.person(); + await activitypub.actors.assert([id]); - const { actor } = helpers.mocks.group({ id }); - const assertion = await activitypub.actors.assertGroup([id]); + const { actor } = helpers.mocks.group({ id }); + const assertion = await activitypub.actors.assertGroup([id]); - assert(assertion.length, 0); + assert(assertion.length, 0); - const exists = await user.exists(id); - assert.strictEqual(exists, false); - }); + const exists = await user.exists(id); + assert.strictEqual(exists, false); + }); - it('should migrate a user to a category if on re-assertion it identifies as an as:Group', async () => { - // This is to handle previous behaviour that saved all as:Group actors as NodeBB users. - const { id } = helpers.mocks.person(); - await activitypub.actors.assert([id]); + it('should migrate a user to a category if on re-assertion it identifies as an as:Group', async () => { + // This is to handle previous behaviour that saved all as:Group actors as NodeBB users. + const { id } = helpers.mocks.person(); + await activitypub.actors.assert([id]); - // Two shares - for (let x = 0; x < 2; x++) { - const { id: pid } = helpers.mocks.note(); - // eslint-disable-next-line no-await-in-loop - const { tid } = await activitypub.notes.assert(0, pid, { skipChecks: 1 }); - // eslint-disable-next-line no-await-in-loop - await db.sortedSetAdd(`uid:${id}:shares`, Date.now(), tid); - } + helpers.mocks.group({ id }); + const assertion = await activitypub.actors.assertGroup([id]); - const { actor } = helpers.mocks.group({ id }); - const assertion = await activitypub.actors.assertGroup([id]); + assert(assertion && Array.isArray(assertion) && assertion.length === 1); - assert(assertion && Array.isArray(assertion) && assertion.length === 1); + const exists = await user.exists(id); + assert.strictEqual(exists, false); + }); - const { topic_count, post_count } = await categories.getCategoryData(id); - assert.strictEqual(topic_count, 2); - assert.strictEqual(post_count, 2); + it('should migrate any shares by that user, into topics in the category', async () => { + const { id } = helpers.mocks.person(); + await activitypub.actors.assert([id]); - const exists = await user.exists(id); - assert.strictEqual(exists, false); - }); + // Two shares + for (let x = 0; x < 2; x++) { + const { id: pid } = helpers.mocks.note(); + // eslint-disable-next-line no-await-in-loop + const { tid } = await activitypub.notes.assert(0, pid, { skipChecks: 1 }); + // eslint-disable-next-line no-await-in-loop + await db.sortedSetAdd(`uid:${id}:shares`, Date.now(), tid); + } + + helpers.mocks.group({ id }); + await activitypub.actors.assertGroup([id]); + + const { topic_count, post_count } = await categories.getCategoryData(id); + assert.strictEqual(topic_count, 2); + assert.strictEqual(post_count, 2); + }); + + it('should migrate any local followers into category watches', async () => { + const { id } = helpers.mocks.person(); + await activitypub.actors.assert([id]); + + const followerUid = await user.create({ username: utils.generateUUID() }); + await Promise.all([ + db.sortedSetAdd(`followingRemote:${followerUid}`, Date.now(), id), + db.sortedSetAdd(`followersRemote:${id}`, Date.now(), followerUid), + ]); + + helpers.mocks.group({ id }); + await activitypub.actors.assertGroup([id]); + + const states = await categories.getWatchState([id], followerUid); + assert.strictEqual(states[0], categories.watchStates.tracking); + }) + }) }); describe('edge cases: loopback handles and uris', () => {