feat(federation): block unlisted posts from syndication queue

This commit is contained in:
svemagie
2026-03-08 17:21:07 +01:00
parent 21262c31a9
commit 1dedd763a6
2 changed files with 209 additions and 2 deletions
+2 -2
View File
@@ -4,8 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"postinstall": "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-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.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",
"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/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-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.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 node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
"postinstall": "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-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.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",
"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/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-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-frontend-serviceworker-file.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 node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
@@ -0,0 +1,207 @@
import { access, readFile, writeFile } from "node:fs/promises";
const endpointSyndicateCandidates = [
"node_modules/@indiekit/endpoint-syndicate/lib/utils.js",
"node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/utils.js",
];
const activityPubIndexCandidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/index.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/index.js",
];
const activityPubFederationSetupCandidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js",
];
const patchSpecs = [
{
name: "endpoint-syndicate-source-url-unlisted-guard",
candidates: endpointSyndicateCandidates,
oldSnippet: ` postData = await postsCollection.findOne({
"properties.url": url,
});`,
newSnippet: ` postData = await postsCollection.findOne({
"properties.url": url,
"properties.post-status": {
$ne: "draft",
},
// Exclude unlisted posts from automatic syndication/federation.
"properties.visibility": {
$ne: "unlisted",
},
});`,
},
{
name: "endpoint-syndicate-get-post-data-pending-unlisted-guard",
candidates: endpointSyndicateCandidates,
oldSnippet: ` "properties.post-status": {
$ne: "draft",
},
})`,
newSnippet: ` "properties.post-status": {
$ne: "draft",
},
// Exclude unlisted posts from automatic syndication/federation.
"properties.visibility": {
$ne: "unlisted",
},
})`,
},
{
name: "endpoint-syndicate-get-all-post-data-unlisted-guard",
candidates: endpointSyndicateCandidates,
oldSnippet: ` "properties.post-status": {
$ne: "draft",
},
})`,
newSnippet: ` "properties.post-status": {
$ne: "draft",
},
// Exclude unlisted posts from automatic syndication/federation.
"properties.visibility": {
$ne: "unlisted",
},
})`,
},
{
name: "activitypub-syndicator-unlisted-guard",
candidates: activityPubIndexCandidates,
oldSnippet: ` async syndicate(properties) {
if (!self._federation) {
return undefined;
}
try {`,
newSnippet: ` async syndicate(properties) {
if (!self._federation) {
return undefined;
}
const visibility = String(properties?.visibility || "").toLowerCase();
if (visibility === "unlisted") {
console.info(
"[ActivityPub] Skipping federation for unlisted post: " +
(properties?.url || "unknown"),
);
await logActivity(self._collections.ap_activities, {
direction: "outbound",
type: "Syndicate",
actorUrl: self._publicationUrl,
objectUrl: properties?.url,
summary: "Syndication skipped: post visibility is unlisted",
}).catch(() => {});
return undefined;
}
try {`,
},
{
name: "activitypub-outbox-unlisted-guard",
candidates: activityPubFederationSetupCandidates,
oldSnippet: ` const pageSize = 20;
const skip = cursor ? Number.parseInt(cursor, 10) : 0;
const total = await postsCollection.countDocuments();
const posts = await postsCollection
.find()`,
newSnippet: ` const pageSize = 20;
const skip = cursor ? Number.parseInt(cursor, 10) : 0;
const federationVisibilityQuery = {
"properties.post-status": { $ne: "draft" },
"properties.visibility": { $ne: "unlisted" },
};
const total = await postsCollection.countDocuments(
federationVisibilityQuery,
);
const posts = await postsCollection
.find(federationVisibilityQuery)`,
},
{
name: "activitypub-outbox-counter-unlisted-guard",
candidates: activityPubFederationSetupCandidates,
oldSnippet: ` .setCounter(async (ctx, identifier) => {
if (identifier !== handle) return 0;
const postsCollection = collections.posts;
if (!postsCollection) return 0;
return await postsCollection.countDocuments();
})`,
newSnippet: ` .setCounter(async (ctx, identifier) => {
if (identifier !== handle) return 0;
const postsCollection = collections.posts;
if (!postsCollection) return 0;
return await postsCollection.countDocuments({
"properties.post-status": { $ne: "draft" },
"properties.visibility": { $ne: "unlisted" },
});
})`,
},
{
name: "activitypub-object-dispatch-unlisted-guard",
candidates: activityPubFederationSetupCandidates,
oldSnippet: ` const post = await collections.posts.findOne({ "properties.url": postUrl });
if (!post) return null;`,
newSnippet: ` const post = await collections.posts.findOne({ "properties.url": postUrl });
if (!post) return null;
if (post?.properties?.["post-status"] === "draft") return null;
if (post?.properties?.visibility === "unlisted") return null;`,
},
];
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 spec.candidates) {
if (!(await exists(filePath))) {
continue;
}
foundAnyTarget = true;
checkedFiles.add(filePath);
const source = await readFile(filePath, "utf8");
let updated = source;
let replacements = 0;
if (source.includes(spec.oldSnippet)) {
updated = source.replace(spec.oldSnippet, spec.newSnippet);
replacements = 1;
}
if (replacements === 0 || 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] No federation patch targets found");
} else if (patchedFiles.size === 0) {
console.log("[postinstall] federation unlisted guards already patched");
} else {
console.log(
`[postinstall] Patched federation unlisted guards in ${patchedFiles.size}/${checkedFiles.size} file(s)`,
);
}