From e83b08fcc8e8b501acf2ca0e297b9ccf934b7748 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 15 May 2026 01:04:42 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20remote=20actor=20display=20names=20in=20?= =?UTF-8?q?thought=20cards=20=E2=80=94=20use=20last=20URL=20segment=20as?= =?UTF-8?q?=20username,=20resolve=20display=5Fname=20after=20intern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/adapters/postgres/src/activitypub.rs | 38 +++++++++++++++---- .../src/services/federation_event.rs | 16 ++++++++ crates/domain/src/ports.rs | 8 ++++ crates/domain/src/testing.rs | 8 ++++ 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/crates/adapters/postgres/src/activitypub.rs b/crates/adapters/postgres/src/activitypub.rs index 664d034..85fc66e 100644 --- a/crates/adapters/postgres/src/activitypub.rs +++ b/crates/adapters/postgres/src/activitypub.rs @@ -154,13 +154,18 @@ impl ActivityPubRepository for PgActivityPubRepository { return Ok(id); } let new_id = uuid::Uuid::new_v4(); - let raw = actor_ap_url - .path() - .trim_start_matches('/') - .replace('/', "_"); - // username column is VARCHAR(32); truncate long paths (e.g. UUID-based actor URLs) - let handle = if raw.len() <= 32 { - raw + // Use the last path segment as username (e.g. /users/alice → "alice"). + // Falls back to a random short id for long segments (e.g. UUID-based actor URLs). + // username column is VARCHAR(32). + let last_seg = actor_ap_url + .path_segments() + .and_then(|mut s| s.next_back()) + .unwrap_or("") + .to_string(); + let handle = if last_seg.is_empty() { + format!("remote_{}", &new_id.to_string()[..13]) + } else if last_seg.len() <= 32 { + last_seg } else { format!("remote_{}", &new_id.to_string()[..13]) }; @@ -185,6 +190,25 @@ impl ActivityPubRepository for PgActivityPubRepository { }) } + async fn update_remote_actor_display( + &self, + user_id: &UserId, + display_name: Option<&str>, + avatar_url: Option<&str>, + ) -> Result<(), DomainError> { + sqlx::query( + "UPDATE users SET display_name=$1, avatar_url=$2, updated_at=NOW() + WHERE id=$3 AND local=false", + ) + .bind(display_name) + .bind(avatar_url) + .bind(user_id.as_uuid()) + .execute(&self.pool) + .await + .map_err(|e| DomainError::Internal(e.to_string())) + .map(|_| ()) + } + async fn accept_note( &self, ap_id: &Url, diff --git a/crates/application/src/services/federation_event.rs b/crates/application/src/services/federation_event.rs index 784716a..bfc6725 100644 --- a/crates/application/src/services/federation_event.rs +++ b/crates/application/src/services/federation_event.rs @@ -136,6 +136,22 @@ impl FederationEventService { let author_id = self.ap_repo.intern_remote_actor(&actor_url).await?; + // Resolve and cache display info so thought cards show proper names. + let profiles = self + .federation_action + .resolve_actor_profiles(vec![actor_ap_url.clone()]) + .await; + if let Some(profile) = profiles.into_iter().next() { + let _ = self + .ap_repo + .update_remote_actor_display( + &author_id, + profile.display_name.as_deref(), + profile.avatar_url.as_deref(), + ) + .await; + } + for note in notes { let ap_id = match url::Url::parse(¬e.ap_id) { Ok(u) => u, diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index 8c4e11d..ef67625 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -342,6 +342,14 @@ pub trait ActivityPubRepository: Send + Sync { /// Idempotent — safe to call multiple times with the same URL. async fn intern_remote_actor(&self, actor_ap_url: &url::Url) -> Result; + /// Update display_name and avatar_url for an already-interned remote actor. + async fn update_remote_actor_display( + &self, + user_id: &UserId, + display_name: Option<&str>, + avatar_url: Option<&str>, + ) -> Result<(), DomainError>; + // ── Inbox processing (remote → local) ─────────────────────────── /// Persist an incoming remote Note. Idempotent on ap_id. diff --git a/crates/domain/src/testing.rs b/crates/domain/src/testing.rs index 8184c8c..3947546 100644 --- a/crates/domain/src/testing.rs +++ b/crates/domain/src/testing.rs @@ -779,6 +779,14 @@ impl ActivityPubRepository for TestStore { self.users.lock().unwrap().push(user); Ok(uid) } + async fn update_remote_actor_display( + &self, + _user_id: &UserId, + _display_name: Option<&str>, + _avatar_url: Option<&str>, + ) -> Result<(), DomainError> { + Ok(()) + } async fn accept_note( &self, _ap_id: &url::Url,