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 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-23 07:20:50 +01:00
parent ed18446e05
commit 6c13eb85a5
2 changed files with 10 additions and 6 deletions

View File

@@ -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;
}

View File

@@ -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,
};