fix: resolve posts by AP URL in search when resolve=true
Deploy Indiekit Server / deploy (push) Successful in 1m14s

Phanpy calls GET /api/v2/search?q=<post_url>&resolve=true&type=statuses
before liking or boosting a remote post. The search endpoint only did
content-text regex matching — never found posts by URL — so it returned
statuses:[] and Phanpy showed "Failed to load post."

Patch ap-search-url-resolve adds a URL-based ap_timeline lookup (by uid
or url) when resolve=true and the query starts with "http", covering
Threads posts and any other AP posts already in the timeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sven
2026-04-15 20:35:00 +02:00
parent 60076225db
commit 41660a96fe
+67
View File
@@ -152,4 +152,71 @@ for (const f of ACCOUNTS_CANDIDATES) {
}
if (!hashDone) console.log(`[postinstall] ${SCRIPT}: ap-accounts-id-hash — no target file found or no changes`);
// ── patch-ap-search-url-resolve: resolve posts by URL when resolve=true ────────
// Phanpy calls GET /api/v2/search?q=<post_url>&resolve=true&type=statuses before
// liking/boosting a remote post. The search endpoint only did content-text regex
// search — never found posts by URL → statuses:[] → "Failed to load post" error.
// Fix: when resolve=true and query is a URL, look up ap_timeline by uid/url first.
// Two targeted replacements:
// A) inject URL-resolve block before the content search (unique anchor: "results.statuses = items.map")
// B) change assignment to push so existing array stays intact
const SEARCH_MARKER = "// [patch] ap-search-url-resolve";
const SEARCH_CANDIDATES = apPath("lib/mastodon/routes/search.js");
// Replacement A: inject URL-resolve block + switch assignment to push
// Anchor chosen because it is unique in search.js.
const SEARCH_OLD_A = " results.statuses = items.map((item) =>";
const SEARCH_NEW_A = [
" // URL resolve: find post by AP URL before content search. " + SEARCH_MARKER,
" if (resolve && query.startsWith(\"http\")) { " + SEARCH_MARKER,
" const resolvedItem = await collections.ap_timeline.findOne({ " + SEARCH_MARKER,
" isContext: { $ne: true }, " + SEARCH_MARKER,
" $or: [{ uid: query }, { url: query }], " + SEARCH_MARKER,
" }); " + SEARCH_MARKER,
" if (resolvedItem) { " + SEARCH_MARKER,
" results.statuses.push(serializeStatus(resolvedItem, { " + SEARCH_MARKER,
" baseUrl, favouritedIds: new Set(), rebloggedIds: new Set(), " + SEARCH_MARKER,
" bookmarkedIds: new Set(), pinnedIds: new Set(), " + SEARCH_MARKER,
" })); " + SEARCH_MARKER,
" } " + SEARCH_MARKER,
" } " + SEARCH_MARKER,
" results.statuses.push(...items.map((item) =>",
].join("\n");
// Replacement B: close the push call (was ` );` → ` ));`)
// Unique because it is immediately followed by the closing brace + Hashtag comment.
const SEARCH_OLD_B = " }),\n );\n }\n\n // ─── Hashtag";
const SEARCH_NEW_B = " }),\n ));\n }\n\n // ─── Hashtag";
let searchDone = false;
for (const f of SEARCH_CANDIDATES) {
if (!(await fileExists(f))) continue;
const src = await readFile(f, "utf8");
if (src.includes(SEARCH_MARKER)) {
console.log(`[postinstall] ${SCRIPT}: ap-search-url-resolve already applied in ${f}`);
searchDone = true; break;
}
let updated = src;
let changed = false;
if (updated.includes(SEARCH_OLD_A)) {
updated = updated.replace(SEARCH_OLD_A, SEARCH_NEW_A);
changed = true;
} else {
console.warn(`[postinstall] ${SCRIPT}: ap-search-url-resolve A — anchor not found in ${f}`);
}
if (updated.includes(SEARCH_OLD_B)) {
updated = updated.replace(SEARCH_OLD_B, SEARCH_NEW_B);
} else {
console.warn(`[postinstall] ${SCRIPT}: ap-search-url-resolve B — closing anchor not found in ${f}`);
changed = false; // both must succeed
}
if (changed && updated !== src) {
await writeFile(f, updated, "utf8");
console.log(`[postinstall] ${SCRIPT}: applied ap-search-url-resolve to ${f}`);
total++; searchDone = true; break;
}
}
if (!searchDone) console.log(`[postinstall] ${SCRIPT}: ap-search-url-resolve — no target file found or no changes`);
console.log(`[postinstall] ${SCRIPT}: done (${total} patch(es) applied)`);