From c7cc94f7dd38d6e82016fcc50ea47d8a51afbeb5 Mon Sep 17 00:00:00 2001 From: Mandeep Singh Date: Thu, 6 Aug 2026 19:16:23 -0700 Subject: [PATCH] fix(cli): buzz dms list always returns empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmd_list_dms queried kind:41001 (KIND_DM_CREATED), but the relay's handle_dm_open never emits that kind — it's defined in kind.rs and registered as a known kind, but nothing publishes it. So dms list returned [] for every DM, even active ones with messages. DM channels are regular NIP-29 channels (channel_type = "dm") that do get discovery events: kind:39002 membership and kind:39000 metadata tagged ["t","dm"] with a "p" tag per participant. This mirrors the existing channels list --member path (kind:39002 -> kind:39000) instead of the dead kind:41001 query. Verified against a live relay with an active DM that previously returned [] via dms list. Signed-off-by: Mandeep Singh --- crates/buzz-cli/src/commands/dms.rs | 80 ++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/crates/buzz-cli/src/commands/dms.rs b/crates/buzz-cli/src/commands/dms.rs index 589e411827..07ea927d61 100644 --- a/crates/buzz-cli/src/commands/dms.rs +++ b/crates/buzz-cli/src/commands/dms.rs @@ -1,40 +1,66 @@ use uuid::Uuid; -use crate::client::{extract_d_tag, normalize_write_response, BuzzClient}; +use crate::client::{extract_d_tag, extract_p_tags, normalize_write_response, BuzzClient}; use crate::error::CliError; use crate::validate::{parse_uuid, sdk_err, validate_hex64}; -/// List DM conversations by querying kind:41001 (relay-confirmed DMs) filtered by our pubkey. +/// List DM conversations. +/// +/// DMs are NIP-29 channels with `channel_type = "dm"` — the relay never emits a +/// kind:41001 confirmation event (that kind is reserved but unused), so this +/// follows the same discovery path as `channels list --member`: find channel +/// ids from our kind:39002 membership events, then fetch kind:39000 metadata +/// for those ids and keep only the ones tagged `["t", "dm"]`. pub async fn cmd_list_dms(client: &BuzzClient, limit: Option) -> Result<(), CliError> { let my_pk = client.keys().public_key().to_hex(); let limit = limit.unwrap_or(50).min(200); - let filter = serde_json::json!({ - "kinds": [41001], + + let member_filter = serde_json::json!({ + "kinds": [39002], "#p": [my_pk], - "limit": limit }); - let resp = client.query(&filter).await?; - let events: Vec = serde_json::from_str(&resp).unwrap_or_default(); - let dms: Vec = events + let member_events = client.query_paginated(member_filter, limit).await?; + let channel_ids: Vec = member_events .iter() - .map(|e| { - let dm_id = extract_d_tag(e); - let participants: Vec = e - .get("tags") - .and_then(|t| t.as_array()) - .map(|tags| { - tags.iter() - .filter_map(|tag| { - let arr = tag.as_array()?; - if arr.first()?.as_str()? == "p" { - arr.get(1)?.as_str().map(|s| s.to_string()) - } else { - None - } + .map(extract_d_tag) + .filter(|id| !id.is_empty()) + .collect(); + if channel_ids.is_empty() { + println!("[]"); + return Ok(()); + } + + let metadata_filter = serde_json::json!({ + "kinds": [39000], + "#d": channel_ids, + }); + let metadata_events = client.query_paginated(metadata_filter, limit).await?; + + let is_dm = |e: &serde_json::Value| -> bool { + e.get("tags") + .and_then(|t| t.as_array()) + .map(|tags| { + tags.iter().any(|tag| { + tag.as_array() + .map(|a| { + a.first().and_then(|v| v.as_str()) == Some("t") + && a.get(1).and_then(|v| v.as_str()) == Some("dm") }) - .collect() + .unwrap_or(false) }) - .unwrap_or_default(); + }) + .unwrap_or(false) + }; + + let mut dms: Vec = metadata_events + .iter() + .filter(|e| is_dm(e)) + .map(|e| { + let dm_id = extract_d_tag(e); + let participants: Vec = extract_p_tags(e) + .into_iter() + .filter_map(|p| p.get("pubkey")?.as_str().map(|s| s.to_string())) + .collect(); serde_json::json!({ "dm_id": dm_id, "participants": participants, @@ -42,6 +68,12 @@ pub async fn cmd_list_dms(client: &BuzzClient, limit: Option) -> Result<(), }) }) .collect(); + dms.sort_by(|a, b| { + let a_ts = a.get("created_at").and_then(|v| v.as_u64()).unwrap_or(0); + let b_ts = b.get("created_at").and_then(|v| v.as_u64()).unwrap_or(0); + b_ts.cmp(&a_ts) + }); + let output = serde_json::to_string(&dms).unwrap_or_default(); println!("{output}"); Ok(())