From ccb9cc99a200e7b2698c09552dec57633241d7b4 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Sat, 21 Mar 2026 17:50:48 +0100 Subject: [PATCH] fix: follow/unfollow fails for remotely resolved profiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/mastodon/helpers/account-cache.js | 19 +++++++++++++++++++ lib/mastodon/routes/accounts.js | 5 +++++ package.json | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/mastodon/helpers/account-cache.js b/lib/mastodon/helpers/account-cache.js index 0407855..e061e58 100644 --- a/lib/mastodon/helpers/account-cache.js +++ b/lib/mastodon/helpers/account-cache.js @@ -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 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; +} diff --git a/lib/mastodon/routes/accounts.js b/lib/mastodon/routes/accounts.js index a5cfb8e..66239cb 100644 --- a/lib/mastodon/routes/accounts.js +++ b/lib/mastodon/routes/accounts.js @@ -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) { diff --git a/package.json b/package.json index 36083ba..f9408b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "3.7.0", + "version": "3.7.1", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit",