100 lines
3.6 KiB
JavaScript
100 lines
3.6 KiB
JavaScript
/**
|
||
* Patch: fix POST /api/v1/statuses response ID to match ap_timeline published date.
|
||
*
|
||
* Root cause:
|
||
* The POST /api/v1/statuses handler returns `id: String(Date.now())` — the
|
||
* wall-clock time when the response is sent. The ap_timeline item inserted by
|
||
* patch-ap-mastodon-reply-threading uses `published: data.properties.published`,
|
||
* which is set BEFORE postContent.create() runs (before the Gitea write, which
|
||
* can take several seconds).
|
||
*
|
||
* When Phanpy/Elk receives the creation response and the user then replies to that
|
||
* post, the client sends `in_reply_to_id: <id from creation response>`. The handler
|
||
* calls findTimelineItemById with that id. The ±1 second range query looks for
|
||
* published ≈ Date.now(), but the stored item has published = postCreationTime
|
||
* (potentially 5–15 seconds earlier). The range misses → inReplyTo = null →
|
||
* jf2["in-reply-to"] not set → getPostType returns "note" instead of "reply".
|
||
*
|
||
* Fix:
|
||
* 1. Also import encodeCursor alongside decodeCursor.
|
||
* 2. Use encodeCursor(data.properties.published) as the status ID in the response.
|
||
* Falls back to String(Date.now()) if published is missing/invalid.
|
||
*
|
||
* This ensures the creation response ID matches what findTimelineItemById will
|
||
* resolve in subsequent in_reply_to_id lookups.
|
||
*/
|
||
|
||
import { access, readFile, writeFile } from "node:fs/promises";
|
||
|
||
const MARKER = "// [patch] ap-mastodon-status-id";
|
||
|
||
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",
|
||
];
|
||
|
||
// Change 1: add encodeCursor to the decodeCursor import
|
||
const OLD_IMPORT = `import { decodeCursor } from "../helpers/pagination.js";`;
|
||
const NEW_IMPORT = `import { decodeCursor, encodeCursor } from "../helpers/pagination.js"; ${MARKER}`;
|
||
|
||
// Change 2: replace String(Date.now()) with encodeCursor(data.properties.published)
|
||
const OLD_ID = ` res.json({
|
||
id: String(Date.now()),`;
|
||
const NEW_ID = ` res.json({
|
||
id: encodeCursor(data.properties.published) || String(Date.now()), ${MARKER}`;
|
||
|
||
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-mastodon-status-id: already applied to ${filePath}`);
|
||
continue;
|
||
}
|
||
|
||
if (!source.includes(OLD_IMPORT)) {
|
||
console.warn(`[postinstall] patch-ap-mastodon-status-id: import snippet not found in ${filePath}`);
|
||
continue;
|
||
}
|
||
|
||
if (!source.includes(OLD_ID)) {
|
||
console.warn(`[postinstall] patch-ap-mastodon-status-id: response id snippet not found in ${filePath}`);
|
||
continue;
|
||
}
|
||
|
||
let updated = source
|
||
.replace(OLD_IMPORT, NEW_IMPORT)
|
||
.replace(OLD_ID, NEW_ID);
|
||
|
||
if (updated === source) {
|
||
console.log(`[postinstall] patch-ap-mastodon-status-id: no changes in ${filePath}`);
|
||
continue;
|
||
}
|
||
|
||
await writeFile(filePath, updated, "utf8");
|
||
patched += 1;
|
||
console.log(`[postinstall] Applied patch-ap-mastodon-status-id to ${filePath}`);
|
||
}
|
||
|
||
if (checked === 0) {
|
||
console.log("[postinstall] patch-ap-mastodon-status-id: no target files found");
|
||
} else if (patched === 0) {
|
||
console.log("[postinstall] patch-ap-mastodon-status-id: already up to date");
|
||
} else {
|
||
console.log(`[postinstall] patch-ap-mastodon-status-id: patched ${patched} file(s)`);
|
||
}
|