From d3b7ecad15f2bea2e0b9e0f687ebe3cfa71de11b Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 14 May 2026 16:47:17 +0200 Subject: [PATCH] fix(ap): add url field to Note, handle Delete(actor) and Tombstone objects --- .../activitypub-base/src/activities.rs | 33 +++++++++++++++++-- .../adapters/activitypub-base/src/service.rs | 3 +- crates/adapters/activitypub/src/note.rs | 3 ++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/crates/adapters/activitypub-base/src/activities.rs b/crates/adapters/activitypub-base/src/activities.rs index c1c49cd..14d093e 100644 --- a/crates/adapters/activitypub-base/src/activities.rs +++ b/crates/adapters/activitypub-base/src/activities.rs @@ -337,7 +337,7 @@ pub struct DeleteActivity { #[serde(rename = "type", default)] pub(crate) kind: DeleteType, pub(crate) actor: ObjectId, - pub(crate) object: Url, + pub(crate) object: serde_json::Value, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub(crate) to: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] @@ -368,11 +368,38 @@ impl Activity for DeleteActivity { return Ok(()); } let actor_url = self.actor.inner().clone(); + + // Extract object URL — handles plain string and Tombstone {"id":"...","type":"Tombstone"} + let object_url_str = match &self.object { + serde_json::Value::String(s) => s.clone(), + serde_json::Value::Object(o) => o + .get("id") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .unwrap_or_default(), + _ => String::new(), + }; + let Ok(object_url) = Url::parse(&object_url_str) else { + tracing::warn!(actor = %actor_url, "Delete activity has unparseable object, ignoring"); + return Ok(()); + }; + + // Actor self-deletion: Mastodon sends Delete(actor_url) when an account is deleted. + if object_url == *self.actor.inner() { + data.object_handler + .on_actor_removed(&actor_url) + .await + .map_err(|e| Error::from(anyhow::anyhow!(e)))?; + tracing::info!(actor = %actor_url, "received Delete(actor) — remote account deleted"); + return Ok(()); + } + + // Normal note deletion. data.object_handler - .on_delete(&self.object, &actor_url) + .on_delete(&object_url, &actor_url) .await .map_err(|e| Error::from(anyhow::anyhow!(e)))?; - tracing::info!(object = %self.object, "received delete activity"); + tracing::info!(object = %object_url, "received Delete(note)"); Ok(()) } } diff --git a/crates/adapters/activitypub-base/src/service.rs b/crates/adapters/activitypub-base/src/service.rs index 8f5b4ec..abc14ec 100644 --- a/crates/adapters/activitypub-base/src/service.rs +++ b/crates/adapters/activitypub-base/src/service.rs @@ -39,6 +39,7 @@ fn thought_note_json( let mut note = serde_json::json!({ "type": "Note", "id": ap_id.to_string(), + "url": ap_id.to_string(), "attributedTo": local_actor.ap_id.to_string(), "content": thought.content.as_str(), "published": thought.created_at.to_rfc3339(), @@ -653,7 +654,7 @@ impl ActivityPubService { id: delete_id, kind: Default::default(), actor: ObjectId::from(local_actor.ap_id.clone()), - object: ap_id, + object: serde_json::json!(ap_id.to_string()), to: vec![crate::urls::AS_PUBLIC.to_string()], cc: vec![local_actor.followers_url.to_string()], }; diff --git a/crates/adapters/activitypub/src/note.rs b/crates/adapters/activitypub/src/note.rs index d0e2ab9..28f1465 100644 --- a/crates/adapters/activitypub/src/note.rs +++ b/crates/adapters/activitypub/src/note.rs @@ -11,6 +11,7 @@ pub struct ThoughtNote { #[serde(rename = "type")] pub kind: NoteType, pub id: Url, + pub url: Url, // Mastodon uses this as the clickable link pub attributed_to: Url, pub content: String, pub published: DateTime, @@ -39,6 +40,7 @@ impl ThoughtNote { ) -> Self { Self { kind: Default::default(), + url: id.clone(), id, attributed_to: actor_url, content, @@ -71,5 +73,6 @@ mod tests { let json = serde_json::to_string(¬e).unwrap(); assert!(json.contains(AS_PUBLIC)); assert!(json.contains("Hello world")); + assert!(json.contains("\"url\"")); } }