diff --git a/scripts/patch-ap-interactions-cleanup-preserve.mjs b/scripts/patch-ap-interactions-cleanup-preserve.mjs new file mode 100644 index 00000000..590d3e52 --- /dev/null +++ b/scripts/patch-ap-interactions-cleanup-preserve.mjs @@ -0,0 +1,112 @@ +/** + * Patch: preserve liked/bookmarked/boosted items during timeline cleanup. + * + * Root cause: + * cleanupTimeline() removes old remote posts from ap_timeline and also + * deletes their ap_interactions entries. Any post the user has liked, + * bookmarked, or boosted disappears from GET /api/v1/favourites and + * GET /api/v1/bookmarks after the next daily cleanup run. + * + * Fix: + * Before deleting, fetch the set of objectUrls that have ap_interactions + * entries (likes, bookmarks, boosts). Filter those out of the deletion + * candidate list so they are preserved in ap_timeline (and their + * ap_interactions entries are never touched). + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const MARKER = "// [patch] ap-interactions-cleanup-preserve"; + +const candidates = [ + "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/timeline-cleanup.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/timeline-cleanup.js", +]; + +const OLD_SNIPPET = ` const removedUids = toDelete.map((item) => item.uid).filter(Boolean); + + // Delete old timeline items by UID + const deleteResult = await collections.ap_timeline.deleteMany({ + _id: { $in: toDelete.map((item) => item._id) }, + }); + + // Clean up stale interactions for removed items + let interactionsRemoved = 0; + if (removedUids.length > 0 && collections.ap_interactions) { + const interactionResult = await collections.ap_interactions.deleteMany({ + objectUrl: { $in: removedUids }, + }); + interactionsRemoved = interactionResult.deletedCount || 0; + }`; + +const NEW_SNIPPET = ` const removedUids = toDelete.map((item) => item.uid).filter(Boolean); + + // Preserve items the user has interacted with (liked, bookmarked, boosted). ${MARKER} + // Deleting them would silently remove entries from the Favourites/Bookmarks pages. + let interactedUids = new Set(); + if (removedUids.length > 0 && collections.ap_interactions) { + const interacted = await collections.ap_interactions.distinct("objectUrl"); + interactedUids = new Set(interacted); + } + const itemsToDelete = toDelete.filter((item) => !interactedUids.has(item.uid)); + const uidsToDelete = itemsToDelete.map((item) => item.uid).filter(Boolean); + + if (!itemsToDelete.length) { + return { removed: 0, interactionsRemoved: 0 }; + } + + // Delete old timeline items by UID + const deleteResult = await collections.ap_timeline.deleteMany({ + _id: { $in: itemsToDelete.map((item) => item._id) }, + }); + + // Clean up stale interactions for removed items + let interactionsRemoved = 0; + if (uidsToDelete.length > 0 && collections.ap_interactions) { + const interactionResult = await collections.ap_interactions.deleteMany({ + objectUrl: { $in: uidsToDelete }, + }); + interactionsRemoved = interactionResult.deletedCount || 0; + }`; + +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-cleanup-preserve: already applied to ${filePath}`); + continue; + } + + if (!source.includes(OLD_SNIPPET)) { + console.warn(`[postinstall] patch-ap-interactions-cleanup-preserve: 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-cleanup-preserve to ${filePath}`); +} + +if (checked === 0) { + console.log("[postinstall] patch-ap-interactions-cleanup-preserve: no target files found"); +} else if (patched === 0) { + console.log("[postinstall] patch-ap-interactions-cleanup-preserve: already up to date"); +} else { + console.log(`[postinstall] patch-ap-interactions-cleanup-preserve: patched ${patched} file(s)`); +}