0
2
// Who a private gallery is shared with.
// GET /xrpc/social.grain.unspecced.getSpaceMembers?space=...
//
// The member list lives on the space's authority and is not enumerated to the
// network, so this only works for the author, through their own session. Grain's
// _space_invites table is not consulted: it records what grain shared, while
// this answers what the space actually holds.
import { defineQuery, InvalidRequestError } from "$hatk";
import { listGalleryMembers, parseSpaceUri } from "../spaces/client.ts";
import { throwSpaceError } from "../spaces/errors.ts";
interface MemberRow {
did: string;
}
export default defineQuery("social.grain.unspecced.getSpaceMembers", async (ctx) => {
const { ok, db, viewer, pds, params, lookup, blobUrl } = ctx;
if (!viewer) throw new InvalidRequestError("Authentication required");
let authority: string;
try {
({ authority } = parseSpaceUri(params.space));
} catch {
throw new InvalidRequestError(`Not a space uri: ${params.space}`);
}
if (authority !== viewer.did) {
throw new InvalidRequestError("Only the gallery's author can see its members", "NotAuthorized");
}
let dids: string[];
try {
const body = await listGalleryMembers(pds, params.space, 100);
// pds.js's createSpace puts the owner on their own member list, since
// membership is what a member-list policy consults. True, and not what
// "shared with" means to the person reading it.
dids = ((body.members ?? []) as MemberRow[])
.map((m) => m.did)
.filter((did) => did !== authority);
} catch (err) {
return throwSpaceError(err, db, viewer.did);
}
if (dids.length === 0) return ok({ members: [] });
// Profiles come from the public index, which may know nothing about an
// account on a PDS grain has never seen. A bare DID is still a member.
const profiles = await lookup("social.grain.actor.profile", "did", dids);
const handleRows = (await db.query(
`SELECT did, handle FROM _repos WHERE did IN (${dids.map((_, i) => `$${i + 1}`).join(",")})`,
dids,
)) as { did: string; handle: string }[];
const handles = new Map(handleRows.map((r) => [r.did, r.handle]));
return ok({
members: dids.map((did) => {
const profile = profiles.get(did);
const value = profile?.value as { displayName?: string; avatar?: unknown } | undefined;
return {
did,
...(handles.get(did) ? { handle: handles.get(did) } : {}),
...(value?.displayName ? { displayName: value.displayName } : {}),
...(profile ? { avatar: blobUrl(did, value?.avatar, "avatar") ?? undefined } : {}),
};
}),
});
});