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

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

Pools are read and written through grain-serve

Merged
Merged chadtmiller.com asked to merge into feature/rust from grain.social/grain:spaces/pools

What changes

grain-serve now serves groups' pools (crates/grain-serve/src/spaces/pool.rs), the social.grain.group.pool.* methods as their lexicons say: createGallery, deleteGallery, getGallery, listGalleries, listFeed, createComment, deleteComment and putFavorite.

A pool is the group's social.grain.group space, keyed self. Its members write their own records into their own repos there: galleries, photos, items, favorites and comments. None of it reaches a firehose. A read lists the space's repos at its authority with the viewer's credential, then reads the five collections from each repo where it is held. Favorites and comments are gathered across all the repos and attached to the gallery each one names. Galleries come back newest first, comments oldest first.

Who reads what. A pool's galleries, photos, favorites, comments and pictures (through /space-blob) are read only by members the group's host admits to the pool. Anyone else gets 400 NotAuthorized. That includes an account on the group's roster whose pool the host has not opened to them; tests cover both cases.

listFeed is every pool of the groups the viewer is in, found as getGroup finds them. A pool that refuses is skipped; if every pool refuses, the feed returns that refusal. An account in no groups gets an empty feed. A group signed in as itself reads its own pool.

Writes are the caller's own. Each write names the caller's repo, so no one removes anyone else's gallery, favorite or comment.

  • createGallery, createComment and putFavorite with on: true are refused with NotAuthorized unless the host admits the caller to the pool. The caller's PDS would accept the write into their own repo anyway, but the authority lists only the repos of members it lets write, so nobody would ever read it.
  • Records are checked against their lexicon before writing (InvalidRecord).
  • putFavorite finds the caller's existing favorite by reading their own favorites, so calling it twice still leaves one favorite.
  • deleteGallery reads the gallery's items to find its photos instead of guessing their keys. It returns the number of records removed.
  • Taking your own records back is never refused, not even after leaving the group: deleteGallery, deleteComment, and putFavorite with on: false. The test covers this.

Shared with private galleries. gallery.rs now exposes gallery_writes (the gallery, photos and items, used by both kinds of gallery) and people (a person as grain shows one in a space). spaces::refused drops a credential that a host refuses.

From knotless

  • Reading a pool: the read is knotless's space::Remote's, as read_with / repos_with in pull 1 copy it. It lists the space's writers at the authority with the viewer's credential (listRepos) and reads each writer's repo where it is held.
  • Caching and membership: every answer is unshared and membership is re-proven every minute (pull 3).
  • Divergence: as in pull 3, a pool is read live and not synced into an index the way knotless syncs a private repository's space (spaces.rs, space_sync.rs). That costs five requests per repo on every read, listed under Shortcomings, and is the case where a sync would pay off most.

Why

This is the fifth pull of the spaces stack and covers the last of the space methods the web calls.

How it was checked

crates/grain-serve/tests/spaces.rs runs on the alpha against the stand-in group host from pull 4. Two members join and one outsider does not.

Refusals tested:

  • createGallery with a bad group, a bad key, 101 photos, no title, or an over-long title;
  • an outsider writing a gallery, writing a comment, favoriting, listing the pool, reading a gallery, or fetching a picture; their feed comes back empty;
  • a request with no caller gets 401;
  • an account on the roster but not in the pool gets NotAuthorized on its feed.

Member flow tested:

  • the second member lists the gallery with its cover and the author's profile;
  • favoriting twice leaves one favorite;
  • empty comments and bad subjects are refused;
  • comments, including a reply;
  • getGallery read by the author and by the fan (favorite counts, viewerFav, comments with their authors, photos in order), and for a gallery that isn't there;
  • the picture through /space-blob;
  • the fan's feed, with the group's view;
  • deleting a comment, unfavoriting twice, and withdrawing the gallery (5 records), after which the pool is empty;
  • two galleries of 60 and 50 photos, so the photo collection is read past its first page;
  • after the fan leaves the group, favoriting is refused, but unfavoriting and deleting a comment succeed;
  • a bad group refused on every write.

The group, acting by service token, gets 401 on its feed. fmt and clippy ran here. The coverage gate is left to the runner.

Differences from the old server

  • createGallery, createComment and putFavorite (giving a favorite) now return NotAuthorized up front for a non-member. The old server wrote the record and returned success, and the write was then invisible to everyone.
  • createGallery, like the other writes here, checks the caller's PDS serves spaces (SpacesUnsupported), because it gets the caller's pool credential first. The old server made no such check.
  • createComment and putFavorite check records against their lexicons. The old server wrote them with validate: false and no check.
  • Host failures are 502 UpstreamFailure, as in pull 3.

Shortcomings

  • A group signed in by service token, as when acting from its host, cannot read its own feed: a credential needs a web session, so it gets 401.
  • One member repo failing to answer fails the whole pool read, as it did on the old server.
  • A pool read makes five requests per repo, on every request, with no caching. Each collection is read to at most 2,000 records.
  • The simplespace path (pds.js, zds) is not run here.
  • createGroup and getGroupHost are still not ported (see pull 4).

Review

Start with read_pool and pooling in spaces/pool.rs. The handlers are short.

🤖 Generated with Claude Code

Asked for changes

This adds the social.grain.group.pool.* methods to grain-serve. A pool is read by listing the space's repos and reading five collections from each, and writes go into the caller's own repo. The read paths and deleteGallery look sound. I found one behaviour that contradicts what the pull says it does.

The body says "Deleting your own records is never refused". putFavorite with on: false is a delete, but it calls pooling first. A member who has been removed from the pool, or whose write access has been revoked, therefore gets NotAuthorized when they try to take back their favorite. Their favorite stays in their repo, and the authority no longer lists that repo for them. deleteComment and deleteGallery have no such check. Only call pooling when on is true.

This path isn't tested, as far as the description says. Please add a test for unfavoriting after the caller has lost pool access, or drop the claim.

crates/grain-serve/src/spaces/pool.rsline 592Outdated
"createdAt": grain::keys::now_iso(),
});
let record = &checked(app, FAVORITES, record)?;
pooling(app, &did, FAVORITE, &input.group).await?;
review.knotless.dev

This runs for on: false too, so a member who has lost pool access can't withdraw a favorite. That contradicts "deleting your own records is never refused", and deleteComment and deleteGallery don't have the check. Only call pooling when input.on is true.

Asked for changes

My earlier review asked for putFavorite to skip pooling when on is false, so a member who has lost pool access can still take back their favorite. That wasn't done. At head 59c2388, putFavorite still calls pooling(app, &did, FAVORITE, &input.group).await? for every request. A removed member who sends on: false therefore gets NotAuthorized. The description still says "Deleting your own records is never refused". Please call pooling only when on is true, and add a test for unfavoriting after access is lost. If you'd rather keep the check, drop that claim from the description instead.

crates/grain-serve/src/spaces/pool.rsline 592Outdated
"createdAt": grain::keys::now_iso(),
});
let record = &checked(app, FAVORITES, record)?;
pooling(app, &did, FAVORITE, &input.group).await?;
review.knotless.dev

Call pooling only when input.on is true. As written, this refuses a member's attempt to remove their own favorite once they've lost pool access.

Asked for changes

This is the third round, and the issue from my two earlier reviews is still there. At head b3a8785, put_favorite still calls pooling(...) for every request, including on: false. A member who has lost pool access therefore can't take back their favorite and gets NotAuthorized. The description still says "Deleting your own records is never refused". I also found no test for unfavoriting after access is lost.

Please call pooling only when input.on is true, and add a test for unfavoriting after access is lost. If you'd rather keep the check, remove that claim from the description instead.

crates/grain-serve/src/spaces/pool.rsline 591Outdated
"createdAt": grain::keys::now_iso(),
});
let record = &checked(app, FAVORITES, record)?;
pooling(app, &did, FAVORITE, &input.group).await?;
review.knotless.dev

Make this if input.on { pooling(...).await?; } so a removed member can still delete their own favorite.

chadtmiller.com

Done at f63abbe (dba7c9c): pooling runs only when input.on is true. The pool test now has the fan favorite and comment, then leave the group. After that, favoriting is NotAuthorized, while unfavoriting (200, on: false) and deleting the comment both succeed.

chadtmiller.com added 15 commits
Approved

The issue from my earlier rounds is fixed. At head f63abbe, put_favorite calls pooling only when input.on is true, with a comment saying why. A member who has lost pool access can now take back their favorite, which matches the description. The description also says a test covers unfavoriting after the caller leaves the group. I did not open the test file to confirm that. I have no other findings.

Approved

The head moved from f63abbe, which I approved, to 1caa934. The only change is a merge of spaces/groups, which brings a commit that names a group's rule by its at-uri. put_favorite in spaces/pool.rs still calls pooling only when input.on is true, so a member who has lost pool access can still take back a favorite. I only read pool.rs at this head and did not open the merged groups change or the test file. I have no new findings.

Merged as 92c202b
Part of a stack of 2
  1. Pools are read and written through grain-serve Landed as 92c202b
  2. main