feat: v2 rewrite — hexagonal arch, ActivityPub federation, NATS, deployment-ready #1

Merged
GKaszewski merged 334 commits from v2 into master 2026-05-16 09:42:43 +00:00
Showing only changes of commit eaf079069f - Show all commits

View File

@@ -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();