feat(application): federate local likes + locality guard prevents remote boost re-broadcast
This commit is contained in:
@@ -93,6 +93,12 @@ impl FederationEventService {
|
||||
user_id,
|
||||
thought_id,
|
||||
} => {
|
||||
// Only fan-out if the booster is a local user. Remote boosts must not be re-broadcast.
|
||||
let booster = match self.users.find_by_id(user_id).await? {
|
||||
Some(u) if u.local => u,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let _ = booster;
|
||||
let thought = match self.thoughts.find_by_id(thought_id).await? {
|
||||
Some(t) => t,
|
||||
None => return Ok(()),
|
||||
@@ -220,6 +226,56 @@ impl FederationEventService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
DomainEvent::LikeAdded {
|
||||
like_id: _,
|
||||
user_id,
|
||||
thought_id,
|
||||
} => {
|
||||
// Only federate: local liker + remote thought (has ap_id) + author has inbox.
|
||||
let liker = match self.users.find_by_id(user_id).await? {
|
||||
Some(u) if u.local => u,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let _ = liker;
|
||||
let thought = match self.thoughts.find_by_id(thought_id).await? {
|
||||
Some(t) if t.ap_id.is_some() => t,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let author = match self.users.find_by_id(&thought.user_id).await? {
|
||||
Some(u) if u.inbox_url.is_some() => u,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let object_ap_id = thought.ap_id.unwrap();
|
||||
let inbox_url = author.inbox_url.unwrap();
|
||||
self.ap
|
||||
.broadcast_like(user_id, &object_ap_id, &inbox_url)
|
||||
.await
|
||||
}
|
||||
|
||||
DomainEvent::LikeRemoved {
|
||||
user_id,
|
||||
thought_id,
|
||||
} => {
|
||||
let liker = match self.users.find_by_id(user_id).await? {
|
||||
Some(u) if u.local => u,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let _ = liker;
|
||||
let thought = match self.thoughts.find_by_id(thought_id).await? {
|
||||
Some(t) if t.ap_id.is_some() => t,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let author = match self.users.find_by_id(&thought.user_id).await? {
|
||||
Some(u) if u.inbox_url.is_some() => u,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let object_ap_id = thought.ap_id.unwrap();
|
||||
let inbox_url = author.inbox_url.unwrap();
|
||||
self.ap
|
||||
.broadcast_undo_like(user_id, &object_ap_id, &inbox_url)
|
||||
.await
|
||||
}
|
||||
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -436,6 +492,7 @@ mod tests {
|
||||
let store = TestStore::default();
|
||||
let alice = alice();
|
||||
let thought = local_thought(alice.id.clone()); // ap_id = None
|
||||
store.users.lock().unwrap().push(alice.clone());
|
||||
store.thoughts.lock().unwrap().push(thought.clone());
|
||||
|
||||
let spy = Arc::new(SpyPort::default());
|
||||
@@ -463,6 +520,7 @@ mod tests {
|
||||
let mut thought = local_thought(alice.id.clone());
|
||||
thought.local = false;
|
||||
thought.ap_id = Some("https://mastodon.social/users/bob/statuses/123".into());
|
||||
store.users.lock().unwrap().push(alice.clone());
|
||||
store.thoughts.lock().unwrap().push(thought.clone());
|
||||
|
||||
let spy = Arc::new(SpyPort::default());
|
||||
@@ -693,4 +751,103 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn like_added_local_user_remote_thought_broadcasts_like() {
|
||||
let store = TestStore::default();
|
||||
|
||||
let mut author = User::new_local(
|
||||
UserId::new(),
|
||||
Username::new("remote_author").unwrap(),
|
||||
Email::new("r@remote.example").unwrap(),
|
||||
PasswordHash("h".into()),
|
||||
);
|
||||
author.local = false;
|
||||
author.inbox_url = Some("https://mastodon.social/users/author/inbox".into());
|
||||
|
||||
let mut thought = local_thought(author.id.clone());
|
||||
thought.ap_id = Some("https://mastodon.social/posts/123".into());
|
||||
|
||||
let liker = alice();
|
||||
|
||||
store.users.lock().unwrap().push(author.clone());
|
||||
store.users.lock().unwrap().push(liker.clone());
|
||||
store.thoughts.lock().unwrap().push(thought.clone());
|
||||
|
||||
let spy = Arc::new(SpyPort::default());
|
||||
svc(&store, spy.clone())
|
||||
.process(&DomainEvent::LikeAdded {
|
||||
like_id: LikeId::new(),
|
||||
user_id: liker.id,
|
||||
thought_id: thought.id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(spy.liked.lock().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn like_added_remote_user_skips_broadcast() {
|
||||
let store = TestStore::default();
|
||||
|
||||
let author = alice();
|
||||
let thought = local_thought(author.id.clone()); // local thought — no ap_id
|
||||
|
||||
let mut remote_liker = User::new_local(
|
||||
UserId::new(),
|
||||
Username::new("bob").unwrap(),
|
||||
Email::new("bob@remote").unwrap(),
|
||||
PasswordHash("h".into()),
|
||||
);
|
||||
remote_liker.local = false;
|
||||
|
||||
store.users.lock().unwrap().push(author);
|
||||
store.users.lock().unwrap().push(remote_liker.clone());
|
||||
store.thoughts.lock().unwrap().push(thought.clone());
|
||||
|
||||
let spy = Arc::new(SpyPort::default());
|
||||
svc(&store, spy.clone())
|
||||
.process(&DomainEvent::LikeAdded {
|
||||
like_id: LikeId::new(),
|
||||
user_id: remote_liker.id,
|
||||
thought_id: thought.id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(spy.liked.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn boost_added_remote_user_skips_broadcast() {
|
||||
let store = TestStore::default();
|
||||
|
||||
let author = alice();
|
||||
let thought = local_thought(author.id.clone());
|
||||
|
||||
let mut remote_booster = User::new_local(
|
||||
UserId::new(),
|
||||
Username::new("bob").unwrap(),
|
||||
Email::new("bob@remote").unwrap(),
|
||||
PasswordHash("h".into()),
|
||||
);
|
||||
remote_booster.local = false;
|
||||
|
||||
store.users.lock().unwrap().push(author);
|
||||
store.users.lock().unwrap().push(remote_booster.clone());
|
||||
store.thoughts.lock().unwrap().push(thought.clone());
|
||||
|
||||
let spy = Arc::new(SpyPort::default());
|
||||
svc(&store, spy.clone())
|
||||
.process(&DomainEvent::BoostAdded {
|
||||
boost_id: BoostId::new(),
|
||||
user_id: remote_booster.id,
|
||||
thought_id: thought.id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(spy.announced.lock().unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user