0
2
// Returns followers of `actor` that `viewer` also follows.
// GET /xrpc/social.grain.unspecced.getKnownFollowers?actor=did:...&viewer=did:...
import { defineQuery, type GrainActorProfile } from "$hatk";
import { lookupHandles } from "../helpers/lookupHandles.ts";
import { blockFilter } from "../filters/blockMute.ts";
import { resolveHandle } from "../helpers/resolveHandle.ts";
export default defineQuery("social.grain.unspecced.getKnownFollowers", async (ctx) => {
const { ok, params, lookup, blobUrl } = ctx;
const { viewer, limit = 50 } = params;
const actor = params.actor ? await resolveHandle(ctx.db, params.actor) : null;
if (!actor || !viewer || actor === viewer) return ok({ items: [] });
// Find DIDs that follow `actor` AND are followed by `viewer`.
//
// Taken-down accounts are hidden from everyone, matching every feed. Blocked
// accounts (either direction) are hidden too — this list is rendered as a
// facepile on the profile, and a block should not leave a face behind.
// Mutes are not applied, matching getGalleryFavorites: a mute hides someone's
// content, not the fact that they follow someone.
//
// GROUP BY collapses the fan-out from the f2 join, which duplicates a row
// whenever the viewer holds more than one follow record for the same account
// (this happens in prod). Deduping after LIMIT would silently shrink the page.
const rows = (await ctx.db.query(
`SELECT f1.did AS did
FROM "social.grain.graph.follow" f1
JOIN "social.grain.graph.follow" f2 ON f2.did = $2 AND f2.subject = f1.did
LEFT JOIN _repos r ON r.did = f1.did
WHERE f1.subject = $1
AND (r.status IS NULL OR r.status NOT IN ('takendown', 'deactivated', 'deleted'))
AND ${blockFilter("f1.did", "$2")}
GROUP BY f1.did
ORDER BY MAX(f1.created_at) DESC
LIMIT $3`,
[actor, viewer, Number(limit)],
)) as { did: string }[];
const dids = [...new Set(rows.map((r) => r.did))];
const profiles = await lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids);
const handleMap = await lookupHandles(ctx.db, dids);
const items = dids.map((did) => {
const p = profiles.get(did);
return {
did,
handle: p?.handle ?? handleMap.get(did) ?? did,
displayName: p?.value.displayName,
description: p?.value.description,
avatar: p ? blobUrl(did, p.value.avatar, "avatar") : undefined,
};
});
return ok({ items });
});