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

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

Accounts mute and unmute, and list whom they block and mute

Closed
Closed chadtmiller.com asked to merge into feature/rust from grain.social/grain:gaps/graph

What changes

Muting. social.grain.graph.muteActor and unmuteActor keep a mute in the index's mute table (crates/grain-serve/src/graph.rs, index/graph.rs). The apps call them with a service token, the web with its cookie from a page of the site, as every procedure is held (viewer::writer). Muting one already muted, or unmuting one not muted, changes nothing; an account cannot mute itself, and actor is a DID, as the lexicon says. What a muted account says the reads already leave out (read::moderation::unmuted).

Lists. social.grain.graph.getMutes and getBlocks list the viewer's own, the latest first, paged by the cursor every list here takes (<createdAt>::<uri or did>), limit 1 to 100. Each item is who the account is (did, handle, displayName, avatar), and a block's its blockUri. Every one is listed, taken down or not, so that each can be taken back.

This is the first of the pulls that close what the clients call and grain-serve did not serve. Each is its own, on feature/rust, to land alone: gaps/graph (this), gaps/stories (Stories are archived, and what each account watched follows it), gaps/sitemap (The sitemap names the profiles and galleries a search engine may show), gaps/deletes (Galleries and accounts are deleted, and a deleted account only once its host agrees), gaps/cross-post (The web's Bluesky cross-post goes through grain-serve, and a test says so), gaps/web (grain-serve hands the web's pages to its app, at --web).

What the clients call, and what grain-serve serves

W is the web as web/reads and web/writes-client leave it; I and A are the iOS and Android apps, which still call the old names (mapped through lexicons/README.md). "Served" is feature/rust before these pulls.

Method (old name the apps call)CallersServedPull
social.grain.actor.getProfile (unspecced.getActorProfile)W I Ayes
social.grain.actor.getPreferences (dev.hatk.getPreferences)W I Ayes
social.grain.actor.putPreferences (dev.hatk.putPreference)W I Ayes
social.grain.actor.getFavorites (getActorFavorites)W I Ayes
social.grain.actor.getSuggestedFollowsW Iyes
social.grain.actor.searchActors (searchProfiles)W I Ayes
social.grain.actor.searchActorsTypeaheadW Ayes
social.grain.feed.describeFeedsWyes
social.grain.feed.getFeed (dev.hatk.getFeed)W I Ayes
social.grain.gallery.getGallery, getFavorites, getCameras, getLocations, searchGalleriesW I Ayes
social.grain.comment.getThread (getCommentThread)W I Ayes
social.grain.graph.getFollowers, getFollowing, getKnownFollowersW I Ayes
social.grain.graph.getBlocksW I Anothis one, gaps/graph
social.grain.graph.getMutesW I Anothis one, gaps/graph
social.grain.graph.muteActor, unmuteActorW I Anothis one, gaps/graph
social.grain.notification.listNotifications, getUnreadCount (getNotifications)W I Ayes
social.grain.notification.updateSeen (putPreference of lastSeenNotifications)W I Ayes
social.grain.notification.registerPush, unregisterPush (dev.hatk.push.*)I Ayes
social.grain.story.getAuthors, getStories, getStoryW I Ayes
social.grain.story.getArchive (getStoryArchive)W I Anogaps/stories
social.grain.story.markViewed (markStoriesViewed)W I Anogaps/stories
social.grain.server.getSitemapW (server side)nogaps/sitemap
social.grain.gallery.deleteGalleryW I Anogaps/deletes
social.grain.server.deleteAccountW I Anogaps/deletes
social.grain.repo.createRecord, putRecord, deleteRecord, applyWrites, uploadBlobWyes
the same, of app.bsky.feed.post (the Bluesky cross-post)W, I (by dev.hatk.createRecord/putRecord)yes: said to be refused, but it is takengaps/cross-post, a test only
dev.hatk.createRecord, putRecord, deleteRecord, applyWrites, uploadBlobI Ano, by designthe apps write com.atproto.repo.* to their own PDS, then social.grain.sync.indexRecords (served)
dev.hatk.getRecordI Ano, by designcom.atproto.repo.getRecord at the account's PDS
com.atproto.moderation.createReport (dev.hatk.createReport)W I Ayes
dev.hatk.describeLabelsI Ain partcom.atproto.label.queryLabels is served; the label definitions are only social.grain.admin.listLabelDefinitions, admins only. Open: see below
com.atproto.repo.getRecord, com.atproto.sync.getBlobWnot grain-serve'sthe account's PDS
com.atproto.identity.resolveHandleW Inot grain-serve'spublic.api.bsky.app
private galleries (privateGallery.*, getSpaceSupport, /space-blob)Wnothe spaces port (spaces/galleries)
groups and pools (group.*, group.pool.*)Wnothe spaces port (spaces/groups, spaces/pools)
/oauth/session, /oauth/login, /oauth/switch, /oauth/logout, /oauth-client-metadata.jsonWyes
/og/profile/...Wyes
every other path: the web's pagesbrowsersno: Caddy sends them to the appgaps/web (GRAIN_WEB)
/oauth/par, /oauth/authorize, /oauth/token (the old server as the apps' authorization server)I Anoopen: see below

Open, not built here:

  • The apps' logins. iOS and Android log in to grain.social as their OAuth authorization server. grain-serve is no authorization server; lexicons/README.md's plan is for the apps to log in at their own PDS and call grain-serve with service tokens. That is the apps' move, and nothing in grain-serve stands in for it.
  • Label definitions for the apps. dev.hatk.describeLabels gave anyone grain's label definitions. Bluesky's way is the labeler's app.bsky.labeler.service record, read by app.bsky.labeler.getServices; grain publishes none yet. The web ships them in $lib/labels, and the apps can too until grain does.

Why

The web (web/writes-client) and both apps call these, and grain-serve answered 501.

knotless has no mutes. Its dev.knotless.listBlocks (knotless-serve/src/web/account_settings.rs) reads blocks from the account's own repo at its PDS, so a block lists as soon as it is made; grain's are in the index already, which a web write reaches before it answers and an app's reaches through indexRecords, so getBlocks reads the index and keeps grain's lexicon, whose shape the web was moved onto.

Differences from the old server

  • A cursor names the last row's time and URI (or DID), not an offset, so a page does not skip or repeat as mutes and blocks are made between pages.
  • An account with no handle the index knows is handle.invalid, as every view here names one, where the old server gave its DID as its handle.
  • A mute's actor that is not a DID is refused (400), and muting oneself is InvalidRequest, where the old server threw a 500.
  • limit out of 1 to 100 is refused, not clamped.

How it was checked

crates/grain-serve/tests/graph.rs, against the reference PDS for the service tokens: muting three accounts and one again, the list paged and its items, unmuting twice, a self-mute, a handle and no actor refused, no token and the wrong method's token refused; blocks listed newest first with their records, another's block left out, one taken down kept, paged, and bad limits and cursors refused. index/graph.rs has a unit test of the table, and the plan check (the_reads_read_by_index) covers both lists' queries. The rest is the runner's.

Shortcomings

  • The web's muting goes through viewer::writer, so it needs a page of the site, as the other procedures do; not tried in a browser.
  • Mutes stay in the index when an account is deleted until gaps/deletes lands, whose purge takes them.

🤖 Generated with Claude Code

Approved an earlier version

This adds muteActor and unmuteActor, which write the existing mute table through viewer::writer, and getMutes and getBlocks, which are keyset-paged reads of the viewer's own rows. I found nothing that must change.

  • Schema: the mute table exists with primary key (did, subject). INSERT OR IGNORE and DELETE therefore give the idempotent behaviour the body describes.
  • Cursors: the cursor and limit handling reuses Params::after, Params::limit and page. The row-value comparisons match the ORDER BY, so pages should neither skip nor repeat rows.
  • Validation: the actor is checked as a DID, self-mute is refused, and the routes follow the pattern of the other procedures.
  • Tests: they cover paging, taken-down accounts staying listed, bad input and auth failures.

I did not run the checks or read their results. Please confirm the harness and rust checks are green before merging, since the coverage gate and the answer-schema check are strict.

Approved

Carried forward: the change this pull makes at 3c2008a56e19d9c70dfb0dd50c39d5a591de49ad is the one reviewed at af38c5ea523d0898e6875eaa3ab57104d5ca3cf4, so that review stands. What is new comes from its base.

Landed on feature/rust at fc5f686 through the batch pull Seven reviewed pulls land on feature/rust together, checked once, checked there as a whole. This pull's head is in feature/rust's history.