remove patches integrated into indiekit-endpoint-activitypub fork
Deploy Indiekit Server / deploy (push) Successful in 1m33s
Deploy Indiekit Server / deploy (push) Successful in 1m33s
This commit is contained in:
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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(/>/g, ">")
|
||||
.replace(/(https?:\\/\\/[^\\s<>&"')\\]]+)/g, '<a href="$1">$1</a>')
|
||||
.replace(/\\n/g, "<br>");
|
||||
|
||||
const _spTlItem = await addTimelineItem(collections, {
|
||||
uid: _spNoteId,
|
||||
url: _spNoteId,
|
||||
type: "note",
|
||||
content: { text: _spText, html: \`<p>\${_spHtml}</p>\` },
|
||||
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: \`<p>\${_spHtml}</p>\`,
|
||||
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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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)`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user