fix(activitypub-base): deterministic announce IDs so Undo(Announce) can reference original activity

This commit is contained in:
2026-05-14 14:16:48 +02:00
parent b0b3c6a59b
commit 2485869af6
2 changed files with 39 additions and 1 deletions

View File

@@ -175,8 +175,19 @@ impl ActivityPubService {
return Ok(()); 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 { 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(), kind: Default::default(),
actor: activitypub_federation::fetch::object_id::ObjectId::from(local_actor.ap_id.clone()), actor: activitypub_federation::fetch::object_id::ObjectId::from(local_actor.ap_id.clone()),
object: object_ap_id, object: object_ap_id,
@@ -235,6 +246,17 @@ impl ActivityPubService {
return Ok(()); 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_id = crate::urls::activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?;
let undo = crate::activities::UndoActivity { let undo = crate::activities::UndoActivity {
id: undo_id, id: undo_id,
@@ -242,6 +264,7 @@ impl ActivityPubService {
actor: activitypub_federation::fetch::object_id::ObjectId::from(local_actor.ap_id.clone()), actor: activitypub_federation::fetch::object_id::ObjectId::from(local_actor.ap_id.clone()),
object: serde_json::json!({ object: serde_json::json!({
"type": "Announce", "type": "Announce",
"id": announce_id.to_string(),
"actor": local_actor.ap_id.to_string(), "actor": local_actor.ap_id.to_string(),
"object": object_ap_id.to_string(), "object": object_ap_id.to_string(),
}), }),

View File

@@ -414,6 +414,21 @@ mod tests {
assert_eq!(undo_announced[0], "https://mastodon.social/users/bob/statuses/456"); 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] #[tokio::test]
async fn thought_updated_does_not_broadcast_if_user_missing() { async fn thought_updated_does_not_broadcast_if_user_missing() {
let store = TestStore::default(); let store = TestStore::default();