From da89554ef99935f8e2f9960a65c0468221f12549 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Mon, 23 Mar 2026 08:30:25 +0100 Subject: [PATCH] fix(mastodon): profile avatar lost after first enrichment; actor published non-UTC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two profile display fixes: 1. Avatar not persisting across requests: resolveRemoteAccount fetches the correct avatar via lookupWithSecurity, but only updated the in-memory serialized status — never the DB or the cache. On the next request serializeStatus rebuilt the account from item.author.photo (empty if the actor was on a Secure Mode server when the item arrived), and enrichAccountStats skipped re-fetching because follower counts were already > 0. Fix: include avatarUrl in cacheAccountStats; in collectAccount always check the cache first (for avatar + createdAt) regardless of whether counts are already populated. 2. actor.published may not be UTC: Temporal.Instant.toString() preserves the original timezone offset from the AP actor object; wrap in new Date(...).toISOString() so created_at is always UTC ISO 8601. Co-Authored-By: Claude Sonnet 4.6 --- lib/mastodon/helpers/account-cache.js | 4 ++-- lib/mastodon/helpers/enrich-accounts.js | 19 ++++++++++++++----- lib/mastodon/helpers/resolve-account.js | 16 +++++++++++----- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/mastodon/helpers/account-cache.js b/lib/mastodon/helpers/account-cache.js index f4d3a21..9ee96e9 100644 --- a/lib/mastodon/helpers/account-cache.js +++ b/lib/mastodon/helpers/account-cache.js @@ -11,7 +11,7 @@ import { remoteActorId } from "./id-mapping.js"; const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour const MAX_ENTRIES = 500; -// Map +// Map const cache = new Map(); // Reverse map: accountId (hash) → actorUrl @@ -21,7 +21,7 @@ const idToUrl = new Map(); /** * Store account stats in cache. * @param {string} actorUrl - The actor's URL (cache key) - * @param {object} stats - { followersCount, followingCount, statusesCount, createdAt } + * @param {object} stats - { followersCount, followingCount, statusesCount, createdAt, avatarUrl } */ export function cacheAccountStats(actorUrl, stats) { if (!actorUrl) return; diff --git a/lib/mastodon/helpers/enrich-accounts.js b/lib/mastodon/helpers/enrich-accounts.js index 9ecc04e..fafc449 100644 --- a/lib/mastodon/helpers/enrich-accounts.js +++ b/lib/mastodon/helpers/enrich-accounts.js @@ -77,18 +77,27 @@ export async function enrichAccountStats(statuses, pluginOptions, baseUrl) { */ function collectAccount(account, map) { if (!account?.url) return; - if (account.followers_count > 0 || account.statuses_count > 0) return; - // Check cache first — if cached, apply immediately + // Always check cache first — applies avatar + createdAt even for already-enriched accounts. + // avatarUrl is stored in the cache by resolveRemoteAccount so it survives across requests + // even when the timeline item's author.photo is empty (e.g. actor was on a Secure Mode + // server when the item was originally received). const cached = getCachedAccountStats(account.url); if (cached) { - account.followers_count = cached.followersCount || 0; - account.following_count = cached.followingCount || 0; - account.statuses_count = cached.statusesCount || 0; + account.followers_count = cached.followersCount || account.followers_count || 0; + account.following_count = cached.followingCount || account.following_count || 0; + account.statuses_count = cached.statusesCount || account.statuses_count || 0; if (cached.createdAt) account.created_at = cached.createdAt; + if (cached.avatarUrl) { + account.avatar = cached.avatarUrl; + account.avatar_static = cached.avatarUrl; + } return; } + // Skip remote resolution if counts are already populated from some other source + if (account.followers_count > 0 || account.statuses_count > 0) return; + // Queue for remote resolution if (!map.has(account.url)) { map.set(account.url, []); diff --git a/lib/mastodon/helpers/resolve-account.js b/lib/mastodon/helpers/resolve-account.js index 74bc3c9..c8379fb 100644 --- a/lib/mastodon/helpers/resolve-account.js +++ b/lib/mastodon/helpers/resolve-account.js @@ -86,10 +86,15 @@ export async function resolveRemoteAccount(acct, pluginOptions, baseUrl) { if (outbox?.totalItems != null) statusesCount = outbox.totalItems; } catch { /* ignore */ } - // Get published/created date - const published = actor.published - ? String(actor.published) - : null; + // Get published/created date — normalize to UTC ISO so clients display it correctly. + // Temporal.Instant.toString() preserves the original timezone offset; + // passing through new Date() converts to "YYYY-MM-DDTHH:mm:ss.sssZ". + let published = null; + if (actor.published) { + try { + published = new Date(String(actor.published)).toISOString(); + } catch { /* ignore unparseable dates */ } + } // Profile fields from attachments const fields = []; @@ -124,12 +129,13 @@ export async function resolveRemoteAccount(acct, pluginOptions, baseUrl) { account.following_count = followingCount; account.statuses_count = statusesCount; - // Cache stats so embedded account objects in statuses can use them + // Cache stats (+ avatar URL) so embedded account objects in statuses can use them cacheAccountStats(actorUrl, { followersCount, followingCount, statusesCount, createdAt: published || undefined, + avatarUrl: avatarUrl || undefined, }); return account;