From 77aad6594705e845ee92416274d5089a3c3fa49b Mon Sep 17 00:00:00 2001 From: Ricardo Date: Sun, 22 Feb 2026 21:21:55 +0100 Subject: [PATCH] fix: include reply author in cc and log delivery failures Quick replies only sent to followers, never directly to the replied-to author's server. The author was also missing from the Note's cc field, so Mastodon couldn't thread or notify. Now resolves the author before constructing the Note, includes them in ccs, sends directly to their inbox, and logs failures instead of silently swallowing them. --- lib/controllers/compose.js | 99 +++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/lib/controllers/compose.js b/lib/controllers/compose.js index 1c1ec28..4655588 100644 --- a/lib/controllers/compose.js +++ b/lib/controllers/compose.js @@ -204,32 +204,10 @@ export function submitComposeController(mountPath, plugin) { "https://www.w3.org/ns/activitystreams#Public", ); const followersUri = ctx.getFollowersUri(handle); - const note = new Note({ - id: new URL(noteId), - attribution: actorUri, - content: content.trim(), - replyTarget: inReplyTo ? new URL(inReplyTo) : undefined, - published: Temporal.Now.instant(), - to: publicAddress, - cc: followersUri, - }); - const create = new Create({ - id: new URL(`${noteId}#activity`), - actor: actorUri, - object: note, - to: publicAddress, - cc: followersUri, - }); - - // Send to followers - await ctx.sendActivity({ identifier: handle }, "followers", create, { - preferSharedInbox: true, - syncCollection: true, - orderingKey: noteId, - }); - - // If replying, also send to the original author + // Resolve the original author BEFORE constructing the Note, + // so we can include them in cc (required for threading/notification) + let recipient = null; if (inReplyTo) { try { const documentLoader = await ctx.getDocumentLoader({ @@ -246,21 +224,64 @@ export function submitComposeController(mountPath, plugin) { const author = await remoteObject.getAttributedTo({ documentLoader, }); - const recipient = Array.isArray(author) - ? author[0] - : author; - - if (recipient) { - await ctx.sendActivity( - { identifier: handle }, - recipient, - create, - { orderingKey: noteId }, - ); - } + recipient = Array.isArray(author) ? author[0] : author; } - } catch { - // Non-critical — followers still got it + } catch (error) { + console.warn( + `[ActivityPub] lookupObject failed for ${inReplyTo} (quick reply):`, + error.message, + ); + } + } + + // Build cc list: always include followers, add original author for replies + const ccList = [followersUri]; + if (recipient?.id) { + ccList.push(recipient.id); + } + + const note = new Note({ + id: new URL(noteId), + attribution: actorUri, + content: content.trim(), + replyTarget: inReplyTo ? new URL(inReplyTo) : undefined, + published: Temporal.Now.instant(), + to: publicAddress, + ccs: ccList, + }); + + const create = new Create({ + id: new URL(`${noteId}#activity`), + actor: actorUri, + object: note, + to: publicAddress, + ccs: ccList, + }); + + // Send to followers + await ctx.sendActivity({ identifier: handle }, "followers", create, { + preferSharedInbox: true, + syncCollection: true, + orderingKey: noteId, + }); + + // Also send directly to the original author's inbox + if (recipient) { + try { + await ctx.sendActivity( + { identifier: handle }, + recipient, + create, + { orderingKey: noteId }, + ); + console.info( + `[ActivityPub] Sent quick reply directly to ${recipient.id?.href || "author"}`, + ); + } catch (error) { + console.warn( + `[ActivityPub] Direct delivery to author failed (quick reply):`, + error.message, + ); } } diff --git a/package.json b/package.json index 61ad296..1800bff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "2.0.6", + "version": "2.0.7", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit",