~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2
67 lines · 2.6 KB Raw History
// 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 } : {}),
      };
    }),
  });
});