From b53afe2ed37bbcfb0f2ba4d8f0bd38944b2ba3d1 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 20 Mar 2026 12:01:03 +0100 Subject: [PATCH] fix(ap): include commentary in repost ActivityPub activities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reposts with a body (commentary) were silently broken in two ways: 1. jf2ToAS2Activity() always emitted a bare Announce pointing at the external URL (e.g. fromjason.xyz). That URL doesn't serve AP JSON, so Mastodon couldn't fetch the object and dropped the activity from followers' timelines — the post only appeared when explicitly searched. 2. jf2ToActivityStreams() (content negotiation / search) hard-coded the Note content to just '🔁 ', completely ignoring properties.content. Fix via patch-ap-repost-commentary.mjs (4 targeted replacements): - jf2ToAS2Activity(): skip the Announce early-return when commentary is present and fall through to the existing Create(Note) path instead. Pure reposts (no body) keep the Announce behaviour unchanged. - jf2ToAS2Activity() content block: add a repost branch that formats the Note as '

🔁 ' (mirrors bookmark/like). - jf2ToActivityStreams(): extract commentary and prepend it to the Note content when present. Patch registered in both postinstall and serve chains. Co-Authored-By: Claude Sonnet 4.6 --- package.json | 4 +- scripts/patch-ap-repost-commentary.mjs | 167 +++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 scripts/patch-ap-repost-commentary.mjs diff --git a/package.json b/package.json index e574dbac..135e5273 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-endpoint-posts-search-tags.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs", - "serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-endpoint-posts-search-tags.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", + "postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-endpoint-posts-search-tags.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs", + "serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-posts-ai-fields.mjs && node scripts/patch-endpoint-posts-ai-cleanup.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-preset-eleventy-ai-frontmatter.mjs && node scripts/patch-micropub-ai-block-resync.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-prefill-url.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-endpoint-posts-search-tags.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-ap-url-lookup-api.mjs && node scripts/patch-ap-allow-private-address.mjs && node scripts/patch-ap-repost-commentary.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/scripts/patch-ap-repost-commentary.mjs b/scripts/patch-ap-repost-commentary.mjs new file mode 100644 index 00000000..b8881c0a --- /dev/null +++ b/scripts/patch-ap-repost-commentary.mjs @@ -0,0 +1,167 @@ +/** + * Patch: include commentary in ActivityPub output for reposts. + * + * Root cause (two bugs in jf2-to-as2.js): + * + * 1. jf2ToAS2Activity() (Fedify delivery) always generates a bare + * `Announce { object: }` for repost posts, even when the + * post has a body (the author's commentary). External URLs like + * fromjason.xyz don't serve ActivityPub JSON, so Mastodon receives the + * Announce but cannot fetch the object — the activity is silently dropped + * from followers' timelines. The post only appears when searched because + * Mastodon then fetches the blog's own AP Note representation directly. + * + * 2. jf2ToActivityStreams() (content negotiation / search) returns a Note + * whose `content` field is hardcoded to `🔁 `, completely ignoring + * any commentary text in properties.content. + * + * Fix: + * - jf2ToAS2Activity(): if the repost has commentary, skip the early + * Announce return and fall through to the existing Create(Note) path so + * the text is included and the activity is a proper federated Note. + * Pure reposts (no commentary) keep the Announce behaviour. + * - jf2ToAS2Activity() content block: add a `repost` branch that formats + * the note as `

🔁 ` (mirroring bookmark/like). + * - jf2ToActivityStreams(): extract commentary from properties.content and + * prepend it to the note content when present. + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const candidates = [ + "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js", +]; + +const MARKER = "// repost-commentary fix"; + +// --------------------------------------------------------------------------- +// Fix A – jf2ToActivityStreams(): add commentary variable before the return +// --------------------------------------------------------------------------- +const OLD_CN_VARS = ` const repostOf = properties["repost-of"]; + const postUrl = resolvePostUrl(properties.url, publicationUrl); + return { + "@context": "https://www.w3.org/ns/activitystreams",`; + +const NEW_CN_VARS = ` const repostOf = properties["repost-of"]; + const postUrl = resolvePostUrl(properties.url, publicationUrl); + const commentary = linkifyUrls(properties.content?.html || properties.content || ""); // repost-commentary fix + return { + "@context": "https://www.w3.org/ns/activitystreams",`; + +// --------------------------------------------------------------------------- +// Fix B – jf2ToActivityStreams(): use commentary in the content field +// --------------------------------------------------------------------------- +const OLD_CN_CONTENT = ` cc: [\`\${actorUrl.replace(/\\/$/, "")}/followers\`], + content: \`\\u{1F501} \${repostOf}\`,`; + +const NEW_CN_CONTENT = ` cc: [\`\${actorUrl.replace(/\\/$/, "")}/followers\`], + content: commentary // repost-commentary fix + ? \`\${commentary}

\\u{1F501} \${repostOf}\` // repost-commentary fix + : \`\\u{1F501} \${repostOf}\`, // repost-commentary fix`; + +// --------------------------------------------------------------------------- +// Fix C – jf2ToAS2Activity(): only Announce when there is no commentary; +// fall through to Create(Note) when commentary is present +// --------------------------------------------------------------------------- +const OLD_AS2_ANNOUNCE = ` if (!repostOf) return null; + return new Announce({ + actor: actorUri, + object: new URL(repostOf), + to: new URL("https://www.w3.org/ns/activitystreams#Public"), + }); + }`; + +const NEW_AS2_ANNOUNCE = ` if (!repostOf) return null; + const repostContent = properties.content?.html || properties.content || ""; // repost-commentary fix + if (!repostContent) { // repost-commentary fix + return new Announce({ + actor: actorUri, + object: new URL(repostOf), + to: new URL("https://www.w3.org/ns/activitystreams#Public"), + }); + } // repost-commentary fix + // Has commentary — fall through to Create(Note) so the text is federated // repost-commentary fix + }`; + +// --------------------------------------------------------------------------- +// Fix D – jf2ToAS2Activity() content block: add repost branch +// --------------------------------------------------------------------------- +const OLD_AS2_CONTENT = ` } else { + noteOptions.content = linkifyUrls(properties.content?.html || properties.content || ""); + }`; + +const NEW_AS2_CONTENT = ` } else if (postType === "repost") { // repost-commentary fix + const repostUrl = properties["repost-of"]; // repost-commentary fix + const commentary = linkifyUrls(properties.content?.html || properties.content || ""); // repost-commentary fix + noteOptions.content = commentary // repost-commentary fix + ? \`\${commentary}

\\u{1F501} \${repostUrl}\` // repost-commentary fix + : \`\\u{1F501} \${repostUrl}\`; // repost-commentary fix + } else { + noteOptions.content = linkifyUrls(properties.content?.html || properties.content || ""); + }`; + +// --------------------------------------------------------------------------- + +async function exists(filePath) { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +let checked = 0; +let patched = 0; + +for (const filePath of candidates) { + if (!(await exists(filePath))) { + continue; + } + + checked += 1; + let source = await readFile(filePath, "utf8"); + + if (source.includes(MARKER)) { + console.log(`[postinstall] patch-ap-repost-commentary: already applied to ${filePath}`); + continue; + } + + let updated = source; + let changed = false; + + // Apply each replacement, warn if the old string is not found + const replacements = [ + ["Fix A (CN vars)", OLD_CN_VARS, NEW_CN_VARS], + ["Fix B (CN content)", OLD_CN_CONTENT, NEW_CN_CONTENT], + ["Fix C (AS2 announce)", OLD_AS2_ANNOUNCE, NEW_AS2_ANNOUNCE], + ["Fix D (AS2 content block)", OLD_AS2_CONTENT, NEW_AS2_CONTENT], + ]; + + for (const [label, oldStr, newStr] of replacements) { + if (updated.includes(oldStr)) { + updated = updated.replace(oldStr, newStr); + changed = true; + } else { + console.warn(`[postinstall] patch-ap-repost-commentary: ${label} snippet not found in ${filePath} — skipping`); + } + } + + if (!changed || updated === source) { + console.log(`[postinstall] patch-ap-repost-commentary: no changes applied to ${filePath}`); + continue; + } + + await writeFile(filePath, updated, "utf8"); + patched += 1; + console.log(`[postinstall] Applied patch-ap-repost-commentary to ${filePath}`); +} + +if (checked === 0) { + console.log("[postinstall] patch-ap-repost-commentary: no target files found"); +} else if (patched === 0) { + console.log("[postinstall] patch-ap-repost-commentary: already up to date"); +} else { + console.log(`[postinstall] patch-ap-repost-commentary: patched ${patched}/${checked} file(s)`); +}