From 521bc04c4ea2e8463673a4e77b60d6d3de9a7750 Mon Sep 17 00:00:00 2001 From: Sven Date: Fri, 29 May 2026 09:34:06 +0200 Subject: [PATCH] fix(ap): filter self-sent Create activities from inbox to prevent Anonymous self-mentions handleCreate in inbox-handlers.js had no guard against activities from the local actor itself. When svemagie.net posts mentioned itself (e.g. syndication loopback or self-tagging), the Create arrived in the inbox, matched the Mention tag check for ourActorUrl, and was stored as an inbound notification. The actor could not be resolved (hairpin NAT) so the display name was 'Anonymous'. Fix: patch-ap-inbox-self-mention-filter.mjs adds a self-exclusion gate: if (actorUrl && actorUrl === ourActorUrl) return; ...immediately after ourActorUrl is derived (before DM/reply/mention/timeline branches). Defense-in-depth: mention-tag condition also gets actorUrl !== ourActorUrl. --- README.md | 3 + .../patch-ap-inbox-self-mention-filter.mjs | 100 ++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 scripts/patch-ap-inbox-self-mention-filter.mjs diff --git a/README.md b/README.md index 575025ee..c4ea5b45 100644 --- a/README.md +++ b/README.md @@ -460,6 +460,9 @@ The Bluesky `listNotifications` API paginates backwards in time — using the cu **`patch-conversations-bluesky-self-filter.mjs`** Self-interactions from the blog's own Bluesky account (likes, reposts, replies) appeared as inbound interactions in the conversations endpoint. Two-pronged fix: (1) the scheduler skips storing any notification whose author handle matches `BLUESKY_IDENTIFIER`; (2) the conversations controller filters out items where the author URL matches the site owner's Bluesky profile URL. +**`patch-ap-inbox-self-mention-filter.mjs`** +Self-sent Create activities (posts from the local AP actor mentioning itself) appeared as inbound "mention" notifications, displayed as "Anonymous" because the own actor URL is unreachable from inside the jail (hairpin NAT). Root cause: `handleCreate` in `inbox-handlers.js` had no self-exclusion guard. Fix: insert `if (actorUrl === ourActorUrl) return` immediately after `ourActorUrl` is derived, so all processing for self-sent creates is skipped. Defense-in-depth: also guard the mention-tag branch with `actorUrl !== ourActorUrl`. + ### Files **`patch-endpoint-files-upload-route.mjs`** diff --git a/scripts/patch-ap-inbox-self-mention-filter.mjs b/scripts/patch-ap-inbox-self-mention-filter.mjs new file mode 100644 index 00000000..f966396f --- /dev/null +++ b/scripts/patch-ap-inbox-self-mention-filter.mjs @@ -0,0 +1,100 @@ +/** + * Patch: filter out self-sent Create activities in the AP inbox. + * + * When the local AP actor sends a post that mentions itself (e.g. cross-posting + * loops, self-tagging, or loopback delivery), handleCreate receives a Create + * activity whose actor URL matches ourActorUrl. Without a self-exclusion guard + * these are stored as inbound notifications (type "mention" or "reply") and + * displayed as "Anonymous" because the own actor is unreachable via hairpin NAT. + * + * Fix: insert a self-exclusion gate immediately after ourActorUrl is derived. + * Defense-in-depth: also add `actorUrl !== ourActorUrl` to the mention tag check + * so the mention branch is safe even if the gate is ever moved or regressed. + */ + +import { access, readFile, writeFile } from "node:fs/promises"; + +const candidates = [ + "node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-handlers.js", + "node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-handlers.js", +]; + +const patchSpecs = [ + { + name: "ap-inbox-self-create-gate", + marker: "// [patch] ap-inbox-self-create-gate", + oldSnippet: ` const ourActorUrl = ctx.getActorUri(handle).href; + const followersUrl = ctx.getFollowersUri(handle)?.href || ""; + + if (computeVisibility(object, { ourActorUrl, followersUrl }) === "direct") {`, + newSnippet: ` const ourActorUrl = ctx.getActorUri(handle).href; + const followersUrl = ctx.getFollowersUri(handle)?.href || ""; + + // [patch] ap-inbox-self-create-gate + // Never process activities sent by our own actor as inbound — they produce + // spurious "Anonymous" notifications due to hairpin NAT blocking self-fetch. + if (actorUrl && actorUrl === ourActorUrl) return; + + if (computeVisibility(object, { ourActorUrl, followersUrl }) === "direct") {`, + }, + { + name: "ap-inbox-mention-self-guard", + marker: "// [patch] ap-inbox-mention-self-guard", + oldSnippet: ` if (tag.type === "Mention" && tag.href?.href === ourActorUrl) {`, + newSnippet: ` if (tag.type === "Mention" && tag.href?.href === ourActorUrl && actorUrl !== ourActorUrl) { // [patch] ap-inbox-mention-self-guard`, + }, +]; + +async function exists(filePath) { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +const checkedFiles = new Set(); +const patchedFiles = new Set(); + +for (const spec of patchSpecs) { + let foundAnyTarget = false; + + for (const filePath of candidates) { + if (!(await exists(filePath))) continue; + + foundAnyTarget = true; + checkedFiles.add(filePath); + + const source = await readFile(filePath, "utf8"); + + if (source.includes(spec.marker)) { + continue; + } + + if (!source.includes(spec.oldSnippet)) { + console.warn(`[postinstall] ${spec.name}: oldSnippet not found in ${filePath} — upstream may have changed`); + continue; + } + + const updated = source.replace(spec.oldSnippet, spec.newSnippet); + if (updated === source) continue; + + await writeFile(filePath, updated, "utf8"); + patchedFiles.add(filePath); + } + + if (!foundAnyTarget) { + console.log(`[postinstall] ${spec.name}: no target files found`); + } +} + +if (checkedFiles.size === 0) { + console.log("[postinstall] ap-inbox-self-mention-filter: no target files found"); +} else if (patchedFiles.size === 0) { + console.log("[postinstall] ap-inbox-self-mention-filter: patches already applied"); +} else { + console.log( + `[postinstall] Patched ap-inbox-self-mention-filter in ${patchedFiles.size}/${checkedFiles.size} file(s)`, + ); +}