Skip to main content

v2_typing_probe/
v2_typing_probe.rs

1//! Per-relay ephemeral-forwarding probe: subscribe to EACH community relay
2//! individually for kind-21059 wraps on a channel's chat plane, trigger the
3//! resident test bot's `!typing`, and record which relay forwards whose typing.
4//! Isolates "relay accepted but silently dropped" (OK-true lie) from client bugs.
5//!
6//! Run from the SAME data dir as a bot that already joined the community:
7//! ```sh
8//! cargo run -p vector_sdk --example v2_typing_probe -- "<channel name>"
9//! ```
10
11use nostr_sdk::prelude::*;
12use vector_sdk::VectorBot;
13
14#[tokio::main]
15async fn main() -> vector_sdk::Result<()> {
16    let channel_name = std::env::args().nth(1).unwrap_or_else(|| "spam and testing".into());
17
18    let data_dir = std::env::temp_dir().join("v2_send_once_data");
19    let bot = VectorBot::builder().data_dir(&data_dir).build().await?;
20    println!("── probe online as {}", bot.npub());
21
22    // Find the joined v2 community + the target channel id.
23    let (mut community_hex, mut channel_hex) = (None::<String>, None::<String>);
24    for c in bot.core().list_communities().await {
25        if c.get("version").and_then(|v| v.as_u64()) != Some(2) {
26            continue;
27        }
28        let cid = c.get("community_id").and_then(|v| v.as_str()).map(String::from);
29        for ch in c.get("channels").and_then(|v| v.as_array()).cloned().unwrap_or_default() {
30            if ch.get("name").and_then(|n| n.as_str()) == Some(channel_name.as_str()) {
31                community_hex = cid.clone();
32                channel_hex = ch.get("channel_id").and_then(|i| i.as_str()).map(String::from);
33            }
34        }
35    }
36    let (community_hex, channel_hex) = match (community_hex, channel_hex) {
37        (Some(c), Some(ch)) => (c, ch),
38        _ => {
39            eprintln!("!! channel \"{channel_name}\" not held locally — join first (run v2_send_once once)");
40            std::process::exit(1);
41        }
42    };
43
44    // Load the community row for relays + root, and derive the channel plane pk
45    // exactly as the send/subscribe paths do.
46    let cid = vector_core::community::CommunityId(vector_core::simd::hex::hex_to_bytes_32(&community_hex));
47    let community = vector_core::db::community::load_community_v2(&cid)
48        .map_err(vector_core::VectorError::Other)?
49        .ok_or(vector_core::VectorError::Other("community row missing".into()))?;
50    let chid = vector_core::community::ChannelId(vector_core::simd::hex::hex_to_bytes_32(&channel_hex));
51    let ch = community.channel(&chid).ok_or(vector_core::VectorError::Other("channel missing".into()))?;
52    let (secret, epoch) = community.channel_secret(ch);
53    let plane_pk = vector_core::community::v2::derive::channel_group_key(&secret, &chid, epoch).pk();
54    println!("── channel \"{channel_name}\" plane pk = {plane_pk}");
55    println!("── community relays: {:?}", community.relays);
56
57    // One RAW single-relay client per community relay, each with its own live
58    // typing subscription — per-relay forwarding becomes directly observable.
59    // Every arriving wrap is opened with the plane key and its rumor dumped
60    // verbatim: exactly what Armada's `openWrap` + freshness check would see.
61    let group = vector_core::community::v2::derive::channel_group_key(&secret, &chid, epoch);
62    for relay in community.relays.clone() {
63        let plane_pk = plane_pk;
64        let group = group.clone();
65        tokio::spawn(async move {
66            let client = Client::default();
67            if client.add_relay(&relay).await.is_err() {
68                println!("[{relay}] add failed");
69                return;
70            }
71            client.connect().await;
72            let filter = Filter::new().kind(Kind::Custom(21059)).author(plane_pk);
73            if let Err(e) = client.subscribe(filter, None).await {
74                println!("[{relay}] subscribe failed: {e}");
75                return;
76            }
77            println!("[{relay}] subscribed");
78            let mut notifications = client.notifications();
79            while let Ok(n) = notifications.recv().await {
80                if let RelayPoolNotification::Event { event, .. } = n {
81                    println!(
82                        "[{relay}] ← 21059 wrap id={} wrap_created_at={} size={}B",
83                        &event.id.to_hex()[..12],
84                        event.created_at,
85                        event.content.len()
86                    );
87                    match vector_core::community::v2::stream::open_wrap(&event, &group) {
88                        Ok(opened) => {
89                            let now = std::time::SystemTime::now()
90                                .duration_since(std::time::UNIX_EPOCH)
91                                .map(|d| d.as_millis() as u64)
92                                .unwrap_or(0);
93                            let age = now as i128 - opened.at_ms as i128;
94                            println!(
95                                "    rumor: kind={} author={} created_at={} content={:?}",
96                                opened.rumor.kind,
97                                &opened.author.to_hex()[..12],
98                                opened.rumor.created_at,
99                                opened.rumor.content
100                            );
101                            println!("    tags:  {:?}", opened.rumor.tags.iter().map(|t| t.as_slice().to_vec()).collect::<Vec<_>>());
102                            println!("    seal:  kind={} created_at={}", opened.seal.kind, opened.seal.created_at);
103                            println!("    at_ms={} (age {age} ms → Armada freshness gate {} 8000ms)", opened.at_ms, if age <= 8000 { "PASSES ≤" } else { "FAILS >" });
104                        }
105                        Err(e) => println!("    !! open failed: {e}"),
106                    }
107                }
108            }
109        });
110    }
111    tokio::time::sleep(std::time::Duration::from_secs(4)).await;
112
113    // Trigger the resident bot: a normal chat message it answers with ch.typing().
114    println!("── sending !typing into \"{channel_name}\"…");
115    match bot.channel(&channel_hex).send("!typing").await {
116        Ok(_) => println!("── trigger sent; watching 25s for the bot's typing wrap…"),
117        Err(e) => eprintln!("!! trigger send failed: {e}"),
118    }
119    tokio::time::sleep(std::time::Duration::from_secs(25)).await;
120    println!("── probe done");
121    Ok(())
122}