diff --git a/scripts/patch-actor-aliases-successor.mjs b/scripts/patch-actor-aliases-successor.mjs deleted file mode 100644 index 23a283ec..00000000 --- a/scripts/patch-actor-aliases-successor.mjs +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Patch @rmdes/indiekit-endpoint-activitypub federation-setup.js: - * - personOptions.alsoKnownAs → personOptions.aliases (Fedify uses 'aliases') - * - Add movedTo → personOptions.successor (Fedify uses 'successor') - */ - -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 = "// patch-actor-aliases-successor: applied"; - -const OLD = ` if (profile.alsoKnownAs?.length > 0) { - personOptions.alsoKnownAs = profile.alsoKnownAs.map((u) => new URL(u)); - }`; - -const NEW = ` if (profile.alsoKnownAs?.length > 0) { - personOptions.aliases = profile.alsoKnownAs.map((u) => new URL(u)); - } - if (profile.movedTo) { - personOptions.successor = new URL(profile.movedTo); - } - ${MARKER}`; - -async function exists(p) { - try { await access(p); return true; } catch { return false; } -} - -let patched = false; -for (const filePath of candidates) { - if (!(await exists(filePath))) continue; - const src = await readFile(filePath, "utf8"); - if (src.includes(MARKER)) { - console.log(`[postinstall] patch-actor-aliases-successor: already applied in ${filePath}`); - patched = true; - break; - } - if (!src.includes(OLD)) { - console.log(`[postinstall] patch-actor-aliases-successor: target not found in ${filePath}`); - continue; - } - await writeFile(filePath, src.replace(OLD, NEW), "utf8"); - console.log(`[postinstall] patch-actor-aliases-successor: applied to ${filePath}`); - patched = true; - break; -} - -if (!patched) { - console.log("[postinstall] patch-actor-aliases-successor: no target file found"); -} diff --git a/scripts/patch-ap-collection-count-estimate.mjs b/scripts/patch-ap-collection-count-estimate.mjs deleted file mode 100644 index aeb567af..00000000 --- a/scripts/patch-ap-collection-count-estimate.mjs +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Patch: replace countDocuments() with estimatedDocumentCount() in - * federation-setup.js followers/following collection dispatchers. - * - * All four calls are already wrapped in cachedQuery(), but on cache miss - * countDocuments() runs a full aggregate scan. estimatedDocumentCount() - * reads collection metadata — O(1). - * - * Covers: ap_followers (×2) and ap_following (×2) in collection pagination - * and counter dispatchers. - */ - -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 = "// [patch] collection-count-estimate"; - -const replacements = [ - { - old: ` const t = await collections.ap_followers.countDocuments(); - return [d, t];`, - new: ` const t = await collections.ap_followers.estimatedDocumentCount(); ${MARKER} - return [d, t];`, - }, - { - old: ` return await collections.ap_followers.countDocuments();`, - new: ` return await collections.ap_followers.estimatedDocumentCount(); ${MARKER}`, - }, - { - old: ` const t = await collections.ap_following.countDocuments(); - return [d, t];`, - new: ` const t = await collections.ap_following.estimatedDocumentCount(); ${MARKER} - return [d, t];`, - }, - { - old: ` return await collections.ap_following.countDocuments();`, - new: ` return await collections.ap_following.estimatedDocumentCount(); ${MARKER}`, - }, -]; - -async function exists(p) { - try { await access(p); return true; } catch { return false; } -} - -let patched = false; -for (const filePath of candidates) { - if (!(await exists(filePath))) continue; - let src = await readFile(filePath, "utf8"); - if (src.includes(MARKER)) { - console.log(`[postinstall] patch-ap-collection-count-estimate: already applied in ${filePath}`); - patched = true; - break; - } - let changed = 0; - for (const { old, new: replacement } of replacements) { - if (src.includes(old)) { - src = src.replace(old, replacement); - changed++; - } - } - if (changed === 0) { - console.log(`[postinstall] patch-ap-collection-count-estimate: target snippets not found in ${filePath}`); - continue; - } - await writeFile(filePath, src, "utf8"); - console.log(`[postinstall] patch-ap-collection-count-estimate: applied ${changed}/4 replacements in ${filePath}`); - patched = true; - break; -} - -if (!patched) { - console.log("[postinstall] patch-ap-collection-count-estimate: no target file found"); -} diff --git a/scripts/patch-ap-dm-bsky-source-content.mjs b/scripts/patch-ap-dm-bsky-source-content.mjs deleted file mode 100644 index 129fd74e..00000000 --- a/scripts/patch-ap-dm-bsky-source-content.mjs +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Patch: extract DM content from object.source.content when object.content is empty. - * - * Bluesky DMs bridged via bsky.brid.gy arrive as Create activities wrapping a - * ChatMessage object. Bridgy Fed sends the message text in `source.content` - * (plain text, mediaType: text/plain) without a `content` field (HTML). - * - * The DM handler only checked `object.content`, so all bsky.brid.gy DMs were - * stored with empty content, making them unviewable in Mona. - * - * Fix: after extracting `rawHtml` from `object.content`, also read - * `object.source?.content` as a plain-text fallback for `contentText` when - * `rawHtml` is empty. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-handlers.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-handlers.js", -]; - -const MARKER = "// [patch] bsky-source-content-fallback"; - -const OLD = ` const rawHtml = object.content?.toString() || ""; - const contentHtml = sanitizeContent(rawHtml); - const contentText = rawHtml.replace(/<[^>]*>/g, "").substring(0, 500);`; - -const NEW = ` const rawHtml = object.content?.toString() || ""; - const sourceText = object.source?.content?.toString() || ""; // [patch] bsky-source-content-fallback - const contentHtml = sanitizeContent(rawHtml); - const contentText = rawHtml - ? rawHtml.replace(/<[^>]*>/g, "").substring(0, 500) - : sourceText.substring(0, 500);`; - -async function exists(p) { - try { await access(p); return true; } catch { return false; } -} - -let patched = false; -for (const filePath of candidates) { - if (!(await exists(filePath))) continue; - const src = await readFile(filePath, "utf8"); - if (src.includes(MARKER)) { - console.log(`[postinstall] patch-ap-dm-bsky-source-content: already applied in ${filePath}`); - patched = true; - break; - } - if (!src.includes(OLD)) { - console.log(`[postinstall] patch-ap-dm-bsky-source-content: target snippet not found in ${filePath}`); - continue; - } - await writeFile(filePath, src.replace(OLD, NEW), "utf8"); - console.log(`[postinstall] patch-ap-dm-bsky-source-content: applied to ${filePath}`); - patched = true; - break; -} - -if (!patched) { - console.log("[postinstall] patch-ap-dm-bsky-source-content: no target file found"); -} diff --git a/scripts/patch-ap-dm-send-serialize-id.mjs b/scripts/patch-ap-dm-send-serialize-id.mjs deleted file mode 100644 index d195eefd..00000000 --- a/scripts/patch-ap-dm-send-serialize-id.mjs +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Patch: capture addTimelineItem return value for DM serializeStatus. - * - * When sending an outbound DM via the Mastodon API, addTimelineItem returns - * the stored MongoDB document (which has _id). The return value was ignored, - * so the original timelineItem object had no _id. serializeStatus then crashed - * on item._id.toString() → "Cannot read properties of undefined (reading 'toString')". - * - * Fix: capture the addTimelineItem result and pass it to serializeStatus. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "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", -]; - -const MARKER = "// [patch] dm-send-serialize-id"; - -const OLD = ` try { - await addTimelineItem(collections, timelineItem); - } catch (storeError) { - console.warn("[Mastodon API] Failed to store outbound DM in timeline:", storeError.message); - } - - // Return a full serialized status so clients (Phanpy, Elk) can render it - const status = serializeStatus(timelineItem, {`; - -const NEW = ` let storedDmItem = timelineItem; // [patch] dm-send-serialize-id - try { - const _dmResult = await addTimelineItem(collections, timelineItem); - if (_dmResult?._id) storedDmItem = _dmResult; - } catch (storeError) { - console.warn("[Mastodon API] Failed to store outbound DM in timeline:", storeError.message); - } - - // Return a full serialized status so clients (Phanpy, Elk) can render it - const status = serializeStatus(storedDmItem, {`; - -async function exists(p) { - try { await access(p); return true; } catch { return false; } -} - -let patched = false; -for (const filePath of candidates) { - if (!(await exists(filePath))) continue; - const src = await readFile(filePath, "utf8"); - if (src.includes(MARKER)) { - console.log(`[postinstall] patch-ap-dm-send-serialize-id: already applied in ${filePath}`); - patched = true; - break; - } - if (!src.includes(OLD)) { - console.log(`[postinstall] patch-ap-dm-send-serialize-id: target snippet not found in ${filePath}`); - continue; - } - await writeFile(filePath, src.replace(OLD, NEW), "utf8"); - console.log(`[postinstall] patch-ap-dm-send-serialize-id: applied to ${filePath}`); - patched = true; - break; -} - -if (!patched) { - console.log("[postinstall] patch-ap-dm-send-serialize-id: no target file found"); -} diff --git a/scripts/patch-ap-followers-count-estimate.mjs b/scripts/patch-ap-followers-count-estimate.mjs deleted file mode 100644 index 3cd1d095..00000000 --- a/scripts/patch-ap-followers-count-estimate.mjs +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Patch: replace ap_followers.countDocuments() with estimatedDocumentCount() - * in syndicator.js log statement. - * - * countDocuments() runs an aggregate({$match:{}, $group:{n:$sum:1}}) scan. - * For a logging call this is unnecessary — estimatedDocumentCount() reads - * collection metadata (O(1), no lock contention). - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js", -]; - -const MARKER = "// [patch] followers-count-estimate"; - -const OLD = ` // Count followers for logging - const followerCount = - await plugin._collections.ap_followers.countDocuments();`; - -const NEW = ` // Count followers for logging - const followerCount = - await plugin._collections.ap_followers.estimatedDocumentCount(); ${MARKER}`; - -async function exists(p) { - try { await access(p); return true; } catch { return false; } -} - -let patched = false; -for (const filePath of candidates) { - if (!(await exists(filePath))) continue; - const src = await readFile(filePath, "utf8"); - if (src.includes(MARKER)) { - console.log(`[postinstall] patch-ap-followers-count-estimate: already applied in ${filePath}`); - patched = true; - break; - } - if (!src.includes(OLD)) { - console.log(`[postinstall] patch-ap-followers-count-estimate: target snippet not found in ${filePath}`); - continue; - } - await writeFile(filePath, src.replace(OLD, NEW), "utf8"); - console.log(`[postinstall] patch-ap-followers-count-estimate: applied to ${filePath}`); - patched = true; - break; -} - -if (!patched) { - console.log("[postinstall] patch-ap-followers-count-estimate: no target file found"); -} diff --git a/scripts/patch-ap-key-freshness-nonblocking.mjs b/scripts/patch-ap-key-freshness-nonblocking.mjs deleted file mode 100644 index cd78807d..00000000 --- a/scripts/patch-ap-key-freshness-nonblocking.mjs +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Patch: make touchKeyFreshness() fire-and-forget in inbox-listeners.js. - * - * touchKeyFreshness() is non-critical tracking (records lastSeenAt on - * ap_key_freshness). Under burst AP traffic, 11 parallel awaited upserts - * compete for MongoDB write locks — profiling shows 8-second delays on - * this single call while holding up inbox processing. - * - * Removing the await means inbox listeners don't block on the tracking - * write. The .catch(() => {}) mirrors the existing error handling inside - * the function itself. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-listeners.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-listeners.js", -]; - -const MARKER = "// [patch] key-freshness-nonblocking"; - -async function exists(p) { - try { await access(p); return true; } catch { return false; } -} - -let patched = false; -for (const filePath of candidates) { - if (!(await exists(filePath))) continue; - let src = await readFile(filePath, "utf8"); - if (src.includes(MARKER)) { - console.log(`[postinstall] patch-ap-key-freshness-nonblocking: already applied in ${filePath}`); - patched = true; - break; - } - const before = src; - // Replace all: await touchKeyFreshness(...) → touchKeyFreshness(...).catch(() => {}) - src = src.replace( - /await touchKeyFreshness\(([^)]+)\);/g, - `touchKeyFreshness($1).catch(() => {}); ${MARKER}`, - ); - const count = (src.match(/key-freshness-nonblocking/g) || []).length; - if (count === 0) { - console.log(`[postinstall] patch-ap-key-freshness-nonblocking: target not found in ${filePath}`); - continue; - } - await writeFile(filePath, src, "utf8"); - console.log(`[postinstall] patch-ap-key-freshness-nonblocking: applied to ${count} calls in ${filePath}`); - patched = true; - break; -} - -if (!patched) { - console.log("[postinstall] patch-ap-key-freshness-nonblocking: no target file found"); -} diff --git a/scripts/patch-ap-mastodon-short-post-bypass.mjs b/scripts/patch-ap-mastodon-short-post-bypass.mjs deleted file mode 100644 index 491be99a..00000000 --- a/scripts/patch-ap-mastodon-short-post-bypass.mjs +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Patch: skip Micropub blog post creation for short standalone Mastodon posts. - * - * Very short status texts (≤ 280 chars, no media, no reply) are fediverse-only - * interactions — they federate via AP but must NOT create a permanent blog entry. - * Posts with in_reply_to, media, or text > 280 characters go through the full - * Micropub pipeline as before. - * - * Short posts get a canonical URL at /activitypub/objects/note/{uuid} (same as - * DMs), are stored in ap_timeline, and delivered to followers via Fedify's - * ctx.sendActivity({ "followers" }) — the same mechanism the AP syndicator uses. - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "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", -]; - -const MARKER = "// [patch] short-post-bypass"; - -const OLD = ` // Resolve in_reply_to URL from status ID (cursor or ObjectId) - let inReplyTo = null; - if (inReplyToId) {`; - -const NEW = ` // Short standalone posts (≤ 280 chars, no media, no reply) skip Micropub - // and go directly to AP — no blog entry is created. // [patch] short-post-bypass - const _shortPostLimit = 280; - const _isShortPost = - !inReplyToId && - (!mediaIds || mediaIds.length === 0) && - visibility !== "direct" && - (statusText || "").trim().length > 0 && - (statusText || "").trim().length <= _shortPostLimit; - - if (_isShortPost) { - const _spPluginOptions = req.app.locals.mastodonPluginOptions || {}; - const _spPublicationUrl = (_spPluginOptions.publicationUrl || baseUrl).replace(/\\/+$/, ""); - const _spFederation = _spPluginOptions.federation; - const _spHandle = _spPluginOptions.handle || "user"; - const _spHostname = (() => { try { return new URL(_spPublicationUrl).hostname; } catch { return ""; } })(); - const _spProfile = await collections.ap_profile.findOne({}); - - const { randomUUID } = await import("node:crypto"); - const _spUuid = randomUUID(); - const _spNoteId = \`\${_spPublicationUrl}/activitypub/objects/note/\${_spUuid}\`; - const _spNow = new Date().toISOString(); - - const _spText = (statusText || "").trim(); - const _spHtml = _spText - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/(https?:\\/\\/[^\\s<>&"')\\]]+)/g, '$1') - .replace(/\\n/g, "
"); - - const _spTlItem = await addTimelineItem(collections, { - uid: _spNoteId, - url: _spNoteId, - type: "note", - content: { text: _spText, html: \`

\${_spHtml}

\` }, - author: { - name: _spProfile?.name || _spHandle, - url: _spProfile?.url || _spPublicationUrl, - photo: _spProfile?.icon || "", - handle: \`@\${_spHandle}@\${_spHostname}\`, - emojis: [], - bot: false, - }, - published: _spNow, - createdAt: _spNow, - inReplyTo: null, - visibility: visibility || "public", - sensitive: sensitive === true || sensitive === "true", - category: [], - counts: { likes: 0, boosts: 0, replies: 0 }, - }); - - if (_spFederation) { - try { - const { Note, Create } = await import("@fedify/fedify/vocab"); - const _spCtx = _spFederation.createContext( - new URL(_spPublicationUrl + "/"), - { handle: _spHandle, publicationUrl: _spPublicationUrl }, - ); - const _spActorUri = _spCtx.getActorUri(_spHandle); - const _spNote = new Note({ - id: new URL(_spNoteId), - attributedTo: _spActorUri, - content: \`

\${_spHtml}

\`, - published: new Date(_spNow), - }); - const _spCreate = new Create({ - id: new URL(\`\${_spNoteId}#create\`), - actor: _spActorUri, - object: _spNote, - }); - await _spCtx.sendActivity( - { identifier: _spHandle }, - "followers", - _spCreate, - { preferSharedInbox: true, syncCollection: true, orderingKey: _spNoteId }, - ); - console.info(\`[Mastodon API] Short post federated to followers (no blog entry): \${_spNoteId}\`); - } catch (_spApErr) { - console.warn(\`[Mastodon API] Short post AP federation error: \${_spApErr.message}\`); - } - } - - const _spStatus = serializeStatus(_spTlItem, { - baseUrl, - favouritedIds: new Set(), - rebloggedIds: new Set(), - bookmarkedIds: new Set(), - pinnedIds: new Set(), - }); - - if (idempotencyKey && collections.ap_idempotency) { - const { createHash } = await import("node:crypto"); - const _spKey = createHash("sha256").update(\`\${baseUrl}:\${idempotencyKey}\`).digest("hex"); - await collections.ap_idempotency.insertOne({ key: _spKey, response: _spStatus, createdAt: new Date() }).catch(() => {}); - } - - return res.json(_spStatus); - } - - // Resolve in_reply_to URL from status ID (cursor or ObjectId) - let inReplyTo = null; - if (inReplyToId) {`; - -async function exists(p) { - try { await access(p); return true; } catch { return false; } -} - -let patched = false; -for (const filePath of candidates) { - if (!(await exists(filePath))) continue; - const src = await readFile(filePath, "utf8"); - if (src.includes(MARKER)) { - console.log(`[postinstall] patch-ap-mastodon-short-post-bypass: already applied in ${filePath}`); - patched = true; - break; - } - if (!src.includes(OLD)) { - console.log(`[postinstall] patch-ap-mastodon-short-post-bypass: target snippet not found in ${filePath}`); - continue; - } - await writeFile(filePath, src.replace(OLD, NEW), "utf8"); - console.log(`[postinstall] patch-ap-mastodon-short-post-bypass: applied to ${filePath}`); - patched = true; - break; -} - -if (!patched) { - console.log("[postinstall] patch-ap-mastodon-short-post-bypass: no target file found"); -} diff --git a/scripts/patch-ap-syndicate-deploy-delay.mjs b/scripts/patch-ap-syndicate-deploy-delay.mjs deleted file mode 100644 index 182ecd41..00000000 --- a/scripts/patch-ap-syndicate-deploy-delay.mjs +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Patch: add a 2-minute deploy delay before ActivityPub syndication. - * - * When a post is created (via Micropub or Mastodon-API compose), the - * blog's deploy pipeline (Gitea → Eleventy → rsync) takes ~1-2 minutes. - * If ActivityPub fires immediately, followers receive a Create activity - * whose `url` still 404s — Mastodon clients show a broken link. - * - * This patch inserts a `setTimeout`-based delay at the top of the - * `syndicate()` function. The delay is configurable via the - * AP_SYNDICATE_DELAY_MS environment variable (default: 120000 = 2 min). - */ - -import { access, readFile, writeFile } from "node:fs/promises"; - -const candidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js", - "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js", -]; - -const MARKER = "// [patch] ap-syndicate-deploy-delay"; - -// The line right after the early-exit guards — insert before the -// try { const actorUrl = ... } block. -const INSERT_AFTER = ` try { - const actorUrl = plugin._getActorUrl();`; - -const DELAY_BLOCK = ` // [patch] ap-syndicate-deploy-delay — wait for deploy pipeline before federating - const _apDelay = Number.parseInt(process.env.AP_SYNDICATE_DELAY_MS ?? "120000", 10); - if (_apDelay > 0) { - console.info(\`[ActivityPub] Delaying syndication by \${_apDelay}ms for \${properties.url}\`); - await new Promise((resolve) => setTimeout(resolve, _apDelay)); - } - ${MARKER} - try { - const actorUrl = plugin._getActorUrl();`; - -async function exists(p) { - try { await access(p); return true; } catch { return false; } -} - -let patched = false; -for (const filePath of candidates) { - if (!(await exists(filePath))) continue; - let src = await readFile(filePath, "utf8"); - if (src.includes(MARKER)) { - console.log(`[postinstall] patch-ap-syndicate-deploy-delay: already applied in ${filePath}`); - patched = true; - break; - } - if (!src.includes(INSERT_AFTER)) { - console.log(`[postinstall] patch-ap-syndicate-deploy-delay: insertion point not found in ${filePath}`); - continue; - } - src = src.replace(INSERT_AFTER, DELAY_BLOCK); - await writeFile(filePath, src, "utf8"); - console.log(`[postinstall] patch-ap-syndicate-deploy-delay: applied in ${filePath}`); - patched = true; - break; -} - -if (!patched) { - console.log("[postinstall] patch-ap-syndicate-deploy-delay: no target file found"); -} diff --git a/scripts/patch-federation-unlisted-guards.mjs b/scripts/patch-federation-unlisted-guards.mjs deleted file mode 100644 index 39c8b05e..00000000 --- a/scripts/patch-federation-unlisted-guards.mjs +++ /dev/null @@ -1,143 +0,0 @@ -import { access, readFile, writeFile } from "node:fs/promises"; - -// activitypub index.js and federation-setup.js unlisted guards are now -// built into the fork — only endpoint-syndicate (separate package) needs patching. -// backfill-timeline.js also needs patching — it runs on every startup and -// backfills ap_timeline from posts without filtering unlisted posts. - -const endpointSyndicateCandidates = [ - "node_modules/@indiekit/endpoint-syndicate/lib/utils.js", - "node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/utils.js", -]; - -const backfillTimelineCandidates = [ - "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/backfill-timeline.js", -]; - -const patchSpecs = [ - { - name: "backfill-timeline-unlisted-guard", - candidates: backfillTimelineCandidates, - oldSnippet: ` const allPosts = await posts - .find({ - "properties.post-status": { $ne: "draft" }, - "properties.deleted": { $exists: false }, - "properties.url": { $exists: true }, - }) - .toArray();`, - newSnippet: ` const allPosts = await posts - .find({ - "properties.post-status": { $ne: "draft" }, - "properties.deleted": { $exists: false }, - "properties.url": { $exists: true }, - // Exclude unlisted posts — they must not appear in ap_timeline (Mastodon API). - "properties.visibility": { $ne: "unlisted" }, - }) - .toArray();`, - }, - { - name: "endpoint-syndicate-source-url-unlisted-guard", - candidates: endpointSyndicateCandidates, - oldSnippet: ` postData = await postsCollection.findOne({ - "properties.url": url, - });`, - newSnippet: ` postData = await postsCollection.findOne({ - "properties.url": url, - "properties.post-status": { - $ne: "draft", - }, - // Exclude unlisted posts from automatic syndication/federation. - "properties.visibility": { - $ne: "unlisted", - }, - });`, - }, - { - name: "endpoint-syndicate-get-post-data-pending-unlisted-guard", - candidates: endpointSyndicateCandidates, - oldSnippet: ` "properties.post-status": { - $ne: "draft", - }, - })`, - newSnippet: ` "properties.post-status": { - $ne: "draft", - }, - // Exclude unlisted posts from automatic syndication/federation. - "properties.visibility": { - $ne: "unlisted", - }, - })`, - }, - { - name: "endpoint-syndicate-get-all-post-data-unlisted-guard", - candidates: endpointSyndicateCandidates, - oldSnippet: ` "properties.post-status": { - $ne: "draft", - }, - })`, - newSnippet: ` "properties.post-status": { - $ne: "draft", - }, - // Exclude unlisted posts from automatic syndication/federation. - "properties.visibility": { - $ne: "unlisted", - }, - })`, - }, -]; - -async function exists(filePath) { - try { - await access(filePath); - return true; - } catch { - return false; - } -} - -const checkedFiles = new Set(); -const patchedFiles = new Set(); - -for (const spec of patchSpecs) { - let foundAnyTarget = false; - - for (const filePath of spec.candidates) { - if (!(await exists(filePath))) { - continue; - } - - foundAnyTarget = true; - checkedFiles.add(filePath); - - const source = await readFile(filePath, "utf8"); - - let updated = source; - let replacements = 0; - - if (source.includes(spec.oldSnippet)) { - updated = source.replace(spec.oldSnippet, spec.newSnippet); - replacements = 1; - } - - if (replacements === 0 || updated === source) { - continue; - } - - await writeFile(filePath, updated, "utf8"); - patchedFiles.add(filePath); - } - - if (!foundAnyTarget) { - console.log(`[postinstall] ${spec.name}: no target files found`); - } -} - -if (checkedFiles.size === 0) { - console.log("[postinstall] No federation patch targets found"); -} else if (patchedFiles.size === 0) { - console.log("[postinstall] federation unlisted guards already patched"); -} else { - console.log( - `[postinstall] Patched federation unlisted guards in ${patchedFiles.size}/${checkedFiles.size} file(s)`, - ); -}