fix(pds-server): forward poster handle to AppView on ingest push

The PDS's best-effort push to /internal/ingest-commit didn't
include the poster's handle. The AppView's indexer then stored
'\'' (empty) and the timeline UI fell back to '@did:plc:<snip>…'
synthetic identifiers — which is fine for Bluesky (PLC directory
resolves the rest), but local-PDS users have 'did🔑' DIDs that
no resolver can look up, so the synthetic handle stuck forever
and the profile endpoint could never resolve 'handle → did'.

Plumb the handle through:
- appview_push.rs: IngestCommitBody gains an optional 'handle'
  field; push_create / push_follow_create take Option<&str>
- routes/helpers.rs: new 'lookup_handle(state, did)' helper that
  hits the 'users' table (in practice always finds the row for an
  authenticated route; logs a warning otherwise)
- routes/repo.rs (createRecord) and routes/feed.rs (feed.like.create):
  resolve 'did → handle' from the users table before the spawned
  ingest push, pass it through

The AppView-side companion commit stores the handle on the new
row and adds a Jetstream identity-event backfill, so by the
time this PR is merged timelines render real '@handle' again.
This commit is contained in:
tomdebone
2026-07-07 21:50:41 +02:00
parent abea4d2a8a
commit 184e0dfe03
4 changed files with 65 additions and 5 deletions
+3 -1
View File
@@ -17,7 +17,7 @@
//! removed from the MST, a new commit is signed, the AppView is
//! told to drop the row, and we return the new commit CID + rev.
use crate::routes::helpers::{apply_repo_write, err, to_sqlx_error, RepoWriteOutcome};
use crate::routes::helpers::{apply_repo_write, err, lookup_handle, to_sqlx_error, RepoWriteOutcome};
use at_repo::blockstore::Blockstore;
use crate::routes::types::ErrorBody;
use crate::state::AppState;
@@ -273,6 +273,7 @@ pub async fn create_like(
let value_cid_str = value_cid.to_string();
let push_record = record.clone();
let push_rkey = rkey.clone();
let push_handle_str: Option<String> = lookup_handle(&state, &did).await;
let commit = apply_and_commit(&state, &did, move |repo| {
let value_cid = value_cid;
@@ -317,6 +318,7 @@ pub async fn create_like(
if let Err(e) = push_handle
.push_create(
&push_did,
push_handle_str.as_deref(),
LIKE_COLLECTION,
&push_rkey_owned,
&push_cid_owned,
+22 -1
View File
@@ -453,4 +453,25 @@ async fn persist_user_blocks_in_tx(
})?;
}
Ok(())
}
}
/// Look up the current handle for `did` from the `users` table.
///
/// Returned as `Option<String>` (rather than an empty default) so the
/// caller can decide what to do when the row hasn't been found yet —
/// in practice the row is always present for an authenticated route,
/// but we'd rather log a warning than silently emit a bogus empty
/// handle into the AppView's `posts.handle` column.
///
/// Called once per `createRecord` / `feed.like.create` write, just
/// before the AppView push, so the liker's/poster's display handle
/// lands on the AppView row at write time — otherwise the AppView has
/// no source for `did:key:` handles and the timeline renders them as
/// `@did:key:z16D…` snippets.
pub async fn lookup_handle(state: &AppState, did: &str) -> Option<String> {
sqlx::query_scalar::<_, String>("SELECT handle FROM users WHERE did = $1")
.bind(did)
.fetch_optional(&state.db)
.await
.ok()
.flatten()
}
+19 -2
View File
@@ -1,5 +1,5 @@
use crate::routes::helpers::{
apply_repo_write, err, to_sqlx_error, RepoWriteOutcome,
apply_repo_write, err, lookup_handle, to_sqlx_error, RepoWriteOutcome,
};
use crate::routes::types::{CreateRecordReq, CreateRecordResp};
use crate::state::AppState;
@@ -92,6 +92,16 @@ pub async fn create_record(
let push_rkey = rkey.clone();
let push_cid = value_cid.to_string();
let push_record = req.record.clone();
// Resolve the poster's current handle from the local users
// table *before* the spawn — the closure can't easily borrow
// `&state` after we hand ownership to the spawned task.
let push_handle_str: Option<String> = match lookup_handle(&state, &did).await {
h @ Some(_) => h,
None => {
tracing::warn!(did = %did, "appview push: no handle in users table; timeline will show @did-prefix");
None
}
};
let collection = req.collection.clone();
let outcome = apply_repo_write(&state, &did, move |repo| {
@@ -136,7 +146,14 @@ pub async fn create_record(
// the AppView.
tokio::spawn(async move {
if let Err(e) = push_handle
.push_create(&push_did, &push_coll, &push_rkey, &push_cid, &push_record)
.push_create(
&push_did,
push_handle_str.as_deref(),
&push_coll,
&push_rkey,
&push_cid,
&push_record,
)
.await
{
tracing::warn!(error = %e, did = %push_did, "appview push_create failed; jetstream will replay");