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

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

serve: A login names its account by handle at once

Merged chadtmiller.com asked to merge into feature/rust from grain.social/grain:serve/login-handle

Only what this pull adds. It is built on grain runs on a Rust server modelled on knotless, without hatk.

crates/grain-serve/src/index/mod.rs+8−3
@@ -445,10 +445,15 @@ impl Index {
self.tracked.lock().unwrap().contains(did)
}
/// Keeps `handle` as the tracked account `did`'s. A handle names one
/// account, so another that had it has none until it is told its own.
pub fn set_handle(&self, did: &str, handle: &str) -> rusqlite::Result<()> {
let db = self.db.lock().unwrap();
db.execute("UPDATE repo SET handle = ?2 WHERE did = ?1", [did, handle])?;
Ok(())
let mut db = self.db.lock().unwrap();
let tx = db.transaction()?;
let sql = "UPDATE repo SET handle = NULL WHERE handle = ?2 AND did <> ?1";
tx.execute(sql, [did, handle])?;
tx.execute("UPDATE repo SET handle = ?2 WHERE did = ?1", [did, handle])?;
tx.commit()
}
/// The tracked account `did`'s handle, and how far backfill has it.
crates/grain-serve/src/login.rs+16−0
@@ -531,9 +531,25 @@ async fn keep(
// Asked again, for an account made or named only now.
anyhow::ensure!(!index(app).taken_down(&session.did)?, "taken down here");
index(app).keep_web_session(&session, keys::now_micros())?;
named(app, &ctx, &session.did, &session.handle).await?;
Ok(session.did)
}
/// Tracks `did`, which has logged in, and keeps `handle`, the one its PDS
/// says it has, as its handle if it resolves to `did`: so its profile is
/// found by handle before backfill reads it. A handle that does not is
/// left for backfill to look up. An identity event or backfill later
/// keeps what it finds over it.
async fn named(app: &App, ctx: &Ctx, did: &str, handle: &str) -> anyhow::Result<()> {
index(app).track(did)?;
let handle = handle.to_lowercase();
let found = identity::resolve_handle(ctx, &handle).await.ok();
if found.as_deref() == Some(did) {
index(app).set_handle(did, &handle)?;
}
Ok(())
}
/// Keeps the session a server hands this client for an account it made,
/// asked for at `url` with `body` and `authorization`
/// ([`oauth::provisioned`]), named by `handle`. Returns the account. Its
crates/grain-serve/tests/login.rs+45−0
@@ -805,3 +805,48 @@ async fn a_login_seeds_a_grain_profile_from_bluesky() {
.await
.unwrap();
}
#[tokio::test]
async fn a_login_names_its_account_by_handle_at_once() {
let site = site().await;
let a = net::account().await;
// An account the index had under a's handle has it no longer once a
// logs in: a handle names one account.
let stale = "did:plc:stalehandleholder";
site.index.track(stale).unwrap();
site.index.set_handle(stale, &a.handle).unwrap();
let mut jar = Jar::default();
site.log_in(&mut jar, &a).await;
// Before backfill reads the account.
assert!(site.index.tracks(&a.did));
let (handle, state) = site.index.repo(&a.did).unwrap().unwrap();
assert_eq!(handle.as_deref(), Some(a.handle.as_str()));
assert_eq!(state, "pending", "backfill has not read it");
assert_eq!(site.index.repo(stale).unwrap().unwrap().0, None);
// Its profile is found by handle once seeded, as by DID.
indexed_or_bare(&site, &a.did).await;
let by_handle = format!("/xrpc/social.grain.actor.getProfile?actor={}", a.handle);
let res = site.get(&mut Jar::default(), &by_handle).await;
assert_eq!(res.status().as_u16(), 200);
let body: Value = res.json().await.unwrap();
assert_eq!(body["did"], a.did.as_str());
assert_eq!(body["handle"], a.handle.as_str());
}
/// Waits for `did`'s grain profile, named or bare, to be in the index.
async fn indexed_or_bare(site: &web::Site, did: &str) {
let uri = format!("at://{did}/social.grain.actor.profile/self");
for _ in 0..200 {
let held = site
.index
.get::<grain_serve::index::records::Profile>(&uri)
.unwrap();
if held.is_some() {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
panic!("no profile was seeded for {did}");
}