diff --git a/scripts/patch-inbox-skip-view-activity-parse.mjs b/scripts/patch-inbox-skip-view-activity-parse.mjs index 2fd218f6..748dce28 100644 --- a/scripts/patch-inbox-skip-view-activity-parse.mjs +++ b/scripts/patch-inbox-skip-view-activity-parse.mjs @@ -7,10 +7,23 @@ * listener is reached, so the .on(View, ...) no-op handler added earlier * never fires. * - * Fix: in createFedifyMiddleware (federation-bridge.js), add an early-return - * guard that checks req.body.type === "View" and responds 200 immediately, - * so Fedify never attempts to parse the activity. Returning 200 (rather than - * 4xx) prevents the sending server from retrying. + * Root cause of the previous (broken) patch: Express's JSON body parser only + * handles `application/json`, not `application/activity+json`. So `req.body` + * is always undefined for ActivityPub inbox POSTs, meaning the check + * `req.body?.type === "View"` never matched and Fedify still received the raw + * stream. + * + * Fix (two changes to federation-bridge.js): + * + * 1. In createFedifyMiddleware: for ActivityPub POST requests where the body + * hasn't been parsed yet, buffer the raw stream, JSON-parse it, and store + * the result on req.body before the guard runs. Then check type === "View" + * and return 200 if so (preventing retries from the sender). + * + * 2. In fromExpressRequest: extend the content-type check to also handle + * `application/activity+json` and `application/ld+json` bodies (i.e. use + * JSON.stringify(req.body) to reconstruct the stream), so that non-View + * ActivityPub activities are forwarded correctly to Fedify. */ import { access, readFile, writeFile } from "node:fs/promises"; @@ -21,9 +34,67 @@ const candidates = [ ]; const patchSpecs = [ + // --- Patch 1: extend fromExpressRequest to handle activity+json bodies --- { - name: "inbox-skip-view-activity-parse", - marker: "// PeerTube View parse skip", + name: "from-express-request-activity-json-fix", + marker: "// PeerTube activity+json body fix", + oldSnippet: ` if (ct.includes("application/json")) { + body = JSON.stringify(req.body); + } else if (ct.includes("application/x-www-form-urlencoded")) {`, + newSnippet: ` // PeerTube activity+json body fix + if (ct.includes("application/json") || ct.includes("activity+json") || ct.includes("ld+json")) { + body = JSON.stringify(req.body); + } else if (ct.includes("application/x-www-form-urlencoded")) {`, + }, + + // --- Patch 2a: replace the old (broken) v1 guard with the buffering v2 guard --- + // Handles the case where the previous version of this script was already run. + { + name: "inbox-skip-view-activity-parse-v2", + marker: "// PeerTube View parse skip v2", + oldSnippet: ` // Short-circuit PeerTube View (WatchAction) activities before Fedify + // attempts JSON-LD parsing. Fedify's vocab parser throws on PeerTube's + // Schema.org extensions (e.g. InteractionCounter), causing a + // "Failed to parse activity" error. Return 200 to prevent retries. + // PeerTube View parse skip + if (req.method === "POST" && req.body?.type === "View") { + return res.status(200).end(); + } + const request = fromExpressRequest(req);`, + newSnippet: ` // Short-circuit PeerTube View (WatchAction) activities before Fedify + // attempts JSON-LD parsing. Fedify's vocab parser throws on PeerTube's + // Schema.org extensions (e.g. InteractionCounter), causing a + // "Failed to parse activity" error. Return 200 to prevent retries. + // PeerTube View parse skip v2 + const _apct = req.headers["content-type"] || ""; + if ( + req.method === "POST" && + !req.body && + req.readable && + (_apct.includes("activity+json") || _apct.includes("ld+json")) + ) { + // Express doesn't parse application/activity+json, so buffer it ourselves. + const _chunks = []; + for await (const _chunk of req) { + _chunks.push(Buffer.isBuffer(_chunk) ? _chunk : Buffer.from(_chunk)); + } + try { + req.body = JSON.parse(Buffer.concat(_chunks).toString("utf8")); + } catch { + req.body = {}; + } + } + if (req.method === "POST" && req.body?.type === "View") { + return res.status(200).end(); + } + const request = fromExpressRequest(req);`, + }, + + // --- Patch 2b: apply the buffering v2 guard on a fresh (unpatched) file --- + // Handles the case where neither v1 nor v2 patch has been applied yet. + { + name: "inbox-skip-view-activity-parse-v2-fresh", + marker: "// PeerTube View parse skip v2", oldSnippet: ` return async (req, res, next) => { try { const request = fromExpressRequest(req);`, @@ -33,7 +104,25 @@ const patchSpecs = [ // attempts JSON-LD parsing. Fedify's vocab parser throws on PeerTube's // Schema.org extensions (e.g. InteractionCounter), causing a // "Failed to parse activity" error. Return 200 to prevent retries. - // PeerTube View parse skip + // PeerTube View parse skip v2 + const _apct = req.headers["content-type"] || ""; + if ( + req.method === "POST" && + !req.body && + req.readable && + (_apct.includes("activity+json") || _apct.includes("ld+json")) + ) { + // Express doesn't parse application/activity+json, so buffer it ourselves. + const _chunks = []; + for await (const _chunk of req) { + _chunks.push(Buffer.isBuffer(_chunk) ? _chunk : Buffer.from(_chunk)); + } + try { + req.body = JSON.parse(Buffer.concat(_chunks).toString("utf8")); + } catch { + req.body = {}; + } + } if (req.method === "POST" && req.body?.type === "View") { return res.status(200).end(); } @@ -82,6 +171,7 @@ for (const spec of patchSpecs) { await writeFile(filePath, updated, "utf8"); patchedFiles.add(filePath); + console.log(`[postinstall] Applied ${spec.name} to ${filePath}`); } if (!foundAnyTarget) { @@ -92,7 +182,7 @@ for (const spec of patchSpecs) { if (checkedFiles.size === 0) { console.log("[postinstall] No federation-bridge files found for View activity parse-skip patch"); } else if (patchedFiles.size === 0) { - console.log("[postinstall] inbox-skip-view-activity-parse patch already applied"); + console.log("[postinstall] inbox-skip-view-activity-parse patch already up to date"); } else { console.log( `[postinstall] Patched inbox-skip-view-activity-parse in ${patchedFiles.size}/${checkedFiles.size} file(s)`,