From 9180c2ed01134329693b5e95c4ff2d4bce3a0081 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Fri, 21 Aug 2026 11:06:44 -0400 Subject: [PATCH] fix(cli): honor relay-attributed self-deletes Signed-off-by: John Tennant --- crates/buzz-cli/src/commands/messages.rs | 99 +++++++++++++++++++++--- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/crates/buzz-cli/src/commands/messages.rs b/crates/buzz-cli/src/commands/messages.rs index 7a0f5b6ed9..f5d67b50e0 100644 --- a/crates/buzz-cli/src/commands/messages.rs +++ b/crates/buzz-cli/src/commands/messages.rs @@ -121,6 +121,55 @@ async fn resolve_channel_id(client: &BuzzClient, event_id: &str) -> Result Option { + event + .get("pubkey") + .and_then(serde_json::Value::as_str) + .and_then(|pubkey| PublicKey::from_hex(pubkey).ok()) + .map(|pubkey| pubkey.to_hex()) +} + +fn effective_target_author(event: &serde_json::Value, relay_self: &str) -> Option { + let signer = normalized_event_pubkey(event)?; + if signer != relay_self { + return Some(signer); + } + + for tag_name in ["actor", "p"] { + let attributed = event + .get("tags") + .and_then(serde_json::Value::as_array) + .into_iter() + .flatten() + .filter_map(serde_json::Value::as_array) + .find(|tag| tag.first().and_then(serde_json::Value::as_str) == Some(tag_name)) + .and_then(|tag| tag.get(1)) + .and_then(serde_json::Value::as_str) + .and_then(|pubkey| PublicKey::from_hex(pubkey).ok()) + .map(|pubkey| pubkey.to_hex()); + if attributed.is_some() { + return attributed; + } + } + + Some(signer) +} + +async fn relay_self_pubkey(client: &BuzzClient) -> Result { + let raw = client + .get_public("/") + .await + .map_err(|error| CliError::Other(format!("failed to fetch relay info: {error}")))?; + let document: serde_json::Value = serde_json::from_str(&raw) + .map_err(|error| CliError::Other(format!("invalid relay info document: {error}")))?; + document + .get("self") + .and_then(serde_json::Value::as_str) + .and_then(|pubkey| PublicKey::from_hex(pubkey).ok()) + .map(|pubkey| pubkey.to_hex()) + .ok_or_else(|| CliError::Other("relay info document missing valid 'self' pubkey".into())) +} + fn resolve_names_to_pubkeys( names: &[String], name_to_pubkeys: &std::collections::HashMap>, @@ -831,10 +880,21 @@ pub async fn cmd_delete_message( let target_event = fetch_event(client, event_id).await?; let channel_uuid = channel_id_from_event(event_id, &target_event)?; let target_eid = parse_event_id(event_id)?; - let is_self_authored = target_event - .get("pubkey") - .and_then(serde_json::Value::as_str) - .is_some_and(|pubkey| pubkey.eq_ignore_ascii_case(&client.keys().public_key().to_hex())); + let signer = client.keys().public_key().to_hex(); + let target_signer = normalized_event_pubkey(&target_event) + .ok_or_else(|| CliError::Other("target event missing valid 'pubkey'".into()))?; + let target_author = if target_signer == signer { + target_signer + } else { + // If relay identity is unavailable, retain the existing audited + // moderation path instead of making deletes depend on NIP-11 uptime. + relay_self_pubkey(client) + .await + .ok() + .and_then(|relay_self| effective_target_author(&target_event, &relay_self)) + .unwrap_or(target_signer) + }; + let is_self_authored = target_author == signer; let builder = build_cli_delete_message( channel_uuid, @@ -1083,11 +1143,11 @@ pub async fn dispatch( #[cfg(test)] mod tests { use super::{ - build_cli_delete_message, channel_id_from_event, cmd_get_thread, event_mention_pubkeys, - find_root_from_tags, match_profiles_by_name, merge_message_mentions, missing_members, - normalize_explicit_mentions, parse_member_pubkeys, resolve_names_to_pubkeys, - resolve_thread_target, thread_ref_from_event, thread_ref_from_parent_tags, BuzzClient, - CliError, Uuid, + build_cli_delete_message, channel_id_from_event, cmd_get_thread, effective_target_author, + event_mention_pubkeys, find_root_from_tags, match_profiles_by_name, merge_message_mentions, + missing_members, normalize_explicit_mentions, parse_member_pubkeys, + resolve_names_to_pubkeys, resolve_thread_target, thread_ref_from_event, + thread_ref_from_parent_tags, BuzzClient, CliError, Uuid, }; use buzz_sdk::mentions::{ extract_at_mentions_with_known, extract_at_names, match_names_to_profiles, MentionProfile, @@ -1157,6 +1217,27 @@ mod tests { assert_eq!(event.kind.as_u16(), 9005); } + #[test] + fn relay_signed_target_uses_trusted_effective_author() { + let event = json!({ + "pubkey": PK_VALID_A, + "tags": [ + ["actor", PK_VALID_B], + ["p", PK_VALID_C], + ], + }); + + assert_eq!( + effective_target_author(&event, PK_VALID_A).as_deref(), + Some(PK_VALID_B), + ); + assert_eq!( + effective_target_author(&event, PK_VALID_C).as_deref(), + Some(PK_VALID_A), + "attribution tags on a non-relay signer must be ignored", + ); + } + #[tokio::test] async fn malformed_channel_is_rejected_before_thread_fetch() { let client =