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",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "node scripts/setup-gitea-url-rewrite.mjs",
|
"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",
|
"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-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",
|
"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"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"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
|
* Patch: AP OG image generation in jf2-to-as2.js.
|
||||||
* send guard, interactions cleanup preserve, interactions accounts uid,
|
|
||||||
* inbox ignore View activity, inbox skip View activity parse.
|
|
||||||
*
|
*
|
||||||
* 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-compose-default-checked
|
||||||
* - patch-ap-og-image
|
|
||||||
* - patch-ap-repost-announce-fix
|
* - patch-ap-repost-announce-fix
|
||||||
* - patch-ap-interactions-send-guard
|
* - patch-ap-interactions-send-guard
|
||||||
* - patch-ap-interactions-cleanup-preserve
|
* - patch-ap-interactions-cleanup-preserve
|
||||||
* - patch-ap-interactions-accounts-uid
|
* - patch-ap-interactions-accounts-uid
|
||||||
* - patch-inbox-ignore-view-activity
|
* - 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";
|
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; }
|
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";
|
const SCRIPT = "patch-ap-mastodon-misc";
|
||||||
|
|
||||||
// ── patch-ap-og-image: regex-based, two replacements per file ─────────────────
|
// ── 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_MARKER = "// og-image-v2";
|
||||||
const OG_CANDIDATES = apPath("lib/jf2-to-as2.js");
|
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;
|
let total = 0;
|
||||||
for (const p of PATCHES) {
|
let ogDone = false;
|
||||||
let done = false;
|
for (const f of OG_CANDIDATES) {
|
||||||
for (const f of p.files) {
|
if (!(await fileExists(f))) continue;
|
||||||
const r = await applyPatch(f, p.marker, p.oldSnippet, p.newSnippet);
|
const src = await readFile(f, "utf8");
|
||||||
if (r === "applied") {
|
if (src.includes(OG_MARKER)) {
|
||||||
console.log(`[postinstall] ${SCRIPT}: applied ${p.name} to ${f}`);
|
console.log(`[postinstall] ${SCRIPT}: ap-og-image already applied in ${f}`);
|
||||||
total++; done = true; break;
|
ogDone = 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`);
|
let updated = src;
|
||||||
}
|
let changed = false;
|
||||||
|
if (CN_BLOCK_RE.test(updated)) {
|
||||||
// ── Run og-image patch (regex-based, two replacements per file) ───────────────
|
updated = updated.replace(CN_BLOCK_RE, NEW_CN);
|
||||||
|
changed = true;
|
||||||
{
|
} else {
|
||||||
let ogDone = false;
|
console.warn(`[postinstall] ${SCRIPT}: ap-og-image — jf2ToActivityStreams OG block not found in ${f}`);
|
||||||
for (const f of OG_CANDIDATES) {
|
}
|
||||||
if (!(await fileExists(f))) continue;
|
if (AS2_BLOCK_RE.test(updated)) {
|
||||||
const src = await readFile(f, "utf8");
|
updated = updated.replace(AS2_BLOCK_RE, NEW_AS2);
|
||||||
if (src.includes(OG_MARKER)) {
|
changed = true;
|
||||||
console.log(`[postinstall] ${SCRIPT}: ap-og-image already applied in ${f}`);
|
} else {
|
||||||
ogDone = true; break;
|
console.warn(`[postinstall] ${SCRIPT}: ap-og-image — jf2ToAS2Activity OG block not found in ${f}`);
|
||||||
}
|
}
|
||||||
let updated = src;
|
if (changed && updated !== src) {
|
||||||
let changed = false;
|
await writeFile(f, updated, "utf8");
|
||||||
if (CN_BLOCK_RE.test(updated)) {
|
console.log(`[postinstall] ${SCRIPT}: applied ap-og-image to ${f}`);
|
||||||
updated = updated.replace(CN_BLOCK_RE, NEW_CN);
|
total++; ogDone = true; break;
|
||||||
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)`);
|
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.
|
* Consolidated patch: AP syndication guards in syndicator.js.
|
||||||
*
|
*
|
||||||
* Absorbs:
|
* Absorbs:
|
||||||
* - patch-ap-syndicate-dedup (prevent double-posting)
|
|
||||||
* - patch-ap-syndicate-skip-checkin (skip location checkins)
|
* - patch-ap-syndicate-skip-checkin (skip location checkins)
|
||||||
* - patch-ap-syndicate-skip-draft (skip draft posts)
|
* - patch-ap-syndicate-skip-draft (skip draft posts)
|
||||||
* - patch-ap-syndicate-skip-unlisted (skip unlisted posts)
|
* - patch-ap-syndicate-skip-unlisted (skip unlisted posts)
|
||||||
*
|
*
|
||||||
* ORDER MATTERS: dedup → checkin → draft → unlisted
|
* Note: patch-ap-syndicate-dedup integrated into source directly.
|
||||||
* checkin/draft/unlisted chain (each newSnippet is the next oldSnippet).
|
*
|
||||||
* dedup patches a separate anchor and must run first (before checkin
|
* ORDER MATTERS: checkin → draft → unlisted
|
||||||
* rewrites the surrounding context).
|
* Each newSnippet is the next oldSnippet.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { access, readFile, writeFile } from "node:fs/promises";
|
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 SCRIPT = "patch-ap-syndication";
|
||||||
|
|
||||||
const PATCHES = [
|
const PATCHES = [
|
||||||
// ORDER: dedup → checkin → draft → unlisted (each builds on prior output)
|
// ORDER: 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();`,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "skip-checkin",
|
name: "skip-checkin",
|
||||||
|
|||||||
Reference in New Issue
Block a user