From 4d3211cabaf963c971019e13d4dfb7bc99fc14dc Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 26 Mar 2026 12:04:40 -0400 Subject: [PATCH] fix: regression where topic moves during Announce(Create(Note)) stopped working, added test for #14040, fix broken AP test helper mock --- src/activitypub/inbox.js | 4 +-- src/activitypub/notes.js | 2 +- src/topics/tools.js | 4 +-- test/activitypub/helpers.js | 5 +-- test/activitypub/inbox.js | 72 +++++++++++++++++++++++++++++++------ 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index cd14579362..b8967cbef5 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -53,7 +53,7 @@ inbox.create = async (req) => { cid = Array.from(cids)[0]; } - const asserted = await activitypub.notes.assert(0, object, { cid }); + const asserted = await activitypub.notes.assert(req.uid || 0, object, { cid }); if (asserted) { await activitypub.feps.announce(object.id, req.body); // api.activitypub.add(req, { pid: object.id }); @@ -461,7 +461,7 @@ inbox.announce = async (req) => { return; } - const assertion = await activitypub.notes.assert(0, pid, { cid, skipChecks: true }); + const assertion = await activitypub.notes.assert(req.uid || 0, pid, { cid, skipChecks: true }); if (!assertion) { return; } diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 8522965826..1b570ea689 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -121,7 +121,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { if (options.cid && cid === -1) { // Move topic if currently uncategorized - await api.topics.move({ uid: 'system' }, { tid, cid: options.cid }); + await topics.tools.move(tid, { cid: options.cid, uid: 'system' }); } const exists = await posts.exists(chain.map(p => p.pid)); diff --git a/src/topics/tools.js b/src/topics/tools.js index f5e9b13dc7..e43a7df208 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -233,7 +233,7 @@ module.exports = function (Topics) { }; topicTools.move = async function (tid, data) { - const cid = parseInt(data.cid, 10); + const cid = utils.isNumber(data.cid) ? parseInt(data.cid, 10) : data.cid; const topicData = await Topics.getTopicData(tid); if (!topicData) { throw new Error('[[error:no-topic]]'); @@ -241,7 +241,7 @@ module.exports = function (Topics) { if (cid === topicData.cid) { throw new Error('[[error:cant-move-topic-to-same-category]]'); } - if (!utils.isNumber(cid) || !utils.isNumber(topicData.cid)) { + if (data.uid !== 'system' && (!utils.isNumber(cid) || !utils.isNumber(topicData.cid))) { throw new Error('[[error:cant-move-topic-to-from-remote-categories]]'); } diff --git a/test/activitypub/helpers.js b/test/activitypub/helpers.js index c026f23e1d..eb3056d2af 100644 --- a/test/activitypub/helpers.js +++ b/test/activitypub/helpers.js @@ -55,12 +55,13 @@ Helpers.mocks.group = (override = {}) => { type: 'Group', ...override, }); + const { hostname } = new URL(id); activitypub._cache.set(`0;${id}`, actor); - activitypub.helpers._webfingerCache.set(`${actor.preferredUsername}@example.org`, { + activitypub.helpers._webfingerCache.set(`${actor.preferredUsername}@${hostname}`, { actorUri: id, username: id, - hostname: 'example.org', + hostname, }); return { id, actor }; diff --git a/test/activitypub/inbox.js b/test/activitypub/inbox.js index 4c812a873d..bfcdb690e9 100644 --- a/test/activitypub/inbox.js +++ b/test/activitypub/inbox.js @@ -183,20 +183,72 @@ describe('Inbox', () => { }); describe('(Create)', () => { - it('should create a new topic in a remote category if addressed', async () => { - const { id: remoteCid } = helpers.mocks.group(); - const { id, note } = helpers.mocks.note({ - audience: [remoteCid], + describe('newly-discovered topic', () => { + before(async function () { + const { id: remoteCid } = helpers.mocks.group(); + const { id, note } = helpers.mocks.note({ + audience: [remoteCid], + }); + this.id = id; + this.remoteCid = remoteCid; + let { activity } = helpers.mocks.create(note); + ({ activity } = helpers.mocks.announce({ actor: remoteCid, object: activity })); + + await activitypub.inbox.announce({ body: activity }); }); - let { activity } = helpers.mocks.create(note); - ({ activity } = helpers.mocks.announce({ actor: remoteCid, object: activity })); - await activitypub.inbox.announce({ body: activity }); + it('should create a new topic in a remote category if addressed', async function () { + assert(await posts.exists(this.id)); - assert(await posts.exists(id)); + const cid = await posts.getCidByPid(this.id); + assert.strictEqual(cid, this.remoteCid); + }); + }); - const cid = await posts.getCidByPid(id); - assert.strictEqual(cid, remoteCid); + describe.only('known topic in cid -1 (author domain != announcer domain)', async () => { + /** + * This happens if follower receives object from microblog user before the community announces it. + * It's probably more likely to occur because the Create(Note) is a single hop whereas the reflected + * Announce(Create(Note)) takes two hops. + * + * If the author and announcer domain are the same, the object should already be correctly classified. + */ + before(async function () { + const { id: remoteCid } = helpers.mocks.group({ + id: `https://example.social/${utils.generateUUID()}`, + }); + await activitypub.actors.assertGroup([remoteCid]); + const uid = await user.create({ username: utils.generateUUID().slice(0, 10) }); + + this.uid = uid; + this.remoteCid = remoteCid; + }); + + it('should create a topic in cid -1', async function () { + const { id, note } = helpers.mocks.note({ + to: [activitypub._constants.publicAddress, this.remoteCid], + }); + + const { activity } = helpers.mocks.create(note); + await activitypub.inbox.create({ uid: this.uid, body: activity }); + + this.id = id; + this.note = note; + this.activity = activity; + + const cid = await posts.getCidByPid(this.id); + assert.strictEqual(cid, -1); + }); + + it('should handle the Announce(Create) from the remote category', async function () { + const { activity } = helpers.mocks.announce({ actor: this.remoteCid, object: this.activity }); + await activitypub.inbox.announce({ uid: this.uid, body: activity }); + }); + + it('should be categorized in the remote category', async function () { + const cid = await posts.getCidByPid(this.id); + assert.strictEqual(cid, this.remoteCid); + }); }); });