diff --git a/crates/application/src/services/federation_event.rs b/crates/application/src/services/federation_event.rs index 7c748e6..540d975 100644 --- a/crates/application/src/services/federation_event.rs +++ b/crates/application/src/services/federation_event.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use domain::{ errors::DomainError, events::DomainEvent, + models::thought::Visibility, ports::{OutboundFederationPort, ThoughtRepository, UserRepository}, }; @@ -17,7 +18,7 @@ impl FederationEventService { match event { DomainEvent::ThoughtCreated { thought_id, user_id, .. } => { let thought = match self.thoughts.find_by_id(thought_id).await? { - Some(t) if t.local => t, + Some(t) if t.local && matches!(t.visibility, Visibility::Public | Visibility::Unlisted) => t, _ => return Ok(()), }; let user = match self.users.find_by_id(user_id).await? { @@ -36,7 +37,7 @@ impl FederationEventService { DomainEvent::ThoughtUpdated { thought_id, user_id } => { let thought = match self.thoughts.find_by_id(thought_id).await? { - Some(t) if t.local => t, + Some(t) if t.local && matches!(t.visibility, Visibility::Public | Visibility::Unlisted) => t, _ => return Ok(()), }; let user = match self.users.find_by_id(user_id).await? { @@ -265,6 +266,56 @@ mod tests { assert_eq!(announced[0], "https://mastodon.social/users/bob/statuses/123"); } + #[tokio::test] + async fn direct_thought_created_does_not_broadcast() { + let store = TestStore::default(); + let alice = alice(); + let thought = Thought::new_local( + ThoughtId::new(), alice.id.clone(), + Content::new_local("private").unwrap(), + None, Visibility::Direct, None, false, + ); + store.users.lock().unwrap().push(alice.clone()); + store.thoughts.lock().unwrap().push(thought.clone()); + + let spy = Arc::new(SpyPort::default()); + svc(&store, spy.clone()) + .process(&DomainEvent::ThoughtCreated { + thought_id: thought.id.clone(), + user_id: alice.id.clone(), + in_reply_to_id: None, + }) + .await + .unwrap(); + + assert!(spy.created.lock().unwrap().is_empty()); + } + + #[tokio::test] + async fn followers_only_thought_does_not_broadcast_publicly() { + let store = TestStore::default(); + let alice = alice(); + let thought = Thought::new_local( + ThoughtId::new(), alice.id.clone(), + Content::new_local("for followers").unwrap(), + None, Visibility::Followers, None, false, + ); + store.users.lock().unwrap().push(alice.clone()); + store.thoughts.lock().unwrap().push(thought.clone()); + + let spy = Arc::new(SpyPort::default()); + svc(&store, spy.clone()) + .process(&DomainEvent::ThoughtCreated { + thought_id: thought.id.clone(), + user_id: alice.id.clone(), + in_reply_to_id: None, + }) + .await + .unwrap(); + + assert!(spy.created.lock().unwrap().is_empty()); + } + #[tokio::test] async fn unrelated_events_are_noop() { let store = TestStore::default();