From 99dd89b60d0420f16563a8f38098c6e84a619222 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 15 May 2026 00:25:54 +0200 Subject: [PATCH] feat(domain): ActorConnectionSummary, ConnectionType, RemoteActorConnectionRepository, FetchActorConnections event --- .../adapters/activitypub-base/src/service.rs | 14 ++++ crates/adapters/event-payload/src/lib.rs | 35 ++++++++++ crates/domain/src/events.rs | 6 ++ .../src/models/actor_connection_summary.rs | 7 ++ crates/domain/src/models/connection_type.rs | 14 ++++ crates/domain/src/models/mod.rs | 2 + crates/domain/src/ports.rs | 35 ++++++++++ crates/domain/src/testing.rs | 65 +++++++++++++++++++ 8 files changed, 178 insertions(+) create mode 100644 crates/domain/src/models/actor_connection_summary.rs create mode 100644 crates/domain/src/models/connection_type.rs diff --git a/crates/adapters/activitypub-base/src/service.rs b/crates/adapters/activitypub-base/src/service.rs index 7c1ec3a..d31928f 100644 --- a/crates/adapters/activitypub-base/src/service.rs +++ b/crates/adapters/activitypub-base/src/service.rs @@ -1599,6 +1599,20 @@ impl domain::ports::FederationActionPort for ActivityPubService { Ok(notes) } + + async fn fetch_actor_urls_from_collection( + &self, + _collection_url: &str, + ) -> Result, domain::errors::DomainError> { + Ok(vec![]) + } + + async fn resolve_actor_profiles( + &self, + _urls: Vec, + ) -> Vec { + vec![] + } } #[cfg(test)] diff --git a/crates/adapters/event-payload/src/lib.rs b/crates/adapters/event-payload/src/lib.rs index ba635f7..ebf758d 100644 --- a/crates/adapters/event-payload/src/lib.rs +++ b/crates/adapters/event-payload/src/lib.rs @@ -72,6 +72,12 @@ pub enum EventPayload { actor_ap_url: String, outbox_url: String, }, + FetchActorConnections { + actor_ap_url: String, + collection_url: String, + connection_type: String, + page: u32, + }, } impl EventPayload { @@ -93,6 +99,7 @@ impl EventPayload { Self::UserUnblocked { .. } => "users.unblocked", Self::UserRegistered { .. } => "users.registered", Self::FetchRemoteActorPosts { .. } => "federation.fetch_outbox", + Self::FetchActorConnections { .. } => "federation.fetch_connections", } } } @@ -209,6 +216,17 @@ impl From<&DomainEvent> for EventPayload { actor_ap_url: actor_ap_url.clone(), outbox_url: outbox_url.clone(), }, + DomainEvent::FetchActorConnections { + actor_ap_url, + collection_url, + connection_type, + page, + } => Self::FetchActorConnections { + actor_ap_url: actor_ap_url.clone(), + collection_url: collection_url.clone(), + connection_type: connection_type.clone(), + page: *page, + }, } } } @@ -334,6 +352,17 @@ impl TryFrom for DomainEvent { actor_ap_url, outbox_url, }, + EventPayload::FetchActorConnections { + actor_ap_url, + collection_url, + connection_type, + page, + } => DomainEvent::FetchActorConnections { + actor_ap_url, + collection_url, + connection_type, + page, + }, }) } } @@ -419,6 +448,12 @@ mod tests { actor_ap_url: "https://mastodon.social/users/alice".into(), outbox_url: "https://mastodon.social/users/alice/outbox".into(), }, + EventPayload::FetchActorConnections { + actor_ap_url: "https://mastodon.social/users/alice".into(), + collection_url: "https://mastodon.social/users/alice/followers".into(), + connection_type: "followers".into(), + page: 1, + }, ]; let mut subjects: Vec<&str> = samples.iter().map(|p| p.subject()).collect(); subjects.sort(); diff --git a/crates/domain/src/events.rs b/crates/domain/src/events.rs index e7ef3eb..2e6b879 100644 --- a/crates/domain/src/events.rs +++ b/crates/domain/src/events.rs @@ -64,6 +64,12 @@ pub enum DomainEvent { actor_ap_url: String, outbox_url: String, }, + FetchActorConnections { + actor_ap_url: String, + collection_url: String, + connection_type: String, + page: u32, + }, } pub struct EventEnvelope { diff --git a/crates/domain/src/models/actor_connection_summary.rs b/crates/domain/src/models/actor_connection_summary.rs new file mode 100644 index 0000000..9aec42d --- /dev/null +++ b/crates/domain/src/models/actor_connection_summary.rs @@ -0,0 +1,7 @@ +#[derive(Debug, Clone)] +pub struct ActorConnectionSummary { + pub url: String, + pub handle: String, + pub display_name: Option, + pub avatar_url: Option, +} diff --git a/crates/domain/src/models/connection_type.rs b/crates/domain/src/models/connection_type.rs new file mode 100644 index 0000000..78f2e7e --- /dev/null +++ b/crates/domain/src/models/connection_type.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConnectionType { + Followers, + Following, +} + +impl ConnectionType { + pub fn as_str(&self) -> &'static str { + match self { + Self::Followers => "followers", + Self::Following => "following", + } + } +} diff --git a/crates/domain/src/models/mod.rs b/crates/domain/src/models/mod.rs index 3588235..9b08768 100644 --- a/crates/domain/src/models/mod.rs +++ b/crates/domain/src/models/mod.rs @@ -1,4 +1,6 @@ +pub mod actor_connection_summary; pub mod api_key; +pub mod connection_type; pub mod feed; pub mod notification; pub mod remote_actor; diff --git a/crates/domain/src/ports.rs b/crates/domain/src/ports.rs index 15f3831..8c4e11d 100644 --- a/crates/domain/src/ports.rs +++ b/crates/domain/src/ports.rs @@ -194,6 +194,31 @@ pub trait RemoteActorRepository: Send + Sync { async fn find_by_url(&self, url: &str) -> Result, DomainError>; } +#[async_trait] +pub trait RemoteActorConnectionRepository: Send + Sync { + async fn upsert_connections( + &self, + actor_url: &str, + connection_type: &str, + page: u32, + actors: &[crate::models::actor_connection_summary::ActorConnectionSummary], + ) -> Result<(), DomainError>; + + async fn list_connections( + &self, + actor_url: &str, + connection_type: &str, + page: u32, + ) -> Result, DomainError>; + + async fn connection_page_age( + &self, + actor_url: &str, + connection_type: &str, + page: u32, + ) -> Result>, DomainError>; +} + #[async_trait] pub trait FederationActionPort: Send + Sync { async fn lookup_actor(&self, handle: &str) -> Result; @@ -214,6 +239,16 @@ pub trait FederationActionPort: Send + Sync { outbox_url: &str, page: u32, ) -> Result, DomainError>; + + async fn fetch_actor_urls_from_collection( + &self, + collection_url: &str, + ) -> Result, DomainError>; + + async fn resolve_actor_profiles( + &self, + urls: Vec, + ) -> Vec; } #[async_trait] diff --git a/crates/domain/src/testing.rs b/crates/domain/src/testing.rs index 4fb97c8..8184c8c 100644 --- a/crates/domain/src/testing.rs +++ b/crates/domain/src/testing.rs @@ -575,6 +575,52 @@ impl FederationActionPort for TestStore { ) -> Result, DomainError> { Ok(vec![]) } + + async fn fetch_actor_urls_from_collection( + &self, + _collection_url: &str, + ) -> Result, DomainError> { + Ok(vec![]) + } + + async fn resolve_actor_profiles( + &self, + _urls: Vec, + ) -> Vec { + vec![] + } +} + +#[async_trait] +impl RemoteActorConnectionRepository for TestStore { + async fn upsert_connections( + &self, + _actor_url: &str, + _connection_type: &str, + _page: u32, + _actors: &[crate::models::actor_connection_summary::ActorConnectionSummary], + ) -> Result<(), DomainError> { + Ok(()) + } + + async fn list_connections( + &self, + _actor_url: &str, + _connection_type: &str, + _page: u32, + ) -> Result, DomainError> + { + Ok(vec![]) + } + + async fn connection_page_age( + &self, + _actor_url: &str, + _connection_type: &str, + _page: u32, + ) -> Result>, DomainError> { + Ok(None) + } } #[async_trait] @@ -851,6 +897,25 @@ mod federation_port_tests { .unwrap(); assert!(notes.is_empty()); } + + #[tokio::test] + async fn test_store_resolve_actor_profiles_returns_empty() { + let store = TestStore::default(); + let result = store + .resolve_actor_profiles(vec!["https://example.com/users/alice".into()]) + .await; + assert!(result.is_empty()); + } + + #[tokio::test] + async fn test_store_fetch_collection_urls_returns_empty() { + let store = TestStore::default(); + let urls = store + .fetch_actor_urls_from_collection("https://example.com/users/alice/followers") + .await + .unwrap(); + assert!(urls.is_empty()); + } } #[cfg(test)]