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:
@@ -32,6 +32,11 @@ use std::time::Duration;
|
||||
#[derive(Debug, Serialize)]
|
||||
struct IngestCommitBody<'a> {
|
||||
did: &'a str,
|
||||
/// The poster's current handle. Optional in the wire payload —
|
||||
/// the AppView's indexer treats an empty/missing handle as the
|
||||
/// existing empty-string placeholder, which the Jetstream
|
||||
/// `identity` event path will eventually backfill.
|
||||
handle: Option<&'a str>,
|
||||
collection: &'a str,
|
||||
action: &'a str,
|
||||
rkey: &'a str,
|
||||
@@ -63,6 +68,14 @@ impl AppViewPushClient {
|
||||
/// AT-Protocol record value as JSON — the AppView's indexer reads
|
||||
/// `embed` / `reply` off it, which is why we can't just send the CID.
|
||||
///
|
||||
/// `handle` is the poster's current handle. Pass `Some(handle)` for
|
||||
/// local-PDS users so the AppView's `posts.handle` column is
|
||||
/// populated immediately (otherwise the timeline renders handles as
|
||||
/// `@did:plc:…` snippets and the profile endpoint can't resolve
|
||||
/// `handle → did`). For the `app.bsky.feed.like` / `app.bsky.feed.repost`
|
||||
/// collections the AppView also needs it so the liker's handle
|
||||
/// lands on the `likes.liker_handle` column.
|
||||
///
|
||||
/// Returns `Ok(true)` if the AppView applied the commit, `Ok(false)`
|
||||
/// if it returned a non-2xx status (logged as warn), and `Err(_)` if
|
||||
/// the request itself failed. The caller should treat any non-Ok as
|
||||
@@ -70,6 +83,7 @@ impl AppViewPushClient {
|
||||
pub async fn push_create(
|
||||
&self,
|
||||
did: &str,
|
||||
handle: Option<&str>,
|
||||
collection: &str,
|
||||
rkey: &str,
|
||||
cid: &str,
|
||||
@@ -77,6 +91,7 @@ impl AppViewPushClient {
|
||||
) -> Result<bool> {
|
||||
self.push(
|
||||
did,
|
||||
handle,
|
||||
collection,
|
||||
"create",
|
||||
rkey,
|
||||
@@ -93,19 +108,21 @@ impl AppViewPushClient {
|
||||
collection: &str,
|
||||
rkey: &str,
|
||||
) -> Result<bool> {
|
||||
self.push(did, collection, "delete", rkey, None, None, None)
|
||||
self.push(did, None, collection, "delete", rkey, None, None, None)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn push_follow_create(
|
||||
&self,
|
||||
did: &str,
|
||||
handle: Option<&str>,
|
||||
rkey: &str,
|
||||
subject_did: &str,
|
||||
record: &Value,
|
||||
) -> Result<bool> {
|
||||
self.push(
|
||||
did,
|
||||
handle,
|
||||
"app.bsky.graph.follow",
|
||||
"create",
|
||||
rkey,
|
||||
@@ -124,6 +141,7 @@ impl AppViewPushClient {
|
||||
) -> Result<bool> {
|
||||
self.push(
|
||||
did,
|
||||
None,
|
||||
"app.bsky.graph.follow",
|
||||
"delete",
|
||||
rkey,
|
||||
@@ -137,6 +155,7 @@ impl AppViewPushClient {
|
||||
async fn push(
|
||||
&self,
|
||||
did: &str,
|
||||
handle: Option<&str>,
|
||||
collection: &str,
|
||||
action: &str,
|
||||
rkey: &str,
|
||||
@@ -147,6 +166,7 @@ impl AppViewPushClient {
|
||||
let url = format!("{}/internal/ingest-commit", self.base_url);
|
||||
let body = IngestCommitBody {
|
||||
did,
|
||||
handle,
|
||||
collection,
|
||||
action,
|
||||
rkey,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user