chore: remove 3 redundant AP patches, trim 2 to site-specific only
Deploy Indiekit Server / deploy (push) Successful in 1m21s
Deploy Indiekit Server / deploy (push) Successful in 1m21s
Patches integrated into @rmdes/indiekit-endpoint-activitypub source: - patch-ap-mastodon-accounts.mjs (deleted) - patch-ap-mastodon-notifications.mjs (deleted) - patch-ap-oauth-token-expiry-fix.mjs (deleted) - patch-ap-syndication.mjs: dedup block removed (now in source) - patch-ap-mastodon-misc.mjs: rewritten to og-image only (~70 lines) Remaining AP patches are site-specific and stay. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -5,8 +5,8 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/setup-gitea-url-rewrite.mjs",
|
||||
"postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-put-fallback.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-oauth-token-expiry-fix.mjs && node scripts/patch-ap-startup-gate-bypass.mjs && node scripts/patch-ap-federation-infra.mjs && node scripts/patch-ap-syndication.mjs && node scripts/patch-ap-mastodon-statuses.mjs && node scripts/patch-ap-mastodon-accounts.mjs && node scripts/patch-ap-mastodon-notifications.mjs && node scripts/patch-ap-mastodon-misc.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node scripts/patch-microsub-batch-concurrency.mjs",
|
||||
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/preflight-startup-gate.mjs && node scripts/patch-ap-startup-gate-bypass.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-put-fallback.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-microsub-batch-concurrency.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-oauth-token-expiry-fix.mjs && node scripts/patch-ap-federation-infra.mjs && node scripts/patch-ap-syndication.mjs && node scripts/patch-ap-mastodon-statuses.mjs && node scripts/patch-ap-mastodon-accounts.mjs && node scripts/patch-ap-mastodon-notifications.mjs && node scripts/patch-ap-mastodon-misc.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node --max-old-space-size=1024 --max-semi-space-size=32 --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
|
||||
"postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-put-fallback.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-startup-gate-bypass.mjs && node scripts/patch-ap-federation-infra.mjs && node scripts/patch-ap-syndication.mjs && node scripts/patch-ap-mastodon-statuses.mjs && node scripts/patch-ap-mastodon-misc.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node scripts/patch-microsub-batch-concurrency.mjs",
|
||||
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/preflight-startup-gate.mjs && node scripts/patch-ap-startup-gate-bypass.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-put-fallback.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-microsub-batch-concurrency.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-federation-infra.mjs && node scripts/patch-ap-syndication.mjs && node scripts/patch-ap-mastodon-statuses.mjs && node scripts/patch-ap-mastodon-misc.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node --max-old-space-size=1024 --max-semi-space-size=32 --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
/**
|
||||
* Consolidated patch: Mastodon account resolution fixes.
|
||||
*
|
||||
* Absorbs:
|
||||
* - patch-ap-actor-cache-await
|
||||
* Await the ap_actor_cache MongoDB write so the entry exists before the
|
||||
* search response is sent, making follow reliable after server restarts.
|
||||
*
|
||||
* - patch-ap-resolve-account-timeout-safe
|
||||
* Fix unhandled rejection crash when withTimeout() timer fires before the
|
||||
* original promise settles, and parallelise collection count fetches with
|
||||
* Promise.allSettled (max 5 s instead of worst-case 15 s).
|
||||
*
|
||||
* Both patches target:
|
||||
* lib/mastodon/helpers/resolve-account.js
|
||||
*/
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
const SCRIPT = "patch-ap-mastodon-accounts";
|
||||
|
||||
const PATCHES = [
|
||||
{
|
||||
name: "ap-actor-cache-await",
|
||||
files: apPath("lib/mastodon/helpers/resolve-account.js"),
|
||||
marker: "// [patch] ap-actor-cache-await",
|
||||
oldSnippet: ` // Persist actor URL mapping to MongoDB so follow/unfollow survives server restarts
|
||||
if (collections?.ap_actor_cache && actorUrl) {
|
||||
const hashId = remoteActorId(actorUrl);
|
||||
collections.ap_actor_cache.updateOne(
|
||||
{ _id: hashId },
|
||||
{ $set: { actorUrl, updatedAt: new Date() } },
|
||||
{ upsert: true },
|
||||
).catch(() => {}); // fire-and-forget, non-fatal
|
||||
}`,
|
||||
newSnippet: ` // Persist actor URL mapping to MongoDB so follow/unfollow survives server restarts
|
||||
// [patch] ap-actor-cache-await
|
||||
if (collections?.ap_actor_cache && actorUrl) {
|
||||
const hashId = remoteActorId(actorUrl);
|
||||
await collections.ap_actor_cache.updateOne(
|
||||
{ _id: hashId },
|
||||
{ $set: { actorUrl, updatedAt: new Date() } },
|
||||
{ upsert: true },
|
||||
).catch(() => {}); // non-fatal, but now awaited so entry exists before response
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "ap-resolve-account-timeout-safe",
|
||||
files: apPath("lib/mastodon/helpers/resolve-account.js"),
|
||||
marker: "// [patch] ap-resolve-account-timeout-safe",
|
||||
oldSnippet: ` const withTimeout = (promise, ms = 5000) =>
|
||||
Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms))]);
|
||||
|
||||
let followersCount = 0;
|
||||
let followingCount = 0;
|
||||
let statusesCount = 0;
|
||||
try {
|
||||
const followers = await withTimeout(actor.getFollowers());
|
||||
if (followers?.totalItems != null) followersCount = followers.totalItems;
|
||||
} catch { /* ignore */ }
|
||||
try {
|
||||
const following = await withTimeout(actor.getFollowing());
|
||||
if (following?.totalItems != null) followingCount = following.totalItems;
|
||||
} catch { /* ignore */ }
|
||||
try {
|
||||
const outbox = await withTimeout(actor.getOutbox());
|
||||
if (outbox?.totalItems != null) statusesCount = outbox.totalItems;
|
||||
} catch { /* ignore */ }`,
|
||||
newSnippet: ` const withTimeout = (promise, ms = 5000) => { // [patch] ap-resolve-account-timeout-safe
|
||||
const abort = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms));
|
||||
promise.catch(() => {}); // suppress unhandled rejection if timeout settles first
|
||||
return Promise.race([promise, abort]);
|
||||
};
|
||||
|
||||
// Fetch collection counts in parallel (max 5 s each) [patch] ap-resolve-account-timeout-safe
|
||||
const [followersResult, followingResult, outboxResult] = await Promise.allSettled([
|
||||
withTimeout(actor.getFollowers()),
|
||||
withTimeout(actor.getFollowing()),
|
||||
withTimeout(actor.getOutbox()),
|
||||
]);
|
||||
const followersCount = followersResult.status === "fulfilled" && followersResult.value?.totalItems != null ? followersResult.value.totalItems : 0;
|
||||
const followingCount = followingResult.status === "fulfilled" && followingResult.value?.totalItems != null ? followingResult.value.totalItems : 0;
|
||||
const statusesCount = outboxResult.status === "fulfilled" && outboxResult.value?.totalItems != null ? outboxResult.value.totalItems : 0;`,
|
||||
},
|
||||
];
|
||||
|
||||
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`);
|
||||
}
|
||||
console.log(`[postinstall] ${SCRIPT}: done (${total} patch(es) applied)`);
|
||||
@@ -1,17 +1,19 @@
|
||||
/**
|
||||
* 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.
|
||||
* Patch: AP OG image generation in jf2-to-as2.js.
|
||||
*
|
||||
* Absorbs:
|
||||
* Adds `image` property to ActivityStreams objects using:
|
||||
* - post photo attachment (if present), or
|
||||
* - generated OG image at /og/{slug}.png
|
||||
*
|
||||
* Note: All other patches previously in this file have been integrated
|
||||
* into the @rmdes/indiekit-endpoint-activitypub source directly:
|
||||
* - 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
|
||||
* - patch-inbox-skip-view-activity-parse (raw body fixes)
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
@@ -30,31 +32,9 @@ 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");
|
||||
@@ -92,379 +72,35 @@ const NEW_AS2 = ` const _ogPhotoF = properties.photo && asArray(properties.phot
|
||||
});
|
||||
}`;
|
||||
|
||||
// ── 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: 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"));`,
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
// ── 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`);
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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`);
|
||||
}
|
||||
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)`);
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Patch: include notif.url in batchFetchStatuses so mention notifications
|
||||
* resolve their associated status correctly.
|
||||
*
|
||||
* Root cause:
|
||||
* batchFetchStatuses() only collected notif.targetUrl for the batch lookup.
|
||||
* serializeNotification() looks up mentions by notif.url (the incoming
|
||||
* reply URL), not notif.targetUrl (the own post being replied to). These
|
||||
* are different URLs, so the statusMap never has an entry for the mention →
|
||||
* fallback fires → status.id = notif._id.toString() (a notification ObjectId,
|
||||
* not a timeline ObjectId) → Phanpy uses that ID for subsequent requests →
|
||||
* GET /api/v1/statuses/:id/context returns 404 because ap_timeline has no
|
||||
* document with that _id.
|
||||
*
|
||||
* Fix:
|
||||
* Collect both notif.url and notif.targetUrl so the batch covers all URL
|
||||
* shapes used by any notification type.
|
||||
*/
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
const SCRIPT = "patch-ap-mastodon-notifications";
|
||||
|
||||
const PATCHES = [
|
||||
{
|
||||
name: "ap-notifications-status-lookup",
|
||||
files: apPath("lib/mastodon/routes/notifications.js"),
|
||||
marker: "// [patch] ap-notifications-status-lookup",
|
||||
oldSnippet: ` const targetUrls = [
|
||||
...new Set(
|
||||
notifications
|
||||
.map((n) => n.targetUrl)
|
||||
.filter(Boolean),
|
||||
),
|
||||
];`,
|
||||
newSnippet: ` const targetUrls = [ // [patch] ap-notifications-status-lookup
|
||||
...new Set(
|
||||
notifications
|
||||
.flatMap((n) => [n.targetUrl, n.url])
|
||||
.filter(Boolean),
|
||||
),
|
||||
];`,
|
||||
},
|
||||
];
|
||||
|
||||
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`);
|
||||
}
|
||||
console.log(`[postinstall] ${SCRIPT}: done (${total} patch(es) applied)`);
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* Patch: fix OAuth access tokens expiring 10 minutes after login.
|
||||
*
|
||||
* Root cause:
|
||||
* The authorization code document is created with expiresAt = now + 10 min
|
||||
* (the authorization code TTL). When the access token is issued via the
|
||||
* authorization_code grant, the existing document is updated in-place with
|
||||
* $set: { accessToken, refreshToken, ... } but expiresAt is never cleared.
|
||||
*
|
||||
* The tokenRequired middleware then rejects the token once 10 minutes pass:
|
||||
* if (token.expiresAt && token.expiresAt < new Date()) return null;
|
||||
*
|
||||
* The refresh_token grant already has $unset: { expiresAt: "" } — this patch
|
||||
* applies the same fix to the authorization_code exchange path.
|
||||
*
|
||||
* Symptom: Mastodon clients (Phanpy, etc.) receive 200 on /api/v2/instance but
|
||||
* 401 on all authenticated endpoints ~10 minutes after logging in.
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const MARKER = "// [patch] ap-oauth-token-expiry-fix";
|
||||
|
||||
const candidates = [
|
||||
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/oauth.js",
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/oauth.js",
|
||||
];
|
||||
|
||||
const OLD_SNIPPET = ` {
|
||||
$set: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
refreshExpiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL),
|
||||
},
|
||||
},`;
|
||||
|
||||
const NEW_SNIPPET = ` {
|
||||
$set: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
refreshExpiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL),
|
||||
},
|
||||
$unset: { expiresAt: "" }, ${MARKER}
|
||||
},`;
|
||||
|
||||
async function exists(p) {
|
||||
try { await access(p); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
let totalPatched = 0;
|
||||
let totalChecked = 0;
|
||||
|
||||
for (const filePath of candidates) {
|
||||
if (!(await exists(filePath))) continue;
|
||||
totalChecked++;
|
||||
|
||||
const source = await readFile(filePath, "utf8");
|
||||
if (source.includes(MARKER)) {
|
||||
console.log(`[postinstall] patch-ap-oauth-token-expiry-fix: already applied to ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!source.includes(OLD_SNIPPET)) {
|
||||
console.warn(`[postinstall] patch-ap-oauth-token-expiry-fix: snippet not found in ${filePath} (upstream changed?)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const updated = source.replace(OLD_SNIPPET, NEW_SNIPPET);
|
||||
await writeFile(filePath, updated, "utf8");
|
||||
console.log(`[postinstall] Applied patch-ap-oauth-token-expiry-fix to ${filePath}`);
|
||||
totalPatched++;
|
||||
}
|
||||
|
||||
if (totalChecked === 0) {
|
||||
console.log("[postinstall] patch-ap-oauth-token-expiry-fix: no target files found");
|
||||
} else if (totalPatched === 0) {
|
||||
console.log("[postinstall] patch-ap-oauth-token-expiry-fix: already up to date");
|
||||
} else {
|
||||
console.log(`[postinstall] patch-ap-oauth-token-expiry-fix: patched ${totalPatched} file(s)`);
|
||||
}
|
||||
@@ -2,15 +2,14 @@
|
||||
* Consolidated patch: AP syndication guards in syndicator.js.
|
||||
*
|
||||
* Absorbs:
|
||||
* - patch-ap-syndicate-dedup (prevent double-posting)
|
||||
* - patch-ap-syndicate-skip-checkin (skip location checkins)
|
||||
* - patch-ap-syndicate-skip-draft (skip draft posts)
|
||||
* - patch-ap-syndicate-skip-unlisted (skip unlisted posts)
|
||||
*
|
||||
* ORDER MATTERS: dedup → checkin → draft → unlisted
|
||||
* checkin/draft/unlisted chain (each newSnippet is the next oldSnippet).
|
||||
* dedup patches a separate anchor and must run first (before checkin
|
||||
* rewrites the surrounding context).
|
||||
* Note: patch-ap-syndicate-dedup integrated into source directly.
|
||||
*
|
||||
* ORDER MATTERS: checkin → draft → unlisted
|
||||
* Each newSnippet is the next oldSnippet.
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
@@ -45,31 +44,7 @@ async function applyPatch(filePath, marker, oldSnippet, newSnippet) {
|
||||
const SCRIPT = "patch-ap-syndication";
|
||||
|
||||
const PATCHES = [
|
||||
// ORDER: dedup → checkin → draft → unlisted (each builds on prior output)
|
||||
|
||||
{
|
||||
name: "dedup",
|
||||
files: apPath("lib/syndicator.js"),
|
||||
marker: "// [patch] ap-syndicate-dedup",
|
||||
oldSnippet: ` try {
|
||||
const actorUrl = plugin._getActorUrl();`,
|
||||
newSnippet: ` // Dedup: skip re-federation if we've already sent an activity for this URL. // [patch] ap-syndicate-dedup
|
||||
// ap_activities is the authoritative record of "already federated".
|
||||
try {
|
||||
const existingActivity = await plugin._collections.ap_activities?.findOne({
|
||||
direction: "outbound",
|
||||
type: { $in: ["Create", "Announce", "Update"] },
|
||||
objectUrl: properties.url,
|
||||
});
|
||||
if (existingActivity) {
|
||||
console.info(\`[ActivityPub] Skipping duplicate syndication for \${properties.url} — already sent (\${existingActivity.type})\`);
|
||||
return properties.url || undefined;
|
||||
}
|
||||
} catch { /* DB unavailable — proceed */ }
|
||||
|
||||
try {
|
||||
const actorUrl = plugin._getActorUrl();`,
|
||||
},
|
||||
// ORDER: checkin → draft → unlisted (each builds on prior output)
|
||||
|
||||
{
|
||||
name: "skip-checkin",
|
||||
|
||||
Reference in New Issue
Block a user