From 754ae7a80c2f2543a827dacbf759bdfb0def8a7d Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 10 Apr 2026 10:13:32 +0200 Subject: [PATCH] fix: make interaction retrieval robust against three failure modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - patch-ap-interactions-cleanup-preserve: daily timeline cleanup was silently deleting liked/bookmarked posts from ap_timeline along with their ap_interactions records; exempts interacted items from cleanup - patch-ap-interactions-accounts-uid: account statuses route added ix.objectUrl to interaction Sets instead of item.uid, causing wrong favourited/reblogged state when url ≠ uid - patch-ap-interactions-context-state: thread context (ancestors/ descendants) used empty interaction Sets; now batch-loads real state Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 3 + memory/project_activitypub.md | 28 +++++ package.json | 4 +- .../patch-ap-interactions-accounts-uid.mjs | 98 +++++++++++++++++ .../patch-ap-interactions-context-state.mjs | 104 ++++++++++++++++++ 5 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 scripts/patch-ap-interactions-accounts-uid.mjs create mode 100644 scripts/patch-ap-interactions-context-state.mjs diff --git a/CLAUDE.md b/CLAUDE.md index 64f3d711..2247f5e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -178,6 +178,9 @@ npm install git+https://gitea.giersig.eu/svemagie/indiekit-endpoint-activitypub | AP lookup returns 302 / auth redirect | nginx not forwarding `Host`/`X-Forwarded-Proto` — see `patch-ap-federation-bridge-base-url` | | `findTimelineItemById` returns null | Item not yet in `ap_timeline` (build not finished) or TZ-offset date mismatch — `$dateFromString` range query should catch offsets | | Favourite/reblog hangs in Mastodon client | `resolveAuthor` timeout — `Promise.race` 5 s cap should prevent this | +| Liked/bookmarked posts disappear from Favourites/Bookmarks over time | Daily timeline cleanup was deleting them — fixed by `patch-ap-interactions-cleanup-preserve` (exempts items present in `ap_interactions`) | +| Favourited/reblogged state wrong on account statuses timeline | `accounts.js` added `ix.objectUrl` to the Sets instead of `item.uid` — fixed by `patch-ap-interactions-accounts-uid` | +| Liked posts show as not-liked in thread context (ancestors/descendants) | Context endpoint used empty interaction Sets — fixed by `patch-ap-interactions-context-state` | | "Empty reply from server" on webmention poller | Poller routing through nginx (returns 444 for wrong Host) — must use `INDIEKIT_DIRECT_URL` | | HTTP Signature 401 errors on all inbound activities | nginx forwarding wrong `Host` header — fixed by `patch-ap-signature-host-header` (overrides to `blog.giersig.eu`) | | HTTP Signature verify errors flooding logs for deleted/migrated actors | Expected noise — `patch-ap-inbox-delivery-debug` suppresses to `fatal`; real errors surface at `error` level | diff --git a/memory/project_activitypub.md b/memory/project_activitypub.md index 4572f465..ea7e2136 100644 --- a/memory/project_activitypub.md +++ b/memory/project_activitypub.md @@ -218,6 +218,34 @@ non-followers not stored in `ap_timeline`. Also added else-if branch in `handleCreate` to store replies to own posts in `ap_timeline` even when sender is not in `ap_following`. +### `patch-ap-interactions-cleanup-preserve` *(2026-04-10)* +**File:** `lib/timeline-cleanup.js` → `cleanupTimeline()` +**Problem:** The daily cleanup deleted remote posts from `ap_timeline` (beyond `retentionLimit`) AND +deleted their corresponding `ap_interactions` entries. Any post the user had liked, bookmarked, or +boosted would silently disappear from `GET /api/v1/favourites` and `GET /api/v1/bookmarks` after the +next cleanup run. This was intermittent — only triggered once per day. +**Fix:** Before deleting, call `ap_interactions.distinct("objectUrl")` to get all URLs the user has +interacted with. Filter those UIDs out of `toDelete` so they are never removed from `ap_timeline` +(and their `ap_interactions` entries remain intact). + +### `patch-ap-interactions-accounts-uid` *(2026-04-10)* +**File:** `lib/mastodon/routes/accounts.js` → account statuses route +**Problem:** When loading interaction state for `GET /api/v1/accounts/:id/statuses`, the code built +`lookupUrls` from both `item.uid` and `item.url`, found matching interactions, but then added +`ix.objectUrl` directly to `favouritedIds`/`rebloggedIds`/`bookmarkedIds`. `serializeStatus` checks +`favouritedIds.has(item.uid)` — so when `ix.objectUrl === item.url` and `item.url !== item.uid` (common +for remote posts), the interaction state showed wrong (not-liked even though liked). +**Fix:** Build a `urlToUid` map before the lookup and resolve `ix.objectUrl` to the canonical `item.uid` +before adding to the Sets (same approach as `loadInteractionState` in `timelines.js`). + +### `patch-ap-interactions-context-state` *(2026-04-10)* +**File:** `lib/mastodon/routes/statuses.js` → `GET /api/v1/statuses/:id/context` +**Problem:** Thread ancestors and descendants were serialized with `emptyInteractions` (all empty Sets). +Any post the user had liked or bookmarked showed as not-liked/not-bookmarked when viewing thread context. +**Fix:** Replace `emptyInteractions` with a batch `ap_interactions` lookup for all context items +(ancestors + descendants), using a `urlToUid` map to resolve to canonical UIDs. One MongoDB query +for the entire thread instead of N+1 calls. + ### `patch-ap-status-reply-id` *(updated 2026-04-09)* **Files:** `lib/mastodon/entities/status.js` + `lib/mastodon/routes/statuses.js` **Original problem (2026-04-01):** `in_reply_to_id` was tautological `item.inReplyTo ? null : null`. diff --git a/package.json b/package.json index 3dd68abb..5fe78d1f 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "index.js", "scripts": { "preinstall": "node scripts/setup-gitea-url-rewrite.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-podroll-opml-upload.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-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.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-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.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-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-signature-host-header.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs && node scripts/patch-ap-repost-announce-fix.mjs && node scripts/patch-inbox-ignore-view-activity.mjs && node scripts/patch-inbox-skip-view-activity-parse.mjs && node scripts/patch-ap-oauth-token-expiry-fix.mjs && node scripts/patch-ap-startup-gate-bypass.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.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/preflight-startup-gate.mjs && node scripts/patch-ap-startup-gate-bypass.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-podroll-opml-upload.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-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.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-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.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-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-signature-host-header.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs && node scripts/patch-ap-repost-announce-fix.mjs && node scripts/patch-inbox-ignore-view-activity.mjs && node scripts/patch-inbox-skip-view-activity-parse.mjs && node scripts/patch-ap-oauth-token-expiry-fix.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node --require ./metrics-shim.cjs 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-podroll-opml-upload.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-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.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-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.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-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-signature-host-header.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-interactions-cleanup-preserve.mjs && node scripts/patch-ap-interactions-accounts-uid.mjs && node scripts/patch-ap-interactions-context-state.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs && node scripts/patch-ap-repost-announce-fix.mjs && node scripts/patch-inbox-ignore-view-activity.mjs && node scripts/patch-inbox-skip-view-activity-parse.mjs && node scripts/patch-ap-oauth-token-expiry-fix.mjs && node scripts/patch-ap-startup-gate-bypass.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.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/preflight-startup-gate.mjs && node scripts/patch-ap-startup-gate-bypass.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-podroll-opml-upload.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-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.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-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.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-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-signature-host-header.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-interactions-cleanup-preserve.mjs && node scripts/patch-ap-interactions-accounts-uid.mjs && node scripts/patch-ap-interactions-context-state.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs && node scripts/patch-ap-repost-announce-fix.mjs && node scripts/patch-inbox-ignore-view-activity.mjs && node scripts/patch-inbox-skip-view-activity-parse.mjs && node scripts/patch-ap-oauth-token-expiry-fix.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node --require ./metrics-shim.cjs 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-interactions-accounts-uid.mjs b/scripts/patch-ap-interactions-accounts-uid.mjs new file mode 100644 index 00000000..b9fa6ba7 --- /dev/null +++ b/scripts/patch-ap-interactions-accounts-uid.mjs @@ -0,0 +1,98 @@ +/** + * Patch: fix interaction state in GET /api/v1/accounts/:id/statuses. + * + * Root cause: + * The account statuses route loads interaction state (liked/boosted/bookmarked) + * for the returned timeline items. It builds the lookup URL list correctly + * (both item.uid and item.url), but then adds ix.objectUrl directly to the + * favouritedIds/rebloggedIds/bookmarkedIds Sets. serializeStatus() checks + * favouritedIds.has(item.uid) — so when ix.objectUrl === item.url and + * item.url !== item.uid, the check fails and the post shows as not-liked. + * + * Fix: + * Build a urlToUid map (same approach as loadInteractionState in timelines.js) + * and resolve ix.objectUrl to the canonical uid before adding to the Sets. + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const MARKER = "// [patch] ap-interactions-accounts-uid"; + +const candidates = [ + "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/accounts.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/accounts.js", +]; + +const OLD_SNIPPET = ` const lookupUrls = items.flatMap((i) => [i.uid, i.url].filter(Boolean)); + if (lookupUrls.length > 0) { + const interactions = await collections.ap_interactions + .find({ objectUrl: { $in: lookupUrls } }) + .toArray(); + for (const ix of interactions) { + if (ix.type === "like") favouritedIds.add(ix.objectUrl); + else if (ix.type === "boost") rebloggedIds.add(ix.objectUrl); + else if (ix.type === "bookmark") bookmarkedIds.add(ix.objectUrl); + } + }`; + +const NEW_SNIPPET = ` const urlToUid = new Map(); ${MARKER} + for (const i of items) { + if (i.uid) { + urlToUid.set(i.uid, i.uid); + if (i.url && i.url !== i.uid) urlToUid.set(i.url, i.uid); + } + } + const lookupUrls = [...urlToUid.keys()]; + if (lookupUrls.length > 0) { + const interactions = await collections.ap_interactions + .find({ objectUrl: { $in: lookupUrls } }) + .toArray(); + for (const ix of interactions) { + const uid = urlToUid.get(ix.objectUrl) || ix.objectUrl; + if (ix.type === "like") favouritedIds.add(uid); + else if (ix.type === "boost") rebloggedIds.add(uid); + else if (ix.type === "bookmark") bookmarkedIds.add(uid); + } + }`; + +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; + + const source = await readFile(filePath, "utf8"); + + if (source.includes(MARKER)) { + console.log(`[postinstall] patch-ap-interactions-accounts-uid: already applied to ${filePath}`); + continue; + } + + if (!source.includes(OLD_SNIPPET)) { + console.warn(`[postinstall] patch-ap-interactions-accounts-uid: snippet not found in ${filePath}`); + continue; + } + + const updated = source.replace(OLD_SNIPPET, NEW_SNIPPET); + await writeFile(filePath, updated, "utf8"); + patched += 1; + console.log(`[postinstall] Applied patch-ap-interactions-accounts-uid to ${filePath}`); +} + +if (checked === 0) { + console.log("[postinstall] patch-ap-interactions-accounts-uid: no target files found"); +} else if (patched === 0) { + console.log("[postinstall] patch-ap-interactions-accounts-uid: already up to date"); +} else { + console.log(`[postinstall] patch-ap-interactions-accounts-uid: patched ${patched} file(s)`); +} diff --git a/scripts/patch-ap-interactions-context-state.mjs b/scripts/patch-ap-interactions-context-state.mjs new file mode 100644 index 00000000..6707995b --- /dev/null +++ b/scripts/patch-ap-interactions-context-state.mjs @@ -0,0 +1,104 @@ +/** + * Patch: load real interaction state for thread context ancestors/descendants. + * + * Root cause: + * GET /api/v1/statuses/:id/context serializes all ancestors and descendants + * using emptyInteractions (all empty Sets). Any post in the thread that the + * user has liked/bookmarked/boosted shows as not-liked/not-bookmarked/not-boosted. + * + * Fix: + * Replace emptyInteractions with a batch lookup against ap_interactions for + * all items in the thread (same approach as loadInteractionState in timelines.js). + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const MARKER = "// [patch] ap-interactions-context-state"; + +const candidates = [ + "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js", +]; + +const OLD_SNIPPET = ` // Serialize all items + const emptyInteractions = { + favouritedIds: new Set(), + rebloggedIds: new Set(), + bookmarkedIds: new Set(), + pinnedIds: new Set(), + }; + + const allItems = [...ancestors, ...descendants]; + const { replyIdMap, replyAccountIdMap } = await resolveReplyIds(collections.ap_timeline, allItems); + const serializeOpts = { baseUrl, ...emptyInteractions, replyIdMap, replyAccountIdMap };`; + +const NEW_SNIPPET = ` // Serialize all items + const allItems = [...ancestors, ...descendants]; + const { replyIdMap, replyAccountIdMap } = await resolveReplyIds(collections.ap_timeline, allItems); + + // Load real interaction state for thread context ${MARKER} + const ctxFavouritedIds = new Set(); + const ctxRebloggedIds = new Set(); + const ctxBookmarkedIds = new Set(); + if (allItems.length > 0 && collections.ap_interactions) { + const ctxUrlToUid = new Map(); + for (const ci of allItems) { + if (ci.uid) { ctxUrlToUid.set(ci.uid, ci.uid); } + if (ci.url && ci.url !== ci.uid) { ctxUrlToUid.set(ci.url, ci.uid || ci.url); } + } + const ctxLookupUrls = [...ctxUrlToUid.keys()]; + if (ctxLookupUrls.length > 0) { + const ctxInteractions = await collections.ap_interactions + .find({ objectUrl: { $in: ctxLookupUrls } }) + .toArray(); + for (const ci of ctxInteractions) { + const uid = ctxUrlToUid.get(ci.objectUrl) || ci.objectUrl; + if (ci.type === "like") ctxFavouritedIds.add(uid); + else if (ci.type === "boost") ctxRebloggedIds.add(uid); + else if (ci.type === "bookmark") ctxBookmarkedIds.add(uid); + } + } + } + const serializeOpts = { baseUrl, favouritedIds: ctxFavouritedIds, rebloggedIds: ctxRebloggedIds, bookmarkedIds: ctxBookmarkedIds, pinnedIds: new Set(), replyIdMap, replyAccountIdMap };`; + +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; + + const source = await readFile(filePath, "utf8"); + + if (source.includes(MARKER)) { + console.log(`[postinstall] patch-ap-interactions-context-state: already applied to ${filePath}`); + continue; + } + + if (!source.includes(OLD_SNIPPET)) { + console.warn(`[postinstall] patch-ap-interactions-context-state: snippet not found in ${filePath}`); + continue; + } + + const updated = source.replace(OLD_SNIPPET, NEW_SNIPPET); + await writeFile(filePath, updated, "utf8"); + patched += 1; + console.log(`[postinstall] Applied patch-ap-interactions-context-state to ${filePath}`); +} + +if (checked === 0) { + console.log("[postinstall] patch-ap-interactions-context-state: no target files found"); +} else if (patched === 0) { + console.log("[postinstall] patch-ap-interactions-context-state: already up to date"); +} else { + console.log(`[postinstall] patch-ap-interactions-context-state: patched ${patched} file(s)`); +}