patch: short Mastodon posts skip Micropub blog pipeline
Deploy Indiekit Server / deploy (push) Successful in 1m37s
Deploy Indiekit Server / deploy (push) Successful in 1m37s
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Patch: skip Micropub blog post creation for short standalone Mastodon posts.
|
||||
*
|
||||
* Very short status texts (≤ 280 chars, no media, no reply) are fediverse-only
|
||||
* interactions — they federate via AP but must NOT create a permanent blog entry.
|
||||
* Posts with in_reply_to, media, or text > 280 characters go through the full
|
||||
* Micropub pipeline as before.
|
||||
*
|
||||
* Short posts get a canonical URL at /activitypub/objects/note/{uuid} (same as
|
||||
* DMs), are stored in ap_timeline, and delivered to followers via Fedify's
|
||||
* ctx.sendActivity({ "followers" }) — the same mechanism the AP syndicator uses.
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const candidates = [
|
||||
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js",
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js",
|
||||
];
|
||||
|
||||
const MARKER = "// [patch] short-post-bypass";
|
||||
|
||||
const OLD = ` // Resolve in_reply_to URL from status ID (cursor or ObjectId)
|
||||
let inReplyTo = null;
|
||||
if (inReplyToId) {`;
|
||||
|
||||
const NEW = ` // Short standalone posts (≤ 280 chars, no media, no reply) skip Micropub
|
||||
// and go directly to AP — no blog entry is created. // [patch] short-post-bypass
|
||||
const _shortPostLimit = 280;
|
||||
const _isShortPost =
|
||||
!inReplyToId &&
|
||||
(!mediaIds || mediaIds.length === 0) &&
|
||||
visibility !== "direct" &&
|
||||
(statusText || "").trim().length > 0 &&
|
||||
(statusText || "").trim().length <= _shortPostLimit;
|
||||
|
||||
if (_isShortPost) {
|
||||
const _spPluginOptions = req.app.locals.mastodonPluginOptions || {};
|
||||
const _spPublicationUrl = (_spPluginOptions.publicationUrl || baseUrl).replace(/\\/+$/, "");
|
||||
const _spFederation = _spPluginOptions.federation;
|
||||
const _spHandle = _spPluginOptions.handle || "user";
|
||||
const _spHostname = (() => { try { return new URL(_spPublicationUrl).hostname; } catch { return ""; } })();
|
||||
const _spProfile = await collections.ap_profile.findOne({});
|
||||
|
||||
const { randomUUID } = await import("node:crypto");
|
||||
const _spUuid = randomUUID();
|
||||
const _spNoteId = \`\${_spPublicationUrl}/activitypub/objects/note/\${_spUuid}\`;
|
||||
const _spNow = new Date().toISOString();
|
||||
|
||||
const _spText = (statusText || "").trim();
|
||||
const _spHtml = _spText
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/(https?:\\/\\/[^\\s<>&"')\\]]+)/g, '<a href="$1">$1</a>')
|
||||
.replace(/\\n/g, "<br>");
|
||||
|
||||
const _spTlItem = await addTimelineItem(collections, {
|
||||
uid: _spNoteId,
|
||||
url: _spNoteId,
|
||||
type: "note",
|
||||
content: { text: _spText, html: \`<p>\${_spHtml}</p>\` },
|
||||
author: {
|
||||
name: _spProfile?.name || _spHandle,
|
||||
url: _spProfile?.url || _spPublicationUrl,
|
||||
photo: _spProfile?.icon || "",
|
||||
handle: \`@\${_spHandle}@\${_spHostname}\`,
|
||||
emojis: [],
|
||||
bot: false,
|
||||
},
|
||||
published: _spNow,
|
||||
createdAt: _spNow,
|
||||
inReplyTo: null,
|
||||
visibility: visibility || "public",
|
||||
sensitive: sensitive === true || sensitive === "true",
|
||||
category: [],
|
||||
counts: { likes: 0, boosts: 0, replies: 0 },
|
||||
});
|
||||
|
||||
if (_spFederation) {
|
||||
try {
|
||||
const { Note, Create } = await import("@fedify/fedify/vocab");
|
||||
const _spCtx = _spFederation.createContext(
|
||||
new URL(_spPublicationUrl + "/"),
|
||||
{ handle: _spHandle, publicationUrl: _spPublicationUrl },
|
||||
);
|
||||
const _spActorUri = _spCtx.getActorUri(_spHandle);
|
||||
const _spNote = new Note({
|
||||
id: new URL(_spNoteId),
|
||||
attributedTo: _spActorUri,
|
||||
content: \`<p>\${_spHtml}</p>\`,
|
||||
published: new Date(_spNow),
|
||||
});
|
||||
const _spCreate = new Create({
|
||||
id: new URL(\`\${_spNoteId}#create\`),
|
||||
actor: _spActorUri,
|
||||
object: _spNote,
|
||||
});
|
||||
await _spCtx.sendActivity(
|
||||
{ identifier: _spHandle },
|
||||
"followers",
|
||||
_spCreate,
|
||||
{ preferSharedInbox: true, syncCollection: true, orderingKey: _spNoteId },
|
||||
);
|
||||
console.info(\`[Mastodon API] Short post federated to followers (no blog entry): \${_spNoteId}\`);
|
||||
} catch (_spApErr) {
|
||||
console.warn(\`[Mastodon API] Short post AP federation error: \${_spApErr.message}\`);
|
||||
}
|
||||
}
|
||||
|
||||
const _spStatus = serializeStatus(_spTlItem, {
|
||||
baseUrl,
|
||||
favouritedIds: new Set(),
|
||||
rebloggedIds: new Set(),
|
||||
bookmarkedIds: new Set(),
|
||||
pinnedIds: new Set(),
|
||||
});
|
||||
|
||||
if (idempotencyKey && collections.ap_idempotency) {
|
||||
const { createHash } = await import("node:crypto");
|
||||
const _spKey = createHash("sha256").update(\`\${baseUrl}:\${idempotencyKey}\`).digest("hex");
|
||||
await collections.ap_idempotency.insertOne({ key: _spKey, response: _spStatus, createdAt: new Date() }).catch(() => {});
|
||||
}
|
||||
|
||||
return res.json(_spStatus);
|
||||
}
|
||||
|
||||
// Resolve in_reply_to URL from status ID (cursor or ObjectId)
|
||||
let inReplyTo = null;
|
||||
if (inReplyToId) {`;
|
||||
|
||||
async function exists(p) {
|
||||
try { await access(p); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
let patched = false;
|
||||
for (const filePath of candidates) {
|
||||
if (!(await exists(filePath))) continue;
|
||||
const src = await readFile(filePath, "utf8");
|
||||
if (src.includes(MARKER)) {
|
||||
console.log(`[postinstall] patch-ap-mastodon-short-post-bypass: already applied in ${filePath}`);
|
||||
patched = true;
|
||||
break;
|
||||
}
|
||||
if (!src.includes(OLD)) {
|
||||
console.log(`[postinstall] patch-ap-mastodon-short-post-bypass: target snippet not found in ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
await writeFile(filePath, src.replace(OLD, NEW), "utf8");
|
||||
console.log(`[postinstall] patch-ap-mastodon-short-post-bypass: applied to ${filePath}`);
|
||||
patched = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!patched) {
|
||||
console.log("[postinstall] patch-ap-mastodon-short-post-bypass: no target file found");
|
||||
}
|
||||
Reference in New Issue
Block a user