diff --git a/lib/mastodon/entities/account.js b/lib/mastodon/entities/account.js index 425f48f..217ad38 100644 --- a/lib/mastodon/entities/account.js +++ b/lib/mastodon/entities/account.js @@ -7,6 +7,7 @@ */ import { accountId } from "../helpers/id-mapping.js"; import { sanitizeHtml, stripHtml } from "./sanitize.js"; +import { getCachedAccountStats } from "../helpers/account-cache.js"; /** * Serialize an actor as a Mastodon Account entity. @@ -111,6 +112,20 @@ export function serializeAccount(actor, { baseUrl, isLocal = false, handle = "" statuses_count: actor.statusesCount || 0, followers_count: actor.followersCount || 0, following_count: actor.followingCount || 0, + // Enrich from cache if counts are 0 (embedded accounts in statuses lack counts) + ...((!actor.statusesCount && !actor.followersCount && !isLocal) + ? (() => { + const cached = getCachedAccountStats(url); + return cached + ? { + statuses_count: cached.statusesCount || 0, + followers_count: cached.followersCount || 0, + following_count: cached.followingCount || 0, + created_at: cached.createdAt || actor.createdAt || new Date().toISOString(), + } + : {}; + })() + : {}), moved: actor.movedTo || null, suspended: false, limited: false, diff --git a/lib/mastodon/helpers/account-cache.js b/lib/mastodon/helpers/account-cache.js new file mode 100644 index 0000000..0407855 --- /dev/null +++ b/lib/mastodon/helpers/account-cache.js @@ -0,0 +1,51 @@ +/** + * In-memory cache for remote account stats (followers, following, statuses). + * + * Populated by resolveRemoteAccount() when a profile is fetched. + * Read by serializeAccount() to enrich embedded account objects in statuses. + * + * LRU-style with TTL — entries expire after 1 hour. + */ + +const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour +const MAX_ENTRIES = 500; + +// Map +const cache = new Map(); + +/** + * Store account stats in cache. + * @param {string} actorUrl - The actor's URL (cache key) + * @param {object} stats - { followersCount, followingCount, statusesCount, createdAt } + */ +export function cacheAccountStats(actorUrl, stats) { + if (!actorUrl) return; + + // Evict oldest if at capacity + if (cache.size >= MAX_ENTRIES) { + const oldest = cache.keys().next().value; + cache.delete(oldest); + } + + cache.set(actorUrl, { ...stats, cachedAt: Date.now() }); +} + +/** + * Get cached account stats. + * @param {string} actorUrl - The actor's URL + * @returns {object|null} Stats or null if not cached/expired + */ +export function getCachedAccountStats(actorUrl) { + if (!actorUrl) return null; + + const entry = cache.get(actorUrl); + if (!entry) return null; + + // Check TTL + if (Date.now() - entry.cachedAt > CACHE_TTL_MS) { + cache.delete(actorUrl); + return null; + } + + return entry; +} diff --git a/lib/mastodon/helpers/resolve-account.js b/lib/mastodon/helpers/resolve-account.js index d83b08a..cd4cbe4 100644 --- a/lib/mastodon/helpers/resolve-account.js +++ b/lib/mastodon/helpers/resolve-account.js @@ -5,6 +5,7 @@ * Shared by accounts.js (lookup) and search.js (resolve=true). */ import { serializeAccount } from "../entities/account.js"; +import { cacheAccountStats } from "./account-cache.js"; /** * @param {string} acct - Account identifier (user@domain or URL) @@ -115,6 +116,14 @@ 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 + cacheAccountStats(actorUrl, { + followersCount, + followingCount, + statusesCount, + createdAt: published || undefined, + }); + return account; } catch (error) { console.warn(`[Mastodon API] Remote account resolution failed for ${acct}:`, error.message); diff --git a/package.json b/package.json index 8828288..7b060bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "3.6.6", + "version": "3.6.7", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit",