Files
indiekit-server/scripts/patch-ap-like-activity-dispatcher.mjs
Sven 535e6f5e9c fix(activitypub): add Like vocab import in activity dispatcher patch
On fresh installs where the old wrong patch was never applied, Like was
absent from the @fedify/fedify/vocab imports, causing a ReferenceError
at startup. The dispatcher patch now adds Like to the import block if
missing, making it self-contained and install-order independent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:05:34 +01:00

114 lines
3.7 KiB
JavaScript

/**
* Patch: register a Fedify Like activity dispatcher in federation-setup.js.
*
* Per ActivityPub §3.1, objects with an `id` MUST be dereferenceable at that
* URI. The Like activities produced by jf2ToAS2Activity (after patch-ap-like-
* activity-id.mjs adds an id) need a corresponding Fedify object dispatcher so
* that fetching /activitypub/activities/like/{id} returns the Like activity.
*
* Fix:
* Add federation.setObjectDispatcher(Like, ...) after the Article dispatcher
* in setupObjectDispatchers(). The handler looks up the post, calls
* jf2ToAS2Activity, and returns the Like if that's what was produced.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"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 MARKER = "// ap-like-activity-dispatcher patch";
const OLD_SNIPPET = ` // Article dispatcher
federation.setObjectDispatcher(
Article,
\`\${mountPath}/objects/article/{+id}\`,
async (ctx, { id }) => {
const obj = await resolvePost(ctx, id);
return obj instanceof Article ? obj : null;
},
);
}`;
const NEW_SNIPPET = ` // Article dispatcher
federation.setObjectDispatcher(
Article,
\`\${mountPath}/objects/article/{+id}\`,
async (ctx, { id }) => {
const obj = await resolvePost(ctx, id);
return obj instanceof Article ? obj : null;
},
);
// Like activity dispatcher — makes AP-like activities dereferenceable (AP §3.1)
// ap-like-activity-dispatcher patch
federation.setObjectDispatcher(
Like,
\`\${mountPath}/activities/like/{+id}\`,
async (ctx, { id }) => {
if (!collections.posts || !publicationUrl) return null;
const postUrl = \`\${publicationUrl.replace(/\\/$/, "")}/\${id}\`;
const post = await collections.posts.findOne({
"properties.url": { $in: [postUrl, postUrl + "/"] },
});
if (!post) return null;
if (post?.properties?.["post-status"] === "draft") return null;
if (post?.properties?.visibility === "unlisted") return null;
if (post.properties?.deleted) return null;
const actorUrl = ctx.getActorUri(handle).href;
const activity = await jf2ToAS2Activity(post.properties, actorUrl, publicationUrl);
return activity instanceof Like ? activity : null;
},
);
}`;
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)) {
continue; // already patched
}
if (!source.includes(OLD_SNIPPET)) {
console.log(`[postinstall] patch-ap-like-activity-dispatcher: snippet not found in ${filePath}`);
continue;
}
// Ensure Like is imported from @fedify/fedify/vocab (may be absent on fresh installs)
if (!source.includes(" Like,")) {
source = source.replace(" Note,", " Like,\n Note,");
}
source = source.replace(OLD_SNIPPET, NEW_SNIPPET);
await writeFile(filePath, source, "utf8");
patched += 1;
console.log(`[postinstall] Applied patch-ap-like-activity-dispatcher to ${filePath}`);
}
if (checked === 0) {
console.log("[postinstall] patch-ap-like-activity-dispatcher: no target files found");
} else if (patched === 0) {
console.log("[postinstall] patch-ap-like-activity-dispatcher: already up to date");
} else {
console.log(`[postinstall] patch-ap-like-activity-dispatcher: patched ${patched}/${checked} file(s)`);
}