fix(mastodon): use lookupWithSecurity for remote profile resolution

Replace direct ctx.lookupObject() call in resolveRemoteAccount with
lookupWithSecurity() so servers that reject signed GETs are retried
unsigned. Also add 5 s Promise.race timeouts to followers/following/
outbox collection fetches to prevent profile loads from hanging on
slow remote servers.

Fixes missing profile pictures and zero follower stats in Mastodon
client views.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-22 21:24:03 +01:00
parent 7b838ea295
commit ed18446e05

View File

@@ -6,6 +6,7 @@
*/ */
import { serializeAccount } from "../entities/account.js"; import { serializeAccount } from "../entities/account.js";
import { cacheAccountStats } from "./account-cache.js"; import { cacheAccountStats } from "./account-cache.js";
import { lookupWithSecurity } from "../../lookup-helpers.js";
/** /**
* @param {string} acct - Account identifier (user@domain or URL) * @param {string} acct - Account identifier (user@domain or URL)
@@ -37,7 +38,9 @@ export async function resolveRemoteAccount(acct, pluginOptions, baseUrl) {
return null; return null;
} }
const actor = await ctx.lookupObject(actorUri); // Use signed→unsigned fallback so servers rejecting signed GETs still resolve
const documentLoader = await ctx.getDocumentLoader({ identifier: handle });
const actor = await lookupWithSecurity(ctx, actorUri, { documentLoader });
if (!actor) return null; if (!actor) return null;
// Extract data from the Fedify actor object // Extract data from the Fedify actor object
@@ -61,20 +64,23 @@ export async function resolveRemoteAccount(acct, pluginOptions, baseUrl) {
headerUrl = image?.url?.href || ""; headerUrl = image?.url?.href || "";
} catch { /* ignore */ } } catch { /* ignore */ }
// Get collection counts (followers, following, outbox) // Get collection counts (followers, following, outbox) — with 5 s timeout each
const withTimeout = (promise, ms = 5000) =>
Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms))]);
let followersCount = 0; let followersCount = 0;
let followingCount = 0; let followingCount = 0;
let statusesCount = 0; let statusesCount = 0;
try { try {
const followers = await actor.getFollowers(); const followers = await withTimeout(actor.getFollowers());
if (followers?.totalItems != null) followersCount = followers.totalItems; if (followers?.totalItems != null) followersCount = followers.totalItems;
} catch { /* ignore */ } } catch { /* ignore */ }
try { try {
const following = await actor.getFollowing(); const following = await withTimeout(actor.getFollowing());
if (following?.totalItems != null) followingCount = following.totalItems; if (following?.totalItems != null) followingCount = following.totalItems;
} catch { /* ignore */ } } catch { /* ignore */ }
try { try {
const outbox = await actor.getOutbox(); const outbox = await withTimeout(actor.getOutbox());
if (outbox?.totalItems != null) statusesCount = outbox.totalItems; if (outbox?.totalItems != null) statusesCount = outbox.totalItems;
} catch { /* ignore */ } } catch { /* ignore */ }