diff --git a/lib/mastodon/routes/accounts.js b/lib/mastodon/routes/accounts.js index 5b6bdaf..a5cfb8e 100644 --- a/lib/mastodon/routes/accounts.js +++ b/lib/mastodon/routes/accounts.js @@ -155,6 +155,65 @@ router.get("/api/v1/accounts/lookup", async (req, res, next) => { } }); +// ─── GET /api/v1/accounts/relationships ────────────────────────────────────── +// MUST be before /accounts/:id to prevent Express matching "relationships" as :id + +router.get("/api/v1/accounts/relationships", async (req, res, next) => { + try { + let ids = req.query["id[]"] || req.query.id || []; + if (!Array.isArray(ids)) ids = [ids]; + + if (ids.length === 0) { + return res.json([]); + } + + const collections = req.app.locals.mastodonCollections; + + const [followers, following, blocked, muted] = await Promise.all([ + collections.ap_followers.find({}).toArray(), + collections.ap_following.find({}).toArray(), + collections.ap_blocked.find({}).toArray(), + collections.ap_muted.find({}).toArray(), + ]); + + const followerIds = new Set(followers.map((f) => remoteActorId(f.actorUrl))); + const followingIds = new Set(following.map((f) => remoteActorId(f.actorUrl))); + const blockedIds = new Set(blocked.map((b) => remoteActorId(b.url))); + const mutedIds = new Set(muted.filter((m) => m.url).map((m) => remoteActorId(m.url))); + + const relationships = ids.map((id) => ({ + id, + following: followingIds.has(id), + showing_reblogs: followingIds.has(id), + notifying: false, + languages: [], + followed_by: followerIds.has(id), + blocking: blockedIds.has(id), + blocked_by: false, + muting: mutedIds.has(id), + muting_notifications: mutedIds.has(id), + requested: false, + requested_by: false, + domain_blocking: false, + endorsed: false, + note: "", + })); + + res.json(relationships); + } catch (error) { + next(error); + } +}); + +// ─── GET /api/v1/accounts/familiar_followers ───────────────────────────────── +// MUST be before /accounts/:id + +router.get("/api/v1/accounts/familiar_followers", (req, res) => { + let ids = req.query["id[]"] || req.query.id || []; + if (!Array.isArray(ids)) ids = [ids]; + res.json(ids.map((id) => ({ id, accounts: [] }))); +}); + // ─── GET /api/v1/accounts/:id ──────────────────────────────────────────────── router.get("/api/v1/accounts/:id", async (req, res, next) => { @@ -183,8 +242,18 @@ router.get("/api/v1/accounts/:id", async (req, res, next) => { // Resolve remote actor from followers, following, or timeline const { actor, actorUrl } = await resolveActorData(id, collections); if (actor) { + // Try remote resolution to get real counts (followers, following, statuses) + const remoteAccount = await resolveRemoteAccount( + actorUrl, + pluginOptions, + baseUrl, + ); + if (remoteAccount) { + return res.json(remoteAccount); + } + + // Fallback to local data const account = serializeAccount(actor, { baseUrl }); - // Count this actor's posts in our timeline account.statuses_count = await collections.ap_timeline.countDocuments({ "author.url": actorUrl, }); @@ -354,66 +423,6 @@ router.get("/api/v1/accounts/:id/following", async (req, res, next) => { } }); -// ─── GET /api/v1/accounts/relationships ────────────────────────────────────── - -router.get("/api/v1/accounts/relationships", async (req, res, next) => { - try { - // id[] can come as single value or array - let ids = req.query["id[]"] || req.query.id || []; - if (!Array.isArray(ids)) ids = [ids]; - - if (ids.length === 0) { - return res.json([]); - } - - const collections = req.app.locals.mastodonCollections; - - // Load all followers/following for efficient lookup - const [followers, following, blocked, muted] = await Promise.all([ - collections.ap_followers.find({}).toArray(), - collections.ap_following.find({}).toArray(), - collections.ap_blocked.find({}).toArray(), - collections.ap_muted.find({}).toArray(), - ]); - - const followerIds = new Set(followers.map((f) => remoteActorId(f.actorUrl))); - const followingIds = new Set(following.map((f) => remoteActorId(f.actorUrl))); - const blockedIds = new Set(blocked.map((b) => remoteActorId(b.url))); - const mutedIds = new Set(muted.filter((m) => m.url).map((m) => remoteActorId(m.url))); - - const relationships = ids.map((id) => ({ - id, - following: followingIds.has(id), - showing_reblogs: followingIds.has(id), - notifying: false, - languages: [], - followed_by: followerIds.has(id), - blocking: blockedIds.has(id), - blocked_by: false, - muting: mutedIds.has(id), - muting_notifications: mutedIds.has(id), - requested: false, - requested_by: false, - domain_blocking: false, - endorsed: false, - note: "", - })); - - res.json(relationships); - } catch (error) { - next(error); - } -}); - -// ─── GET /api/v1/accounts/familiar_followers ───────────────────────────────── - -router.get("/api/v1/accounts/familiar_followers", (req, res) => { - // Stub — returns empty for each requested ID - let ids = req.query["id[]"] || req.query.id || []; - if (!Array.isArray(ids)) ids = [ids]; - res.json(ids.map((id) => ({ id, accounts: [] }))); -}); - // ─── POST /api/v1/accounts/:id/follow ─────────────────────────────────────── router.post("/api/v1/accounts/:id/follow", async (req, res, next) => { diff --git a/package.json b/package.json index 485ce6e..8828288 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "3.6.5", + "version": "3.6.6", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit",