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

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2
104 lines · 3.7 KB Raw History
// Private galleries other accounts have shared with the viewer.
//   GET /xrpc/social.grain.unspecced.listSharedGalleries
//
// The one place grain keeps something the protocol does not. A space's member
// list lives with its authority and being added leaves no trace on the member's
// own PDS, so a reader has nothing to enumerate — listSpaces on their server
// returns only spaces they have written into, which for a reader is none.
//
// So grain records who a gallery was shared with as it shares it. That record
// is a hint and never an authority: every row here is confirmed by actually
// reading the gallery through a credential the authority issues, so a viewer
// since removed from the space sees a 403 and the row is dropped from the
// answer rather than shown.

import { defineQuery, InvalidRequestError } from "$hatk";
import { listSpaceRecords, parseSpaceUri, SpaceError } from "../spaces/client.ts";
import { throwSpaceError } from "../spaces/errors.ts";

interface InviteRow {
  space: string;
  author_did: string;
  created_at: string;
}

export default defineQuery("social.grain.unspecced.listSharedGalleries", async (ctx) => {
  const { ok, db, viewer, params } = ctx;
  if (!viewer) throw new InvalidRequestError("Authentication required");

  const limit = Math.min(Number(params.limit) || 25, 50);

  const rows = (await db.query(
    `SELECT space, author_did, created_at FROM _space_invites
     WHERE member_did = $1
     ORDER BY created_at DESC
     LIMIT $2`,
    [viewer.did, limit],
  )) as InviteRow[];
  if (rows.length === 0) return ok({ galleries: [] });

  let galleries;
  try {
    galleries = await Promise.all(
      rows.map(async (row) => {
        let skey: string;
        let authority: string;
        try {
          ({ skey, authority } = parseSpaceUri(row.space));
        } catch {
          return null;
        }

        try {
          const records = await listSpaceRecords(
            ctx,
            viewer.did,
            row.space,
            authority,
            "social.grain.gallery",
          );
          const record = records.find((r) => r.rkey === skey) ?? records[0];
          const value = record?.value as { title?: string; createdAt?: string } | undefined;

          return {
            space: row.space,
            skey,
            author: row.author_did,
            ...(value?.title ? { title: value.title } : {}),
            ...(value?.createdAt ? { createdAt: value.createdAt } : {}),
          };
        } catch (err) {
          // Removed from the space, or the gallery is gone. Either way it is
          // not the viewer's to see, and a row we cannot read is not one to
          // show. A dead session is not that — it fails every row, and is
          // reported rather than rendered as an empty list.
          if (err instanceof SpaceError && err.status !== 401) return null;
          throw err;
        }
      }),
    );
  } catch (err) {
    return throwSpaceError(err, db, viewer.did);
  }

  const items = galleries.filter((g): g is NonNullable<typeof g> => g !== null);

  // Handles, so a shared gallery says who shared it. A DID that resolves to
  // nothing keeps the row — the gallery is still readable.
  const authors = [...new Set(items.map((g) => g.author))];
  const handleRows =
    authors.length > 0
      ? ((await db.query(
          `SELECT did, handle FROM _repos WHERE did IN (${authors.map((_, i) => `$${i + 1}`).join(",")})`,
          authors,
        )) as { did: string; handle: string }[])
      : [];
  const handles = new Map(handleRows.map((r) => [r.did, r.handle]));

  return ok({
    galleries: items.map((g) => ({
      ...g,
      ...(handles.get(g.author) ? { authorHandle: handles.get(g.author) } : {}),
    })),
  });
});