From 2485869af6bf08bc6f139982e2287f70b1225411 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 14 May 2026 14:16:48 +0200 Subject: [PATCH] fix(activitypub-base): deterministic announce IDs so Undo(Announce) can reference original activity --- .../adapters/activitypub-base/src/service.rs | 25 ++++++++++++++++++- .../src/services/federation_event.rs | 15 +++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/adapters/activitypub-base/src/service.rs b/crates/adapters/activitypub-base/src/service.rs index 8089a78..d0b24a3 100644 --- a/crates/adapters/activitypub-base/src/service.rs +++ b/crates/adapters/activitypub-base/src/service.rs @@ -175,8 +175,19 @@ impl ActivityPubService { return Ok(()); } + // Deterministic ID so Undo(Announce) can reference this same activity. + let announce_id = url::Url::parse(&format!( + "{}/activities/announce/{}", + self.base_url, + uuid::Uuid::new_v5( + &uuid::Uuid::NAMESPACE_URL, + format!("{}/{}", local_user_id, object_ap_id).as_bytes(), + ) + )) + .map_err(|e| anyhow::anyhow!("{e}"))?; + let announce = crate::activities::AnnounceActivity { - id: crate::urls::activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?, + id: announce_id, kind: Default::default(), actor: activitypub_federation::fetch::object_id::ObjectId::from(local_actor.ap_id.clone()), object: object_ap_id, @@ -235,6 +246,17 @@ impl ActivityPubService { return Ok(()); } + // Reconstruct the same deterministic announce ID used when the boost was sent. + let announce_id = url::Url::parse(&format!( + "{}/activities/announce/{}", + self.base_url, + uuid::Uuid::new_v5( + &uuid::Uuid::NAMESPACE_URL, + format!("{}/{}", local_user_id, object_ap_id).as_bytes(), + ) + )) + .map_err(|e| anyhow::anyhow!("{e}"))?; + let undo_id = crate::urls::activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?; let undo = crate::activities::UndoActivity { id: undo_id, @@ -242,6 +264,7 @@ impl ActivityPubService { actor: activitypub_federation::fetch::object_id::ObjectId::from(local_actor.ap_id.clone()), object: serde_json::json!({ "type": "Announce", + "id": announce_id.to_string(), "actor": local_actor.ap_id.to_string(), "object": object_ap_id.to_string(), }), diff --git a/crates/application/src/services/federation_event.rs b/crates/application/src/services/federation_event.rs index 181a947..31fe29b 100644 --- a/crates/application/src/services/federation_event.rs +++ b/crates/application/src/services/federation_event.rs @@ -414,6 +414,21 @@ mod tests { assert_eq!(undo_announced[0], "https://mastodon.social/users/bob/statuses/456"); } + #[tokio::test] + async fn boost_removed_does_not_broadcast_if_thought_missing() { + let store = TestStore::default(); + let alice = alice(); + let spy = Arc::new(SpyPort::default()); + svc(&store, spy.clone()) + .process(&DomainEvent::BoostRemoved { + user_id: alice.id.clone(), + thought_id: ThoughtId::new(), // doesn't exist in store + }) + .await + .unwrap(); + assert!(spy.undo_announced.lock().unwrap().is_empty()); + } + #[tokio::test] async fn thought_updated_does_not_broadcast_if_user_missing() { let store = TestStore::default();