/** * Patch: fix inbound reply/like/boost handling — publicationUrl missing in inbox handlers. * * Root cause: * setupFederation() passes `publicationUrl` to its own scope but does NOT * pass it to registerInboxListeners(). All inbox handlers (handleCreate, * handleLike, handleAnnounce) read `collections._publicationUrl` to gate * notifications and timeline storage, but that property is never set on the * collections object. * * Consequence: * - handleCreate: `if (pubUrl && inReplyTo.startsWith(pubUrl))` is always * false → reply notifications are never created. * - handleAnnounce: boost notifications for our content never created. * - handleCreate: replies to our posts from non-followed accounts are never * stored in ap_timeline → invisible in Mastodon client conversation views. * * Fix A (federation-setup.js): * Set `collections._publicationUrl = publicationUrl` immediately before * registerInboxListeners() so the value flows through to all handlers. * * Fix B (inbox-handlers.js): * In handleCreate, add an else-if branch that stores replies to our own posts * in ap_timeline even when the replier is not in ap_following. This runs only * when pubUrl is correctly set (Fix A). */ import { access, readFile, writeFile } from "node:fs/promises"; const MARKER = "// [patch] ap-inbox-publication-url"; // ── Fix A: federation-setup.js ──────────────────────────────────────────────── const federationCandidates = [ "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js", "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js", ]; const OLD_REGISTER = ` registerInboxListeners(inboxChain, { collections, handle, storeRawActivities, });`; const NEW_REGISTER = ` // Expose publicationUrl on collections so inbox handlers can gate ${MARKER} // notifications/timeline-storage to our own content only. collections._publicationUrl = publicationUrl; registerInboxListeners(inboxChain, { collections, handle, storeRawActivities, });`; // ── Fix B: inbox-handlers.js ────────────────────────────────────────────────── const handlersCandidates = [ "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-handlers.js", "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-handlers.js", ]; const OLD_FOLLOWED_TAGS = ` } else if (collections.ap_followed_tags) { // Not a followed account — check if the post's hashtags match any followed tags`; const NEW_FOLLOWED_TAGS = ` } else if (pubUrl && inReplyTo && inReplyTo.startsWith(pubUrl)) { // Reply to our post from a non-followed account — store in timeline ${MARKER} // so it appears in the Mastodon client API's conversation/notification view. try { const timelineItem = await extractObjectData(object, { actorFallback: actorObj, documentLoader: authLoader, }); timelineItem.visibility = computeVisibility(object); await addTimelineItem(collections, timelineItem); } catch (error) { console.error("[inbox-handlers] Failed to store reply timeline item:", error.message); } } else if (collections.ap_followed_tags) { // Not a followed account — check if the post's hashtags match any followed tags`; async function exists(p) { try { await access(p); return true; } catch { return false; } } async function patch(candidates, oldSnippet, newSnippet, label) { let checked = 0; let patched = 0; for (const filePath of candidates) { if (!(await exists(filePath))) continue; checked++; const source = await readFile(filePath, "utf8"); if (source.includes(MARKER)) { console.log(`[postinstall] patch-ap-inbox-publication-url: ${label} already applied to ${filePath}`); continue; } if (!source.includes(oldSnippet)) { console.warn(`[postinstall] patch-ap-inbox-publication-url: ${label} snippet not found in ${filePath}`); continue; } await writeFile(filePath, source.replace(oldSnippet, newSnippet), "utf8"); patched++; console.log(`[postinstall] Applied patch-ap-inbox-publication-url (${label}) to ${filePath}`); } return { checked, patched }; } const a = await patch(federationCandidates, OLD_REGISTER, NEW_REGISTER, "set _publicationUrl"); const b = await patch(handlersCandidates, OLD_FOLLOWED_TAGS, NEW_FOLLOWED_TAGS, "store reply from non-follower"); const total = a.patched + b.patched; if (a.checked + b.checked === 0) { console.log("[postinstall] patch-ap-inbox-publication-url: no target files found"); } else if (total === 0) { console.log("[postinstall] patch-ap-inbox-publication-url: already up to date"); } else { console.log(`[postinstall] patch-ap-inbox-publication-url: patched ${total} file(s)`); }