From 6c13eb85a5201ba01e6d35f84013db78acfd6d88 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Mon, 23 Mar 2026 07:20:50 +0100 Subject: [PATCH] fix(mastodon-api): pass createdAt for follower/following accounts; URL-type AP lookup - In accounts.js: all places that build an actor object from ap_followers or ap_following docs now include `createdAt: f.createdAt || undefined`. Previously the field was omitted, causing serializeAccount() to fall back to `new Date().toISOString()`, making every follower/following appear to have joined "just now" in the Mastodon client. Affected: GET /api/v1/accounts/:id/followers, /following, /lookup, and the resolveActorData() fallback used by GET /api/v1/accounts/:id. - In resolve-account.js: HTTP actor URLs are now passed to lookupWithSecurity() as a native URL object instead of a bare string, matching Fedify's preferred type. The acct:user@domain WebFinger path remains a string (new URL() would misparse the @ as a user-info separator under WHATWG rules). Co-Authored-By: Claude Sonnet 4.6 --- lib/mastodon/helpers/resolve-account.js | 6 ++++-- lib/mastodon/routes/accounts.js | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/mastodon/helpers/resolve-account.js b/lib/mastodon/helpers/resolve-account.js index 811090c..74bc3c9 100644 --- a/lib/mastodon/helpers/resolve-account.js +++ b/lib/mastodon/helpers/resolve-account.js @@ -24,7 +24,9 @@ export async function resolveRemoteAccount(acct, pluginOptions, baseUrl) { { handle, publicationUrl }, ); - // Determine lookup URI + // Determine lookup URI. + // acct:user@domain — kept as a string; Fedify resolves it via WebFinger. + // HTTP URLs — converted to URL objects for type-correct AP object fetch. let actorUri; if (acct.includes("@")) { const parts = acct.replace(/^@/, "").split("@"); @@ -33,7 +35,7 @@ export async function resolveRemoteAccount(acct, pluginOptions, baseUrl) { if (!username || !domain) return null; actorUri = `acct:${username}@${domain}`; } else if (acct.startsWith("http")) { - actorUri = acct; + actorUri = new URL(acct); } else { return null; } diff --git a/lib/mastodon/routes/accounts.js b/lib/mastodon/routes/accounts.js index 07a0ddb..10ee6b7 100644 --- a/lib/mastodon/routes/accounts.js +++ b/lib/mastodon/routes/accounts.js @@ -112,7 +112,7 @@ router.get("/api/v1/accounts/lookup", async (req, res, next) => { if (follower) { return res.json( serializeAccount( - { name: follower.name, url: follower.actorUrl, photo: follower.avatar, handle: follower.handle, bannerUrl: follower.banner || "" }, + { name: follower.name, url: follower.actorUrl, photo: follower.avatar, handle: follower.handle, bannerUrl: follower.banner || "", createdAt: follower.createdAt || undefined }, { baseUrl }, ), ); @@ -128,7 +128,7 @@ router.get("/api/v1/accounts/lookup", async (req, res, next) => { if (following) { return res.json( serializeAccount( - { name: following.name, url: following.actorUrl, photo: following.avatar, handle: following.handle }, + { name: following.name, url: following.actorUrl, photo: following.avatar, handle: following.handle, createdAt: following.createdAt || undefined }, { baseUrl }, ), ); @@ -396,7 +396,7 @@ router.get("/api/v1/accounts/:id/followers", async (req, res, next) => { const accounts = followers.map((f) => serializeAccount( - { name: f.name, url: f.actorUrl, photo: f.avatar, handle: f.handle, bannerUrl: f.banner || "" }, + { name: f.name, url: f.actorUrl, photo: f.avatar, handle: f.handle, bannerUrl: f.banner || "", createdAt: f.createdAt || undefined }, { baseUrl }, ), ); @@ -429,7 +429,7 @@ router.get("/api/v1/accounts/:id/following", async (req, res, next) => { const accounts = following.map((f) => serializeAccount( - { name: f.name, url: f.actorUrl, photo: f.avatar, handle: f.handle, bannerUrl: f.banner || "" }, + { name: f.name, url: f.actorUrl, photo: f.avatar, handle: f.handle, bannerUrl: f.banner || "", createdAt: f.createdAt || undefined }, { baseUrl }, ), ); @@ -786,6 +786,7 @@ async function resolveActorData(id, collections) { photo: f.avatar, handle: f.handle, bannerUrl: f.banner || "", + createdAt: f.createdAt || undefined, }, actorUrl: f.actorUrl, }; @@ -803,6 +804,7 @@ async function resolveActorData(id, collections) { photo: f.avatar, handle: f.handle, bannerUrl: f.banner || "", + createdAt: f.createdAt || undefined, }, actorUrl: f.actorUrl, };