From ee0b0bb58a237e267bf1b5d95013780a49bf92ac Mon Sep 17 00:00:00 2001 From: Ricardo Date: Mon, 23 Mar 2026 08:25:24 +0100 Subject: [PATCH] fix: broadcast Update activity (not Create) in broadcastPostUpdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jf2ToAS2Activity() always returns a Create activity. For post edits, Fediverse servers expect an Update wrapping the updated object, not a second Create. Extract the Note/Article from the Create via getObject(), then wrap it in new Update({ actor, object: note }) — matching the existing broadcastActorUpdate() pattern exactly. --- index.js | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 8e611c6..4ca609b 100644 --- a/index.js +++ b/index.js @@ -1180,24 +1180,37 @@ export default class ActivityPubEndpoint { if (!this._federation) return; try { + const { Update } = await import("@fedify/fedify/vocab"); const actorUrl = this._getActorUrl(); - const activity = jf2ToAS2Activity( + const handle = this.options.actor.handle; + const ctx = this._federation.createContext( + new URL(this._publicationUrl), + { handle, publicationUrl: this._publicationUrl }, + ); + + // Build the Note/Article object by calling jf2ToAS2Activity() and + // extracting the wrapped object from the returned Create activity. + // For post edits, Fediverse servers expect an Update activity wrapping + // the updated object — NOT a second Create activity. + const createActivity = jf2ToAS2Activity( properties, actorUrl, this._publicationUrl, { visibility: this.options.defaultVisibility }, ); - if (!activity) { + if (!createActivity) { console.warn(`[ActivityPub] broadcastPostUpdate: could not convert post to AS2 for ${properties?.url}`); return; } - const handle = this.options.actor.handle; - const ctx = this._federation.createContext( - new URL(this._publicationUrl), - { handle, publicationUrl: this._publicationUrl }, - ); + // Extract the Note/Article object from the Create wrapper, then build + // an Update activity around it — matching broadcastActorUpdate() pattern. + const noteObject = await createActivity.getObject(); + const activity = new Update({ + actor: ctx.getActorUri(handle), + object: noteObject, + }); const followers = await this._collections.ap_followers .find({}) @@ -1702,6 +1715,7 @@ export default class ActivityPubEndpoint { }, 10_000); // Run one-time migrations (idempotent — safe to run on every startup) + console.info("[ActivityPub] Init: starting post-refollow setup"); runSeparateMentionsMigration(this._collections).then(({ skipped, updated }) => { if (!skipped) { console.log(`[ActivityPub] Migration separate-mentions: updated ${updated} timeline items`); @@ -1748,6 +1762,7 @@ export default class ActivityPubEndpoint { }); // Start async inbox queue processor (processes one item every 3s) + console.info("[ActivityPub] Init: starting inbox queue processor"); this._inboxProcessorInterval = startInboxProcessor( this._collections, () => this._federation?.createContext(new URL(this._publicationUrl), {