/** * Patch: fix DELETE /api/v1/statuses/:id — two bugs. * * Bug 1 (ReferenceError — primary failure): * Line: await collections.ap_timeline.deleteOne({ _id: objectId }); * `objectId` is never defined in the route handler. MongoDB ObjectId is * imported as the class `ObjectId`, not an instance. Every delete request * throws ReferenceError → 500 → the timeline entry is never removed. * Fix: use `item._id` (the document's own _id from findTimelineItemById). * * Bug 2 (AP Delete not broadcast): * The route calls postContent.delete() directly, bypassing the Indiekit * framework that normally invokes syndicator.delete(). No Delete(Note) * activity is ever sent to followers — they keep seeing the post. * Fix: * a) Add broadcastDelete: (url) => pluginRef.broadcastDelete(url) to * mastodonPluginOptions in index.js so the router can reach it. * b) Call req.app.locals.mastodonPluginOptions.broadcastDelete(postUrl) * in the delete route after the timeline entry is removed. */ import { access, readFile, writeFile } from "node:fs/promises"; const MARKER = "// [patch] ap-mastodon-delete-fix"; const indexCandidates = [ "node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/index.js", ]; const statusesCandidates = [ "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", ]; // ── Change A: expose broadcastDelete in mastodonPluginOptions (index.js) ────── const OLD_PLUGIN_OPTS = ` loadRsaKey: () => pluginRef._loadRsaPrivateKey(), broadcastActorUpdate: () => pluginRef.broadcastActorUpdate(),`; const NEW_PLUGIN_OPTS = ` loadRsaKey: () => pluginRef._loadRsaPrivateKey(), broadcastActorUpdate: () => pluginRef.broadcastActorUpdate(), broadcastDelete: (url) => pluginRef.broadcastDelete(url), ${MARKER}`; // ── Change B: fix objectId → item._id (statuses.js) ────────────────────────── const OLD_DELETE_ONE = ` // Delete from timeline await collections.ap_timeline.deleteOne({ _id: objectId });`; const NEW_DELETE_ONE = ` // Delete from timeline await collections.ap_timeline.deleteOne({ _id: item._id }); ${MARKER}`; // ── Change C: call broadcastDelete after timeline removal (statuses.js) ─────── const OLD_AFTER_DELETE = ` // Delete from timeline await collections.ap_timeline.deleteOne({ _id: item._id }); ${MARKER} // Clean up interactions`; const NEW_AFTER_DELETE = ` // Delete from timeline await collections.ap_timeline.deleteOne({ _id: item._id }); ${MARKER} // Broadcast AP Delete activity to followers ${MARKER} const _pluginOpts = req.app.locals.mastodonPluginOptions || {}; if (_pluginOpts.broadcastDelete && postUrl) { _pluginOpts.broadcastDelete(postUrl).catch((err) => console.warn(\`[Mastodon API] broadcastDelete failed for \${postUrl}: \${err.message}\`), ); } // Clean up interactions`; async function exists(p) { try { await access(p); return true; } catch { return false; } } async function patchFile(filePath, replacements) { const source = await readFile(filePath, "utf8"); if (source.includes(MARKER)) { console.log(`[postinstall] patch-ap-mastodon-delete-fix: already applied to ${filePath}`); return false; } let updated = source; let applied = 0; for (const { old: oldSnippet, newSnippet, label } of replacements) { if (!updated.includes(oldSnippet)) { console.warn(`[postinstall] patch-ap-mastodon-delete-fix: snippet "${label}" not found in ${filePath}`); continue; } updated = updated.replace(oldSnippet, newSnippet); applied++; } if (applied === 0) return false; await writeFile(filePath, updated, "utf8"); console.log(`[postinstall] Applied patch-ap-mastodon-delete-fix to ${filePath} (${applied} change(s))`); return true; } let totalPatched = 0; let totalChecked = 0; // Patch index.js candidates (Change A) for (const filePath of indexCandidates) { if (!(await exists(filePath))) continue; totalChecked++; const ok = await patchFile(filePath, [ { old: OLD_PLUGIN_OPTS, newSnippet: NEW_PLUGIN_OPTS, label: "broadcastDelete in pluginOptions" }, ]); if (ok) totalPatched++; } // Patch statuses.js candidates (Changes B + C together, in sequence) for (const filePath of statusesCandidates) { if (!(await exists(filePath))) continue; totalChecked++; const source = await readFile(filePath, "utf8"); if (source.includes(MARKER)) { console.log(`[postinstall] patch-ap-mastodon-delete-fix: already applied to ${filePath}`); continue; } // Apply B first, then C (C depends on B's output) let updated = source; let applied = 0; if (!updated.includes(OLD_DELETE_ONE)) { console.warn(`[postinstall] patch-ap-mastodon-delete-fix: "objectId fix" snippet not found in ${filePath}`); } else { updated = updated.replace(OLD_DELETE_ONE, NEW_DELETE_ONE); applied++; } if (!updated.includes(OLD_AFTER_DELETE)) { console.warn(`[postinstall] patch-ap-mastodon-delete-fix: "broadcastDelete call" snippet not found in ${filePath}`); } else { updated = updated.replace(OLD_AFTER_DELETE, NEW_AFTER_DELETE); applied++; } if (applied === 0) continue; await writeFile(filePath, updated, "utf8"); console.log(`[postinstall] Applied patch-ap-mastodon-delete-fix to ${filePath} (${applied}/2 change(s))`); totalPatched++; } if (totalChecked === 0) { console.log("[postinstall] patch-ap-mastodon-delete-fix: no target files found"); } else if (totalPatched === 0) { console.log("[postinstall] patch-ap-mastodon-delete-fix: already up to date"); } else { console.log(`[postinstall] patch-ap-mastodon-delete-fix: patched ${totalPatched} file(s)`); }