feat: consolidated patch-ap-mastodon-misc (compose, og-image, interactions, inbox)
Absorbs patch-ap-compose-default-checked, patch-ap-og-image, patch-ap-repost-announce-fix, patch-ap-interactions-send-guard, patch-ap-interactions-cleanup-preserve, patch-ap-interactions-accounts-uid, patch-inbox-ignore-view-activity, and patch-inbox-skip-view-activity-parse into a single script using the shared applyPatch helper pattern. Added inbox-ignore-view-activity-handler-v2 variant to handle the evolved upstream inbox-listeners.js that uses enqueueActivity instead of try/catch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,571 @@
|
|||||||
|
/**
|
||||||
|
* Consolidated patch: AP compose, OG image, repost announce, interactions
|
||||||
|
* send guard, interactions cleanup preserve, interactions accounts uid,
|
||||||
|
* inbox ignore View activity, inbox skip View activity parse.
|
||||||
|
*
|
||||||
|
* Absorbs:
|
||||||
|
* - patch-ap-compose-default-checked
|
||||||
|
* - patch-ap-og-image
|
||||||
|
* - patch-ap-repost-announce-fix
|
||||||
|
* - patch-ap-interactions-send-guard
|
||||||
|
* - patch-ap-interactions-cleanup-preserve
|
||||||
|
* - patch-ap-interactions-accounts-uid
|
||||||
|
* - patch-inbox-ignore-view-activity
|
||||||
|
* - patch-inbox-skip-view-activity-parse
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { access, readFile, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
const AP_BASE = "@rmdes/indiekit-endpoint-activitypub";
|
||||||
|
const AP_ROOTS = [
|
||||||
|
`node_modules/${AP_BASE}`,
|
||||||
|
`node_modules/@indiekit/indiekit/node_modules/${AP_BASE}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
function apPath(rel) {
|
||||||
|
return AP_ROOTS.map(r => `${r}/${rel}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileExists(p) {
|
||||||
|
try { await access(p); return true; } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyPatch(filePath, marker, oldSnippet, newSnippet) {
|
||||||
|
if (!(await fileExists(filePath))) return "file_not_found";
|
||||||
|
const src = await readFile(filePath, "utf8");
|
||||||
|
if (src.includes(marker)) return "already_applied";
|
||||||
|
if (!src.includes(oldSnippet)) return "snippet_not_found";
|
||||||
|
await writeFile(filePath, src.replace(oldSnippet, newSnippet), "utf8");
|
||||||
|
return "applied";
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyRegexPatch: like applyPatch but uses a regex instead of a literal string match.
|
||||||
|
// oldRegex must be a RegExp (tested + replaced against src).
|
||||||
|
async function applyRegexPatch(filePath, marker, oldRegex, newSnippet) {
|
||||||
|
if (!(await fileExists(filePath))) return "file_not_found";
|
||||||
|
const src = await readFile(filePath, "utf8");
|
||||||
|
if (src.includes(marker)) return "already_applied";
|
||||||
|
if (!oldRegex.test(src)) return "snippet_not_found";
|
||||||
|
await writeFile(filePath, src.replace(oldRegex, newSnippet), "utf8");
|
||||||
|
return "applied";
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCRIPT = "patch-ap-mastodon-misc";
|
||||||
|
|
||||||
|
// ── patch-ap-og-image: regex-based, two replacements per file ─────────────────
|
||||||
|
// This patch is handled separately below the main loop because it uses regexes
|
||||||
|
// and applies two independent replacements to the same file.
|
||||||
|
|
||||||
|
const OG_MARKER = "// og-image-v2";
|
||||||
|
const OG_CANDIDATES = apPath("lib/jf2-to-as2.js");
|
||||||
|
|
||||||
|
const CN_BLOCK_RE =
|
||||||
|
/ const og(?:Slug|Match) = postUrl && postUrl\.match\([^\n]+\n if \(og(?:Slug|Match)\) \{[\s\S]*?\n \}/;
|
||||||
|
|
||||||
|
const AS2_BLOCK_RE =
|
||||||
|
/ const og(?:SlugF|MatchF) = postUrl && postUrl\.match\([^\n]+\n if \(og(?:SlugF|MatchF)\) \{[\s\S]*?\n \}/;
|
||||||
|
|
||||||
|
const NEW_CN = ` const _ogPhoto = properties.photo && asArray(properties.photo)[0]; // og-image-v2
|
||||||
|
const _ogPhotoUrl = _ogPhoto && (typeof _ogPhoto === "string" ? _ogPhoto : _ogPhoto.url); // og-image-v2
|
||||||
|
const ogSlug = postUrl && postUrl.match(/\\/([\\\w-]+)\\/?$/)?.[1]; // og-image-v2
|
||||||
|
const _ogUrl = _ogPhotoUrl
|
||||||
|
? resolveMediaUrl(_ogPhotoUrl, publicationUrl) // og-image-v2
|
||||||
|
: ogSlug ? \`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogSlug}.png\` : null; // og-image-v2
|
||||||
|
if (_ogUrl) { // og-image-v2
|
||||||
|
object.image = {
|
||||||
|
type: "Image",
|
||||||
|
url: _ogUrl, // og-image-v2
|
||||||
|
mediaType: _ogPhotoUrl ? guessMediaType(_ogUrl) : "image/png", // og-image-v2
|
||||||
|
};
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const NEW_AS2 = ` const _ogPhotoF = properties.photo && asArray(properties.photo)[0]; // og-image-v2
|
||||||
|
const _ogPhotoUrlF = _ogPhotoF && (typeof _ogPhotoF === "string" ? _ogPhotoF : _ogPhotoF.url); // og-image-v2
|
||||||
|
const ogSlugF = postUrl && postUrl.match(/\\/([\\\w-]+)\\/?$/)?.[1]; // og-image-v2
|
||||||
|
const _ogUrlF = _ogPhotoUrlF
|
||||||
|
? resolveMediaUrl(_ogPhotoUrlF, publicationUrl) // og-image-v2
|
||||||
|
: ogSlugF ? \`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogSlugF}.png\` : null; // og-image-v2
|
||||||
|
if (_ogUrlF) { // og-image-v2
|
||||||
|
noteOptions.image = new Image({
|
||||||
|
url: new URL(_ogUrlF), // og-image-v2
|
||||||
|
mediaType: _ogPhotoUrlF ? guessMediaType(_ogUrlF) : "image/png", // og-image-v2
|
||||||
|
});
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// ── Standard patches (string-replace) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
const PATCHES = [
|
||||||
|
// ── patch-ap-compose-default-checked ──────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: "ap-compose-default-checked",
|
||||||
|
files: [
|
||||||
|
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/controllers/compose.js",
|
||||||
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/controllers/compose.js",
|
||||||
|
],
|
||||||
|
marker: "// [patch] ap-compose-default-checked",
|
||||||
|
oldSnippet: ` // Default-check only AP (Fedify) and Bluesky targets
|
||||||
|
// "@rick@rmendes.net" = AP Fedify, "@rmendes.net" = Bluesky
|
||||||
|
for (const target of syndicationTargets) {
|
||||||
|
const name = target.name || "";
|
||||||
|
target.defaultChecked = name === "@rick@rmendes.net" || name === "@rmendes.net";
|
||||||
|
}`,
|
||||||
|
newSnippet: ` // Pre-check syndication targets based on their configured checked state // [patch] ap-compose-default-checked
|
||||||
|
for (const target of syndicationTargets) { // [patch] ap-compose-default-checked
|
||||||
|
target.defaultChecked = target.checked === true; // [patch] ap-compose-default-checked
|
||||||
|
} // [patch] ap-compose-default-checked`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-ap-repost-announce-fix ──────────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: "ap-repost-announce-fix",
|
||||||
|
files: apPath("lib/jf2-to-as2.js"),
|
||||||
|
marker: "// [patch] ap-repost-announce-fix",
|
||||||
|
oldSnippet: ` // Reposts are always public — upstream @rmdes addressing
|
||||||
|
if (postType === "repost") {
|
||||||
|
const repostOf = properties["repost-of"];
|
||||||
|
if (!repostOf) return null;
|
||||||
|
const repostContent = properties.content?.html || properties.content || "";
|
||||||
|
if (!repostContent) {
|
||||||
|
// Pure repost — send as a native Announce (boost) so remote servers
|
||||||
|
// can display it as a boost of the original post.
|
||||||
|
return new Announce({
|
||||||
|
actor: actorUri,
|
||||||
|
object: new URL(repostOf),
|
||||||
|
to: new URL("https://www.w3.org/ns/activitystreams#Public"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Has commentary — fall through to Create(Note) so the text is federated.
|
||||||
|
// The note content block below handles the "repost" post-type.
|
||||||
|
}`,
|
||||||
|
newSnippet: ` // Reposts are always public — upstream @rmdes addressing
|
||||||
|
if (postType === "repost") {
|
||||||
|
const repostOf = Array.isArray(properties["repost-of"])
|
||||||
|
? properties["repost-of"][0]
|
||||||
|
: properties["repost-of"];
|
||||||
|
if (!repostOf) return null;
|
||||||
|
const repostContent = properties.content?.html || properties.content || "";
|
||||||
|
if (!repostContent) {
|
||||||
|
// Only send Announce if repost-of is an ActivityPub URL.
|
||||||
|
// Non-AP URLs (web articles) cannot be federated as a boost — fall
|
||||||
|
// through to Create(Note) which renders as "🔁 <link>" on the fediverse.
|
||||||
|
if (await isApUrl(repostOf)) { // [patch] ap-repost-announce-fix
|
||||||
|
const actorPath = new URL(actorUrl).pathname;
|
||||||
|
const mp = actorPath.replace(/\\/users\\/[^/]+$/, "");
|
||||||
|
const postRelPath = (properties.url || "")
|
||||||
|
.replace(publicationUrl.replace(/\\/$/, ""), "")
|
||||||
|
.replace(/^\\//, "")
|
||||||
|
.replace(/\\/$/, "");
|
||||||
|
const announceId = \`\${publicationUrl.replace(/\\/$/, "")}\${mp}/activities/boost/\${postRelPath}\`;
|
||||||
|
return new Announce({
|
||||||
|
id: new URL(announceId),
|
||||||
|
actor: actorUri,
|
||||||
|
object: new URL(repostOf),
|
||||||
|
to: new URL("https://www.w3.org/ns/activitystreams#Public"),
|
||||||
|
cc: new URL(\`\${actorUrl.replace(/\\/$/, "")}/followers\`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Has commentary or non-AP repost-of URL — fall through to Create(Note) so the text is federated.
|
||||||
|
// The note content block below handles the "repost" post-type.
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-ap-interactions-send-guard: likePost ────────────────────────────
|
||||||
|
{
|
||||||
|
name: "ap-interactions-send-guard-like",
|
||||||
|
files: [
|
||||||
|
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/interactions.js",
|
||||||
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/interactions.js",
|
||||||
|
],
|
||||||
|
marker: "// [patch] ap-interactions-send-guard",
|
||||||
|
oldSnippet: ` if (recipient) {
|
||||||
|
await ctx.sendActivity({ identifier: handle }, recipient, like, {
|
||||||
|
orderingKey: targetUrl,
|
||||||
|
});
|
||||||
|
}`,
|
||||||
|
newSnippet: ` if (recipient) {
|
||||||
|
try { // [patch] ap-interactions-send-guard
|
||||||
|
await ctx.sendActivity({ identifier: handle }, recipient, like, {
|
||||||
|
orderingKey: targetUrl,
|
||||||
|
});
|
||||||
|
} catch { /* delivery failed — interaction still recorded locally */ }
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-ap-interactions-send-guard: boostPost ───────────────────────────
|
||||||
|
{
|
||||||
|
name: "ap-interactions-send-guard-boost",
|
||||||
|
files: [
|
||||||
|
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/interactions.js",
|
||||||
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/helpers/interactions.js",
|
||||||
|
],
|
||||||
|
marker: "// [patch] ap-interactions-send-guard",
|
||||||
|
oldSnippet: ` // Send to followers
|
||||||
|
await ctx.sendActivity({ identifier: handle }, "followers", announce, {
|
||||||
|
preferSharedInbox: true,
|
||||||
|
syncCollection: true,
|
||||||
|
orderingKey: targetUrl,
|
||||||
|
});`,
|
||||||
|
newSnippet: ` // Send to followers
|
||||||
|
try { // [patch] ap-interactions-send-guard
|
||||||
|
await ctx.sendActivity({ identifier: handle }, "followers", announce, {
|
||||||
|
preferSharedInbox: true,
|
||||||
|
syncCollection: true,
|
||||||
|
orderingKey: targetUrl,
|
||||||
|
});
|
||||||
|
} catch { /* delivery failed — interaction still recorded locally */ }`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-ap-interactions-cleanup-preserve ────────────────────────────────
|
||||||
|
{
|
||||||
|
name: "ap-interactions-cleanup-preserve",
|
||||||
|
files: apPath("lib/timeline-cleanup.js"),
|
||||||
|
marker: "// [patch] ap-interactions-cleanup-preserve",
|
||||||
|
oldSnippet: ` 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;
|
||||||
|
}`,
|
||||||
|
newSnippet: ` const removedUids = toDelete.map((item) => item.uid).filter(Boolean);
|
||||||
|
|
||||||
|
// Preserve items the user has interacted with (liked, bookmarked, boosted). // [patch] ap-interactions-cleanup-preserve
|
||||||
|
// 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;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-ap-interactions-accounts-uid ───────────────────────────────────
|
||||||
|
{
|
||||||
|
name: "ap-interactions-accounts-uid",
|
||||||
|
files: [
|
||||||
|
"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",
|
||||||
|
],
|
||||||
|
marker: "// [patch] ap-interactions-accounts-uid",
|
||||||
|
oldSnippet: ` 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);
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
newSnippet: ` const urlToUid = new Map(); // [patch] ap-interactions-accounts-uid
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-inbox-ignore-view-activity: import ──────────────────────────────
|
||||||
|
{
|
||||||
|
name: "inbox-ignore-view-activity-import",
|
||||||
|
files: apPath("lib/inbox-listeners.js"),
|
||||||
|
marker: "// View imported",
|
||||||
|
oldSnippet: ` Undo,
|
||||||
|
Update,
|
||||||
|
} from "@fedify/fedify/vocab";`,
|
||||||
|
newSnippet: ` Undo,
|
||||||
|
Update,
|
||||||
|
View, // View imported
|
||||||
|
} from "@fedify/fedify/vocab";`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-inbox-ignore-view-activity: handler ────────────────────────────
|
||||||
|
// Two variants: one for files with the console.warn block (original upstream),
|
||||||
|
// one for files with the simpler enqueueActivity-only structure (evolved upstream).
|
||||||
|
{
|
||||||
|
name: "inbox-ignore-view-activity-handler",
|
||||||
|
files: apPath("lib/inbox-listeners.js"),
|
||||||
|
marker: "// PeerTube View handler",
|
||||||
|
oldSnippet: ` console.info(\`[ActivityPub] Flag received from \${reporterName} — \${reportedIds.length} objects reported\`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ActivityPub] Flag handler error:", error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}`,
|
||||||
|
newSnippet: ` console.info(\`[ActivityPub] Flag received from \${reporterName} — \${reportedIds.length} objects reported\`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ActivityPub] Flag handler error:", error.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// ── View (PeerTube watch) ─────────────────────────────────────────────
|
||||||
|
// PeerTube broadcasts View (WatchAction) activities to all followers
|
||||||
|
// whenever someone watches a video. Fedify has no built-in handler for
|
||||||
|
// this type, producing noisy "Unsupported activity type" log errors.
|
||||||
|
// Silently accept and discard. // PeerTube View handler
|
||||||
|
.on(View, async () => {});
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inbox-ignore-view-activity-handler-v2",
|
||||||
|
files: apPath("lib/inbox-listeners.js"),
|
||||||
|
marker: "// PeerTube View handler",
|
||||||
|
oldSnippet: ` await enqueueActivity(collections, {
|
||||||
|
activityType: "Flag",
|
||||||
|
actorUrl,
|
||||||
|
rawJson: await flag.toJsonLd(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}`,
|
||||||
|
newSnippet: ` await enqueueActivity(collections, {
|
||||||
|
activityType: "Flag",
|
||||||
|
actorUrl,
|
||||||
|
rawJson: await flag.toJsonLd(),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// ── View (PeerTube watch) ─────────────────────────────────────────────
|
||||||
|
// PeerTube broadcasts View (WatchAction) activities to all followers
|
||||||
|
// whenever someone watches a video. Fedify has no built-in handler for
|
||||||
|
// this type, producing noisy "Unsupported activity type" log errors.
|
||||||
|
// Silently accept and discard. // PeerTube View handler
|
||||||
|
.on(View, async () => {});
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-inbox-skip-view-activity-parse: fromExpressRequest fix ──────────
|
||||||
|
{
|
||||||
|
name: "from-express-request-activity-json-fix",
|
||||||
|
files: apPath("lib/federation-bridge.js"),
|
||||||
|
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")) {
|
||||||
|
// Use original raw bytes when available (set by createFedifyMiddleware buffer guard).
|
||||||
|
// JSON.stringify() changes byte layout, breaking Fedify's HTTP Signature Digest check.
|
||||||
|
body = req._rawBody || JSON.stringify(req.body); // raw body digest fix
|
||||||
|
} else if (ct.includes("application/x-www-form-urlencoded")) {`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-inbox-skip-view-activity-parse: upgrade v1 raw-body fix ─────────
|
||||||
|
{
|
||||||
|
name: "from-express-request-raw-body-fix",
|
||||||
|
files: apPath("lib/federation-bridge.js"),
|
||||||
|
marker: "req._rawBody || JSON.stringify",
|
||||||
|
oldSnippet: ` // 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")) {`,
|
||||||
|
newSnippet: ` // PeerTube activity+json body fix
|
||||||
|
if (ct.includes("application/json") || ct.includes("activity+json") || ct.includes("ld+json")) {
|
||||||
|
// Use original raw bytes when available (set by createFedifyMiddleware buffer guard).
|
||||||
|
// JSON.stringify() changes byte layout, breaking Fedify's HTTP Signature Digest check.
|
||||||
|
body = req._rawBody || JSON.stringify(req.body); // raw body digest fix
|
||||||
|
} else if (ct.includes("application/x-www-form-urlencoded")) {`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-inbox-skip-view-activity-parse: middleware buffer _rawBody fix ──
|
||||||
|
{
|
||||||
|
name: "inbox-buffer-raw-body-fix",
|
||||||
|
files: apPath("lib/federation-bridge.js"),
|
||||||
|
marker: "req._rawBody = _raw",
|
||||||
|
oldSnippet: ` 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"));`,
|
||||||
|
newSnippet: ` const _chunks = [];
|
||||||
|
for await (const _chunk of req) {
|
||||||
|
_chunks.push(Buffer.isBuffer(_chunk) ? _chunk : Buffer.from(_chunk));
|
||||||
|
}
|
||||||
|
const _raw = Buffer.concat(_chunks); // raw body digest fix
|
||||||
|
req._rawBody = _raw; // Preserve original bytes for Fedify HTTP Signature Digest verification
|
||||||
|
try {
|
||||||
|
req.body = JSON.parse(_raw.toString("utf8"));`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-inbox-skip-view-activity-parse: v2 guard replacing v1 ───────────
|
||||||
|
{
|
||||||
|
name: "inbox-skip-view-activity-parse-v2",
|
||||||
|
files: apPath("lib/federation-bridge.js"),
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
const _raw = Buffer.concat(_chunks); // raw body digest fix
|
||||||
|
req._rawBody = _raw; // Preserve original bytes for Fedify HTTP Signature Digest verification
|
||||||
|
try {
|
||||||
|
req.body = JSON.parse(_raw.toString("utf8"));
|
||||||
|
} catch {
|
||||||
|
req.body = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (req.method === "POST" && req.body?.type === "View") {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
const request = fromExpressRequest(req);`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── patch-inbox-skip-view-activity-parse: v2 guard on fresh file ──────────
|
||||||
|
{
|
||||||
|
name: "inbox-skip-view-activity-parse-v2-fresh",
|
||||||
|
files: apPath("lib/federation-bridge.js"),
|
||||||
|
marker: "// PeerTube View parse skip v2",
|
||||||
|
oldSnippet: ` return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const request = fromExpressRequest(req);`,
|
||||||
|
newSnippet: ` return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
const _raw = Buffer.concat(_chunks); // raw body digest fix
|
||||||
|
req._rawBody = _raw; // Preserve original bytes for Fedify HTTP Signature Digest verification
|
||||||
|
try {
|
||||||
|
req.body = JSON.parse(_raw.toString("utf8"));
|
||||||
|
} catch {
|
||||||
|
req.body = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (req.method === "POST" && req.body?.type === "View") {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
const request = fromExpressRequest(req);`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Run standard patches ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
for (const p of PATCHES) {
|
||||||
|
let done = false;
|
||||||
|
for (const f of p.files) {
|
||||||
|
const r = await applyPatch(f, p.marker, p.oldSnippet, p.newSnippet);
|
||||||
|
if (r === "applied") {
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: applied ${p.name} to ${f}`);
|
||||||
|
total++; done = true; break;
|
||||||
|
} else if (r === "already_applied") {
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: ${p.name} already applied in ${f}`);
|
||||||
|
done = true; break;
|
||||||
|
} else if (r === "snippet_not_found") {
|
||||||
|
console.warn(`[postinstall] ${SCRIPT}: ${p.name} — snippet not found in ${f}, skipping`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!done) console.log(`[postinstall] ${SCRIPT}: ${p.name} — no target file found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Run og-image patch (regex-based, two replacements per file) ───────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
let ogDone = false;
|
||||||
|
for (const f of OG_CANDIDATES) {
|
||||||
|
if (!(await fileExists(f))) continue;
|
||||||
|
const src = await readFile(f, "utf8");
|
||||||
|
if (src.includes(OG_MARKER)) {
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: ap-og-image already applied in ${f}`);
|
||||||
|
ogDone = true; break;
|
||||||
|
}
|
||||||
|
let updated = src;
|
||||||
|
let changed = false;
|
||||||
|
if (CN_BLOCK_RE.test(updated)) {
|
||||||
|
updated = updated.replace(CN_BLOCK_RE, NEW_CN);
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
console.warn(`[postinstall] ${SCRIPT}: ap-og-image — jf2ToActivityStreams OG block not found in ${f}`);
|
||||||
|
}
|
||||||
|
if (AS2_BLOCK_RE.test(updated)) {
|
||||||
|
updated = updated.replace(AS2_BLOCK_RE, NEW_AS2);
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
console.warn(`[postinstall] ${SCRIPT}: ap-og-image — jf2ToAS2Activity OG block not found in ${f}`);
|
||||||
|
}
|
||||||
|
if (changed && updated !== src) {
|
||||||
|
await writeFile(f, updated, "utf8");
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: applied ap-og-image to ${f}`);
|
||||||
|
total++; ogDone = true; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ogDone) console.log(`[postinstall] ${SCRIPT}: ap-og-image — no target file found or no changes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[postinstall] ${SCRIPT}: done (${total} patch(es) applied)`);
|
||||||
Reference in New Issue
Block a user