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

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

A web login seeds a grain profile from the account's Bluesky profile

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

What changes

A web login seeds a grain profile. Once the callback has kept the session, login::profile::seed runs in the background (tokio::spawn), so the redirect never waits on it and a failure is only logged (seeding a profile from Bluesky). If the account's social.grain.actor.profile/self is missing or has no displayName, it reads app.bsky.actor.profile/self and writes a grain profile with its displayName, description and avatar (the blob ref as is), and a createdAt kept from the grain profile there was, or now. An existing one is put over (putRecord, swapped on the CID read); otherwise one is created at rkey self.

The write goes through the web session with write::put and a new write::create, factored out of createRecord as put already was of putRecord. So it is checked against the lexicon, refreshed and retried as other web writes are, and indexed before it returns. sync::get_record is shared for the two reads.

Why

The old server's on-login hook did this, so a new grain account shows the name and picture it has on Bluesky. Ported as it was, with two differences:

  • Both profiles are read from the account's PDS, not the grain one from the index. The index may not have the account yet at its first login, and reading it there would create over a profile that exists (refused) or miss one to keep createdAt from.
  • The put swaps on the CID it read, so an edit made meanwhile is not overwritten.

Mirrored from knotless: knotless-serve's login callback sets off spaces::discover(&app, &did) once the session is kept, a tokio::spawn that never holds up the redirect and whose failure is only logged. profile::seed has the same shape and sits at the same point in callback. knotless itself makes no profile at login (its pages fall back on the Bluesky profile when reading), so the seeding is ported from the old server.

The apps' sign-in (server: The apps sign in at their own PDS, as grain-serve's public client) has no hook for this. The apps sign in at their own PDS and call grain-serve with service tokens through atproto-proxy. grain-serve never sees the login, and it holds no session to write to their repo with: viewer::viewer only verifies a token per call. So no first call could do the write. The apps do need the same, since they used to sign in through the old server and got its hook. It has to be done in each app after sign-in: read both profiles from its PDS, write the grain profile there, then call social.grain.sync.indexRecords. Not built here.

How it was checked

check, harness, rust, test and lexicons pass at fc53a26. Added:

  • a_login_seeds_a_grain_profile_from_bluesky (tests/login.rs, reference PDS). An account with a Bluesky profile and avatar and no grain profile gets one, in the index and at its PDS. One with an unnamed grain profile the index never saw is put over and keeps its createdAt. One with a named grain profile is left alone. One with no Bluesky profile gets nothing. Every login still answers 303.
  • An account with no web session: seeded fails with AuthenticationRequired, and the background task ends quietly.
  • a_bluesky_profile_gives_what_it_has (unit): empty strings and banner/pronouns are left out, and createdAt is kept.

Shortcomings

  • An account with an unnamed grain profile and a Bluesky profile with no name is put over on every login, as the old server did.
  • A Bluesky avatar the grain lexicon would not take, such as a webp, fails the grain write's check. That is logged and nothing is written.
  • Group accounts made through login::provisioned are not seeded, as they have no Bluesky profile.

Review

Start at crates/grain-serve/src/login/profile.rs, then the one line in login::callback.

🤖 Generated with Claude Code

Approved an earlier version

This pull seeds a grain profile from the account's Bluesky profile in a background task after a web login. It does the write through the web session with a new write::create, factored out of createRecord the way put was out of putRecord.

I found nothing that must change.

  • Refactor: create_record now delegates to create with the same body, so the XRPC behaviour is unchanged.
  • Seeding logic: it reads both profiles from the PDS, skips an account whose grain profile already has a name, and keeps createdAt when there was a profile. It puts over an existing profile with swapRecord set to the CID it read and creates at self otherwise.
  • Failure handling: failures are only logged, so the redirect never waits on them.
  • Tests: the integration test covers the made, put-over-unnamed, named and no-Bluesky cases. The unit test covers field filtering and createdAt.

I couldn't confirm the tests pass: the rust, harness, test and check runs were still queued when I looked. Please make sure they come back green before it merges, especially the coverage gate on the harness.

If an account logs in twice at nearly the same moment, both background tasks could read "no grain profile" and race to create it. The loser's create fails and is only logged, so this is harmless.

Approved

Since my earlier approval, the pull has one new commit. seed now returns its JoinHandle, and callback drops it, so the redirect still doesn't wait. The test now also covers a DID with no web session. seeded returns an AuthenticationRequired error for it, and the awaited seed handle finishes cleanly because the failure is only logged. I have nothing to add.

The rust, harness, test and check runs were still queued when I looked. Make sure they pass before this merges, especially the coverage gate on the harness.

Landed on feature/rust in batch/four (…/3mzbjho3qgd2b, 785dbe4), with its head fc53a26 unchanged.