96 lines
3.4 KiB
JavaScript
96 lines
3.4 KiB
JavaScript
/**
|
|
* Patch: dedup guard in AP syndicator.syndicate() to prevent double-posting.
|
|
*
|
|
* Root cause:
|
|
* The build CI calls POST /syndicate?source_url=X (force=true) after every
|
|
* Eleventy build. When syndicateToTargets() records the first syndication, it
|
|
* issues a Micropub update to save the syndication URL — which commits a new
|
|
* file to Gitea, triggering another build. That second build's CI call also
|
|
* hits the syndicate endpoint with force=true.
|
|
*
|
|
* In force mode with no mp-syndicate-to, syndicateToTargets() re-selects
|
|
* targets whose origin matches any existing syndication URL. Since the AP
|
|
* syndicator's UID (publicationUrl, e.g. "https://blog.giersig.eu/") and the
|
|
* first syndication return value (properties.url, e.g.
|
|
* "https://blog.giersig.eu/notes/my-post/") share the same origin,
|
|
* the AP syndicator is matched and called a second time → duplicate Create(Note).
|
|
*
|
|
* Fix:
|
|
* At the start of syndicate(), query ap_activities for an existing outbound
|
|
* Create/Announce/Update for properties.url. If found, log and return the
|
|
* existing URL without re-federating.
|
|
*
|
|
* This is self-contained (no CI or force-mode changes needed) and correct
|
|
* regardless of how syndication is triggered.
|
|
*/
|
|
|
|
import { access, readFile, writeFile } from "node:fs/promises";
|
|
|
|
const MARKER = "// [patch] ap-syndicate-dedup";
|
|
|
|
const candidates = [
|
|
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js",
|
|
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js",
|
|
];
|
|
|
|
const OLD = ` try {
|
|
const actorUrl = plugin._getActorUrl();`;
|
|
|
|
const NEW = ` // Dedup: skip re-federation if we've already sent an activity for this URL. ${MARKER}
|
|
// 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();`;
|
|
|
|
async function exists(filePath) {
|
|
try {
|
|
await access(filePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
let checked = 0;
|
|
let patched = 0;
|
|
|
|
for (const filePath of candidates) {
|
|
if (!(await exists(filePath))) continue;
|
|
checked += 1;
|
|
|
|
const source = await readFile(filePath, "utf8");
|
|
|
|
if (source.includes(MARKER)) {
|
|
console.log(`[postinstall] patch-ap-syndicate-dedup: already applied to ${filePath}`);
|
|
continue;
|
|
}
|
|
|
|
if (!source.includes(OLD)) {
|
|
console.warn(`[postinstall] patch-ap-syndicate-dedup: snippet not found in ${filePath}`);
|
|
continue;
|
|
}
|
|
|
|
await writeFile(filePath, source.replace(OLD, NEW), "utf8");
|
|
patched += 1;
|
|
console.log(`[postinstall] Applied patch-ap-syndicate-dedup to ${filePath}`);
|
|
}
|
|
|
|
if (checked === 0) {
|
|
console.log("[postinstall] patch-ap-syndicate-dedup: no target files found");
|
|
} else if (patched === 0) {
|
|
console.log("[postinstall] patch-ap-syndicate-dedup: already up to date");
|
|
} else {
|
|
console.log(`[postinstall] patch-ap-syndicate-dedup: patched ${patched} file(s)`);
|
|
}
|