diff --git a/crates/adapters/postgres/src/activitypub.rs b/crates/adapters/postgres/src/activitypub.rs index 44d7330..b51a184 100644 --- a/crates/adapters/postgres/src/activitypub.rs +++ b/crates/adapters/postgres/src/activitypub.rs @@ -10,7 +10,7 @@ use url::Url; use domain::{ errors::DomainError, models::thought::{Thought, Visibility}, - ports::{ActivityPubRepository, OutboxEntry}, + ports::{ActivityPubRepository, ActorApUrls, OutboxEntry}, value_objects::{Content, ThoughtId, UserId, Username}, }; @@ -297,6 +297,34 @@ impl ActivityPubRepository for PgActivityPubRepository { .into_domain()?; Ok(n as u64) } + + async fn get_thought_ap_id( + &self, + thought_id: &ThoughtId, + ) -> Result, DomainError> { + sqlx::query_scalar::<_, String>( + "SELECT ap_id FROM thoughts WHERE id = $1 AND ap_id IS NOT NULL", + ) + .bind(thought_id.as_uuid()) + .fetch_optional(&self.pool) + .await + .into_domain() + } + + async fn get_actor_ap_urls( + &self, + user_id: &UserId, + ) -> Result, DomainError> { + sqlx::query_as::<_, (String, String)>( + "SELECT ap_id, inbox_url FROM users \ + WHERE id = $1 AND ap_id IS NOT NULL AND inbox_url IS NOT NULL", + ) + .bind(user_id.as_uuid()) + .fetch_optional(&self.pool) + .await + .into_domain() + .map(|opt| opt.map(|(ap_id, inbox_url)| ActorApUrls { ap_id, inbox_url })) + } } #[cfg(test)] diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index 7063618..93147b7 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -331,6 +331,13 @@ pub trait SearchPort: Send + Sync { ) -> Result, DomainError>; } +/// AP-protocol endpoints for a locally-stored user (local or interned remote). +#[derive(Debug, Clone)] +pub struct ActorApUrls { + pub ap_id: String, + pub inbox_url: String, +} + /// A local thought ready for AP serialization, with the author's username /// pre-joined so the handler can build AP URLs without a second query. #[derive(Debug, Clone)] @@ -413,6 +420,18 @@ pub trait ActivityPubRepository: Send + Sync { /// Total locally-authored thought count for NodeInfo responses. async fn count_local_notes(&self) -> Result; + + /// Return the ActivityPub object URL for a thought, if one is stored. + /// Returns None for local thoughts (caller constructs URL from base_url + thought_id). + async fn get_thought_ap_id( + &self, + thought_id: &ThoughtId, + ) -> Result, DomainError>; + + /// Return the AP actor URL and inbox URL for a user, if stored. + /// Returns None for users that have not been federated. + async fn get_actor_ap_urls(&self, user_id: &UserId) + -> Result, DomainError>; } #[async_trait] diff --git a/crates/domain/src/testing.rs b/crates/domain/src/testing.rs index 832624d..8fbce63 100644 --- a/crates/domain/src/testing.rs +++ b/crates/domain/src/testing.rs @@ -882,6 +882,18 @@ impl ActivityPubRepository for TestStore { .filter(|t| t.local) .count() as u64) } + async fn get_thought_ap_id( + &self, + _thought_id: &ThoughtId, + ) -> Result, DomainError> { + Ok(None) + } + async fn get_actor_ap_urls( + &self, + _user_id: &UserId, + ) -> Result, DomainError> { + Ok(None) + } } #[async_trait]