0
2
// Add a gallery to a group's pool.
// POST /xrpc/social.grain.unspecced.createPoolGallery
//
// The pool is one space, anchored on the group, and the group made it.
// Nothing is created here and nothing is accepted: the member writes their own
// gallery into it, in their own repo, and being a member is the whole
// permission — their PDS asks the group's host whether they may write, and
// the host answers from the roster.
//
// That is also what makes leaving clean. The records are the author's, so a
// gallery goes with them; the pool is the space, so losing membership loses the
// read. Neither needs anything deleted.
//
// Blobs are uploaded beforehand through the ordinary blob endpoint: a space
// record's blobs are ordinary account blobs, and only reading them back goes
// through the space.
import { defineProcedure, InvalidRequestError } from "$hatk";
import { poolUri } from "../spaces/client.ts";
import { throwSpaceError } from "../spaces/errors.ts";
interface PhotoInput {
photo: unknown;
alt?: string;
aspectRatio: { width: number; height: number };
}
export default defineProcedure("social.grain.unspecced.createPoolGallery", async (ctx) => {
const { ok, db, viewer, pds, input } = ctx;
if (!viewer) throw new InvalidRequestError("Authentication required");
const { group, rkey, title, description } = input;
const photos = (input.photos ?? []) as PhotoInput[];
// No support probe first, unlike a private gallery. There the author's PDS
// has to create a space, and asking beforehand is the difference between a
// clear message and an upload wasted; here the space already exists, the one
// call is a write into it, and its failure says what went wrong. A PDS that
// serves describe only to an authenticated caller — the opensocial dev
// network's does — would otherwise be read as having no spaces at all.
const space = poolUri(group);
const createdAt = new Date().toISOString();
// The gallery, its photos and the items joining them, in one commit. A
// half-written gallery is worse here than in the public repo: there is no
// indexer to reconcile it against later.
//
// Records inside a space have no uri of their own — the address is the space,
// the repo, the collection and the rkey — so an item names its gallery and
// its photo by the uri each would have in the author's public repo. Every
// reader assembles them the same way.
const self = (collection: string, key: string) => `at://${viewer.did}/${collection}/${key}`;
const writes: Record<string, unknown>[] = [
{
$type: "com.atproto.space.applyWrites#create",
collection: "social.grain.gallery",
rkey,
value: {
$type: "social.grain.gallery",
title,
...(description ? { description } : {}),
createdAt,
},
},
];
photos.forEach((photo, index) => {
const photoRkey = `${rkey}-${index}`;
writes.push({
$type: "com.atproto.space.applyWrites#create",
collection: "social.grain.photo",
rkey: photoRkey,
value: {
$type: "social.grain.photo",
photo: photo.photo,
alt: photo.alt || "",
aspectRatio: photo.aspectRatio,
createdAt,
},
});
writes.push({
$type: "com.atproto.space.applyWrites#create",
collection: "social.grain.gallery.item",
rkey: photoRkey,
value: {
$type: "social.grain.gallery.item",
gallery: self("social.grain.gallery", rkey),
item: self("social.grain.photo", photoRkey),
position: index,
createdAt,
},
});
});
try {
await pds("com.atproto.space.applyWrites", {
method: "POST",
body: { space, repo: viewer.did, writes },
});
} catch (err) {
return throwSpaceError(err, db, viewer.did);
}
return ok({ space, uri: self("social.grain.gallery", rkey) });
});