feat: consolidated patch-ap-syndication (dedup, checkin, draft, unlisted)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* 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).
|
||||||
|
*/
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a single patch to a file.
|
||||||
|
* Returns: "applied" | "already_applied" | "snippet_not_found" | "file_not_found"
|
||||||
|
*/
|
||||||
|
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-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();`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "skip-checkin",
|
||||||
|
files: apPath("lib/syndicator.js"),
|
||||||
|
marker: "// [patch] ap-syndicate-skip-checkin",
|
||||||
|
oldSnippet: ` async syndicate(properties) {
|
||||||
|
if (!plugin._federation) {
|
||||||
|
return undefined;
|
||||||
|
}`,
|
||||||
|
newSnippet: ` async syndicate(properties) {
|
||||||
|
if (!plugin._federation) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip location checkins — they have a JF2 \`location\` property. // [patch] ap-syndicate-skip-checkin
|
||||||
|
if (properties.location) {
|
||||||
|
console.info(\`[ActivityPub] Skipping syndication for location checkin: \${properties.url}\`);
|
||||||
|
return undefined;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "skip-draft",
|
||||||
|
files: apPath("lib/syndicator.js"),
|
||||||
|
marker: "// [patch] ap-syndicate-skip-draft",
|
||||||
|
oldSnippet: ` // Skip location checkins — they have a JF2 \`location\` property. // [patch] ap-syndicate-skip-checkin
|
||||||
|
if (properties.location) {
|
||||||
|
console.info(\`[ActivityPub] Skipping syndication for location checkin: \${properties.url}\`);
|
||||||
|
return undefined;
|
||||||
|
}`,
|
||||||
|
newSnippet: ` // Skip location checkins — they have a JF2 \`location\` property. // [patch] ap-syndicate-skip-checkin
|
||||||
|
if (properties.location) {
|
||||||
|
console.info(\`[ActivityPub] Skipping syndication for location checkin: \${properties.url}\`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip draft posts — they should not be federated to followers. // [patch] ap-syndicate-skip-draft
|
||||||
|
if (properties["post-status"] === "draft") {
|
||||||
|
console.info(\`[ActivityPub] Skipping syndication for draft post: \${properties.url}\`);
|
||||||
|
return undefined;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "skip-unlisted",
|
||||||
|
files: apPath("lib/syndicator.js"),
|
||||||
|
marker: "// [patch] ap-syndicate-skip-unlisted",
|
||||||
|
oldSnippet: ` // Skip draft posts — they should not be federated to followers. // [patch] ap-syndicate-skip-draft
|
||||||
|
if (properties["post-status"] === "draft") {
|
||||||
|
console.info(\`[ActivityPub] Skipping syndication for draft post: \${properties.url}\`);
|
||||||
|
return undefined;
|
||||||
|
}`,
|
||||||
|
newSnippet: ` // Skip draft posts — they should not be federated to followers. // [patch] ap-syndicate-skip-draft
|
||||||
|
if (properties["post-status"] === "draft") {
|
||||||
|
console.info(\`[ActivityPub] Skipping syndication for draft post: \${properties.url}\`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip unlisted posts — they should not be federated to followers. // [patch] ap-syndicate-skip-unlisted
|
||||||
|
if (properties.visibility === "unlisted") {
|
||||||
|
console.info(\`[ActivityPub] Skipping syndication for unlisted post: \${properties.url}\`);
|
||||||
|
return undefined;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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)`);
|
||||||
Reference in New Issue
Block a user