fix: follow/unfollow fails for remotely resolved profiles

POST /accounts/:id/follow returned 404 for actors resolved via Fedify
(like @_followback@tags.pub) because resolveActorUrl only checked local
data (followers/following/timeline). These actors aren't in local
collections — they were resolved on-demand via WebFinger.

Fix: add reverse lookup map (accountId hash → actorUrl) to the account
cache. When resolveRemoteAccount resolves a profile, the hash-to-URL
mapping is stored alongside the stats. resolveActorUrl checks this
cache before scanning local collections.
This commit is contained in:
Ricardo
2026-03-21 17:50:48 +01:00
parent 30eff8e6c7
commit ccb9cc99a2
3 changed files with 25 additions and 1 deletions

View File

@@ -7,12 +7,18 @@
* LRU-style with TTL — entries expire after 1 hour.
*/
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 }>
const cache = new Map();
// Reverse map: accountId (hash) → actorUrl
// Populated alongside the stats cache for follow/unfollow lookups
const idToUrl = new Map();
/**
* Store account stats in cache.
* @param {string} actorUrl - The actor's URL (cache key)
@@ -28,6 +34,10 @@ export function cacheAccountStats(actorUrl, stats) {
}
cache.set(actorUrl, { ...stats, cachedAt: Date.now() });
// Maintain reverse lookup
const hashId = remoteActorId(actorUrl);
if (hashId) idToUrl.set(hashId, actorUrl);
}
/**
@@ -49,3 +59,12 @@ export function getCachedAccountStats(actorUrl) {
return entry;
}
/**
* Reverse lookup: get actor URL from account hash ID.
* @param {string} hashId - The 24-char hex account ID
* @returns {string|null} Actor URL or null
*/
export function getActorUrlFromId(hashId) {
return idToUrl.get(hashId) || null;
}

View File

@@ -10,6 +10,7 @@ import { serializeStatus } from "../entities/status.js";
import { accountId, remoteActorId } from "../helpers/id-mapping.js";
import { buildPaginationQuery, parseLimit, setPaginationHeaders } from "../helpers/pagination.js";
import { resolveRemoteAccount } from "../helpers/resolve-account.js";
import { getActorUrlFromId } from "../helpers/account-cache.js";
const router = express.Router(); // eslint-disable-line new-cap
@@ -714,6 +715,10 @@ async function resolveActorUrl(id, collections) {
return profile.url;
}
// Check account cache reverse lookup (populated by resolveRemoteAccount)
const cachedUrl = getActorUrlFromId(id);
if (cachedUrl) return cachedUrl;
// Check followers
const followers = await collections.ap_followers.find({}).toArray();
for (const f of followers) {