fix(activitypub): populate in_reply_to_id in Mastodon status serializer
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m11s
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m11s
- status.js: in_reply_to_id was always null (both branches of ternary returned null — TODO left unfilled). Changed to item.inReplyToId || null. - statuses.js POST handler: timeline insert now stores inReplyToId from the in_reply_to_id cursor the client already sent, so own replies are threaded correctly in Phanpy/Elk. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
96
scripts/patch-ap-status-reply-id.mjs
Normal file
96
scripts/patch-ap-status-reply-id.mjs
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Patch: fix in_reply_to_id always being null in Mastodon status serializer.
|
||||
*
|
||||
* Bug:
|
||||
* status.js line 207:
|
||||
* in_reply_to_id: item.inReplyTo ? null : null, // TODO: resolve to local ID
|
||||
*
|
||||
* Both branches of the ternary return null, so in_reply_to_id is ALWAYS null.
|
||||
* Mastodon clients (Phanpy, Elk) use this field to display reply threading —
|
||||
* without it, replies appear as standalone posts with no thread context.
|
||||
*
|
||||
* Fix (two changes):
|
||||
*
|
||||
* A) status.js — use item.inReplyToId (the encoded cursor of the parent post)
|
||||
* instead of the tautological null.
|
||||
*
|
||||
* B) statuses.js POST /api/v1/statuses handler — when pre-inserting own posts
|
||||
* into ap_timeline (reply-threading patch), also store
|
||||
* inReplyToId: inReplyToId || null
|
||||
* (inReplyToId is already in scope as the raw in_reply_to_id param from the
|
||||
* client, which IS a valid encodeCursor value.)
|
||||
*
|
||||
* Note: inbound AP replies from remote servers will still have inReplyToId = null
|
||||
* until a separate patch populates it from ap_timeline lookups. Own replies via
|
||||
* the Mastodon client API are fully fixed by this patch.
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const MARKER = "// [patch] ap-status-reply-id";
|
||||
|
||||
// ── Change A: fix tautological null in status.js ──────────────────────────────
|
||||
|
||||
const statusEntityCandidates = [
|
||||
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/entities/status.js",
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/entities/status.js",
|
||||
];
|
||||
|
||||
const OLD_TAUTOLOGY = ` in_reply_to_id: item.inReplyTo ? null : null, // TODO: resolve to local ID`;
|
||||
const NEW_REPLY_ID = ` in_reply_to_id: item.inReplyToId || null, ${MARKER}`;
|
||||
|
||||
// ── Change B: store inReplyToId in the Mastodon API timeline insert ───────────
|
||||
|
||||
const statusesRouteCandidates = [
|
||||
"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 OLD_REPLY_INSERT = ` inReplyTo: inReplyTo || null, // [patch] ap-mastodon-reply-threading`;
|
||||
const NEW_REPLY_INSERT = ` inReplyTo: inReplyTo || null, // [patch] ap-mastodon-reply-threading
|
||||
inReplyToId: inReplyToId || null, ${MARKER}`;
|
||||
|
||||
async function exists(p) {
|
||||
try { await access(p); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
async function applyPatch(candidates, oldSnippet, newSnippet, label) {
|
||||
let checked = 0;
|
||||
let patched = 0;
|
||||
|
||||
for (const filePath of candidates) {
|
||||
if (!(await exists(filePath))) continue;
|
||||
checked++;
|
||||
|
||||
const source = await readFile(filePath, "utf8");
|
||||
if (source.includes(MARKER)) {
|
||||
console.log(`[postinstall] patch-ap-status-reply-id: ${label} already applied to ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!source.includes(oldSnippet)) {
|
||||
console.warn(`[postinstall] patch-ap-status-reply-id: ${label} snippet not found in ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await writeFile(filePath, source.replace(oldSnippet, newSnippet), "utf8");
|
||||
patched++;
|
||||
console.log(`[postinstall] Applied patch-ap-status-reply-id (${label}) to ${filePath}`);
|
||||
}
|
||||
|
||||
return { checked, patched };
|
||||
}
|
||||
|
||||
const a = await applyPatch(statusEntityCandidates, OLD_TAUTOLOGY, NEW_REPLY_ID, "status entity");
|
||||
const b = await applyPatch(statusesRouteCandidates, OLD_REPLY_INSERT, NEW_REPLY_INSERT, "timeline insert");
|
||||
|
||||
const totalChecked = a.checked + b.checked;
|
||||
const totalPatched = a.patched + b.patched;
|
||||
|
||||
if (totalChecked === 0) {
|
||||
console.log("[postinstall] patch-ap-status-reply-id: no target files found");
|
||||
} else if (totalPatched === 0) {
|
||||
console.log("[postinstall] patch-ap-status-reply-id: already up to date");
|
||||
} else {
|
||||
console.log(`[postinstall] patch-ap-status-reply-id: patched ${totalPatched} file(s)`);
|
||||
}
|
||||
Reference in New Issue
Block a user