mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
fix(mastodon): profile avatar lost after first enrichment; actor published non-UTC
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 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import { remoteActorId } from "./id-mapping.js";
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||
const MAX_ENTRIES = 500;
|
||||
|
||||
// Map<actorUrl, { followersCount, followingCount, statusesCount, createdAt, cachedAt }>
|
||||
// Map<actorUrl, { followersCount, followingCount, statusesCount, createdAt, avatarUrl, cachedAt }>
|
||||
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;
|
||||
|
||||
@@ -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, []);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user